Skip to content
Closed
Show file tree
Hide file tree
Changes from 8 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .aspect/config.axl
Original file line number Diff line number Diff line change
Expand Up @@ -362,7 +362,8 @@ def config(ctx: ConfigContext):
# Run with: aspect dev test-bazel-flags
ctx.tasks.add(bazel_flags_tests)

# bazel_runner.axl dispatch helpers: the bazel_attempt_end/build_end seam.
# bazel_runner.axl: the bazel_attempt_end/build_end dispatch seam, plus
# target_patterns (when the `targets` default is suppressed).
# Run with: aspect dev test-bazel-runner
ctx.tasks.add(bazel_runner_tests)

Expand Down
18 changes: 16 additions & 2 deletions .buildkite/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,12 @@ steps:
aspect test --task:name test-bk
# Smoke-tests the new test-task flags. `--target-pattern-file` is forwarded
# to Bazel verbatim, so the assertion is just that aspect resolves the file
# and the run completes. `--coverage` is exercised against an sh_test rather
# and the run completes. The IDE/BSP step re-runs the same pattern file in the
# shape the IntelliJ Bazel plugin produces through the tools/bazel wrapper —
# Bazel's own flag spellings behind `--bazel-flag` — and asserts the caller's
# BEP file is actually written. The CLI appends its own
# `--build_event_binary_file` last, and the Bazel option is single-valued.
# `--coverage` is exercised against an sh_test rather
# than a rust_test: `toolchains_llvm_bootstrapped` 0.5.2 (pinned in
# MODULE.bazel) ships without `libclang_rt.profile.a`, so any rust_test under
# --collect_code_coverage fails CppLink on libld/libc shared libs (upstream
Expand All @@ -356,7 +361,16 @@ steps:
echo "# smoke: targets forwarded via --target_pattern_file" > $$PATTERNS
echo "//examples/test_states:always_pass" >> $$PATTERNS
aspect test --task:name test-bk-target-pattern-file --target-pattern-file=$$PATTERNS
rm -f $$PATTERNS

echo "--- :aspect: IDE/BSP shape — forwarded --target_pattern_file + --build_event_binary_file"
BEP=$$(mktemp)
aspect build --task:name test-bk-ide-passthrough \
--bazel-flag=--target_pattern_file=$$PATTERNS \
--bazel-flag=--build_event_binary_file=$$BEP \
--bazel-flag=--build_event_binary_file_upload_mode=wait_for_upload_complete \
--bazel-flag=--tool_tag=bazelbsp:3.2.0
test -s $$BEP || { echo "caller's --build_event_binary_file was not written"; exit 1; }
rm -f $$BEP $$PATTERNS

echo "--- :aspect: aspect test --coverage (+ --coverage-report + --coverage-tool)"
REPORT=$$(mktemp)
Expand Down
18 changes: 16 additions & 2 deletions .github/workflows/ci-workflows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,12 @@ jobs:
# Smoke-tests the new test-task flags. Ported from the Buildkite
# `test-flags-task` step. `--target-pattern-file` is forwarded to Bazel
# verbatim, so the assertion is just that aspect resolves the file and the run
# completes. `--coverage` is exercised against an sh_test rather than a
# completes. The IDE/BSP step re-runs the same pattern file in the shape the
# IntelliJ Bazel plugin produces through the tools/bazel wrapper — Bazel's own
# flag spellings behind `--bazel-flag` — and asserts the caller's BEP file is
# actually written. The CLI appends its own `--build_event_binary_file` last,
# and the Bazel option is single-valued.
# `--coverage` is exercised against an sh_test rather than a
# rust_test: toolchains_llvm_bootstrapped 0.5.2 (pinned in MODULE.bazel) ships
# without libclang_rt.profile.a, so any rust_test under --collect_code_coverage
# fails CppLink (upstream hermeticbuild/hermetic-llvm#318, fixed in #468 — not
Expand All @@ -636,7 +641,16 @@ jobs:
echo "# smoke: targets forwarded via --target_pattern_file" > "$PATTERNS"
echo "//examples/test_states:always_pass" >> "$PATTERNS"
aspect test --task:name test-gha-target-pattern-file --target-pattern-file="$PATTERNS"
rm -f "$PATTERNS"

echo "--- IDE/BSP shape — forwarded --target_pattern_file + --build_event_binary_file"
BEP=$(mktemp)
aspect build --task:name test-gha-ide-passthrough \
--bazel-flag=--target_pattern_file="$PATTERNS" \
--bazel-flag=--build_event_binary_file="$BEP" \
--bazel-flag=--build_event_binary_file_upload_mode=wait_for_upload_complete \
--bazel-flag=--tool_tag=bazelbsp:3.2.0
test -s "$BEP" || { echo "caller's --build_event_binary_file was not written"; exit 1; }
rm -f "$BEP" "$PATTERNS"

echo "--- aspect test --coverage (+ --coverage-report + --coverage-tool)"
REPORT=$(mktemp)
Expand Down
6 changes: 5 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -14,4 +14,8 @@ site
# macOS desktop services files
.DS_Store

.claude
.claude

# IDE project state and the JetBrains Bazel plugin's generated aspect files
.idea/
.bazelbsp/
45 changes: 44 additions & 1 deletion crates/aspect-cli/src/builtins/aspect/bazel/build_events.axl
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,11 @@ once rather than twice. Because that can leave no sink at all — and the Aspect
Web UI link keys on an id only a sink mints — a task pairs the collect call with
`bes_streamed_by_bazel`, which redirects the link to Bazel's invocation id.

`collect_bes_sinks` also covers the one Bazel-facing BEP flag the CLI would
otherwise clobber: a caller's `--build_event_binary_file` becomes a CLI file sink,
because `Build::spawn` appends the CLI's own path to that single-valued option
last and last wins. See `collect_bes_sinks` for what that changes.

The Aspect login JWT is attached to an Aspect-owned backend's sink metadata when
the user has not supplied their own `authorization` header — see
`aspect_endpoint_auth.axl` for the host gate and best-effort credential
Expand Down Expand Up @@ -55,6 +60,22 @@ def bazel_bes_backend(rc, command: str) -> str:
return ""
return rc.flag_value("--bes_backend", command = command) or ""

def bazel_bep_file(rc, command: str) -> str:
"""The `--build_event_binary_file` path the caller asked Bazel to write, or `""`.

Reads through the run command, so it sees the flag wherever it came from —
`--bazel-flag=--build_event_binary_file=…`, a `.bazelrc`, or an expanded
`--config`. `rc` may be `None` for a caller with no run command to consult.

Nobody but the caller sets this: the CLI's own BEP file is appended inside
`Build::spawn`, after rc expansion, and never passes through the rc.

Public so `build_events_test.axl` can assert the resolution.
"""
if rc == None:
return ""
return rc.flag_value("--build_event_binary_file", command = command) or ""

def _drop_bazel_streamed(items: list, uri_of, bazel_backend: str) -> list:
"""`items` minus those whose `uri_of(item)` names the same endpoint as Bazel's
own `bazel_backend`.
Expand Down Expand Up @@ -106,16 +127,38 @@ def collect_bes_sinks(ctx, bazel_trait, rc, command: str = "build", extra_backen
Sinks to the endpoint Bazel's own `--bes_backend` uploads to are dropped from
both groups so the invocation is streamed once — see `_drop_bazel_streamed`.

Last comes a file sink for the caller's own `--build_event_binary_file`, when
they asked for one. Bazel cannot write two: the option is single-valued and
`Build::spawn` appends the CLI's path last, so the caller's file would
silently never be written. Writing it as a sink instead re-creates it from
Bazel's own byte stream — the file sinks share the BES reader's raw-bytes
path, so the result is what Bazel would have written. This is what lets IDE /
BSP tooling (the IntelliJ Bazel plugin) drive builds through the
`tools/bazel` wrapper and still get its BEP back. Two consequences worth
knowing: `--build_event_binary_file_upload_mode` no longer governs that file
(the caller's `wait()` on the returned sinks is what completes it, before the
task concludes), and `--build_event_json_file` / `--build_event_text_file`
are untouched — different flags, so Bazel still writes those itself.

The one call a bazel-spawning task makes; pair it with
`bes_streamed_by_bazel` to keep the Aspect Web UI link resolvable. The caller
`wait()`s the returned sinks after the build.
"""
bazel_backend = bazel_bes_backend(rc, command)
return (
sinks = (
collect_bes_from_args(ctx, extra_backends = extra_backends, bazel_backend = bazel_backend) +
_drop_bazel_streamed(list(bazel_trait.build_event_sinks), lambda s: s.uri, bazel_backend)
)

# Not routed through `_drop_bazel_streamed`: a file is a local dump, not a
# second upload to an endpoint Bazel already streams to.
bep_file = bazel_bep_file(rc, command)
if bep_file:
trace.event("bes.caller_bep_file", fields = {"path": bep_file, "command": command})
sinks.append(bazel.build_events.file(path = bep_file))

return sinks

def dropped_bes_backends(ctx, bazel_trait, rc, command: str = "build", extra_backends = []) -> list[str]:
"""The BES endpoints `collect_bes_sinks` skipped because Bazel uploads to them
itself, deduped. Same inputs as that call — pass the same arguments.
Expand Down
45 changes: 43 additions & 2 deletions crates/aspect-cli/src/builtins/aspect/bazel/build_events_test.axl
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
"""Tests for `bazel/build_events.axl` — the BES upload summary line, the gRPC-sink
filter shared by the announce/summary helpers, sink collection, and the
duplicate-stream drop that keeps the CLI from streaming to an endpoint Bazel's
own `--bes_backend` already uploads to.
own `--bes_backend` already uploads to, and the file-sink tee that keeps a
caller's `--build_event_binary_file` from being silently clobbered.

Run with:
aspect dev test-bes-sinks
"""

load("@aspect//bazel/build_events.axl", "bazel_bes_backend", "bes_results_line", "bes_streamed_by_bazel", "bes_upload_line", "collect_bes_from_args", "collect_bes_sinks", "dropped_bes_backends", "grpc_backends")
load("@aspect//bazel/build_events.axl", "bazel_bep_file", "bazel_bes_backend", "bes_results_line", "bes_streamed_by_bazel", "bes_upload_line", "collect_bes_from_args", "collect_bes_sinks", "dropped_bes_backends", "grpc_backends")

def _eq(label, got, want):
if got != want:
Expand Down Expand Up @@ -248,6 +249,44 @@ def _test_collect_bes_sinks(_):
[None, "grpcs://bes.other.example.com"],
)

def _test_bazel_bep_file(_):
"""`bazel_bep_file` reads the caller's `--build_event_binary_file` off the run
command, so it sees the flag from `--bazel-flag=`, a `.bazelrc`, or a
`--config` expansion alike."""
_eq("caller asked for one", bazel_bep_file(_fake_rc({"--build_event_binary_file": "/tmp/bep.binpb"}), "build"), "/tmp/bep.binpb")
_eq("flag unset", bazel_bep_file(_fake_rc({}), "build"), "")
_eq("no run command", bazel_bep_file(None, "build"), "")

def _test_collect_bes_sinks_tees_caller_bep_file(_):
"""A caller's `--build_event_binary_file` becomes a trailing file sink, so the
path they asked for is written from Bazel's own byte stream.

Without it their file is silently empty: the Bazel option is single-valued and
`Build::spawn` appends the CLI's own path last."""
trait = _fake_trait([struct(uri = "grpcs://bes.other.example.com")])

def uris(rc_values):
ctx = _fake_ctx(bes_backends = [])
return [s.uri for s in collect_bes_sinks(ctx, trait, _fake_rc(rc_values))]

_eq(
"file sink appended after the trait's own",
uris({"--build_event_binary_file": "/tmp/bep.binpb"}),
["grpcs://bes.other.example.com", None],
)
_eq("no BEP flag, no extra sink", uris({}), ["grpcs://bes.other.example.com"])

# The tee is a local dump, so it survives the duplicate-stream drop that
# suppresses a CLI sink to the endpoint Bazel itself uploads to.
duplicate = "grpcs://bes.acme.aspect.build"
ctx = _fake_ctx(bes_backends = [duplicate])
rc = _fake_rc({"--bes_backend": duplicate, "--build_event_binary_file": "/tmp/bep.binpb"})
_eq(
"kept while every gRPC sink is suppressed",
[s.uri for s in collect_bes_sinks(ctx, _fake_trait([struct(uri = duplicate)]), rc)],
[None],
)

def _test_bes_streamed_by_bazel(_):
"""True only when Bazel's `--bes_backend` is the runner's own Aspect BES
backend — the case where the Web UI still holds the invocation (under
Expand Down Expand Up @@ -280,6 +319,8 @@ _UNIT_TESTS = [
_test_bazel_bes_backend,
_test_collect_drops_duplicate_backends,
_test_collect_bes_sinks,
_test_bazel_bep_file,
_test_collect_bes_sinks_tees_caller_bep_file,
_test_bes_streamed_by_bazel,
]

Expand Down
78 changes: 65 additions & 13 deletions crates/aspect-cli/src/builtins/aspect/private/lib/bazel_runner.axl
Original file line number Diff line number Diff line change
Expand Up @@ -218,6 +218,38 @@ def _emit_terminal(ctx, lifecycle, command, data, exit_code, targets = []):
flagged = final_status == "warning",
)


def target_patterns(explicit: bool, cli_targets: list[str], own_pattern_file: str, rc, command: str) -> list[str]:
"""The target patterns to place on Bazel's command line.

Bazel rejects an invocation carrying both command-line patterns and
`--target_pattern_file` — that pair has no last-wins rule — so the single
decision here is whether to materialize the `targets` arg's declared
default (`["..."]`), a pattern the user never typed. Explicit patterns
always pass through: when they conflict with a pattern file, Bazel reports
it, and this runner does not grow a second spelling of that error.

A pattern file counts no matter which spelling delivered it: aspect's own
`--target-pattern-file` arrives as `own_pattern_file`, while Bazel's
`--target_pattern_file` reaches `rc` — from `--bazel-flag`, `.bazelrc`, or
a `--config` expansion alike, resolved with Bazel's own last-wins parsing.

Args:
explicit: whether the user actually passed target patterns
(`ctx.args.is_explicit("targets")`) rather than
inheriting the arg's default.
cli_targets: `ctx.args.targets` — never empty (the arg declares
`default = ["..."]`, `minimum = 1`).
own_pattern_file: value of aspect's `--target-pattern-file`, or `""`.
rc: the active `RunCommand`; only `flag_value` is used.
command: Bazel subcommand whose options `rc` resolves.
"""
if explicit:
return cli_targets
if own_pattern_file or rc.flag_value("--target_pattern_file", command = command):
return []
return cli_targets

def run_bazel_task(ctx: TaskContext, command: str, targets = None) -> TaskConclusion:
"""Shared impl for build/test tasks.

Expand Down Expand Up @@ -258,26 +290,22 @@ def run_bazel_task(ctx: TaskContext, command: str, targets = None) -> TaskConclu
deployment = deployment_endpoint_flags(ctx)
base_flags.extend(deployment.base_flags)

if targets == None:
# --target-pattern-file is only honored when the task declares it.
pattern_file = getattr(ctx.args, "target_pattern_file", "") if hasattr(ctx.args, "target_pattern_file") else ""
if pattern_file:
if ctx.args.is_explicit("targets"):
fail("--target-pattern-file cannot be combined with command-line target patterns")
if not ctx.std.fs.exists(pattern_file):
fail("--target-pattern-file: file not found: " + pattern_file)
base_flags.append("--target_pattern_file=" + pattern_file)
targets = []
else:
targets = ctx.args.targets
# Patterns resolve after the rc parse below, so `setup` opens with what the
# user stated and the spawn refines it to the resolved list.
if targets != None:
subject = " ".join(targets)
elif ctx.args.is_explicit("targets"):
subject = " ".join(ctx.args.targets)
else:
subject = ""

data = init_data()

# The single pre-task `setup` phase: status-surface init + first render, rc
# parse + `use_rc`, health checks. The active run command drives the
# build/test below (a failed health check concludes the surface and fails
# the task inside setup_phase).
rc = setup_phase(ctx, lifecycle, " ".join(targets), "bazel_results", data, hc_trait, bazel_trait, command, bazel_base_flags = base_flags)
rc = setup_phase(ctx, lifecycle, subject, "bazel_results", data, hc_trait, bazel_trait, command, bazel_base_flags = base_flags)
announce_version, announce_command = resolve_bazel_announce(ctx)

# Announced with the first spawn below (the streams belong to the spawn) and
Expand All @@ -299,6 +327,26 @@ def run_bazel_task(ctx: TaskContext, command: str, targets = None) -> TaskConclu
invocation_flags = aspect_endpoint_auth_flags(ctx, rc, command)
invocation_flags.extend(bes_results_url_flag(ctx, rc, bes_sinks, deployment, command))

if targets == None:
# aspect's own `--target-pattern-file` (declared by build/test only) is a
# per-invocation flag, not rc material — and keeping it out of the parsed
# rc is what lets `target_patterns` ask `rc` about the forwarded
# spelling. Forwarded rather than expanded into argv, since the flag
# exists to bypass OS command-line length limits.
own_pattern_file = getattr(ctx.args, "target_pattern_file", "") if hasattr(ctx.args, "target_pattern_file") else ""
if own_pattern_file:
if not ctx.std.fs.exists(own_pattern_file):
fail("--target-pattern-file: file not found: " + own_pattern_file)
invocation_flags.append("--target_pattern_file=" + own_pattern_file)

targets = target_patterns(
ctx.args.is_explicit("targets"),
ctx.args.targets,
own_pattern_file,
rc,
command,
)

# The same viewer reaches the CI surfaces through `data` — they key the link on the
# sink's own id rather than reading it off the command line. A property of the
# resolved flags, so it is restored onto each retry's fresh `data`.
Expand Down Expand Up @@ -369,6 +417,10 @@ def run_bazel_task(ctx: TaskContext, command: str, targets = None) -> TaskConclu
# work, just attempted again.
emoji = "🧪" if command == "test" else "🔨",
),
# The resolved patterns are only known after the rc parse, so the
# `setup` surface opened without them; name them here. A pattern-file
# run resolves to no patterns, and `""` means "no change".
subject = " ".join(targets),
)

# Disclose what this bazel call was wired with, after the spawn phase
Expand Down
Loading
Loading