github.com/bazelbuild/rules_go@v0.47.2-0.20240515105122-e7ddb9ea474e/go/runfiles/runfiles.go (about)

     1  // Copyright 2020, 2021 Google LLC
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     https://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  // Package runfiles provides access to Bazel runfiles.
    16  //
    17  // # Usage
    18  //
    19  // This package has two main entry points, the global functions Rlocation and Env,
    20  // and the Runfiles type.
    21  //
    22  // # Global functions
    23  //
    24  // For simple use cases that don’t require hermetic behavior, use the Rlocation and
    25  // Env functions to access runfiles.  Use Rlocation to find the filesystem location
    26  // of a runfile, and use Env to obtain environmental variables to pass on to
    27  // subprocesses.
    28  //
    29  // # Runfiles type
    30  //
    31  // If you need hermetic behavior or want to change the runfiles discovery
    32  // process, use New to create a Runfiles object.  New accepts a few options to
    33  // change the discovery process.  Runfiles objects have methods Rlocation and Env,
    34  // which correspond to the package-level functions.  On Go 1.16, *Runfiles
    35  // implements fs.FS, fs.StatFS, and fs.ReadFileFS.
    36  package runfiles
    37  
    38  import (
    39  	"bufio"
    40  	"errors"
    41  	"fmt"
    42  	"os"
    43  	"path/filepath"
    44  	"strings"
    45  )
    46  
    47  const (
    48  	directoryVar       = "RUNFILES_DIR"
    49  	legacyDirectoryVar = "JAVA_RUNFILES"
    50  	manifestFileVar    = "RUNFILES_MANIFEST_FILE"
    51  )
    52  
    53  type repoMappingKey struct {
    54  	sourceRepo             string
    55  	targetRepoApparentName string
    56  }
    57  
    58  // Runfiles allows access to Bazel runfiles.  Use New to create Runfiles
    59  // objects; the zero Runfiles object always returns errors.  See
    60  // https://docs.bazel.build/skylark/rules.html#runfiles for some information on
    61  // Bazel runfiles.
    62  type Runfiles struct {
    63  	// We don’t need concurrency control since Runfiles objects are
    64  	// immutable once created.
    65  	impl        runfiles
    66  	env         []string
    67  	repoMapping map[repoMappingKey]string
    68  	sourceRepo  string
    69  }
    70  
    71  const noSourceRepoSentinel = "_not_a_valid_repository_name"
    72  
    73  // New creates a given Runfiles object.  By default, it uses os.Args and the
    74  // RUNFILES_MANIFEST_FILE and RUNFILES_DIR environmental variables to find the
    75  // runfiles location.  This can be overwritten by passing some options.
    76  //
    77  // See section “Runfiles discovery” in
    78  // https://docs.google.com/document/d/e/2PACX-1vSDIrFnFvEYhKsCMdGdD40wZRBX3m3aZ5HhVj4CtHPmiXKDCxioTUbYsDydjKtFDAzER5eg7OjJWs3V/pub.
    79  func New(opts ...Option) (*Runfiles, error) {
    80  	var o options
    81  	o.sourceRepo = noSourceRepoSentinel
    82  	for _, a := range opts {
    83  		a.apply(&o)
    84  	}
    85  
    86  	if o.sourceRepo == noSourceRepoSentinel {
    87  		o.sourceRepo = SourceRepo(CallerRepository())
    88  	}
    89  
    90  	if o.manifest == "" {
    91  		o.manifest = ManifestFile(os.Getenv(manifestFileVar))
    92  	}
    93  	if o.manifest != "" {
    94  		return o.manifest.new(o.sourceRepo)
    95  	}
    96  
    97  	if o.directory == "" {
    98  		o.directory = Directory(os.Getenv(directoryVar))
    99  	}
   100  	if o.directory != "" {
   101  		return o.directory.new(o.sourceRepo)
   102  	}
   103  
   104  	if o.program == "" {
   105  		o.program = ProgramName(os.Args[0])
   106  	}
   107  	manifest := ManifestFile(o.program + ".runfiles_manifest")
   108  	if stat, err := os.Stat(string(manifest)); err == nil && stat.Mode().IsRegular() {
   109  		return manifest.new(o.sourceRepo)
   110  	}
   111  
   112  	dir := Directory(o.program + ".runfiles")
   113  	if stat, err := os.Stat(string(dir)); err == nil && stat.IsDir() {
   114  		return dir.new(o.sourceRepo)
   115  	}
   116  
   117  	return nil, errors.New("runfiles: no runfiles found")
   118  }
   119  
   120  // Rlocation returns the (relative or absolute) path name of a runfile.
   121  // The runfile name must be a runfile-root relative path, using the slash (not
   122  // backslash) as directory separator. It is typically of the form
   123  // "repo/path/to/pkg/file".
   124  //
   125  // If r is the zero Runfiles object, Rlocation always returns an error. If the
   126  // runfiles manifest maps s to an empty name (indicating an empty runfile not
   127  // present in the filesystem), Rlocation returns an error that wraps ErrEmpty.
   128  //
   129  // See section “Library interface” in
   130  // https://docs.google.com/document/d/e/2PACX-1vSDIrFnFvEYhKsCMdGdD40wZRBX3m3aZ5HhVj4CtHPmiXKDCxioTUbYsDydjKtFDAzER5eg7OjJWs3V/pub.
   131  func (r *Runfiles) Rlocation(path string) (string, error) {
   132  	if r.impl == nil {
   133  		return "", errors.New("runfiles: uninitialized Runfiles object")
   134  	}
   135  
   136  	if path == "" {
   137  		return "", errors.New("runfiles: path may not be empty")
   138  	}
   139  	if err := isNormalizedPath(path); err != nil {
   140  		return "", err
   141  	}
   142  
   143  	// See https://github.com/bazelbuild/bazel/commit/b961b0ad6cc2578b98d0a307581e23e73392ad02
   144  	if strings.HasPrefix(path, `\`) {
   145  		return "", fmt.Errorf("runfiles: path %q is absolute without a drive letter", path)
   146  	}
   147  	if filepath.IsAbs(path) {
   148  		return path, nil
   149  	}
   150  
   151  	mappedPath := path
   152  	split := strings.SplitN(path, "/", 2)
   153  	if len(split) == 2 {
   154  		key := repoMappingKey{r.sourceRepo, split[0]}
   155  		if targetRepoDirectory, exists := r.repoMapping[key]; exists {
   156  			mappedPath = targetRepoDirectory + "/" + split[1]
   157  		}
   158  	}
   159  
   160  	p, err := r.impl.path(mappedPath)
   161  	if err != nil {
   162  		return "", Error{path, err}
   163  	}
   164  	return p, nil
   165  }
   166  
   167  func isNormalizedPath(s string) error {
   168  	if strings.HasPrefix(s, "../") || strings.Contains(s, "/../") || strings.HasSuffix(s, "/..") {
   169  		return fmt.Errorf(`runfiles: path %q must not contain ".." segments`, s)
   170  	}
   171  	if strings.HasPrefix(s, "./") || strings.Contains(s, "/./") || strings.HasSuffix(s, "/.") {
   172  		return fmt.Errorf(`runfiles: path %q must not contain "." segments`, s)
   173  	}
   174  	if strings.Contains(s, "//") {
   175  		return fmt.Errorf(`runfiles: path %q must not contain "//"`, s)
   176  	}
   177  	return nil
   178  }
   179  
   180  // loadRepoMapping loads the repo mapping (if it exists) using the impl.
   181  // This mutates the Runfiles object, but is idempotent.
   182  func (r *Runfiles) loadRepoMapping() error {
   183  	repoMappingPath, err := r.impl.path(repoMappingRlocation)
   184  	// If Bzlmod is disabled, the repository mapping manifest isn't created, so
   185  	// it is not an error if it is missing.
   186  	if err != nil {
   187  		return nil
   188  	}
   189  	r.repoMapping, err = parseRepoMapping(repoMappingPath)
   190  	// If the repository mapping manifest exists, it must be valid.
   191  	return err
   192  }
   193  
   194  // Env returns additional environmental variables to pass to subprocesses.
   195  // Each element is of the form “key=value”.  Pass these variables to
   196  // Bazel-built binaries so they can find their runfiles as well.  See the
   197  // Runfiles example for an illustration of this.
   198  //
   199  // The return value is a newly-allocated slice; you can modify it at will.  If
   200  // r is the zero Runfiles object, the return value is nil.
   201  func (r *Runfiles) Env() []string {
   202  	return r.env
   203  }
   204  
   205  // WithSourceRepo returns a Runfiles instance identical to the current one,
   206  // except that it uses the given repository's repository mapping when resolving
   207  // runfiles paths.
   208  func (r *Runfiles) WithSourceRepo(sourceRepo string) *Runfiles {
   209  	if r.sourceRepo == sourceRepo {
   210  		return r
   211  	}
   212  	clone := *r
   213  	clone.sourceRepo = sourceRepo
   214  	return &clone
   215  }
   216  
   217  // Option is an option for the New function to override runfiles discovery.
   218  type Option interface {
   219  	apply(*options)
   220  }
   221  
   222  // ProgramName is an Option that sets the program name. If not set, New uses
   223  // os.Args[0].
   224  type ProgramName string
   225  
   226  // SourceRepo is an Option that sets the canonical name of the repository whose
   227  // repository mapping should be used to resolve runfiles paths. If not set, New
   228  // uses the repository containing the source file from which New is called.
   229  // Use CurrentRepository to get the name of the current repository.
   230  type SourceRepo string
   231  
   232  // Error represents a failure to look up a runfile.
   233  type Error struct {
   234  	// Runfile name that caused the failure.
   235  	Name string
   236  
   237  	// Underlying error.
   238  	Err error
   239  }
   240  
   241  // Error implements error.Error.
   242  func (e Error) Error() string {
   243  	return fmt.Sprintf("runfile %s: %s", e.Name, e.Err.Error())
   244  }
   245  
   246  // Unwrap returns the underlying error, for errors.Unwrap.
   247  func (e Error) Unwrap() error { return e.Err }
   248  
   249  // ErrEmpty indicates that a runfile isn’t present in the filesystem, but
   250  // should be created as an empty file if necessary.
   251  var ErrEmpty = errors.New("empty runfile")
   252  
   253  type options struct {
   254  	program    ProgramName
   255  	manifest   ManifestFile
   256  	directory  Directory
   257  	sourceRepo SourceRepo
   258  }
   259  
   260  func (p ProgramName) apply(o *options)  { o.program = p }
   261  func (m ManifestFile) apply(o *options) { o.manifest = m }
   262  func (d Directory) apply(o *options)    { o.directory = d }
   263  func (sr SourceRepo) apply(o *options)  { o.sourceRepo = sr }
   264  
   265  type runfiles interface {
   266  	path(string) (string, error)
   267  }
   268  
   269  // The runfiles root symlink under which the repository mapping can be found.
   270  // https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/Runfiles.java;l=424
   271  const repoMappingRlocation = "_repo_mapping"
   272  
   273  // Parses a repository mapping manifest file emitted with Bzlmod enabled.
   274  func parseRepoMapping(path string) (map[repoMappingKey]string, error) {
   275  	r, err := os.Open(path)
   276  	if err != nil {
   277  		// The repo mapping manifest only exists with Bzlmod, so it's not an
   278  		// error if it's missing. Since any repository name not contained in the
   279  		// mapping is assumed to be already canonical, an empty map is
   280  		// equivalent to not applying any mapping.
   281  		return nil, nil
   282  	}
   283  	defer r.Close()
   284  
   285  	// Each line of the repository mapping manifest has the form:
   286  	// canonical name of source repo,apparent name of target repo,target repo runfiles directory
   287  	// https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/RepoMappingManifestAction.java;l=117
   288  	s := bufio.NewScanner(r)
   289  	repoMapping := make(map[repoMappingKey]string)
   290  	for s.Scan() {
   291  		fields := strings.SplitN(s.Text(), ",", 3)
   292  		if len(fields) != 3 {
   293  			return nil, fmt.Errorf("runfiles: bad repo mapping line %q in file %s", s.Text(), path)
   294  		}
   295  		repoMapping[repoMappingKey{fields[0], fields[1]}] = fields[2]
   296  	}
   297  
   298  	if err = s.Err(); err != nil {
   299  		return nil, fmt.Errorf("runfiles: error parsing repo mapping file %s: %w", path, err)
   300  	}
   301  
   302  	return repoMapping, nil
   303  }