Skip to content

fix(cli): IDEs with plugins using BSP can now use aspect cli instead of vanilla bazel - #1370

Open
mcook42 wants to merge 7 commits into
mainfrom
fix/ide-bsp-sync
Open

fix(cli): IDEs with plugins using BSP can now use aspect cli instead of vanilla bazel#1370
mcook42 wants to merge 7 commits into
mainfrom
fix/ide-bsp-sync

Conversation

@mcook42

@mcook42 mcook42 commented Aug 4, 2026

Copy link
Copy Markdown
Member

The JetBrains Bazel plugin (bazelbsp:3.2.0) drives IDE sync by shelling out to bazel on PATH. In a repo with this project's tools/bazel hook, that hook routes build/test to aspect and rewrites every Bazel-native flag to --bazel-flag=…. Three independent defects in that path caused an IntelliJ sync to fail outright, so the only way to use the plugin was to set ASPECT_WRAPPER_SKIP=1 and give up aspect's BES streaming, remote config, and task reporting entirely. See the Build Server Protocol (BSP) used by the plugin, if curious.

Each fix is a separate commit.

1. A caller-forwarded --target_pattern_file collided with the default target pattern

run_bazel_task suppressed its default target pattern only for aspect's own --target-pattern-file arg. Bazel's spelling arrives as --bazel-flag=--target_pattern_file=<path>, which was invisible to the runner, so ctx.args.targets fell back to its default. Bazel rejected the invocation before doing any work:

ERROR: Command-line target pattern and --target_pattern_file cannot both be specified

Both sync passes died here. That pair has no last-wins rule in Bazel — it is a hard conflict — so the only decision this runner owns is whether to invent a pattern the user never typed. Every precedence question belongs to Bazel's own option parser.

A new target_patterns helper in bazel_runner.axl makes exactly that one decision, and asks rc.flag_value("--target_pattern_file", command = …) whether a pattern file is in effect. A RunCommand is the effective option set rather than just the rc file — CLI flags live in it as the synthetic <command line> source — so that answers for the forwarded spelling using the same last-wins / =-form / two-token matching as crates/bazelrc, and picks up a pattern file from .bazelrc or a --config expansion for free.

Reaching it only required the ordering to be fixed. Aspect's own --target-pattern-file was appended to base_flags, which is an input to parse_rc, forcing patterns to resolve before any rc existed. It now goes through the per-invocation flags instead, so resolution moves after the rc parse. Two consequences, both wanted:

  • The flag no longer lands in the rc's always bucket, so it is no longer expanded for every command — including any sidecar query/info reusing the active run command (query has no --target_pattern_file; it uses --query_file).
  • Precedence flips: aspect's explicit --target-pattern-file now comes after a forwarded --bazel-flag=--target_pattern_file, so Bazel's last-wins picks the aspect arg when both are given.

Only the default is suppressed. An explicit pattern still reaches Bazel alongside the flag, so Bazel itself emits the error above — and the runner's own fail("--target-pattern-file cannot be combined with command-line target patterns") is dropped, leaving one spelling of that error. The existence check on --target-pattern-file stays, since aspect resolves that path itself.

2. A caller's --build_event_binary_file was silently never written

Build::spawn appends the CLI's own --build_event_binary_file after every user flag. Bazel's option is single-valued, so last-wins meant a caller who asked for a BEP file got a path that was created but never written to. The plugin parses sync results from that file, so even with (1) fixed, it would have read an empty file.

Fixed by collecting a file sink for the caller's path in collect_bes_sinks, rather than stripping or reordering flags. The CLI's path still wins on the command line; the caller's file is recreated from Bazel's own byte stream, since file sinks share the BES reader's raw-bytes path. Reordering was not an option in any case — the CLI's own "file" is a named pipe it reads the stream from, not a file on disk.

Placing it in collect_bes_sinks centralizes every bazel-spawning task; all of them hit the same clobbering. Deliberately not routed through _drop_bazel_streamed: a local file dump is not a second upload to an endpoint Bazel already streams to, and that helper's contract already keeps items whose uri_of is empty.

Two consequences, both documented in the docstring:

  • --build_event_binary_file_upload_mode no longer governs that file. The caller's existing sink.wait() completes it before the task concludes — which is what the plugin's wait_for_upload_complete was asking for anyway.
  • --build_event_json_file / --build_event_text_file are untouched; different flags, so Bazel still writes those itself.

3. bazel config was misrouted to aspect

$ bazel config --dump_all --output=json
error: unrecognized subcommand 'config'

BAZEL_VERBS in tools/bazel was generated from bazel help's "Available commands" list, which hides config. A verb missing from that table is treated as a custom aspect task and routed to aspect. The plugin calls bazel config --dump_all --output=json during sync and parses stdout as JSON, so this failed the sync on its own.

The authoritative list is bazel help completion's BAZEL_COMMAND_LIST, which includes hidden commands — verified against Bazel 9.0.1, config is the sole difference. The regeneration recipe and why it must not be bazel help are now recorded in both the script comment and tools/bazel.md:

bazel help completion | sed -n 's/^BAZEL_COMMAND_LIST="\(.*\)"$/\1/p'

The gap also silently degraded the pre-verb disambiguation walk, since KNOWN_VERBS_STR is built from the same table.

Also ignores .idea/ and .bazelbsp/, both of which are generated by the plugin.


Changes are visible to end-users: yes

  • Searched for relevant documentation and updated as needed: yes — tools/bazel.md documents BAZEL_VERBS and now carries the regeneration source and rationale
  • Breaking change (forces users to change their own code or config): no — each change either honors a flag that was previously ignored or routes a verb that previously errored
  • Suggested release notes appear below: yes

The Aspect CLI now serves as the bazel binary for IDE and BSP tooling, such as the IntelliJ Bazel plugin. A caller-forwarded --target_pattern_file no longer collides with the CLI's default target pattern; a caller's --build_event_binary_file is now written instead of being silently clobbered by the CLI's own; and bazel config reaches Bazel through the tools/bazel wrapper rather than erroring as an unknown aspect subcommand. An IntelliJ sync no longer needs ASPECT_WRAPPER_SKIP=1, so IDE-driven builds keep BES streaming, remote config, and task reporting.

Test plan

  • New test cases added — target_patterns in bazel_runner_test.axl (default forwarded when nothing supplies patterns; suppressed by a forwarded flag; suppressed by aspect's own arg; explicit patterns always forwarded; the lookup is command-scoped). The =-form / two-token / last-wins matching is deliberately not re-asserted against a faked rc — that behavior is flag_value_list's and is covered in crates/bazelrc/src/lib.rs (flag_value_eq_form_last_wins, flag_value_two_token_form). Plus bazel_bep_file resolution and the collect_bes_sinks file-sink append in bazel/build_events_test.axl
  • Covered by existing test cases — aspect tests axl (910 cases) and the full aspect test suite (26/26) both pass
  • New CI coverage — the existing test-flags-task step in both .buildkite/pipeline.yaml and .github/workflows/ci-workflows.yaml now runs a single aspect build in the plugin's exact shape and asserts a non-empty BEP at the caller's path. The unit tests cover the two resolvers in isolation; this asserts that the composed command line is accepted by Bazel and that the file lands on disk.
  • Manual testing; please provide instructions so we can reproduce:

The failure mode was captured on main first, so each result below is a before/after rather than an assertion that nothing broke:

Check Before (on main) After
Plugin-shaped invocation cannot both be specified, exit 2 exit 0, Found 1 target from the file
Caller's BEP file never created non-empty (~48 KB locally), contains the target label
WARNING: BES was not properly closed present gone
Two-token --target_pattern_file <path> cannot both be specified exit 0, BEP non-empty (~50 KB)
Pattern file from a .bazelrc line default pattern collided exit 0 — now suppresses the default too
aspect's --target-pattern-file + a forwarded one base-flag position: forwarded won aspect's arg wins (Bazel last-wins, verified by which target built)
bazel config --dump_all --output=json unrecognized subcommand 'config', exit 2 clean JSON on stdout, exit 0
BAZEL_VERBS vs BAZEL_COMMAND_LIST differs by config identical
Default target pattern (no flag forwarded) -- //... -//exclude/... unchanged
Explicit pattern + forwarded file Bazel's collision error unchanged — still Bazel's own error, exit 2
# Build and put the binary on a stable path (see the note below on why the copy matters).
ASPECT_WRAPPER_SKIP=1 bazel build //:cli
mkdir -p target/ci && cp -f "$(ASPECT_WRAPPER_SKIP=1 bazel info bazel-bin)/crates/aspect-cli/aspect-cli" target/ci/aspect-cli

# Unit tests (these are `group = ["dev"]` tasks, not bazel test targets).
aspect dev test-bazel-runner
aspect dev test-bes-sinks
aspect tests axl

# Replay the plugin's invocation shape.
PATTERNS=$(mktemp); echo "//examples/test_states:always_pass" > "$PATTERNS"
BEP=$(mktemp); rm -f "$BEP"
aspect build \
  --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" && grep -qa always_pass "$BEP" && echo "caller's BEP written with real events"

# A pattern file from an rc file (or a --config expansion) suppresses the default too,
# which the earlier ctx.args.bazel_flags scan could not see.
RC=$(mktemp); echo "build --target_pattern_file=$PATTERNS" > "$RC"
aspect build --bazel-startup-flag=--bazelrc="$RC"

# Both spellings at once: aspect's own arg is appended last, so Bazel's last-wins picks it.
OTHER=$(mktemp); echo "//examples/test_states:always_fail" > "$OTHER"
aspect build --target-pattern-file="$PATTERNS" --bazel-flag=--target_pattern_file="$OTHER"  # builds always_pass

# Verb routing. Note 2>/dev/null, not 2>&1 — Bazel's "INFO: Invocation ID" goes to
# stderr and would corrupt the parse. The plugin reads stdout only.
comm -3 \
  <(ASPECT_WRAPPER_SKIP=1 bazel help completion | sed -n 's/^BAZEL_COMMAND_LIST="\(.*\)"$/\1/p' | tr ' ' '\n' | sed '/^$/d' | sort -u) \
  <(sed -n '/^BAZEL_VERBS=(/,/^)/p' tools/bazel | sed '1d;$d' | tr ' ' '\n' | sed '/^$/d' | sort -u)
bazel config --dump_all --output=json 2>/dev/null | python3 -c 'import json,sys; json.load(sys.stdin); print("valid JSON")'

End-to-end in IntelliJ IDEA 2026.2 with the Bazel plugin

Both the plugin's build and Sync Project actions complete against this branch. The build was confirmed from aspect's own BEP for the invocation (/tmp/<uuid>.bep.binpb), which carries the plugin's full fingerprint:

  • tool tag bazelbsp:3.2.0
  • --target_pattern_file=…/IntelliJIdea2026.2/tmp/targets-8273310712140375819
  • --build_event_binary_file=/tmp/bazel-bep-output9212484283891080652.tmp alongside --build_event_binary_file_upload_mode=wait_for_upload_complete
  • targets resolved from that file//examples/deliverable:py_deliverable, :py_deliverable2
  • outcome SUCCESS, with no cannot both be specified and no unrecognized subcommand

Two things that will waste a reviewer's time if unstated:

  1. Copy the built binary to target/ci/, as above. .aspect/version.axl prefers local sources over the pinned release, but bazel-bin is a symlink to the last-built configuration, and the IDE's own build applies a Starlark transition that repoints it at k8-fastbuild-ST-<hash>/bin — where no aspect-cli exists. The launcher then falls through to the pinned release, and the IDE reports an unrelated path "bazel/build_metadata.axl" does not exist in module aspect error, which looks like a failure of this change. This is the same hazard that .aspect/bootstrap.sh copies to target/ci/ to avoid.
  2. mcp__idea__build_project needs rebuild=true. With rebuild=false it returns isSuccess: true without invoking Bazel at all, and the wrapper trace goes to the Bazel plugin's build console, not idea.log — so neither the status nor the IDE log is usable evidence. Assert on the BEP artifact instead.

Unrelated failure worth knowing about

bazel test //... fails with CI exclusion regression: //exclude was not excluded. That is the deliberate tripwire in exclude/BUILD.bazel firing correctly: an explicit //... overrides the CLI's default pattern, so //exclude gets built. It is cause (1) in the tripwire's own message and is unrelated to this PR — CI runs aspect test with no patterns. The one-command discriminator:

aspect test //... --bazel-flag=--nobuild 2>&1 | grep -oE ' -- //\.\.\..*$'   # " -- //..."
aspect test        --bazel-flag=--nobuild 2>&1 | grep -oE ' -- //\.\.\..*$'   # " -- //... -//exclude/..."

Not measured here, reported for follow-up: aspect spawns Bazel with --ignore_all_rc_files --max_idle_secs=1200 while wrapper-passthrough verbs (config, query, info, mod) reach Bazel with no startup flags. Bazel keys its server on startup options, so the mismatch could force a server restart per sync, costing the IDE its analysis cache.

mcook42 added 5 commits August 4, 2026 13:37
The runner only suppressed its default target pattern for aspect's own
`--target-pattern-file` arg. Bazel's own spelling arriving as
`--bazel-flag=--target_pattern_file=<path>` was invisible to it, so
`ctx.args.targets` fell back to its default and Bazel rejected the
invocation outright:

    ERROR: Command-line target pattern and --target_pattern_file
           cannot both be specified

That is exactly the shape the IntelliJ Bazel plugin produces: the
`tools/bazel` wrapper rewrites every Bazel-native flag to
`--bazel-flag=`, so an IDE sync never reaches Bazel.

Suppress the default when the caller forwarded the flag and gave no
explicit patterns. Only the *default* is suppressed — explicit patterns
still reach Bazel alongside the flag so Bazel emits the error above
itself, rather than this runner growing a second spelling of it.

The new `forwarded_flag_value` helper reads `ctx.args.bazel_flags` rather
than the parsed rc because target patterns are resolved before
`parse_rc` runs: the resolved pattern file has to be in the `base_flags`
the rc is built from. `--target_pattern_file` is per-invocation and not
rc material, so nothing is lost.
`Build::spawn` appends the CLI's own `--build_event_binary_file` after
every user flag. Bazel's option is single-valued, so last-wins meant a
caller who asked for a BEP file silently got nothing — the file was
created by whoever made the temp path and never written to.

The IntelliJ Bazel plugin drives sync on exactly that flag, so even with
the target-pattern collision fixed it would parse an empty file.

Collect a file sink for the caller's path instead of stripping or
reordering flags. The CLI's path still wins on the command line; the
caller's file is re-created from Bazel's own byte stream, since file
sinks share the BES reader's raw-bytes path. Placed in
`collect_bes_sinks` so all eight bazel-spawning tasks are covered — every
one of them hit the same clobbering.

Two consequences: `--build_event_binary_file_upload_mode` no longer
governs that file (the caller's existing `sink.wait()` 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.
`BAZEL_VERBS` was generated from `bazel help`'s "Available commands"
list, which hides `config`. A verb missing from that table is treated as
a custom aspect task, so `bazel config` became `aspect config` →
`error: unrecognized subcommand 'config'`.

The IntelliJ Bazel plugin calls `bazel config --dump_all --output=json`
during sync and parses stdout as JSON, so this failed the sync outright.

Regenerate from `bazel help completion`'s `BAZEL_COMMAND_LIST`, which
includes hidden commands — verified against Bazel 9.0.1, `config` is the
sole difference. Record the regeneration recipe and *why* it must not be
`bazel help` in the comment, and in tools/bazel.md.

The gap also silently degraded the pre-verb disambiguation walk, since
KNOWN_VERBS_STR is built from the same table.
The unit tests cover the two resolvers, but nothing exercised the pair
end to end: that the composed bazel command line is one Bazel accepts,
and that the caller's BEP file actually lands on disk.

Extend the existing `test-flags-task` step in both pipelines — it already
writes the pattern file — with one `aspect build` in the shape the
IntelliJ Bazel plugin produces (Bazel's own flag spellings behind
`--bazel-flag`), asserting a non-empty BEP at the caller's path.
Both are generated: .idea/ by the IDE, .bazelbsp/ by the JetBrains Bazel
plugin (its injected aspect .bzl files), and both showed as untracked
after an IntelliJ sync.
@aspect-workflows

aspect-workflows Bot commented Aug 4, 2026

Copy link
Copy Markdown

✨ Aspect Workflows Tasks

📅 Wed Aug 5 14:37:45 UTC 2026

❌ 1 failed task

  • ❌ delivery-uncacheable [delivery] · ⏱ 39.4s · ✨ Aspect · 🐙 GitHub Actions
    💬 failed in deliver · Delivery failed (1 delivery fail)

⚠️ 3 flagged tasks

  • ⚠️ delivery-gha-debug [delivery] · ⏱ 53.2s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Delivery complete (1 delivered · 2 warn · 4 skipped)
  • ⚠️ delivery-gha [delivery] · ⏱ 53.5s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Delivery complete (1 delivered · 2 warn · 4 skipped)
  • ⚠️ delivery-uncacheable-warn [delivery] · ⏱ 13.7s · ✨ Aspect · 🐙 GitHub Actions
    💬 Delivery complete (1 warn)

✅ 29 successful tasks

  • ✅ axl-smoke-gha-bootstrap [build] · ⏱ 52.7s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (1 built)
  • ✅ run-axl-smoke [run] · ⏱ 19.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Ran //examples/deliverable:py_deliverable
  • ✅ run-axl-smoke-2 [run] · ⏱ 13.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Ran //examples/deliverable:sh_deliverable
  • ✅ axl-tests-gha-bootstrap [build] · ⏱ 24.5s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (1 built)
  • ✅ build-gha-debug [build] · ⏱ 6m 34s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (172 built)
  • ✅ build-gha [build] · ⏱ 6m 47s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (172 built)
  • ✅ build-gha-ephemeral [build] · ⏱ 35.7s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (10 built)
  • ✅ buildifier-gha-debug [buildifier] · ⏱ 32s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ buildifier-gha [buildifier] · ⏱ 54s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ delivery-no-remote-exec [delivery] · ⏱ 9.9s · ✨ Aspect · 🐙 GitHub Actions
    💬 Delivery complete (no deliveries)
  • ✅ format-gha-debug [format] · ⏱ 1m 27s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task [format] · ⏱ 1m 9s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-2 [format] · ⏱ 10.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-3 [format] · ⏱ 10.1s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-format-repeat-task-4 [format] · ⏱ 10.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ format-gha [format] · ⏱ 1m 25s · 🐙 GitHub Actions · ☑️ Check
    💬 Format complete (clean)
  • ✅ gazelle-gha-debug [gazelle] · ⏱ 28.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-from-source-gha-debug [gazelle] · ⏱ 2m 14s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-from-source-gha [gazelle] · ⏱ 1m 55s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ gazelle-gha [gazelle] · ⏱ 37.8s · 🐙 GitHub Actions · ☑️ Check
    💬 Gazelle complete (clean)
  • ✅ init-shell [build] · ⏱ 51.6s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (10 built)
  • ✅ lint-gha-debug [lint] · ⏱ 40.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Lint complete (clean)
  • ✅ lint-gha [lint] · ⏱ 39.2s · 🐙 GitHub Actions · ☑️ Check
    💬 Lint complete (clean)
  • ✅ test-gha-debug [test] · ⏱ 33.4s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (26/26 passed · 26 cached)
  • ✅ test-gha-ide-passthrough [build] · ⏱ 19.5s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel build complete (1 built)
  • ✅ test-gha-coverage [test] · ⏱ 32.8s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-gha-target-pattern-file [test] · ⏱ 33s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed · 1 cached)
  • ✅ test-gha [test] · ⏱ 9m 8s · ✨ Aspect · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (26/26 passed · 26 cached)
  • ✅ test-gha-ephemeral [test] · ⏱ 55.9s · 🐙 GitHub Actions · ☑️ Check
    💬 Bazel test complete (1/1 passed)

🔁 Reproduce

❌ delivery (delivery-uncacheable · delivery-gha-debug · delivery-gha · delivery-uncacheable-warn)

# --mode=always --track-state=false for off-runner with no state backend.
aspect delivery \
  --commit-sha=f7876f0e2318e70b77b4fbee9d092bad6749ceaf \
  --mode=always \
  --track-state=false \
  --dry-run=true

Install aspect: aspect.build/docs/cli/install


⏱ Last updated Wed Aug 5 15:35:59 UTC 2026 · 📊 GitHub API quota 50/15,000 (0% used, resets in 54m)
🚀 Powered by Aspect CLI (v0.0.0-dev)  |  Aspect Build · X · LinkedIn · YouTube

@mcook42
mcook42 requested review from jbedard and thesayyn August 4, 2026 21:07
@mcook42
mcook42 marked this pull request as ready for review August 4, 2026 21:07
@mcook42 mcook42 changed the title Fix/ide bsp sync fix(cli): ide's with plugins using BSP can now use aspect cli instead of vanilla bazel Aug 4, 2026
"""
eq_prefix = name + "="
value = ""
for i, flag in enumerate(bazel_flags):

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.

this sounds really unprincipled/workaroubd. can we understand what's really wrong and fix it?

I'd rather let bazel decide what's last to win rather than us deciding it.

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.

It was a workaround, and I should have caught this before posting, so thanks for holding me accountable.

The runner already avoided sending a target pattern when Aspect's own --target-pattern-file flag was set. The bug is that it only checked that one spelling. When the pattern file arrives as a forwarded --bazel-flag=--target_pattern_file=<path>, as an IntelliJ sync sends, the runner never sees it and falls back to its default pattern of .... Bazel then gets both that pattern and the pattern file and refuses to run, since it has no last-wins rule for that pair.

The fix is to check the flags Bazel will actually run with, rather than a specific arg. We now ask the parsed command with rc.flag_value("--target_pattern_file", ...), which detects the flag regardless of how it arrived, and skips sending a pattern if it is set. If the user typed patterns, we always send them and let Bazel report the conflict, so our own error in that case is removed.

Getting to that point took a reordering. The parsed command did not yet exist when patterns were being decided, because we were adding Aspect's own pattern file flag to the same list of flags that gets parsed. Moving that flag out of that list let target resolution happen after the parse, which is what makes rc.flag_value available to ask.

@jbedard

jbedard commented Aug 4, 2026

Copy link
Copy Markdown
Member

This seems like 3 separate issues that can have independent fixes?

@mcook42

mcook42 commented Aug 4, 2026

Copy link
Copy Markdown
Member Author

This seems like 3 separate issues that can have independent fixes?

You're right that these are three separate issues, and I could split them into three separate PRs. However, until all three of these issues are fixed, the IntelliJ/JetBrains Bazel IDE plugin won't work. The three fixes in this PR need to land before the plugin works with aspect CLI, so in my mind it was best to land them all in a single review.

I'm happy to close this PR and open three independent PRs for smaller reviews if that's preferred.

@mcook42 mcook42 changed the title fix(cli): ide's with plugins using BSP can now use aspect cli instead of vanilla bazel fix(cli): IDEs with plugins using BSP can now use aspect cli instead of vanilla bazel Aug 4, 2026
@mcook42
mcook42 marked this pull request as draft August 5, 2026 14:17
`run_bazel_task` only suppressed its target-pattern default for aspect's own
`--target-pattern-file` arg. Bazel's spelling reaching aspect as
`--bazel-flag=--target_pattern_file=<path>` — what `tools/bazel` produces for
every Bazel-native flag, and so what an IntelliJ BSP sync sends — lands in
`ctx.args.bazel_flags` instead, invisible there. `ctx.args.targets` then fell
back to its declared default `["..."]`, which reaches Bazel as residue
alongside the forwarded flag:

    ERROR: Command-line target pattern and --target_pattern_file
           cannot both be specified

That pair has no last-wins rule in Bazel — it is a hard conflict. So the only
decision this runner owns is whether to invent a pattern the user never typed;
every precedence question belongs to Bazel's own option parser.

A `RunCommand` is the *effective* option set, not just the rc file — CLI flags
live in it as the `<command line>` source — so
`rc.flag_value("--target_pattern_file", ...)` answers for the forwarded
spelling with the same last-wins / `=`-form / two-token matching as
`crates/bazelrc`. Reaching it only needed the ordering fixed: aspect's own
`--target-pattern-file` moves from `base_flags` (an *input* to `parse_rc`) to
the per-invocation `flags`, freeing patterns to resolve after the rc parse.
That also keeps the flag out of the rc's `always` bucket, so it is no longer
expanded for every command.

Explicit patterns are always forwarded, so Bazel emits the error above itself
and the runner's second spelling of it is dropped. The existence check on
`--target-pattern-file` stays, since aspect resolves that path.

Supersedes the reverted `forwarded_flag_value` scan (c993267 / 2af86d1),
which reimplemented Bazel's option parsing in AXL.
@mcook42
mcook42 marked this pull request as ready for review August 5, 2026 17:04
@mcook42
mcook42 requested a review from thesayyn August 5, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants