feat: plugin system (#64)
* feat: add the fix-visibility example
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* feat: plugin system
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: tests
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* fix: remove BUILD.bazel
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: move pkgs
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
* refactor: move setup outside versioned sdk
Signed-off-by: Thulio Ferraz Assis <3149049+f0rmiga@users.noreply.github.com>
diff --git a/.bazelignore b/.bazelignore
new file mode 100644
index 0000000..d838da9
--- /dev/null
+++ b/.bazelignore
@@ -0,0 +1 @@
+examples/
diff --git a/cmd/aspect/build/BUILD.bazel b/cmd/aspect/build/BUILD.bazel
index 2acd656..1bbec84 100644
--- a/cmd/aspect/build/BUILD.bazel
+++ b/cmd/aspect/build/BUILD.bazel
@@ -6,12 +6,13 @@
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 e7a88ca..04b1150 100644
--- a/cmd/aspect/build/build.go
+++ b/cmd/aspect/build/build.go
@@ -9,12 +9,13 @@
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 @@
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 cea46cb..9f2f380 100644
--- a/cmd/aspect/root/BUILD.bazel
+++ b/cmd/aspect/root/BUILD.bazel
@@ -13,6 +13,7 @@
"//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 0000000..f88d95c
--- /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 0000000..7bde932
--- /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 5f92d59..ac8d5eb 100644
--- a/cmd/aspect/root/root.go
+++ b/cmd/aspect/root/root.go
@@ -19,6 +19,7 @@
"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 @@
// ### 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 0000000..7e6fdb0
--- /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 0000000..ec8b135
--- /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 0000000..ea1a49b
--- /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 0000000..8b9fdfd
--- /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 0000000..6494351
--- /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 0000000..4c7d2a1
--- /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 a806200..4fec50c 100644
--- a/go.bzl
+++ b/go.bzl
@@ -341,6 +341,12 @@
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",
sum = "h1:AKDB1HM5PWEA7i4nhcpwOrO2byshxBjXVn/J/3+z5/0=",
@@ -365,6 +371,12 @@
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",
sum = "h1:Rqb66Oo1X/eSV1x66xbDccZjhJigjg0+e82kpwzSwCI=",
@@ -431,6 +443,12 @@
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",
sum = "h1:nfCOvKYfkgYP8hkirhJocXT2+zOD8yUNjXaWfTlyFKI=",
@@ -449,7 +467,12 @@
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 @@
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 @@
)
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 799cc8c..35f81e4 100644
--- a/go.mod
+++ b/go.mod
@@ -6,10 +6,11 @@
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 @@
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 49ee2c7..613653a 100644
--- a/go.sum
+++ b/go.sum
@@ -51,7 +51,6 @@
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/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/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/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/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/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.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-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-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.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-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 f0f3e75..e8aa7e3 100644
--- a/pkg/aspect/build/BUILD.bazel
+++ b/pkg/aspect/build/BUILD.bazel
@@ -11,7 +11,6 @@
"//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 fa45e9b..faf024c 100644
--- a/pkg/aspect/build/build.go
+++ b/pkg/aspect/build/build.go
@@ -14,7 +14,6 @@
"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 @@
// 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 @@
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 1cd88c4..23e4a5a 100644
--- a/pkg/aspect/build/build_test.go
+++ b/pkg/aspect/build/build_test.go
@@ -62,7 +62,7 @@
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 @@
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 @@
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 @@
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 @@
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 182b928..b4f2bb3 100644
--- a/pkg/hooks/BUILD.bazel
+++ b/pkg/hooks/BUILD.bazel
@@ -5,5 +5,8 @@
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 c1ff33a..81eda8c 100644
--- a/pkg/hooks/hooks.go
+++ b/pkg/hooks/hooks.go
@@ -8,27 +8,37 @@
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 @@
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 3cdce77..0222c9a 100644
--- a/pkg/ioutils/BUILD.bazel
+++ b/pkg/ioutils/BUILD.bazel
@@ -2,7 +2,11 @@
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 0000000..4aa3375
--- /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 0000000..5374025
--- /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 0000000..4a16614
--- /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 0000000..47bd569
--- /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 0000000..97fb60d
--- /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 0000000..aac80ce
--- /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 0000000..fba85b5
--- /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 0000000..7ecf3b1
--- /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 0000000..0adcde8
--- /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 <Enter> 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 0000000..c60e02e
--- /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 0000000..f605be9
--- /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 0000000..46271ca
--- /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 460857b..0000000
--- 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/pkg/plugins/fix_visibility/plugin.go b/pkg/plugins/fix_visibility/plugin.go
deleted file mode 100644
index 8085c57..0000000
--- a/pkg/plugins/fix_visibility/plugin.go
+++ /dev/null
@@ -1,179 +0,0 @@
-/*
-Copyright © 2021 Aspect Build Systems Inc
-
-Not licensed for re-use.
-*/
-
-package fix_visibility
-
-import (
- "bytes"
- "fmt"
- "io"
- "os"
- "regexp"
- "strings"
-
- "github.com/bazelbuild/bazel-gazelle/label"
- "github.com/bazelbuild/buildtools/edit"
- "github.com/manifoldco/promptui"
- isatty "github.com/mattn/go-isatty"
-
- buildeventstream "aspect.build/cli/bazel/buildeventstream/proto"
-)
-
-type FixVisibilityPlugin struct {
- stdout io.Writer
- buildozer Runner
- isInteractiveMode bool
- applyFixPrompt promptui.Prompt
- targetsToFix *fixOrderedSet
-}
-
-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)
-}
-
-func NewPlugin(
- stdout io.Writer,
- buildozer Runner,
- isInteractiveMode bool,
- applyFixPrompt promptui.Prompt,
-) *FixVisibilityPlugin {
- return &FixVisibilityPlugin{
- stdout: stdout,
- buildozer: buildozer,
- isInteractiveMode: isInteractiveMode,
- targetsToFix: &fixOrderedSet{nodes: make(map[fixNode]struct{})},
- applyFixPrompt: applyFixPrompt,
- }
-}
-
-var visibilityIssueRegex = regexp.MustCompile(`.*target '(.*)' is not visible from target '(.*)'.*`)
-
-const visibilityIssueSubstring = "is not visible from target"
-
-func (plugin *FixVisibilityPlugin) BEPEventCallback(event *buildeventstream.BuildEvent) error {
- aborted := event.GetAborted()
- if aborted != nil &&
- aborted.Reason == buildeventstream.Aborted_ANALYSIS_FAILURE &&
- strings.Contains(aborted.Description, visibilityIssueSubstring) {
- matches := visibilityIssueRegex.FindStringSubmatch(aborted.Description)
- if len(matches) == 3 {
- plugin.targetsToFix.insert(matches[1], matches[2])
- }
- }
- return nil
-}
-
-const removePrivateVisibilityBuildozerCommand = "remove visibility //visibility:private"
-
-func (plugin *FixVisibilityPlugin) PostBuildHook() error {
- if plugin.targetsToFix.size == 0 {
- return nil
- }
-
- for node := plugin.targetsToFix.head; node != nil; node = node.next {
- fromLabel, err := label.Parse(node.from)
- if err != nil {
- return fmt.Errorf("failed to fix visibility: %w", err)
- }
- fromLabel.Name = "__pkg__"
-
- hasPrivateVisibility, err := plugin.hasPrivateVisibility(node.toFix)
- if err != nil {
- return fmt.Errorf("failed to fix visibility: %w", err)
- }
-
- var applyFix bool
- if plugin.isInteractiveMode {
- _, err := plugin.applyFixPrompt.Run()
- applyFix = err == nil
- }
-
- addVisibilityBuildozerCommand := fmt.Sprintf("add visibility %s", fromLabel)
- if applyFix {
- 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 {
- 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)
- if hasPrivateVisibility {
- fmt.Fprintf(plugin.stdout, "buildozer '%s' %s\n", removePrivateVisibilityBuildozerCommand, node.toFix)
- }
- }
- }
-
- return nil
-}
-
-func (plugin *FixVisibilityPlugin) hasPrivateVisibility(toFix string) (bool, error) {
- 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)
- }
- return bytes.Contains(visibility, []byte("//visibility:private")), nil
-}
-
-type fixOrderedSet struct {
- head *fixNode
- tail *fixNode
- nodes map[fixNode]struct{}
- size int
-}
-
-func (s *fixOrderedSet) insert(toFix, from string) {
- node := fixNode{
- toFix: toFix,
- from: from,
- }
- if _, exists := s.nodes[node]; !exists {
- s.nodes[node] = struct{}{}
- if s.head == nil {
- s.head = &node
- } else {
- s.tail.next = &node
- }
- s.tail = &node
- s.size++
- }
-}
-
-type fixNode struct {
- next *fixNode
- toFix string
- from string
-}
-
-type Runner interface {
- Run(args ...string) ([]byte, error)
-}
-
-type buildozer struct{}
-
-func (b *buildozer) Run(args ...string) ([]byte, error) {
- var stdout bytes.Buffer
- var stderr strings.Builder
- edit.ShortenLabelsFlag = true
- edit.DeleteWithComments = true
- opts := &edit.Options{
- OutWriter: &stdout,
- ErrWriter: &stderr,
- NumIO: 200,
- }
- if ret := edit.Buildozer(opts, args); ret != 0 {
- return stdout.Bytes(), fmt.Errorf("failed to run buildozer: exit code %d: %s", ret, stderr.String())
- }
- return stdout.Bytes(), nil
-}
diff --git a/plugins/fix_visibility/BUILD.bazel b/plugins/fix_visibility/BUILD.bazel
new file mode 100644
index 0000000..69b8313
--- /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/plugins/fix_visibility/plugin.go b/plugins/fix_visibility/plugin.go
new file mode 100644
index 0000000..c7cf21d
--- /dev/null
+++ b/plugins/fix_visibility/plugin.go
@@ -0,0 +1,186 @@
+/*
+Copyright © 2021 Aspect Build Systems Inc
+
+Not licensed for re-use.
+*/
+
+package main
+
+import (
+ "bytes"
+ "fmt"
+ "os"
+ "regexp"
+ "strings"
+
+ "github.com/bazelbuild/bazel-gazelle/label"
+ "github.com/bazelbuild/buildtools/edit"
+ goplugin "github.com/hashicorp/go-plugin"
+ "github.com/manifoldco/promptui"
+
+ 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 {
+ buildozer runner
+ targetsToFix *fixOrderedSet
+}
+
+// NewDefaultPlugin creates a new FixVisibilityPlugin with the default
+// dependencies.
+func NewDefaultPlugin() *FixVisibilityPlugin {
+ return NewPlugin(&buildozer{})
+}
+
+// NewPlugin creates a new FixVisibilityPlugin.
+func NewPlugin(buildozer runner) *FixVisibilityPlugin {
+ return &FixVisibilityPlugin{
+ buildozer: buildozer,
+ targetsToFix: &fixOrderedSet{nodes: make(map[fixNode]struct{})},
+ }
+}
+
+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 &&
+ aborted.Reason == buildeventstream.Aborted_ANALYSIS_FAILURE &&
+ strings.Contains(aborted.Description, visibilityIssueSubstring) {
+ matches := visibilityIssueRegex.FindStringSubmatch(aborted.Description)
+ if len(matches) == 3 {
+ plugin.targetsToFix.insert(matches[1], matches[2])
+ }
+ }
+ return nil
+}
+
+const removePrivateVisibilityBuildozerCommand = "remove visibility //visibility:private"
+
+// 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
+ }
+
+ for node := plugin.targetsToFix.head; node != nil; node = node.next {
+ fromLabel, err := label.Parse(node.from)
+ if err != nil {
+ return fmt.Errorf("failed to fix visibility: %w", err)
+ }
+ fromLabel.Name = "__pkg__"
+
+ hasPrivateVisibility, err := plugin.hasPrivateVisibility(node.toFix)
+ if err != nil {
+ return fmt.Errorf("failed to fix visibility: %w", err)
+ }
+
+ var applyFix bool
+ 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 {
+ return fmt.Errorf("failed to fix visibility: %w", err)
+ }
+ if hasPrivateVisibility {
+ if _, err := plugin.buildozer.run(removePrivateVisibilityBuildozerCommand, node.toFix); err != nil {
+ return fmt.Errorf("failed to fix visibility: %w", err)
+ }
+ }
+ } else {
+ 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(os.Stdout, "buildozer '%s' %s\n", removePrivateVisibilityBuildozerCommand, node.toFix)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (plugin *FixVisibilityPlugin) hasPrivateVisibility(toFix string) (bool, error) {
+ 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)
+ }
+ return bytes.Contains(visibility, []byte("//visibility:private")), nil
+}
+
+type fixOrderedSet struct {
+ head *fixNode
+ tail *fixNode
+ nodes map[fixNode]struct{}
+ size int
+}
+
+func (s *fixOrderedSet) insert(toFix, from string) {
+ node := fixNode{
+ toFix: toFix,
+ from: from,
+ }
+ if _, exists := s.nodes[node]; !exists {
+ s.nodes[node] = struct{}{}
+ if s.head == nil {
+ s.head = &node
+ } else {
+ s.tail.next = &node
+ }
+ s.tail = &node
+ s.size++
+ }
+}
+
+type fixNode struct {
+ next *fixNode
+ toFix string
+ from string
+}
+
+type runner interface {
+ run(args ...string) ([]byte, error)
+}
+
+type buildozer struct{}
+
+func (b *buildozer) run(args ...string) ([]byte, error) {
+ var stdout bytes.Buffer
+ var stderr strings.Builder
+ edit.ShortenLabelsFlag = true
+ edit.DeleteWithComments = true
+ opts := &edit.Options{
+ OutWriter: &stdout,
+ ErrWriter: &stderr,
+ NumIO: 200,
+ }
+ if ret := edit.Buildozer(opts, args); ret != 0 {
+ return stdout.Bytes(), fmt.Errorf("failed to run buildozer: exit code %d: %s", ret, stderr.String())
+ }
+ return stdout.Bytes(), nil
+}