From c31bf06984bff1e8ca527e3cc313f0231d07a8d6 Mon Sep 17 00:00:00 2001 From: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com> Date: Sat, 11 Dec 2021 11:29:53 -0800 Subject: [PATCH] feat: invoke inside workspace interceptor (#83) * feat: implement WorkspaceFinder * test: added TestWorkspaceFinder * fix: implementation, tests and usage Signed-off-by: Thulio Ferraz Assis Co-authored-by: Chai Varier --- .gitignore | 1 + cmd/aspect/build/BUILD.bazel | 1 + cmd/aspect/build/build.go | 8 +- cmd/aspect/clean/BUILD.bazel | 3 + cmd/aspect/clean/clean.go | 27 +++- cmd/aspect/info/BUILD.bazel | 1 + cmd/aspect/info/info.go | 3 +- cmd/aspect/test/BUILD.bazel | 1 + cmd/aspect/test/test.go | 11 +- pkg/aspect/build/build.go | 4 +- pkg/aspect/build/build_test.go | 18 +-- pkg/aspect/clean/clean.go | 8 +- pkg/aspect/clean/clean_test.go | 38 ++--- pkg/aspect/info/info.go | 3 +- pkg/aspect/test/test.go | 8 +- pkg/aspect/test/test_test.go | 7 +- pkg/bazel/BUILD.bazel | 12 +- pkg/bazel/bazel.go | 64 ++++++-- pkg/bazel/bazel_test.go | 23 --- pkg/bazel/bazelisk.go | 188 ++++++++++-------------- pkg/bazel/flags.go | 49 ------- pkg/bazel/mock/BUILD.bazel | 10 +- pkg/pathutils/BUILD.bazel | 23 +++ pkg/pathutils/mock/BUILD.bazel | 26 ++++ pkg/pathutils/mock/doc.go | 2 + pkg/pathutils/pathutils.go | 93 ++++++++++++ pkg/pathutils/pathutils_test.go | 249 ++++++++++++++++++++++++++++++++ pkg/stdlib/mock/BUILD.bazel | 1 + pkg/stdlib/remap.go | 7 +- 29 files changed, 620 insertions(+), 269 deletions(-) delete mode 100644 pkg/bazel/bazel_test.go delete mode 100644 pkg/bazel/flags.go create mode 100644 pkg/pathutils/BUILD.bazel create mode 100644 pkg/pathutils/mock/BUILD.bazel create mode 100644 pkg/pathutils/mock/doc.go create mode 100644 pkg/pathutils/pathutils.go create mode 100644 pkg/pathutils/pathutils_test.go diff --git a/.gitignore b/.gitignore index 01fb9d4d5..ade7945c6 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /bazel-* /.idea /.cache +/.ijwb diff --git a/cmd/aspect/build/BUILD.bazel b/cmd/aspect/build/BUILD.bazel index 6303ee70c..f21f90b7c 100644 --- a/cmd/aspect/build/BUILD.bazel +++ b/cmd/aspect/build/BUILD.bazel @@ -11,6 +11,7 @@ go_library( "//pkg/aspecterrors", "//pkg/bazel", "//pkg/ioutils", + "//pkg/pathutils", "//pkg/plugin/system", "//pkg/plugin/system/bep", "@com_github_spf13_cobra//:cobra", diff --git a/cmd/aspect/build/build.go b/cmd/aspect/build/build.go index 48836b4b9..4d5769d70 100644 --- a/cmd/aspect/build/build.go +++ b/cmd/aspect/build/build.go @@ -17,6 +17,7 @@ import ( "aspect.build/cli/pkg/aspecterrors" "aspect.build/cli/pkg/bazel" "aspect.build/cli/pkg/ioutils" + "aspect.build/cli/pkg/pathutils" "aspect.build/cli/pkg/plugin/system" "aspect.build/cli/pkg/plugin/system/bep" ) @@ -35,14 +36,14 @@ func NewDefaultBuildCmd(pluginSystem system.PluginSystem) *cobra.Command { func NewBuildCmd( streams ioutils.Streams, pluginSystem system.PluginSystem, - bzl bazel.Spawner, + bzl bazel.Bazel, ) *cobra.Command { cmd := &cobra.Command{ Use: "build", Short: "Builds the specified targets, using the options.", Long: "Invokes bazel build on the specified targets. " + "See 'bazel help target-syntax' for details and examples on how to specify targets to build.", - RunE: func(cmd *cobra.Command, args []string) (exitErr error) { + RunE: pathutils.InvokeCmdInsideWorkspace(func(workspaceRoot string, cmd *cobra.Command, args []string) (exitErr error) { isInteractiveMode, err := cmd.Root().PersistentFlags().GetBool(rootFlags.InteractiveFlagName) if err != nil { return err @@ -62,11 +63,12 @@ func NewBuildCmd( } }() + bzl.SetWorkspaceRoot(workspaceRoot) b := build.New(streams, bzl) return pluginSystem.WithBESBackend(cmd.Context(), func(besBackend bep.BESBackend) error { return b.Run(args, besBackend) }) - }, + }), } return cmd diff --git a/cmd/aspect/clean/BUILD.bazel b/cmd/aspect/clean/BUILD.bazel index d33eca353..b98b29e1f 100644 --- a/cmd/aspect/clean/BUILD.bazel +++ b/cmd/aspect/clean/BUILD.bazel @@ -7,6 +7,9 @@ go_library( visibility = ["//visibility:public"], deps = [ "//pkg/aspect/clean", + "//pkg/bazel", + "//pkg/ioutils", + "//pkg/pathutils", "@com_github_mattn_go_isatty//:go-isatty", "@com_github_spf13_cobra//:cobra", ], diff --git a/cmd/aspect/clean/clean.go b/cmd/aspect/clean/clean.go index ea187e174..0a43dc3c9 100644 --- a/cmd/aspect/clean/clean.go +++ b/cmd/aspect/clean/clean.go @@ -13,12 +13,20 @@ import ( "github.com/spf13/cobra" "aspect.build/cli/pkg/aspect/clean" + "aspect.build/cli/pkg/bazel" + "aspect.build/cli/pkg/ioutils" + "aspect.build/cli/pkg/pathutils" ) -// NewDefaultCleanCmd creates a new clean cobra command. +// NewDefaultCleanCmd creates a new default clean cobra command. func NewDefaultCleanCmd() *cobra.Command { - isInteractive := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) - b := clean.NewDefault(isInteractive) + return NewCleanCmd(ioutils.DefaultStreams, bazel.New()) +} + +// NewCleanCmd creates a new clean cobra command. +func NewCleanCmd(streams ioutils.Streams, bzl bazel.Bazel) *cobra.Command { + var expunge bool + var expungeAsync bool cmd := &cobra.Command{ Use: "clean", @@ -53,16 +61,23 @@ Workaround inconistent state: Such problems are fixable and these bugs are a high priority. If you ever find an incorrect incremental build, please file a bug report, and only use clean as a temporary workaround.`, - RunE: b.Run, + RunE: pathutils.InvokeCmdInsideWorkspace(func(workspaceRoot string, cmd *cobra.Command, args []string) (exitErr error) { + bzl.SetWorkspaceRoot(workspaceRoot) + isInteractive := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) + c := clean.NewDefault(bzl, isInteractive) + c.Expunge = expunge + c.ExpungeAsync = expungeAsync + return c.Run(cmd, args) + }), } - cmd.PersistentFlags().BoolVarP(&b.Expunge, "expunge", "", false, `Remove the entire output_base tree. + cmd.PersistentFlags().BoolVarP(&expunge, "expunge", "", false, `Remove the entire output_base tree. This removes all build output, external repositories, and temp files created by Bazel. It also stops the Bazel server after the clean, equivalent to the shutdown command.`) - cmd.PersistentFlags().BoolVarP(&b.ExpungeAsync, "expunge_async", "", false, `Expunge in the background. + cmd.PersistentFlags().BoolVarP(&expungeAsync, "expunge_async", "", false, `Expunge in the background. It is safe to invoke a Bazel command in the same workspace while the asynchronous expunge continues to run. Note, however, that this may introduce IO contention.`) diff --git a/cmd/aspect/info/BUILD.bazel b/cmd/aspect/info/BUILD.bazel index 809d488f9..d1a1a391c 100644 --- a/cmd/aspect/info/BUILD.bazel +++ b/cmd/aspect/info/BUILD.bazel @@ -8,6 +8,7 @@ go_library( deps = [ "//pkg/aspect/info", "//pkg/ioutils", + "//pkg/pathutils", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/aspect/info/info.go b/cmd/aspect/info/info.go index 0ce183c04..b530eceed 100644 --- a/cmd/aspect/info/info.go +++ b/cmd/aspect/info/info.go @@ -11,6 +11,7 @@ import ( "aspect.build/cli/pkg/aspect/info" "aspect.build/cli/pkg/ioutils" + "aspect.build/cli/pkg/pathutils" ) func NewDefaultInfoCmd() *cobra.Command { @@ -42,7 +43,7 @@ the bazel User Manual, and can be programmatically obtained with See also 'bazel version' for more detailed bazel version information.`, Args: cobra.MaximumNArgs(1), - RunE: v.Run, + RunE: pathutils.InvokeCmdInsideWorkspace(v.Run), } cmd.PersistentFlags().BoolVarP(&v.ShowMakeEnv, "show_make_env", "", false, `include the set of key/value pairs in the "Make" environment, diff --git a/cmd/aspect/test/BUILD.bazel b/cmd/aspect/test/BUILD.bazel index 2fbf5c4a3..5600f25e0 100644 --- a/cmd/aspect/test/BUILD.bazel +++ b/cmd/aspect/test/BUILD.bazel @@ -9,6 +9,7 @@ go_library( "//pkg/aspect/test", "//pkg/bazel", "//pkg/ioutils", + "//pkg/pathutils", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/aspect/test/test.go b/cmd/aspect/test/test.go index 7037eb987..977f7e673 100644 --- a/cmd/aspect/test/test.go +++ b/cmd/aspect/test/test.go @@ -12,15 +12,14 @@ import ( "aspect.build/cli/pkg/aspect/test" "aspect.build/cli/pkg/bazel" "aspect.build/cli/pkg/ioutils" + "aspect.build/cli/pkg/pathutils" ) func NewDefaultTestCmd() *cobra.Command { return NewTestCmd(ioutils.DefaultStreams, bazel.New()) } -func NewTestCmd(streams ioutils.Streams, bzl bazel.Spawner) *cobra.Command { - v := test.New(streams, bzl) - +func NewTestCmd(streams ioutils.Streams, bzl bazel.Bazel) *cobra.Command { cmd := &cobra.Command{ Use: "test", Short: "Builds the specified targets and runs all test targets among them.", @@ -35,7 +34,11 @@ don't forget to pass all your 'build' options to 'test' too. See 'bazel help target-syntax' for details and examples on how to specify targets. `, - RunE: v.Run, + RunE: pathutils.InvokeCmdInsideWorkspace(func(workspaceRoot string, cmd *cobra.Command, args []string) (exitErr error) { + bzl.SetWorkspaceRoot(workspaceRoot) + t := test.New(streams, bzl) + return t.Run(cmd, args) + }), } return cmd diff --git a/pkg/aspect/build/build.go b/pkg/aspect/build/build.go index a0631ad5c..e7abc17c2 100644 --- a/pkg/aspect/build/build.go +++ b/pkg/aspect/build/build.go @@ -18,13 +18,13 @@ import ( // Build represents the aspect build command. type Build struct { ioutils.Streams - bzl bazel.Spawner + bzl bazel.Bazel } // New creates a Build command. func New( streams ioutils.Streams, - bzl bazel.Spawner, + bzl bazel.Bazel, ) *Build { return &Build{ Streams: streams, diff --git a/pkg/aspect/build/build_test.go b/pkg/aspect/build/build_test.go index ad3dcc12e..5595778b0 100644 --- a/pkg/aspect/build/build_test.go +++ b/pkg/aspect/build/build_test.go @@ -28,12 +28,12 @@ func TestBuild(t *testing.T) { defer ctrl.Finish() streams := ioutils.Streams{} - spawner := bazel_mock.NewMockSpawner(ctrl) + bzl := bazel_mock.NewMockBazel(ctrl) expectErr := &aspecterrors.ExitError{ Err: fmt.Errorf("failed to run bazel build"), ExitCode: 5, } - spawner. + bzl. EXPECT(). Spawn([]string{"build", "--bes_backend=grpc://127.0.0.1:12345", "//..."}). Return(expectErr.ExitCode, expectErr.Err) @@ -48,7 +48,7 @@ func TestBuild(t *testing.T) { Errors(). Times(1) - b := build.New(streams, spawner) + b := build.New(streams, bzl) err := b.Run([]string{"//..."}, besBackend) g.Expect(err).To(MatchError(expectErr)) @@ -61,8 +61,8 @@ func TestBuild(t *testing.T) { var stderr strings.Builder streams := ioutils.Streams{Stderr: &stderr} - spawner := bazel_mock.NewMockSpawner(ctrl) - spawner. + bzl := bazel_mock.NewMockBazel(ctrl) + bzl. EXPECT(). Spawn([]string{"build", "--bes_backend=grpc://127.0.0.1:12345", "//..."}). Return(0, nil) @@ -81,7 +81,7 @@ func TestBuild(t *testing.T) { }). Times(1) - b := build.New(streams, spawner) + b := build.New(streams, bzl) err := b.Run([]string{"//..."}, besBackend) g.Expect(err).To(MatchError(&aspecterrors.ExitError{ExitCode: 1})) @@ -94,8 +94,8 @@ func TestBuild(t *testing.T) { defer ctrl.Finish() streams := ioutils.Streams{} - spawner := bazel_mock.NewMockSpawner(ctrl) - spawner. + bzl := bazel_mock.NewMockBazel(ctrl) + bzl. EXPECT(). Spawn([]string{"build", "--bes_backend=grpc://127.0.0.1:12345", "//..."}). Return(0, nil) @@ -110,7 +110,7 @@ func TestBuild(t *testing.T) { Errors(). Times(1) - b := build.New(streams, spawner) + b := build.New(streams, bzl) err := b.Run([]string{"//..."}, besBackend) g.Expect(err).To(BeNil()) diff --git a/pkg/aspect/clean/clean.go b/pkg/aspect/clean/clean.go index 4e7cacdb6..084d5a859 100644 --- a/pkg/aspect/clean/clean.go +++ b/pkg/aspect/clean/clean.go @@ -55,7 +55,7 @@ type PromptRunner interface { // Clean represents the aspect clean command. type Clean struct { ioutils.Streams - bzl bazel.Spawner + bzl bazel.Bazel isInteractiveMode bool Behavior SelectRunner @@ -70,7 +70,7 @@ type Clean struct { // New creates a Clean command. func New( streams ioutils.Streams, - bzl bazel.Spawner, + bzl bazel.Bazel, isInteractiveMode bool) *Clean { return &Clean{ Streams: streams, @@ -79,10 +79,10 @@ func New( } } -func NewDefault(isInteractive bool) *Clean { +func NewDefault(bzl bazel.Bazel, isInteractive bool) *Clean { c := New( ioutils.DefaultStreams, - bazel.New(), + bzl, isInteractive) c.Behavior = &promptui.Select{ Label: "Clean can have a few behaviors. Which do you want?", diff --git a/pkg/aspect/clean/clean_test.go b/pkg/aspect/clean/clean_test.go index 085d4e5f2..efc3da899 100644 --- a/pkg/aspect/clean/clean_test.go +++ b/pkg/aspect/clean/clean_test.go @@ -65,13 +65,13 @@ func TestClean(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - spawner := mock.NewMockSpawner(ctrl) - spawner. + bzl := mock.NewMockBazel(ctrl) + bzl. EXPECT(). Spawn([]string{"clean"}). Return(0, nil) - b := clean.New(ioutils.Streams{}, spawner, false) + b := clean.New(ioutils.Streams{}, bzl, false) g.Expect(b.Run(nil, []string{})).Should(Succeed()) }) @@ -80,13 +80,13 @@ func TestClean(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - spawner := mock.NewMockSpawner(ctrl) - spawner. + bzl := mock.NewMockBazel(ctrl) + bzl. EXPECT(). Spawn([]string{"clean", "--expunge"}). Return(0, nil) - b := clean.New(ioutils.Streams{}, spawner, false) + b := clean.New(ioutils.Streams{}, bzl, false) b.Expunge = true g.Expect(b.Run(nil, []string{})).Should(Succeed()) }) @@ -96,13 +96,13 @@ func TestClean(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - spawner := mock.NewMockSpawner(ctrl) - spawner. + bzl := mock.NewMockBazel(ctrl) + bzl. EXPECT(). Spawn([]string{"clean", "--expunge_async"}). Return(0, nil) - b := clean.New(ioutils.Streams{}, spawner, false) + b := clean.New(ioutils.Streams{}, bzl, false) b.ExpungeAsync = true g.Expect(b.Run(nil, []string{})).Should(Succeed()) }) @@ -112,15 +112,15 @@ func TestClean(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - spawner := mock.NewMockSpawner(ctrl) - spawner. + bzl := mock.NewMockBazel(ctrl) + bzl. EXPECT(). Spawn([]string{"clean"}). Return(0, nil) var stdout strings.Builder streams := ioutils.Streams{Stdout: &stdout} - b := clean.New(streams, spawner, true) + b := clean.New(streams, bzl, true) b.Behavior = chooseReclaim{} b.Remember = deny{} @@ -134,15 +134,15 @@ func TestClean(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - spawner := mock.NewMockSpawner(ctrl) - spawner. + bzl := mock.NewMockBazel(ctrl) + bzl. EXPECT(). Spawn([]string{"clean"}). Return(0, nil).AnyTimes() var stdout strings.Builder streams := ioutils.Streams{Stdout: &stdout} - b := clean.New(streams, spawner, true) + b := clean.New(streams, bzl, true) viper := *viper.New() cfg, err := os.CreateTemp(os.Getenv("TEST_TMPDIR"), "cfg***.ini") @@ -161,7 +161,7 @@ func TestClean(t *testing.T) { g.Expect(string(content)).To(Equal("[clean]\nskip_prompt=true\n\n")) // If we run it again, there should be no prompt - c := clean.New(streams, spawner, true) + c := clean.New(streams, bzl, true) c.Prefs = viper g.Expect(c.Run(nil, []string{})).Should(Succeed()) }) @@ -193,8 +193,8 @@ func TestClean(t *testing.T) { ctrl := gomock.NewController(t) defer ctrl.Finish() - spawner := mock.NewMockSpawner(ctrl) - spawner. + bzl := mock.NewMockBazel(ctrl) + bzl. EXPECT(). Spawn([]string{"clean"}). Return(0, nil) @@ -202,7 +202,7 @@ func TestClean(t *testing.T) { var stdout strings.Builder streams := ioutils.Streams{Stdout: &stdout} - c := clean.New(streams, spawner, true) + c := clean.New(streams, bzl, true) c.Behavior = chooseWorkaround{} c.Workaround = confirm{} g.Expect(c.Run(nil, []string{})).Should(Succeed()) diff --git a/pkg/aspect/info/info.go b/pkg/aspect/info/info.go index 35fdf5c68..dfab3b799 100644 --- a/pkg/aspect/info/info.go +++ b/pkg/aspect/info/info.go @@ -26,7 +26,7 @@ func New(streams ioutils.Streams) *Info { } } -func (v *Info) Run(_ *cobra.Command, args []string) error { +func (v *Info) Run(workspaceRoot string, _ *cobra.Command, args []string) error { bazelCmd := []string{"info"} if v.ShowMakeEnv { // Propagate the flag @@ -34,6 +34,7 @@ func (v *Info) Run(_ *cobra.Command, args []string) error { } bazelCmd = append(bazelCmd, args...) bzl := bazel.New() + bzl.SetWorkspaceRoot(workspaceRoot) if exitCode, err := bzl.Spawn(bazelCmd); exitCode != 0 { err = &aspecterrors.ExitError{ diff --git a/pkg/aspect/test/test.go b/pkg/aspect/test/test.go index 701706b10..e89156199 100644 --- a/pkg/aspect/test/test.go +++ b/pkg/aspect/test/test.go @@ -16,21 +16,21 @@ import ( type Test struct { ioutils.Streams - bzl bazel.Spawner + bzl bazel.Bazel } -func New(streams ioutils.Streams, bzl bazel.Spawner) *Test { +func New(streams ioutils.Streams, bzl bazel.Bazel) *Test { return &Test{ Streams: streams, bzl: bzl, } } -func (v *Test) Run(_ *cobra.Command, args []string) error { +func (t *Test) Run(_ *cobra.Command, args []string) error { bazelCmd := []string{"test"} bazelCmd = append(bazelCmd, args...) - if exitCode, err := v.bzl.Spawn(bazelCmd); exitCode != 0 { + if exitCode, err := t.bzl.Spawn(bazelCmd); exitCode != 0 { err = &aspecterrors.ExitError{ Err: err, ExitCode: exitCode, diff --git a/pkg/aspect/test/test_test.go b/pkg/aspect/test/test_test.go index 1a8d7c079..7e492c3ed 100644 --- a/pkg/aspect/test/test_test.go +++ b/pkg/aspect/test/test_test.go @@ -12,19 +12,18 @@ import ( // Embrace the stutter :) func TestTest(t *testing.T) { - t.Run("test calls bazel test", func(t *testing.T) { g := NewGomegaWithT(t) ctrl := gomock.NewController(t) defer ctrl.Finish() - spawner := mock.NewMockSpawner(ctrl) - spawner. + bzl := mock.NewMockBazel(ctrl) + bzl. EXPECT(). Spawn([]string{"test"}). Return(0, nil) - b := test.New(ioutils.Streams{}, spawner) + b := test.New(ioutils.Streams{}, bzl) g.Expect(b.Run(nil, []string{})).Should(Succeed()) }) } diff --git a/pkg/bazel/BUILD.bazel b/pkg/bazel/BUILD.bazel index 21a9a6f8d..ea55e099c 100644 --- a/pkg/bazel/BUILD.bazel +++ b/pkg/bazel/BUILD.bazel @@ -1,4 +1,4 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") +load("@io_bazel_rules_go//go:def.bzl", "go_library") load("@io_bazel_rules_go//proto:def.bzl", "go_proto_library") load("@rules_proto//proto:defs.bzl", "proto_library") @@ -7,7 +7,6 @@ go_library( srcs = [ "bazel.go", "bazelisk.go", - "flags.go", ], embed = [":bazel_go_proto"], importpath = "aspect.build/cli/pkg/bazel", @@ -23,15 +22,6 @@ go_library( ], ) -go_test( - name = "bazel_test", - srcs = ["bazel_test.go"], - deps = [ - ":bazel", - "@com_github_onsi_gomega//:gomega", - ], -) - proto_library( name = "bazel_proto", srcs = ["flags.proto"], diff --git a/pkg/bazel/bazel.go b/pkg/bazel/bazel.go index 894cbd6d3..43f3497f9 100644 --- a/pkg/bazel/bazel.go +++ b/pkg/bazel/bazel.go @@ -7,23 +7,35 @@ Not licensed for re-use. package bazel import ( + "encoding/base64" + "fmt" + "io" + "io/ioutil" + "github.com/bazelbuild/bazelisk/core" "github.com/bazelbuild/bazelisk/repositories" - "io" + "google.golang.org/protobuf/proto" ) -type Spawner interface { +type Bazel interface { + SetWorkspaceRoot(workspaceRoot string) Spawn(command []string) (int, error) + RunCommand(command []string, out io.Writer) (int, error) } -type Bazel struct { +type bazel struct { + workspaceRoot string } -func New() *Bazel { - return &Bazel{} +func New() Bazel { + return &bazel{} } -func (*Bazel) createRepositories() *core.Repositories { +func (b *bazel) SetWorkspaceRoot(workspaceRoot string) { + b.workspaceRoot = workspaceRoot +} + +func (*bazel) createRepositories() *core.Repositories { gcs := &repositories.GCSRepo{} gitHub := repositories.CreateGitHubRepo(core.GetEnvOrConfig("BAZELISK_GITHUB_TOKEN")) // Fetch LTS releases, release candidates and Bazel-at-commits from GCS, forks and rolling releases from GitHub. @@ -33,12 +45,46 @@ func (*Bazel) createRepositories() *core.Repositories { // Spawn is similar to the main() function of bazelisk // see https://github.com/bazelbuild/bazelisk/blob/7c3d9d5/bazelisk.go -func (b *Bazel) Spawn(command []string) (int, error) { +func (b *bazel) Spawn(command []string) (int, error) { return b.RunCommand(command, nil) } -func (b *Bazel) RunCommand(command []string, out io.Writer) (int, error) { +func (b *bazel) RunCommand(command []string, out io.Writer) (int, error) { repos := b.createRepositories() - exitCode, err := RunBazelisk(command, repos, out) + bazelisk := NewBazelisk(b.workspaceRoot) + exitCode, err := bazelisk.Run(command, repos, out) return exitCode, err } + +func (b *bazel) Flags() (map[string]*FlagInfo, error) { + r, w := io.Pipe() + decoder := base64.NewDecoder(base64.StdEncoding, r) + bazelErrs := make(chan error, 1) + defer close(bazelErrs) + go func() { + defer w.Close() + _, err := b.RunCommand([]string{"help", "flags-as-proto"}, w) + bazelErrs <- err + }() + + helpProtoBytes, err := ioutil.ReadAll(decoder) + if err != nil { + return nil, fmt.Errorf("failed to get Bazel flags: %w", err) + } + + if err := <-bazelErrs; err != nil { + return nil, fmt.Errorf("failed to get Bazel flags: %w", err) + } + + flagCollection := &FlagCollection{} + if err := proto.Unmarshal(helpProtoBytes, flagCollection); err != nil { + return nil, fmt.Errorf("failed to get Bazel flags: %w", err) + } + + flags := make(map[string]*FlagInfo) + for i := range flagCollection.FlagInfos { + flags[*flagCollection.FlagInfos[i].Name] = flagCollection.FlagInfos[i] + } + + return flags, nil +} diff --git a/pkg/bazel/bazel_test.go b/pkg/bazel/bazel_test.go deleted file mode 100644 index 8fec5c2d8..000000000 --- a/pkg/bazel/bazel_test.go +++ /dev/null @@ -1,23 +0,0 @@ -/* -Copyright © 2021 Aspect Build Systems Inc - -Not licensed for re-use. -*/ - -package bazel_test - -import ( - "testing" - - . "github.com/onsi/gomega" - - "aspect.build/cli/pkg/bazel" -) - -func TestBazel(t *testing.T) { - t.Run("satisfies the Spawner interface", func(t *testing.T) { - g := NewGomegaWithT(t) - var bzl bazel.Spawner = bazel.New() - g.Expect(bzl).To(Not(BeNil())) - }) -} diff --git a/pkg/bazel/bazelisk.go b/pkg/bazel/bazelisk.go index ced2bb9e6..7f80a59b6 100644 --- a/pkg/bazel/bazelisk.go +++ b/pkg/bazel/bazelisk.go @@ -3,11 +3,6 @@ package bazel import ( "bufio" "fmt" - "github.com/bazelbuild/bazelisk/core" - "github.com/bazelbuild/bazelisk/httputil" - "github.com/bazelbuild/bazelisk/platforms" - "github.com/bazelbuild/bazelisk/versions" - "github.com/mitchellh/go-homedir" "io" "io/ioutil" "log" @@ -21,6 +16,12 @@ import ( "strings" "sync" "syscall" + + "github.com/bazelbuild/bazelisk/core" + "github.com/bazelbuild/bazelisk/httputil" + "github.com/bazelbuild/bazelisk/platforms" + "github.com/bazelbuild/bazelisk/versions" + "github.com/mitchellh/go-homedir" ) const ( @@ -37,11 +38,19 @@ var ( fileConfigOnce sync.Once ) -// RunBazelisk runs the main Bazelisk logic for the given arguments and Bazel repositories. -func RunBazelisk(args []string, repos *core.Repositories, out io.Writer) (int, error) { - httputil.UserAgent = getUserAgent() +type Bazelisk struct { + workspaceRoot string +} + +func NewBazelisk(workspaceRoot string) *Bazelisk { + return &Bazelisk{workspaceRoot: workspaceRoot} +} + +// Run runs the main Bazelisk logic for the given arguments and Bazel repositories. +func (bazelisk *Bazelisk) Run(args []string, repos *core.Repositories, out io.Writer) (int, error) { + httputil.UserAgent = bazelisk.getUserAgent() - bazeliskHome := GetEnvOrConfig("BAZELISK_HOME") + bazeliskHome := bazelisk.GetEnvOrConfig("BAZELISK_HOME") if len(bazeliskHome) == 0 { userCacheDir, err := os.UserCacheDir() if err != nil { @@ -56,7 +65,7 @@ func RunBazelisk(args []string, repos *core.Repositories, out io.Writer) (int, e return -1, fmt.Errorf("could not create directory %s: %v", bazeliskHome, err) } - bazelVersionString, err := getBazelVersion() + bazelVersionString, err := bazelisk.getBazelVersion() if err != nil { return -1, fmt.Errorf("could not get Bazel version: %v", err) } @@ -73,7 +82,7 @@ func RunBazelisk(args []string, repos *core.Repositories, out io.Writer) (int, e // If we aren't using a local Bazel binary, we'll have to parse the version string and // download the version that the user wants. if !filepath.IsAbs(bazelPath) { - bazelFork, bazelVersion, err := parseBazelForkAndVersion(bazelVersionString) + bazelFork, bazelVersion, err := bazelisk.parseBazelForkAndVersion(bazelVersionString) if err != nil { return -1, fmt.Errorf("could not parse Bazel fork and version: %v", err) } @@ -84,19 +93,19 @@ func RunBazelisk(args []string, repos *core.Repositories, out io.Writer) (int, e return -1, fmt.Errorf("could not resolve the version '%s' to an actual version number: %v", bazelVersion, err) } - bazelForkOrURL := dirForURL(GetEnvOrConfig(core.BaseURLEnv)) + bazelForkOrURL := dirForURL(bazelisk.GetEnvOrConfig(core.BaseURLEnv)) if len(bazelForkOrURL) == 0 { bazelForkOrURL = bazelFork } baseDirectory := filepath.Join(bazeliskHome, "downloads", bazelForkOrURL) - bazelPath, err = downloadBazel(bazelFork, resolvedBazelVersion, baseDirectory, repos, downloader) + bazelPath, err = bazelisk.downloadBazel(bazelFork, resolvedBazelVersion, baseDirectory, repos, downloader) if err != nil { return -1, fmt.Errorf("could not download Bazel: %v", err) } } else { baseDirectory := filepath.Join(bazeliskHome, "local") - bazelPath, err = linkLocalBazel(baseDirectory, bazelPath) + bazelPath, err = bazelisk.linkLocalBazel(baseDirectory, bazelPath) if err != nil { return -1, fmt.Errorf("cound not link local Bazel: %v", err) } @@ -105,7 +114,7 @@ func RunBazelisk(args []string, repos *core.Repositories, out io.Writer) (int, e // --print_env must be the first argument. if len(args) > 0 && args[0] == "--print_env" { // print environment variables for sub-processes - cmd := makeBazelCmd(bazelPath, args, nil) + cmd := bazelisk.makeBazelCmd(bazelPath, args, nil) for _, val := range cmd.Env { fmt.Println(val) } @@ -114,21 +123,21 @@ func RunBazelisk(args []string, repos *core.Repositories, out io.Writer) (int, e // --strict and --migrate must be the first argument. if len(args) > 0 && (args[0] == "--strict" || args[0] == "--migrate") { - cmd, err := getBazelCommand(args) + cmd, err := bazelisk.getBazelCommand(args) if err != nil { return -1, err } - newFlags, err := getIncompatibleFlags(bazelPath, cmd) + newFlags, err := bazelisk.getIncompatibleFlags(bazelPath, cmd) if err != nil { return -1, fmt.Errorf("could not get the list of incompatible flags: %v", err) } if args[0] == "--migrate" { - migrate(bazelPath, args[1:], newFlags) + bazelisk.migrate(bazelPath, args[1:], newFlags) } else { // When --strict is present, it expands to the list of --incompatible_ flags // that should be enabled for the given Bazel version. - args = insertArgs(args[1:], newFlags) + args = bazelisk.insertArgs(args[1:], newFlags) } } @@ -152,14 +161,14 @@ func RunBazelisk(args []string, repos *core.Repositories, out io.Writer) (int, e } } - exitCode, err := runBazel(bazelPath, args, out) + exitCode, err := bazelisk.runBazel(bazelPath, args, out) if err != nil { return -1, fmt.Errorf("could not run Bazel: %v", err) } return exitCode, nil } -func getBazelCommand(args []string) (string, error) { +func (bazelisk *Bazelisk) getBazelCommand(args []string) (string, error) { for _, a := range args { if !strings.HasPrefix(a, "-") { return a, nil @@ -168,8 +177,8 @@ func getBazelCommand(args []string) (string, error) { return "", fmt.Errorf("could not find a valid Bazel command in %q. Please run `bazel help` if you need help on how to use Bazel.", strings.Join(args, " ")) } -func getUserAgent() string { - agent := GetEnvOrConfig("BAZELISK_USER_AGENT") +func (bazelisk *Bazelisk) getUserAgent() string { + agent := bazelisk.GetEnvOrConfig("BAZELISK_USER_AGENT") if len(agent) > 0 { return agent } @@ -177,22 +186,14 @@ func getUserAgent() string { } // GetEnvOrConfig reads a configuration value from the environment, but fall back to reading it from .bazeliskrc in the workspace root. -func GetEnvOrConfig(name string) string { +func (bazelisk *Bazelisk) GetEnvOrConfig(name string) string { if val := os.Getenv(name); val != "" { return val } // Parse .bazeliskrc in the workspace root, once, if it can be found. fileConfigOnce.Do(func() { - workingDirectory, err := os.Getwd() - if err != nil { - return - } - workspaceRoot := findWorkspaceRoot(workingDirectory) - if workspaceRoot == "" { - return - } - rcFilePath := filepath.Join(workspaceRoot, ".bazeliskrc") + rcFilePath := filepath.Join(bazelisk.workspaceRoot, ".bazeliskrc") contents, err := ioutil.ReadFile(rcFilePath) if err != nil { if os.IsNotExist(err) { @@ -218,36 +219,7 @@ func GetEnvOrConfig(name string) string { return fileConfig[name] } -// isValidWorkspace returns true iff the supplied path is the workspace root, defined by the presence of -// a file named WORKSPACE or WORKSPACE.bazel -// see https://github.com/bazelbuild/bazel/blob/8346ea4cfdd9fbd170d51a528fee26f912dad2d5/src/main/cpp/workspace_layout.cc#L37 -func isValidWorkspace(path string) bool { - info, err := os.Stat(path) - if err != nil { - return false - } - - return !info.IsDir() -} - -func findWorkspaceRoot(root string) string { - if isValidWorkspace(filepath.Join(root, "WORKSPACE")) { - return root - } - - if isValidWorkspace(filepath.Join(root, "WORKSPACE.bazel")) { - return root - } - - parentDirectory := filepath.Dir(root) - if parentDirectory == root { - return "" - } - - return findWorkspaceRoot(parentDirectory) -} - -func getBazelVersion() (string, error) { +func (bazelisk *Bazelisk) getBazelVersion() (string, error) { // Check in this order: // - env var "USE_BAZEL_VERSION" is set to a specific version. // - env var "USE_NIGHTLY_BAZEL" or "USE_BAZEL_NIGHTLY" is set -> latest @@ -260,19 +232,13 @@ func getBazelVersion() (string, error) { // - workspace_root/.bazelversion exists -> read contents, that version. // - workspace_root/WORKSPACE contains a version -> that version. (TODO) // - fallback: latest release - bazelVersion := GetEnvOrConfig("USE_BAZEL_VERSION") + bazelVersion := bazelisk.GetEnvOrConfig("USE_BAZEL_VERSION") if len(bazelVersion) != 0 { return bazelVersion, nil } - workingDirectory, err := os.Getwd() - if err != nil { - return "", fmt.Errorf("could not get working directory: %v", err) - } - - workspaceRoot := findWorkspaceRoot(workingDirectory) - if len(workspaceRoot) != 0 { - bazelVersionPath := filepath.Join(workspaceRoot, ".bazelversion") + if len(bazelisk.workspaceRoot) != 0 { + bazelVersionPath := filepath.Join(bazelisk.workspaceRoot, ".bazelversion") if _, err := os.Stat(bazelVersionPath); err == nil { f, err := os.Open(bazelVersionPath) if err != nil { @@ -296,7 +262,7 @@ func getBazelVersion() (string, error) { return "latest", nil } -func parseBazelForkAndVersion(bazelForkAndVersion string) (string, string, error) { +func (bazelisk *Bazelisk) parseBazelForkAndVersion(bazelForkAndVersion string) (string, string, error) { var bazelFork, bazelVersion string versionInfo := strings.Split(bazelForkAndVersion, "/") @@ -312,7 +278,7 @@ func parseBazelForkAndVersion(bazelForkAndVersion string) (string, string, error return bazelFork, bazelVersion, nil } -func downloadBazel(fork string, version string, baseDirectory string, repos *core.Repositories, downloader core.DownloadFunc) (string, error) { +func (bazelisk *Bazelisk) downloadBazel(fork string, version string, baseDirectory string, repos *core.Repositories, downloader core.DownloadFunc) (string, error) { pathSegment, err := platforms.DetermineBazelFilename(version, false) if err != nil { return "", fmt.Errorf("could not determine path segment to use for Bazel binary: %v", err) @@ -321,14 +287,14 @@ func downloadBazel(fork string, version string, baseDirectory string, repos *cor destFile := "bazel" + platforms.DetermineExecutableFilenameSuffix() destinationDir := filepath.Join(baseDirectory, pathSegment, "bin") - if url := GetEnvOrConfig(core.BaseURLEnv); url != "" { + if url := bazelisk.GetEnvOrConfig(core.BaseURLEnv); url != "" { return repos.DownloadFromBaseURL(url, version, destinationDir, destFile) } return downloader(destinationDir, destFile) } -func copyFile(src, dst string, perm os.FileMode) error { +func (bazelisk *Bazelisk) copyFile(src, dst string, perm os.FileMode) error { srcFile, err := os.Open(src) if err != nil { return err @@ -346,7 +312,7 @@ func copyFile(src, dst string, perm os.FileMode) error { return err } -func linkLocalBazel(baseDirectory string, bazelPath string) (string, error) { +func (bazelisk *Bazelisk) linkLocalBazel(baseDirectory string, bazelPath string) (string, error) { normalizedBazelPath := dirForURL(bazelPath) destinationDir := filepath.Join(baseDirectory, normalizedBazelPath, "bin") err := os.MkdirAll(destinationDir, 0755) @@ -358,7 +324,7 @@ func linkLocalBazel(baseDirectory string, bazelPath string) (string, error) { err = os.Symlink(bazelPath, destinationPath) // If can't create Symlink, fallback to copy if err != nil { - err = copyFile(bazelPath, destinationPath, 0755) + err = bazelisk.copyFile(bazelPath, destinationPath, 0755) if err != nil { return "", fmt.Errorf("cound not copy file from %s to %s: %v", bazelPath, destinationPath, err) } @@ -367,18 +333,12 @@ func linkLocalBazel(baseDirectory string, bazelPath string) (string, error) { return destinationPath, nil } -func maybeDelegateToWrapper(bazel string) string { - if GetEnvOrConfig(skipWrapperEnv) != "" { - return bazel - } - - wd, err := os.Getwd() - if err != nil { +func (bazelisk *Bazelisk) maybeDelegateToWrapper(bazel string) string { + if bazelisk.GetEnvOrConfig(skipWrapperEnv) != "" { return bazel } - root := findWorkspaceRoot(wd) - wrapper := filepath.Join(root, wrapperPath) + wrapper := filepath.Join(bazelisk.workspaceRoot, wrapperPath) if stat, err := os.Stat(wrapper); err != nil || stat.IsDir() || stat.Mode().Perm()&0001 == 0 { return bazel } @@ -386,7 +346,7 @@ func maybeDelegateToWrapper(bazel string) string { return wrapper } -func prependDirToPathList(cmd *exec.Cmd, dir string) { +func (bazelisk *Bazelisk) prependDirToPathList(cmd *exec.Cmd, dir string) { found := false for idx, val := range cmd.Env { splits := strings.Split(val, "=") @@ -405,15 +365,15 @@ func prependDirToPathList(cmd *exec.Cmd, dir string) { } } -func makeBazelCmd(bazel string, args []string, out io.Writer) *exec.Cmd { - execPath := maybeDelegateToWrapper(bazel) +func (bazelisk *Bazelisk) makeBazelCmd(bazel string, args []string, out io.Writer) *exec.Cmd { + execPath := bazelisk.maybeDelegateToWrapper(bazel) cmd := exec.Command(execPath, args...) cmd.Env = append(os.Environ(), skipWrapperEnv+"=true") if execPath != bazel { cmd.Env = append(cmd.Env, fmt.Sprintf("%s=%s", bazelReal, bazel)) } - prependDirToPathList(cmd, filepath.Dir(execPath)) + bazelisk.prependDirToPathList(cmd, filepath.Dir(execPath)) cmd.Stdin = os.Stdin if out == nil { cmd.Stdout = os.Stdout @@ -424,8 +384,8 @@ func makeBazelCmd(bazel string, args []string, out io.Writer) *exec.Cmd { return cmd } -func runBazel(bazel string, args []string, out io.Writer) (int, error) { - cmd := makeBazelCmd(bazel, args, out) +func (bazelisk *Bazelisk) runBazel(bazel string, args []string, out io.Writer) (int, error) { + cmd := bazelisk.makeBazelCmd(bazel, args, out) err := cmd.Start() if err != nil { return 1, fmt.Errorf("could not start Bazel: %v", err) @@ -454,9 +414,9 @@ func runBazel(bazel string, args []string, out io.Writer) (int, error) { } // getIncompatibleFlags returns all incompatible flags for the current Bazel command in alphabetical order. -func getIncompatibleFlags(bazelPath, cmd string) ([]string, error) { +func (bazelisk *Bazelisk) getIncompatibleFlags(bazelPath, cmd string) ([]string, error) { out := strings.Builder{} - if _, err := runBazel(bazelPath, []string{"help", cmd, "--short"}, &out); err != nil { + if _, err := bazelisk.runBazel(bazelPath, []string{"help", cmd, "--short"}, &out); err != nil { return nil, fmt.Errorf("unable to determine incompatible flags with binary %s: %v", bazelPath, err) } @@ -472,7 +432,7 @@ func getIncompatibleFlags(bazelPath, cmd string) ([]string, error) { // insertArgs will insert newArgs in baseArgs. If baseArgs contains the // "--" argument, newArgs will be inserted before that. Otherwise, newArgs // is appended. -func insertArgs(baseArgs []string, newArgs []string) []string { +func (bazelisk *Bazelisk) insertArgs(baseArgs []string, newArgs []string) []string { var result []string inserted := false for _, arg := range baseArgs { @@ -489,14 +449,14 @@ func insertArgs(baseArgs []string, newArgs []string) []string { return result } -func shutdownIfNeeded(bazelPath string) { - bazeliskClean := GetEnvOrConfig("BAZELISK_SHUTDOWN") +func (bazelisk *Bazelisk) shutdownIfNeeded(bazelPath string) { + bazeliskClean := bazelisk.GetEnvOrConfig("BAZELISK_SHUTDOWN") if len(bazeliskClean) == 0 { return } fmt.Printf("bazel shutdown\n") - exitCode, err := runBazel(bazelPath, []string{"shutdown"}, nil) + exitCode, err := bazelisk.runBazel(bazelPath, []string{"shutdown"}, nil) fmt.Printf("\n") if err != nil { log.Fatalf("failed to run bazel shutdown: %v", err) @@ -507,14 +467,14 @@ func shutdownIfNeeded(bazelPath string) { } } -func cleanIfNeeded(bazelPath string) { - bazeliskClean := GetEnvOrConfig("BAZELISK_CLEAN") +func (bazelisk *Bazelisk) cleanIfNeeded(bazelPath string) { + bazeliskClean := bazelisk.GetEnvOrConfig("BAZELISK_CLEAN") if len(bazeliskClean) == 0 { return } fmt.Printf("bazel clean --expunge\n") - exitCode, err := runBazel(bazelPath, []string{"clean", "--expunge"}, nil) + exitCode, err := bazelisk.runBazel(bazelPath, []string{"clean", "--expunge"}, nil) fmt.Printf("\n") if err != nil { log.Fatalf("failed to run clean: %v", err) @@ -526,14 +486,14 @@ func cleanIfNeeded(bazelPath string) { } // migrate will run Bazel with each flag separately and report which ones are failing. -func migrate(bazelPath string, baseArgs []string, flags []string) { +func (bazelisk *Bazelisk) migrate(bazelPath string, baseArgs []string, flags []string) { // 1. Try with all the flags. - args := insertArgs(baseArgs, flags) + args := bazelisk.insertArgs(baseArgs, flags) fmt.Printf("\n\n--- Running Bazel with all incompatible flags\n\n") - shutdownIfNeeded(bazelPath) - cleanIfNeeded(bazelPath) + bazelisk.shutdownIfNeeded(bazelPath) + bazelisk.cleanIfNeeded(bazelPath) fmt.Printf("bazel %s\n", strings.Join(args, " ")) - exitCode, err := runBazel(bazelPath, args, nil) + exitCode, err := bazelisk.runBazel(bazelPath, args, nil) if err != nil { log.Fatalf("could not run Bazel: %v", err) } @@ -545,10 +505,10 @@ func migrate(bazelPath string, baseArgs []string, flags []string) { // 2. Try with no flags, as a sanity check. args = baseArgs fmt.Printf("\n\n--- Running Bazel with no incompatible flags\n\n") - shutdownIfNeeded(bazelPath) - cleanIfNeeded(bazelPath) + bazelisk.shutdownIfNeeded(bazelPath) + bazelisk.cleanIfNeeded(bazelPath) fmt.Printf("bazel %s\n", strings.Join(args, " ")) - exitCode, err = runBazel(bazelPath, args, nil) + exitCode, err = bazelisk.runBazel(bazelPath, args, nil) if err != nil { log.Fatalf("could not run Bazel: %v", err) } @@ -561,12 +521,12 @@ func migrate(bazelPath string, baseArgs []string, flags []string) { var passList []string var failList []string for _, arg := range flags { - args = insertArgs(baseArgs, []string{arg}) + args = bazelisk.insertArgs(baseArgs, []string{arg}) fmt.Printf("\n\n--- Running Bazel with %s\n\n", arg) - shutdownIfNeeded(bazelPath) - cleanIfNeeded(bazelPath) + bazelisk.shutdownIfNeeded(bazelPath) + bazelisk.cleanIfNeeded(bazelPath) fmt.Printf("bazel %s\n", strings.Join(args, " ")) - exitCode, err = runBazel(bazelPath, args, nil) + exitCode, err = bazelisk.runBazel(bazelPath, args, nil) if err != nil { log.Fatalf("could not run Bazel: %v", err) } diff --git a/pkg/bazel/flags.go b/pkg/bazel/flags.go deleted file mode 100644 index 9051e675a..000000000 --- a/pkg/bazel/flags.go +++ /dev/null @@ -1,49 +0,0 @@ -/* -Copyright © 2021 Aspect Build Systems Inc - -Not licensed for re-use. -*/ - -package bazel - -import ( - "encoding/base64" - "fmt" - "io" - "io/ioutil" - - "google.golang.org/protobuf/proto" -) - -func (b *Bazel) Flags() (map[string]*FlagInfo, error) { - r, w := io.Pipe() - decoder := base64.NewDecoder(base64.StdEncoding, r) - bazelErrs := make(chan error, 1) - defer close(bazelErrs) - go func() { - defer w.Close() - _, err := b.RunCommand([]string{"help", "flags-as-proto"}, w) - bazelErrs <- err - }() - - helpProtoBytes, err := ioutil.ReadAll(decoder) - if err != nil { - return nil, fmt.Errorf("failed to get Bazel flags: %w", err) - } - - if err := <-bazelErrs; err != nil { - return nil, fmt.Errorf("failed to get Bazel flags: %w", err) - } - - flagCollection := &FlagCollection{} - if err := proto.Unmarshal(helpProtoBytes, flagCollection); err != nil { - return nil, fmt.Errorf("failed to get Bazel flags: %w", err) - } - - flags := make(map[string]*FlagInfo) - for i := range flagCollection.FlagInfos { - flags[*flagCollection.FlagInfos[i].Name] = flagCollection.FlagInfos[i] - } - - return flags, nil -} diff --git a/pkg/bazel/mock/BUILD.bazel b/pkg/bazel/mock/BUILD.bazel index 165b58e88..63231ba6f 100644 --- a/pkg/bazel/mock/BUILD.bazel +++ b/pkg/bazel/mock/BUILD.bazel @@ -1,12 +1,12 @@ load("@bazel_gomock//:gomock.bzl", "gomock") load("@io_bazel_rules_go//go:def.bzl", "go_library") -# gazelle:exclude mock_spawner_test.go +# gazelle:exclude mock_bazel_test.go gomock( - name = "mock_spawner_source", - out = "mock_spawner_test.go", - interfaces = ["Spawner"], + name = "mock_bazel_source", + out = "mock_bazel_test.go", + interfaces = ["Bazel"], library = "//pkg/bazel", package = "mock", visibility = ["//visibility:private"], @@ -16,7 +16,7 @@ go_library( name = "mock", srcs = [ "doc.go", - ":mock_spawner_source", # keep + ":mock_bazel_source", # keep ], importpath = "aspect.build/cli/pkg/bazel/mock", visibility = ["//:__subpackages__"], diff --git a/pkg/pathutils/BUILD.bazel b/pkg/pathutils/BUILD.bazel new file mode 100644 index 000000000..023e802c7 --- /dev/null +++ b/pkg/pathutils/BUILD.bazel @@ -0,0 +1,23 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "pathutils", + srcs = ["pathutils.go"], + importpath = "aspect.build/cli/pkg/pathutils", + visibility = ["//visibility:public"], + deps = ["@com_github_spf13_cobra//:cobra"], +) + +go_test( + name = "pathutils_test", + srcs = ["pathutils_test.go"], + data = glob(["testfixtures/**/*"]), + embed = [":pathutils"], + deps = [ + "//pkg/pathutils/mock", + "//pkg/stdlib/mock", + "@com_github_golang_mock//gomock", + "@com_github_onsi_gomega//:gomega", + "@com_github_spf13_cobra//:cobra", + ], +) diff --git a/pkg/pathutils/mock/BUILD.bazel b/pkg/pathutils/mock/BUILD.bazel new file mode 100644 index 000000000..1e5f9d962 --- /dev/null +++ b/pkg/pathutils/mock/BUILD.bazel @@ -0,0 +1,26 @@ +load("@bazel_gomock//:gomock.bzl", "gomock") +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +# gazelle:exclude mock_finder_test.go + +gomock( + name = "mock_finder_source", + out = "mock_finder_test.go", + interfaces = ["Finder"], + library = "//pkg/pathutils", + package = "mock", + visibility = ["//visibility:private"], +) + +go_library( + name = "mock", + srcs = [ + "doc.go", + ":mock_finder_source", # keep + ], + importpath = "aspect.build/cli/pkg/pathutils/mock", + visibility = ["//:__subpackages__"], + deps = [ + "@com_github_golang_mock//gomock", # keep + ], +) diff --git a/pkg/pathutils/mock/doc.go b/pkg/pathutils/mock/doc.go new file mode 100644 index 000000000..747ba638a --- /dev/null +++ b/pkg/pathutils/mock/doc.go @@ -0,0 +1,2 @@ +// Package mock contains generated files. +package mock diff --git a/pkg/pathutils/pathutils.go b/pkg/pathutils/pathutils.go new file mode 100644 index 000000000..ef3f22461 --- /dev/null +++ b/pkg/pathutils/pathutils.go @@ -0,0 +1,93 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package pathutils + +import ( + "fmt" + "io/fs" + "os" + "path" + "path/filepath" + + "github.com/spf13/cobra" +) + +// https://github.com/bazelbuild/bazel/blob/8346ea4c/src/main/cpp/workspace_layout.cc#L37 +var WorkspaceFilenames = []string{"WORKSPACE", "WORKSPACE.bazel"} + +// CobraRunEFn is the function signature for the cobra RunE function in the cobra.Command. +type CobraRunEFn func(cmd *cobra.Command, args []string) (exitErr error) + +// RunWorkspaceFn is the function signature based on CobraRunEFn that includes the workspace root. +type RunWorkspaceFn func(workspaceRoot string, cmd *cobra.Command, args []string) (exitErr error) + +// InvokeCmdInsideWorkspace verifies if the current working directory is inside a Bazel workspace, +// then invokes the provided toRun function, injecting the found workspace root path. +func InvokeCmdInsideWorkspace(toRun RunWorkspaceFn) CobraRunEFn { + wd, err := os.Getwd() + if err != nil { + panic(err) + } + return invokeCmdInsideWorkspace(defaultWorkspaceFinder, wd, toRun) +} + +func invokeCmdInsideWorkspace( + finder Finder, + wd string, + toRun RunWorkspaceFn, +) CobraRunEFn { + return func(cmd *cobra.Command, args []string) (exitErr error) { + workspacePath, err := finder.Find(wd) + if err != nil { + return fmt.Errorf("failed to run command %q: %w", cmd.Use, err) + } + if workspacePath == "" { + err = fmt.Errorf("the current working directory %q is not a Bazel workspace", wd) + return fmt.Errorf("failed to run command %q: %w", cmd.Use, err) + } + workspaceRoot := path.Dir(workspacePath) + return toRun(workspaceRoot, cmd, args) + } +} + +// Finder wraps the Find method that performs the finding of the WORKSPACE file +// in the user's Bazel project. +type Finder interface { + Find(wd string) (string, error) +} + +type workspaceFinder struct { + osStat func(string) (fs.FileInfo, error) +} + +var defaultWorkspaceFinder = &workspaceFinder{osStat: os.Stat} + +// Find tries to find a file that marks the root of a Bazel workspace +// (WORKSPACE or WORKSPACE.bazel). If the returned path is empty and no error +// was produced, the user's current working directory is not a Bazel workspace. +func (f *workspaceFinder) Find(wd string) (string, error) { + for { + if wd == "." || wd == filepath.Dir(wd) { + return "", nil + } + for _, workspaceFilename := range WorkspaceFilenames { + workspacePath := path.Join(wd, workspaceFilename) + fileInfo, err := f.osStat(workspacePath) + if err != nil { + if os.IsNotExist(err) { + continue + } + return "", fmt.Errorf("failed to find bazel workspace: %w", err) + } + if fileInfo.IsDir() { + continue + } + return workspacePath, nil + } + wd = filepath.Dir(wd) + } +} diff --git a/pkg/pathutils/pathutils_test.go b/pkg/pathutils/pathutils_test.go new file mode 100644 index 000000000..e5dd2e139 --- /dev/null +++ b/pkg/pathutils/pathutils_test.go @@ -0,0 +1,249 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package pathutils + +import ( + "fmt" + "io/fs" + "os" + "testing" + + "github.com/golang/mock/gomock" + . "github.com/onsi/gomega" + "github.com/spf13/cobra" + + pathutils_mock "aspect.build/cli/pkg/pathutils/mock" + stdlib_mock "aspect.build/cli/pkg/stdlib/mock" +) + +func TestInvokeCmdInsideWorkspace(t *testing.T) { + t.Run("when the workspace finder fails, the returned function fails", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + wd := "fake_working_directory/foo/bar" + expectedErr := fmt.Errorf("failed to find yada yada yada") + + finder := pathutils_mock.NewMockFinder(ctrl) + finder.EXPECT(). + Find(wd). + Return("", expectedErr). + Times(1) + + cmd := &cobra.Command{Use: "fake"} + + err := invokeCmdInsideWorkspace(finder, wd, nil)(cmd, nil) + g.Expect(err).To(MatchError(expectedErr)) + }) + + t.Run("when the workspace finder returns empty, the returned function fails", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + wd := "fake_working_directory/foo/bar" + cmdName := "fake" + expectedErrStr := fmt.Sprintf("failed to run command %q: the current working directory %q is not a Bazel workspace", cmdName, wd) + + finder := pathutils_mock.NewMockFinder(ctrl) + finder.EXPECT(). + Find(wd). + Return("", nil). + Times(1) + + cmd := &cobra.Command{Use: cmdName} + + err := invokeCmdInsideWorkspace(finder, wd, nil)(cmd, nil) + g.Expect(err).To(MatchError(expectedErrStr)) + }) + + t.Run("succeeds", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + wd := "fake_working_directory/foo/bar" + workspacePath := "fake_working_directory/WORKSPACE" + expectedWorkspaceRoot := "fake_working_directory" + + finder := pathutils_mock.NewMockFinder(ctrl) + finder.EXPECT(). + Find(wd). + Return(workspacePath, nil). + Times(1) + + cmd := &cobra.Command{Use: "fake"} + args := []string{"foo", "bar"} + + err := invokeCmdInsideWorkspace(finder, wd, func(workspaceRoot string, _cmd *cobra.Command, _args []string) (exitErr error) { + g.Expect(workspaceRoot).To(Equal(expectedWorkspaceRoot)) + g.Expect(_cmd).To(Equal(cmd)) + g.Expect(_args).To(Equal(args)) + return nil + })(cmd, args) + g.Expect(err).To(BeNil()) + }) +} + +func TestWorkspaceFinder(t *testing.T) { + t.Run("when os.Stat fails, Find fails", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + wd := "fake_working_directory/foo/bar" + expectedErr := fmt.Errorf("os.Stat failed") + + finder := &workspaceFinder{osStat: func(s string) (fs.FileInfo, error) { + return nil, expectedErr + }} + workspacePath, err := finder.Find(wd) + g.Expect(workspacePath).To(BeEmpty()) + g.Expect(err).To(MatchError(expectedErr)) + }) + + t.Run("when a WORKSPACE is not found, the returned workspacePath is empty", func(t *testing.T) { + g := NewGomegaWithT(t) + + // We also make sure that Find doesn't get into an infinite loop. + wds := []string{ + "/level_1/level_2", + "/level_1/level_2/level_3", + "level_1/level_2", + "level_1", + "level_1/", + "/level_1/", + "/level_1/level_2/", + ".", + "/", + "", + } + + for _, wd := range wds { + finder := &workspaceFinder{osStat: func(s string) (fs.FileInfo, error) { + return nil, os.ErrNotExist + }} + workspacePath, err := finder.Find(wd) + g.Expect(workspacePath).To(BeEmpty()) + g.Expect(err).To(BeNil()) + } + }) + + t.Run("succeeds", func(t *testing.T) { + t.Run("case 1", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + wd := "fake_working_directory/foo/bar" + + fsFileInfo := stdlib_mock.NewMockFSFileInfo(ctrl) + fsFileInfo.EXPECT(). + IsDir(). + Return(false). + Times(1) + + finder := &workspaceFinder{osStat: func(s string) (fs.FileInfo, error) { + return fsFileInfo, nil + }} + workspacePath, err := finder.Find(wd) + g.Expect(workspacePath).To(Equal("fake_working_directory/foo/bar/WORKSPACE")) + g.Expect(err).To(BeNil()) + }) + t.Run("case 2", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + wd := "fake_working_directory/foo/bar" + + fsFileInfo := stdlib_mock.NewMockFSFileInfo(ctrl) + gomock.InOrder( + fsFileInfo.EXPECT(). + IsDir(). + Return(true). + Times(1), + fsFileInfo.EXPECT(). + IsDir(). + Return(false). + Times(1), + ) + + finder := &workspaceFinder{osStat: func(s string) (fs.FileInfo, error) { + return fsFileInfo, nil + }} + workspacePath, err := finder.Find(wd) + g.Expect(workspacePath).To(Equal("fake_working_directory/foo/bar/WORKSPACE.bazel")) + g.Expect(err).To(BeNil()) + }) + t.Run("case 3", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + wd := "fake_working_directory/foo/bar" + + fsFileInfo := stdlib_mock.NewMockFSFileInfo(ctrl) + gomock.InOrder( + fsFileInfo.EXPECT(). + IsDir(). + Return(true). + Times(1), + fsFileInfo.EXPECT(). + IsDir(). + Return(true). + Times(1), + fsFileInfo.EXPECT(). + IsDir(). + Return(false). + Times(1), + ) + + finder := &workspaceFinder{osStat: func(s string) (fs.FileInfo, error) { + return fsFileInfo, nil + }} + workspacePath, err := finder.Find(wd) + g.Expect(workspacePath).To(Equal("fake_working_directory/foo/WORKSPACE")) + g.Expect(err).To(BeNil()) + }) + t.Run("case 4", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + wd := "fake_working_directory/foo/bar" + + fsFileInfo := stdlib_mock.NewMockFSFileInfo(ctrl) + gomock.InOrder( + fsFileInfo.EXPECT(). + IsDir(). + Return(true). + Times(1), + fsFileInfo.EXPECT(). + IsDir(). + Return(true). + Times(1), + fsFileInfo.EXPECT(). + IsDir(). + Return(true). + Times(1), + fsFileInfo.EXPECT(). + IsDir(). + Return(false). + Times(1), + ) + + finder := &workspaceFinder{osStat: func(s string) (fs.FileInfo, error) { + return fsFileInfo, nil + }} + workspacePath, err := finder.Find(wd) + g.Expect(workspacePath).To(Equal("fake_working_directory/foo/WORKSPACE.bazel")) + g.Expect(err).To(BeNil()) + }) + }) +} diff --git a/pkg/stdlib/mock/BUILD.bazel b/pkg/stdlib/mock/BUILD.bazel index 5cb74fed8..bdf14ca5a 100644 --- a/pkg/stdlib/mock/BUILD.bazel +++ b/pkg/stdlib/mock/BUILD.bazel @@ -7,6 +7,7 @@ gomock( name = "mock_stdlib_source", out = "mock_stdlib_test.go", interfaces = [ + "FSFileInfo", "NetAddr", "NetListener", ], diff --git a/pkg/stdlib/remap.go b/pkg/stdlib/remap.go index 2f5158d0b..9ffb3dded 100644 --- a/pkg/stdlib/remap.go +++ b/pkg/stdlib/remap.go @@ -6,7 +6,12 @@ Not licensed for re-use. package stdlib -import "net" +import ( + "io/fs" + "net" +) + +type FSFileInfo = fs.FileInfo type NetAddr = net.Addr type NetListener = net.Listener