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