github.com/bazelbuild/rules_go@v0.47.2-0.20240515105122-e7ddb9ea474e/go/tools/bazel/runfiles.go (about)

     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  
    15  package bazel
    16  
    17  import (
    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  
    31  const (
    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.
    47  func 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.
   100  func 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.
   168  type 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.
   183  func 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.
   230  func 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.
   243  func 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'.
   252  func 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  
   269  var 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  
   294  type index struct {
   295  	indexWithWorkspace     map[indexKey]*RunfileEntry
   296  	indexIgnoringWorksapce map[string]*RunfileEntry
   297  }
   298  
   299  func newIndex() index {
   300  	return index{
   301  		indexWithWorkspace:     make(map[indexKey]*RunfileEntry),
   302  		indexIgnoringWorksapce: make(map[string]*RunfileEntry),
   303  	}
   304  }
   305  
   306  func (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  
   314  func (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  
   325  func (i *index) GetIgnoringWorkspace(shortPath string) (*RunfileEntry, bool) {
   326  	entry, ok := i.indexIgnoringWorksapce[shortPath]
   327  	return entry, ok
   328  }
   329  
   330  type indexKey struct {
   331  	workspace string
   332  	shortPath string
   333  }
   334  
   335  func ensureRunfiles() error {
   336  	runfiles.once.Do(initRunfiles)
   337  	return runfiles.err
   338  }
   339  
   340  func 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  			if strings.HasPrefix(entry.ShortPath, "../") {
   399  				entry.ShortPath = entry.ShortPath[len("../"):]
   400  				if i := strings.IndexByte(entry.ShortPath, '/'); i >= 0 {
   401  					entry.Workspace = entry.ShortPath[:i]
   402  					entry.ShortPath = entry.ShortPath[i+1:]
   403  				}
   404  			}
   405  
   406  			runfiles.list = append(runfiles.list, entry)
   407  			runfiles.index.Put(&entry)
   408  		}
   409  	}
   410  
   411  	runfiles.workspace = os.Getenv("TEST_WORKSPACE")
   412  
   413  	if dir := os.Getenv("RUNFILES_DIR"); dir != "" {
   414  		runfiles.dir = dir
   415  	} else if dir = os.Getenv("TEST_SRCDIR"); dir != "" {
   416  		runfiles.dir = dir
   417  	} else if runtime.GOOS != "windows" {
   418  		dir, err := os.Getwd()
   419  		if err != nil {
   420  			runfiles.err = fmt.Errorf("error locating runfiles dir: %v", err)
   421  			return
   422  		}
   423  
   424  		parent := filepath.Dir(dir)
   425  		if strings.HasSuffix(parent, ".runfiles") {
   426  			runfiles.dir = parent
   427  			if runfiles.workspace == "" {
   428  				runfiles.workspace = filepath.Base(dir)
   429  			}
   430  		} else {
   431  			runfiles.err = errors.New("could not locate runfiles directory")
   432  			return
   433  		}
   434  	}
   435  
   436  	if runfiles.dir != "" {
   437  		fis, err := ioutil.ReadDir(runfiles.dir)
   438  		if err != nil {
   439  			runfiles.err = fmt.Errorf("could not open runfiles directory: %v", err)
   440  			return
   441  		}
   442  		for _, fi := range fis {
   443  			if fi.IsDir() {
   444  				runfiles.workspaces = append(runfiles.workspaces, fi.Name())
   445  			}
   446  		}
   447  		sort.Strings(runfiles.workspaces)
   448  	}
   449  }