diff --git a/go/tools/bazel/runfiles.go b/go/tools/bazel/runfiles.go index dc143385ae..6a0ba787f3 100644 --- a/go/tools/bazel/runfiles.go +++ b/go/tools/bazel/runfiles.go @@ -40,6 +40,10 @@ const ( // Runfile may be called from tests invoked with 'bazel test' and // binaries invoked with 'bazel run'. On Windows, // only tests invoked with 'bazel test' are supported. +// +// Deprecated: Use github.com/bazelbuild/rules_go/go/tools/bazel/runfiles +// instead for cross-platform support matching the behavior of the +// Bazel-provided runfiles libraries. func Runfile(path string) (string, error) { // Search in working directory if _, err := os.Stat(path); err == nil { diff --git a/go/tools/bazel/runfiles/BUILD.bazel b/go/tools/bazel/runfiles/BUILD.bazel new file mode 100644 index 0000000000..3c5a0acd71 --- /dev/null +++ b/go/tools/bazel/runfiles/BUILD.bazel @@ -0,0 +1,53 @@ +# Copyright 2020 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. + +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "runfiles", + srcs = [ + "directory.go", + "fs.go", + "global.go", + "manifest.go", + "runfiles.go", + ], + importpath = "github.com/bazelbuild/rules_go/go/tools/bazel/runfiles", + visibility = ["//visibility:public"], +) + +go_test( + name = "runfiles_test", + srcs = [ + "fs_test.go", + "runfiles_test.go", + ], + data = [ + "test.txt", + "//go/tools/bazel/runfiles/testprog", + "@bazel_tools//tools/bash/runfiles", + ], + deps = [":runfiles"], +) + +exports_files( + ["test.txt"], + visibility = ["//go/tools/bazel/runfiles/testprog:__pkg__"], +) + +alias( + name = "go_default_library", + actual = ":runfiles", + visibility = ["//visibility:public"], +) diff --git a/go/tools/bazel/runfiles/directory.go b/go/tools/bazel/runfiles/directory.go new file mode 100644 index 0000000000..e5156ce46f --- /dev/null +++ b/go/tools/bazel/runfiles/directory.go @@ -0,0 +1,30 @@ +// Copyright 2020, 2021, 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 + +import "path/filepath" + +// Directory specifies the location of the runfiles directory. You can pass +// this as an option to New. If unset or empty, use the value of the +// environmental variable RUNFILES_DIR. +type Directory string + +func (d Directory) new() *Runfiles { + return &Runfiles{d, directoryVar + "=" + string(d)} +} + +func (d Directory) path(s string) (string, error) { + return filepath.Join(string(d), filepath.FromSlash(s)), nil +} diff --git a/go/tools/bazel/runfiles/fs.go b/go/tools/bazel/runfiles/fs.go new file mode 100644 index 0000000000..d5ee3f8796 --- /dev/null +++ b/go/tools/bazel/runfiles/fs.go @@ -0,0 +1,98 @@ +// Copyright 2021, 2022 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. + +// +build go1.16 + +package runfiles + +import ( + "errors" + "io" + "io/fs" + "os" + "time" +) + +// Open implements fs.FS.Open. +func (r *Runfiles) Open(name string) (fs.File, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} + } + p, err := r.Path(name) + if errors.Is(err, ErrEmpty) { + return emptyFile(name), nil + } + if err != nil { + return nil, pathError("open", name, err) + } + return os.Open(p) +} + +// Stat implements fs.StatFS.Stat. +func (r *Runfiles) Stat(name string) (fs.FileInfo, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "stat", Path: name, Err: fs.ErrInvalid} + } + p, err := r.Path(name) + if errors.Is(err, ErrEmpty) { + return emptyFileInfo(name), nil + } + if err != nil { + return nil, pathError("stat", name, err) + } + return os.Stat(p) +} + +// ReadFile implements fs.ReadFileFS.ReadFile. +func (r *Runfiles) ReadFile(name string) ([]byte, error) { + if !fs.ValidPath(name) { + return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrInvalid} + } + p, err := r.Path(name) + if errors.Is(err, ErrEmpty) { + return nil, nil + } + if err != nil { + return nil, pathError("open", name, err) + } + return os.ReadFile(p) +} + +type emptyFile string + +func (f emptyFile) Stat() (fs.FileInfo, error) { return emptyFileInfo(f), nil } +func (f emptyFile) Read([]byte) (int, error) { return 0, io.EOF } +func (emptyFile) Close() error { return nil } + +type emptyFileInfo string + +func (i emptyFileInfo) Name() string { return string(i) } +func (emptyFileInfo) Size() int64 { return 0 } +func (emptyFileInfo) Mode() fs.FileMode { return 0444 } +func (emptyFileInfo) ModTime() time.Time { return time.Time{} } +func (emptyFileInfo) IsDir() bool { return false } +func (emptyFileInfo) Sys() interface{} { return nil } + +func pathError(op, name string, err error) error { + if err == nil { + return nil + } + var rerr Error + if errors.As(err, &rerr) { + // Unwrap the error because we don’t need the failing name + // twice. + return &fs.PathError{Op: op, Path: rerr.Name, Err: rerr.Err} + } + return &fs.PathError{Op: op, Path: name, Err: err} +} diff --git a/go/tools/bazel/runfiles/fs_test.go b/go/tools/bazel/runfiles/fs_test.go new file mode 100644 index 0000000000..9594b161b5 --- /dev/null +++ b/go/tools/bazel/runfiles/fs_test.go @@ -0,0 +1,121 @@ +// Copyright 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. + +//go:build go1.16 +// +build go1.16 + +package runfiles_test + +import ( + "io" + "io/fs" + "os" + "path/filepath" + "runtime" + "testing" + "testing/fstest" + + "github.com/bazelbuild/rules_go/go/tools/bazel/runfiles" +) + +func TestFS(t *testing.T) { + fsys, err := runfiles.New() + if err != nil { + t.Fatal(err) + } + + // Ensure that the Runfiles object implements FS interfaces. + var _ fs.FS = fsys + var _ fs.StatFS = fsys + var _ fs.ReadFileFS = fsys + + if runtime.GOOS == "windows" { + // Currently the result of + // + // fsys.Path("io_bazel_rules_go/go/tools/bazel/runfiles/test.txt") + // fsys.Path("bazel_tools/tools/bash/runfiles/runfiles.bash") + // fsys.Path("io_bazel_rules_go/go/tools/bazel/runfiles/testprog/testprog") + // + // would be a full path like these + // + // C:\b\bk-windows-1z0z\bazel\rules-go-golang\go\tools\bazel\runfiles\test.txt + // C:\b\zslxztin\external\bazel_tools\tools\bash\runfiles\runfiles.bash + // C:\b\pm4ep4b2\execroot\io_bazel_rules_go\bazel-out\x64_windows-fastbuild\bin\go\tools\bazel\runfiles\testprog\testprog + // + // Which does not follow any particular patter / rules. + // This makes it very hard to define what we are looking for on Windows. + // So let's skip this for now. + return + } + + expected1 := "io_bazel_rules_go/go/tools/bazel/runfiles/test.txt" + expected2 := "io_bazel_rules_go/go/tools/bazel/runfiles/testprog/testprog_/testprog" + expected3 := "bazel_tools/tools/bash/runfiles/runfiles.bash" + if err := fstest.TestFS(fsys, expected1, expected2, expected3); err != nil { + t.Error(err) + } +} + +func TestFS_empty(t *testing.T) { + dir := t.TempDir() + manifest := filepath.Join(dir, "manifest") + if err := os.WriteFile(manifest, []byte("__init__.py \n"), 0o600); err != nil { + t.Fatal(err) + } + fsys, err := runfiles.New(runfiles.ManifestFile(manifest), runfiles.ProgramName("/invalid"), runfiles.Directory("/invalid")) + if err != nil { + t.Fatal(err) + } + t.Run("Open", func(t *testing.T) { + fd, err := fsys.Open("__init__.py") + if err != nil { + t.Fatal(err) + } + defer fd.Close() + got, err := io.ReadAll(fd) + if err != nil { + t.Error(err) + } + if len(got) != 0 { + t.Errorf("got nonempty contents: %q", got) + } + }) + t.Run("Stat", func(t *testing.T) { + got, err := fsys.Stat("__init__.py") + if err != nil { + t.Fatal(err) + } + if got.Name() != "__init__.py" { + t.Errorf("Name: got %q, want %q", got.Name(), "__init__.py") + } + if got.Size() != 0 { + t.Errorf("Size: got %d, want %d", got.Size(), 0) + } + if !got.Mode().IsRegular() { + t.Errorf("IsRegular: got %v, want %v", got.Mode().IsRegular(), true) + } + if got.IsDir() { + t.Errorf("IsDir: got %v, want %v", got.IsDir(), false) + } + }) + t.Run("ReadFile", func(t *testing.T) { + got, err := fsys.ReadFile("__init__.py") + if err != nil { + t.Error(err) + } + if len(got) != 0 { + t.Errorf("got nonempty contents: %q", got) + } + }) +} diff --git a/go/tools/bazel/runfiles/global.go b/go/tools/bazel/runfiles/global.go new file mode 100644 index 0000000000..982b63bcb2 --- /dev/null +++ b/go/tools/bazel/runfiles/global.go @@ -0,0 +1,60 @@ +// 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 + +import "sync" + +// Path returns the absolute path name of a runfile. The runfile name must be +// a relative path, using the slash (not backslash) as directory separator. If +// the runfiles manifest maps s to an empty name (indicating an empty runfile +// not present in the filesystem), Path returns an error that wraps ErrEmpty. +func Path(s string) (string, error) { + r, err := g.get() + if err != nil { + return "", err + } + return r.Path(s) +} + +// 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. +func Env() ([]string, error) { + r, err := g.get() + if err != nil { + return nil, err + } + return r.Env(), nil +} + +type global struct { + once sync.Once + runfiles *Runfiles + err error +} + +func (g *global) get() (*Runfiles, error) { + g.once.Do(g.init) + return g.runfiles, g.err +} + +func (g *global) init() { + g.runfiles, g.err = New() +} + +var g global diff --git a/go/tools/bazel/runfiles/manifest.go b/go/tools/bazel/runfiles/manifest.go new file mode 100644 index 0000000000..dac945c8b4 --- /dev/null +++ b/go/tools/bazel/runfiles/manifest.go @@ -0,0 +1,86 @@ +// 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 + +import ( + "bufio" + "fmt" + "os" + "path" + "path/filepath" + "strings" +) + +// ManifestFile specifies the location of the runfile manifest file. You can +// pass this as an option to New. If unset or empty, use the value of the +// environmental variable RUNFILES_MANIFEST_FILE. +type ManifestFile string + +func (f ManifestFile) new() (*Runfiles, error) { + m, err := f.parse() + if err != nil { + return nil, err + } + + return &Runfiles{m, manifestFileVar + "=" + string(f)}, nil +} + +type manifest map[string]string + +func (f ManifestFile) parse() (manifest, error) { + r, err := os.Open(string(f)) + if err != nil { + return nil, fmt.Errorf("runfiles: can’t open manifest file: %w", err) + } + defer r.Close() + + s := bufio.NewScanner(r) + m := make(manifest) + for s.Scan() { + fields := strings.SplitN(s.Text(), " ", 2) + if len(fields) != 2 || fields[0] == "" { + return nil, fmt.Errorf("runfiles: bad manifest line %q in file %s", s.Text(), f) + } + m[fields[0]] = filepath.FromSlash(fields[1]) + } + + if err := s.Err(); err != nil { + return nil, fmt.Errorf("runfiles: error parsing manifest file %s: %w", f, err) + } + + return m, nil +} + +func (m manifest) path(s string) (string, error) { + r, ok := m[s] + if ok && r == "" { + return "", ErrEmpty + } + if ok { + return r, nil + } + + // If path references a runfile that lies under a directory that itself is a + // runfile, then only the directory is listed in the manifest. Look up all + // prefixes of path in the manifest. + for prefix := s; prefix != ""; prefix, _ = path.Split(prefix) { + prefix = strings.TrimSuffix(prefix, "/") + if prefixMatch, ok := m[prefix]; ok { + return prefixMatch + filepath.FromSlash(strings.TrimPrefix(s, prefix)), nil + } + } + + return "", os.ErrNotExist +} diff --git a/go/tools/bazel/runfiles/runfiles.go b/go/tools/bazel/runfiles/runfiles.go new file mode 100644 index 0000000000..c0ebbdb299 --- /dev/null +++ b/go/tools/bazel/runfiles/runfiles.go @@ -0,0 +1,202 @@ +// 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 Path and Env, +// and the Runfiles type. +// +// Global functions +// +// For simple use cases that don’t require hermetic behavior, use the Path and +// Env functions to access runfiles. Use Path 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 Path 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 ( + "errors" + "fmt" + "os" + "path/filepath" + "strings" +) + +const ( + directoryVar = "RUNFILES_DIR" + manifestFileVar = "RUNFILES_MANIFEST_FILE" +) + +// 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 +} + +// 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 + for _, a := range opts { + a.apply(&o) + } + + if o.manifest == "" { + o.manifest = ManifestFile(os.Getenv(manifestFileVar)) + } + if o.manifest != "" { + return o.manifest.new() + } + + if o.directory == "" { + o.directory = Directory(os.Getenv(directoryVar)) + } + if o.directory != "" { + return o.directory.new(), nil + } + + 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() + } + + dir := Directory(o.program + ".runfiles") + if stat, err := os.Stat(string(dir)); err == nil && stat.IsDir() { + return dir.new(), nil + } + + return nil, errors.New("runfiles: no runfiles found") +} + +// Path returns the 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, Path always returns an error. If the runfiles +// manifest maps s to an empty name (indicating an empty runfile not present in the +// filesystem), Path returns an error that wraps ErrEmpty. +// +// See section “Library interface” in +// https://docs.google.com/document/d/e/2PACX-1vSDIrFnFvEYhKsCMdGdD40wZRBX3m3aZ5HhVj4CtHPmiXKDCxioTUbYsDydjKtFDAzER5eg7OjJWs3V/pub. +func (r *Runfiles) Path(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 !isNormalizedPath(path) { + return "", fmt.Errorf("runfiles: path %q is not normalized", path) + } + + // 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 + } + + p, err := r.impl.path(path) + if err != nil { + return "", Error{path, err} + } + return p, nil +} + +func isNormalizedPath(s string) bool { + return !strings.HasPrefix(s, "../") && !strings.Contains(s, "/..") && + !strings.HasPrefix(s, "./") && !strings.HasSuffix(s, "/.") && + !strings.Contains(s, "/./") && !strings.Contains(s, "//") +} + +// 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 { + if r.env == "" { + return nil + } + return []string{r.env} +} + +// 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 + +// 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 +} + +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 } + +type runfiles interface { + path(string) (string, error) +} diff --git a/go/tools/bazel/runfiles/runfiles_test.go b/go/tools/bazel/runfiles/runfiles_test.go new file mode 100644 index 0000000000..c4852bee9b --- /dev/null +++ b/go/tools/bazel/runfiles/runfiles_test.go @@ -0,0 +1,142 @@ +// Copyright 2020, 2021, 2022 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_test + +import ( + "errors" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "github.com/bazelbuild/rules_go/go/tools/bazel/runfiles" +) + +func TestPath_FileLookup(t *testing.T) { + path, err := runfiles.Path("io_bazel_rules_go/go/tools/bazel/runfiles/test.txt") + if err != nil { + t.Fatal(err) + } + b, err := os.ReadFile(path) + if err != nil { + t.Fatal(err) + } + got := strings.TrimSpace(string(b)) + want := "hi!" + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestPath_SubprocessRunfilesLookup(t *testing.T) { + r, err := runfiles.New() + if err != nil { + panic(err) + } + // The binary “testprog” is itself built with Bazel, and needs + // runfiles. + testprogRpath := "io_bazel_rules_go/go/tools/bazel/runfiles/testprog/testprog_/testprog" + if runtime.GOOS == "windows" { + testprogRpath += ".exe" + } + prog, err := r.Path(testprogRpath) + if err != nil { + panic(err) + } + cmd := exec.Command(prog) + // We add r.Env() after os.Environ() so that runfile environment + // variables override anything set in the process environment. + cmd.Env = append(os.Environ(), r.Env()...) + out, err := cmd.Output() + if err != nil { + t.Fatal(err) + } + got := strings.TrimSpace(string(out)) + want := "hi!" + if got != want { + t.Errorf("got %q, want %q", got, want) + } +} + +func TestPath_errors(t *testing.T) { + r, err := runfiles.New() + if err != nil { + t.Fatal(err) + } + for _, s := range []string{"", "/..", "../", "a/../b", "a//b", "a/./b", `\a`} { + t.Run(s, func(t *testing.T) { + if got, err := r.Path(s); err == nil { + t.Errorf("got %q, want error", got) + } + }) + } +} + +func TestRunfiles_zero(t *testing.T) { + var r runfiles.Runfiles + if got, err := r.Path("a"); err == nil { + t.Errorf("Path: got %q, want error", got) + } + if got := r.Env(); got != nil { + t.Errorf("Env: got %v, want nil", got) + } +} + +func TestRunfiles_empty(t *testing.T) { + dir := t.TempDir() + manifest := filepath.Join(dir, "manifest") + if err := os.WriteFile(manifest, []byte("__init__.py \n"), 0o600); err != nil { + t.Fatal(err) + } + r, err := runfiles.New(runfiles.ManifestFile(manifest)) + if err != nil { + t.Fatal(err) + } + _, got := r.Path("__init__.py") + want := runfiles.ErrEmpty + if !errors.Is(got, want) { + t.Errorf("Path for empty file: got error %q, want something that wraps %q", got, want) + } +} + +func TestRunfiles_manifestWithDir(t *testing.T) { + dir := t.TempDir() + manifest := filepath.Join(dir, "manifest") + if err := os.WriteFile(manifest, []byte("foo/dir path/to/foo/dir\n"), 0o600); err != nil { + t.Fatal(err) + } + r, err := runfiles.New(runfiles.ManifestFile(manifest)) + if err != nil { + t.Fatal(err) + } + + for rlocation, want := range map[string]string{ + "foo/dir": filepath.FromSlash("path/to/foo/dir"), + "foo/dir/file": filepath.FromSlash("path/to/foo/dir/file"), + "foo/dir/deeply/nested/file": filepath.FromSlash("path/to/foo/dir/deeply/nested/file"), + } { + t.Run(rlocation, func(t *testing.T) { + got, err := r.Path(rlocation) + if err != nil { + t.Fatalf("Path failed: got unexpected error %q", err) + } + if got != want { + t.Errorf("Path failed: got %q, want %q", got, want) + } + }) + } +} diff --git a/go/tools/bazel/runfiles/test.txt b/go/tools/bazel/runfiles/test.txt new file mode 100644 index 0000000000..32aad8c353 --- /dev/null +++ b/go/tools/bazel/runfiles/test.txt @@ -0,0 +1 @@ +hi! diff --git a/go/tools/bazel/runfiles/testprog/BUILD.bazel b/go/tools/bazel/runfiles/testprog/BUILD.bazel new file mode 100644 index 0000000000..46958bdb1a --- /dev/null +++ b/go/tools/bazel/runfiles/testprog/BUILD.bazel @@ -0,0 +1,30 @@ +# Copyright 2020 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. + +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "testprog_lib", + srcs = ["main.go"], + importpath = "github.com/bazelbuild/rules_go/go/tools/bazel/runfiles/testprog", + visibility = ["//visibility:private"], + deps = ["//go/tools/bazel/runfiles"], +) + +go_binary( + name = "testprog", + data = ["//go/tools/bazel/runfiles:test.txt"], + embed = [":testprog_lib"], + visibility = ["//go/tools/bazel/runfiles:__pkg__"], +) diff --git a/go/tools/bazel/runfiles/testprog/main.go b/go/tools/bazel/runfiles/testprog/main.go new file mode 100644 index 0000000000..5bcd8f5db6 --- /dev/null +++ b/go/tools/bazel/runfiles/testprog/main.go @@ -0,0 +1,34 @@ +// Copyright 2020 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 main + +import ( + "fmt" + "io/ioutil" + + "github.com/bazelbuild/rules_go/go/tools/bazel/runfiles" +) + +func main() { + path, err := runfiles.Path("io_bazel_rules_go/go/tools/bazel/runfiles/test.txt") + if err != nil { + panic(err) + } + b, err := ioutil.ReadFile(path) + if err != nil { + panic(err) + } + fmt.Println(string(b)) +}