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