diff options
Diffstat (limited to 'go/runfiles/runfiles.go')
-rw-r--r-- | go/runfiles/runfiles.go | 305 |
1 files changed, 305 insertions, 0 deletions
diff --git a/go/runfiles/runfiles.go b/go/runfiles/runfiles.go new file mode 100644 index 00000000..bc57b17b --- /dev/null +++ b/go/runfiles/runfiles.go @@ -0,0 +1,305 @@ +// Copyright 2020, 2021 Google LLC +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// https://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +// Package runfiles provides access to Bazel runfiles. +// +// Usage +// +// This package has two main entry points, the global functions Rlocation and Env, +// and the Runfiles type. +// +// Global functions +// +// For simple use cases that don’t require hermetic behavior, use the Rlocation and +// Env functions to access runfiles. Use Rlocation to find the filesystem location +// of a runfile, and use Env to obtain environmental variables to pass on to +// subprocesses. +// +// Runfiles type +// +// If you need hermetic behavior or want to change the runfiles discovery +// process, use New to create a Runfiles object. New accepts a few options to +// change the discovery process. Runfiles objects have methods Rlocation and Env, +// which correspond to the package-level functions. On Go 1.16, *Runfiles +// implements fs.FS, fs.StatFS, and fs.ReadFileFS. +package runfiles + +import ( + "bufio" + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +const ( + directoryVar = "RUNFILES_DIR" + manifestFileVar = "RUNFILES_MANIFEST_FILE" +) + +type repoMappingKey struct { + sourceRepo string + targetRepoApparentName string +} + +// Runfiles allows access to Bazel runfiles. Use New to create Runfiles +// objects; the zero Runfiles object always returns errors. See +// https://docs.bazel.build/skylark/rules.html#runfiles for some information on +// Bazel runfiles. +type Runfiles struct { + // We don’t need concurrency control since Runfiles objects are + // immutable once created. + impl runfiles + env string + repoMapping map[repoMappingKey]string + sourceRepo string +} + +const noSourceRepoSentinel = "_not_a_valid_repository_name" + +// New creates a given Runfiles object. By default, it uses os.Args and the +// RUNFILES_MANIFEST_FILE and RUNFILES_DIR environmental variables to find the +// runfiles location. This can be overwritten by passing some options. +// +// See section “Runfiles discovery” in +// https://docs.google.com/document/d/e/2PACX-1vSDIrFnFvEYhKsCMdGdD40wZRBX3m3aZ5HhVj4CtHPmiXKDCxioTUbYsDydjKtFDAzER5eg7OjJWs3V/pub. +func New(opts ...Option) (*Runfiles, error) { + var o options + o.sourceRepo = noSourceRepoSentinel + for _, a := range opts { + a.apply(&o) + } + + if o.sourceRepo == noSourceRepoSentinel { + o.sourceRepo = SourceRepo(CallerRepository()) + } + + if o.manifest == "" { + o.manifest = ManifestFile(os.Getenv(manifestFileVar)) + } + if o.manifest != "" { + return o.manifest.new(o.sourceRepo) + } + + if o.directory == "" { + o.directory = Directory(os.Getenv(directoryVar)) + } + if o.directory != "" { + return o.directory.new(o.sourceRepo) + } + + if o.program == "" { + o.program = ProgramName(os.Args[0]) + } + manifest := ManifestFile(o.program + ".runfiles_manifest") + if stat, err := os.Stat(string(manifest)); err == nil && stat.Mode().IsRegular() { + return manifest.new(o.sourceRepo) + } + + dir := Directory(o.program + ".runfiles") + if stat, err := os.Stat(string(dir)); err == nil && stat.IsDir() { + return dir.new(o.sourceRepo) + } + + return nil, errors.New("runfiles: no runfiles found") +} + +// Rlocation returns the (relative or absolute) path name of a runfile. +// The runfile name must be a runfile-root relative path, using the slash (not +// backslash) as directory separator. It is typically of the form +// "repo/path/to/pkg/file". +// +// If r is the zero Runfiles object, Rlocation always returns an error. If the +// runfiles manifest maps s to an empty name (indicating an empty runfile not +// present in the filesystem), Rlocation returns an error that wraps ErrEmpty. +// +// See section “Library interface” in +// https://docs.google.com/document/d/e/2PACX-1vSDIrFnFvEYhKsCMdGdD40wZRBX3m3aZ5HhVj4CtHPmiXKDCxioTUbYsDydjKtFDAzER5eg7OjJWs3V/pub. +func (r *Runfiles) Rlocation(path string) (string, error) { + if r.impl == nil { + return "", errors.New("runfiles: uninitialized Runfiles object") + } + + if path == "" { + return "", errors.New("runfiles: path may not be empty") + } + if err := isNormalizedPath(path); err != nil { + return "", err + } + + // See https://github.com/bazelbuild/bazel/commit/b961b0ad6cc2578b98d0a307581e23e73392ad02 + if strings.HasPrefix(path, `\`) { + return "", fmt.Errorf("runfiles: path %q is absolute without a drive letter", path) + } + if filepath.IsAbs(path) { + return path, nil + } + + mappedPath := path + split := strings.SplitN(path, "/", 2) + if len(split) == 2 { + key := repoMappingKey{r.sourceRepo, split[0]} + if targetRepoDirectory, exists := r.repoMapping[key]; exists { + mappedPath = targetRepoDirectory + "/" + split[1] + } + } + + p, err := r.impl.path(mappedPath) + if err != nil { + return "", Error{path, err} + } + return p, nil +} + +func isNormalizedPath(s string) error { + if strings.HasPrefix(s, "../") || strings.Contains(s, "/../") || strings.HasSuffix(s, "/..") { + return fmt.Errorf(`runfiles: path %q must not contain ".." segments`, s) + } + if strings.HasPrefix(s, "./") || strings.Contains(s, "/./") || strings.HasSuffix(s, "/.") { + return fmt.Errorf(`runfiles: path %q must not contain "." segments`, s) + } + if strings.Contains(s, "//") { + return fmt.Errorf(`runfiles: path %q must not contain "//"`, s) + } + return nil +} + +// loadRepoMapping loads the repo mapping (if it exists) using the impl. +// This mutates the Runfiles object, but is idempotent. +func (r *Runfiles) loadRepoMapping() error { + repoMappingPath, err := r.impl.path(repoMappingRlocation) + // If Bzlmod is disabled, the repository mapping manifest isn't created, so + // it is not an error if it is missing. + if err != nil { + return nil + } + r.repoMapping, err = parseRepoMapping(repoMappingPath) + // If the repository mapping manifest exists, it must be valid. + return err +} + +// Env returns additional environmental variables to pass to subprocesses. +// Each element is of the form “key=value”. Pass these variables to +// Bazel-built binaries so they can find their runfiles as well. See the +// Runfiles example for an illustration of this. +// +// The return value is a newly-allocated slice; you can modify it at will. If +// r is the zero Runfiles object, the return value is nil. +func (r *Runfiles) Env() []string { + if r.env == "" { + return nil + } + return []string{r.env} +} + +// WithSourceRepo returns a Runfiles instance identical to the current one, +// except that it uses the given repository's repository mapping when resolving +// runfiles paths. +func (r *Runfiles) WithSourceRepo(sourceRepo string) *Runfiles { + if r.sourceRepo == sourceRepo { + return r + } + clone := *r + clone.sourceRepo = sourceRepo + return &clone +} + +// Option is an option for the New function to override runfiles discovery. +type Option interface { + apply(*options) +} + +// ProgramName is an Option that sets the program name. If not set, New uses +// os.Args[0]. +type ProgramName string + +// SourceRepo is an Option that sets the canonical name of the repository whose +// repository mapping should be used to resolve runfiles paths. If not set, New +// uses the repository containing the source file from which New is called. +// Use CurrentRepository to get the name of the current repository. +type SourceRepo string + +// Error represents a failure to look up a runfile. +type Error struct { + // Runfile name that caused the failure. + Name string + + // Underlying error. + Err error +} + +// Error implements error.Error. +func (e Error) Error() string { + return fmt.Sprintf("runfile %s: %s", e.Name, e.Err.Error()) +} + +// Unwrap returns the underlying error, for errors.Unwrap. +func (e Error) Unwrap() error { return e.Err } + +// ErrEmpty indicates that a runfile isn’t present in the filesystem, but +// should be created as an empty file if necessary. +var ErrEmpty = errors.New("empty runfile") + +type options struct { + program ProgramName + manifest ManifestFile + directory Directory + sourceRepo SourceRepo +} + +func (p ProgramName) apply(o *options) { o.program = p } +func (m ManifestFile) apply(o *options) { o.manifest = m } +func (d Directory) apply(o *options) { o.directory = d } +func (sr SourceRepo) apply(o *options) { o.sourceRepo = sr } + +type runfiles interface { + path(string) (string, error) +} + +// The runfiles root symlink under which the repository mapping can be found. +// https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/Runfiles.java;l=424 +const repoMappingRlocation = "_repo_mapping" + +// Parses a repository mapping manifest file emitted with Bzlmod enabled. +func parseRepoMapping(path string) (map[repoMappingKey]string, error) { + r, err := os.Open(path) + if err != nil { + // The repo mapping manifest only exists with Bzlmod, so it's not an + // error if it's missing. Since any repository name not contained in the + // mapping is assumed to be already canonical, an empty map is + // equivalent to not applying any mapping. + return nil, nil + } + defer r.Close() + + // Each line of the repository mapping manifest has the form: + // canonical name of source repo,apparent name of target repo,target repo runfiles directory + // https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/RepoMappingManifestAction.java;l=117 + s := bufio.NewScanner(r) + repoMapping := make(map[repoMappingKey]string) + for s.Scan() { + fields := strings.SplitN(s.Text(), ",", 3) + if len(fields) != 3 { + return nil, fmt.Errorf("runfiles: bad repo mapping line %q in file %s", s.Text(), path) + } + repoMapping[repoMappingKey{fields[0], fields[1]}] = fields[2] + } + + if err = s.Err(); err != nil { + return nil, fmt.Errorf("runfiles: error parsing repo mapping file %s: %w", path, err) + } + + return repoMapping, nil +} |