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