// 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" legacyDirectoryVar = "JAVA_RUNFILES" 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 { return 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 }