blob: 3e4792f5b0081ba85edfd94d746b43f9709c6803 [file] [log] [blame]
// Copyright 2020, 2021 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
// Package runfiles provides access to Bazel runfiles.
//
// # Usage
//
// This package has two main entry points, the global functions Rlocation and Env,
// and the Runfiles type.
//
// # Global functions
//
// For simple use cases that don’t require hermetic behavior, use the Rlocation and
// Env functions to access runfiles. Use Rlocation to find the filesystem location
// of a runfile, and use Env to obtain environmental variables to pass on to
// subprocesses.
//
// # Runfiles type
//
// If you need hermetic behavior or want to change the runfiles discovery
// process, use New to create a Runfiles object. New accepts a few options to
// change the discovery process. Runfiles objects have methods Rlocation and Env,
// which correspond to the package-level functions. On Go 1.16, *Runfiles
// implements fs.FS, fs.StatFS, and fs.ReadFileFS.
package runfiles
import (
"bufio"
"errors"
"fmt"
"os"
"path/filepath"
"strings"
)
const (
directoryVar = "RUNFILES_DIR"
legacyDirectoryVar = "JAVA_RUNFILES"
manifestFileVar = "RUNFILES_MANIFEST_FILE"
)
type repoMappingKey struct {
sourceRepo string
targetRepoApparentName string
}
// Runfiles allows access to Bazel runfiles. Use New to create Runfiles
// objects; the zero Runfiles object always returns errors. See
// https://docs.bazel.build/skylark/rules.html#runfiles for some information on
// Bazel runfiles.
type Runfiles struct {
// We don’t need concurrency control since Runfiles objects are
// immutable once created.
impl runfiles
env []string
repoMapping map[repoMappingKey]string
sourceRepo string
}
const noSourceRepoSentinel = "_not_a_valid_repository_name"
// New creates a given Runfiles object. By default, it uses os.Args and the
// RUNFILES_MANIFEST_FILE and RUNFILES_DIR environmental variables to find the
// runfiles location. This can be overwritten by passing some options.
//
// See section “Runfiles discovery” in
// https://docs.google.com/document/d/e/2PACX-1vSDIrFnFvEYhKsCMdGdD40wZRBX3m3aZ5HhVj4CtHPmiXKDCxioTUbYsDydjKtFDAzER5eg7OjJWs3V/pub.
func New(opts ...Option) (*Runfiles, error) {
var o options
o.sourceRepo = noSourceRepoSentinel
for _, a := range opts {
a.apply(&o)
}
if o.sourceRepo == noSourceRepoSentinel {
o.sourceRepo = SourceRepo(CallerRepository())
}
if o.manifest == "" {
o.manifest = ManifestFile(os.Getenv(manifestFileVar))
}
if o.manifest != "" {
return o.manifest.new(o.sourceRepo)
}
if o.directory == "" {
o.directory = Directory(os.Getenv(directoryVar))
}
if o.directory != "" {
return o.directory.new(o.sourceRepo)
}
if o.program == "" {
o.program = ProgramName(os.Args[0])
}
manifest := ManifestFile(o.program + ".runfiles_manifest")
if stat, err := os.Stat(string(manifest)); err == nil && stat.Mode().IsRegular() {
return manifest.new(o.sourceRepo)
}
dir := Directory(o.program + ".runfiles")
if stat, err := os.Stat(string(dir)); err == nil && stat.IsDir() {
return dir.new(o.sourceRepo)
}
return nil, errors.New("runfiles: no runfiles found")
}
// Rlocation returns the (relative or absolute) path name of a runfile.
// The runfile name must be a runfile-root relative path, using the slash (not
// backslash) as directory separator. It is typically of the form
// "repo/path/to/pkg/file".
//
// If r is the zero Runfiles object, Rlocation always returns an error. If the
// runfiles manifest maps s to an empty name (indicating an empty runfile not
// present in the filesystem), Rlocation returns an error that wraps ErrEmpty.
//
// See section “Library interface” in
// https://docs.google.com/document/d/e/2PACX-1vSDIrFnFvEYhKsCMdGdD40wZRBX3m3aZ5HhVj4CtHPmiXKDCxioTUbYsDydjKtFDAzER5eg7OjJWs3V/pub.
func (r *Runfiles) Rlocation(path string) (string, error) {
if r.impl == nil {
return "", errors.New("runfiles: uninitialized Runfiles object")
}
if path == "" {
return "", errors.New("runfiles: path may not be empty")
}
if err := isNormalizedPath(path); err != nil {
return "", err
}
// See https://github.com/bazelbuild/bazel/commit/b961b0ad6cc2578b98d0a307581e23e73392ad02
if strings.HasPrefix(path, `\`) {
return "", fmt.Errorf("runfiles: path %q is absolute without a drive letter", path)
}
if filepath.IsAbs(path) {
return path, nil
}
mappedPath := path
split := strings.SplitN(path, "/", 2)
if len(split) == 2 {
key := repoMappingKey{r.sourceRepo, split[0]}
if targetRepoDirectory, exists := r.repoMapping[key]; exists {
mappedPath = targetRepoDirectory + "/" + split[1]
}
}
p, err := r.impl.path(mappedPath)
if err != nil {
return "", Error{path, err}
}
return p, nil
}
func isNormalizedPath(s string) error {
if strings.HasPrefix(s, "../") || strings.Contains(s, "/../") || strings.HasSuffix(s, "/..") {
return fmt.Errorf(`runfiles: path %q must not contain ".." segments`, s)
}
if strings.HasPrefix(s, "./") || strings.Contains(s, "/./") || strings.HasSuffix(s, "/.") {
return fmt.Errorf(`runfiles: path %q must not contain "." segments`, s)
}
if strings.Contains(s, "//") {
return fmt.Errorf(`runfiles: path %q must not contain "//"`, s)
}
return nil
}
// loadRepoMapping loads the repo mapping (if it exists) using the impl.
// This mutates the Runfiles object, but is idempotent.
func (r *Runfiles) loadRepoMapping() error {
repoMappingPath, err := r.impl.path(repoMappingRlocation)
// If Bzlmod is disabled, the repository mapping manifest isn't created, so
// it is not an error if it is missing.
if err != nil {
return nil
}
r.repoMapping, err = parseRepoMapping(repoMappingPath)
// If the repository mapping manifest exists, it must be valid.
return err
}
// Env returns additional environmental variables to pass to subprocesses.
// Each element is of the form “key=value”. Pass these variables to
// Bazel-built binaries so they can find their runfiles as well. See the
// Runfiles example for an illustration of this.
//
// The return value is a newly-allocated slice; you can modify it at will. If
// r is the zero Runfiles object, the return value is nil.
func (r *Runfiles) Env() []string {
return r.env
}
// WithSourceRepo returns a Runfiles instance identical to the current one,
// except that it uses the given repository's repository mapping when resolving
// runfiles paths.
func (r *Runfiles) WithSourceRepo(sourceRepo string) *Runfiles {
if r.sourceRepo == sourceRepo {
return r
}
clone := *r
clone.sourceRepo = sourceRepo
return &clone
}
// Option is an option for the New function to override runfiles discovery.
type Option interface {
apply(*options)
}
// ProgramName is an Option that sets the program name. If not set, New uses
// os.Args[0].
type ProgramName string
// SourceRepo is an Option that sets the canonical name of the repository whose
// repository mapping should be used to resolve runfiles paths. If not set, New
// uses the repository containing the source file from which New is called.
// Use CurrentRepository to get the name of the current repository.
type SourceRepo string
// Error represents a failure to look up a runfile.
type Error struct {
// Runfile name that caused the failure.
Name string
// Underlying error.
Err error
}
// Error implements error.Error.
func (e Error) Error() string {
return fmt.Sprintf("runfile %s: %s", e.Name, e.Err.Error())
}
// Unwrap returns the underlying error, for errors.Unwrap.
func (e Error) Unwrap() error { return e.Err }
// ErrEmpty indicates that a runfile isn’t present in the filesystem, but
// should be created as an empty file if necessary.
var ErrEmpty = errors.New("empty runfile")
type options struct {
program ProgramName
manifest ManifestFile
directory Directory
sourceRepo SourceRepo
}
func (p ProgramName) apply(o *options) { o.program = p }
func (m ManifestFile) apply(o *options) { o.manifest = m }
func (d Directory) apply(o *options) { o.directory = d }
func (sr SourceRepo) apply(o *options) { o.sourceRepo = sr }
type runfiles interface {
path(string) (string, error)
}
// The runfiles root symlink under which the repository mapping can be found.
// https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/Runfiles.java;l=424
const repoMappingRlocation = "_repo_mapping"
// Parses a repository mapping manifest file emitted with Bzlmod enabled.
func parseRepoMapping(path string) (map[repoMappingKey]string, error) {
r, err := os.Open(path)
if err != nil {
// The repo mapping manifest only exists with Bzlmod, so it's not an
// error if it's missing. Since any repository name not contained in the
// mapping is assumed to be already canonical, an empty map is
// equivalent to not applying any mapping.
return nil, nil
}
defer r.Close()
// Each line of the repository mapping manifest has the form:
// canonical name of source repo,apparent name of target repo,target repo runfiles directory
// https://cs.opensource.google/bazel/bazel/+/1b073ac0a719a09c9b2d1a52680517ab22dc971e:src/main/java/com/google/devtools/build/lib/analysis/RepoMappingManifestAction.java;l=117
s := bufio.NewScanner(r)
repoMapping := make(map[repoMappingKey]string)
for s.Scan() {
fields := strings.SplitN(s.Text(), ",", 3)
if len(fields) != 3 {
return nil, fmt.Errorf("runfiles: bad repo mapping line %q in file %s", s.Text(), path)
}
repoMapping[repoMappingKey{fields[0], fields[1]}] = fields[2]
}
if err = s.Err(); err != nil {
return nil, fmt.Errorf("runfiles: error parsing repo mapping file %s: %w", path, err)
}
return repoMapping, nil
}