Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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 @@ -367,7 +367,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
11 changes: 10 additions & 1 deletion .buildkite/pipeline.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -329,7 +329,11 @@ 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` — asserting
# that the composed command line is one Bazel accepts.
# `--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,6 +360,11 @@ 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

echo "--- :aspect: IDE/BSP shape — forwarded --target_pattern_file"
aspect build --task:name test-bk-ide-target-pattern-file \
--bazel-flag=--target_pattern_file=$$PATTERNS \
--bazel-flag=--tool_tag=bazelbsp:3.2.0
rm -f $$PATTERNS

echo "--- :aspect: aspect test --coverage (+ --coverage-report + --coverage-tool)"
Expand Down
11 changes: 10 additions & 1 deletion .github/workflows/ci-workflows.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -613,7 +613,11 @@ 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` — asserting that the composed
# command line is one Bazel accepts.
# `--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,6 +640,11 @@ 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"

echo "--- IDE/BSP shape — forwarded --target_pattern_file"
aspect build --task:name test-gha-ide-target-pattern-file \
--bazel-flag=--target_pattern_file="$PATTERNS" \
--bazel-flag=--tool_tag=bazelbsp:3.2.0
rm -f "$PATTERNS"

echo "--- aspect test --coverage (+ --coverage-report + --coverage-tool)"
Expand Down
77 changes: 64 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,37 @@ 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]:

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can rc have a type? I have no idea what it is...

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I traced it back to lifecycle.axl.setup_phase where it's defined as

RunCommand | None: the active run command with `bazel_trait` (also
registered via `ctx.bazel.use_rc`), else `None`. Does not return when a
health check fails (step 4).

There are a few other places where the rc value is passed around, so to type this for real I'd probably need to follow up with another PR to correctly type the rc value throughout the call chain

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe you can do that in a followup if the context is in your (or claudes) brain atm?

"""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 +289,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 +326,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)

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Validate missing pattern files before setup

When a user passes aspect build/test --target-pattern-file with a nonexistent path, this fail() now runs after setup_phase() has already sent the initial running TaskUpdate, and it aborts before the normal _emit_terminal() path sends final=True. In CI with the GitHub/Buildkite lifecycle handlers that initial surface/check is left in the setup/running state (or later orphaned) instead of being marked as the task failure; keep this validation before setup_phase() or emit a terminal failed update before aborting.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mcook42 please read over this and debate if it's worth thinking about

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 +416,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
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,17 @@ bazel-driving task calls after `build.wait()` / `test.wait()` to fire
helpers are short, but they're the contract every task depends on —
silent breakage here would skip every Workflows-feature recovery hook.

Also covers `target_patterns`, which decides whether the `targets` arg's
default reaches Bazel — get that wrong and a caller-forwarded
`--target_pattern_file` collides with an invented pattern, which Bazel
rejects outright.

Run with:
aspect dev test-bazel-runner
"""

load("@aspect//bazel.axl", "BazelTrait", "dispatch_bazel_attempt_end", "dispatch_bazel_build_end")
load("./bazel_runner.axl", "target_patterns")

def _eq(label, got, want):
if got != want:
Expand Down Expand Up @@ -84,13 +90,65 @@ def _test_attempt_dispatch_does_not_fire_build_end(ctx: TaskContext) -> None:
_eq("attempt fired", recorder["attempt"], [("a", 0)])
_eq("build_end not fired", recorder["build_end"], [])

def _fake_rc(values = {}):
"""Stand-in for a `RunCommand`: `target_patterns` calls only `flag_value`.
Same shape as the fakes in `bazel_flags_test.axl` /
`deployment_flags_test.axl`, keyed `{command: {flag: value}}` so a test can
pin that the lookup is command-scoped. The real last-wins / `=`-form /
two-token matching lives in `crates/bazelrc` and is tested there."""
return struct(flag_value = lambda name, command: values.get(command, {}).get(name))

def _test_default_reaches_bazel_when_nothing_else_supplies_patterns(_):
"""No pattern file anywhere → the arg default is the target list."""
_eq(
"default pattern forwarded",
target_patterns(False, ["..."], "", _fake_rc(), "build"),
["..."],
)

def _test_forwarded_flag_suppresses_the_default(_):
"""`--bazel-flag=--target_pattern_file=…` with no positionals: Bazel must
see the flag and no residue, or it fails the invocation outright."""
rc = _fake_rc({"build": {"--target_pattern_file": "/tmp/p"}})
_eq("default suppressed", target_patterns(False, ["..."], "", rc, "build"), [])

def _test_explicit_patterns_always_reach_bazel(_):
"""Explicit patterns alongside a forwarded pattern file are forwarded as-is
so Bazel reports the conflict — the runner owns no second spelling of it."""
rc = _fake_rc({"build": {"--target_pattern_file": "/tmp/p"}})
_eq(
"explicit patterns preserved",
target_patterns(True, ["//foo:bar"], "", rc, "build"),
["//foo:bar"],
)

def _test_aspect_own_flag_suppresses_the_default(_):
"""aspect's own `--target-pattern-file` is resolved before the rc exists, so
it is passed in directly rather than read back off `rc`."""
_eq(
"default suppressed",
target_patterns(False, ["..."], "/tmp/p", _fake_rc(), "build"),
[],
)

def _test_lookup_is_command_scoped(_):
"""A pattern file set for `build` must not suppress the default for a
`test` run — `flag_value` is asked about the running subcommand."""
rc = _fake_rc({"build": {"--target_pattern_file": "/tmp/p"}})
_eq("test unaffected", target_patterns(False, ["..."], "", rc, "test"), ["..."])

def _test_impl(ctx: TaskContext) -> int:
_test_attempt_dispatch_empty_is_noop(ctx)
_test_build_end_dispatch_empty_is_noop(ctx)
_test_attempt_dispatch_invokes_in_order(ctx)
_test_build_end_dispatch_invokes_in_order(ctx)
_test_attempt_dispatch_does_not_fire_build_end(ctx)
print("bazel_runner_test.axl: OK (5 sections)")
_test_default_reaches_bazel_when_nothing_else_supplies_patterns(ctx)
_test_forwarded_flag_suppresses_the_default(ctx)
_test_explicit_patterns_always_reach_bazel(ctx)
_test_aspect_own_flag_suppresses_the_default(ctx)
_test_lookup_is_command_scoped(ctx)
print("bazel_runner_test.axl: OK (10 sections)")
return 0

bazel_runner_tests = task(
Expand Down
Loading