xref: /aosp_15_r20/external/bazelbuild-rules_go/go/tools/bazel/runfiles.go (revision 9bb1b549b6a84214c53be0924760be030e66b93a)
1// Copyright 2018 The Bazel Authors.
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//      http://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
15package bazel
16
17import (
18	"bytes"
19	"errors"
20	"fmt"
21	"io/ioutil"
22	"os"
23	"path"
24	"path/filepath"
25	"runtime"
26	"sort"
27	"strings"
28	"sync"
29)
30
31const (
32	RUNFILES_MANIFEST_FILE = "RUNFILES_MANIFEST_FILE"
33	RUNFILES_DIR           = "RUNFILES_DIR"
34)
35
36// Runfile returns an absolute path to the file named by "path", which
37// should be a relative path from the workspace root to the file within
38// the bazel workspace.
39//
40// Runfile may be called from tests invoked with 'bazel test' and
41// binaries invoked with 'bazel run'. On Windows,
42// only tests invoked with 'bazel test' are supported.
43//
44// Deprecated: Use github.com/bazelbuild/rules_go/go/runfiles instead for
45// cross-platform support matching the behavior of the Bazel-provided runfiles
46// libraries.
47func Runfile(path string) (string, error) {
48	// Search in working directory
49	if _, err := os.Stat(path); err == nil {
50		return filepath.Abs(path)
51	}
52
53	if err := ensureRunfiles(); err != nil {
54		return "", err
55	}
56
57	// Search manifest if we have one.
58	if entry, ok := runfiles.index.GetIgnoringWorkspace(path); ok {
59		return entry.Path, nil
60	}
61
62	if strings.HasPrefix(path, "../") || strings.HasPrefix(path, "external/") {
63		pathParts := strings.Split(path, "/")
64		if len(pathParts) >= 3 {
65			workspace := pathParts[1]
66			pathInsideWorkspace := strings.Join(pathParts[2:], "/")
67			if path := runfiles.index.Get(workspace, pathInsideWorkspace); path != "" {
68				return path, nil
69			}
70		}
71	}
72
73	// Search the main workspace.
74	if runfiles.workspace != "" {
75		mainPath := filepath.Join(runfiles.dir, runfiles.workspace, path)
76		if _, err := os.Stat(mainPath); err == nil {
77			return mainPath, nil
78		}
79	}
80
81	// Search other workspaces.
82	for _, w := range runfiles.workspaces {
83		workPath := filepath.Join(runfiles.dir, w, path)
84		if _, err := os.Stat(workPath); err == nil {
85			return workPath, nil
86		}
87	}
88
89	return "", fmt.Errorf("Runfile %s: could not locate file", path)
90}
91
92// FindBinary returns an absolute path to the binary built from a go_binary
93// rule in the given package with the given name. FindBinary is similar to
94// Runfile, but it accounts for varying configurations and file extensions,
95// which may cause the binary to have different paths on different platforms.
96//
97// FindBinary may be called from tests invoked with 'bazel test' and
98// binaries invoked with 'bazel run'. On Windows,
99// only tests invoked with 'bazel test' are supported.
100func FindBinary(pkg, name string) (string, bool) {
101	if err := ensureRunfiles(); err != nil {
102		return "", false
103	}
104
105	// If we've gathered a list of runfiles, either by calling ListRunfiles or
106	// parsing the manifest on Windows, just use that instead of searching
107	// directories. Return the first match. The manifest on Windows may contain
108	// multiple entries for the same file.
109	if runfiles.list != nil {
110		if runtime.GOOS == "windows" {
111			name += ".exe"
112		}
113		for _, entry := range runfiles.list {
114			if path.Base(entry.ShortPath) != name {
115				continue
116			}
117			pkgDir := path.Dir(path.Dir(entry.ShortPath))
118			if pkgDir == "." {
119				pkgDir = ""
120			}
121			if pkgDir != pkg {
122				continue
123			}
124			return entry.Path, true
125		}
126		return "", false
127	}
128
129	dir, err := Runfile(pkg)
130	if err != nil {
131		return "", false
132	}
133	var found string
134	stopErr := errors.New("stop")
135	err = filepath.Walk(dir, func(path string, info os.FileInfo, err error) error {
136		if err != nil {
137			return err
138		}
139		if info.IsDir() {
140			return nil
141		}
142		base := filepath.Base(path)
143		stem := strings.TrimSuffix(base, ".exe")
144		if stem != name {
145			return nil
146		}
147		if runtime.GOOS != "windows" {
148			if st, err := os.Stat(path); err != nil {
149				return err
150			} else if st.Mode()&0111 == 0 {
151				return nil
152			}
153		}
154		if stem == name {
155			found = path
156			return stopErr
157		}
158		return nil
159	})
160	if err == stopErr {
161		return found, true
162	} else {
163		return "", false
164	}
165}
166
167// A RunfileEntry describes a runfile.
168type RunfileEntry struct {
169	// Workspace is the bazel workspace the file came from. For example,
170	// this would be "io_bazel_rules_go" for a file in rules_go.
171	Workspace string
172
173	// ShortPath is a relative, slash-separated path from the workspace root
174	// to the file. For non-binary files, this may be passed to Runfile
175	// to locate a file.
176	ShortPath string
177
178	// Path is an absolute path to the file.
179	Path string
180}
181
182// ListRunfiles returns a list of available runfiles.
183func ListRunfiles() ([]RunfileEntry, error) {
184	if err := ensureRunfiles(); err != nil {
185		return nil, err
186	}
187
188	if runfiles.list == nil && runfiles.dir != "" {
189		runfiles.listOnce.Do(func() {
190			var list []RunfileEntry
191			haveWorkspaces := strings.HasSuffix(runfiles.dir, ".runfiles") && runfiles.workspace != ""
192
193			err := filepath.Walk(runfiles.dir, func(path string, info os.FileInfo, err error) error {
194				if err != nil {
195					return err
196				}
197				rel, _ := filepath.Rel(runfiles.dir, path)
198				rel = filepath.ToSlash(rel)
199				if rel == "." {
200					return nil
201				}
202
203				var workspace, shortPath string
204				if haveWorkspaces {
205					if i := strings.IndexByte(rel, '/'); i < 0 {
206						return nil
207					} else {
208						workspace, shortPath = rel[:i], rel[i+1:]
209					}
210				} else {
211					workspace, shortPath = "", rel
212				}
213
214				list = append(list, RunfileEntry{Workspace: workspace, ShortPath: shortPath, Path: path})
215				return nil
216			})
217			if err != nil {
218				runfiles.err = err
219				return
220			}
221			runfiles.list = list
222		})
223	}
224	return runfiles.list, runfiles.err
225}
226
227// TestWorkspace returns the name of the Bazel workspace for this test.
228// TestWorkspace returns an error if the TEST_WORKSPACE environment variable
229// was not set or SetDefaultTestWorkspace was not called.
230func TestWorkspace() (string, error) {
231	if err := ensureRunfiles(); err != nil {
232		return "", err
233	}
234	if runfiles.workspace != "" {
235		return runfiles.workspace, nil
236	}
237	return "", errors.New("TEST_WORKSPACE not set and SetDefaultTestWorkspace not called")
238}
239
240// SetDefaultTestWorkspace allows you to set a fake value for the
241// environment variable TEST_WORKSPACE if it is not defined. This is useful
242// when running tests on the command line and not through Bazel.
243func SetDefaultTestWorkspace(w string) {
244	ensureRunfiles()
245	runfiles.workspace = w
246}
247
248// RunfilesPath return the path to the runfiles tree.
249// It will return an error if there is no runfiles tree, for example because
250// the executable is run on Windows or was not invoked with 'bazel test'
251// or 'bazel run'.
252func RunfilesPath() (string, error) {
253	if err := ensureRunfiles(); err != nil {
254		return "", err
255	}
256	if runfiles.dir == "" {
257		if runtime.GOOS == "windows" {
258			return "", errors.New("RunfilesPath: no runfiles directory on windows")
259		} else {
260			return "", errors.New("could not locate runfiles directory")
261		}
262	}
263	if runfiles.workspace == "" {
264		return "", errors.New("could not locate runfiles workspace")
265	}
266	return filepath.Join(runfiles.dir, runfiles.workspace), nil
267}
268
269var runfiles = struct {
270	once, listOnce sync.Once
271
272	// list is a list of known runfiles, either loaded from the manifest
273	// or discovered by walking the runfile directory.
274	list []RunfileEntry
275
276	// index maps runfile short paths to absolute paths.
277	index index
278
279	// dir is a path to the runfile directory. Typically this is a directory
280	// named <target>.runfiles, with a subdirectory for each workspace.
281	dir string
282
283	// workspace is workspace where the binary or test was built.
284	workspace string
285
286	// workspaces is a list of other workspace names.
287	workspaces []string
288
289	// err is set when there is an error loading runfiles, for example,
290	// parsing the manifest.
291	err error
292}{}
293
294type index struct {
295	indexWithWorkspace     map[indexKey]*RunfileEntry
296	indexIgnoringWorksapce map[string]*RunfileEntry
297}
298
299func newIndex() index {
300	return index{
301		indexWithWorkspace:     make(map[indexKey]*RunfileEntry),
302		indexIgnoringWorksapce: make(map[string]*RunfileEntry),
303	}
304}
305
306func (i *index) Put(entry *RunfileEntry) {
307	i.indexWithWorkspace[indexKey{
308		workspace: entry.Workspace,
309		shortPath: entry.ShortPath,
310	}] = entry
311	i.indexIgnoringWorksapce[entry.ShortPath] = entry
312}
313
314func (i *index) Get(workspace string, shortPath string) string {
315	entry := i.indexWithWorkspace[indexKey{
316		workspace: workspace,
317		shortPath: shortPath,
318	}]
319	if entry == nil {
320		return ""
321	}
322	return entry.Path
323}
324
325func (i *index) GetIgnoringWorkspace(shortPath string) (*RunfileEntry, bool) {
326	entry, ok := i.indexIgnoringWorksapce[shortPath]
327	return entry, ok
328}
329
330type indexKey struct {
331	workspace string
332	shortPath string
333}
334
335func ensureRunfiles() error {
336	runfiles.once.Do(initRunfiles)
337	return runfiles.err
338}
339
340func initRunfiles() {
341	manifest := os.Getenv("RUNFILES_MANIFEST_FILE")
342	if manifest != "" {
343		// On Windows, Bazel doesn't create a symlink tree of runfiles because
344		// Windows doesn't support symbolic links by default. Instead, runfile
345		// locations are written to a manifest file.
346		runfiles.index = newIndex()
347		data, err := ioutil.ReadFile(manifest)
348		if err != nil {
349			runfiles.err = err
350			return
351		}
352		lineno := 0
353		for len(data) > 0 {
354			i := bytes.IndexByte(data, '\n')
355			var line []byte
356			if i < 0 {
357				line = data
358				data = nil
359			} else {
360				line = data[:i]
361				data = data[i+1:]
362			}
363			lineno++
364
365			// Only TrimRight newlines. Do not TrimRight() completely, because that would remove spaces too.
366			// This is necessary in order to have at least one space in every manifest line.
367			// Some manifest entries don't have any path after this space, namely the "__init__.py" entries.
368			// original comment sourced from: https://github.com/bazelbuild/bazel/blob/09c621e4cf5b968f4c6cdf905ab142d5961f9ddc/src/test/py/bazel/runfiles_test.py#L225
369			line = bytes.TrimRight(line, "\r\n")
370			if len(line) == 0 {
371				continue
372			}
373
374			spaceIndex := bytes.IndexByte(line, ' ')
375			if spaceIndex < 0 {
376				runfiles.err = fmt.Errorf(
377					"error parsing runfiles manifest: %s:%d: no space: '%s'", manifest, lineno, line)
378				return
379			}
380			shortPath := string(line[0:spaceIndex])
381			abspath := ""
382			if len(line) > spaceIndex+1 {
383				abspath = string(line[spaceIndex+1:])
384			}
385
386			entry := RunfileEntry{ShortPath: shortPath, Path: abspath}
387			if i := strings.IndexByte(entry.ShortPath, '/'); i >= 0 {
388				entry.Workspace = entry.ShortPath[:i]
389				entry.ShortPath = entry.ShortPath[i+1:]
390			}
391			if strings.HasPrefix(entry.ShortPath, "external/") {
392				entry.ShortPath = entry.ShortPath[len("external/"):]
393				if i := strings.IndexByte(entry.ShortPath, '/'); i >= 0 {
394					entry.Workspace = entry.ShortPath[:i]
395					entry.ShortPath = entry.ShortPath[i+1:]
396				}
397			}
398
399			runfiles.list = append(runfiles.list, entry)
400			runfiles.index.Put(&entry)
401		}
402	}
403
404	runfiles.workspace = os.Getenv("TEST_WORKSPACE")
405
406	if dir := os.Getenv("RUNFILES_DIR"); dir != "" {
407		runfiles.dir = dir
408	} else if dir = os.Getenv("TEST_SRCDIR"); dir != "" {
409		runfiles.dir = dir
410	} else if runtime.GOOS != "windows" {
411		dir, err := os.Getwd()
412		if err != nil {
413			runfiles.err = fmt.Errorf("error locating runfiles dir: %v", err)
414			return
415		}
416
417		parent := filepath.Dir(dir)
418		if strings.HasSuffix(parent, ".runfiles") {
419			runfiles.dir = parent
420			if runfiles.workspace == "" {
421				runfiles.workspace = filepath.Base(dir)
422			}
423		} else {
424			runfiles.err = errors.New("could not locate runfiles directory")
425			return
426		}
427	}
428
429	if runfiles.dir != "" {
430		fis, err := ioutil.ReadDir(runfiles.dir)
431		if err != nil {
432			runfiles.err = fmt.Errorf("could not open runfiles directory: %v", err)
433			return
434		}
435		for _, fi := range fis {
436			if fi.IsDir() {
437				runfiles.workspaces = append(runfiles.workspaces, fi.Name())
438			}
439		}
440		sort.Strings(runfiles.workspaces)
441	}
442}
443