Skip to content

Commit 9634a23

Browse files
authored
Update bzlmod (#11)
* update example * Add diff syntax hl * Remove "target" attr
1 parent 438363d commit 9634a23

File tree

23 files changed

+4354
-64
lines changed

23 files changed

+4354
-64
lines changed

.bazelversion

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
8.3.1
2+

MODULE.bazel

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ module(
1010
bazel_dep(name = "rules_proto", version = "7.1.0")
1111
bazel_dep(name = "rules_go", version = "0.57.0")
1212
bazel_dep(name = "gazelle", version = "0.45.0")
13-
bazel_dep(name = "build_stack_rules_proto", version = "4.1.0")
13+
bazel_dep(name = "build_stack_rules_proto", version = "4.1.1")
1414

1515
# -------------------------------------------------------------------
1616
# Configuration: Go
@@ -33,7 +33,7 @@ use_repo(
3333
# Configuration: Protobuf Deps
3434
# -------------------------------------------------------------------
3535

36-
proto_repository = use_extension("@build_stack_rules_proto//extensions:proto_repository.bzl", "proto_repository", dev_dependency = True)
36+
proto_repository = use_extension("@build_stack_rules_proto//extensions:proto_repository.bzl", "proto_repository")
3737
proto_repository.archive(
3838
name = "protobufapis",
3939
build_directives = [
@@ -51,7 +51,7 @@ proto_repository.archive(
5151
],
5252
build_file_generation = "clean",
5353
build_file_proto_mode = "file",
54-
cfgs = ["@//:rules_proto_config.yaml"],
54+
cfgs = ["//:rules_proto_config.yaml"],
5555
deleted_files = [
5656
"google/protobuf/*test*.proto",
5757
"google/protobuf/*unittest*.proto",

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.
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 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: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,13 @@
11
package main
22

33
type config struct {
4-
beforeFile string
5-
afterFile string
6-
reportDir string
4+
beforeFile string
5+
afterFile string
6+
reportDir string
7+
matchingStrategy string
8+
port string
9+
unidiff bool
10+
cmpdiff bool
11+
serve bool
12+
open bool
713
}

cmd/aquerydiff/main.go

Lines changed: 69 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,10 @@ import (
44
"flag"
55
"fmt"
66
"log"
7+
"net/http"
78
"os"
9+
"os/exec"
10+
"runtime"
811

912
anpb "github.com/bazelbuild/bazelapis/src/main/protobuf/analysis_v2"
1013
"github.com/stackb/bazel-aquery-differ/pkg/action"
@@ -26,8 +29,14 @@ func run(args []string) error {
2629
flags := flag.NewFlagSet("aquerydiff", flag.ExitOnError)
2730
flags.StringVar(&config.beforeFile, "before", "", "filepath to aquery file (before)")
2831
flags.StringVar(&config.afterFile, "after", "", "filepath to aquery file (after)")
32+
flags.StringVar(&config.matchingStrategy, "match", "output_files", "method used to build mapping of before & after actions (output_files|mnemonic)")
2933
flags.StringVar(&config.reportDir, "report_dir", "", "path to directory where report files should be written")
30-
if err := flags.Parse(os.Args[1:]); err != nil {
34+
flags.StringVar(&config.port, "port", "8000", "port number to use when serving content")
35+
flags.BoolVar(&config.unidiff, "unidiff", false, "compute unidiffs (can be slow)")
36+
flags.BoolVar(&config.cmpdiff, "cmpdiff", true, "compute go-cmp diffs (usually fast)")
37+
flags.BoolVar(&config.serve, "serve", false, "start webserver")
38+
flags.BoolVar(&config.open, "open", false, "open browser to webserver URL")
39+
if err := flags.Parse(args); err != nil {
3140
return err
3241
}
3342

@@ -64,7 +73,21 @@ func run(args []string) error {
6473
return err
6574
}
6675

67-
beforeOnly, afterOnly, both := action.Partition(beforeGraph.OutputMap, afterGraph.OutputMap)
76+
var mapper action.ActionMapper
77+
switch config.matchingStrategy {
78+
case "output_files":
79+
mapper = action.NewOutputFilesMap
80+
case "mnemonic":
81+
mapper = action.NewMnemonicFileMap
82+
default:
83+
return fmt.Errorf("unknown matching strategy '%s'", config.matchingStrategy)
84+
}
85+
86+
beforeOnly, afterOnly, both := action.Partition(
87+
mapper(beforeGraph.Actions),
88+
mapper(afterGraph.Actions),
89+
)
90+
6891
var equal action.OutputPairs
6992
var nonEqual action.OutputPairs
7093

@@ -80,7 +103,17 @@ func run(args []string) error {
80103
}
81104
}
82105

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+
83115
r := report.Html{
116+
Target: target,
84117
BeforeFile: config.beforeFile,
85118
AfterFile: config.afterFile,
86119
Before: beforeGraph,
@@ -89,6 +122,8 @@ func run(args []string) error {
89122
AfterOnly: afterOnly,
90123
Equal: equal,
91124
NonEqual: nonEqual,
125+
Unidiff: config.unidiff,
126+
Cmpdiff: config.cmpdiff,
92127
}
93128

94129
log.Printf("Generating report in: %s", config.reportDir)
@@ -99,5 +134,37 @@ func run(args []string) error {
99134

100135
log.Printf("aquerydiff report available at <%s>", config.reportDir)
101136

137+
if config.serve {
138+
log.Printf("Starting webserver on port %s, serving %s", config.port, config.reportDir)
139+
http.Handle("/", http.FileServer(http.Dir(config.reportDir)))
140+
141+
if config.open {
142+
url := "http://localhost:" + config.port
143+
log.Printf("Opening browser to %s", url)
144+
if err := openBrowser(url); err != nil {
145+
log.Printf("Failed to open browser: %v", err)
146+
}
147+
}
148+
149+
return http.ListenAndServe(":"+config.port, nil)
150+
}
151+
102152
return nil
103153
}
154+
155+
func openBrowser(url string) error {
156+
var cmd string
157+
var args []string
158+
159+
switch runtime.GOOS {
160+
case "windows":
161+
cmd = "cmd"
162+
args = []string{"/c", "start"}
163+
case "darwin":
164+
cmd = "open"
165+
default: // "linux", "freebsd", "openbsd", "netbsd"
166+
cmd = "xdg-open"
167+
}
168+
args = append(args, url)
169+
return exec.Command(cmd, args...).Start()
170+
}

0 commit comments

Comments
 (0)