package bazel import ( "bytes" "context" "fmt" "log" "os" "os/exec" "os/signal" syscall "golang.org/x/sys/unix" ) const ( // BuildWorkspaceDir contains the env var set by bazel run for the root of the // workspace running the command. It can be used to run binaries via bazel but // relative to your shell's current working directory BuildWorkspaceDir = "BUILD_WORKSPACE_DIRECTORY" // TestWorkspace contains the environment variable name populated by `bazel test`, // which can be used to determine if the some code is running in // the Bazel sandbox or not TestWorkspace = "TEST_WORKSPACE" // TEST_TMPDIR is the environment variable where Bazel stores a writeable tmp // directory path for sandboxed test execution. // https://docs.bazel.build/versions/master/test-encyclopedia.html TestTmpDir = "TEST_TMPDIR" // RootRepoName is a constant set to `_main` which is the default repo value // for the root repo of a workspace using bzlmod. It is added here to assist // with the retrieval of runfiles from binaries that make use of them for // whatever reason, i.e. `data` dependents in a `go_library` RootRepoName = "_main" ) var workspaceFiles = []string{"WORKSPACE.bazel", "WORKSPACE"} // IsBazelTest returns true if the code is being executed via `bazel test` func IsBazelTest() bool { _, exists := os.LookupEnv(TestWorkspace) return exists } // IsBazelRun returns true if the code is being executed via `bazel run` func IsBazelRun() bool { _, exists := os.LookupEnv(BuildWorkspaceDir) return exists } // Binary is the Bazel binary to exec when running commands. It can be // overridden at runtime using the BAZEL_BINARY_PATH environment variable or // any of the documented controls (https://github.com/bazelbuild/bazelisk) // or standard wrappers (e.g., from Homebrew installations) func Binary() string { if bin, ok := os.LookupEnv("BAZEL_BINARY_PATH"); ok { return bin } return "bazel" } // Build is a single-shot version of [Bazel.Build] func Build(args ...string) *Cmd { return Command(append([]string{"build"}, args...)...) } // Test is a single-shot version of [Bazel.Test] func Test(args ...string) *Cmd { return Command(append([]string{"test"}, args...)...) } // Run is a single-shot version of [Bazel.Run] func Run(args ...string) *Cmd { return Command(append([]string{"run"}, args...)...) } // Bazel is a command runner for invoking `bazel` with consistent context, // `bazel` binary, and `--config` flags. type Bazel struct { ctx context.Context // Bazel configuration profiles passed to executed commands via --config // https://docs.bazel.build/versions/master/guide.html#--config configs Configs // Bazel binary to execute bin string } func New(opts ...Option) *Bazel { o := &options{ctx: setupSignalHandler(), bin: Binary()} for _, opt := range opts { opt(o) } return &Bazel{o.ctx, o.configs, o.bin} } func (b *Bazel) Cmd(args ...string) *Cmd { // insert our `--config` flags after the bazel subcommand arg and before // user input // https://github.com/golang/go/wiki/SliceTricks#insert args = append(args[:1], append(b.configs.ToArgs(), args[1:]...)...) return makeBazelCmd(b.ctx, b.bin, args...) } func (b *Bazel) Run(args ...string) *Cmd { return b.Cmd(append([]string{"run"}, args...)...) } func (b *Bazel) Build(args ...string) *Cmd { return b.Cmd(append([]string{"build"}, args...)...) } func (b *Bazel) Test(args ...string) *Cmd { return b.Cmd(append([]string{"test"}, args...)...) } // Cmd is a Bazel command. It wraps [exec.Cmd] to provide consistent Stderr/Stdout // and error handling. type Cmd struct { *exec.Cmd } // Command creates a new [Cmd] using [Binary] in the current working directory, // inheriting the current process' environment, and signal handling, for one-off // Bazel commands. func Command(args ...string) *Cmd { return makeBazelCmd(setupSignalHandler(), Binary(), args...) } // Output executes a [Cmd], capturing Stdout/Stderr and returning Stdout. If // the command is unsuccessful, an [exec.ExitError] is returned containing the // Stderr output. func (c *Cmd) Output() ([]byte, error) { // Capture stderr so we can include it in any returned error. os/exec docs // indicate this should be handled automatically by Output(), but isn't // :shrug: var stderr bytes.Buffer c.Stderr = &stderr output, err := c.Cmd.Output() if err != nil { return nil, fmt.Errorf("failed to run '%s': %w: %s", c.Cmd.String(), err, stderr.String()) } return output, nil } // Run executes a [Cmd], sending output to [os.Stdout] and [os.Stderr]. func (c *Cmd) Run() error { c.Cmd.Stdout = os.Stdout c.Cmd.Stderr = os.Stderr if err := c.Cmd.Run(); err != nil { return fmt.Errorf("failed to run '%s': %w", c.String(), err) } return nil } // creates exec.Cmd for Bazel, setting correct working directory and inheriting // the environment func makeBazelCmd(ctx context.Context, bazel string, args ...string) *Cmd { // unrecoverable error, also really hard to try and run a command // without a command and at least one argument if len(args) == 0 { panic("bazel.Cmd: no command provided") } cmd := exec.CommandContext(ctx, bazel, args...) // Inherit process environment and working directory cmd.Env = os.Environ() dir, err := ResolveWd() if err != nil { log.Fatal(err) } cmd.Dir = dir return &Cmd{cmd} } // setupSignalHandler registers for SIGTERM and SIGINT. A context is returned // which is canceled on one of these signals. func setupSignalHandler() context.Context { ctx, cancel := context.WithCancel(context.Background()) c := make(chan os.Signal, 2) signal.Notify(c, os.Interrupt, syscall.SIGTERM) go func() { <-c cancel() <-c os.Exit(1) // second signal. Exit directly. }() return ctx }