diff --git a/WORKSPACE b/WORKSPACE index 78da1a67d..63bdeeec9 100644 --- a/WORKSPACE +++ b/WORKSPACE @@ -41,3 +41,12 @@ go_rules_dependencies() go_register_toolchains(version = "1.16") gazelle_dependencies() + +http_archive( + name = "bazel_gomock", + sha256 = "82a5fb946d2eb0fed80d3d70c2556784ec6cb5c35cd65a1b5e93e46f99681650", + strip_prefix = "bazel_gomock-1.3", + urls = [ + "https://github.com/jmhodges/bazel_gomock/archive/refs/tags/v1.3.tar.gz", + ], +) diff --git a/cmd/aspect/BUILD.bazel b/cmd/aspect/BUILD.bazel index 07b802d2b..9f25845f3 100644 --- a/cmd/aspect/BUILD.bazel +++ b/cmd/aspect/BUILD.bazel @@ -7,7 +7,7 @@ go_library( visibility = ["//cmd:__subpackages__"], deps = [ "//cmd/aspect/root", - "@com_github_spf13_cobra//:cobra", + "//pkg/aspecterrors", ], ) diff --git a/cmd/aspect/build/BUILD.bazel b/cmd/aspect/build/BUILD.bazel new file mode 100644 index 000000000..4aa7eaa78 --- /dev/null +++ b/cmd/aspect/build/BUILD.bazel @@ -0,0 +1,14 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "build", + srcs = ["build.go"], + importpath = "aspect.build/cli/cmd/aspect/build", + visibility = ["//cmd/aspect/root:__pkg__"], + deps = [ + "//pkg/aspect/build", + "//pkg/bazel", + "//pkg/ioutils", + "@com_github_spf13_cobra//:cobra", + ], +) diff --git a/cmd/aspect/build/build.go b/cmd/aspect/build/build.go new file mode 100644 index 000000000..2c46fca37 --- /dev/null +++ b/cmd/aspect/build/build.go @@ -0,0 +1,39 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package build + +import ( + "github.com/spf13/cobra" + + "aspect.build/cli/pkg/aspect/build" + "aspect.build/cli/pkg/bazel" + "aspect.build/cli/pkg/ioutils" +) + +// NewDefaultBuildCmd creates a new build cobra command with the default +// dependencies. +func NewDefaultBuildCmd() *cobra.Command { + return NewBuildCmd(ioutils.DefaultStreams, bazel.New()) +} + +// NewBuildCmd creates a new build cobra command. +func NewBuildCmd( + streams ioutils.Streams, + bzl bazel.Spawner, +) *cobra.Command { + b := build.New(streams, bzl) + + 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: b.Run, + } + + return cmd +} diff --git a/cmd/aspect/main.go b/cmd/aspect/main.go index 12b014aaa..0e62da7aa 100644 --- a/cmd/aspect/main.go +++ b/cmd/aspect/main.go @@ -6,8 +6,14 @@ Not licensed for re-use. package main -import "aspect.build/cli/cmd/aspect/root" -import "github.com/spf13/cobra" +import ( + "errors" + "fmt" + "os" + + "aspect.build/cli/cmd/aspect/root" + "aspect.build/cli/pkg/aspecterrors" +) func main() { // Detect whether we are being run as a tools/bazel wrapper (look for BAZEL_REAL in the environment) @@ -22,5 +28,16 @@ func main() { // - tools/bazel file and put our bootstrap code in there // cmd := root.NewDefaultRootCmd() - cobra.CheckErr(cmd.Execute()) + if err := cmd.Execute(); err != nil { + var exitErr *aspecterrors.ExitError + if errors.As(err, &exitErr) { + if exitErr.Err != nil { + fmt.Fprintln(os.Stderr, "Error:", err) + } + os.Exit(exitErr.ExitCode) + } + + fmt.Fprintln(os.Stderr, "Error:", err) + os.Exit(1) + } } diff --git a/cmd/aspect/root/BUILD.bazel b/cmd/aspect/root/BUILD.bazel index 683cf6fbc..faa150428 100644 --- a/cmd/aspect/root/BUILD.bazel +++ b/cmd/aspect/root/BUILD.bazel @@ -4,8 +4,12 @@ go_library( name = "root", srcs = ["root.go"], importpath = "aspect.build/cli/cmd/aspect/root", - visibility = ["//visibility:public"], + visibility = [ + "//cmd/aspect:__pkg__", + "//cmd/docgen:__pkg__", + ], deps = [ + "//cmd/aspect/build", "//cmd/aspect/docs", "//cmd/aspect/version", "//docs/help/topics", diff --git a/cmd/aspect/root/root.go b/cmd/aspect/root/root.go index c6c8e51df..2345184f6 100644 --- a/cmd/aspect/root/root.go +++ b/cmd/aspect/root/root.go @@ -15,6 +15,7 @@ import ( "github.com/spf13/cobra" "github.com/spf13/viper" + "aspect.build/cli/cmd/aspect/build" "aspect.build/cli/cmd/aspect/docs" "aspect.build/cli/cmd/aspect/version" "aspect.build/cli/docs/help/topics" @@ -33,9 +34,11 @@ func NewDefaultRootCmd() *cobra.Command { func NewRootCmd(streams ioutils.Streams, defaultInteractive bool) *cobra.Command { cmd := &cobra.Command{ - Use: "aspect", - Short: "Aspect.build bazel wrapper", - Long: boldCyan.Sprintf(`Aspect CLI`) + ` is a better frontend for running bazel`, + Use: "aspect", + Short: "Aspect.build bazel wrapper", + SilenceUsage: true, + SilenceErrors: true, + Long: boldCyan.Sprintf(`Aspect CLI`) + ` is a better frontend for running bazel`, } // ### Flags @@ -64,6 +67,7 @@ func NewRootCmd(streams ioutils.Streams, defaultInteractive bool) *cobra.Command // ### Child commands // IMPORTANT: when adding a new command, also update the _DOCS list in /docs/BUILD.bazel + cmd.AddCommand(build.NewDefaultBuildCmd()) cmd.AddCommand(version.NewDefaultVersionCmd()) cmd.AddCommand(docs.NewDefaultDocsCmd()) diff --git a/docs/BUILD.bazel b/docs/BUILD.bazel index d4cbfd497..4fd027dd5 100644 --- a/docs/BUILD.bazel +++ b/docs/BUILD.bazel @@ -4,6 +4,7 @@ load("@bazel_skylib//rules:write_file.bzl", "write_file") # This list must be updated when we add a new command _DOCS = [ "aspect.md", + "aspect_build.md", "aspect_docs.md", "aspect_version.md", ] diff --git a/docs/aspect.md b/docs/aspect.md index c305917b5..24c1aa845 100644 --- a/docs/aspect.md +++ b/docs/aspect.md @@ -16,6 +16,7 @@ Aspect CLI is a better frontend for running bazel ### SEE ALSO +* [aspect build](aspect_build.md) - Builds the specified targets, using the options. * [aspect docs](aspect_docs.md) - Open documentation in the browser * [aspect version](aspect_version.md) - Print the version of aspect CLI as well as tools it invokes diff --git a/docs/aspect_build.md b/docs/aspect_build.md new file mode 100644 index 000000000..7dd414da7 --- /dev/null +++ b/docs/aspect_build.md @@ -0,0 +1,30 @@ +## aspect build + +Builds the specified targets, using the options. + +### Synopsis + +Invokes bazel build on the specified targets. See 'bazel help target-syntax' for details and examples on how to specify targets to build. + +``` +aspect build [flags] +``` + +### Options + +``` + -h, --help help for build +``` + +### Options inherited from parent commands + +``` + --config string config file (default is $HOME/.aspect.yaml) + --interactive Interactive mode (e.g. prompts for user input) +``` + +### SEE ALSO + +* [aspect](aspect.md) - Aspect.build bazel wrapper + +###### Auto generated by spf13/cobra diff --git a/go.bzl b/go.bzl index 728a58f01..bba570560 100644 --- a/go.bzl +++ b/go.bzl @@ -956,6 +956,7 @@ def deps(): ) go_repository( name = "org_golang_google_protobuf", + build_directives = ["gazelle:exclude **/testdata/**/*"], importpath = "google.golang.org/protobuf", sum = "h1:bxAC2xTBsZGibn2RTntX0oH50xLsqy1OxA9tTL3p/lk=", version = "v1.26.0", @@ -999,8 +1000,8 @@ def deps(): go_repository( name = "org_golang_x_net", importpath = "golang.org/x/net", - sum = "h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0=", - version = "v0.0.0-20210428140749-89ef3d95e781", + sum = "h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg=", + version = "v0.0.0-20210903162142-ad29c8ab022f", ) go_repository( name = "org_golang_x_oauth2", @@ -1017,8 +1018,8 @@ def deps(): go_repository( name = "org_golang_x_sys", importpath = "golang.org/x/sys", - sum = "h1:X/2sJAybVknnUnV7AD2HdT6rm2p5BP6eH2j+igduWgk=", - version = "v0.0.0-20210616045830-e2b7044e8c71", + sum = "h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg=", + version = "v0.0.0-20210903071746-97244b99971b", ) go_repository( name = "org_golang_x_term", @@ -1029,8 +1030,8 @@ def deps(): go_repository( name = "org_golang_x_text", importpath = "golang.org/x/text", - sum = "h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M=", - version = "v0.3.6", + sum = "h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk=", + version = "v0.3.7", ) go_repository( name = "org_golang_x_time", diff --git a/go.mod b/go.mod index 96ca1e4f2..1f741ab5b 100644 --- a/go.mod +++ b/go.mod @@ -6,6 +6,7 @@ require ( github.com/bazelbuild/bazelisk v1.10.1 github.com/bazelbuild/rules_go v0.28.0 github.com/fatih/color v1.12.0 + github.com/golang/mock v1.3.1 github.com/magiconair/properties v1.8.5 // indirect github.com/mattn/go-isatty v0.0.13 github.com/mitchellh/go-homedir v1.1.0 @@ -18,5 +19,8 @@ require ( github.com/spf13/cobra v1.1.3 github.com/spf13/jwalterweatherman v1.1.0 // indirect github.com/spf13/viper v1.7.1 + golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f // indirect + golang.org/x/sys v0.0.0-20210903071746-97244b99971b // indirect + golang.org/x/text v0.3.7 // indirect gopkg.in/ini.v1 v1.62.0 // indirect ) diff --git a/go.sum b/go.sum index 4db7f4a8d..f59996560 100644 --- a/go.sum +++ b/go.sum @@ -403,8 +403,9 @@ golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20210428140749-89ef3d95e781 h1:DzZ89McO9/gWPsQXS/FVKAlG02ZjaQ6AlZRBimEYOd0= golang.org/x/net v0.0.0-20210428140749-89ef3d95e781/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= +golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f h1:w6wWR0H+nyVpbSAQbzVEIACVyr/h8l/BEkY6Sokc7Eg= +golang.org/x/net v0.0.0-20210903162142-ad29c8ab022f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45 h1:SVwTIAaPC2U/AvvLNZ2a7OVsmBpC8L5BlwK1whH3hm0= @@ -440,16 +441,18 @@ golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210112080510-489259a85091/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71 h1:X/2sJAybVknnUnV7AD2HdT6rm2p5BP6eH2j+igduWgk= golang.org/x/sys v0.0.0-20210616045830-e2b7044e8c71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210903071746-97244b99971b h1:3Dq0eVHn0uaQJmPO+/aYPI/fRMqdrVDbu7MQcku54gg= +golang.org/x/sys v0.0.0-20210903071746-97244b99971b/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1 h1:v+OssWQX+hTHEmOBgwxdZxK4zHq3yOs8F9J7mk0PY8E= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6 h1:aRYxNxv6iGQlyVaZmk6ZgYEDa+Jg18DxebPSrd6bg1M= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4 h1:SvFZT6jyqRaOeXpc5h/JSfZenJ2O330aBsf7JfSUXmQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/pkg/aspect/build/BUILD.bazel b/pkg/aspect/build/BUILD.bazel new file mode 100644 index 000000000..e4df02fae --- /dev/null +++ b/pkg/aspect/build/BUILD.bazel @@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library", "go_test") + +go_library( + name = "build", + srcs = ["build.go"], + importpath = "aspect.build/cli/pkg/aspect/build", + visibility = ["//cmd/aspect/build:__pkg__"], + deps = [ + "//pkg/aspecterrors", + "//pkg/bazel", + "//pkg/ioutils", + "@com_github_spf13_cobra//:cobra", + ], +) + +go_test( + name = "build_test", + srcs = ["build_test.go"], + deps = [ + ":build", + "//pkg/aspecterrors", + "//pkg/bazel/mock", + "//pkg/ioutils", + "@com_github_golang_mock//gomock", + "@com_github_onsi_gomega//:gomega", + ], +) diff --git a/pkg/aspect/build/build.go b/pkg/aspect/build/build.go new file mode 100644 index 000000000..4e57ba9f7 --- /dev/null +++ b/pkg/aspect/build/build.go @@ -0,0 +1,46 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package build + +import ( + "github.com/spf13/cobra" + + "aspect.build/cli/pkg/aspecterrors" + "aspect.build/cli/pkg/bazel" + "aspect.build/cli/pkg/ioutils" +) + +// Build represents the aspect build command. +type Build struct { + ioutils.Streams + bzl bazel.Spawner +} + +// New creates a Build command. +func New( + streams ioutils.Streams, + bzl bazel.Spawner, +) *Build { + return &Build{ + Streams: streams, + bzl: bzl, + } +} + +// Run runs the aspect build command. +func (b *Build) Run(_ *cobra.Command, args []string) error { + cmd := append([]string{"build"}, args...) + if exitCode, err := b.bzl.Spawn(cmd); exitCode != 0 { + err = &aspecterrors.ExitError{ + Err: err, + ExitCode: exitCode, + } + return err + } + + return nil +} diff --git a/pkg/aspect/build/build_test.go b/pkg/aspect/build/build_test.go new file mode 100644 index 000000000..3dd2b0668 --- /dev/null +++ b/pkg/aspect/build/build_test.go @@ -0,0 +1,65 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package build_test + +import ( + "fmt" + "strings" + "testing" + + "github.com/golang/mock/gomock" + . "github.com/onsi/gomega" + + "aspect.build/cli/pkg/aspect/build" + "aspect.build/cli/pkg/aspecterrors" + "aspect.build/cli/pkg/bazel/mock" + "aspect.build/cli/pkg/ioutils" +) + +func TestBuild(t *testing.T) { + t.Run("when the bazel runner fails, the aspect build fails", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var stdout strings.Builder + streams := ioutils.Streams{Stdout: &stdout} + spawner := mock.NewMockSpawner(ctrl) + expectErr := &aspecterrors.ExitError{ + Err: fmt.Errorf("failed to run bazel build"), + ExitCode: 5, + } + spawner. + EXPECT(). + Spawn([]string{"build", "//..."}). + Return(expectErr.ExitCode, expectErr.Err) + + b := build.New(streams, spawner) + err := b.Run(nil, []string{"//..."}) + + g.Expect(err).To(Equal(expectErr)) + }) + + t.Run("when the bazel runner succeeds, the aspect build succeeds", func(t *testing.T) { + g := NewGomegaWithT(t) + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + var stdout strings.Builder + streams := ioutils.Streams{Stdout: &stdout} + spawner := mock.NewMockSpawner(ctrl) + spawner. + EXPECT(). + Spawn([]string{"build", "//..."}). + Return(0, nil) + + b := build.New(streams, spawner) + err := b.Run(nil, []string{"//..."}) + + g.Expect(err).To(BeNil()) + }) +} diff --git a/pkg/aspecterrors/BUILD.bazel b/pkg/aspecterrors/BUILD.bazel new file mode 100644 index 000000000..49175ecf8 --- /dev/null +++ b/pkg/aspecterrors/BUILD.bazel @@ -0,0 +1,8 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "aspecterrors", + srcs = ["errors.go"], + importpath = "aspect.build/cli/pkg/aspecterrors", + visibility = ["//:__subpackages__"], +) diff --git a/pkg/aspecterrors/errors.go b/pkg/aspecterrors/errors.go new file mode 100644 index 000000000..7388c7ed3 --- /dev/null +++ b/pkg/aspecterrors/errors.go @@ -0,0 +1,23 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package aspecterrors + +// ExitError encapsulates an upstream error and an exit code. It is used by the +// aspect CLI main entrypoint to propagate meaningful exit error codes as the +// aspect CLI exit code. +type ExitError struct { + Err error + ExitCode int +} + +// Error returns the call to the encapsulated error.Error(). +func (err *ExitError) Error() string { + if err.Err != nil { + return err.Err.Error() + } + return "" +} diff --git a/pkg/bazel/BUILD.bazel b/pkg/bazel/BUILD.bazel index d85e11f8b..f1ad4b95e 100644 --- a/pkg/bazel/BUILD.bazel +++ b/pkg/bazel/BUILD.bazel @@ -7,7 +7,7 @@ go_library( "flags.go", ], importpath = "aspect.build/cli/pkg/bazel", - visibility = ["//visibility:public"], + visibility = ["//:__subpackages__"], deps = [ "@com_github_bazelbuild_bazelisk//core:go_default_library", "@com_github_bazelbuild_bazelisk//repositories:go_default_library", diff --git a/pkg/bazel/mock/BUILD.bazel b/pkg/bazel/mock/BUILD.bazel new file mode 100644 index 000000000..165b58e88 --- /dev/null +++ b/pkg/bazel/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_spawner_test.go + +gomock( + name = "mock_spawner_source", + out = "mock_spawner_test.go", + interfaces = ["Spawner"], + library = "//pkg/bazel", + package = "mock", + visibility = ["//visibility:private"], +) + +go_library( + name = "mock", + srcs = [ + "doc.go", + ":mock_spawner_source", # keep + ], + importpath = "aspect.build/cli/pkg/bazel/mock", + visibility = ["//:__subpackages__"], + deps = [ + "@com_github_golang_mock//gomock", # keep + ], +) diff --git a/pkg/bazel/mock/doc.go b/pkg/bazel/mock/doc.go new file mode 100644 index 000000000..747ba638a --- /dev/null +++ b/pkg/bazel/mock/doc.go @@ -0,0 +1,2 @@ +// Package mock contains generated files. +package mock