blob: c7cf21d409c75651d31b2ebb070f6507672fb0d5 [file] [log] [blame]
/*
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
}