Skip to content

Commit b410abb

Browse files
committed
Remove "target" attr
1 parent 88320d8 commit b410abb

8 files changed

Lines changed: 329 additions & 119 deletions

File tree

README.md

Lines changed: 190 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,39 +2,212 @@
22

33
# bazel-aquery-differ
44

5-
This is a port of
6-
<https://github.com/bazelbuild/bazel/blob/master/tools/aquery_differ/aquery_differ.py>
7-
to golang with a nicer UI.
5+
A tool to compare Bazel action query outputs with an interactive HTML report.
6+
This is a re-imagination of the [Bazel
7+
aquery_differ.py](https://github.com/bazelbuild/bazel/blob/master/tools/aquery_differ/aquery_differ.py)
8+
in Go with enhanced visualization features.
9+
10+
## Features
11+
12+
- Compare Bazel action graphs between builds or git commits
13+
- Interactive HTML reports with GitHub-style diff visualization
14+
- Syntax-highlighted diffs (unified diff and go-cmp formats)
15+
- Built-in web server with auto-open browser support
16+
- Native Bazel rules for integration into your build
817

918
## Installation
1019

11-
Download and unzip a release artifact, or git clone and `bazel build //cmd/aquerydiff`.
20+
### As a Bazel Module
1221

13-
## Usage
22+
Add to your `MODULE.bazel`:
23+
24+
```starlark
25+
bazel_dep(name = "bazel-aquery-differ", version = "0.0.0")
26+
```
27+
28+
> **Note**: This module is not yet published to the Bazel Central Registry. For
29+
> now, use an `archive_override` or `git_override` pointing to this repository.
30+
31+
### As a Standalone Binary
32+
33+
Download a release artifact, or build from source:
1434

1535
```bash
16-
aquerydiff --before <BEFORE_FILE> --after <AFTER_FILE> --report_dir <REPORT_DIR>
36+
git clone https://github.com/stackb/bazel-aquery-differ.git
37+
cd bazel-aquery-differ
38+
bazel build //cmd/aquerydiff
39+
```
40+
41+
## Usage
42+
43+
### Using Bazel Rules
44+
45+
Load the rules in your `BUILD.bazel` file:
46+
47+
```starlark
48+
load("@bazel-aquery-differ//rules:defs.bzl", "aquery_diff", "aquery_git_diff")
1749
```
1850

19-
You can generate the `<BEFORE_FILE>` (and `<AFTER_FILE>`) using:
51+
#### Rule: `aquery_diff`
52+
53+
Compare two aquery output files:
54+
55+
```starlark
56+
aquery_diff(
57+
name = "compare_actions",
58+
before = "before.pb",
59+
after = "after.pb",
60+
)
61+
```
62+
63+
**Attributes:**
64+
65+
| Attribute | Type | Default | Description |
66+
|-----------|----------|------------------|------------------------------------------------------------------------------|
67+
| `before` | `label` | **required** | Baseline aquery file (`.pb`, `.proto`, `.textproto`, `.json`, `.jsonproto`) |
68+
| `after` | `label` | **required** | Comparison aquery file (same format options) |
69+
| `match` | `string` | `"output_files"` | Strategy to match before and after actions: `"output_files"` or `"mnemonic"` |
70+
| `serve` | `bool` | `True` | Start web server to view report |
71+
| `open` | `bool` | `True` | Automatically open browser to report |
72+
| `unidiff` | `bool` | `False` | Generate unified diffs (can be slow for large actions) |
73+
| `cmpdiff` | `bool` | `True` | Generate go-cmp diffs (fast, structural comparison) |
74+
75+
Run the comparison:
2076

2177
```bash
22-
bazel aquery //pkg:target-name --output jsonproto > before.json
23-
bazel aquery //pkg:target-name --output textproto > before.textproto
24-
bazel aquery //pkg:target-name --output proto > before.pb
78+
bazel run //path/to:compare_actions
79+
```
80+
81+
> **Performance Note**: The `unidiff` attribute defaults to `False` because generating unified diffs can be prohibitively slow for large actions with many inputs/outputs. The `cmpdiff` format (enabled by default) is much faster and provides good structural comparison for most use cases. Only enable `unidiff` if you need the traditional unified diff format and are willing to wait for the additional processing time.
82+
83+
**Choosing a Match Strategy:**
84+
85+
The `match` attribute determines how actions are paired between the before and
86+
after builds:
87+
88+
- **`output_files`** (default): Actions are matched by their output file paths.
89+
Use this when comparing the same target across different commits or
90+
configurations. This is the most common use case and ensures you're comparing
91+
the exact same action that produces the same outputs.
92+
93+
- **`mnemonic`**: Actions are matched by their mnemonic (action type, e.g.,
94+
"GoCompile", "CppCompile"). Use this when comparing different targets that use
95+
similar build rules. For example, comparing `//old/pkg:binary` vs
96+
`//new/pkg:binary` where both are `go_binary` targets but produce different
97+
output paths. This helps identify how the same type of action differs between
98+
targets.
99+
100+
Example using mnemonic matching:
101+
102+
```starlark
103+
aquery_diff(
104+
name = "compare_go_binaries",
105+
before = "old_binary.pb",
106+
after = "new_binary.pb",
107+
match = "mnemonic", # Compare by action type instead of output path
108+
)
25109
```
26110

27-
> The file extensions are relevant; the proto decoder will be `protojson` if
28-
`.json`, `prototext` if `.textproto` and `proto` otherwise.
111+
#### Rule: `aquery_git_diff`
112+
113+
Compare aquery outputs between git commits:
114+
115+
```starlark
116+
aquery_git_diff(
117+
name = "git_compare",
118+
before = "main",
119+
after = "feature-branch",
120+
target = "//my/package:target",
121+
)
122+
```
123+
124+
**Attributes:**
125+
126+
Same as `aquery_diff`, plus:
127+
128+
| Attribute | Type | Default | Description |
129+
|-----------|----------|--------------|------------------------------------------------------------|
130+
| `target` | `string` | **required** | Bazel target to aquery (e.g., `//pkg:binary`, `deps(...)`) |
131+
| `bazel` | `string` | `"bazel"` | Path to bazel executable |
132+
| `before` | `string` | **required** | Git commit/branch/tag for baseline |
133+
| `after` | `string` | **required** | Git commit/branch/tag for comparison |
29134

135+
This rule will:
136+
1. Check for uncommitted changes (fails if found)
137+
2. Checkout `before` commit and run `bazel aquery`
138+
3. Checkout `after` commit and run `bazel aquery`
139+
4. Restore original commit
140+
5. Generate comparison report
30141

31-
An HTML report and accessory files will be written to the given `--report_dir`,
32-
which you could serve as follows:
142+
### Using the CLI
33143

144+
Generate aquery files using Bazel:
145+
146+
```bash
147+
# Binary proto format (recommended for large graphs)
148+
bazel aquery //pkg:target --output=proto > before.pb
149+
150+
# Text proto format (human-readable)
151+
bazel aquery //pkg:target --output=textproto > before.textproto
152+
153+
# JSON proto format
154+
bazel aquery //pkg:target --output=jsonproto > before.json
34155
```
35-
(cd <REPORT_DIR> && python3 -m http.server 8000) &
156+
157+
> **Supported formats**: The tool automatically detects format based on file extension:
158+
> - Binary: `.pb`, `.proto`
159+
> - Text: `.textproto`
160+
> - JSON: `.json`, `.jsonproto`
161+
162+
Run the comparison:
163+
164+
```bash
165+
aquerydiff \
166+
--before before.pb \
167+
--after after.pb \
168+
--report_dir ./output \
169+
--serve \
170+
--open
36171
```
37172

38-
> Report will look something like:
173+
**CLI Flags:**
174+
175+
- `--before` - Path to baseline aquery file
176+
- `--after` - Path to comparison aquery file
177+
- `--report_dir` - Directory to write HTML report
178+
- `--match` - Matching strategy: `output_files` (default) or `mnemonic`
179+
- `--serve` - Start web server (default: true)
180+
- `--open` - Open browser automatically (default: true)
181+
- `--unidiff` - Generate unified diffs (default: false)
182+
- `--cmpdiff` - Generate go-cmp diffs (default: true)
183+
184+
> **Note**: The report title is automatically derived from the most common target in the action graph.
185+
186+
### Report Output
187+
188+
The HTML report shows:
189+
190+
- **Actions only in before** - Removed actions
191+
- **Actions only in after** - New actions
192+
- **Non-equal actions** - Actions with changes
193+
- **Equal actions** - Unchanged actions
194+
195+
Each action displays:
196+
- Mnemonic (action type)
197+
- Output files
198+
- Links to before/after JSON/textproto representations
199+
- Colorized diffs (unified and/or go-cmp format)
200+
201+
<img width="934" alt="Example report showing action comparison" src="https://user-images.githubusercontent.com/50580/209453563-064db4dd-4068-4d2f-8bb3-35c425bfb8b5.png">
202+
203+
## Example
204+
205+
See the [examples/simple](examples/simple) directory for working examples using both rules.
206+
207+
## Contributing
208+
209+
Contributions welcome! Please open an issue or pull request.
210+
211+
## License
39212

40-
<img width="934" alt="image" src="https://user-images.githubusercontent.com/50580/209453563-064db4dd-4068-4d2f-8bb3-35c425bfb8b5.png">
213+
Apache 2.0

cmd/aquerydiff/config.go

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
package main
22

33
type config struct {
4-
target string
54
beforeFile string
65
afterFile string
76
reportDir string

cmd/aquerydiff/main.go

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ func run(args []string) error {
2727
var config config
2828

2929
flags := flag.NewFlagSet("aquerydiff", flag.ExitOnError)
30-
flags.StringVar(&config.target, "target", "", "the target under analysis")
3130
flags.StringVar(&config.beforeFile, "before", "", "filepath to aquery file (before)")
3231
flags.StringVar(&config.afterFile, "after", "", "filepath to aquery file (after)")
3332
flags.StringVar(&config.matchingStrategy, "match", "output_files", "method used to build mapping of before & after actions (output_files|mnemonic)")
@@ -104,8 +103,17 @@ func run(args []string) error {
104103
}
105104
}
106105

106+
// Derive target from the action graph (prefer before, fallback to after)
107+
target := beforeGraph.GetPrimaryTarget()
108+
if target == "" {
109+
target = afterGraph.GetPrimaryTarget()
110+
}
111+
if target == "" {
112+
target = "unknown"
113+
}
114+
107115
r := report.Html{
108-
Target: config.target,
116+
Target: target,
109117
BeforeFile: config.beforeFile,
110118
AfterFile: config.afterFile,
111119
Before: beforeGraph,

examples/simple/BUILD.bazel

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,5 +11,4 @@ aquery_diff(
1111
name = "diff",
1212
after = "after.pbtext",
1313
before = "before.pbtext",
14-
target = "deps(//cmd/aquerydiff)",
1514
)

pkg/action/graph.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,34 @@ func NewGraph(name string, container *anpb.ActionGraphContainer) (*Graph, error)
5252
}, nil
5353
}
5454

55+
// GetPrimaryTarget returns the most common target from the actions in the graph,
56+
// or empty string if there are no actions.
57+
func (g *Graph) GetPrimaryTarget() string {
58+
if len(g.Actions) == 0 {
59+
return ""
60+
}
61+
62+
// Count target occurrences
63+
targetCounts := make(map[string]int)
64+
for _, action := range g.Actions {
65+
if action.Target != "" {
66+
targetCounts[action.Target]++
67+
}
68+
}
69+
70+
// Find the most common target
71+
var maxTarget string
72+
var maxCount int
73+
for target, count := range targetCounts {
74+
if count > maxCount {
75+
maxTarget = target
76+
maxCount = count
77+
}
78+
}
79+
80+
return maxTarget
81+
}
82+
5583
func Partition(before, after ActionMap) (beforeOnly, afterOnly, both OutputPairs) {
5684
a := make(map[string]bool)
5785
b := make(map[string]bool)

pkg/action/output_pair.go

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,11 @@ type OutputPair struct {
1818
Action *dipb.Action // representative of before/after
1919
Before *dipb.Action
2020
After *dipb.Action
21+
22+
// Cached formatted text for unified diff (lazy initialized)
23+
beforeText string
24+
afterText string
25+
textCached bool
2126
}
2227

2328
func (p *OutputPair) Diff() string {
@@ -29,21 +34,30 @@ func (p *OutputPair) Diff() string {
2934
)
3035
}
3136

32-
func (p *OutputPair) UnifiedDiff() gotextdiff.Unified {
33-
var a string
34-
var b string
37+
// formatTexts computes and caches the formatted text for before/after actions.
38+
// This is done lazily and only once to avoid repeated proto cloning and formatting.
39+
func (p *OutputPair) formatTexts() {
40+
if p.textCached {
41+
return
42+
}
43+
3544
if p.Before != nil {
3645
beforeCopy := proto.Clone(p.Before).(*dipb.Action)
3746
beforeCopy.Id = ""
38-
a = protobuf.FormatProtoText(beforeCopy)
47+
p.beforeText = protobuf.FormatProtoText(beforeCopy)
3948
}
4049
if p.After != nil {
4150
afterCopy := proto.Clone(p.After).(*dipb.Action)
4251
afterCopy.Id = ""
43-
b = protobuf.FormatProtoText(afterCopy)
52+
p.afterText = protobuf.FormatProtoText(afterCopy)
4453
}
45-
edits := myers.ComputeEdits(span.URI(p.Output), a, b)
46-
return gotextdiff.ToUnified(p.Output, p.Output, a, edits)
54+
p.textCached = true
55+
}
56+
57+
func (p *OutputPair) UnifiedDiff() gotextdiff.Unified {
58+
p.formatTexts()
59+
edits := myers.ComputeEdits(span.URI(p.Output), p.beforeText, p.afterText)
60+
return gotextdiff.ToUnified(p.Output, p.Output, p.beforeText, edits)
4761
}
4862

4963
type OutputPairs []*OutputPair

pkg/protobuf/io.go

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,23 +15,33 @@ type marshaler func(m protoreflect.ProtoMessage) ([]byte, error)
1515
type unmarshaler func(b []byte, m protoreflect.ProtoMessage) error
1616

1717
func unmarshalerForFilename(filename string) (unmarshaler, string) {
18-
if filepath.Ext(filename) == ".json" {
18+
ext := filepath.Ext(filename)
19+
switch ext {
20+
case ".json", ".jsonproto":
1921
return protojson.Unmarshal, "json"
20-
}
21-
if filepath.Ext(filename) == ".text" || filepath.Ext(filename) == ".pbtext" || filepath.Ext(filename) == ".textproto" {
22+
case ".textproto":
2223
return prototext.Unmarshal, "text"
24+
case ".proto", ".pb":
25+
return proto.Unmarshal, "proto"
26+
default:
27+
// Default to binary proto for unknown extensions
28+
return proto.Unmarshal, "proto"
2329
}
24-
return proto.Unmarshal, "proto"
2530
}
2631

2732
func marshalerForFilename(filename string) marshaler {
28-
if filepath.Ext(filename) == ".json" {
33+
ext := filepath.Ext(filename)
34+
switch ext {
35+
case ".json", ".jsonproto":
2936
return protojson.Marshal
30-
}
31-
if filepath.Ext(filename) == ".textproto" {
37+
case ".textproto":
3238
return prototext.Marshal
39+
case ".proto", ".pb":
40+
return proto.Marshal
41+
default:
42+
// Default to binary proto for unknown extensions
43+
return proto.Marshal
3344
}
34-
return proto.Marshal
3545
}
3646

3747
func ReadFile(filename string, message protoreflect.ProtoMessage) error {

0 commit comments

Comments
 (0)