...

Source file src/edge-infra.dev/pkg/lib/build/bazel/bazel.go

Documentation: edge-infra.dev/pkg/lib/build/bazel

     1  package bazel
     2  
     3  import (
     4  	"bytes"
     5  	"context"
     6  	"fmt"
     7  	"log"
     8  	"os"
     9  	"os/exec"
    10  	"os/signal"
    11  
    12  	syscall "golang.org/x/sys/unix"
    13  )
    14  
    15  const (
    16  	// BuildWorkspaceDir contains the env var set by bazel run for the root of the
    17  	// workspace running the command.  It can be used to run binaries via bazel but
    18  	// relative to your shell's current working directory
    19  	BuildWorkspaceDir = "BUILD_WORKSPACE_DIRECTORY"
    20  
    21  	// TestWorkspace contains the environment variable name populated by `bazel test`,
    22  	// which can be used to determine if the some code is running in
    23  	// the Bazel sandbox or not
    24  	TestWorkspace = "TEST_WORKSPACE"
    25  
    26  	// TEST_TMPDIR is the environment variable where Bazel stores a writeable tmp
    27  	// directory path for sandboxed test execution.
    28  	// https://docs.bazel.build/versions/master/test-encyclopedia.html
    29  	TestTmpDir = "TEST_TMPDIR"
    30  
    31  	// RootRepoName is a constant set to `_main` which is the default repo value
    32  	// for the root repo of a workspace using bzlmod. It is added here to assist
    33  	// with the retrieval of runfiles from binaries that make use of them for
    34  	// whatever reason, i.e. `data` dependents in a `go_library`
    35  	RootRepoName = "_main"
    36  )
    37  
    38  var workspaceFiles = []string{"WORKSPACE.bazel", "WORKSPACE"}
    39  
    40  // IsBazelTest returns true if the code is being executed via `bazel test`
    41  func IsBazelTest() bool {
    42  	_, exists := os.LookupEnv(TestWorkspace)
    43  	return exists
    44  }
    45  
    46  // IsBazelRun returns true if the code is being executed via `bazel run`
    47  func IsBazelRun() bool {
    48  	_, exists := os.LookupEnv(BuildWorkspaceDir)
    49  	return exists
    50  }
    51  
    52  // Binary is the Bazel binary to exec when running commands. It can be
    53  // overridden at runtime using the BAZEL_BINARY_PATH environment variable or
    54  // any of the documented controls (https://github.com/bazelbuild/bazelisk)
    55  // or standard wrappers (e.g., from Homebrew installations)
    56  func Binary() string {
    57  	if bin, ok := os.LookupEnv("BAZEL_BINARY_PATH"); ok {
    58  		return bin
    59  	}
    60  	return "bazel"
    61  }
    62  
    63  // Build is a single-shot version of [Bazel.Build]
    64  func Build(args ...string) *Cmd {
    65  	return Command(append([]string{"build"}, args...)...)
    66  }
    67  
    68  // Test is a single-shot version of [Bazel.Test]
    69  func Test(args ...string) *Cmd {
    70  	return Command(append([]string{"test"}, args...)...)
    71  }
    72  
    73  // Run is a single-shot version of [Bazel.Run]
    74  func Run(args ...string) *Cmd {
    75  	return Command(append([]string{"run"}, args...)...)
    76  }
    77  
    78  // Bazel is a command runner for invoking `bazel` with consistent context,
    79  // `bazel` binary, and `--config` flags.
    80  type Bazel struct {
    81  	ctx context.Context
    82  	// Bazel configuration profiles passed to executed commands via --config
    83  	// https://docs.bazel.build/versions/master/guide.html#--config
    84  	configs Configs
    85  	// Bazel binary to execute
    86  	bin string
    87  }
    88  
    89  func New(opts ...Option) *Bazel {
    90  	o := &options{ctx: setupSignalHandler(), bin: Binary()}
    91  	for _, opt := range opts {
    92  		opt(o)
    93  	}
    94  
    95  	return &Bazel{o.ctx, o.configs, o.bin}
    96  }
    97  
    98  func (b *Bazel) Cmd(args ...string) *Cmd {
    99  	// insert our `--config` flags after the bazel subcommand arg and before
   100  	// user input
   101  	// https://github.com/golang/go/wiki/SliceTricks#insert
   102  	args = append(args[:1], append(b.configs.ToArgs(), args[1:]...)...)
   103  
   104  	return makeBazelCmd(b.ctx, b.bin, args...)
   105  }
   106  
   107  func (b *Bazel) Run(args ...string) *Cmd {
   108  	return b.Cmd(append([]string{"run"}, args...)...)
   109  }
   110  
   111  func (b *Bazel) Build(args ...string) *Cmd {
   112  	return b.Cmd(append([]string{"build"}, args...)...)
   113  }
   114  
   115  func (b *Bazel) Test(args ...string) *Cmd {
   116  	return b.Cmd(append([]string{"test"}, args...)...)
   117  }
   118  
   119  // Cmd is a Bazel command. It wraps [exec.Cmd] to provide consistent Stderr/Stdout
   120  // and error handling.
   121  type Cmd struct {
   122  	*exec.Cmd
   123  }
   124  
   125  // Command creates a new [Cmd] using [Binary] in the current working directory,
   126  // inheriting the current process' environment, and signal handling, for one-off
   127  // Bazel commands.
   128  func Command(args ...string) *Cmd {
   129  	return makeBazelCmd(setupSignalHandler(), Binary(), args...)
   130  }
   131  
   132  // Output executes a [Cmd], capturing Stdout/Stderr and returning Stdout. If
   133  // the command is unsuccessful, an [exec.ExitError] is returned containing the
   134  // Stderr output.
   135  func (c *Cmd) Output() ([]byte, error) {
   136  	// Capture stderr so we can include it in any returned error. os/exec docs
   137  	// indicate this should be handled automatically by Output(), but isn't
   138  	// :shrug:
   139  	var stderr bytes.Buffer
   140  	c.Stderr = &stderr
   141  
   142  	output, err := c.Cmd.Output()
   143  	if err != nil {
   144  		return nil, fmt.Errorf("failed to run '%s': %w: %s",
   145  			c.Cmd.String(), err, stderr.String())
   146  	}
   147  	return output, nil
   148  }
   149  
   150  // Run executes a [Cmd], sending output to [os.Stdout] and [os.Stderr].
   151  func (c *Cmd) Run() error {
   152  	c.Cmd.Stdout = os.Stdout
   153  	c.Cmd.Stderr = os.Stderr
   154  
   155  	if err := c.Cmd.Run(); err != nil {
   156  		return fmt.Errorf("failed to run '%s': %w", c.String(), err)
   157  	}
   158  	return nil
   159  }
   160  
   161  // creates exec.Cmd for Bazel, setting correct working directory and inheriting
   162  // the environment
   163  func makeBazelCmd(ctx context.Context, bazel string, args ...string) *Cmd {
   164  	// unrecoverable error, also really hard to try and run a command
   165  	// without a command and at least one argument
   166  	if len(args) == 0 {
   167  		panic("bazel.Cmd: no command provided")
   168  	}
   169  
   170  	cmd := exec.CommandContext(ctx, bazel, args...)
   171  
   172  	// Inherit process environment and working directory
   173  	cmd.Env = os.Environ()
   174  
   175  	dir, err := ResolveWd()
   176  	if err != nil {
   177  		log.Fatal(err)
   178  	}
   179  	cmd.Dir = dir
   180  
   181  	return &Cmd{cmd}
   182  }
   183  
   184  // setupSignalHandler registers for SIGTERM and SIGINT. A context is returned
   185  // which is canceled on one of these signals.
   186  func setupSignalHandler() context.Context {
   187  	ctx, cancel := context.WithCancel(context.Background())
   188  
   189  	c := make(chan os.Signal, 2)
   190  	signal.Notify(c, os.Interrupt, syscall.SIGTERM)
   191  	go func() {
   192  		<-c
   193  		cancel()
   194  		<-c
   195  		os.Exit(1) // second signal. Exit directly.
   196  	}()
   197  
   198  	return ctx
   199  }
   200  

View as plain text