xref: /aosp_15_r20/external/bazelbuild-rules_go/go/runfiles/runfiles.go (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
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