Skip to content

Commit

Permalink
Add functions to locate the runfiles tree of a binary (#1331)
Browse files Browse the repository at this point in the history
This change adds a bunch of Bazel-specific functions to locate the
runfiles trees of built binaries and to find binaries within them.

This extra logic is required to support the recent change in the Go
rules that causes the paths to built Go binaries to not be deterministic
(issue #1239).
  • Loading branch information
jmmv authored and jayconrod committed Feb 16, 2018
1 parent c10f500 commit dd3c631
Show file tree
Hide file tree
Showing 3 changed files with 309 additions and 1 deletion.
4 changes: 3 additions & 1 deletion go/tools/bazel/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,6 @@ This directory contains useful utilities for interacting with Bazel from Go.

Currently the `bazel` package supports:

1. Getting the path for a runfile in a test.
* Getting the path for a runfile in a test.

* Finding and entering the location of the runfiles of a binary.
68 changes: 68 additions & 0 deletions go/tools/bazel/bazel.go
Original file line number Diff line number Diff line change
Expand Up @@ -103,3 +103,71 @@ func TestWorkspace() (string, error) {
func SetDefaultTestWorkspace(w string) {
defaultTestWorkspace = w
}

// getCandidates returns the list of all possible "prefix/suffix" paths where there might be an
// optional component in-between the two pieces.
//
// This function exists to cope with issues #1239 because we cannot tell where the built Go
// binaries are located upfront.
func getCandidates(prefix string, suffix string) []string {
candidates := []string{filepath.Join(prefix, suffix)}
if entries, err := ioutil.ReadDir(prefix); err == nil {
for _, entry := range entries {
candidate := filepath.Join(prefix, entry.Name(), suffix)
candidates = append(candidates, candidate)
}
}
return candidates
}

// FindBinary locates the given executable within bazel-bin or the current directory.
//
// "pkg" indicates the relative path to the build package that contains the binary target, and
// "binary" indicates the basename of the binary searched for.
func FindBinary(pkg string, binary string) (string, bool) {
candidates := getCandidates(filepath.Join("bazel-bin", pkg), binary)
candidates = append(candidates, getCandidates(pkg, binary)...)

for _, candidate := range candidates {
// Following symlinks here is intentional because Bazel generates symlinks in
// general and we don't care about that.
if fileInfo, err := os.Stat(candidate); err == nil {
if fileInfo.Mode()&os.ModeType == 0 && fileInfo.Mode()&0100 != 0 {
return candidate, true
}
}
}
return "", false
}

// findRunfiles locates the directory under which a built binary can find its data dependencies
// using relative paths.
func findRunfiles(workspace string, pkg string, binary string, cookie string) (string, bool) {
candidates := getCandidates(filepath.Join("bazel-bin", pkg), filepath.Join(binary+".runfiles", workspace))
candidates = append(candidates, ".")

for _, candidate := range candidates {
if _, err := os.Stat(filepath.Join(candidate, cookie)); err == nil {
return candidate, true
}
}
return "", false
}

// EnterRunfiles locates the directory under which a built binary can find its data dependencies
// using relative paths, and enters that directory.
//
// "workspace" indicates the name of the current project, "pkg" indicates the relative path to the
// build package that contains the binary target, "binary" indicates the basename of the binary
// searched for, and "cookie" indicates an arbitrary data file that we expect to find within the
// runfiles tree.
func EnterRunfiles(workspace string, pkg string, binary string, cookie string) error {
runfiles, ok := findRunfiles(workspace, pkg, binary, cookie)
if !ok {
return fmt.Errorf("cannot find runfiles tree")
}
if err := os.Chdir(runfiles); err != nil {
return fmt.Errorf("cannot enter runfiles tree: %v", err)
}
return nil
}
238 changes: 238 additions & 0 deletions go/tools/bazel/bazel_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,10 +14,61 @@
package bazel

import (
"fmt"
"io/ioutil"
"os"
"strings"
"testing"
)

// makeAndEnterTempdir creates a temporary directory and chdirs into it.
func makeAndEnterTempdir() (func(), error) {
oldCwd, err := os.Getwd()
if err != nil {
return nil, fmt.Errorf("cannot get path to current directory: %v", err)
}

tempDir, err := ioutil.TempDir("", "test")
if err != nil {
return nil, fmt.Errorf("failed to create temporary directory: %v", err)
}

err = os.Chdir(tempDir)
if err != nil {
os.RemoveAll(tempDir)
return nil, fmt.Errorf("cannot enter temporary directory %s: %v", tempDir, err)
}

cleanup := func() {
defer os.RemoveAll(tempDir)
defer os.Chdir(oldCwd)
}
return cleanup, nil
}

// createPaths creates a collection of paths for testing purposes. Paths can end with a /, in
// which case a directory is created; or they can end with a *, in which case an executable file
// is created. (This matches the nomenclature of "ls -F".)
func createPaths(paths []string) error {
for _, path := range paths {
if strings.HasSuffix(path, "/") {
if err := os.MkdirAll(path, 0755); err != nil {
return fmt.Errorf("failed to create directory %s: %v", path, err)
}
} else {
mode := os.FileMode(0644)
if strings.HasSuffix(path, "*") {
path = path[0 : len(path)-1]
mode |= 0111
}
if err := ioutil.WriteFile(path, []byte{}, mode); err != nil {
return fmt.Errorf("failed to create file %s with mode %v: %v", path, mode, err)
}
}
}
return nil
}

func TestRunfile(t *testing.T) {
file := "go/tools/bazel/README.md"
runfile, err := Runfile(file)
Expand Down Expand Up @@ -112,3 +163,190 @@ func TestTestWorkspaceWithDefaultSet(t *testing.T) {
t.Errorf("Unable to get workspace with error %s", err)
}
}

func TestFindBinary(t *testing.T) {
testData := []struct {
name string

pathsToCreate []string
wantBinary string
wantOk bool
}{
{
"NoFiles",
[]string{},
"",
false,
},
{
"CurrentDirectoryNoConfigurationInPath",
[]string{
"some/package/",
"some/package/bin*",
},
"some/package/bin",
true,
},
{
"CurrentDirectoryConfigurationInPath",
[]string{
"some/package/amd64/",
"some/package/arm64/",
"some/package/arm64/bin*",
"some/package/powerpc/",
},
"some/package/arm64/bin",
true,
},
{
"BazelBinNoConfigurationInPath",
[]string{
"bazel-bin/some/package/",
"bazel-bin/some/package/bin*",
"bin", // bazel-bin should be preferred.
},
"bazel-bin/some/package/bin",
true,
},
{
"BazelBinConfigurationInPath",
[]string{
"bazel-bin/some/package/amd64/",
"bazel-bin/some/package/arm64/",
"bazel-bin/some/package/arm64/bin*",
"bazel-bin/some/package/powerpc/",
"bin", // bazel-bin should be preferred.
"some/package/amd64/",
"some/package/amd64/bin", // bazel-bin should be preferred.
},
"bazel-bin/some/package/arm64/bin",
true,
},
{
"IgnoreNonExecutable",
[]string{
"bazel-bin/some/package/amd64/",
"bazel-bin/some/package/amd64/bin",
"bazel-bin/some/package/arm64/",
"bazel-bin/some/package/arm64/bin*",
"bazel-bin/some/package/powerpc/",
"bazel-bin/some/package/powerpc/bin",
},
"bazel-bin/some/package/arm64/bin",
true,
},
}
for _, d := range testData {
t.Run(d.name, func(t *testing.T) {
cleanup, err := makeAndEnterTempdir()
if err != nil {
t.Fatal(err)
}
defer cleanup()

if err := createPaths(d.pathsToCreate); err != nil {
t.Fatal(err)
}

binary, ok := FindBinary("some/package", "bin")
if binary != d.wantBinary || ok != d.wantOk {
t.Errorf("Got %s, %v; want %s, %v", binary, ok, d.wantBinary, d.wantOk)
}
})
}
}

func TestFindRunfiles(t *testing.T) {
testData := []struct {
name string

pathsToCreate []string
wantRunfiles string
wantOk bool
}{
{
"NoFiles",
[]string{},
"",
false,
},
{
"CurrentDirectory",
[]string{
"data-file",
},
".",
true,
},
{
"BazelBinNoConfigurationInPath",
[]string{
"bazel-bin/some/package/bin.runfiles/project/",
"bazel-bin/some/package/bin.runfiles/project/data-file",
"data-file", // bazel-bin should be preferred.
},
"bazel-bin/some/package/bin.runfiles/project",
true,
},
{
"BazelBinConfigurationInPath",
[]string{
"bazel-bin/some/package/amd64/bin.runfiles/project/",
"bazel-bin/some/package/arm64/bin.runfiles/project/",
"bazel-bin/some/package/arm64/bin.runfiles/project/data-file",
"bazel-bin/some/package/powerpc/bin.runfiles/project/",
"data-file", // bazel-bin should be preferred.
},
"bazel-bin/some/package/arm64/bin.runfiles/project",
true,
},
}
for _, d := range testData {
t.Run(d.name, func(t *testing.T) {
cleanup, err := makeAndEnterTempdir()
if err != nil {
t.Fatal(err)
}
defer cleanup()

if err := createPaths(d.pathsToCreate); err != nil {
t.Fatal(err)
}

runfiles, ok := findRunfiles("project", "some/package", "bin", "data-file")
if runfiles != d.wantRunfiles || ok != d.wantOk {
t.Errorf("Got %s, %v; want %s, %v", runfiles, ok, d.wantRunfiles, d.wantOk)
}
})
}
}

func TestEnterRunfiles(t *testing.T) {
cleanup, err := makeAndEnterTempdir()
if err != nil {
t.Fatal(err)
}
defer cleanup()

pathsToCreate := []string{
"bazel-bin/some/package/bin.runfiles/project/",
"bazel-bin/some/package/bin.runfiles/project/data-file",
}
if err := createPaths(pathsToCreate); err != nil {
t.Fatal(err)
}

if err := EnterRunfiles("project", "some/package", "bin", "data-file"); err != nil {
t.Fatalf("Cannot enter runfiles tree: %v", err)
}
// The cleanup routine returned by makeAndEnterTempdir restores the working directory from
// the beginning of the test, so we don't have to worry about it here.

if _, err := os.Lstat("data-file"); err != nil {
wd, err := os.Getwd()
if err != nil {
t.Errorf("failed to get current working directory: %v", err)
}
t.Errorf("data-file not found in current directory (%s); entered invalid runfiles tree?", wd)
}
}

0 comments on commit dd3c631

Please sign in to comment.