aboutsummaryrefslogtreecommitdiff
path: root/go/runfiles/runfiles.go
diff options
context:
space:
mode:
Diffstat (limited to 'go/runfiles/runfiles.go')
-rw-r--r--go/runfiles/runfiles.go305
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
+}