diff --git a/cli/bazelisk/bazelisk.go b/cli/bazelisk/bazelisk.go index aeb07df7513b..9e2896df800e 100644 --- a/cli/bazelisk/bazelisk.go +++ b/cli/bazelisk/bazelisk.go @@ -3,13 +3,14 @@ package bazelisk import ( "fmt" "io" - goLog "log" "os" "path/filepath" "strings" "sync" "syscall" + goLog "log" + "github.com/bazelbuild/bazelisk/config" "github.com/bazelbuild/bazelisk/core" "github.com/bazelbuild/bazelisk/repositories" @@ -23,6 +24,43 @@ var ( setVersionErr error ) +// HandleWrapper re-invokes the CLI using the tools/bazel script if present, +// setting BAZEL_REAL to point to the CLI itself. +// +// This needs to be handled by us, rather than bazelisk, in order to support +// passing --config options using tools/bazel. Otherwise, we'd canonicalize args +// before invoking tools/bazel, which sets --ignore_all_rc_files and prevents +// --config flags from working. +// +// Note that this behavior subtly differs from bazelisk in that BAZEL_REAL will +// point to bb, which is a bazel wrapper rather than an actual bazel binary. +// Despite this difference, in practice we expect this to be a net improvement +// in compatibility. +func HandleWrapper() error { + if os.Getenv("BAZELISK_SKIP_WRAPPER") == "true" { + return nil + } + os.Setenv("BAZELISK_SKIP_WRAPPER", "true") + ws, err := workspace.Path() + if err != nil { + return nil + } + scriptPath := filepath.Join(ws, "tools/bazel") + os.Setenv("BAZEL_REAL", os.Args[0]) + + // Try an exec() call to invoke tools/bazel. If tools/bazel exists and is + // executable then the exec call should replace the current process with a + // tools/bazel process which should then call back into `bb` by invoking + // $BAZEL_REAL, which we've set to args[0]. + // + // If tools/bazel doesn't exist or isn't executable then the exec call will + // just fail and we just silently ignore the error, which is what bazelisk + // does as well. + + _ = syscall.Exec(scriptPath, append([]string{scriptPath}, os.Args[1:]...), os.Environ()) + return nil +} + type RunOpts struct { // Stdout is the Writer where bazelisk should write its stdout. // Defaults to os.Stdout if nil. diff --git a/cli/cmd/bb/bb.go b/cli/cmd/bb/bb.go index e0c8c85bb3f4..c6ad42e12ec8 100644 --- a/cli/cmd/bb/bb.go +++ b/cli/cmd/bb/bb.go @@ -46,6 +46,8 @@ func main() { } func run() (exitCode int, err error) { + bazelisk.HandleWrapper() + start := time.Now() // Record original arguments so we can show them in the UI. originalArgs := append([]string{}, os.Args...) diff --git a/cli/metadata/metadata.go b/cli/metadata/metadata.go index f6081d2332bf..29bc41f7bfd9 100644 --- a/cli/metadata/metadata.go +++ b/cli/metadata/metadata.go @@ -94,7 +94,11 @@ func runGit(dir string, args ...string) (output string, err error) { cmd.Dir = dir b, err := cmd.CombinedOutput() if err != nil { - return "", fmt.Errorf("command `git %s` failed: %s", strings.Join(args, " "), string(b)) + msg := strings.TrimSpace(string(b)) + if msg == "" { + msg = err.Error() + } + return "", fmt.Errorf("command `git %s` failed: %s", strings.Join(args, " "), msg) } return strings.TrimSpace(string(b)), nil } diff --git a/cli/test/integration/cli/cli_test.go b/cli/test/integration/cli/cli_test.go index a041cfaa9064..7a4edc55257c 100644 --- a/cli/test/integration/cli/cli_test.go +++ b/cli/test/integration/cli/cli_test.go @@ -71,6 +71,35 @@ func TestInvokeViaBazelisk(t *testing.T) { } } +func TestInvokeViaBazeliskWithToolsBazelWrapper(t *testing.T) { + ws := testcli.NewWorkspace(t) + testfs.WriteAllFileContents(t, ws, map[string]string{ + ".bazelversion": fmt.Sprintf("%s\n%s\n", testcli.BinaryPath(t), testbazel.BinaryPath(t)), + ".bazelrc": ` +build:foo --action_env=EXIT_CODE=0 +`, + "WORKSPACE": "", + "BUILD": ` +load(":defs.bzl", "run_shell") +run_shell(name = "exit_command", command = "exit ${EXIT_CODE:-1}") +`, + // Note: the tools/bazel script passes a --config flag. After expanding + // and canonicalizing flags, we set --ignore_all_rc_files, which means + // that --config flags are no longer allowed. So, this is testing that + // we invoke tools/bazel *after* calling tools/bazel. + "tools/bazel": `#!/bin/sh +exec "$BAZEL_REAL" "$@" --config=foo +`, + }) + + testfs.MakeExecutable(t, ws, "tools/bazel") + + cmd := testcli.BazeliskCommand(t, ws, "--verbose=1", "build", ":all") + b, err := testcli.CombinedOutput(cmd) + + require.NoError(t, err, "output: %s", string(b)) +} + func TestBazelHelp(t *testing.T) { ws := testcli.NewWorkspace(t) cmd := testcli.Command(t, ws, "help", "completion") diff --git a/cli/testutil/testcli/testcli.go b/cli/testutil/testcli/testcli.go index 315160106ff6..2ea195571230 100644 --- a/cli/testutil/testcli/testcli.go +++ b/cli/testutil/testcli/testcli.go @@ -104,6 +104,25 @@ func NewWorkspace(t *testing.T) string { ws := testbazel.MakeTempWorkspace(t, map[string]string{ "WORKSPACE": "", ".bazelversion": testbazel.BinaryPath(t), + // Add some basic rules for convenience. The run_shell rule is helpful + // for running simple bazel actions. (Built-in rules like sh_test and + // genrule are relatively heavy and can slow down test execution time) + "defs.bzl": ` +def _run_shell_impl(ctx): + out = ctx.actions.declare_file(ctx.label.name) + ctx.actions.run_shell( + outputs = [out], + use_default_shell_env = True, # respect --action_env + command = """ + set -e + touch "%s" + %s + """ % (out.path, ctx.attr.command), + ) + return [DefaultInfo(files = depset([out]))] + +run_shell = rule(implementation = _run_shell_impl, attrs = {"command": attr.string()}) +`, }) // Make it a git workspace to test git metadata. testgit.Init(t, ws)