From 33467839ca1db734a440dfb69626ee3364eae24e Mon Sep 17 00:00:00 2001 From: Simon Jensen Date: Thu, 28 May 2026 14:27:01 +0200 Subject: [PATCH] feat(manifest/bazel): customer flag passthrough for matrix builds and non-conventional hubs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds three repeatable CLI flags so customers can drive the underlying bazel invocations without having to fork the orchestrator: --bazel-flag= appended to every subcommand (cquery, query, mod show_extension, mod dump_repo_mapping) after the orchestrator's own flags. Use for matrix-cell selectors: --bazel-flag=--repo_env=SCALA_VERSION=2.13.18 --bazel-flag=--config=ci-scala-2-13 --bazel-flag=--platforms=//tools:linux_x86_64 --bazel-startup-flag= injected into the startup-flag prefix BEFORE the subcommand, after the orchestrator's startup flags (--bazelrc, --output_user_root, --output_base). Use for host-side knobs: --bazel-startup-flag=--host_jvm_args=-Xmx2g --bazel-maven-repo= appended to the candidate Maven hub list. Use on legacy WORKSPACE workspaces whose hubs use non-conventional names that the conventional probe list doesn't cover, or on custom Bzlmod extensions `mod show_extension` doesn't enumerate: --bazel-maven-repo=my_jars --bazel-maven-repo=test_maven (repeatable) `BazelQueryOptions` gains `extraBazelFlags` and `extraBazelStartupFlags`; the centralised `buildStartupFlags` and the new `userBazelFlags` helpers thread them through every argv builder uniformly (probe cquery, metadata cquery in bazel-cquery, query, mod show_extension, mod dump_repo_mapping). `ExtractBazelOptions` gains the matching three fields, defaulted to undefined when no CLI override was supplied. Flag passthrough is verbatim — Bazel's last-wins precedence handles conflicts between socket.json defaults (`bazelFlags`) and CLI overrides (`extraBazelFlags`). No allowlist; the trust model is the same as running `bazel` directly, and per-invocation `--output_user_root` isolation prevents a hostile flag from poisoning shared state. Tests cover argv shape for both extra-flag arrays (placement before subcommand for startup flags; placement after standard subcommand flags for trailing flags), the cquery argv-shape test, and the extraMavenRepoNames threading end-to-end. --- src/commands/manifest/bazel/bazel-cquery.mts | 13 +++++- .../manifest/bazel/bazel-cquery.test.mts | 15 ++++++ .../manifest/bazel/bazel-query-runner.mts | 36 ++++++++++++--- .../bazel/bazel-query-runner.test.mts | 46 +++++++++++++++++++ .../manifest/bazel/cmd-manifest-bazel.mts | 34 ++++++++++++++ .../manifest/bazel/extract_bazel_to_maven.mts | 13 ++++++ 6 files changed, 149 insertions(+), 8 deletions(-) diff --git a/src/commands/manifest/bazel/bazel-cquery.mts b/src/commands/manifest/bazel/bazel-cquery.mts index 819cce39c..f2659c315 100644 --- a/src/commands/manifest/bazel/bazel-cquery.mts +++ b/src/commands/manifest/bazel/bazel-cquery.mts @@ -75,7 +75,10 @@ function buildMetadataCqueryExpr(repoName: string): string { } // Build the full cquery argv for a per-repo metadata cquery. Exposed for -// argv-shape unit tests without touching `spawn`. +// argv-shape unit tests without touching `spawn`. The startup-flag +// composition mirrors `bazel-query-runner`'s `buildStartupFlags` so +// customer `--bazel-startup-flag` values land before the subcommand and +// `--bazel-flag` values land after the standard cquery flags. export function buildMetadataCqueryArgv( repoName: string, opts: BazelQueryOptions, @@ -90,7 +93,13 @@ export function buildMetadataCqueryArgv( if (opts.bazelOutputBase) { startup.push(`--output_base=${opts.bazelOutputBase}`) } - const userFlags = splitBazelFlags(opts.bazelFlags) + if (opts.extraBazelStartupFlags?.length) { + startup.push(...opts.extraBazelStartupFlags) + } + const userFlags = [ + ...splitBazelFlags(opts.bazelFlags), + ...(opts.extraBazelFlags ?? []), + ] return [ ...startup, 'cquery', diff --git a/src/commands/manifest/bazel/bazel-cquery.test.mts b/src/commands/manifest/bazel/bazel-cquery.test.mts index 7323c0743..651da86d3 100644 --- a/src/commands/manifest/bazel/bazel-cquery.test.mts +++ b/src/commands/manifest/bazel/bazel-cquery.test.mts @@ -114,6 +114,21 @@ describe('buildMetadataCqueryArgv', () => { expect(argv).toContain('--repo_env=SCALA_VERSION=2.13.18') }) + it('threads extraBazelStartupFlags ahead of cquery and extraBazelFlags after the standard flags', () => { + const argv = buildMetadataCqueryArgv('maven', { + bin: 'bazel', + cwd: '/r', + invocationFlags: [], + extraBazelStartupFlags: ['--host_jvm_args=-Xmx2g'], + extraBazelFlags: ['--config=ci'], + }) + const cqueryIdx = argv.indexOf('cquery') + const jvmIdx = argv.indexOf('--host_jvm_args=-Xmx2g') + const configIdx = argv.indexOf('--config=ci') + expect(jvmIdx).toBeLessThan(cqueryIdx) + expect(configIdx).toBeGreaterThan(cqueryIdx) + }) + it('includes invocationFlags between subcommand and target expression', () => { const argv = buildMetadataCqueryArgv('maven', { bin: 'bazel', diff --git a/src/commands/manifest/bazel/bazel-query-runner.mts b/src/commands/manifest/bazel/bazel-query-runner.mts index b53b02e2d..3c13d5250 100644 --- a/src/commands/manifest/bazel/bazel-query-runner.mts +++ b/src/commands/manifest/bazel/bazel-query-runner.mts @@ -19,6 +19,17 @@ export type BazelQueryOptions = { // orchestrator mkdtemp's a fresh path per invocation; the legacy PyPI // path may leave it unset for now. outputUserRoot?: string + // Customer-supplied `--bazel-flag=` values (repeatable on the CLI). + // Each entry is appended verbatim to every subcommand invocation, after + // the orchestrator's own flags and after `bazelFlags`. Used to thread + // matrix-cell selectors like `--config=ci-scala-2-13` or + // `--repo_env=SCALA_VERSION=2.13.18` per CI shard. + extraBazelFlags?: string[] + // Customer-supplied `--bazel-startup-flag=` values (repeatable). + // Each entry is appended to the startup-flag prefix, after the + // orchestrator's own startup flags (--bazelrc, --output_user_root, + // --output_base) and before the subcommand. + extraBazelStartupFlags?: string[] env?: NodeJS.ProcessEnv verbose?: boolean } @@ -49,7 +60,8 @@ export function splitBazelFlags(flags: string | undefined): string[] { // Build the shared startup-flag prefix for any bazel invocation. Centralised // so `--output_user_root` propagates to every spawn — principle 7 of the // Maven design requires per-invocation server isolation across query, -// cquery, and `bazel mod` commands alike. +// cquery, and `bazel mod` commands alike. Customer `--bazel-startup-flag` +// values are appended last so they appear before the subcommand. function buildStartupFlags(opts: BazelQueryOptions): string[] { const startup: string[] = [] if (opts.bazelRc) { @@ -61,11 +73,23 @@ function buildStartupFlags(opts: BazelQueryOptions): string[] { if (opts.bazelOutputBase) { startup.push(`--output_base=${opts.bazelOutputBase}`) } + if (opts.extraBazelStartupFlags?.length) { + startup.push(...opts.extraBazelStartupFlags) + } return startup } +// Compose the user-flag suffix appended after every subcommand's standard +// flags. The legacy whitespace-split `bazelFlags` string and the new +// repeatable `extraBazelFlags` array are concatenated in that order so a +// socket.json default plus a CLI override applies both. No deduplication +// — Bazel's last-wins semantics handle conflicts. +function userBazelFlags(opts: BazelQueryOptions): string[] { + return [...splitBazelFlags(opts.bazelFlags), ...(opts.extraBazelFlags ?? [])] +} + function buildBazelModShowVisibleReposArgv(opts: BazelQueryOptions): string[] { - const userFlags = splitBazelFlags(opts.bazelFlags) + const userFlags = userBazelFlags(opts) return [ ...buildStartupFlags(opts), 'mod', @@ -79,7 +103,7 @@ function buildBazelModShowVisibleReposArgv(opts: BazelQueryOptions): string[] { function buildBazelModShowMavenExtensionArgv( opts: BazelQueryOptions, ): string[] { - const userFlags = splitBazelFlags(opts.bazelFlags) + const userFlags = userBazelFlags(opts) return [ ...buildStartupFlags(opts), 'mod', @@ -90,7 +114,7 @@ function buildBazelModShowMavenExtensionArgv( } function buildBazelModShowPipExtensionArgv(opts: BazelQueryOptions): string[] { - const userFlags = splitBazelFlags(opts.bazelFlags) + const userFlags = userBazelFlags(opts) return [ ...buildStartupFlags(opts), 'mod', @@ -110,7 +134,7 @@ function buildBazelArgv( // Bazel argv shape: query --output= // Keep query output stable and avoid updating Bazel lockfiles while extracting. const queryFlags = ['--lockfile_mode=off', '--noshow_progress'] - const userFlags = splitBazelFlags(opts.bazelFlags) + const userFlags = userBazelFlags(opts) return [ ...buildStartupFlags(opts), 'query', @@ -131,7 +155,7 @@ function buildBazelProbeCqueryArgv( repoName: string, opts: BazelQueryOptions, ): string[] { - const userFlags = splitBazelFlags(opts.bazelFlags) + const userFlags = userBazelFlags(opts) return [ ...buildStartupFlags(opts), 'cquery', diff --git a/src/commands/manifest/bazel/bazel-query-runner.test.mts b/src/commands/manifest/bazel/bazel-query-runner.test.mts index 8477e64ca..0e678e9bd 100644 --- a/src/commands/manifest/bazel/bazel-query-runner.test.mts +++ b/src/commands/manifest/bazel/bazel-query-runner.test.mts @@ -121,6 +121,39 @@ describe('runBazelQuery', () => { expect(argv).toContain('--keep_going') }) + it('appends extraBazelFlags after bazelFlags (CLI overrides socket.json default)', async () => { + await runBazelQuery('q', { + bin: 'bazel', + cwd: '/r', + invocationFlags: [], + bazelFlags: '--config=default', + extraBazelFlags: ['--config=override', '--repo_env=K=V'], + }) + const argv = mocked.mock.calls[0]![1] as string[] + const def = argv.indexOf('--config=default') + const override = argv.indexOf('--config=override') + const envFlag = argv.indexOf('--repo_env=K=V') + expect(def).toBeGreaterThanOrEqual(0) + expect(override).toBeGreaterThan(def) + expect(envFlag).toBeGreaterThan(override) + }) + + it('threads extraBazelStartupFlags ahead of the subcommand but after the orchestrator startup flags', async () => { + await runBazelQuery('q', { + bin: 'bazel', + cwd: '/r', + invocationFlags: [], + outputUserRoot: '/tmp/x', + extraBazelStartupFlags: ['--host_jvm_args=-Xmx2g'], + }) + const argv = mocked.mock.calls[0]![1] as string[] + const root = argv.indexOf('--output_user_root=/tmp/x') + const jvm = argv.indexOf('--host_jvm_args=-Xmx2g') + const subcmd = argv.indexOf('query') + expect(root).toBeLessThan(jvm) + expect(jvm).toBeLessThan(subcmd) + }) + it('forwards env to spawn when provided', async () => { const env = { ...process.env, BAZEL_BENCH: 'yes' } await runBazelQuery('q', { @@ -418,6 +451,19 @@ describe('buildPypiProbeFor', () => { }) }) + it('does nothing when extra flags are absent', async () => { + // Sanity-check the new flag arrays don't pollute argv when empty. + const probe = buildPypiProbeFor({ + bin: 'bazel', + cwd: '/r', + invocationFlags: [], + }) + await probe('pypi') + const argv = mocked.mock.calls[0]![1] as string[] + // No --bazel-startup-flag / --bazel-flag entries should have appeared. + expect(argv.some(a => a.startsWith('--config='))).toBe(false) + }) + it('returns the full triple when the hub has no :pkg targets', async () => { mocked.mockReset() // @ts-ignore — narrow return shape for the test's purposes. diff --git a/src/commands/manifest/bazel/cmd-manifest-bazel.mts b/src/commands/manifest/bazel/cmd-manifest-bazel.mts index c1ca58831..1329d1b47 100644 --- a/src/commands/manifest/bazel/cmd-manifest-bazel.mts +++ b/src/commands/manifest/bazel/cmd-manifest-bazel.mts @@ -46,11 +46,26 @@ const config: CliCommandConfig = { description: 'Path to bazel/bazelisk binary; default: $(which bazelisk) || $(which bazel)', }, + bazelFlag: { + type: 'string', + isMultiple: true, + description: + 'Bazel flag forwarded to every subcommand (repeatable). E.g. ' + + '`--bazel-flag=--config=ci-scala-2-13` to scan a matrix cell.', + }, bazelFlags: { type: 'string', description: 'Flags forwarded to every bazel invocation (single quoted string)', }, + bazelMavenRepo: { + type: 'string', + isMultiple: true, + description: + 'Maven hub repo name to extract in addition to the auto-discovered ' + + 'set (repeatable). For legacy WORKSPACE workspaces with hubs that ' + + 'use non-conventional names. E.g. `--bazel-maven-repo=my_jars`.', + }, bazelOutputBase: { type: 'string', description: 'Bazel --output_base for read-only-cache CI environments', @@ -59,6 +74,13 @@ const config: CliCommandConfig = { type: 'string', description: 'Path to additional .bazelrc fragments forwarded to bazel', }, + bazelStartupFlag: { + type: 'string', + isMultiple: true, + description: + 'Bazel startup flag inserted before the subcommand (repeatable). ' + + 'E.g. `--bazel-startup-flag=--host_jvm_args=-Xmx2g`.', + }, ecosystem: { type: 'string', isMultiple: true, @@ -203,6 +225,11 @@ async function run( ) const { ecosystem } = cli.flags + const bazelFlag = (cli.flags['bazelFlag'] as string[] | undefined) ?? [] + const bazelStartupFlag = + (cli.flags['bazelStartupFlag'] as string[] | undefined) ?? [] + const bazelMavenRepo = + (cli.flags['bazelMavenRepo'] as string[] | undefined) ?? [] let { bazel, bazelFlags, bazelOutputBase, bazelRc, out, verbose } = cli.flags // Set defaults for any flag/arg that is not given. Check socket.json first. @@ -318,6 +345,13 @@ async function run( bazelRc: bazelRc as string | undefined, bin: bazel as string | undefined, cwd, + ...(bazelFlag.length ? { extraBazelFlags: bazelFlag } : {}), + ...(bazelStartupFlag.length + ? { extraBazelStartupFlags: bazelStartupFlag } + : {}), + ...(bazelMavenRepo.length + ? { extraMavenRepoNames: bazelMavenRepo } + : {}), ignoreDirNames: BAZEL_WALKER_IGNORE_DIR_NAMES, ignoreDirPrefixes: BAZEL_WALKER_IGNORE_DIR_PREFIXES, out: out as string, diff --git a/src/commands/manifest/bazel/extract_bazel_to_maven.mts b/src/commands/manifest/bazel/extract_bazel_to_maven.mts index 504da86fa..4830a82db 100644 --- a/src/commands/manifest/bazel/extract_bazel_to_maven.mts +++ b/src/commands/manifest/bazel/extract_bazel_to_maven.mts @@ -39,6 +39,13 @@ export type ExtractBazelOptions = { cwd: string // Optional env override used for python-shim PATH augmentation. env?: NodeJS.ProcessEnv + // Customer-supplied `--bazel-flag=` values; threaded into every + // bazel subcommand after the orchestrator's own flags. Repeatable on + // the CLI; entries are forwarded verbatim. + extraBazelFlags?: string[] | undefined + // Customer-supplied `--bazel-startup-flag=` values; injected into + // the startup-flag prefix before the subcommand. + extraBazelStartupFlags?: string[] | undefined // Customer-supplied Maven hub names augmenting the auto-discovery // candidate set. Wired in by the `--bazel-maven-repo=` flag for // legacy WORKSPACE workspaces whose hubs use non-conventional names @@ -369,6 +376,12 @@ function buildQueryOpts(args: { ...(opts.bazelRc ? { bazelRc: opts.bazelRc } : {}), ...(opts.bazelFlags ? { bazelFlags: opts.bazelFlags } : {}), ...(opts.bazelOutputBase ? { bazelOutputBase: opts.bazelOutputBase } : {}), + ...(opts.extraBazelFlags?.length + ? { extraBazelFlags: opts.extraBazelFlags } + : {}), + ...(opts.extraBazelStartupFlags?.length + ? { extraBazelStartupFlags: opts.extraBazelStartupFlags } + : {}), ...(baseEnv ? { env: baseEnv } : {}), verbose, }