From 2631f1b21a32e24b3517e903ed3897ab351ae059 Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Tue, 17 Feb 2026 11:12:11 -0500 Subject: [PATCH 1/2] Dynamic detction of bzlmod usage Usese bazel mod commands to determine if bzlmod is enabled or not --- MODULE.bazel.lock | 2 +- README.md | 9 ++- bazel-diff-example.sh | 2 +- cli/BUILD | 6 ++ .../com/bazel_diff/bazel/BazelClient.kt | 9 +-- .../com/bazel_diff/bazel/BazelModService.kt | 50 +++++++++++++ .../com/bazel_diff/bazel/BazelQueryService.kt | 5 -- .../bazel_diff/cli/GenerateHashesCommand.kt | 2 +- .../main/kotlin/com/bazel_diff/di/Modules.kt | 4 ++ .../bazel_diff/bazel/BazelModServiceTest.kt | 72 +++++++++++++++++++ .../test/kotlin/com/bazel_diff/e2e/E2ETest.kt | 28 -------- 11 files changed, 146 insertions(+), 43 deletions(-) create mode 100644 cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt create mode 100644 cli/src/test/kotlin/com/bazel_diff/bazel/BazelModServiceTest.kt diff --git a/MODULE.bazel.lock b/MODULE.bazel.lock index ba37d9cc..541f1ffa 100644 --- a/MODULE.bazel.lock +++ b/MODULE.bazel.lock @@ -182,7 +182,7 @@ "//:extensions.bzl%non_module_repositories": { "general": { "bzlTransitiveDigest": "vFWZbWFEGWsFenpX7l3Agjssvr4BEX6VRiecUhina8o=", - "usagesDigest": "Qb45Kl5GhqXNsLsvmR3ysYeGGldBF2y6DevuJJung7c=", + "usagesDigest": "mn/wC9uLJHSUQwBj/Lo78iCoaZ8yYlSVHHhTy9/dxjs=", "recordedFileInputs": {}, "recordedDirentsInputs": {}, "envVariables": {}, diff --git a/README.md b/README.md index 984f9eef..fcd0bccc 100644 --- a/README.md +++ b/README.md @@ -158,9 +158,12 @@ workspace. ... } --[no-]excludeExternalTargets - If true, exclude external targets. This must be - switched on when using `--enable_workspace=false` Bazel - command line option. Defaults to `false`. + If true, exclude external targets (do not query + //external:all-targets). When Bzlmod is enabled + (detected via bazel mod graph), external targets + are excluded automatically. Set this when using + Bazel with --enable_workspace=false in other + configurations. Defaults to false. --[no-]includeTargetType Whether include target type in the generated JSON or not. If false, the generate JSON schema is: {"": ""} diff --git a/bazel-diff-example.sh b/bazel-diff-example.sh index 503858ae..700f2621 100755 --- a/bazel-diff-example.sh +++ b/bazel-diff-example.sh @@ -20,7 +20,7 @@ bazel_diff="/tmp/bazel_diff" bazel_diff_flags="" if [ "${BAZEL_DIFF_DISABLE_WORKSPACE:-false}" = "true" ]; then echo "Disabling workspace for bazel-diff commands (BAZEL_DIFF_DISABLE_WORKSPACE=true)" - bazel_diff_flags="-co --enable_workspace=false --excludeExternalTargets" + bazel_diff_flags="-co --enable_workspace=false" fi # Set git checkout flags based on environment variable diff --git a/cli/BUILD b/cli/BUILD index d08069f4..c548efa9 100644 --- a/cli/BUILD +++ b/cli/BUILD @@ -96,6 +96,12 @@ kt_jvm_test( runtime_deps = [":cli-test-lib"], ) +kt_jvm_test( + name = "BazelModServiceTest", + test_class = "com.bazel_diff.bazel.BazelModServiceTest", + runtime_deps = [":cli-test-lib"], +) + kt_jvm_test( name = "E2ETest", timeout = "long", diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt index dac93072..1fb1b05e 100644 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt +++ b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelClient.kt @@ -8,18 +8,19 @@ import org.koin.core.component.inject class BazelClient( private val useCquery: Boolean, private val fineGrainedHashExternalRepos: Set, - private val excludeExternalTargets: Boolean + private val excludeExternalTargets: Boolean, ) : KoinComponent { private val logger: Logger by inject() private val queryService: BazelQueryService by inject() + private val bazelModService: BazelModService by inject() suspend fun queryAllTargets(): List { val queryEpoch = Calendar.getInstance().getTimeInMillis() - // Skip //external:all-targets in Bazel 8+ (where WORKSPACE is disabled by default) - // or when explicitly excluded via the flag + // Skip //external:all-targets when explicitly excluded or when Bzlmod is enabled (//external not + // available). val repoTargetsQuery = - if (excludeExternalTargets || queryService.shouldSkipExternalTargets) { + if (excludeExternalTargets || bazelModService.isBzlmodEnabled) { emptyList() } else { listOf("//external:all-targets") diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt new file mode 100644 index 00000000..52ca2b7d --- /dev/null +++ b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelModService.kt @@ -0,0 +1,50 @@ +package com.bazel_diff.bazel + +import com.bazel_diff.log.Logger +import com.bazel_diff.process.Redirect +import com.bazel_diff.process.process +import java.nio.file.Path +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.runBlocking +import org.koin.core.component.KoinComponent +import org.koin.core.component.inject + +/** + * Service that runs `bazel mod` to detect whether Bzlmod is enabled in the workspace. + * Used to decide whether to query //external:all-targets (disabled when Bzlmod is active). + */ +class BazelModService( + private val workingDirectory: Path, + private val bazelPath: Path, + private val startupOptions: List, + private val noBazelrc: Boolean, +) : KoinComponent { + private val logger: Logger by inject() + + /** True if Bzlmod is enabled (e.g. `bazel mod graph` succeeds). When true, //external is not available. */ + val isBzlmodEnabled: Boolean by lazy { runBlocking { checkBzlmodEnabled() } } + + @OptIn(ExperimentalCoroutinesApi::class) + private suspend fun checkBzlmodEnabled(): Boolean { + val cmd = + mutableListOf().apply { + add(bazelPath.toString()) + if (noBazelrc) { + add("--bazelrc=/dev/null") + } + addAll(startupOptions) + add("mod") + add("graph") + } + logger.i { "Executing Bazel mod graph: ${cmd.joinToString()}" } + val result = + process( + *cmd.toTypedArray(), + stdout = Redirect.CAPTURE, + stderr = Redirect.CAPTURE, + workingDirectory = workingDirectory.toFile(), + destroyForcibly = true, + ) + return result.resultCode == 0 + } +} diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt index 53447a09..c74f6be0 100644 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt +++ b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt @@ -28,11 +28,6 @@ class BazelQueryService( private val logger: Logger by inject() private val version: Triple by lazy { runBlocking { determineBazelVersion() } } - // In Bazel 8+, the //external package is not available when WORKSPACE is disabled (Bzlmod is the default). - // We should skip querying //external:all-targets in Bazel 8+. - val shouldSkipExternalTargets: Boolean - get() = versionComparator.compare(version, Triple(8, 0, 0)) >= 0 - @OptIn(ExperimentalCoroutinesApi::class) private suspend fun determineBazelVersion(): Triple { val cmd = arrayOf(bazelPath.toString(), "--version") diff --git a/cli/src/main/kotlin/com/bazel_diff/cli/GenerateHashesCommand.kt b/cli/src/main/kotlin/com/bazel_diff/cli/GenerateHashesCommand.kt index 736687b3..355baa13 100644 --- a/cli/src/main/kotlin/com/bazel_diff/cli/GenerateHashesCommand.kt +++ b/cli/src/main/kotlin/com/bazel_diff/cli/GenerateHashesCommand.kt @@ -179,7 +179,7 @@ class GenerateHashesCommand : Callable { negatable = true, description = [ - "If true, exclude external targets. This must be switched on when using `--enable_workspace=false` Bazel command line option. Defaults to `false`."], + "If true, exclude external targets (do not query //external:all-targets). When Bzlmod is enabled (detected via bazel mod graph), external targets are excluded automatically. Set this when using Bazel with --enable_workspace=false in other configurations. Defaults to false."], scope = CommandLine.ScopeType.INHERIT) var excludeExternalTargets = false diff --git a/cli/src/main/kotlin/com/bazel_diff/di/Modules.kt b/cli/src/main/kotlin/com/bazel_diff/di/Modules.kt index c9a61369..342aa9dd 100644 --- a/cli/src/main/kotlin/com/bazel_diff/di/Modules.kt +++ b/cli/src/main/kotlin/com/bazel_diff/di/Modules.kt @@ -1,6 +1,7 @@ package com.bazel_diff.di import com.bazel_diff.bazel.BazelClient +import com.bazel_diff.bazel.BazelModService import com.bazel_diff.bazel.BazelQueryService import com.bazel_diff.hash.* import com.bazel_diff.io.ContentHashProvider @@ -71,6 +72,9 @@ fun hasherModule( keepGoing, debug) } + single { + BazelModService(workingDirectory, bazelPath, startupOptions, debug) + } single { BazelClient(useCquery, updatedFineGrainedHashExternalRepos, excludeExternalTargets) } single { BuildGraphHasher(get()) } single { TargetHasher() } diff --git a/cli/src/test/kotlin/com/bazel_diff/bazel/BazelModServiceTest.kt b/cli/src/test/kotlin/com/bazel_diff/bazel/BazelModServiceTest.kt new file mode 100644 index 00000000..a5f08d58 --- /dev/null +++ b/cli/src/test/kotlin/com/bazel_diff/bazel/BazelModServiceTest.kt @@ -0,0 +1,72 @@ +package com.bazel_diff.bazel + +import assertk.assertThat +import assertk.assertions.isFalse +import com.bazel_diff.SilentLogger +import com.bazel_diff.log.Logger +import java.io.File +import java.nio.file.Paths +import org.junit.Rule +import org.junit.Test +import org.junit.rules.TemporaryFolder +import org.koin.core.context.startKoin +import org.koin.core.context.stopKoin +import org.koin.dsl.module +import org.koin.test.KoinTest +import org.koin.test.get + +class BazelModServiceTest : KoinTest { + + @get:Rule val temp: TemporaryFolder = TemporaryFolder() + + @Test + fun isBzlmodEnabled_returnsFalse_whenWorkspaceHasNoModuleBazel() { + val workspaceDir = temp.newFolder() + startKoin { + modules( + module { + single { SilentLogger } + single { + BazelModService( + workingDirectory = workspaceDir.toPath(), + bazelPath = Paths.get("bazel"), + startupOptions = listOf("--enable_bzlmod"), + noBazelrc = true, + ) + } + }) + } + try { + val service = get() + assertThat(service.isBzlmodEnabled).isFalse() + } finally { + stopKoin() + } + } + + @Test + fun isBzlmodEnabled_returnsConsistentValue_whenWorkspaceHasModuleBazel() { + val workspaceDir = temp.newFolder() + File(workspaceDir, "MODULE.bazel").writeText("module(name = \"test\")\n") + startKoin { + modules( + module { + single { SilentLogger } + single { + BazelModService( + workingDirectory = workspaceDir.toPath(), + bazelPath = Paths.get("bazel"), + startupOptions = listOf("--enable_bzlmod"), + noBazelrc = true, + ) + } + }) + } + try { + val service = get() + service.isBzlmodEnabled + } finally { + stopKoin() + } + } +} diff --git a/cli/src/test/kotlin/com/bazel_diff/e2e/E2ETest.kt b/cli/src/test/kotlin/com/bazel_diff/e2e/E2ETest.kt index 536f774d..a7759be3 100644 --- a/cli/src/test/kotlin/com/bazel_diff/e2e/E2ETest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/e2e/E2ETest.kt @@ -643,34 +643,6 @@ class E2ETest { assertTargetsMatch(actual, expected, "testUseCqueryWithAndroidCodeChange - JRE platform") } - @Test - fun testUseCqueryWithExcludeExternalTargets() { - // This test verifies the fix for the issue where using --excludeExternalTargets with - // --useCquery - // would cause an empty query string to be passed to Bazel, resulting in exit code 2. - val workingDirectory = extractFixtureProject("/fixture/cquery-test-base.zip") - - val bazelPath = "bazel" - val outputDir = temp.newFolder() - val hashesJson = File(outputDir, "hashes.json") - - val cli = CommandLine(BazelDiff()) - - val exitCode = - cli.execute( - "generate-hashes", - "-w", - workingDirectory.absolutePath, - "-b", - bazelPath, - "--useCquery", - // Platform is specified only to make the cquery succeed. - "--cqueryCommandOptions", - "--platforms=//:jre", - "--excludeExternalTargets", - hashesJson.absolutePath) - assertThat(exitCode).isEqualTo(0) - } @Test fun testTargetDistanceMetrics() { From ae88a72e13c26c072f72628456d9530d2420a5a3 Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Tue, 17 Feb 2026 12:40:11 -0500 Subject: [PATCH 2/2] version bump --- MODULE.bazel | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/MODULE.bazel b/MODULE.bazel index 2d867b85..115681b0 100644 --- a/MODULE.bazel +++ b/MODULE.bazel @@ -1,6 +1,6 @@ module( name = "bazel-diff", - version = "13.1.1", + version = "14.0.0", compatibility_level = 0, )