Skip to content

Commit

Permalink
Create an interface for executing Bazel commands
Browse files Browse the repository at this point in the history
This makes it easier to plug the Target Determinator into larger
applications that already have tight control over how Bazel is executed
(eg. when using this as a plugin of Aspect's CLI tool).
  • Loading branch information
shs96c authored and sitaktif committed Jul 4, 2022
1 parent d4ef4cc commit 8fb39d6
Show file tree
Hide file tree
Showing 5 changed files with 91 additions and 39 deletions.
6 changes: 4 additions & 2 deletions cli/flags.go
Original file line number Diff line number Diff line change
Expand Up @@ -137,15 +137,17 @@ func ResolveCommonConfig(commonFlags *CommonFlags, beforeRevStr string) (*Common
return nil, fmt.Errorf("failed to resolve the \"after\" (i.e. original) git revision: %w", err)
}

outputBase, err := pkg.BazelOutputBase(*commonFlags.BazelPath, workingDirectory)
bazelCmd := pkg.DefaultBazelCmd{BazelPath: *commonFlags.BazelPath}

outputBase, err := pkg.BazelOutputBase(workingDirectory, bazelCmd)
if err != nil {
return nil, fmt.Errorf("failed to resolve the bazel output base: %w", err)
}

context := &pkg.Context{
WorkspacePath: workingDirectory,
OriginalRevision: afterRev,
BazelPath: *commonFlags.BazelPath,
BazelCmd: bazelCmd,
BazelOutputBase: outputBase,
DeleteCachedWorktree: commonFlags.DeleteCachedWorktree,
IgnoredFiles: *commonFlags.IgnoredFiles,
Expand Down
11 changes: 5 additions & 6 deletions driver/driver.go
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"strings"

Expand Down Expand Up @@ -110,11 +109,11 @@ func main() {
}

log.Printf("Running %s on %d targets", commandVerb, len(targets))
cmd := exec.Command(config.Context.BazelPath, commandVerb, "--target_pattern_file", targetPatternFile.Name())
cmd.Dir = config.Context.WorkspacePath
cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr
if err := cmd.Run(); err != nil {
result, err := config.Context.BazelCmd.Execute(
pkg.BazelCmdConfig{Dir: config.Context.WorkspacePath, Stdout: os.Stdout, Stderr: os.Stderr},
commandVerb, "--target_pattern_file", targetPatternFile.Name())

if result != 0 || err != nil {
log.Fatal(err)
}
}
Expand Down
1 change: 1 addition & 0 deletions pkg/BUILD.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test")
go_library(
name = "pkg",
srcs = [
"bazel.go",
"hash_cache.go",
"target_determinator.go",
"targets_list.go",
Expand Down
44 changes: 44 additions & 0 deletions pkg/bazel.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package pkg

import (
"io"
"os/exec"
)

type BazelCmdConfig struct {
// Dir represents the working directory to use for the command.
// If Dir is the empty string, use the calling process's current directory.
Dir string

// Stdout and Stderr specify the process's standard output and error.
// A nil value redirects the output to /dev/null.
// The behavior is the same as the exec.Command struct.
Stdout io.Writer
Stderr io.Writer
}

type BazelCmd interface {
Execute(config BazelCmdConfig, args ...string) (int, error)
}

type DefaultBazelCmd struct {
BazelPath string
}

// Execute calls bazel with the provided arguments.
// It returns the exit status code or -1 if it errored before the process could start.
func (c DefaultBazelCmd) Execute(config BazelCmdConfig, args ...string) (int, error) {
cmd := exec.Command(c.BazelPath, args...)
cmd.Dir = config.Dir
cmd.Stdout = config.Stdout
cmd.Stderr = config.Stderr

if err := cmd.Run(); err != nil {
if exitError, ok := err.(*exec.ExitError); ok {
return exitError.ExitCode(), err
} else {
return -1, err
}
}
return 0, nil
}
68 changes: 37 additions & 31 deletions pkg/target_determinator.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,8 +100,8 @@ type Context struct {
WorkspacePath string
// OriginalRevision is the git revision the repo was in when initializing the context.
OriginalRevision LabelledGitRev
// BazelPath is the path (or basename to be looked up in $PATH) of the Bazel to invoke.
BazelPath string
// BazelCmd is used to execute when necessary Bazel.
BazelCmd BazelCmd
// BazelOutputBase is the path of the Bazel output base directory of the original workspace.
BazelOutputBase string
// DeleteCachedWorktree represents whether we should keep worktrees around for reuse in future invocations.
Expand Down Expand Up @@ -160,7 +160,7 @@ func LoadIncompleteMetadata(context *Context, rev LabelledGitRev, targets Target
context = &Context{
WorkspacePath: context.WorkspacePath,
OriginalRevision: context.OriginalRevision,
BazelPath: context.BazelPath,
BazelCmd: context.BazelCmd,
BazelOutputBase: context.BazelOutputBase,
DeleteCachedWorktree: context.DeleteCachedWorktree,
IgnoredFiles: context.IgnoredFiles,
Expand Down Expand Up @@ -513,10 +513,12 @@ func clearAnalysisCache(context *Context) error {
// Discard the analysis cache:
{
var stderr bytes.Buffer
cmd := exec.Command(context.BazelPath, "--output_base", context.BazelOutputBase, "build", "--discard_analysis_cache")
cmd.Dir = context.WorkspacePath
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {

result, err := context.BazelCmd.Execute(
BazelCmdConfig{Dir: context.WorkspacePath, Stderr: &stderr},
"--output_base", context.BazelOutputBase, "build", "--discard_analysis_cache")

if result != 0 || err != nil {
return fmt.Errorf("failed to discard Bazel analysis cache in %v: %w. Stderr from Bazel ↓↓\n%v", context.WorkspacePath, err, stderr.String())
}
}
Expand All @@ -525,39 +527,42 @@ func clearAnalysisCache(context *Context) error {
// Perform a no-op build to flush any in-build state from the previous one.
{
var stderr bytes.Buffer
cmd := exec.Command(context.BazelPath, "--output_base", context.BazelOutputBase, "build")
cmd.Dir = context.WorkspacePath
cmd.Stderr = &stderr
if err := cmd.Run(); err != nil {

result, err := context.BazelCmd.Execute(
BazelCmdConfig{Dir: context.WorkspacePath, Stderr: &stderr},
"--output_base", context.BazelOutputBase, "build")

if result != 0 || err != nil {
return fmt.Errorf("failed to run no-op build after discarding Bazel analysis cache in %v: %w. Stderr:\n%v",
context.WorkspacePath, err, stderr.String())
}
}
return nil
}

func BazelOutputBase(bazelPath string, workspacePath string) (string, error) {
return bazelInfo(bazelPath, workspacePath, "output_base")
func BazelOutputBase(workingDirectory string, BazelCmd BazelCmd) (string, error) {
return bazelInfo(workingDirectory, BazelCmd, "output_base")
}

func BazelRelease(bazelPath string, workspacePath string) (string, error) {
return bazelInfo(bazelPath, workspacePath, "release")
func BazelRelease(workingDirectory string, BazelCmd BazelCmd) (string, error) {
return bazelInfo(workingDirectory, BazelCmd, "release")
}

func bazelInfo(bazelPath string, workspacePath string, key string) (string, error) {
func bazelInfo(workingDirectory string, bazelCmd BazelCmd, key string) (string, error) {
var stdoutBuf, stderrBuf bytes.Buffer
cmd := exec.Command(bazelPath, "info", key)
cmd.Dir = workspacePath
cmd.Stdout = &stdoutBuf
cmd.Stderr = &stderrBuf
if err := cmd.Run(); err != nil {
return "", fmt.Errorf("failed to get the Bazel %v for the %v: %w. Stderr:\n%v", key, workspacePath, err, stderrBuf.String())

result, err := bazelCmd.Execute(
BazelCmdConfig{Dir: workingDirectory, Stdout: &stdoutBuf, Stderr: &stderrBuf},
"info", key)

if result != 0 || err != nil {
return "", fmt.Errorf("failed to get the Bazel %v: %w. Stderr:\n%v", key, err, stderrBuf.String())
}
return strings.TrimRight(stdoutBuf.String(), "\n"), nil
}

func doQueryDeps(context *Context, targets TargetsList) (*QueryResults, error) {
bazelRelease, err := BazelRelease(context.BazelPath, context.WorkspacePath)
bazelRelease, err := BazelRelease(context.WorkspacePath, context.BazelCmd)
if err != nil {
return nil, fmt.Errorf("failed to resolve the bazel release: %w", err)
}
Expand Down Expand Up @@ -613,21 +618,22 @@ func doQueryDeps(context *Context, targets TargetsList) (*QueryResults, error) {

func runToCqueryResult(context *Context, pattern string) (*analysis.CqueryResult, error) {
log.Printf("Running cquery on %s", pattern)
var output bytes.Buffer
var stdout bytes.Buffer
var stderr bytes.Buffer
queryCmd := exec.Command(context.BazelPath, "--output_base", context.BazelOutputBase, "cquery", "--output=proto", pattern)
queryCmd.Dir = context.WorkspacePath
queryCmd.Stdout = &output
queryCmd.Stderr = &stderr
if err := queryCmd.Run(); err != nil {

returnVal, err := context.BazelCmd.Execute(
BazelCmdConfig{Dir: context.WorkspacePath, Stdout: &stdout, Stderr: &stderr},
"--output_base", context.BazelOutputBase, "cquery", "--output=proto", pattern)

if returnVal != 0 || err != nil {
return nil, fmt.Errorf("failed to run cquery on %s: %w. Stderr:\n%v", pattern, err, stderr.String())
}

content := output.Bytes()
content := stdout.Bytes()

var result analysis.CqueryResult
if err := proto.Unmarshal(content, &result); err != nil {
return nil, fmt.Errorf("failed to unmarshal cquery output: %w", err)
return nil, fmt.Errorf("failed to unmarshal cquery stdout: %w", err)
}
return &result, nil
}
Expand Down

0 comments on commit 8fb39d6

Please sign in to comment.