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 }