This repository was archived by the owner on May 23, 2025. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathplugin.go
More file actions
311 lines (273 loc) · 9.71 KB
/
plugin.go
File metadata and controls
311 lines (273 loc) · 9.71 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
/*
* Copyright 2022 Aspect Build Systems, Inc. All rights reserved.
*
* Licensed under the aspect.build Community License (the "License");
* you may not use this file except in compliance with the License.
* Full License text is in the LICENSE file included in the root of this repository
* and at https://aspect.build/communitylicense
*/
// The fix-visibility is a plugin for the aspect CLI. When running in interactive
// mode, it offers to automatically fix visibility issues, otherwise, it prints to
// the terminal the buildozer commands necessary to perform the fix manually.
//
// This plugin is also a reference implementation of a plugin using the Go SDK.
// You will find the code below commented to your satisfaction.
package main
import (
"bytes"
"fmt"
"log"
"os"
"regexp"
"strings"
"sync"
"time"
"github.com/aspect-build/aspect-cli/bazel/buildeventstream"
"github.com/aspect-build/aspect-cli/pkg/ioutils"
"github.com/aspect-build/aspect-cli/pkg/plugin/sdk/v1alpha4/config"
aspectplugin "github.com/aspect-build/aspect-cli/pkg/plugin/sdk/v1alpha4/plugin"
"github.com/bazelbuild/bazel-gazelle/label"
"github.com/bazelbuild/buildtools/edit"
goplugin "github.com/hashicorp/go-plugin"
"github.com/manifoldco/promptui"
)
// main starts up the plugin as a child process of the CLI and connects the gRPC communication.
func main() {
goplugin.Serve(config.NewConfigFor(&FixVisibilityPlugin{
buildozer: &buildozer{},
targetsToFix: &fixOrderedSet{nodes: make(map[fixNode]struct{})},
besChan: make(chan orderedBuildEvent, 100),
}))
}
// FixVisibilityPlugin implements an aspect CLI plugin.
type FixVisibilityPlugin struct {
aspectplugin.Base
buildozer runner
targetsToFix *fixOrderedSet
besOnce sync.Once
besChan chan orderedBuildEvent
besHandlerWaitGroup sync.WaitGroup
}
type orderedBuildEvent struct {
event *buildeventstream.BuildEvent
sequenceNumber int64
}
const visibilityIssueSubstring = "is not visible from target"
const removePrivateVisibilityBuildozerCommand = "remove visibility //visibility:private"
var visibilityIssueRegex = regexp.MustCompile(fmt.Sprintf(`.*target '(.*)' %s '(.*)'.*`, visibilityIssueSubstring))
func (plugin *FixVisibilityPlugin) BEPEventCallback(event *buildeventstream.BuildEvent, sequenceNumber int64) error {
plugin.besChan <- orderedBuildEvent{event: event, sequenceNumber: sequenceNumber}
plugin.besOnce.Do(func() {
plugin.besHandlerWaitGroup.Add(1)
go func() {
defer plugin.besHandlerWaitGroup.Done()
var nextSn int64 = 1
eventBuf := make(map[int64]*buildeventstream.BuildEvent)
for o := range plugin.besChan {
if o.sequenceNumber == 0 {
// Zero is an invalid squence number. Process the event since we can't order it.
if err := plugin.BEPEventHandler(o.event); err != nil {
log.Printf("error handling build event: %v\n", err)
}
continue
}
// Check for duplicate sequence numbers
if _, exists := eventBuf[o.sequenceNumber]; exists {
log.Printf("duplicate sequence number %v\n", o.sequenceNumber)
continue
}
// Add the event to the buffer
eventBuf[o.sequenceNumber] = o.event
// Process events in order
for {
if orderedEvent, exists := eventBuf[nextSn]; exists {
if err := plugin.BEPEventHandler(orderedEvent); err != nil {
log.Printf("error handling build event: %v\n", err)
}
delete(eventBuf, nextSn) // Remove processed event
nextSn++ // Move to the next expected sequence
} else {
break
}
}
}
}()
})
return nil
}
// BEPEventHandler processes all the analysis failures that represent a visibility issue,
// collecting them for later processing in the post-build hook execution.
func (plugin *FixVisibilityPlugin) BEPEventHandler(event *buildeventstream.BuildEvent) error {
// First, verify if the received event is of the type Aborted. The visibility
// issue events are emitted as ANALYSIS_FAILUE, so if there's an analysis
// failure and the description of the event contains the known-issue string,
// we perform a regex match to extract the targets. Note that strings.Contains
// is much cheaper than relying on the regex matching, so we only call regex
// when we are absolutely sure it will return a valid match.
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 {
// Here, we insert the matched targets in a linked list for processing
// in the post-build hook.
plugin.targetsToFix.insert(matches[1], matches[2])
}
}
return nil
}
// 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 {
// Close the build events channel
close(plugin.besChan)
// Wait for all build events to come in
if !waitGroupWithTimeout(&plugin.besHandlerWaitGroup, 60*time.Second) {
log.Printf("timed out waiting for BES events\n")
}
if plugin.targetsToFix.size == 0 {
return nil
}
// For each collected visibility issue...
for node := plugin.targetsToFix.head; node != nil; node = node.next {
// ... we construct the label for the target we want to add to the target
// being fixed.
fromLabel, err := label.Parse(node.from)
if err != nil {
return fmt.Errorf("failed to fix visibility: %w", err)
}
fromLabel.Name = "__pkg__"
// We need to verify if the target being fixed contains //visibility:private,
// otherwise Bazel will yell at us since we will need to remove it to add
// any package to the visibility attribute.
hasPrivateVisibility, err := plugin.hasPrivateVisibility(node.toFix)
if err != nil {
return fmt.Errorf("failed to fix visibility: %w", err)
}
// We check whether it's running in interactive mode, if so, send a request
// to prompt the user using the promptRunner injected by the CLI core in
// this method.
var applyFix bool
if isInteractiveMode {
applyFixPrompt := promptui.Prompt{
Label: "Would you like to auto-fix to the visibility attribute",
IsConfirm: true,
}
_, err := promptRunner.Run(applyFixPrompt)
// Since the prompt is a boolean, any non-nil error should represent a NO.
applyFix = err == nil
}
// Here we either perform the fix automatically, or print the commands for
// the user to perform the fixes manually.
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
}
// PostTestHook satisfies the Plugin interface. In this case, it just calls the
// PostBuildHook.
func (plugin *FixVisibilityPlugin) PostTestHook(
isInteractiveMode bool,
promptRunner ioutils.PromptRunner,
) error {
return plugin.PostBuildHook(isInteractiveMode, promptRunner)
}
// PostRunHook satisfies the Plugin interface. In this case, it just calls the
// PostBuildHook.
func (plugin *FixVisibilityPlugin) PostRunHook(
isInteractiveMode bool,
promptRunner ioutils.PromptRunner,
) error {
return plugin.PostBuildHook(isInteractiveMode, promptRunner)
}
// waitGroupWithTimeout waits for a WaitGroup with a specified timeout.
func waitGroupWithTimeout(wg *sync.WaitGroup, timeout time.Duration) bool {
done := make(chan struct{})
// Run a goroutine to close the channel when WaitGroup is done
go func() {
wg.Wait()
close(done)
}()
select {
case <-done:
// WaitGroup finished within timeout
return true
case <-time.After(timeout):
// Timeout occurred
return false
}
}
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
}