diff --git a/.bazelignore b/.bazelignore new file mode 100644 index 000000000..d838da986 --- /dev/null +++ b/.bazelignore @@ -0,0 +1 @@ +examples/ diff --git a/cmd/aspect/build/BUILD.bazel b/cmd/aspect/build/BUILD.bazel index 2acd65646..1bbec8473 100644 --- a/cmd/aspect/build/BUILD.bazel +++ b/cmd/aspect/build/BUILD.bazel @@ -6,12 +6,13 @@ go_library( importpath = "aspect.build/cli/cmd/aspect/build", visibility = ["//cmd/aspect/root:__pkg__"], deps = [ + "//cmd/aspect/root/flags", "//pkg/aspect/build", "//pkg/aspect/build/bep", "//pkg/bazel", "//pkg/hooks", "//pkg/ioutils", - "//pkg/plugins/fix_visibility", + "//pkg/plugin/system", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/cmd/aspect/build/build.go b/cmd/aspect/build/build.go index e7a88ca89..04b115075 100644 --- a/cmd/aspect/build/build.go +++ b/cmd/aspect/build/build.go @@ -9,12 +9,13 @@ package build import ( "github.com/spf13/cobra" + rootFlags "aspect.build/cli/cmd/aspect/root/flags" "aspect.build/cli/pkg/aspect/build" "aspect.build/cli/pkg/aspect/build/bep" "aspect.build/cli/pkg/bazel" "aspect.build/cli/pkg/hooks" "aspect.build/cli/pkg/ioutils" - "aspect.build/cli/pkg/plugins/fix_visibility" + "aspect.build/cli/pkg/plugin/system" ) // NewDefaultBuildCmd creates a new build cobra command with the default @@ -35,21 +36,30 @@ func NewBuildCmd( besBackend bep.BESBackend, hooks *hooks.Hooks, ) *cobra.Command { - // TODO(f0rmiga): this should also be part of the plugin design, as - // registering BEP event subscribers should not be hardcoded here. - var fixVisibilityPlugin build.Plugin = fix_visibility.NewDefaultPlugin() - besBackend.RegisterSubscriber(fixVisibilityPlugin.BEPEventCallback) - hooks.RegisterPostBuild(fixVisibilityPlugin.PostBuildHook) - - b := build.New(streams, bzl, besBackend, hooks) - 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) { - return b.Run(cmd.Context(), cmd, args) + pluginSystem := system.NewPluginSystem() + if err := pluginSystem.Configure(streams); err != nil { + return err + } + defer pluginSystem.TearDown() + + for node := pluginSystem.PluginList().Head; node != nil; node = node.Next { + besBackend.RegisterSubscriber(node.Plugin.BEPEventCallback) + hooks.RegisterPostBuild(node.Plugin.PostBuildHook) + } + + isInteractiveMode, err := cmd.Root().PersistentFlags().GetBool(rootFlags.InteractiveFlagName) + if err != nil { + return err + } + + b := build.New(streams, bzl, besBackend, hooks) + return b.Run(cmd.Context(), cmd, args, isInteractiveMode) }, } diff --git a/cmd/aspect/root/BUILD.bazel b/cmd/aspect/root/BUILD.bazel index cea46cb13..9f2f38083 100644 --- a/cmd/aspect/root/BUILD.bazel +++ b/cmd/aspect/root/BUILD.bazel @@ -13,6 +13,7 @@ go_library( "//cmd/aspect/clean", "//cmd/aspect/docs", "//cmd/aspect/info", + "//cmd/aspect/root/flags", "//cmd/aspect/test", "//cmd/aspect/version", "//docs/help/topics", diff --git a/cmd/aspect/root/flags/BUILD.bazel b/cmd/aspect/root/flags/BUILD.bazel new file mode 100644 index 000000000..f88d95c21 --- /dev/null +++ b/cmd/aspect/root/flags/BUILD.bazel @@ -0,0 +1,8 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "flags", + srcs = ["config.go"], + importpath = "aspect.build/cli/cmd/aspect/root/flags", + visibility = ["//visibility:public"], +) diff --git a/cmd/aspect/root/flags/config.go b/cmd/aspect/root/flags/config.go new file mode 100644 index 000000000..7bde932e4 --- /dev/null +++ b/cmd/aspect/root/flags/config.go @@ -0,0 +1,14 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package flags + +const ( + // ConfigFlagName is the --config flag for the root command. + ConfigFlagName = "config" + // InteractiveFlagName is the --interactive flag for the root command. + InteractiveFlagName = "interactive" +) diff --git a/cmd/aspect/root/root.go b/cmd/aspect/root/root.go index 5f92d59ce..ac8d5ebc8 100644 --- a/cmd/aspect/root/root.go +++ b/cmd/aspect/root/root.go @@ -19,6 +19,7 @@ import ( "aspect.build/cli/cmd/aspect/clean" "aspect.build/cli/cmd/aspect/docs" "aspect.build/cli/cmd/aspect/info" + "aspect.build/cli/cmd/aspect/root/flags" "aspect.build/cli/cmd/aspect/test" "aspect.build/cli/cmd/aspect/version" "aspect.build/cli/docs/help/topics" @@ -49,8 +50,8 @@ func NewRootCmd(streams ioutils.Streams, defaultInteractive bool) *cobra.Command // ### Flags var cfgFile string var interactive bool - cmd.PersistentFlags().StringVar(&cfgFile, "config", "", "config file (default is $HOME/.aspect.yaml)") - cmd.PersistentFlags().BoolVar(&interactive, "interactive", defaultInteractive, "Interactive mode (e.g. prompts for user input)") + cmd.PersistentFlags().StringVar(&cfgFile, flags.ConfigFlagName, "", "config file (default is $HOME/.aspect.yaml)") + cmd.PersistentFlags().BoolVar(&interactive, flags.InteractiveFlagName, defaultInteractive, "Interactive mode (e.g. prompts for user input)") // ### Viper if cfgFile != "" { diff --git a/examples/fix-visibility/.aspectplugins b/examples/fix-visibility/.aspectplugins new file mode 100644 index 000000000..7e6fdb0be --- /dev/null +++ b/examples/fix-visibility/.aspectplugins @@ -0,0 +1,4 @@ +- name: fix-visibility + from: ../../bazel-bin/plugins/fix_visibility/fix_visibility_/fix_visibility + # The possible log levels are: TRACE, DEBUG, INFO, WARN, ERROR, OFF. + log_level: OFF diff --git a/examples/fix-visibility/.gitignore b/examples/fix-visibility/.gitignore new file mode 100644 index 000000000..ec8b1352f --- /dev/null +++ b/examples/fix-visibility/.gitignore @@ -0,0 +1,2 @@ +.aspect/ +bazel-* diff --git a/examples/fix-visibility/README.md b/examples/fix-visibility/README.md new file mode 100644 index 000000000..ea1a49b55 --- /dev/null +++ b/examples/fix-visibility/README.md @@ -0,0 +1,4 @@ +# fix-visibility + +This example exercises the `fix-visibility` plugin. Run the `aspect` CLI in this +workspace and it should fix the visibility issue. diff --git a/examples/fix-visibility/WORKSPACE b/examples/fix-visibility/WORKSPACE new file mode 100644 index 000000000..8b9fdfd23 --- /dev/null +++ b/examples/fix-visibility/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "fix_visibility") diff --git a/examples/fix-visibility/bar/BUILD.bazel b/examples/fix-visibility/bar/BUILD.bazel new file mode 100644 index 000000000..649435189 --- /dev/null +++ b/examples/fix-visibility/bar/BUILD.bazel @@ -0,0 +1,7 @@ +genrule( + name = "bar", + srcs = [], + outs = ["bar.txt"], + cmd = "echo 'bar' > '$@'", + visibility = ["//visibility:private"], +) diff --git a/examples/fix-visibility/foo/BUILD.bazel b/examples/fix-visibility/foo/BUILD.bazel new file mode 100644 index 000000000..4c7d2a10d --- /dev/null +++ b/examples/fix-visibility/foo/BUILD.bazel @@ -0,0 +1,8 @@ +genrule( + name = "foo", + srcs = ["//bar"], + outs = ["foo.sh"], + cmd = "echo '#!/bin/sh\ncat $(execpath //bar)' > '$@'", + executable = True, + visibility = ["//visibility:public"], +) diff --git a/go.bzl b/go.bzl index a80620093..4fec50c2d 100644 --- a/go.bzl +++ b/go.bzl @@ -340,6 +340,12 @@ def deps(): sum = "h1:dH3aiDG9Jvb5r5+bYHsikaOUIpcM0xvgMXVoDkXMzJM=", version = "v0.5.1", ) + go_repository( + name = "com_github_hashicorp_go_hclog", + importpath = "github.com/hashicorp/go-hclog", + sum = "h1:bkKf0BeBXcSYa7f5Fyi9gMuQ8gNsxeiNpZjR6VxNZeo=", + version = "v1.0.0", + ) go_repository( name = "com_github_hashicorp_go_immutable_radix", importpath = "github.com/hashicorp/go-immutable-radix", @@ -364,6 +370,12 @@ def deps(): sum = "h1:sNCoNyDEvN1xa+X0baata4RdcpKwcMS6DH+xwfqPgjw=", version = "v0.0.1", ) + go_repository( + name = "com_github_hashicorp_go_plugin", + importpath = "github.com/hashicorp/go-plugin", + sum = "h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM=", + version = "v1.4.3", + ) go_repository( name = "com_github_hashicorp_go_rootcerts", importpath = "github.com/hashicorp/go-rootcerts", @@ -430,6 +442,12 @@ def deps(): sum = "h1:YZ7UKsJv+hKjqGVUUbtE3HNj79Eln2oQ75tniF6iPt0=", version = "v0.8.2", ) + go_repository( + name = "com_github_hashicorp_yamux", + importpath = "github.com/hashicorp/yamux", + sum = "h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M=", + version = "v0.0.0-20180604194846-3520598351bb", + ) go_repository( name = "com_github_hpcloud_tail", importpath = "github.com/hpcloud/tail", @@ -449,7 +467,12 @@ def deps(): sum = "h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM=", version = "v1.0.0", ) - + go_repository( + name = "com_github_jhump_protoreflect", + importpath = "github.com/jhump/protoreflect", + sum = "h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE=", + version = "v1.6.0", + ) go_repository( name = "com_github_json_iterator_go", importpath = "github.com/json-iterator/go", @@ -607,7 +630,12 @@ def deps(): sum = "h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=", version = "v1.4.8", ) - + go_repository( + name = "com_github_oklog_run", + importpath = "github.com/oklog/run", + sum = "h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw=", + version = "v1.0.0", + ) go_repository( name = "com_github_onsi_ginkgo", importpath = "github.com/onsi/ginkgo", @@ -939,6 +967,7 @@ def deps(): ) go_repository( name = "org_golang_google_grpc", + build_file_proto_mode = "disable", importpath = "google.golang.org/grpc", sum = "h1:/9BgsAsa5nWe26HqOlvlgJnqBuktYOLCgjCPqsa56W0=", version = "v1.38.0", diff --git a/go.mod b/go.mod index 799cc8c20..35f81e47e 100644 --- a/go.mod +++ b/go.mod @@ -6,10 +6,11 @@ require ( github.com/bazelbuild/bazel-gazelle v0.23.0 github.com/bazelbuild/bazelisk v1.10.1 github.com/bazelbuild/buildtools v0.0.0-20210920153738-d6daef01a1a2 - github.com/bazelbuild/rules_go v0.28.0 github.com/fatih/color v1.12.0 github.com/golang/mock v1.5.0 github.com/golang/protobuf v1.5.2 + github.com/hashicorp/go-hclog v1.0.0 + github.com/hashicorp/go-plugin v1.4.3 github.com/manifoldco/promptui v0.8.0 github.com/mattn/go-isatty v0.0.13 github.com/mitchellh/go-homedir v1.1.0 @@ -23,4 +24,5 @@ require ( google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c google.golang.org/grpc v1.38.0 google.golang.org/protobuf v1.26.0 + gopkg.in/yaml.v2 v2.4.0 ) diff --git a/go.sum b/go.sum index 49ee2c79a..613653ac5 100644 --- a/go.sum +++ b/go.sum @@ -51,7 +51,6 @@ github.com/bazelbuild/buildtools v0.0.0-20200718160251-b1667ff58f71/go.mod h1:5J github.com/bazelbuild/buildtools v0.0.0-20210920153738-d6daef01a1a2 h1:hWvEw/36XcpZzKZB2LBYhKSGt72ETiIhudjxd637+4w= github.com/bazelbuild/buildtools v0.0.0-20210920153738-d6daef01a1a2/go.mod h1:689QdV3hBP7Vo9dJMmzhoYIyo/9iMhEmHkJcnaPRCbo= github.com/bazelbuild/rules_go v0.0.0-20190719190356-6dae44dc5cab/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M= -github.com/bazelbuild/rules_go v0.28.0 h1:fNtx0dJpG5ENGdMj3/GICoi/7z+ixB3IIW5rERTzOgM= github.com/bazelbuild/rules_go v0.28.0/go.mod h1:MC23Dc/wkXEyk3Wpq6lCqz0ZAYOZDw2DR5y3N1q2i7M= github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bketelsen/crypt v0.0.4/go.mod h1:aI6NrJ0pMGgvZKL1iVgXLnfIFJtfV+bKCoqOes/6LfM= @@ -165,9 +164,14 @@ github.com/hashicorp/consul/api v1.1.0/go.mod h1:VmuI/Lkw1nC05EYQWNKwWGbkg+FbDBt github.com/hashicorp/consul/sdk v0.1.1/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.1/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= +github.com/hashicorp/go-hclog v0.14.1/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= +github.com/hashicorp/go-hclog v1.0.0 h1:bkKf0BeBXcSYa7f5Fyi9gMuQ8gNsxeiNpZjR6VxNZeo= +github.com/hashicorp/go-hclog v1.0.0/go.mod h1:whpDNt7SSdeAju8AWKIWsul05p54N/39EeqMAyrmvFQ= github.com/hashicorp/go-immutable-radix v1.0.0/go.mod h1:0y9vanUI8NX6FsYoO3zeMjhV/C5i9g4Q3DwcSNZ4P60= github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iPBM1vqhUKIvfM= github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= +github.com/hashicorp/go-plugin v1.4.3 h1:DXmvivbWD5qdiBts9TpBC7BYL1Aia5sxbRgQB+v6UZM= +github.com/hashicorp/go-plugin v1.4.3/go.mod h1:5fGEH17QVwTTcR0zV7yhDPLLmFX9YSZ38b18Udy6vYQ= github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= @@ -184,11 +188,15 @@ github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb h1:b5rjCoWHc7eqmAS4/qyk21ZsHyb6Mxv/jykxvNTkU4M= +github.com/hashicorp/yamux v0.0.0-20180604194846-3520598351bb/go.mod h1:+NfK9FKeTrX5uv1uIXGdwYDTeHna2qgaIlx54MXqjAM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/jhump/protoreflect v1.6.0 h1:h5jfMVslIg6l29nsMs0D8Wj17RDVdNYti0vDN/PZZoE= +github.com/jhump/protoreflect v1.6.0/go.mod h1:eaTn3RZAmMBcV0fifFvlm6VHNz3wSkYyXYWUh7ymB74= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= @@ -211,10 +219,13 @@ github.com/magiconair/properties v1.8.5/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPK github.com/manifoldco/promptui v0.8.0 h1:R95mMF+McvXZQ7j1g8ucVZE1gLP3Sv6j9vlF9kyRqQo= github.com/manifoldco/promptui v0.8.0/go.mod h1:n4zTdgP0vr0S3w7/O/g98U+e0gwLScEXGwov2nIKuGQ= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= +github.com/mattn/go-colorable v0.1.4/go.mod h1:U0ppj6V5qS13XJ6of8GYAs25YV2eR4EVcfRqFIhoBtE= github.com/mattn/go-colorable v0.1.8 h1:c1ghPdyEDarC70ftn0y+A/Ee++9zz8ljHG1b13eJ0s8= github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= +github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.13 h1:qdl+GuBjcsKKDco5BsxPJlId98mSWNKqYA+Co0SC1yA= github.com/mattn/go-isatty v0.0.13/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= @@ -223,6 +234,8 @@ github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceT github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-testing-interface v0.0.0-20171004221916-a61a99592b77/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/go-testing-interface v1.0.0 h1:fzU/JVNcaqHQEcVFAKeR41fkiLdIPrefOvVG1VZ96U0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= @@ -236,6 +249,8 @@ github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3Rllmb github.com/nxadm/tail v1.4.4/go.mod h1:kenIhsEOeOJmVchQTgglprH7qJGnHDVpk1VPCcaMI8A= github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= +github.com/oklog/run v1.0.0 h1:Ru7dDtJNOyC66gQ5dQmaCa0qIsAUFY3sFpK1Xk8igrw= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= github.com/onsi/ginkgo v1.12.1/go.mod h1:zj2OWP4+oCPe1qIXoGWkgMRwljMUYCdkwsT2108oapk= github.com/onsi/ginkgo v1.16.4 h1:29JGrr5oVBm5ulCWet69zQkzWipVXIol6ygQUe/EzNc= @@ -351,6 +366,7 @@ golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= +golang.org/x/net v0.0.0-20180530234432-1e491301e022/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -421,6 +437,7 @@ golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -431,6 +448,7 @@ golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -566,6 +584,7 @@ google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= +google.golang.org/genproto v0.0.0-20170818010345-ee236bd376b0/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -608,6 +627,7 @@ google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c h1:wtujag7C+4D6KMoulW9YauvK2lgdvCMS260jsqqBXr0= google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= +google.golang.org/grpc v1.8.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= diff --git a/pkg/aspect/build/BUILD.bazel b/pkg/aspect/build/BUILD.bazel index f0f3e75c1..e8aa7e39e 100644 --- a/pkg/aspect/build/BUILD.bazel +++ b/pkg/aspect/build/BUILD.bazel @@ -11,7 +11,6 @@ go_library( "//pkg/bazel", "//pkg/hooks", "//pkg/ioutils", - "//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto", "@com_github_spf13_cobra//:cobra", ], ) diff --git a/pkg/aspect/build/build.go b/pkg/aspect/build/build.go index fa45e9b9f..faf024cd9 100644 --- a/pkg/aspect/build/build.go +++ b/pkg/aspect/build/build.go @@ -14,7 +14,6 @@ import ( "github.com/spf13/cobra" - buildeventstream "aspect.build/cli/bazel/buildeventstream/proto" "aspect.build/cli/pkg/aspect/build/bep" "aspect.build/cli/pkg/aspecterrors" "aspect.build/cli/pkg/bazel" @@ -47,11 +46,16 @@ func New( // Run runs the aspect build command, calling `bazel build` with a local Build // Event Protocol backend used by Aspect plugins to subscribe to build events. -func (b *Build) Run(ctx context.Context, cmd *cobra.Command, args []string) (exitErr error) { +func (b *Build) Run( + ctx context.Context, + cmd *cobra.Command, + args []string, + isInteractiveMode bool, +) (exitErr error) { // TODO(f0rmiga): this is a hook for the build command and should be discussed // as part of the plugin design. defer func() { - errs := b.hooks.ExecutePostBuild().Errors() + errs := b.hooks.ExecutePostBuild(isInteractiveMode).Errors() if len(errs) > 0 { for _, err := range errs { fmt.Fprintf(b.Streams.Stderr, "Error: failed to run build command: %v\n", err) @@ -95,12 +99,3 @@ func (b *Build) Run(ctx context.Context, cmd *cobra.Command, args []string) (exi return nil } - -// Plugin defines only the methods for the build command. -type Plugin interface { - // BEPEventsSubscriber is used to verify whether an Aspect plugin registers - // itself to receive the Build Event Protocol events. - BEPEventCallback(event *buildeventstream.BuildEvent) error - // TODO(f0rmiga): test the build hooks after implementing the plugin system. - PostBuildHook() error -} diff --git a/pkg/aspect/build/build_test.go b/pkg/aspect/build/build_test.go index 1cd88c461..23e4a5a9a 100644 --- a/pkg/aspect/build/build_test.go +++ b/pkg/aspect/build/build_test.go @@ -62,7 +62,7 @@ func TestBuild(t *testing.T) { hooks := hooks.New() b := build.New(streams, spawner, besBackend, hooks) ctx := context.Background() - err := b.Run(ctx, nil, []string{"//..."}) + err := b.Run(ctx, nil, []string{"//..."}, false) g.Expect(err).To(MatchError(fmt.Errorf("failed to run build command: %w", setupErr))) }) @@ -106,7 +106,7 @@ func TestBuild(t *testing.T) { hooks := hooks.New() b := build.New(streams, spawner, besBackend, hooks) ctx := context.Background() - err := b.Run(ctx, nil, []string{"//..."}) + err := b.Run(ctx, nil, []string{"//..."}, false) g.Expect(err).To(MatchError(fmt.Errorf("failed to run build command: %w", serveWaitErr))) }) @@ -154,7 +154,7 @@ func TestBuild(t *testing.T) { hooks := hooks.New() b := build.New(streams, spawner, besBackend, hooks) ctx := context.Background() - err := b.Run(ctx, nil, []string{"//..."}) + err := b.Run(ctx, nil, []string{"//..."}, false) g.Expect(err).To(MatchError(expectErr)) }) @@ -203,7 +203,7 @@ func TestBuild(t *testing.T) { hooks := hooks.New() b := build.New(streams, spawner, besBackend, hooks) ctx := context.Background() - err := b.Run(ctx, nil, []string{"//..."}) + err := b.Run(ctx, nil, []string{"//..."}, false) g.Expect(err).To(MatchError(&aspecterrors.ExitError{ExitCode: 1})) g.Expect(stderr.String()).To(Equal("Error: failed to run build command: error 1\nError: failed to run build command: error 2\n")) @@ -249,7 +249,7 @@ func TestBuild(t *testing.T) { hooks := hooks.New() b := build.New(streams, spawner, besBackend, hooks) ctx := context.Background() - err := b.Run(ctx, nil, []string{"//..."}) + err := b.Run(ctx, nil, []string{"//..."}, false) g.Expect(err).To(BeNil()) }) diff --git a/pkg/hooks/BUILD.bazel b/pkg/hooks/BUILD.bazel index 182b92854..b4f2bb37b 100644 --- a/pkg/hooks/BUILD.bazel +++ b/pkg/hooks/BUILD.bazel @@ -5,5 +5,8 @@ go_library( srcs = ["hooks.go"], importpath = "aspect.build/cli/pkg/hooks", visibility = ["//visibility:public"], - deps = ["//pkg/aspecterrors"], + deps = [ + "//pkg/aspecterrors", + "//pkg/ioutils", + ], ) diff --git a/pkg/hooks/hooks.go b/pkg/hooks/hooks.go index c1ff33ad0..81eda8c5e 100644 --- a/pkg/hooks/hooks.go +++ b/pkg/hooks/hooks.go @@ -8,27 +8,37 @@ package hooks import ( "aspect.build/cli/pkg/aspecterrors" + "aspect.build/cli/pkg/ioutils" ) +// Hooks represent the possible hook points from the plugin system. It accepts +// registrations and can execute them as requested at the appropriate times. type Hooks struct { postBuild *hookList } +// New instantiates a new Hooks. func New() *Hooks { return &Hooks{ postBuild: &hookList{}, } } +// RegisterPostBuild registers a post-build hook function. func (hooks *Hooks) RegisterPostBuild(fn PostBuildFn) { hooks.postBuild.insert(fn) } -func (hooks *Hooks) ExecutePostBuild() *aspecterrors.ErrorList { +// ExecutePostBuild executes the post-build hook functions in sequence they were +// registered. +func (hooks *Hooks) ExecutePostBuild(isInteractiveMode bool) *aspecterrors.ErrorList { errors := &aspecterrors.ErrorList{} node := hooks.postBuild.head for node != nil { - if err := node.fn.(PostBuildFn)(); err != nil { + // promptRunner is nil here because it has to satisfy the PostBuild + // signature to comply with the go-plugin library. The real promptRunner is + // instantiated when the gRPC call is made. + if err := node.fn.(PostBuildFn)(isInteractiveMode, nil); err != nil { errors.Insert(err) } node = node.next @@ -36,7 +46,8 @@ func (hooks *Hooks) ExecutePostBuild() *aspecterrors.ErrorList { return errors } -type PostBuildFn func() error +// PostBuildFn matches the plugin PostBuildHook method signature. +type PostBuildFn func(isInteractiveMode bool, promptRunner ioutils.PromptRunner) error type hookList struct { head *hookNode diff --git a/pkg/ioutils/BUILD.bazel b/pkg/ioutils/BUILD.bazel index 3cdce7756..0222c9aa6 100644 --- a/pkg/ioutils/BUILD.bazel +++ b/pkg/ioutils/BUILD.bazel @@ -2,7 +2,11 @@ load("@io_bazel_rules_go//go:def.bzl", "go_library") go_library( name = "ioutils", - srcs = ["streams.go"], + srcs = [ + "prompt.go", + "streams.go", + ], importpath = "aspect.build/cli/pkg/ioutils", visibility = ["//visibility:public"], + deps = ["@com_github_manifoldco_promptui//:promptui"], ) diff --git a/pkg/ioutils/prompt.go b/pkg/ioutils/prompt.go new file mode 100644 index 000000000..4aa337523 --- /dev/null +++ b/pkg/ioutils/prompt.go @@ -0,0 +1,15 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package ioutils + +import "github.com/manifoldco/promptui" + +// PromptRunner is the interface that wraps the promptui.Prompt and makes a call +// to it from the aspect CLI Core. +type PromptRunner interface { + Run(prompt promptui.Prompt) (string, error) +} diff --git a/pkg/plugin/sdk/v1alpha1/config/BUILD.bazel b/pkg/plugin/sdk/v1alpha1/config/BUILD.bazel new file mode 100644 index 000000000..53740256a --- /dev/null +++ b/pkg/plugin/sdk/v1alpha1/config/BUILD.bazel @@ -0,0 +1,12 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "config", + srcs = ["config.go"], + importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/config", + visibility = ["//visibility:public"], + deps = [ + "//pkg/plugin/sdk/v1alpha1/plugin", + "@com_github_hashicorp_go_plugin//:go-plugin", + ], +) diff --git a/pkg/plugin/sdk/v1alpha1/config/config.go b/pkg/plugin/sdk/v1alpha1/config/config.go new file mode 100644 index 000000000..4a166143c --- /dev/null +++ b/pkg/plugin/sdk/v1alpha1/config/config.go @@ -0,0 +1,41 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package config + +import ( + goplugin "github.com/hashicorp/go-plugin" + + "aspect.build/cli/pkg/plugin/sdk/v1alpha1/plugin" +) + +// DefaultPluginName is the name each aspect plugin must provide. +const DefaultPluginName = "aspectplugin" + +// Handshake is the shared handshake config for the v1alpha1 protocol. +var Handshake = goplugin.HandshakeConfig{ + ProtocolVersion: 1, + MagicCookieKey: "PLUGIN", + MagicCookieValue: "ASPECT", +} + +// PluginMap represents the plugin interfaces allowed to be implemented by a +// plugin executable. +var PluginMap = map[string]goplugin.Plugin{ + DefaultPluginName: &plugin.GRPCPlugin{}, +} + +// NewConfigFor returns the default configuration for the passed Plugin +// implementation. +func NewConfigFor(p plugin.Plugin) *goplugin.ServeConfig { + return &goplugin.ServeConfig{ + HandshakeConfig: Handshake, + Plugins: map[string]goplugin.Plugin{ + DefaultPluginName: &plugin.GRPCPlugin{Impl: p}, + }, + GRPCServer: goplugin.DefaultGRPCServer, + } +} diff --git a/pkg/plugin/sdk/v1alpha1/plugin/BUILD.bazel b/pkg/plugin/sdk/v1alpha1/plugin/BUILD.bazel new file mode 100644 index 000000000..47bd569cf --- /dev/null +++ b/pkg/plugin/sdk/v1alpha1/plugin/BUILD.bazel @@ -0,0 +1,19 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "plugin", + srcs = [ + "grpc.go", + "interface.go", + ], + importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/plugin", + visibility = ["//visibility:public"], + deps = [ + "//pkg/ioutils", + "//pkg/plugin/sdk/v1alpha1/proto", + "//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto", + "@com_github_hashicorp_go_plugin//:go-plugin", + "@com_github_manifoldco_promptui//:promptui", + "@org_golang_google_grpc//:go_default_library", + ], +) diff --git a/pkg/plugin/sdk/v1alpha1/plugin/grpc.go b/pkg/plugin/sdk/v1alpha1/plugin/grpc.go new file mode 100644 index 000000000..97fb60d8f --- /dev/null +++ b/pkg/plugin/sdk/v1alpha1/plugin/grpc.go @@ -0,0 +1,175 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +// grpc.go hides all the complexity of doing the gRPC calls between the aspect +// Core and a Plugin implementation by providing simple abstractions from the +// point of view of Plugin maintainers. +package plugin + +import ( + "context" + "fmt" + + goplugin "github.com/hashicorp/go-plugin" + "github.com/manifoldco/promptui" + "google.golang.org/grpc" + + buildeventstream "aspect.build/cli/bazel/buildeventstream/proto" + "aspect.build/cli/pkg/ioutils" + "aspect.build/cli/pkg/plugin/sdk/v1alpha1/proto" +) + +// GRPCPlugin represents a Plugin that communicates over gRPC. +type GRPCPlugin struct { + goplugin.Plugin + Impl Plugin +} + +// GRPCServer registers an instance of the GRPCServer in the Plugin binary. +func (p *GRPCPlugin) GRPCServer(broker *goplugin.GRPCBroker, s *grpc.Server) error { + proto.RegisterPluginServer(s, &GRPCServer{Impl: p.Impl, broker: broker}) + return nil +} + +// GRPCClient returns a client to perform the RPC calls to the Plugin +// instance from the Core. +func (p *GRPCPlugin) GRPCClient(ctx context.Context, broker *goplugin.GRPCBroker, c *grpc.ClientConn) (interface{}, error) { + return &GRPCClient{client: proto.NewPluginClient(c), broker: broker}, nil +} + +// GRPCServer implements the gRPC server that runs on the Plugin instances. +type GRPCServer struct { + Impl Plugin + broker *goplugin.GRPCBroker +} + +// BEPEventCallback translates the gRPC call to the Plugin BEPEventCallback +// implementation. +func (m *GRPCServer) BEPEventCallback( + ctx context.Context, + req *proto.BEPEventCallbackReq, +) (*proto.BEPEventCallbackRes, error) { + return &proto.BEPEventCallbackRes{}, m.Impl.BEPEventCallback(req.Event) +} + +// PostBuildHook translates the gRPC call to the Plugin PostBuildHook +// implementation. It starts a prompt runner that is passed to the Plugin +// instance to be able to perform prompt actions to the CLI user. +func (m *GRPCServer) PostBuildHook( + ctx context.Context, + req *proto.PostBuildHookReq, +) (*proto.PostBuildHookRes, error) { + conn, err := m.broker.Dial(req.BrokerId) + if err != nil { + return nil, err + } + defer conn.Close() + + client := proto.NewPrompterClient(conn) + prompter := &PrompterGRPCClient{client: client} + return &proto.PostBuildHookRes{}, + m.Impl.PostBuildHook(req.IsInteractiveMode, prompter) +} + +// GRPCClient implements the gRPC client that is used by the Core to communicate +// with the Plugin instances. +type GRPCClient struct { + client proto.PluginClient + broker *goplugin.GRPCBroker +} + +// BEPEventCallback is called from the Core to execute the Plugin +// BEPEventCallback. +func (m *GRPCClient) BEPEventCallback(event *buildeventstream.BuildEvent) error { + _, err := m.client.BEPEventCallback(context.Background(), &proto.BEPEventCallbackReq{Event: event}) + return err +} + +// PostBuildHook is called from the Core to execute the Plugin PostBuildHook. It +// starts the prompt runner server and ignores the prompt runner argument since +// the signature of this method has to match the Plugin interface. +func (m *GRPCClient) PostBuildHook(isInteractiveMode bool, _ ioutils.PromptRunner) error { + prompterServer := &PrompterGRPCServer{} + var s *grpc.Server + serverFunc := func(opts []grpc.ServerOption) *grpc.Server { + s = grpc.NewServer(opts...) + proto.RegisterPrompterServer(s, prompterServer) + return s + } + brokerID := m.broker.NextId() + go m.broker.AcceptAndServe(brokerID, serverFunc) + req := &proto.PostBuildHookReq{ + BrokerId: brokerID, + IsInteractiveMode: isInteractiveMode, + } + _, err := m.client.PostBuildHook(context.Background(), req) + s.Stop() + return err +} + +// PrompterGRPCServer implements the gRPC server that runs on the Core and is +// passed to the Plugin to allow prompt actions to the CLI user. +type PrompterGRPCServer struct{} + +// Run translates the gRPC call to perform a prompt Run on the Core. +func (p *PrompterGRPCServer) Run( + ctx context.Context, + req *proto.PromptRunReq, +) (*proto.PromptRunRes, error) { + prompt := &promptui.Prompt{ + Label: req.GetLabel(), + Default: req.GetDefault(), + AllowEdit: req.GetAllowEdit(), + Mask: []rune(req.GetMask())[0], + HideEntered: req.GetHideEntered(), + IsConfirm: req.GetIsConfirm(), + IsVimMode: req.GetIsVimMode(), + } + + result, err := prompt.Run() + res := &proto.PromptRunRes{Result: result} + if err != nil { + res.Error = &proto.PromptRunRes_Error{ + Happened: true, + Message: err.Error(), + } + } + + return res, nil +} + +// PrompterGRPCClient implements the gRPC client that is used by the Plugin +// instance to communicate with the Core to request prompt actions from the +// user. +type PrompterGRPCClient struct { + client proto.PrompterClient +} + +// Run is called from the Plugin to request the Core to run the given +// promptui.Prompt. +func (p *PrompterGRPCClient) Run(prompt promptui.Prompt) (string, error) { + label, isString := prompt.Label.(string) + if !isString { + return "", fmt.Errorf("label '%+v' must be a string", prompt.Label) + } + req := &proto.PromptRunReq{ + Label: label, + Default: prompt.Default, + AllowEdit: prompt.AllowEdit, + Mask: string(prompt.Mask), + HideEntered: prompt.HideEntered, + IsConfirm: prompt.IsConfirm, + IsVimMode: prompt.IsVimMode, + } + res, err := p.client.Run(context.Background(), req) + if err != nil { + return "", err + } + if res.Error != nil && res.Error.Happened { + return "", fmt.Errorf(res.Error.Message) + } + return res.Result, nil +} diff --git a/pkg/plugin/sdk/v1alpha1/plugin/interface.go b/pkg/plugin/sdk/v1alpha1/plugin/interface.go new file mode 100644 index 000000000..aac80cec2 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha1/plugin/interface.go @@ -0,0 +1,21 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package plugin + +import ( + buildeventstream "aspect.build/cli/bazel/buildeventstream/proto" + "aspect.build/cli/pkg/ioutils" +) + +// Plugin determines how an aspect Plugin should be implemented. +type Plugin interface { + BEPEventCallback(event *buildeventstream.BuildEvent) error + PostBuildHook( + isInteractiveMode bool, + promptRunner ioutils.PromptRunner, + ) error +} diff --git a/pkg/plugin/sdk/v1alpha1/proto/BUILD.bazel b/pkg/plugin/sdk/v1alpha1/proto/BUILD.bazel new file mode 100644 index 000000000..fba85b5b5 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha1/proto/BUILD.bazel @@ -0,0 +1,26 @@ +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") + +proto_library( + name = "proto_proto", + srcs = ["plugin.proto"], + visibility = ["//visibility:public"], + deps = ["//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto:proto_proto"], +) + +go_proto_library( + name = "proto_go_proto", + compilers = ["@io_bazel_rules_go//proto:go_grpc"], + importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/proto", + proto = ":proto_proto", + visibility = ["//visibility:public"], + deps = ["//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto"], +) + +go_library( + name = "proto", + embed = [":proto_go_proto"], + importpath = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/proto", + visibility = ["//visibility:public"], +) diff --git a/pkg/plugin/sdk/v1alpha1/proto/dummy.go b/pkg/plugin/sdk/v1alpha1/proto/dummy.go new file mode 100644 index 000000000..7ecf3b15f --- /dev/null +++ b/pkg/plugin/sdk/v1alpha1/proto/dummy.go @@ -0,0 +1,6 @@ +//go:build dummy +// +build dummy + +// This file exists to make the go tooling happy. This package is generated by +// bazel. +package proto diff --git a/pkg/plugin/sdk/v1alpha1/proto/plugin.proto b/pkg/plugin/sdk/v1alpha1/proto/plugin.proto new file mode 100644 index 000000000..0adcde881 --- /dev/null +++ b/pkg/plugin/sdk/v1alpha1/proto/plugin.proto @@ -0,0 +1,68 @@ +syntax = "proto3"; + +package proto; + +import "third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto/build_event_stream.proto"; + +option go_package = "aspect.build/cli/pkg/plugin/sdk/v1alpha1/proto"; + +// Plugin is the service used by the Core to communicate with a Plugin instance. +service Plugin { + rpc BEPEventCallback(BEPEventCallbackReq) returns (BEPEventCallbackRes); + rpc PostBuildHook(PostBuildHookReq) returns (PostBuildHookRes); +} + +message BEPEventCallbackReq { + build_event_stream.BuildEvent event = 1; +} + +message BEPEventCallbackRes {} + +message PostBuildHookReq { + uint32 broker_id = 1; + bool is_interactive_mode = 2; +} + +message PostBuildHookRes {} + +// Prompter is the service used by the Plugin instances to request prompt +// actions to the Core from the CLI users. +service Prompter { + rpc Run(PromptRunReq) returns (PromptRunRes); +} + +// PromptRunReq maps the relevant values from +// (github.com/manifoldco/promptui).Prompt. +message PromptRunReq { + // Label is the value displayed on the command line prompt. + string label = 1; + // Default is the initial value for the prompt. This value will be displayed + // next to the prompt's label and the user will be able to view or change it + // depending on the options. + string default = 2; + // AllowEdit lets the user edit the default value. If false, any key press + // other than automatically clears the default value. + bool allow_edit = 3; + // Mask is an optional rune that sets which character to display instead of + // the entered characters. This allows hiding private information like + // passwords. + string mask = 5; + // HideEntered sets whether to hide the text after the user has pressed enter. + bool hide_entered = 6; + // IsConfirm makes the prompt ask for a yes or no ([Y/N]) question rather than + // request an input. When set, most properties related to input will be + // ignored. + bool is_confirm = 8; + // IsVimMode enables vi-like movements (hjkl) and editing. + bool is_vim_mode = 9; +} + +// PromptRunRes maps the returned values from promptui.Run. +message PromptRunRes { + string result = 1; + message Error { + bool happened = 1; + string message = 2; + } + Error error = 2; +} diff --git a/pkg/plugin/system/BUILD.bazel b/pkg/plugin/system/BUILD.bazel new file mode 100644 index 000000000..c60e02eb7 --- /dev/null +++ b/pkg/plugin/system/BUILD.bazel @@ -0,0 +1,19 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_library") + +go_library( + name = "system", + srcs = [ + "aspectplugins.go", + "setup.go", + ], + importpath = "aspect.build/cli/pkg/plugin/system", + visibility = ["//visibility:public"], + deps = [ + "//pkg/ioutils", + "//pkg/plugin/sdk/v1alpha1/config", + "//pkg/plugin/sdk/v1alpha1/plugin", + "@com_github_hashicorp_go_hclog//:go-hclog", + "@com_github_hashicorp_go_plugin//:go-plugin", + "@in_gopkg_yaml_v2//:yaml_v2", + ], +) diff --git a/pkg/plugin/system/aspectplugins.go b/pkg/plugin/system/aspectplugins.go new file mode 100644 index 000000000..f605be91d --- /dev/null +++ b/pkg/plugin/system/aspectplugins.go @@ -0,0 +1,115 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package system + +import ( + "fmt" + "io/fs" + "io/ioutil" + "os" + "path" + "path/filepath" + + yaml "gopkg.in/yaml.v2" +) + +const ( + workspaceFilename = "WORKSPACE" + aspectpluginsFilename = ".aspectplugins" +) + +// AspectPlugin represents a plugin entry in the plugins file. +type AspectPlugin struct { + Name string `yaml:"name"` + From string `yaml:"from"` + LogLevel string `yaml:"log_level"` + Properties map[string]interface{} `yaml:"properties"` +} + +// Finder is the interface that wraps the simple Find method that performs the +// finding of the plugins file in the user system. +type Finder interface { + Find() (string, error) +} + +type finder struct { + osGetwd func() (string, error) + osStat func(string) (fs.FileInfo, error) +} + +// NewFinder instantiates a default internal implementation of the Finder +// interface. +func NewFinder() Finder { + return &finder{ + osGetwd: os.Getwd, + osStat: os.Stat, + } +} + +// Find finds the .aspectplugins file under a Bazel workspace. If the returned +// path is empty and no error was produced, the file doesn't exist. +func (f *finder) Find() (string, error) { + cwd, err := f.osGetwd() + if err != nil { + return "", fmt.Errorf("failed to find .aspectplugins: %w", err) + } + for { + workspacePath := path.Join(cwd, workspaceFilename) + if _, err := f.osStat(workspacePath); err != nil { + if !os.IsNotExist(err) { + return "", fmt.Errorf("failed to find .aspectplugins: %w", err) + } + cwd = filepath.Dir(cwd) + continue + } + aspectpluginsPath := path.Join(cwd, aspectpluginsFilename) + if _, err := f.osStat(aspectpluginsPath); err != nil { + if !os.IsNotExist(err) { + return "", fmt.Errorf("failed to find .aspectplugins: %w", err) + } + break + } + return aspectpluginsPath, nil + } + return "", nil +} + +// Parser is the interface that wraps the Parse method that performs the parsing +// of a plugins file. +type Parser interface { + Parse(aspectpluginsPath string) ([]AspectPlugin, error) +} + +type parser struct { + ioutilReadFile func(filename string) ([]byte, error) + yamlUnmarshalStrict func(in []byte, out interface{}) (err error) +} + +// NewParser instantiates a default internal implementation of the Parser +// interface. +func NewParser() Parser { + return &parser{ + ioutilReadFile: ioutil.ReadFile, + yamlUnmarshalStrict: yaml.UnmarshalStrict, + } +} + +// Parse parses a plugins file. +func (p *parser) Parse(aspectpluginsPath string) ([]AspectPlugin, error) { + if aspectpluginsPath == "" { + return []AspectPlugin{}, nil + } + aspectpluginsData, err := p.ioutilReadFile(aspectpluginsPath) + if err != nil { + return nil, fmt.Errorf("failed to parse .aspectplugins: %w", err) + } + var aspectplugins []AspectPlugin + if err := p.yamlUnmarshalStrict(aspectpluginsData, &aspectplugins); err != nil { + return nil, fmt.Errorf("failed to parse .aspectplugins: %w", err) + } + return aspectplugins, nil +} diff --git a/pkg/plugin/system/setup.go b/pkg/plugin/system/setup.go new file mode 100644 index 000000000..46271cae8 --- /dev/null +++ b/pkg/plugin/system/setup.go @@ -0,0 +1,153 @@ +/* +Copyright © 2021 Aspect Build Systems Inc + +Not licensed for re-use. +*/ + +package system + +import ( + "fmt" + "os/exec" + + hclog "github.com/hashicorp/go-hclog" + goplugin "github.com/hashicorp/go-plugin" + + "aspect.build/cli/pkg/ioutils" + "aspect.build/cli/pkg/plugin/sdk/v1alpha1/config" + "aspect.build/cli/pkg/plugin/sdk/v1alpha1/plugin" +) + +// PluginSystem is the interface that defines all the methods for the aspect CLI +// plugin system intended to be used by the Core. +type PluginSystem interface { + Configure(streams ioutils.Streams) error + PluginList() *PluginList + TearDown() +} + +type pluginSystem struct { + finder Finder + parser Parser + clientFactory ClientFactory + clients []ClientProvider + plugins *PluginList +} + +// NewPluginSystem instantiates a default internal implementation of the +// PluginSystem interface. +func NewPluginSystem() PluginSystem { + return &pluginSystem{ + finder: NewFinder(), + parser: NewParser(), + clientFactory: &clientFactory{}, + plugins: &PluginList{}, + } +} + +// Configure configures the plugin system. +func (ps *pluginSystem) Configure(streams ioutils.Streams) error { + aspectpluginsPath, err := ps.finder.Find() + if err != nil { + return fmt.Errorf("failed to configure plugin system: %w", err) + } + aspectplugins, err := ps.parser.Parse(aspectpluginsPath) + if err != nil { + return fmt.Errorf("failed to configure plugin system: %w", err) + } + + ps.clients = make([]ClientProvider, 0, len(aspectplugins)) + for _, aspectplugin := range aspectplugins { + logLevel := hclog.LevelFromString(aspectplugin.LogLevel) + if logLevel == hclog.NoLevel { + logLevel = hclog.Error + } + pluginLogger := hclog.New(&hclog.LoggerOptions{ + Name: aspectplugin.Name, + Level: logLevel, + }) + // TODO(f0rmiga): make this loop concurrent so that all plugins are + // configured faster. + clientConfig := &goplugin.ClientConfig{ + HandshakeConfig: config.Handshake, + Plugins: config.PluginMap, + Cmd: exec.Command(aspectplugin.From), + AllowedProtocols: []goplugin.Protocol{goplugin.ProtocolGRPC}, + SyncStdout: streams.Stdout, + SyncStderr: streams.Stderr, + Logger: pluginLogger, + } + client := ps.clientFactory.New(clientConfig) + ps.clients = append(ps.clients, client) + + rpcClient, err := client.Client() + if err != nil { + return fmt.Errorf("failed to configure plugin system: %w", err) + } + + rawplugin, err := rpcClient.Dispense(config.DefaultPluginName) + if err != nil { + return fmt.Errorf("failed to configure plugin system: %w", err) + } + + aspectplugin := rawplugin.(plugin.Plugin) + ps.plugins.insert(aspectplugin) + } + + return nil +} + +// TearDown tears down the plugin system, making all the necessary actions to +// clean up the system. +func (ps *pluginSystem) TearDown() { + for _, client := range ps.clients { + client.Kill() + } +} + +// PluginList returns the list of configures plugins. +func (ps *pluginSystem) PluginList() *PluginList { + return ps.plugins +} + +// ClientFactory hides the call to goplugin.NewClient. +type ClientFactory interface { + New(*goplugin.ClientConfig) ClientProvider +} + +type clientFactory struct{} + +// New calls the goplugin.NewClient with the given config. +func (*clientFactory) New(config *goplugin.ClientConfig) ClientProvider { + return goplugin.NewClient(config) +} + +// ClientProvider is an interface for goplugin.Client returned by +// goplugin.NewClient. +type ClientProvider interface { + Client() (goplugin.ClientProtocol, error) + Kill() +} + +// PluginList implements a simple linked list for the parsed plugins from the +// plugins file. +type PluginList struct { + Head *PluginNode + tail *PluginNode +} + +func (l *PluginList) insert(p plugin.Plugin) { + node := &PluginNode{Plugin: p} + if l.Head == nil { + l.Head = node + } else { + l.tail.Next = node + } + l.tail = node +} + +// PluginNode is a node in the PluginList linked list. +type PluginNode struct { + Next *PluginNode + Plugin plugin.Plugin +} diff --git a/pkg/plugins/fix_visibility/BUILD.bazel b/pkg/plugins/fix_visibility/BUILD.bazel deleted file mode 100644 index 460857bdd..000000000 --- a/pkg/plugins/fix_visibility/BUILD.bazel +++ /dev/null @@ -1,15 +0,0 @@ -load("@io_bazel_rules_go//go:def.bzl", "go_library") - -go_library( - name = "fix_visibility", - srcs = ["plugin.go"], - importpath = "aspect.build/cli/pkg/plugins/fix_visibility", - visibility = ["//cmd/aspect/build:__pkg__"], - deps = [ - "//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto", - "@bazel_gazelle//label:go_default_library", - "@com_github_bazelbuild_buildtools//edit:go_default_library", - "@com_github_manifoldco_promptui//:promptui", - "@com_github_mattn_go_isatty//:go-isatty", - ], -) diff --git a/plugins/fix_visibility/BUILD.bazel b/plugins/fix_visibility/BUILD.bazel new file mode 100644 index 000000000..69b831390 --- /dev/null +++ b/plugins/fix_visibility/BUILD.bazel @@ -0,0 +1,27 @@ +load("@io_bazel_rules_go//go:def.bzl", "go_binary", "go_library") + +go_library( + name = "fix_visibility_lib", + srcs = ["plugin.go"], + importpath = "aspect.build/cli/plugins/fix_visibility", + visibility = ["//visibility:private"], + deps = [ + "//pkg/ioutils", + "//pkg/plugin/sdk/v1alpha1/config", + "//third-party/github.com/bazelbuild/bazel/src/main/java/com/google/devtools/build/lib/buildeventstream/proto", + "@bazel_gazelle//label:go_default_library", + "@com_github_bazelbuild_buildtools//edit:go_default_library", + "@com_github_hashicorp_go_plugin//:go-plugin", + "@com_github_manifoldco_promptui//:promptui", + ], +) + +go_binary( + name = "fix_visibility", + embed = [":fix_visibility_lib"], + gc_linkopts = [ + "-s", + "-w", + ], + visibility = ["//visibility:public"], +) diff --git a/pkg/plugins/fix_visibility/plugin.go b/plugins/fix_visibility/plugin.go similarity index 59% rename from pkg/plugins/fix_visibility/plugin.go rename to plugins/fix_visibility/plugin.go index 8085c5731..c7cf21d40 100644 --- a/pkg/plugins/fix_visibility/plugin.go +++ b/plugins/fix_visibility/plugin.go @@ -4,60 +4,56 @@ Copyright © 2021 Aspect Build Systems Inc Not licensed for re-use. */ -package fix_visibility +package main import ( "bytes" "fmt" - "io" "os" "regexp" "strings" "github.com/bazelbuild/bazel-gazelle/label" "github.com/bazelbuild/buildtools/edit" + goplugin "github.com/hashicorp/go-plugin" "github.com/manifoldco/promptui" - isatty "github.com/mattn/go-isatty" buildeventstream "aspect.build/cli/bazel/buildeventstream/proto" + "aspect.build/cli/pkg/ioutils" + "aspect.build/cli/pkg/plugin/sdk/v1alpha1/config" ) +func main() { + goplugin.Serve(config.NewConfigFor(NewDefaultPlugin())) +} + +// FixVisibilityPlugin implements an aspect CLI plugin. type FixVisibilityPlugin struct { - stdout io.Writer - buildozer Runner - isInteractiveMode bool - applyFixPrompt promptui.Prompt - targetsToFix *fixOrderedSet + buildozer runner + targetsToFix *fixOrderedSet } +// NewDefaultPlugin creates a new FixVisibilityPlugin with the default +// dependencies. func NewDefaultPlugin() *FixVisibilityPlugin { - isInteractiveMode := isatty.IsTerminal(os.Stdout.Fd()) || isatty.IsCygwinTerminal(os.Stdout.Fd()) - applyFixPrompt := promptui.Prompt{ - Label: "Would you like to apply the visibility fixes", - IsConfirm: true, - } - return NewPlugin(os.Stdout, &buildozer{}, isInteractiveMode, applyFixPrompt) + return NewPlugin(&buildozer{}) } -func NewPlugin( - stdout io.Writer, - buildozer Runner, - isInteractiveMode bool, - applyFixPrompt promptui.Prompt, -) *FixVisibilityPlugin { +// NewPlugin creates a new FixVisibilityPlugin. +func NewPlugin(buildozer runner) *FixVisibilityPlugin { return &FixVisibilityPlugin{ - stdout: stdout, - buildozer: buildozer, - isInteractiveMode: isInteractiveMode, - targetsToFix: &fixOrderedSet{nodes: make(map[fixNode]struct{})}, - applyFixPrompt: applyFixPrompt, + buildozer: buildozer, + targetsToFix: &fixOrderedSet{nodes: make(map[fixNode]struct{})}, } } -var visibilityIssueRegex = regexp.MustCompile(`.*target '(.*)' is not visible from target '(.*)'.*`) - const visibilityIssueSubstring = "is not visible from target" +var visibilityIssueRegex = regexp.MustCompile(fmt.Sprintf(`.*target '(.*)' %s '(.*)'.*`, visibilityIssueSubstring)) + +// BEPEventCallback satisfies the Plugin interface. It process all the analysis +// failures that represent a visibility issue, collecting them for later +// processing in the post-build hook execution. func (plugin *FixVisibilityPlugin) BEPEventCallback(event *buildeventstream.BuildEvent) error { aborted := event.GetAborted() if aborted != nil && @@ -73,7 +69,14 @@ func (plugin *FixVisibilityPlugin) BEPEventCallback(event *buildeventstream.Buil const removePrivateVisibilityBuildozerCommand = "remove visibility //visibility:private" -func (plugin *FixVisibilityPlugin) PostBuildHook() error { +// PostBuildHook satisfies the Plugin interface. It prompts the user for +// automatic fixes when in interactive mode. If the user rejects the automatic +// fixes, or if running in non-interactive mode, the commands to perform the fixes +// are printed to the terminal. +func (plugin *FixVisibilityPlugin) PostBuildHook( + isInteractiveMode bool, + promptRunner ioutils.PromptRunner, +) error { if plugin.targetsToFix.size == 0 { return nil } @@ -91,26 +94,30 @@ func (plugin *FixVisibilityPlugin) PostBuildHook() error { } var applyFix bool - if plugin.isInteractiveMode { - _, err := plugin.applyFixPrompt.Run() + if isInteractiveMode { + applyFixPrompt := promptui.Prompt{ + Label: "Would you like to apply the visibility fixes", + IsConfirm: true, + } + _, err := promptRunner.Run(applyFixPrompt) applyFix = err == nil } addVisibilityBuildozerCommand := fmt.Sprintf("add visibility %s", fromLabel) if applyFix { - if _, err := plugin.buildozer.Run(addVisibilityBuildozerCommand, node.toFix); err != nil { + if _, err := plugin.buildozer.run(addVisibilityBuildozerCommand, node.toFix); err != nil { return fmt.Errorf("failed to fix visibility: %w", err) } if hasPrivateVisibility { - if _, err := plugin.buildozer.Run(removePrivateVisibilityBuildozerCommand, node.toFix); err != nil { + if _, err := plugin.buildozer.run(removePrivateVisibilityBuildozerCommand, node.toFix); err != nil { return fmt.Errorf("failed to fix visibility: %w", err) } } } else { - fmt.Fprintf(plugin.stdout, "To fix the visibility errors, run:\n") - fmt.Fprintf(plugin.stdout, "buildozer '%s' %s\n", addVisibilityBuildozerCommand, node.toFix) + fmt.Fprintf(os.Stdout, "To fix the visibility errors, run:\n") + fmt.Fprintf(os.Stdout, "buildozer '%s' %s\n", addVisibilityBuildozerCommand, node.toFix) if hasPrivateVisibility { - fmt.Fprintf(plugin.stdout, "buildozer '%s' %s\n", removePrivateVisibilityBuildozerCommand, node.toFix) + fmt.Fprintf(os.Stdout, "buildozer '%s' %s\n", removePrivateVisibilityBuildozerCommand, node.toFix) } } } @@ -119,7 +126,7 @@ func (plugin *FixVisibilityPlugin) PostBuildHook() error { } func (plugin *FixVisibilityPlugin) hasPrivateVisibility(toFix string) (bool, error) { - visibility, err := plugin.buildozer.Run("print visibility", toFix) + visibility, err := plugin.buildozer.run("print visibility", toFix) if err != nil { return false, fmt.Errorf("failed to check if target has private visibility: %w", err) } @@ -156,13 +163,13 @@ type fixNode struct { from string } -type Runner interface { - Run(args ...string) ([]byte, error) +type runner interface { + run(args ...string) ([]byte, error) } type buildozer struct{} -func (b *buildozer) Run(args ...string) ([]byte, error) { +func (b *buildozer) run(args ...string) ([]byte, error) { var stdout bytes.Buffer var stderr strings.Builder edit.ShortenLabelsFlag = true