From d9a656206e8bcddc5c2ca0bed2f2fea043658acc Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Wed, 13 May 2026 05:20:29 -0400 Subject: [PATCH 1/3] Add reproducer test for #184 transitive external repo change not detected The user reported that modifying a file inside an external repo C does not propagate to an internal target A, when A depends on external repo B and B in turn depends on C. The original example uses rules_python's pip_parse where requirements.txt resolves into a separate external repo per package: A is a py_test rule, B is @moto, C is @cryptography. Bumping cryptography in requirements.txt does not surface A as impacted. This is functionally the same root behaviour as #197's still-unfixed case (external-repo wrapping), but the #184 user described it as a deeper transitive build-time chain, so a separate fixture documents that shape. New workspace `bzlmod_transitive_external`: //:consume genrule that depends on @outer//:lib @outer//:lib genrule in module `outer` that depends on @inner//:data @inner//:data filegroup wrapping inner/data.txt `outer` and `inner` are real bzlmod modules brought in via the root module's `local_path_override`s. outer's MODULE.bazel just declares `bazel_dep(name = "inner")` (only the root module is allowed to use local_path_override). `.bazelignore` excludes the two sub-trees so they are not also treated as packages of the main module. Verified by hand against the locally built CLI (Bazel 9.1): inner/data.txt = "hello" -> "world" $ bazel-diff generate-hashes -w A ... ; bazel-diff generate-hashes -w B ... $ diff <(jq -S '.hashes' A) <(jq -S '.hashes' B) # empty $ bazel-diff get-impacted-targets ... # impacted file is empty The expected behaviour: //:consume should be impacted because the build-time content it transitively consumes through @outer / @inner has changed. I also briefly removed @Ignore locally to confirm the new test fails for the right reason on current bazel-diff: testTransitiveExternalRepoChangeImpactsConsumer_reproducerForIssue184 AssertionFailedError: //:consume should be impacted when inner/data.txt changes (transitive external dep via @outer -> @inner per #184); got: [] Restored @Ignore so the test is skipped under `bazel test //cli:E2ETest`. Remove @Ignore once the fix lands. Refs https://github.com/Tinder/bazel-diff/issues/184 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../test/kotlin/com/bazel_diff/e2e/E2ETest.kt | 97 ++++ .../bzlmod_transitive_external/.bazelignore | 2 + .../bzlmod_transitive_external/BUILD | 6 + .../bzlmod_transitive_external/MODULE.bazel | 16 + .../MODULE.bazel.lock | 470 ++++++++++++++++++ .../bzlmod_transitive_external/WORKSPACE | 1 + .../bzlmod_transitive_external/inner/BUILD | 10 + .../inner/MODULE.bazel | 4 + .../inner/WORKSPACE | 1 + .../bzlmod_transitive_external/inner/data.txt | 1 + .../bzlmod_transitive_external/outer/BUILD | 7 + .../outer/MODULE.bazel | 6 + .../outer/WORKSPACE | 1 + 13 files changed, 622 insertions(+) create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/.bazelignore create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/BUILD create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/MODULE.bazel create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/MODULE.bazel.lock create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/WORKSPACE create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/BUILD create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/MODULE.bazel create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/WORKSPACE create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/data.txt create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/BUILD create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/MODULE.bazel create mode 100644 cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/WORKSPACE 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 da24e12..6b44cc4 100644 --- a/cli/src/test/kotlin/com/bazel_diff/e2e/E2ETest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/e2e/E2ETest.kt @@ -1701,6 +1701,103 @@ class E2ETest { .isEqualTo("//external:com_github_pkg_errors") } + // ------------------------------------------------------------------------ + // Reproducer for https://github.com/Tinder/bazel-diff/issues/184 + // ------------------------------------------------------------------------ + // The user reported that a change inside an external repo C does not propagate to an + // internal target A, when A depends on external repo B and B in turn depends on C. The + // concrete report uses rules_python's pip_parse, where requirements.txt resolves into a + // separate external repo for every package; the user's py_test rule (A) depends on @moto + // (B), and @moto's BUILD targets depend on @cryptography (C). Bumping cryptography in + // requirements.txt does not surface A as impacted. + // + // The root cause as described in the issue is that "external repos are treated as an + // opaque blob" by default, and `--fineGrainedHashExternalRepos` only opens up the named + // repos -- the chain through a wrapping external repo is not followed automatically. + // This is exactly the shape of #197's still-unfixed case (see @Ahajha's comment), but + // the #184 user described it in terms of a transitive build-time dependency rather than + // an alias re-export, so a separate fixture documents the deeper chain. + // + // The reproducer workspace `bzlmod_transitive_external` wires up: + // + // //:consume genrule, depends on @outer//:lib + // @outer//:lib genrule in the outer module, depends on @inner//:data + // @inner//:data filegroup in the inner module wrapping inner/data.txt + // + // Both `outer` and `inner` are real bzlmod modules brought in via `local_path_override`. + // outer's MODULE.bazel declares `bazel_dep(name = "inner")` (no override; only the root + // module is allowed to use local_path_override). `.bazelignore` excludes the two + // sub-trees so bazel does not treat them as packages of the main module. + // + // Verified by hand against the locally built CLI (Bazel 9.1): + // + // $ bazel-diff generate-hashes -w ... # inner/data.txt = "hello" + // $ bazel-diff generate-hashes -w ... # inner/data.txt = "world" + // $ diff <(jq -S '.hashes' A.json) <(jq -S '.hashes' B.json) # empty -- no diff + // $ bazel-diff get-impacted-targets ... + // (impacted file is empty) + // + // The expected behavior is that `//:consume` should be reported as impacted -- the + // build-time content it transitively consumes via @outer/@inner changed. This test + // asserts that expectation. It is `@Ignore`d so CI stays green until #184 is fixed. + @Test + @org.junit.Ignore( + "Reproducer for https://github.com/Tinder/bazel-diff/issues/184 -- transitive " + + "external repo file change is not detected. Remove @Ignore once the fix lands.") + fun testTransitiveExternalRepoChangeImpactsConsumer_reproducerForIssue184() { + val version = getBazelVersion() + org.junit.Assume.assumeNotNull(version) + + val workspaceA = copyTestWorkspace("bzlmod_transitive_external") + val workspaceB = copyTestWorkspace("bzlmod_transitive_external") + + // Mutate the deepest, transitively-consumed file in B. Nothing in workspaceB's main + // module changes; only inner/data.txt (which @inner re-exports via :data) is touched. + val innerDataInB = File(workspaceB, "inner/data.txt") + val original = innerDataInB.readText() + val mutated = "world\n" + assertThat(mutated != original).isEqualTo(true) + innerDataInB.writeText(mutated) + + val outputDir = temp.newFolder() + val from = File(outputDir, "starting_hashes.json") + val to = File(outputDir, "final_hashes.json") + val impactedTargetsOutput = File(outputDir, "impacted_targets.txt") + + val cli = CommandLine(BazelDiff()) + assertThat( + cli.execute( + "generate-hashes", + "-w", workspaceA.absolutePath, + "-b", "bazel", + from.absolutePath)) + .isEqualTo(0) + assertThat( + cli.execute( + "generate-hashes", + "-w", workspaceB.absolutePath, + "-b", "bazel", + to.absolutePath)) + .isEqualTo(0) + assertThat( + cli.execute( + "get-impacted-targets", + "-w", workspaceB.absolutePath, + "-b", "bazel", + "-sh", from.absolutePath, + "-fh", to.absolutePath, + "-o", impactedTargetsOutput.absolutePath)) + .isEqualTo(0) + + val impacted = impactedTargetsOutput.readLines().filter { it.isNotBlank() }.toSet() + val consumerImpacted = impacted.any { it == "//:consume" || it == "@@//:consume" } + assertThat(consumerImpacted) + .transform( + "//:consume should be impacted when inner/data.txt changes (transitive external dep " + + "via @outer -> @inner per #184); got: $impacted") { it } + .isEqualTo(true) + } + private fun copyTestWorkspace(path: String): File { val testProject = temp.newFolder() diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/.bazelignore b/cli/src/test/resources/workspaces/bzlmod_transitive_external/.bazelignore new file mode 100644 index 0000000..6eef27a --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/.bazelignore @@ -0,0 +1,2 @@ +outer +inner diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/BUILD b/cli/src/test/resources/workspaces/bzlmod_transitive_external/BUILD new file mode 100644 index 0000000..bf8a090 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/BUILD @@ -0,0 +1,6 @@ +genrule( + name = "consume", + srcs = ["@outer//:lib"], + outs = ["consume.out"], + cmd = "cat $(SRCS) > $@", +) diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/MODULE.bazel b/cli/src/test/resources/workspaces/bzlmod_transitive_external/MODULE.bazel new file mode 100644 index 0000000..bd1c2b6 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/MODULE.bazel @@ -0,0 +1,16 @@ +module( + name = "bzlmod_transitive_external_test", + version = "0.0.0", +) + +bazel_dep(name = "outer", version = "0.0.0") +local_path_override( + module_name = "outer", + path = "outer", +) + +bazel_dep(name = "inner", version = "0.0.0") +local_path_override( + module_name = "inner", + path = "inner", +) diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/MODULE.bazel.lock b/cli/src/test/resources/workspaces/bzlmod_transitive_external/MODULE.bazel.lock new file mode 100644 index 0000000..895b4e1 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/MODULE.bazel.lock @@ -0,0 +1,470 @@ +{ + "lockFileVersion": 26, + "registryFileHashes": { + "https://bcr.bazel.build/bazel_registry.json": "8a28e4aff06ee60aed2a8c281907fb8bcbf3b753c91fb5a5c57da3215d5b3497", + "https://bcr.bazel.build/modules/abseil-cpp/20210324.2/MODULE.bazel": "7cd0312e064fde87c8d1cd79ba06c876bd23630c83466e9500321be55c96ace2", + "https://bcr.bazel.build/modules/abseil-cpp/20211102.0/MODULE.bazel": "70390338f7a5106231d20620712f7cccb659cd0e9d073d1991c038eb9fc57589", + "https://bcr.bazel.build/modules/abseil-cpp/20230125.1/MODULE.bazel": "89047429cb0207707b2dface14ba7f8df85273d484c2572755be4bab7ce9c3a0", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0.bcr.1/MODULE.bazel": "1c8cec495288dccd14fdae6e3f95f772c1c91857047a098fad772034264cc8cb", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.0/MODULE.bazel": "d253ae36a8bd9ee3c5955384096ccb6baf16a1b1e93e858370da0a3b94f77c16", + "https://bcr.bazel.build/modules/abseil-cpp/20230802.1/MODULE.bazel": "fa92e2eb41a04df73cdabeec37107316f7e5272650f81d6cc096418fe647b915", + "https://bcr.bazel.build/modules/abseil-cpp/20240116.1/MODULE.bazel": "37bcdb4440fbb61df6a1c296ae01b327f19e9bb521f9b8e26ec854b6f97309ed", + "https://bcr.bazel.build/modules/abseil-cpp/20240116.2/MODULE.bazel": "73939767a4686cd9a520d16af5ab440071ed75cec1a876bf2fcfaf1f71987a16", + "https://bcr.bazel.build/modules/abseil-cpp/20250127.1/MODULE.bazel": "c4a89e7ceb9bf1e25cf84a9f830ff6b817b72874088bf5141b314726e46a57c1", + "https://bcr.bazel.build/modules/abseil-cpp/20250512.1/MODULE.bazel": "d209fdb6f36ffaf61c509fcc81b19e81b411a999a934a032e10cd009a0226215", + "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/MODULE.bazel": "51f2312901470cdab0dbdf3b88c40cd21c62a7ed58a3de45b365ddc5b11bcab2", + "https://bcr.bazel.build/modules/abseil-cpp/20250814.1/source.json": "cea3901d7e299da7320700abbaafe57a65d039f10d0d7ea601c4a66938ea4b0c", + "https://bcr.bazel.build/modules/apple_support/1.11.1/MODULE.bazel": "1843d7cd8a58369a444fc6000e7304425fba600ff641592161d9f15b179fb896", + "https://bcr.bazel.build/modules/apple_support/1.15.1/MODULE.bazel": "a0556fefca0b1bb2de8567b8827518f94db6a6e7e7d632b4c48dc5f865bc7c85", + "https://bcr.bazel.build/modules/apple_support/1.21.0/MODULE.bazel": "ac1824ed5edf17dee2fdd4927ada30c9f8c3b520be1b5fd02a5da15bc10bff3e", + "https://bcr.bazel.build/modules/apple_support/1.21.1/MODULE.bazel": "5809fa3efab15d1f3c3c635af6974044bac8a4919c62238cce06acee8a8c11f1", + "https://bcr.bazel.build/modules/apple_support/1.24.2/MODULE.bazel": "0e62471818affb9f0b26f128831d5c40b074d32e6dda5a0d3852847215a41ca4", + "https://bcr.bazel.build/modules/apple_support/1.24.2/source.json": "2c22c9827093250406c5568da6c54e6fdf0ef06238def3d99c71b12feb057a8d", + "https://bcr.bazel.build/modules/bazel_features/1.1.1/MODULE.bazel": "27b8c79ef57efe08efccbd9dd6ef70d61b4798320b8d3c134fd571f78963dbcd", + "https://bcr.bazel.build/modules/bazel_features/1.10.0/MODULE.bazel": "f75e8807570484a99be90abcd52b5e1f390362c258bcb73106f4544957a48101", + "https://bcr.bazel.build/modules/bazel_features/1.11.0/MODULE.bazel": "f9382337dd5a474c3b7d334c2f83e50b6eaedc284253334cf823044a26de03e8", + "https://bcr.bazel.build/modules/bazel_features/1.15.0/MODULE.bazel": "d38ff6e517149dc509406aca0db3ad1efdd890a85e049585b7234d04238e2a4d", + "https://bcr.bazel.build/modules/bazel_features/1.17.0/MODULE.bazel": "039de32d21b816b47bd42c778e0454217e9c9caac4a3cf8e15c7231ee3ddee4d", + "https://bcr.bazel.build/modules/bazel_features/1.18.0/MODULE.bazel": "1be0ae2557ab3a72a57aeb31b29be347bcdc5d2b1eb1e70f39e3851a7e97041a", + "https://bcr.bazel.build/modules/bazel_features/1.19.0/MODULE.bazel": "59adcdf28230d220f0067b1f435b8537dd033bfff8db21335ef9217919c7fb58", + "https://bcr.bazel.build/modules/bazel_features/1.21.0/MODULE.bazel": "675642261665d8eea09989aa3b8afb5c37627f1be178382c320d1b46afba5e3b", + "https://bcr.bazel.build/modules/bazel_features/1.23.0/MODULE.bazel": "fd1ac84bc4e97a5a0816b7fd7d4d4f6d837b0047cf4cbd81652d616af3a6591a", + "https://bcr.bazel.build/modules/bazel_features/1.27.0/MODULE.bazel": "621eeee06c4458a9121d1f104efb80f39d34deff4984e778359c60eaf1a8cb65", + "https://bcr.bazel.build/modules/bazel_features/1.28.0/MODULE.bazel": "4b4200e6cbf8fa335b2c3f43e1d6ef3e240319c33d43d60cc0fbd4b87ece299d", + "https://bcr.bazel.build/modules/bazel_features/1.3.0/MODULE.bazel": "cdcafe83ec318cda34e02948e81d790aab8df7a929cec6f6969f13a489ccecd9", + "https://bcr.bazel.build/modules/bazel_features/1.30.0/MODULE.bazel": "a14b62d05969a293b80257e72e597c2da7f717e1e69fa8b339703ed6731bec87", + "https://bcr.bazel.build/modules/bazel_features/1.33.0/MODULE.bazel": "8b8dc9d2a4c88609409c3191165bccec0e4cb044cd7a72ccbe826583303459f6", + "https://bcr.bazel.build/modules/bazel_features/1.4.1/MODULE.bazel": "e45b6bb2350aff3e442ae1111c555e27eac1d915e77775f6fdc4b351b758b5d7", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/MODULE.bazel": "275a59b5406ff18c01739860aa70ad7ccb3cfb474579411decca11c93b951080", + "https://bcr.bazel.build/modules/bazel_features/1.42.1/source.json": "fcd4396b2df85f64f2b3bb436ad870793ecf39180f1d796f913cc9276d355309", + "https://bcr.bazel.build/modules/bazel_features/1.9.1/MODULE.bazel": "8f679097876a9b609ad1f60249c49d68bfab783dd9be012faf9d82547b14815a", + "https://bcr.bazel.build/modules/bazel_skylib/1.0.3/MODULE.bazel": "bcb0fd896384802d1ad283b4e4eb4d718eebd8cb820b0a2c3a347fb971afd9d8", + "https://bcr.bazel.build/modules/bazel_skylib/1.1.1/MODULE.bazel": "1add3e7d93ff2e6998f9e118022c84d163917d912f5afafb3058e3d2f1545b5e", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.0/MODULE.bazel": "44fe84260e454ed94ad326352a698422dbe372b21a1ac9f3eab76eb531223686", + "https://bcr.bazel.build/modules/bazel_skylib/1.2.1/MODULE.bazel": "f35baf9da0efe45fa3da1696ae906eea3d615ad41e2e3def4aeb4e8bc0ef9a7a", + "https://bcr.bazel.build/modules/bazel_skylib/1.3.0/MODULE.bazel": "20228b92868bf5cfc41bda7afc8a8ba2a543201851de39d990ec957b513579c5", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.1/MODULE.bazel": "a0dcb779424be33100dcae821e9e27e4f2901d9dfd5333efe5ac6a8d7ab75e1d", + "https://bcr.bazel.build/modules/bazel_skylib/1.4.2/MODULE.bazel": "3bd40978e7a1fac911d5989e6b09d8f64921865a45822d8b09e815eaa726a651", + "https://bcr.bazel.build/modules/bazel_skylib/1.5.0/MODULE.bazel": "32880f5e2945ce6a03d1fbd588e9198c0a959bb42297b2cfaf1685b7bc32e138", + "https://bcr.bazel.build/modules/bazel_skylib/1.6.1/MODULE.bazel": "8fdee2dbaace6c252131c00e1de4b165dc65af02ea278476187765e1a617b917", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.0/MODULE.bazel": "0db596f4563de7938de764cc8deeabec291f55e8ec15299718b93c4423e9796d", + "https://bcr.bazel.build/modules/bazel_skylib/1.7.1/MODULE.bazel": "3120d80c5861aa616222ec015332e5f8d3171e062e3e804a2a0253e1be26e59b", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.1/MODULE.bazel": "88ade7293becda963e0e3ea33e7d54d3425127e0a326e0d17da085a5f1f03ff6", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/MODULE.bazel": "69ad6927098316848b34a9142bcc975e018ba27f08c4ff403f50c1b6e646ca67", + "https://bcr.bazel.build/modules/bazel_skylib/1.8.2/source.json": "34a3c8bcf233b835eb74be9d628899bb32999d3e0eadef1947a0a562a2b16ffb", + "https://bcr.bazel.build/modules/buildozer/8.5.1/MODULE.bazel": "a35d9561b3fc5b18797c330793e99e3b834a473d5fbd3d7d7634aafc9bdb6f8f", + "https://bcr.bazel.build/modules/buildozer/8.5.1/source.json": "e3386e6ff4529f2442800dee47ad28d3e6487f36a1f75ae39ae56c70f0cd2fbd", + "https://bcr.bazel.build/modules/google_benchmark/1.8.2/MODULE.bazel": "a70cf1bba851000ba93b58ae2f6d76490a9feb74192e57ab8e8ff13c34ec50cb", + "https://bcr.bazel.build/modules/googletest/1.11.0/MODULE.bazel": "3a83f095183f66345ca86aa13c58b59f9f94a2f81999c093d4eeaa2d262d12f4", + "https://bcr.bazel.build/modules/googletest/1.14.0.bcr.1/MODULE.bazel": "22c31a561553727960057361aa33bf20fb2e98584bc4fec007906e27053f80c6", + "https://bcr.bazel.build/modules/googletest/1.14.0/MODULE.bazel": "cfbcbf3e6eac06ef9d85900f64424708cc08687d1b527f0ef65aa7517af8118f", + "https://bcr.bazel.build/modules/googletest/1.15.2/MODULE.bazel": "6de1edc1d26cafb0ea1a6ab3f4d4192d91a312fd2d360b63adaa213cd00b2108", + "https://bcr.bazel.build/modules/googletest/1.17.0/MODULE.bazel": "dbec758171594a705933a29fcf69293d2468c49ec1f2ebca65c36f504d72df46", + "https://bcr.bazel.build/modules/googletest/1.17.0/source.json": "38e4454b25fc30f15439c0378e57909ab1fd0a443158aa35aec685da727cd713", + "https://bcr.bazel.build/modules/jsoncpp/1.9.5/MODULE.bazel": "31271aedc59e815656f5736f282bb7509a97c7ecb43e927ac1a37966e0578075", + "https://bcr.bazel.build/modules/jsoncpp/1.9.6/MODULE.bazel": "2f8d20d3b7d54143213c4dfc3d98225c42de7d666011528dc8fe91591e2e17b0", + "https://bcr.bazel.build/modules/jsoncpp/1.9.6/source.json": "a04756d367a2126c3541682864ecec52f92cdee80a35735a3cb249ce015ca000", + "https://bcr.bazel.build/modules/libpfm/4.11.0/MODULE.bazel": "45061ff025b301940f1e30d2c16bea596c25b176c8b6b3087e92615adbd52902", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/MODULE.bazel": "6f7b417dcc794d9add9e556673ad25cb3ba835224290f4f848f8e2db1e1fca74", + "https://bcr.bazel.build/modules/nlohmann_json/3.6.1/source.json": "f448c6e8963fdfa7eb831457df83ad63d3d6355018f6574fb017e8169deb43a9", + "https://bcr.bazel.build/modules/platforms/0.0.10/MODULE.bazel": "8cb8efaf200bdeb2150d93e162c40f388529a25852b332cec879373771e48ed5", + "https://bcr.bazel.build/modules/platforms/0.0.11/MODULE.bazel": "0daefc49732e227caa8bfa834d65dc52e8cc18a2faf80df25e8caea151a9413f", + "https://bcr.bazel.build/modules/platforms/0.0.4/MODULE.bazel": "9b328e31ee156f53f3c416a64f8491f7eb731742655a47c9eec4703a71644aee", + "https://bcr.bazel.build/modules/platforms/0.0.5/MODULE.bazel": "5733b54ea419d5eaf7997054bb55f6a1d0b5ff8aedf0176fef9eea44f3acda37", + "https://bcr.bazel.build/modules/platforms/0.0.6/MODULE.bazel": "ad6eeef431dc52aefd2d77ed20a4b353f8ebf0f4ecdd26a807d2da5aa8cd0615", + "https://bcr.bazel.build/modules/platforms/0.0.7/MODULE.bazel": "72fd4a0ede9ee5c021f6a8dd92b503e089f46c227ba2813ff183b71616034814", + "https://bcr.bazel.build/modules/platforms/0.0.8/MODULE.bazel": "9f142c03e348f6d263719f5074b21ef3adf0b139ee4c5133e2aa35664da9eb2d", + "https://bcr.bazel.build/modules/platforms/0.0.9/MODULE.bazel": "4a87a60c927b56ddd67db50c89acaa62f4ce2a1d2149ccb63ffd871d5ce29ebc", + "https://bcr.bazel.build/modules/platforms/1.0.0/MODULE.bazel": "f05feb42b48f1b3c225e4ccf351f367be0371411a803198ec34a389fb22aa580", + "https://bcr.bazel.build/modules/platforms/1.0.0/source.json": "f4ff1fd412e0246fd38c82328eb209130ead81d62dcd5a9e40910f867f733d96", + "https://bcr.bazel.build/modules/protobuf/21.7/MODULE.bazel": "a5a29bb89544f9b97edce05642fac225a808b5b7be74038ea3640fae2f8e66a7", + "https://bcr.bazel.build/modules/protobuf/27.0/MODULE.bazel": "7873b60be88844a0a1d8f80b9d5d20cfbd8495a689b8763e76c6372998d3f64c", + "https://bcr.bazel.build/modules/protobuf/29.0-rc2/MODULE.bazel": "6241d35983510143049943fc0d57937937122baf1b287862f9dc8590fc4c37df", + "https://bcr.bazel.build/modules/protobuf/29.0-rc3/MODULE.bazel": "33c2dfa286578573afc55a7acaea3cada4122b9631007c594bf0729f41c8de92", + "https://bcr.bazel.build/modules/protobuf/29.1/MODULE.bazel": "557c3457560ff49e122ed76c0bc3397a64af9574691cb8201b4e46d4ab2ecb95", + "https://bcr.bazel.build/modules/protobuf/3.19.0/MODULE.bazel": "6b5fbb433f760a99a22b18b6850ed5784ef0e9928a72668b66e4d7ccd47db9b0", + "https://bcr.bazel.build/modules/protobuf/32.1/MODULE.bazel": "89cd2866a9cb07fee9ff74c41ceace11554f32e0d849de4e23ac55515cfada4d", + "https://bcr.bazel.build/modules/protobuf/33.4/MODULE.bazel": "114775b816b38b6d0ca620450d6b02550c60ceedfdc8d9a229833b34a223dc42", + "https://bcr.bazel.build/modules/protobuf/33.4/source.json": "555f8686b4c7d6b5ba731fbea13bf656b4bfd9a7ff629c1d9d3f6e1d6155de79", + "https://bcr.bazel.build/modules/pybind11_bazel/2.11.1/MODULE.bazel": "88af1c246226d87e65be78ed49ecd1e6f5e98648558c14ce99176da041dc378e", + "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/MODULE.bazel": "e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34", + "https://bcr.bazel.build/modules/pybind11_bazel/2.12.0/source.json": "6900fdc8a9e95866b8c0d4ad4aba4d4236317b5c1cd04c502df3f0d33afed680", + "https://bcr.bazel.build/modules/re2/2023-09-01/MODULE.bazel": "cb3d511531b16cfc78a225a9e2136007a48cf8a677e4264baeab57fe78a80206", + "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/MODULE.bazel": "b4963dda9b31080be1905ef085ecd7dd6cd47c05c79b9cdf83ade83ab2ab271a", + "https://bcr.bazel.build/modules/re2/2024-07-02.bcr.1/source.json": "2ff292be6ef3340325ce8a045ecc326e92cbfab47c7cbab4bd85d28971b97ac4", + "https://bcr.bazel.build/modules/re2/2024-07-02/MODULE.bazel": "0eadc4395959969297cbcf31a249ff457f2f1d456228c67719480205aa306daa", + "https://bcr.bazel.build/modules/rules_android/0.1.1/MODULE.bazel": "48809ab0091b07ad0182defb787c4c5328bd3a278938415c00a7b69b50c4d3a8", + "https://bcr.bazel.build/modules/rules_android/0.1.1/source.json": "e6986b41626ee10bdc864937ffb6d6bf275bb5b9c65120e6137d56e6331f089e", + "https://bcr.bazel.build/modules/rules_apple/3.16.0/MODULE.bazel": "0d1caf0b8375942ce98ea944be754a18874041e4e0459401d925577624d3a54a", + "https://bcr.bazel.build/modules/rules_apple/4.1.0/MODULE.bazel": "76e10fd4a48038d3fc7c5dc6e63b7063bbf5304a2e3bd42edda6ec660eebea68", + "https://bcr.bazel.build/modules/rules_apple/4.1.0/source.json": "8ee81e1708756f81b343a5eb2b2f0b953f1d25c4ab3d4a68dc02754872e80715", + "https://bcr.bazel.build/modules/rules_cc/0.0.1/MODULE.bazel": "cb2aa0747f84c6c3a78dad4e2049c154f08ab9d166b1273835a8174940365647", + "https://bcr.bazel.build/modules/rules_cc/0.0.10/MODULE.bazel": "ec1705118f7eaedd6e118508d3d26deba2a4e76476ada7e0e3965211be012002", + "https://bcr.bazel.build/modules/rules_cc/0.0.13/MODULE.bazel": "0e8529ed7b323dad0775ff924d2ae5af7640b23553dfcd4d34344c7e7a867191", + "https://bcr.bazel.build/modules/rules_cc/0.0.15/MODULE.bazel": "6704c35f7b4a72502ee81f61bf88706b54f06b3cbe5558ac17e2e14666cd5dcc", + "https://bcr.bazel.build/modules/rules_cc/0.0.16/MODULE.bazel": "7661303b8fc1b4d7f532e54e9d6565771fea666fbdf839e0a86affcd02defe87", + "https://bcr.bazel.build/modules/rules_cc/0.0.17/MODULE.bazel": "2ae1d8f4238ec67d7185d8861cb0a2cdf4bc608697c331b95bf990e69b62e64a", + "https://bcr.bazel.build/modules/rules_cc/0.0.2/MODULE.bazel": "6915987c90970493ab97393024c156ea8fb9f3bea953b2f3ec05c34f19b5695c", + "https://bcr.bazel.build/modules/rules_cc/0.0.6/MODULE.bazel": "abf360251023dfe3efcef65ab9d56beefa8394d4176dd29529750e1c57eaa33f", + "https://bcr.bazel.build/modules/rules_cc/0.0.8/MODULE.bazel": "964c85c82cfeb6f3855e6a07054fdb159aced38e99a5eecf7bce9d53990afa3e", + "https://bcr.bazel.build/modules/rules_cc/0.0.9/MODULE.bazel": "836e76439f354b89afe6a911a7adf59a6b2518fafb174483ad78a2a2fde7b1c5", + "https://bcr.bazel.build/modules/rules_cc/0.1.1/MODULE.bazel": "2f0222a6f229f0bf44cd711dc13c858dad98c62d52bd51d8fc3a764a83125513", + "https://bcr.bazel.build/modules/rules_cc/0.1.2/MODULE.bazel": "557ddc3a96858ec0d465a87c0a931054d7dcfd6583af2c7ed3baf494407fd8d0", + "https://bcr.bazel.build/modules/rules_cc/0.1.5/MODULE.bazel": "88dfc9361e8b5ae1008ac38f7cdfd45ad738e4fa676a3ad67d19204f045a1fd8", + "https://bcr.bazel.build/modules/rules_cc/0.2.0/MODULE.bazel": "b5c17f90458caae90d2ccd114c81970062946f49f355610ed89bebf954f5783c", + "https://bcr.bazel.build/modules/rules_cc/0.2.13/MODULE.bazel": "eecdd666eda6be16a8d9dc15e44b5c75133405e820f620a234acc4b1fdc5aa37", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/MODULE.bazel": "1849602c86cb60da8613d2de887f9566a6d354a6df6d7009f9d04a14402f9a84", + "https://bcr.bazel.build/modules/rules_cc/0.2.17/source.json": "3832f45d145354049137c0090df04629d9c2b5493dc5c2bf46f1834040133a07", + "https://bcr.bazel.build/modules/rules_cc/0.2.8/MODULE.bazel": "f1df20f0bf22c28192a794f29b501ee2018fa37a3862a1a2132ae2940a23a642", + "https://bcr.bazel.build/modules/rules_foreign_cc/0.9.0/MODULE.bazel": "c9e8c682bf75b0e7c704166d79b599f93b72cfca5ad7477df596947891feeef6", + "https://bcr.bazel.build/modules/rules_fuzzing/0.5.2/MODULE.bazel": "40c97d1144356f52905566c55811f13b299453a14ac7769dfba2ac38192337a8", + "https://bcr.bazel.build/modules/rules_java/4.0.0/MODULE.bazel": "5a78a7ae82cd1a33cef56dc578c7d2a46ed0dca12643ee45edbb8417899e6f74", + "https://bcr.bazel.build/modules/rules_java/5.3.5/MODULE.bazel": "a4ec4f2db570171e3e5eb753276ee4b389bae16b96207e9d3230895c99644b86", + "https://bcr.bazel.build/modules/rules_java/6.5.2/MODULE.bazel": "1d440d262d0e08453fa0c4d8f699ba81609ed0e9a9a0f02cd10b3e7942e61e31", + "https://bcr.bazel.build/modules/rules_java/7.10.0/MODULE.bazel": "530c3beb3067e870561739f1144329a21c851ff771cd752a49e06e3dc9c2e71a", + "https://bcr.bazel.build/modules/rules_java/7.12.2/MODULE.bazel": "579c505165ee757a4280ef83cda0150eea193eed3bef50b1004ba88b99da6de6", + "https://bcr.bazel.build/modules/rules_java/7.2.0/MODULE.bazel": "06c0334c9be61e6cef2c8c84a7800cef502063269a5af25ceb100b192453d4ab", + "https://bcr.bazel.build/modules/rules_java/7.6.1/MODULE.bazel": "2f14b7e8a1aa2f67ae92bc69d1ec0fa8d9f827c4e17ff5e5f02e91caa3b2d0fe", + "https://bcr.bazel.build/modules/rules_java/8.3.2/MODULE.bazel": "7336d5511ad5af0b8615fdc7477535a2e4e723a357b6713af439fe8cf0195017", + "https://bcr.bazel.build/modules/rules_java/8.5.1/MODULE.bazel": "d8a9e38cc5228881f7055a6079f6f7821a073df3744d441978e7a43e20226939", + "https://bcr.bazel.build/modules/rules_java/8.6.1/MODULE.bazel": "f4808e2ab5b0197f094cabce9f4b006a27766beb6a9975931da07099560ca9c2", + "https://bcr.bazel.build/modules/rules_java/9.1.0/MODULE.bazel": "ee63f27e36a3fada80342869361182f120a9819c74320e8e65b1e04ba0cd7a9d", + "https://bcr.bazel.build/modules/rules_java/9.1.0/source.json": "da589573c1dee2c9ac4a568b301269a2e8191110ff0345c1a959fa7ea6c4dfd6", + "https://bcr.bazel.build/modules/rules_jvm_external/4.4.2/MODULE.bazel": "a56b85e418c83eb1839819f0b515c431010160383306d13ec21959ac412d2fe7", + "https://bcr.bazel.build/modules/rules_jvm_external/5.1/MODULE.bazel": "33f6f999e03183f7d088c9be518a63467dfd0be94a11d0055fe2d210f89aa909", + "https://bcr.bazel.build/modules/rules_jvm_external/5.2/MODULE.bazel": "d9351ba35217ad0de03816ef3ed63f89d411349353077348a45348b096615036", + "https://bcr.bazel.build/modules/rules_jvm_external/6.3/MODULE.bazel": "c998e060b85f71e00de5ec552019347c8bca255062c990ac02d051bb80a38df0", + "https://bcr.bazel.build/modules/rules_jvm_external/6.7/MODULE.bazel": "e717beabc4d091ecb2c803c2d341b88590e9116b8bf7947915eeb33aab4f96dd", + "https://bcr.bazel.build/modules/rules_jvm_external/6.7/source.json": "5426f412d0a7fc6b611643376c7e4a82dec991491b9ce5cb1cfdd25fe2e92be4", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/MODULE.bazel": "d269a01a18ee74d0335450b10f62c9ed81f2321d7958a2934e44272fe82dcef3", + "https://bcr.bazel.build/modules/rules_kotlin/1.9.6/source.json": "2faa4794364282db7c06600b7e5e34867a564ae91bda7cae7c29c64e9466b7d5", + "https://bcr.bazel.build/modules/rules_license/0.0.3/MODULE.bazel": "627e9ab0247f7d1e05736b59dbb1b6871373de5ad31c3011880b4133cafd4bd0", + "https://bcr.bazel.build/modules/rules_license/0.0.7/MODULE.bazel": "088fbeb0b6a419005b89cf93fe62d9517c0a2b8bb56af3244af65ecfe37e7d5d", + "https://bcr.bazel.build/modules/rules_license/1.0.0/MODULE.bazel": "a7fda60eefdf3d8c827262ba499957e4df06f659330bbe6cdbdb975b768bb65c", + "https://bcr.bazel.build/modules/rules_license/1.0.0/source.json": "a52c89e54cc311196e478f8382df91c15f7a2bfdf4c6cd0e2675cc2ff0b56efb", + "https://bcr.bazel.build/modules/rules_pkg/0.7.0/MODULE.bazel": "df99f03fc7934a4737122518bb87e667e62d780b610910f0447665a7e2be62dc", + "https://bcr.bazel.build/modules/rules_pkg/1.0.1/MODULE.bazel": "5b1df97dbc29623bccdf2b0dcd0f5cb08e2f2c9050aab1092fd39a41e82686ff", + "https://bcr.bazel.build/modules/rules_pkg/1.0.1/source.json": "bd82e5d7b9ce2d31e380dd9f50c111d678c3bdaca190cb76b0e1c71b05e1ba8a", + "https://bcr.bazel.build/modules/rules_proto/4.0.0/MODULE.bazel": "a7a7b6ce9bee418c1a760b3d84f83a299ad6952f9903c67f19e4edd964894e06", + "https://bcr.bazel.build/modules/rules_proto/5.3.0-21.7/MODULE.bazel": "e8dff86b0971688790ae75528fe1813f71809b5afd57facb44dad9e8eca631b7", + "https://bcr.bazel.build/modules/rules_proto/6.0.0-rc1/MODULE.bazel": "1e5b502e2e1a9e825eef74476a5a1ee524a92297085015a052510b09a1a09483", + "https://bcr.bazel.build/modules/rules_proto/6.0.2/MODULE.bazel": "ce916b775a62b90b61888052a416ccdda405212b6aaeb39522f7dc53431a5e73", + "https://bcr.bazel.build/modules/rules_proto/7.1.0/MODULE.bazel": "002d62d9108f75bb807cd56245d45648f38275cb3a99dcd45dfb864c5d74cb96", + "https://bcr.bazel.build/modules/rules_proto/7.1.0/source.json": "39f89066c12c24097854e8f57ab8558929f9c8d474d34b2c00ac04630ad8940e", + "https://bcr.bazel.build/modules/rules_python/0.10.2/MODULE.bazel": "cc82bc96f2997baa545ab3ce73f196d040ffb8756fd2d66125a530031cd90e5f", + "https://bcr.bazel.build/modules/rules_python/0.23.1/MODULE.bazel": "49ffccf0511cb8414de28321f5fcf2a31312b47c40cc21577144b7447f2bf300", + "https://bcr.bazel.build/modules/rules_python/0.25.0/MODULE.bazel": "72f1506841c920a1afec76975b35312410eea3aa7b63267436bfb1dd91d2d382", + "https://bcr.bazel.build/modules/rules_python/0.28.0/MODULE.bazel": "cba2573d870babc976664a912539b320cbaa7114cd3e8f053c720171cde331ed", + "https://bcr.bazel.build/modules/rules_python/0.31.0/MODULE.bazel": "93a43dc47ee570e6ec9f5779b2e64c1476a6ce921c48cc9a1678a91dd5f8fd58", + "https://bcr.bazel.build/modules/rules_python/0.33.2/MODULE.bazel": "3e036c4ad8d804a4dad897d333d8dce200d943df4827cb849840055be8d2e937", + "https://bcr.bazel.build/modules/rules_python/0.4.0/MODULE.bazel": "9208ee05fd48bf09ac60ed269791cf17fb343db56c8226a720fbb1cdf467166c", + "https://bcr.bazel.build/modules/rules_python/1.3.0/MODULE.bazel": "8361d57eafb67c09b75bf4bbe6be360e1b8f4f18118ab48037f2bd50aa2ccb13", + "https://bcr.bazel.build/modules/rules_python/1.4.1/MODULE.bazel": "8991ad45bdc25018301d6b7e1d3626afc3c8af8aaf4bc04f23d0b99c938b73a6", + "https://bcr.bazel.build/modules/rules_python/1.6.0/MODULE.bazel": "7e04ad8f8d5bea40451cf80b1bd8262552aa73f841415d20db96b7241bd027d8", + "https://bcr.bazel.build/modules/rules_python/1.7.0/MODULE.bazel": "d01f995ecd137abf30238ad9ce97f8fc3ac57289c8b24bd0bf53324d937a14f8", + "https://bcr.bazel.build/modules/rules_python/1.7.0/source.json": "028a084b65dcf8f4dc4f82f8778dbe65df133f234b316828a82e060d81bdce32", + "https://bcr.bazel.build/modules/rules_shell/0.2.0/MODULE.bazel": "fda8a652ab3c7d8fee214de05e7a9916d8b28082234e8d2c0094505c5268ed3c", + "https://bcr.bazel.build/modules/rules_shell/0.3.0/MODULE.bazel": "de4402cd12f4cc8fda2354fce179fdb068c0b9ca1ec2d2b17b3e21b24c1a937b", + "https://bcr.bazel.build/modules/rules_shell/0.6.1/MODULE.bazel": "72e76b0eea4e81611ef5452aa82b3da34caca0c8b7b5c0c9584338aa93bae26b", + "https://bcr.bazel.build/modules/rules_shell/0.6.1/source.json": "20ec05cd5e592055e214b2da8ccb283c7f2a421ea0dc2acbf1aa792e11c03d0c", + "https://bcr.bazel.build/modules/rules_swift/1.16.0/MODULE.bazel": "4a09f199545a60d09895e8281362b1ff3bb08bbde69c6fc87aff5b92fcc916ca", + "https://bcr.bazel.build/modules/rules_swift/2.1.1/MODULE.bazel": "494900a80f944fc7aa61500c2073d9729dff0b764f0e89b824eb746959bc1046", + "https://bcr.bazel.build/modules/rules_swift/2.4.0/MODULE.bazel": "1639617eb1ede28d774d967a738b4a68b0accb40650beadb57c21846beab5efd", + "https://bcr.bazel.build/modules/rules_swift/3.1.2/MODULE.bazel": "72c8f5cf9d26427cee6c76c8e3853eb46ce6b0412a081b2b6db6e8ad56267400", + "https://bcr.bazel.build/modules/rules_swift/3.1.2/source.json": "e85761f3098a6faf40b8187695e3de6d97944e98abd0d8ce579cb2daf6319a66", + "https://bcr.bazel.build/modules/stardoc/0.5.1/MODULE.bazel": "1a05d92974d0c122f5ccf09291442580317cdd859f07a8655f1db9a60374f9f8", + "https://bcr.bazel.build/modules/stardoc/0.5.3/MODULE.bazel": "c7f6948dae6999bf0db32c1858ae345f112cacf98f174c7a8bb707e41b974f1c", + "https://bcr.bazel.build/modules/stardoc/0.7.0/MODULE.bazel": "05e3d6d30c099b6770e97da986c53bd31844d7f13d41412480ea265ac9e8079c", + "https://bcr.bazel.build/modules/stardoc/0.7.2/MODULE.bazel": "fc152419aa2ea0f51c29583fab1e8c99ddefd5b3778421845606ee628629e0e5", + "https://bcr.bazel.build/modules/stardoc/0.7.2/source.json": "58b029e5e901d6802967754adf0a9056747e8176f017cfe3607c0851f4d42216", + "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.1/MODULE.bazel": "5e463fbfba7b1701d957555ed45097d7f984211330106ccd1352c6e0af0dcf91", + "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/MODULE.bazel": "75aab2373a4bbe2a1260b9bf2a1ebbdbf872d3bd36f80bff058dccd82e89422f", + "https://bcr.bazel.build/modules/swift_argument_parser/1.3.1.2/source.json": "5fba48bbe0ba48761f9e9f75f92876cafb5d07c0ce059cc7a8027416de94a05b", + "https://bcr.bazel.build/modules/upb/0.0.0-20220923-a547704/MODULE.bazel": "7298990c00040a0e2f121f6c32544bab27d4452f80d9ce51349b1a28f3005c43", + "https://bcr.bazel.build/modules/zlib/1.2.11/MODULE.bazel": "07b389abc85fdbca459b69e2ec656ae5622873af3f845e1c9d80fe179f3effa0", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/MODULE.bazel": "eec517b5bbe5492629466e11dae908d043364302283de25581e3eb944326c4ca", + "https://bcr.bazel.build/modules/zlib/1.3.1.bcr.5/source.json": "22bc55c47af97246cfc093d0acf683a7869377de362b5d1c552c2c2e16b7a806", + "https://bcr.bazel.build/modules/zlib/1.3.1/MODULE.bazel": "751c9940dcfe869f5f7274e1295422a34623555916eb98c174c1e945594bf198" + }, + "selectedYankedVersions": {}, + "moduleExtensions": { + "@@pybind11_bazel+//:internal_configure.bzl%internal_configure_extension": { + "general": { + "bzlTransitiveDigest": "b+RP7Sgl8KN0VHamrgTqzGLuYPcQ/Mo4ptNkkHUIIlA=", + "usagesDigest": "D1r3lfzMuUBFxgG8V6o0bQTLMk3GkaGOaPzw53wrwyw=", + "recordedInputs": [ + "REPO_MAPPING:pybind11_bazel+,bazel_tools bazel_tools", + "FILE:@@pybind11_bazel+//MODULE.bazel e6f4c20442eaa7c90d7190d8dc539d0ab422f95c65a57cc59562170c58ae3d34" + ], + "generatedRepoSpecs": { + "pybind11": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "build_file": "@@pybind11_bazel+//:pybind11-BUILD.bazel", + "strip_prefix": "pybind11-2.12.0", + "urls": [ + "https://github.com/pybind/pybind11/archive/v2.12.0.zip" + ] + } + } + } + } + }, + "@@rules_kotlin+//src/main/starlark/core/repositories:bzlmod_setup.bzl%rules_kotlin_extensions": { + "general": { + "bzlTransitiveDigest": "Ga4z8lQy1YQ5rAMy+dOl0dqcCEBnYNCXku8x3YQmDZI=", + "usagesDigest": "QI2z8ZUR+mqtbwsf2fLqYdJAkPOHdOV+tF2yVAUgRzw=", + "recordedInputs": [ + "REPO_MAPPING:rules_kotlin+,bazel_tools bazel_tools" + ], + "generatedRepoSpecs": { + "com_github_jetbrains_kotlin_git": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_compiler_git_repository", + "attributes": { + "urls": [ + "https://github.com/JetBrains/kotlin/releases/download/v1.9.23/kotlin-compiler-1.9.23.zip" + ], + "sha256": "93137d3aab9afa9b27cb06a824c2324195c6b6f6179d8a8653f440f5bd58be88" + } + }, + "com_github_jetbrains_kotlin": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:compiler.bzl%kotlin_capabilities_repository", + "attributes": { + "git_repository_name": "com_github_jetbrains_kotlin_git", + "compiler_version": "1.9.23" + } + }, + "com_github_google_ksp": { + "repoRuleId": "@@rules_kotlin+//src/main/starlark/core/repositories:ksp.bzl%ksp_compiler_plugin_repository", + "attributes": { + "urls": [ + "https://github.com/google/ksp/releases/download/1.9.23-1.0.20/artifacts.zip" + ], + "sha256": "ee0618755913ef7fd6511288a232e8fad24838b9af6ea73972a76e81053c8c2d", + "strip_version": "1.9.23-1.0.20" + } + }, + "com_github_pinterest_ktlint": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_file", + "attributes": { + "sha256": "01b2e0ef893383a50dbeb13970fe7fa3be36ca3e83259e01649945b09d736985", + "urls": [ + "https://github.com/pinterest/ktlint/releases/download/1.3.0/ktlint" + ], + "executable": true + } + }, + "rules_android": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "sha256": "cd06d15dd8bb59926e4d65f9003bfc20f9da4b2519985c27e190cddc8b7a7806", + "strip_prefix": "rules_android-0.1.1", + "urls": [ + "https://github.com/bazelbuild/rules_android/archive/v0.1.1.zip" + ] + } + } + } + } + }, + "@@rules_python+//python/extensions:config.bzl%config": { + "general": { + "bzlTransitiveDigest": "iibnRYgg8LpcfmH7EAnVwYePC3jsVaJ6Id8XxUjSZps=", + "usagesDigest": "ZVSXMAGpD+xzVNPuvF1IoLBkty7TROO0+akMapt1pAg=", + "recordedInputs": [ + "REPO_MAPPING:rules_python+,bazel_tools bazel_tools", + "REPO_MAPPING:rules_python+,pypi__build rules_python++config+pypi__build", + "REPO_MAPPING:rules_python+,pypi__click rules_python++config+pypi__click", + "REPO_MAPPING:rules_python+,pypi__colorama rules_python++config+pypi__colorama", + "REPO_MAPPING:rules_python+,pypi__importlib_metadata rules_python++config+pypi__importlib_metadata", + "REPO_MAPPING:rules_python+,pypi__installer rules_python++config+pypi__installer", + "REPO_MAPPING:rules_python+,pypi__more_itertools rules_python++config+pypi__more_itertools", + "REPO_MAPPING:rules_python+,pypi__packaging rules_python++config+pypi__packaging", + "REPO_MAPPING:rules_python+,pypi__pep517 rules_python++config+pypi__pep517", + "REPO_MAPPING:rules_python+,pypi__pip rules_python++config+pypi__pip", + "REPO_MAPPING:rules_python+,pypi__pip_tools rules_python++config+pypi__pip_tools", + "REPO_MAPPING:rules_python+,pypi__pyproject_hooks rules_python++config+pypi__pyproject_hooks", + "REPO_MAPPING:rules_python+,pypi__setuptools rules_python++config+pypi__setuptools", + "REPO_MAPPING:rules_python+,pypi__tomli rules_python++config+pypi__tomli", + "REPO_MAPPING:rules_python+,pypi__wheel rules_python++config+pypi__wheel", + "REPO_MAPPING:rules_python+,pypi__zipp rules_python++config+pypi__zipp" + ], + "generatedRepoSpecs": { + "rules_python_internal": { + "repoRuleId": "@@rules_python+//python/private:internal_config_repo.bzl%internal_config_repo", + "attributes": { + "transition_setting_generators": {}, + "transition_settings": [] + } + }, + "pypi__build": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/e2/03/f3c8ba0a6b6e30d7d18c40faab90807c9bb5e9a1e3b2fe2008af624a9c97/build-1.2.1-py3-none-any.whl", + "sha256": "75e10f767a433d9a86e50d83f418e83efc18ede923ee5ff7df93b6cb0306c5d4", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__click": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/00/2e/d53fa4befbf2cfa713304affc7ca780ce4fc1fd8710527771b58311a3229/click-8.1.7-py3-none-any.whl", + "sha256": "ae74fb96c20a0277a1d615f1e4d73c8414f5a98db8b799a7931d1582f3390c28", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__colorama": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", + "sha256": "4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__importlib_metadata": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/2d/0a/679461c511447ffaf176567d5c496d1de27cbe34a87df6677d7171b2fbd4/importlib_metadata-7.1.0-py3-none-any.whl", + "sha256": "30962b96c0c223483ed6cc7280e7f0199feb01a0e40cfae4d4450fc6fab1f570", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__installer": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/e5/ca/1172b6638d52f2d6caa2dd262ec4c811ba59eee96d54a7701930726bce18/installer-0.7.0-py3-none-any.whl", + "sha256": "05d1933f0a5ba7d8d6296bb6d5018e7c94fa473ceb10cf198a92ccea19c27b53", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__more_itertools": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/50/e2/8e10e465ee3987bb7c9ab69efb91d867d93959095f4807db102d07995d94/more_itertools-10.2.0-py3-none-any.whl", + "sha256": "686b06abe565edfab151cb8fd385a05651e1fdf8f0a14191e4439283421f8684", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__packaging": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/49/df/1fceb2f8900f8639e278b056416d49134fb8d84c5942ffaa01ad34782422/packaging-24.0-py3-none-any.whl", + "sha256": "2ddfb553fdf02fb784c234c7ba6ccc288296ceabec964ad2eae3777778130bc5", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pep517": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/25/6e/ca4a5434eb0e502210f591b97537d322546e4833dcb4d470a48c375c5540/pep517-0.13.1-py3-none-any.whl", + "sha256": "31b206f67165b3536dd577c5c3f1518e8fbaf38cbc57efff8369a392feff1721", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pip": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/8a/6a/19e9fe04fca059ccf770861c7d5721ab4c2aebc539889e97c7977528a53b/pip-24.0-py3-none-any.whl", + "sha256": "ba0d021a166865d2265246961bec0152ff124de910c5cc39f1156ce3fa7c69dc", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pip_tools": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/0d/dc/38f4ce065e92c66f058ea7a368a9c5de4e702272b479c0992059f7693941/pip_tools-7.4.1-py3-none-any.whl", + "sha256": "4c690e5fbae2f21e87843e89c26191f0d9454f362d8acdbd695716493ec8b3a9", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__pyproject_hooks": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/ae/f3/431b9d5fe7d14af7a32340792ef43b8a714e7726f1d7b69cc4e8e7a3f1d7/pyproject_hooks-1.1.0-py3-none-any.whl", + "sha256": "7ceeefe9aec63a1064c18d939bdc3adf2d8aa1988a510afec15151578b232aa2", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__setuptools": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/90/99/158ad0609729111163fc1f674a5a42f2605371a4cf036d0441070e2f7455/setuptools-78.1.1-py3-none-any.whl", + "sha256": "c3a9c4211ff4c309edb8b8c4f1cbfa7ae324c4ba9f91ff254e3d305b9fd54561", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__tomli": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/97/75/10a9ebee3fd790d20926a90a2547f0bf78f371b2f13aa822c759680ca7b9/tomli-2.0.1-py3-none-any.whl", + "sha256": "939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__wheel": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/7d/cd/d7460c9a869b16c3dd4e1e403cce337df165368c71d6af229a74699622ce/wheel-0.43.0-py3-none-any.whl", + "sha256": "55c570405f142630c6b9f72fe09d9b67cf1477fcf543ae5b8dcb1f5b7377da81", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + }, + "pypi__zipp": { + "repoRuleId": "@@bazel_tools//tools/build_defs/repo:http.bzl%http_archive", + "attributes": { + "url": "https://files.pythonhosted.org/packages/da/55/a03fd7240714916507e1fcf7ae355bd9d9ed2e6db492595f1a67f61681be/zipp-3.18.2-py3-none-any.whl", + "sha256": "dce197b859eb796242b0622af1b8beb0a722d52aa2f57133ead08edd5bf5374e", + "type": "zip", + "build_file_content": "package(default_visibility = [\"//visibility:public\"])\n\nload(\"@rules_python//python:py_library.bzl\", \"py_library\")\n\npy_library(\n name = \"lib\",\n srcs = glob([\"**/*.py\"]),\n data = glob([\"**/*\"], exclude=[\n # These entries include those put into user-installed dependencies by\n # data_exclude to avoid non-determinism.\n \"**/*.py\",\n \"**/*.pyc\",\n \"**/*.pyc.*\", # During pyc creation, temp files named *.pyc.NNN are created\n \"**/*.dist-info/RECORD\",\n \"BUILD\",\n \"WORKSPACE\",\n ]),\n # This makes this directory a top-level in the python import\n # search path for anything that depends on this.\n imports = [\".\"],\n)\n" + } + } + } + } + }, + "@@rules_python+//python/uv:uv.bzl%uv": { + "general": { + "bzlTransitiveDigest": "ijW9KS7qsIY+yBVvJ+Nr1mzwQox09j13DnE3iIwaeTM=", + "usagesDigest": "H8dQoNZcoqP+Mu0tHZTi4KHATzvNkM5ePuEqoQdklIU=", + "recordedInputs": [ + "REPO_MAPPING:rules_python+,bazel_tools bazel_tools", + "REPO_MAPPING:rules_python+,platforms platforms" + ], + "generatedRepoSpecs": { + "uv": { + "repoRuleId": "@@rules_python+//python/uv/private:uv_toolchains_repo.bzl%uv_toolchains_repo", + "attributes": { + "toolchain_type": "'@@rules_python+//python/uv:uv_toolchain_type'", + "toolchain_names": [ + "none" + ], + "toolchain_implementations": { + "none": "'@@rules_python+//python:none'" + }, + "toolchain_compatible_with": { + "none": [ + "@platforms//:incompatible" + ] + }, + "toolchain_target_settings": {} + } + } + } + } + } + }, + "facts": {} +} diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/WORKSPACE b/cli/src/test/resources/workspaces/bzlmod_transitive_external/WORKSPACE new file mode 100644 index 0000000..4cd8526 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "bzlmod_transitive_external_test") diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/BUILD b/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/BUILD new file mode 100644 index 0000000..7e004b2 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/BUILD @@ -0,0 +1,10 @@ +exports_files( + ["data.txt"], + visibility = ["//visibility:public"], +) + +filegroup( + name = "data", + srcs = ["data.txt"], + visibility = ["//visibility:public"], +) diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/MODULE.bazel b/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/MODULE.bazel new file mode 100644 index 0000000..a7b3482 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/MODULE.bazel @@ -0,0 +1,4 @@ +module( + name = "inner", + version = "0.0.0", +) diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/WORKSPACE b/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/WORKSPACE new file mode 100644 index 0000000..7dce903 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "inner") diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/data.txt b/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/data.txt new file mode 100644 index 0000000..ce01362 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/inner/data.txt @@ -0,0 +1 @@ +hello diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/BUILD b/cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/BUILD new file mode 100644 index 0000000..bb591f6 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/BUILD @@ -0,0 +1,7 @@ +genrule( + name = "lib", + srcs = ["@inner//:data"], + outs = ["lib.out"], + cmd = "cat $(SRCS) > $@", + visibility = ["//visibility:public"], +) diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/MODULE.bazel b/cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/MODULE.bazel new file mode 100644 index 0000000..de1cba5 --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/MODULE.bazel @@ -0,0 +1,6 @@ +module( + name = "outer", + version = "0.0.0", +) + +bazel_dep(name = "inner", version = "0.0.0") diff --git a/cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/WORKSPACE b/cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/WORKSPACE new file mode 100644 index 0000000..155e58b --- /dev/null +++ b/cli/src/test/resources/workspaces/bzlmod_transitive_external/outer/WORKSPACE @@ -0,0 +1 @@ +workspace(name = "outer") From 367012a3872762a0cacbb5b12c4f960db64d8ea0 Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Wed, 13 May 2026 16:58:38 -0400 Subject: [PATCH 2/3] Fix #184 transitive external repo change now reaches main-repo consumers Before this change, BazelQueryService.queryBzlmodRepos synthesised one //external: target per bzlmod-managed repo, hashed by repo *metadata* only. There was no rule_input edge between those synthetic targets and no content hash of the underlying directory, so a change inside @inner never propagated to @outer or to //:consume when: //:consume -> @outer//:lib @outer//:lib -> @inner//:data @inner//:data -> inner/data.txt The user in #184 saw this with rules_python's pip_parse where @cryptography sits behind @moto behind a py_test rule. The same shape is what's left unfixed in #197 (alias re-export through an external repo). Two changes in BazelQueryService: 1. queryBzlmodRepos now parses `bazel mod graph --output=json` (via BazelModService, newly injected) and, for each repo, computes the list of direct bzlmod deps. For each dep, it emits an addRuleInput("//external:") on the synthetic target so RuleHasher follows the chain //:consume -> //external:outer -> //external:inner during digest computation. We bridge module name -> canonical name by stripping the trailing `+` suffix that bzlmod's canonical-name scheme uses; this works for the no-version-conflict case and skips extension-generated repos (e.g. "rules_jvm_external++maven+maven") because they do not appear in `bazel mod graph` anyway. (Build.Repository.module_key is not populated by current Bazel, so we cannot key off that.) 2. repositoryToTarget now content-hashes the on-disk directory of every `local_repository`-rule repo (which is what `local_path_override` lowers to) and attaches the digest as a synthetic `_bazel_diff_content_hash` attribute. MODULE.bazel.lock is skipped because bazel regenerates it indeterministically. BCR-fetched repos are left alone -- their version is already in the metadata, so a version bump flips the synthetic target's hash via that channel. ModuleGraphParser gains a parseModuleGraphDepEdges helper that returns module_name -> [dep_module_names] from the same JSON parseModuleGraph already consumes. Module name is used as the key because it is present on every entry in `bazel mod graph` output and on every Repository returned by `bazel mod show_repo`, while module_key is not. The reproducer test loses its @Ignore, picks up the standard Bazel 8.6.0+ skip guard (the fix relies on mod show_repo --output=streamed_proto), and is renamed from testTransitiveExternalRepoChangeImpactsConsumer_reproducerForIssue184 to ...regressionForIssue184. Verified: * Manual end-to-end on Bazel 9.1 against the bzlmod_transitive_external fixture: editing inner/data.txt with no extra flags now lists //:consume in the impacted-targets output (was empty before). * bazel test //cli:E2ETest --test_filter=...regressionForIssue184 PASSED in 2.9s. * Full unit-test sweep (12 kt_jvm_test targets) PASSED. * Broad non-Maven E2ETest sweep (testTransitiveExternalRepo*, testBzlmodLocalPathOverride*, testModuleBazelCommentOnly*, testGoModUpdate*, testMacroBzlChange*, testBzlmodShowRepoDetects*, testExcludeExternalTargets*, testFineGrainedHashBzlMod, testBzlmodTransitiveDepsQuery, testGenerateHashesWithCquery, ...) PASSED in 113.6s. Refs https://github.com/Tinder/bazel-diff/issues/184 Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/bazel_diff/bazel/BazelQueryService.kt | 143 +++++++++++++++++- .../com/bazel_diff/bazel/ModuleGraphParser.kt | 50 ++++++ .../test/kotlin/com/bazel_diff/e2e/E2ETest.kt | 52 +++---- 3 files changed, 211 insertions(+), 34 deletions(-) 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 868b16a..8bf7eb8 100644 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt +++ b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt @@ -26,6 +26,7 @@ class BazelQueryService( private val noBazelrc: Boolean, ) : KoinComponent { private val logger: Logger by inject() + private val modService: BazelModService by inject() private val version: Triple by lazy { runBlocking { determineBazelVersion() } } @OptIn(ExperimentalCoroutinesApi::class) @@ -307,16 +308,69 @@ class BazelQueryService( } } + // Discover the bzlmod module-graph edges so we can encode the dep relationships between + // synthetic //external:* targets. Without this, a target that depends on @outer//... only + // sees //external:outer's *metadata* hash and never picks up content changes in @outer's + // own bzlmod deps (e.g. @inner). With these edges in place, RuleHasher follows the chain + // //:consumer -> //external:outer -> //external:inner during digest computation, so a + // change inside @inner propagates all the way to the main-repo consumer without the user + // having to enumerate every wrapping repo in --fineGrainedHashExternalRepos. See + // https://github.com/Tinder/bazel-diff/issues/184 (transitive build-time chain) and + // https://github.com/Tinder/bazel-diff/issues/197 (alias-wrap chain). + val moduleGraphJson = modService.getModuleGraphJson() + val moduleDepEdges = + if (moduleGraphJson != null) { + ModuleGraphParser().parseModuleGraphDepEdges(moduleGraphJson) + } else { + emptyMap() + } + // `bazel mod show_repo` does not populate Repository.module_key in current Bazel, so + // bridge from a module's `name` (always present in `bazel mod graph` output) to that + // repo's `canonical_name` by stripping any trailing `+` suffix produced by + // bzlmod's canonical-name scheme. This is best-effort: it works for the no-version-conflict + // case (canonical = "+" or "+"). Module-extension repos do not appear + // in `bazel mod graph` at all, so they get no synthetic dep edges -- their contents are + // captured via repo metadata + the per-repo content hash below. + val moduleNameToCanonical = mutableMapOf() + for (repo in repos) { + val canonical = repo.canonicalName + val moduleName = canonical.substringBefore('+').ifEmpty { canonical } + // Only register a name -> canonical edge if the canonical "looks like a module repo" + // (single `+`, no extension separator). Skip extension-generated repos like + // "rules_jvm_external++maven+maven". + if (canonical.count { it == '+' } == 1) { + moduleNameToCanonical[moduleName] = canonical + } + } + val canonicalToRootApparent: Map> = + canonicalToApparent.mapValues { it.value.toList() } + val targets = mutableListOf() for (repo in repos) { + // Derive this repo's bzlmod module name from its canonical name and look up its direct + // deps in the module graph. Translate each dep's module name -> its canonical name -> + // root-visible apparent name; that's what `BazelRule.transformRuleInput` collapses + // non-fine-grained `@//...` rule_inputs to, so adding `//external:` + // as a rule_input here is what wires up the dep chain. + val moduleName = + repo.canonicalName.takeIf { it.count { c -> c == '+' } == 1 }?.substringBefore('+') + val depApparentNames = + if (moduleName != null) { + moduleDepEdges[moduleName] + .orEmpty() + .mapNotNull { moduleNameToCanonical[it] } + .flatMap { canonicalToRootApparent[it].orEmpty() } + } else { + emptyList() + } val apparentNames = canonicalToApparent[repo.canonicalName] if (apparentNames != null) { for (apparentName in apparentNames) { - targets.add(repositoryToTarget(repo, apparentName)) + targets.add(repositoryToTarget(repo, apparentName, depApparentNames)) } } else { // Fallback: use canonical name if no apparent name mapping exists - targets.add(repositoryToTarget(repo, repo.canonicalName)) + targets.add(repositoryToTarget(repo, repo.canonicalName, depApparentNames)) } } @@ -328,22 +382,95 @@ class BazelQueryService( * Converts a Build.Repository proto into a synthetic BazelTarget.Rule named * `//external:`. This mirrors how WORKSPACE repos appear as `//external:*` * targets, and matches the names produced by `transformRuleInput` in BazelRule.kt. + * + * For each bzlmod dep of this repo (as discovered from `bazel mod graph`) a corresponding + * `//external:` is added to the rule's `rule_input` list, so + * [RuleHasher] follows the dep chain when computing the digest. For repos backed by a + * `local_repository` rule (which is what `local_path_override` lowers to), the contents + * of the local directory are also rolled into a synthetic `_bazel_diff_content_hash` + * attribute so file content changes inside the repo flip the synthetic target's hash. */ - private fun repositoryToTarget(repo: Build.Repository, targetName: String): BazelTarget.Rule { + private fun repositoryToTarget( + repo: Build.Repository, + targetName: String, + depApparentNames: List + ): BazelTarget.Rule { val ruleClass = repo.repoRuleName.ifEmpty { "bzlmod_repo" } + val attributes = repo.attributeList.toMutableList() + val contentHash = computeLocalRepoContentHash(repo) + if (contentHash != null) { + attributes.add( + Build.Attribute.newBuilder() + .setName("_bazel_diff_content_hash") + .setType(Build.Attribute.Discriminator.STRING) + .setStringValue(contentHash) + .build()) + } + + val ruleBuilder = + Build.Rule.newBuilder() + .setName("//external:$targetName") + .setRuleClass(ruleClass) + .addAllAttribute(attributes) + for (dep in depApparentNames.toSortedSet()) { + if (dep != targetName) ruleBuilder.addRuleInput("//external:$dep") + } + val target = Build.Target.newBuilder() .setType(Build.Target.Discriminator.RULE) - .setRule( - Build.Rule.newBuilder() - .setName("//external:$targetName") - .setRuleClass(ruleClass) - .addAllAttribute(repo.attributeList)) + .setRule(ruleBuilder) .build() return BazelTarget.Rule(target) } + /** + * Returns a stable hex sha256 over the files inside a `local_repository`-backed repo on + * disk, or null if the repo is not local-backed or the directory cannot be read. + * + * `local_path_override(module_name = "X", path = "...")` in MODULE.bazel lowers to a + * `local_repository` rule, whose `path` attribute is relative to the workspace root. Hashing + * that directory makes file content edits surface in the synthetic //external:X target's + * digest, which fixes the "external repo file change is invisible" half of + * [#184](https://github.com/Tinder/bazel-diff/issues/184) / + * [#197](https://github.com/Tinder/bazel-diff/issues/197). + */ + private fun computeLocalRepoContentHash(repo: Build.Repository): String? { + if (repo.repoRuleName != "local_repository") return null + val pathAttr = + repo.attributeList.find { it.name == "path" && it.type == Build.Attribute.Discriminator.STRING } + ?: return null + val pathStr = pathAttr.stringValue.ifEmpty { return null } + val rawPath = java.nio.file.Paths.get(pathStr) + val repoDir = + (if (rawPath.isAbsolute) rawPath.toFile() else workingDirectory.resolve(rawPath).toFile()) + if (!repoDir.exists() || !repoDir.isDirectory) return null + + return try { + val digest = java.security.MessageDigest.getInstance("SHA-256") + repoDir + .walkTopDown() + .filter { it.isFile } + // Skip MODULE.bazel.lock: bazel auto-regenerates it on every invocation in ways + // that don't reflect a real source change (it depends on resolution state). Letting + // it flip the content hash makes generate-hashes non-deterministic across runs. + .filter { it.name != "MODULE.bazel.lock" } + .map { Pair(it.relativeTo(repoDir).invariantSeparatorsPath, it) } + .sortedBy { it.first } + .forEach { (relPath, file) -> + digest.update(relPath.toByteArray(Charsets.UTF_8)) + digest.update(0x00) + digest.update(file.readBytes()) + digest.update(0x00) + } + digest.digest().joinToString("") { "%02x".format(it) } + } catch (e: Exception) { + logger.w { "Failed to content-hash local repo at $repoDir: ${e.message}" } + null + } + } + /** * Discovers the root module's apparent→canonical repo name mapping by running * `bazel mod dump_repo_mapping ""`. Returns a map of apparent name → canonical name. diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt index 20dee6c..1b47622 100644 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt +++ b/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt @@ -69,6 +69,56 @@ class ModuleGraphParser { } } + /** + * Parses the JSON from `bazel mod graph --output=json` and returns each module's direct + * `bazel_dep` neighbours as a `module_name -> [dep_module_name, ...]` map. + * + * Module names (the `name` field of the `module(name = ...)` declaration) are used as the + * key here because the alternative -- `module_key` -- is not always populated on the + * `Build.Repository` protos returned by `bazel mod show_repo`, which is what consumers want + * to look up against. Module names are universally present and sufficient to find a unique + * row in the graph for the common no-multi-version case. + * + * The same module may appear in multiple places in the JSON tree (`bazel mod graph` inlines + * each module once and references it via `unexpanded` afterwards). This method walks every + * `dependencies` array it sees, so even the `unexpanded` references contribute an edge. The + * resulting map is keyed by the parent's `module_name` and contains the union of all direct + * dep names observed across the tree. + * + * Returns an empty map on parse failure (same tolerance as [parseModuleGraph]). + */ + fun parseModuleGraphDepEdges(json: String): Map> { + val edges = mutableMapOf>() + try { + val root = try { + JsonParser.parseString(json).asJsonObject + } catch (_: Exception) { + val start = json.indexOf('{') + if (start < 0) return emptyMap() + JsonParser.parseString(json.substring(start)).asJsonObject + } + extractDepEdges(root, edges) + } catch (_: Exception) { + return emptyMap() + } + return edges.mapValues { it.value.toList() } + } + + private fun extractDepEdges(obj: JsonObject, edges: MutableMap>) { + val name = obj.get("name")?.asString ?: return + val deps = obj.get("dependencies")?.asJsonArray ?: return + val collected = edges.getOrPut(name) { mutableSetOf() } + for (dep in deps) { + if (!dep.isJsonObject) continue + val depObj = dep.asJsonObject + val depName = depObj.get("name")?.asString ?: continue + collected.add(depName) + // Even if this child is `unexpanded`, recurse to pick up edges from its own expansion + // elsewhere in the tree. + extractDepEdges(depObj, edges) + } + } + /** * Compares two module graphs and returns the keys of modules that changed. * 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 6b44cc4..ab3fac8 100644 --- a/cli/src/test/kotlin/com/bazel_diff/e2e/E2ETest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/e2e/E2ETest.kt @@ -1702,51 +1702,51 @@ class E2ETest { } // ------------------------------------------------------------------------ - // Reproducer for https://github.com/Tinder/bazel-diff/issues/184 + // Regression coverage for https://github.com/Tinder/bazel-diff/issues/184 // ------------------------------------------------------------------------ // The user reported that a change inside an external repo C does not propagate to an // internal target A, when A depends on external repo B and B in turn depends on C. The // concrete report uses rules_python's pip_parse, where requirements.txt resolves into a // separate external repo for every package; the user's py_test rule (A) depends on @moto // (B), and @moto's BUILD targets depend on @cryptography (C). Bumping cryptography in - // requirements.txt does not surface A as impacted. + // requirements.txt did not surface A as impacted. // - // The root cause as described in the issue is that "external repos are treated as an - // opaque blob" by default, and `--fineGrainedHashExternalRepos` only opens up the named - // repos -- the chain through a wrapping external repo is not followed automatically. - // This is exactly the shape of #197's still-unfixed case (see @Ahajha's comment), but - // the #184 user described it in terms of a transitive build-time dependency rather than - // an alias re-export, so a separate fixture documents the deeper chain. + // Root cause: BazelQueryService.queryBzlmodRepos synthesised a `//external:` target + // per bzlmod-managed repo, hashed by repo *metadata* only. There was no rule_input edge + // between those synthetic targets and no content hash of the underlying directory, so a + // change inside @inner never reached @outer or //:consume. // - // The reproducer workspace `bzlmod_transitive_external` wires up: + // Fix: queryBzlmodRepos now (a) parses `bazel mod graph --output=json` for the dep edges + // and emits `addRuleInput("//external:")` per direct bzlmod dep, and (b) + // computes a recursive content hash of the directory for `local_repository`-rule repos + // (which is what `local_path_override` lowers to) and attaches it as a synthetic + // `_bazel_diff_content_hash` attribute. RuleHasher then follows the chain + // //:consume -> //external:outer -> //external:inner during digest computation. // + // Workspace `bzlmod_transitive_external`: // //:consume genrule, depends on @outer//:lib // @outer//:lib genrule in the outer module, depends on @inner//:data // @inner//:data filegroup in the inner module wrapping inner/data.txt // // Both `outer` and `inner` are real bzlmod modules brought in via `local_path_override`. - // outer's MODULE.bazel declares `bazel_dep(name = "inner")` (no override; only the root - // module is allowed to use local_path_override). `.bazelignore` excludes the two - // sub-trees so bazel does not treat them as packages of the main module. // - // Verified by hand against the locally built CLI (Bazel 9.1): + // This test asserts the post-fix behaviour: changing inner/data.txt with no extra flags + // surfaces //:consume in the impacted-targets output. // - // $ bazel-diff generate-hashes -w ... # inner/data.txt = "hello" - // $ bazel-diff generate-hashes -w ... # inner/data.txt = "world" - // $ diff <(jq -S '.hashes' A.json) <(jq -S '.hashes' B.json) # empty -- no diff - // $ bazel-diff get-impacted-targets ... - // (impacted file is empty) - // - // The expected behavior is that `//:consume` should be reported as impacted -- the - // build-time content it transitively consumes via @outer/@inner changed. This test - // asserts that expectation. It is `@Ignore`d so CI stays green until #184 is fixed. + // Requires Bazel 8.6.0+ for the `mod show_repo --output=streamed_proto` path that produces + // the synthetic targets the fix mutates; same gating as + // [testBzlmodShowRepoDetectsModuleBazelChanges]. @Test - @org.junit.Ignore( - "Reproducer for https://github.com/Tinder/bazel-diff/issues/184 -- transitive " + - "external repo file change is not detected. Remove @Ignore once the fix lands.") - fun testTransitiveExternalRepoChangeImpactsConsumer_reproducerForIssue184() { + fun testTransitiveExternalRepoChangeImpactsConsumer_regressionForIssue184() { val version = getBazelVersion() org.junit.Assume.assumeNotNull(version) + val v = version!! + val comparator = + compareBy> { it.first }.thenBy { it.second }.thenBy { it.third } + val hasModShowRepo = comparator.compare(v, Triple(8, 6, 0)) >= 0 && v != Triple(9, 0, 0) + org.junit.Assume.assumeTrue( + "Requires Bazel 8.6.0+ or 9.0.1+ (current: ${v.first}.${v.second}.${v.third})", + hasModShowRepo) val workspaceA = copyTestWorkspace("bzlmod_transitive_external") val workspaceB = copyTestWorkspace("bzlmod_transitive_external") From 240255b3378cb867dc087bb17ced73e9e2472376 Mon Sep 17 00:00:00 2001 From: Maxwell Elliott Date: Sun, 17 May 2026 05:22:42 -0400 Subject: [PATCH 3/3] fix #184 PR: break bzlmod dep cycles before synthesising rule_inputs Following up on #184's CI failure: `bazel mod graph` legitimately contains cycles when one module declares a `dev_dependency` on another that depends back on it (e.g. rules_go <-> gazelle). The previous commit emitted both directions as `rule_input`s on the synthetic `//external:*` targets, so RuleHasher recursed and threw `CircularDependencyException`, making generate-hashes fail with exit 1 on any workspace that pulls in gazelle. The CI failures on `testGoModUpdateImpactsGoTargets_regressionForIssue266` and `testExternalGoDepsAppearInHashes_regressionForIssue228` were exactly this: `cli.execute("generate-hashes", ...)` returning 1 instead of 0. Add `ModuleGraphParser.breakCycles` (DFS with sorted node + edge order, dropping any edge that targets a node currently on the DFS path) and call it from `BazelQueryService.queryBzlmodRepos` before deriving dep edges. The result is deterministic and acyclic; dropping the back-edge is conservative because each repo in the cycle still has its own synthetic hash, so main-repo consumers see content changes via their direct dependency on either side. Unit-tested in ModuleGraphParserTest: acyclic input pass-through, the real-world two-node cycle, a three-node cycle, a self-loop, and determinism across repeated calls. Verified locally: the two go-related E2E regressions now pass, plus testTransitiveExternalRepoChangeImpactsConsumer_regressionForIssue184 still passes. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../com/bazel_diff/bazel/BazelQueryService.kt | 7 +- .../com/bazel_diff/bazel/ModuleGraphParser.kt | 45 +++++++++++++ .../bazel_diff/bazel/ModuleGraphParserTest.kt | 64 +++++++++++++++++++ 3 files changed, 115 insertions(+), 1 deletion(-) 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 8bf7eb8..f3d830a 100644 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt +++ b/cli/src/main/kotlin/com/bazel_diff/bazel/BazelQueryService.kt @@ -320,7 +320,12 @@ class BazelQueryService( val moduleGraphJson = modService.getModuleGraphJson() val moduleDepEdges = if (moduleGraphJson != null) { - ModuleGraphParser().parseModuleGraphDepEdges(moduleGraphJson) + val parser = ModuleGraphParser() + // `bazel mod graph` can return cycles (e.g. rules_go <-> gazelle via the latter's + // dev_dependency). Emitting both directions as rule_inputs on the synthetic + // //external:* targets triggers RuleHasher.CircularDependencyException, so break + // cycles into a deterministic DAG before deriving dep edges. + parser.breakCycles(parser.parseModuleGraphDepEdges(moduleGraphJson)) } else { emptyMap() } diff --git a/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt b/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt index 1b47622..dff1882 100644 --- a/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt +++ b/cli/src/main/kotlin/com/bazel_diff/bazel/ModuleGraphParser.kt @@ -119,6 +119,51 @@ class ModuleGraphParser { } } + /** + * Returns a copy of [edges] with back-edges removed so the result is acyclic. + * + * `bazel mod graph` legitimately contains cycles: for example `rules_go` declares + * `bazel_dep(name = "gazelle", dev_dependency = True)` while `gazelle` declares + * `bazel_dep(name = "rules_go")`, so the dep graph has `rules_go <-> gazelle`. Feeding both + * edges into [BazelQueryService.queryBzlmodRepos] as `rule_input`s on the synthetic + * `//external:*` targets makes `RuleHasher` recurse infinitely and throw + * `CircularDependencyException`. We need a cycle-free dep DAG before emitting edges. + * + * The algorithm is a single DFS, visiting nodes in lexicographic order with their out-edges + * also sorted. An edge to a node currently on the DFS path is a back-edge (it would close + * a cycle) and is dropped; every other edge is kept. The result is therefore (a) acyclic + * and (b) deterministic across runs. + * + * Dropping the back-edge is conservative: a content change in the dropped-edge target still + * surfaces via its own synthetic `//external:*` target's hash (each repo gets one), so + * main-repo consumers that depend on either side of the cycle still see the change. We + * only lose the ability to propagate through the cycle itself, which is fine because all + * SCC members are co-dependent and a change in any of them already invalidates their own + * hashes directly. + */ + fun breakCycles(edges: Map>): Map> { + val result = mutableMapOf>() + val visited = mutableSetOf() + val onPath = mutableSetOf() + + fun dfs(node: String) { + if (node in visited) return + onPath.add(node) + val kept = mutableListOf() + for (target in edges[node].orEmpty().sorted()) { + if (target in onPath) continue // back-edge + kept.add(target) + dfs(target) + } + result[node] = kept + onPath.remove(node) + visited.add(node) + } + + for (node in edges.keys.sorted()) dfs(node) + return result + } + /** * Compares two module graphs and returns the keys of modules that changed. * diff --git a/cli/src/test/kotlin/com/bazel_diff/bazel/ModuleGraphParserTest.kt b/cli/src/test/kotlin/com/bazel_diff/bazel/ModuleGraphParserTest.kt index 192847b..1ac4fb6 100644 --- a/cli/src/test/kotlin/com/bazel_diff/bazel/ModuleGraphParserTest.kt +++ b/cli/src/test/kotlin/com/bazel_diff/bazel/ModuleGraphParserTest.kt @@ -274,6 +274,70 @@ class ModuleGraphParserTest { assertThat(result).containsExactlyInAnyOrder("root", "abseil-cpp@20240116.2") } + // --------------------------------------------------------------------------------------- + // breakCycles + // --------------------------------------------------------------------------------------- + + @Test + fun breakCycles_acyclicInput_returnsEdgesUnchanged() { + val edges = mapOf("a" to listOf("b", "c"), "b" to listOf("c"), "c" to emptyList()) + + val result = parser.breakCycles(edges) + + assertThat(result["a"]!!).containsExactlyInAnyOrder("b", "c") + assertThat(result["b"]!!).containsExactlyInAnyOrder("c") + assertThat(result["c"]!!).isEmpty() + } + + @Test + fun breakCycles_twoNodeCycle_dropsOneEdge() { + // The real-world case: rules_go <-> gazelle. Adding both rule_inputs + // makes RuleHasher recurse infinitely; we keep exactly one direction. + val edges = mapOf("gazelle" to listOf("rules_go"), "rules_go" to listOf("gazelle")) + + val result = parser.breakCycles(edges) + + val total = result.values.sumOf { it.size } + assertThat(total).isEqualTo(1) + // Deterministic: sorted DFS starts at "gazelle" first, so its edge survives + // and rules_go's back-edge is the one that gets dropped. + assertThat(result["gazelle"]!!).containsExactlyInAnyOrder("rules_go") + assertThat(result["rules_go"]!!).isEmpty() + } + + @Test + fun breakCycles_threeNodeCycle_breaksCycleDeterministically() { + val edges = mapOf("a" to listOf("b"), "b" to listOf("c"), "c" to listOf("a")) + + val result = parser.breakCycles(edges) + + // Whatever the algorithm picks, the result must be a DAG: total edges = nodes - 1 + // (otherwise the algorithm would have kept a cycle), and both forward edges survive + // because DFS visits a -> b -> c first and then c -> a is the back-edge. + assertThat(result["a"]!!).containsExactlyInAnyOrder("b") + assertThat(result["b"]!!).containsExactlyInAnyOrder("c") + assertThat(result["c"]!!).isEmpty() + } + + @Test + fun breakCycles_selfLoop_dropsSelfEdge() { + val edges = mapOf("a" to listOf("a", "b"), "b" to emptyList()) + + val result = parser.breakCycles(edges) + + assertThat(result["a"]!!).containsExactlyInAnyOrder("b") + } + + @Test + fun breakCycles_isDeterministic() { + val edges = mapOf("gazelle" to listOf("rules_go"), "rules_go" to listOf("gazelle")) + + val first = parser.breakCycles(edges) + val second = parser.breakCycles(edges) + + assertThat(first).isEqualTo(second) + } + @Test fun findChangedModules_withNewGraphEmpty_returnsAllOldModuleKeys() { val oldGraph =