From e0f5597d1315570951f10220c3a401038e91e996 Mon Sep 17 00:00:00 2001 From: Simon Mavi Stewart Date: Thu, 11 Jun 2026 12:26:20 +0100 Subject: [PATCH 1/3] fix: tolerate binary-less aggregators when hashing the v3 lock file A binary-less aggregator coordinate -- one with no artifact of its own, such as a Gradle module-metadata umbrella, an `available-at` redirect, or a `pom` POM -- can share its `group:artifact` key with a classified sibling that owns the only shasum. When it does, the lock file records that key with shasums like `{"unshaded": ...}` and no `jar` entry, so neither AbstractMain.calculateArtifactHash nor its Starlark mirror in v3_lock_file.bzl reconstructs the plain `group:artifact` key. The plain key still appears in the `repositories` and `dependencies` sections, so walking those sections threw a NullPointerException (Java) and failed with "key not found in dictionary" (Starlark) when resolving real repositories. - Skip artifacts with no reconstructed entry when assigning repositories and dependencies during hashing; they contribute no hashable artifact. The Java and Starlark computations are kept in lock-step so the stored __RESOLVED_ARTIFACTS_HASH still matches. - Filter dependency edges that point at a coordinate with no generated target (_artifact_keys + _filter_dependency_targets) so the BUILD-file generator never emits a dangling target reference. Adds a Java regression test reproducing the exact lock-file shape and Starlark unit tests for both guards (and registers the previously unwired tests/unit/v3_lock_file_test.bzl suite). Co-Authored-By: Claude Opus 4.7 --- private/rules/v3_lock_file.bzl | 45 +++++- .../resolver/cmd/AbstractMain.java | 16 ++- .../resolver/lockfile/V3LockFileTest.java | 52 +++++++ tests/unit/BUILD | 3 + tests/unit/v3_lock_file_test.bzl | 135 ++++++++++++++++++ 5 files changed, 246 insertions(+), 5 deletions(-) create mode 100644 tests/unit/v3_lock_file_test.bzl diff --git a/private/rules/v3_lock_file.bzl b/private/rules/v3_lock_file.bzl index 7ec41236c..0d2a788a5 100644 --- a/private/rules/v3_lock_file.bzl +++ b/private/rules/v3_lock_file.bzl @@ -149,12 +149,18 @@ def _compute_lock_file_hash_v3_impl(lock_file_contents, repo_keys): type_info["sha"] = sha all_infos[dep + suffix] = type_info + # Mirror the guard in AbstractMain.calculateArtifactHash. A binary-less aggregator can be listed + # in `repositories`/`dependencies` under its plain `group:artifact` key while `all_infos` only + # holds the classifier-suffixed key of a sibling that owns the shasum, so it has no entry here. + # Skip absent entries rather than failing; they contribute no hashable artifact. for repo in repo_keys: for artifact in lock_file_contents["repositories"][repo]: - all_infos[artifact]["repository"] = repo + if artifact in all_infos: + all_infos[artifact]["repository"] = repo for dep, dep_info in lock_file_contents["dependencies"].items(): - all_infos[dep]["dependencies"] = sorted(dep_info) + if dep in all_infos: + all_infos[dep]["dependencies"] = sorted(dep_info) return _compute_final_hash(all_infos) @@ -231,9 +237,42 @@ def _from_key(key, spoofed_version): return to_return +def _artifact_keys(raw_artifacts): + # The set of keys `_get_artifacts` will actually generate a target for: one per classifier + # shasum of each artifact root. A binary-less aggregator that shares its `group:artifact` root + # with a classified sibling produces only the sibling's suffixed key, never its own plain key. + keys = {} + for (root, data) in raw_artifacts.items(): + parts = root.split(":") + root_unpacked = { + "group": parts[0], + "artifact": parts[1], + "version": data["version"], + } + if len(parts) > 2: + root_unpacked["packaging"] = parts[2] + else: + root_unpacked["packaging"] = "jar" + + for classifier in data.get("shasums", {}).keys(): + root_unpacked["classifier"] = classifier + keys[to_key(root_unpacked)] = True + return keys + +def _filter_dependency_targets(raw_dependencies, valid_keys): + # Drop dependency edges that point at a coordinate with no generated target (e.g. the plain key + # of a binary-less aggregator), which would otherwise produce a dangling target reference. + filtered_dependencies = {} + for (dep, dependencies) in raw_dependencies.items(): + filtered_dependencies[dep] = [target for target in dependencies if target in valid_keys] + return filtered_dependencies + def _get_artifacts(lock_file_contents): raw_artifacts = lock_file_contents.get("artifacts", {}) - dependencies = lock_file_contents.get("dependencies", {}) + dependencies = _filter_dependency_targets( + lock_file_contents.get("dependencies", {}), + _artifact_keys(raw_artifacts), + ) repositories = lock_file_contents.get("repositories", {}) files = lock_file_contents.get("files", {}) skipped = lock_file_contents.get("skipped", []) diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/AbstractMain.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/AbstractMain.java index d7898538e..66a73e36a 100644 --- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/AbstractMain.java +++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/AbstractMain.java @@ -346,19 +346,31 @@ public static Map calculateArtifactHash(Map ren } } + // A binary-less aggregator (e.g. a Gradle module-metadata umbrella) contributes no hashable + // artifact, so it has no entry in `allInfos`. This also happens when such a coordinate shares + // its `group:artifact` key with a classified sibling that owns the only shasum: no `jar` + // placeholder is added, so only the classified key is reconstructed above. Such a coordinate + // can still be listed in the `repositories` and `dependencies` sections, so skip absent entries + // rather than dereferencing null. Map> repositories = sortMapRecursively((Map) rendered.get("repositories")); for (Map.Entry> repo : repositories.entrySet()) { Iterable repoArtifacts = repo.getValue(); for (String art : repoArtifacts) { - allInfos.get(art).put("repository", repo.getKey()); + Map info = allInfos.get(art); + if (info != null) { + info.put("repository", repo.getKey()); + } } } Map> dependencies = sortMapRecursively((Map) rendered.get("dependencies")); for (Map.Entry> dep : dependencies.entrySet()) { - allInfos.get(dep.getKey()).put("dependencies", dep.getValue()); + Map info = allInfos.get(dep.getKey()); + if (info != null) { + info.put("dependencies", dep.getValue()); + } } Map finalHash = new TreeMap<>(); diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFileTest.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFileTest.java index 0dd390dc7..055ad1fb6 100644 --- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFileTest.java +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFileTest.java @@ -18,6 +18,7 @@ import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertNull; +import static org.junit.Assert.assertTrue; import com.github.bazelbuild.rules_jvm_external.Coordinates; import com.github.bazelbuild.rules_jvm_external.resolver.Conflict; @@ -27,6 +28,7 @@ import com.google.gson.GsonBuilder; import java.io.IOException; import java.net.URI; +import java.nio.file.Paths; import java.util.HashMap; import java.util.Map; import java.util.Optional; @@ -65,6 +67,56 @@ public void shouldRenderAggregatingJarsAsJarWithNullShasum() { assertEquals(expected, shasums); } + @Test + public void calculateArtifactHashToleratesAggregatorSharingShortKeyWithClassifiedSibling() { + // Regression test for a real-world crash. A binary-less aggregator (a Gradle module-metadata + // umbrella) shares its `group:artifact` short key with a classified sibling. The sibling owns + // the only shasum, so `ensureArtifactsAllHaveAtLeastOneShaSum` adds no `jar` placeholder and no + // entry is reconstructed for the umbrella in the hash's intermediate map. The umbrella still + // appears in the `repositories` and `dependencies` sections, so hashing must not deref null. + DependencyInfo umbrella = + new DependencyInfo( + new Coordinates("com.example:umbrella:1.0.0"), + repos, + Optional.empty(), + Optional.empty(), + Set.of(new Coordinates("com.example:dep:1.0.0")), + Set.of(), + Set.of(), + new TreeMap<>()); + DependencyInfo unshadedSibling = + new DependencyInfo( + new Coordinates("com.example", "umbrella", "jar", "unshaded", "1.0.0"), + repos, + Optional.of(Paths.get("umbrella-1.0.0-unshaded.jar")), + Optional.of("52b70baa4650255f6c06b4401f9f5ab74038c4d0f50357077033c6bfd504f2aa"), + Set.of(), + Set.of(), + Set.of(), + new TreeMap<>()); + DependencyInfo dep = + new DependencyInfo( + new Coordinates("com.example:dep:1.0.0"), + repos, + Optional.of(Paths.get("dep-1.0.0.jar")), + Optional.of("77e7c2db478e09882e42a74fb2bc821646cbd4e91c20c616fbd5a8c7b7f350b0"), + Set.of(), + Set.of(), + Set.of(), + new TreeMap<>()); + + Map rendered = + new V3LockFile(repos, Set.of(umbrella, unshadedSibling, dep), Set.of(), true).render(); + + // Before the fix this threw NullPointerException at the repositories/dependencies loops. + Map hash = AbstractMain.calculateArtifactHash(rendered); + + // The sha-bearing artifacts are hashed; the binary-less umbrella contributes no hashable entry. + assertTrue(hash.containsKey("com.example:umbrella:jar:unshaded")); + assertTrue(hash.containsKey("com.example:dep")); + assertFalse(hash.containsKey("com.example:umbrella")); + } + @Test public void shouldRoundTripASimpleSetOfDependencies() { V3LockFile roundTripped = roundTrip(new V3LockFile(repos, Set.of(), Set.of(), true)); diff --git a/tests/unit/BUILD b/tests/unit/BUILD index f3a6e1d69..6ccfc95e1 100644 --- a/tests/unit/BUILD +++ b/tests/unit/BUILD @@ -7,6 +7,7 @@ load(":java_utilities_test.bzl", "java_utilities_test_suite") load(":maven_version_test.bzl", "maven_version_test_suite") load(":proxy_test.bzl", "proxy_test_suite") load(":specs_test.bzl", "artifact_specs_test_suite") +load(":v3_lock_file_test.bzl", "v3_lock_file_test_suite") load(":version_catalogs_test.bzl", "version_catalogs_test_suite") amend_artifact_test_suite() @@ -27,4 +28,6 @@ maven_version_test_suite() proxy_test_suite() +v3_lock_file_test_suite() + version_catalogs_test_suite() diff --git a/tests/unit/v3_lock_file_test.bzl b/tests/unit/v3_lock_file_test.bzl new file mode 100644 index 000000000..db2988b53 --- /dev/null +++ b/tests/unit/v3_lock_file_test.bzl @@ -0,0 +1,135 @@ +load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") +load("//private/rules:v3_lock_file.bzl", "v3_lock_file") + +def _get_artifacts_ignores_dependency_targets_without_artifacts_test_impl(ctx): + env = unittest.begin(ctx) + + artifacts = v3_lock_file.get_artifacts({ + "artifacts": { + "com.example:dep": { + "shasums": { + "jar": "def", + }, + "version": "1.0", + }, + "com.example:root": { + "shasums": { + "jar": "abc", + }, + "version": "1.0", + }, + }, + "dependencies": { + "com.example:root": [ + "com.example:dep", + "com.example:missing-native", + ], + }, + "repositories": {}, + "services": {}, + "version": "3", + }) + + # `com.example:missing-native` has no artifact entry, so the dependency edge is dropped rather + # than emitting a dangling target reference. + root = [artifact for artifact in artifacts if artifact["coordinates"] == "com.example:root:1.0"][0] + asserts.equals(env, ["com.example:dep:spoofed-version"], root["deps"]) + + return unittest.end(env) + +get_artifacts_ignores_dependency_targets_without_artifacts_test = unittest.make( + _get_artifacts_ignores_dependency_targets_without_artifacts_test_impl, +) + +def _get_artifacts_keeps_classifier_dependency_targets_test_impl(ctx): + env = unittest.begin(ctx) + + artifacts = v3_lock_file.get_artifacts({ + "artifacts": { + "com.example:dep": { + "shasums": { + "test-fixtures": "def", + }, + "version": "1.0", + }, + "com.example:root": { + "shasums": { + "jar": "abc", + }, + "version": "1.0", + }, + }, + "dependencies": { + "com.example:root": [ + "com.example:dep:jar:test-fixtures", + "com.example:missing-native", + ], + }, + "repositories": {}, + "services": {}, + "version": "3", + }) + + # A classifier dependency target that does have a matching shasum is preserved. + root = [artifact for artifact in artifacts if artifact["coordinates"] == "com.example:root:1.0"][0] + asserts.equals(env, ["com.example:dep:spoofed-version:test-fixtures@jar"], root["deps"]) + + return unittest.end(env) + +get_artifacts_keeps_classifier_dependency_targets_test = unittest.make( + _get_artifacts_keeps_classifier_dependency_targets_test_impl, +) + +def _compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test_impl(ctx): + env = unittest.begin(ctx) + + # A binary-less aggregator (`com.example:umbrella`) shares its `group:artifact` key with a + # classified sibling (`com.example:umbrella:jar:unshaded`) that owns the only shasum. The plain + # key still appears in `repositories` and `dependencies` but is never reconstructed into the + # hash's intermediate map, so computing the hash must not fail on the missing key. + hashes = v3_lock_file.compute_lock_file_hash({ + "artifacts": { + "com.example:dep": { + "shasums": { + "jar": "def", + }, + "version": "1.0", + }, + "com.example:umbrella": { + "shasums": { + "unshaded": "abc", + }, + "version": "1.0", + }, + }, + "dependencies": { + "com.example:umbrella": ["com.example:dep"], + "com.example:umbrella:jar:unshaded": ["com.example:dep"], + }, + "repositories": { + "https://example.com/repo/": [ + "com.example:dep", + "com.example:umbrella", + "com.example:umbrella:jar:unshaded", + ], + }, + "version": "3", + }) + + asserts.true(env, "com.example:umbrella:jar:unshaded" in hashes) + asserts.true(env, "com.example:dep" in hashes) + asserts.false(env, "com.example:umbrella" in hashes) + + return unittest.end(env) + +compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test = unittest.make( + _compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test_impl, +) + +def v3_lock_file_test_suite(): + unittest.suite( + "v3_lock_file_tests", + get_artifacts_ignores_dependency_targets_without_artifacts_test, + get_artifacts_keeps_classifier_dependency_targets_test, + compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test, + ) From 46bd20c44f6f3331414fdae3d3c72825564070d0 Mon Sep 17 00:00:00 2001 From: Simon Mavi Stewart Date: Thu, 11 Jun 2026 14:07:22 +0100 Subject: [PATCH 2/3] fix: clean v3 lockfile artifact references --- private/rules/v3_lock_file.bzl | 45 +----- .../resolver/cmd/AbstractMain.java | 16 +- .../resolver/gradle/GradleResolver.java | 31 ++-- .../resolver/lockfile/V3LockFile.java | 59 ++++++- .../resolver/remote/Downloader.java | 9 +- .../resolver/gradle/GradleResolverTest.java | 34 +++++ .../resolver/lockfile/V3LockFileTest.java | 144 ++++++++++++++++-- .../resolver/maven/DownloaderTest.java | 35 +++++ tests/unit/v3_lock_file_test.bzl | 57 +------ 9 files changed, 286 insertions(+), 144 deletions(-) diff --git a/private/rules/v3_lock_file.bzl b/private/rules/v3_lock_file.bzl index 0d2a788a5..7ec41236c 100644 --- a/private/rules/v3_lock_file.bzl +++ b/private/rules/v3_lock_file.bzl @@ -149,18 +149,12 @@ def _compute_lock_file_hash_v3_impl(lock_file_contents, repo_keys): type_info["sha"] = sha all_infos[dep + suffix] = type_info - # Mirror the guard in AbstractMain.calculateArtifactHash. A binary-less aggregator can be listed - # in `repositories`/`dependencies` under its plain `group:artifact` key while `all_infos` only - # holds the classifier-suffixed key of a sibling that owns the shasum, so it has no entry here. - # Skip absent entries rather than failing; they contribute no hashable artifact. for repo in repo_keys: for artifact in lock_file_contents["repositories"][repo]: - if artifact in all_infos: - all_infos[artifact]["repository"] = repo + all_infos[artifact]["repository"] = repo for dep, dep_info in lock_file_contents["dependencies"].items(): - if dep in all_infos: - all_infos[dep]["dependencies"] = sorted(dep_info) + all_infos[dep]["dependencies"] = sorted(dep_info) return _compute_final_hash(all_infos) @@ -237,42 +231,9 @@ def _from_key(key, spoofed_version): return to_return -def _artifact_keys(raw_artifacts): - # The set of keys `_get_artifacts` will actually generate a target for: one per classifier - # shasum of each artifact root. A binary-less aggregator that shares its `group:artifact` root - # with a classified sibling produces only the sibling's suffixed key, never its own plain key. - keys = {} - for (root, data) in raw_artifacts.items(): - parts = root.split(":") - root_unpacked = { - "group": parts[0], - "artifact": parts[1], - "version": data["version"], - } - if len(parts) > 2: - root_unpacked["packaging"] = parts[2] - else: - root_unpacked["packaging"] = "jar" - - for classifier in data.get("shasums", {}).keys(): - root_unpacked["classifier"] = classifier - keys[to_key(root_unpacked)] = True - return keys - -def _filter_dependency_targets(raw_dependencies, valid_keys): - # Drop dependency edges that point at a coordinate with no generated target (e.g. the plain key - # of a binary-less aggregator), which would otherwise produce a dangling target reference. - filtered_dependencies = {} - for (dep, dependencies) in raw_dependencies.items(): - filtered_dependencies[dep] = [target for target in dependencies if target in valid_keys] - return filtered_dependencies - def _get_artifacts(lock_file_contents): raw_artifacts = lock_file_contents.get("artifacts", {}) - dependencies = _filter_dependency_targets( - lock_file_contents.get("dependencies", {}), - _artifact_keys(raw_artifacts), - ) + dependencies = lock_file_contents.get("dependencies", {}) repositories = lock_file_contents.get("repositories", {}) files = lock_file_contents.get("files", {}) skipped = lock_file_contents.get("skipped", []) diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/AbstractMain.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/AbstractMain.java index 66a73e36a..d7898538e 100644 --- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/AbstractMain.java +++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/cmd/AbstractMain.java @@ -346,31 +346,19 @@ public static Map calculateArtifactHash(Map ren } } - // A binary-less aggregator (e.g. a Gradle module-metadata umbrella) contributes no hashable - // artifact, so it has no entry in `allInfos`. This also happens when such a coordinate shares - // its `group:artifact` key with a classified sibling that owns the only shasum: no `jar` - // placeholder is added, so only the classified key is reconstructed above. Such a coordinate - // can still be listed in the `repositories` and `dependencies` sections, so skip absent entries - // rather than dereferencing null. Map> repositories = sortMapRecursively((Map) rendered.get("repositories")); for (Map.Entry> repo : repositories.entrySet()) { Iterable repoArtifacts = repo.getValue(); for (String art : repoArtifacts) { - Map info = allInfos.get(art); - if (info != null) { - info.put("repository", repo.getKey()); - } + allInfos.get(art).put("repository", repo.getKey()); } } Map> dependencies = sortMapRecursively((Map) rendered.get("dependencies")); for (Map.Entry> dep : dependencies.entrySet()) { - Map info = allInfos.get(dep.getKey()); - if (info != null) { - info.put("dependencies", dep.getValue()); - } + allInfos.get(dep.getKey()).put("dependencies", dep.getValue()); } Map finalHash = new TreeMap<>(); diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolver.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolver.java index 79e9414e3..162263c5c 100644 --- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolver.java +++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolver.java @@ -207,7 +207,7 @@ private ResolutionResult parseDependencies( artifactsByNode.computeIfAbsent(coordinates, k -> new ArrayList<>()).add(artifact); File artifactFile = artifact.getFile(); - if (artifactFile != null && artifactFile.exists()) { + if (artifactFileMatchesCoordinates(coordinates, artifactFile)) { paths.put(coordinates, artifactFile.toPath()); } @@ -294,29 +294,15 @@ private ResolutionResult parseDependencies( } File bestFile = null; - // Prefer jar/aar with name matching artifactId-version to avoid picking wrong version + // Prefer the artifact file matching the resolved coordinates to avoid picking wrong versions + // or metadata files such as POMs. for (GradleResolvedArtifact artifact : entry.getValue()) { File file = artifact.getFile(); - if (file == null || !file.exists()) { - continue; - } - String name = file.getName(); - boolean isJarOrAar = name.endsWith(".jar") || name.endsWith(".aar"); - if (isJarOrAar && name.contains(coords.getArtifactId() + "-" + coords.getVersion())) { + if (artifactFileMatchesCoordinates(coords, file)) { bestFile = file; break; } } - // Fallback: any existing file (including pom) - if (bestFile == null) { - for (GradleResolvedArtifact artifact : entry.getValue()) { - File file = artifact.getFile(); - if (file != null && file.exists()) { - bestFile = file; - break; - } - } - } if (bestFile != null) { paths.put(coords, bestFile.toPath()); } @@ -337,6 +323,15 @@ private String makeDepKey(String group, String artifact, String version) { return group + ":" + artifact + ":" + version; } + private boolean artifactFileMatchesCoordinates(Coordinates coordinates, File file) { + if (file == null || !file.exists()) { + return false; + } + + Path expectedFileName = Paths.get(coordinates.toRepoPath()).getFileName(); + return expectedFileName != null && expectedFileName.toString().equals(file.getName()); + } + private void addDependency( MutableGraph graph, Coordinates parent, diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFile.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFile.java index 8a4cc9380..a734debc6 100644 --- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFile.java +++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFile.java @@ -258,9 +258,13 @@ public Map render() { } }); + Map> finalizedArtifacts = + ensureArtifactsAllHaveAtLeastOneShaSum(artifacts); + Set artifactKeys = artifactKeys(finalizedArtifacts); + Map lock = new LinkedHashMap<>(); - lock.put("artifacts", ensureArtifactsAllHaveAtLeastOneShaSum(artifacts)); - lock.put("dependencies", removeEmptyItems(deps)); + lock.put("artifacts", finalizedArtifacts); + lock.put("dependencies", removeEmptyItems(filterDependencyKeys(deps, artifactKeys))); if (renderPackages) { lock.put("packages", removeEmptyItems(packages)); } @@ -270,7 +274,7 @@ public Map render() { } // repos is a LinkedHashSet which is iterated in insertion order. // Meaning the order from the Starlark repositories array will be preserved. - lock.put("repositories", repos); + lock.put("repositories", filterRepositoryKeys(repos, artifactKeys)); lock.put("skipped", skipped); if (conflicts != null && !conflicts.isEmpty()) { @@ -289,6 +293,55 @@ public Map render() { return lock; } + private Set artifactKeys(Map> artifacts) { + Set keys = new TreeSet<>(); + for (Map.Entry> entry : artifacts.entrySet()) { + String root = entry.getKey(); + @SuppressWarnings("unchecked") + Map shasums = (Map) entry.getValue().get("shasums"); + if (shasums == null) { + continue; + } + + boolean isJarType = root.chars().filter(ch -> ch == ':').count() == 1; + for (String type : shasums.keySet()) { + String suffix = "jar".equals(type) ? "" : (isJarType ? ":jar" : "") + ":" + type; + keys.add(root + suffix); + } + } + return keys; + } + + private Map> filterRepositoryKeys( + Map> repos, Set artifactKeys) { + Map> filtered = new LinkedHashMap<>(); + repos.forEach( + (repo, artifacts) -> + filtered.put( + repo, + artifacts.stream() + .filter(artifactKeys::contains) + .collect(Collectors.toCollection(TreeSet::new)))); + return filtered; + } + + private Map> filterDependencyKeys( + Map> deps, Set artifactKeys) { + Map> filtered = new TreeMap<>(); + deps.forEach( + (dep, dependencies) -> { + if (!artifactKeys.contains(dep)) { + return; + } + filtered.put( + dep, + dependencies.stream() + .filter(artifactKeys::contains) + .collect(Collectors.toCollection(TreeSet::new))); + }); + return filtered; + } + private Map> ensureArtifactsAllHaveAtLeastOneShaSum( Map> artifacts) { for (Map item : artifacts.values()) { diff --git a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java index c5aa48efa..c073bd492 100644 --- a/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java +++ b/private/tools/java/com/github/bazelbuild/rules_jvm_external/resolver/remote/Downloader.java @@ -125,7 +125,7 @@ private DownloadResult performDownload(Coordinates coordsToUse, String path) { Path pathInRepo = null; Path knownPath = knownPaths.get(coordsToUse); - if (knownPath != null && Files.exists(knownPath)) { + if (knownPath != null && knownPathMatchesRequest(knownPath, path)) { pathInRepo = knownPath; } else { // Check the local cache for the path first @@ -179,6 +179,13 @@ private DownloadResult performDownload(Coordinates coordsToUse, String path) { return new DownloadResult(coordsToUse, Set.copyOf(repos), pathInRepo, sha256); } + private boolean knownPathMatchesRequest(Path knownPath, String path) { + Path requestedFileName = Paths.get(path).getFileName(); + return requestedFileName != null + && requestedFileName.equals(knownPath.getFileName()) + && Files.exists(knownPath); + } + private URI buildUri(URI baseUri, String pathInRepo) { String path = baseUri.getPath(); if (!path.endsWith("/")) { diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java index a5cef5709..0e89427d3 100644 --- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/GradleResolverTest.java @@ -86,6 +86,40 @@ public void resolvesSimpleJvmVariant() throws IOException, XMLStreamException { assertEquals(Set.of(baseCoordinates, jvmCoordinates), resolved.nodes()); } + @Test + public void doesNotUsePomAsArtifactPathForAvailableAtModule() + throws IOException, XMLStreamException { + Coordinates baseCoordinates = new Coordinates("com.example:sample:1.0"); + Coordinates jvmCoordinates = new Coordinates("com.example:sample-jvm:1.0"); + MavenRepo mavenRepo = MavenRepo.create(); + GradleModuleMetadataHelper moduleMetadataHelper = new GradleModuleMetadataHelper(mavenRepo); + + Runfiles runfiles = + Runfiles.preload().withSourceRepository(AutoBazelRepository_GradleResolverTest.NAME); + Path baseMetadataPath = + Paths.get( + runfiles.rlocation( + "rules_jvm_external/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-1.0.module")); + String baseMetadata = Files.readString(baseMetadataPath); + moduleMetadataHelper.addToMavenRepo(baseCoordinates.setExtension("pom"), baseMetadata); + + Path jvmMetadataPath = + Paths.get( + runfiles.rlocation( + "rules_jvm_external/tests/com/github/bazelbuild/rules_jvm_external/resolver/gradle/fixtures/simpleJvmVariant/sample-jvm-1.0.module")); + String jvmMetadata = Files.readString(jvmMetadataPath); + moduleMetadataHelper.addToMavenRepo(jvmCoordinates, jvmMetadata); + + ResolutionResult result = + resolver.resolve(prepareRequestFor(mavenRepo.getPath().toUri(), baseCoordinates)); + + assertEquals(Set.of(baseCoordinates, jvmCoordinates), result.getResolution().nodes()); + assertTrue(result.getArtifacts().get(baseCoordinates).getPath().isEmpty()); + assertEquals( + "sample-jvm-1.0.jar", + result.getArtifacts().get(jvmCoordinates).getPath().get().getFileName().toString()); + } + @Test public void resolvesJvmButNotAndroidVariant() throws IOException, XMLStreamException { // This test validates a scenario similar to diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFileTest.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFileTest.java index 055ad1fb6..5a4d03499 100644 --- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFileTest.java +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/lockfile/V3LockFileTest.java @@ -68,29 +68,78 @@ public void shouldRenderAggregatingJarsAsJarWithNullShasum() { } @Test - public void calculateArtifactHashToleratesAggregatorSharingShortKeyWithClassifiedSibling() { - // Regression test for a real-world crash. A binary-less aggregator (a Gradle module-metadata - // umbrella) shares its `group:artifact` short key with a classified sibling. The sibling owns - // the only shasum, so `ensureArtifactsAllHaveAtLeastOneShaSum` adds no `jar` placeholder and no - // entry is reconstructed for the umbrella in the hash's intermediate map. The umbrella still - // appears in the `repositories` and `dependencies` sections, so hashing must not deref null. + public void shouldKeepStandaloneAggregatingJarReferences() { + Coordinates depCoordinates = new Coordinates("com.example:dep:1.0.0"); + DependencyInfo aggregator = + new DependencyInfo( + new Coordinates("com.example:aggregator:1.0.0"), + repos, + Optional.empty(), + Optional.empty(), + Set.of(depCoordinates), + Set.of(), + Set.of(), + new TreeMap<>()); + DependencyInfo dep = + new DependencyInfo( + depCoordinates, + repos, + Optional.of(Paths.get("dep-1.0.0.jar")), + Optional.of("77e7c2db478e09882e42a74fb2bc821646cbd4e91c20c616fbd5a8c7b7f350b0"), + Set.of(), + Set.of(), + Set.of(), + new TreeMap<>()); + + Map rendered = + new V3LockFile(repos, Set.of(aggregator, dep), Set.of(), true).render(); + + Map artifacts = (Map) rendered.get("artifacts"); + Map data = (Map) artifacts.get("com.example:aggregator"); + Map shasums = (Map) data.get("shasums"); + + HashMap expected = new HashMap<>(); + expected.put("jar", null); + assertEquals(expected, shasums); + + Map dependencies = (Map) rendered.get("dependencies"); + assertEquals(Set.of("com.example:dep"), dependencies.get("com.example:aggregator")); + + Map repositories = (Map) rendered.get("repositories"); + assertTrue( + ((Set) repositories.get(defaultRepo.toString())).contains("com.example:aggregator")); + + assertLockReferencesOnlyArtifactKeys(rendered); + assertTrue(AbstractMain.calculateArtifactHash(rendered).containsKey("com.example:aggregator")); + } + + @Test + public void shouldRemoveNonEmittedAggregatorKeysWhenClassifiedSiblingOwnsShasum() { + // A binary-less aggregator can share its `group:artifact` short key with a classified sibling. + // The sibling owns the only shasum, so the plain key is not emitted as an artifact target. + Coordinates umbrellaCoordinates = new Coordinates("com.example:umbrella:1.0.0"); + Coordinates unshadedCoordinates = + new Coordinates("com.example", "umbrella", "jar", "unshaded", "1.0.0"); + Coordinates depCoordinates = new Coordinates("com.example:dep:1.0.0"); + Coordinates rootCoordinates = new Coordinates("com.example:root:1.0.0"); + DependencyInfo umbrella = new DependencyInfo( - new Coordinates("com.example:umbrella:1.0.0"), + umbrellaCoordinates, repos, Optional.empty(), Optional.empty(), - Set.of(new Coordinates("com.example:dep:1.0.0")), + Set.of(depCoordinates), Set.of(), Set.of(), new TreeMap<>()); DependencyInfo unshadedSibling = new DependencyInfo( - new Coordinates("com.example", "umbrella", "jar", "unshaded", "1.0.0"), + unshadedCoordinates, repos, Optional.of(Paths.get("umbrella-1.0.0-unshaded.jar")), Optional.of("52b70baa4650255f6c06b4401f9f5ab74038c4d0f50357077033c6bfd504f2aa"), - Set.of(), + Set.of(depCoordinates), Set.of(), Set.of(), new TreeMap<>()); @@ -104,19 +153,88 @@ public void calculateArtifactHashToleratesAggregatorSharingShortKeyWithClassifie Set.of(), Set.of(), new TreeMap<>()); + DependencyInfo root = + new DependencyInfo( + rootCoordinates, + repos, + Optional.of(Paths.get("root-1.0.0.jar")), + Optional.of("55eb963f66c63e0513f8f0898f24a5d9933b7634b1ac50811f305ec19c926bb8"), + Set.of(umbrellaCoordinates, unshadedCoordinates, depCoordinates), + Set.of(), + Set.of(), + new TreeMap<>()); Map rendered = - new V3LockFile(repos, Set.of(umbrella, unshadedSibling, dep), Set.of(), true).render(); + new V3LockFile(repos, Set.of(umbrella, unshadedSibling, dep, root), Set.of(), true) + .render(); - // Before the fix this threw NullPointerException at the repositories/dependencies loops. + Map artifacts = (Map) rendered.get("artifacts"); + Map umbrellaData = (Map) artifacts.get("com.example:umbrella"); + Map shasums = (Map) umbrellaData.get("shasums"); + assertEquals(Set.of("unshaded"), shasums.keySet()); + + Map repositories = (Map) rendered.get("repositories"); + Set repoArtifacts = (Set) repositories.get(defaultRepo.toString()); + assertFalse(repoArtifacts.contains("com.example:umbrella")); + assertTrue(repoArtifacts.contains("com.example:umbrella:jar:unshaded")); + + Map dependencies = (Map) rendered.get("dependencies"); + assertFalse(dependencies.containsKey("com.example:umbrella")); + Set rootDependencies = (Set) dependencies.get("com.example:root"); + assertFalse(rootDependencies.contains("com.example:umbrella")); + assertTrue(rootDependencies.contains("com.example:umbrella:jar:unshaded")); + + assertLockReferencesOnlyArtifactKeys(rendered); Map hash = AbstractMain.calculateArtifactHash(rendered); - // The sha-bearing artifacts are hashed; the binary-less umbrella contributes no hashable entry. assertTrue(hash.containsKey("com.example:umbrella:jar:unshaded")); assertTrue(hash.containsKey("com.example:dep")); + assertTrue(hash.containsKey("com.example:root")); assertFalse(hash.containsKey("com.example:umbrella")); } + @SuppressWarnings("unchecked") + private static void assertLockReferencesOnlyArtifactKeys(Map rendered) { + Set artifactKeys = artifactKeys(rendered); + + Map> repositories = (Map>) rendered.get("repositories"); + for (Map.Entry> repo : repositories.entrySet()) { + for (String artifact : repo.getValue()) { + assertTrue( + "Repository references non-emitted artifact: " + artifact, + artifactKeys.contains(artifact)); + } + } + + Map> dependencies = (Map>) rendered.get("dependencies"); + for (Map.Entry> dep : dependencies.entrySet()) { + assertTrue( + "Dependency key is not an emitted artifact: " + dep.getKey(), + artifactKeys.contains(dep.getKey())); + for (String target : dep.getValue()) { + assertTrue( + "Dependency references non-emitted artifact: " + target, artifactKeys.contains(target)); + } + } + } + + @SuppressWarnings("unchecked") + private static Set artifactKeys(Map rendered) { + Map> artifacts = + (Map>) rendered.get("artifacts"); + Set keys = new TreeSet<>(); + for (Map.Entry> entry : artifacts.entrySet()) { + String root = entry.getKey(); + Map shasums = (Map) entry.getValue().get("shasums"); + boolean isJarType = root.chars().filter(ch -> ch == ':').count() == 1; + for (String type : shasums.keySet()) { + String suffix = "jar".equals(type) ? "" : (isJarType ? ":jar" : "") + ":" + type; + keys.add(root + suffix); + } + } + return keys; + } + @Test public void shouldRoundTripASimpleSetOfDependencies() { V3LockFile roundTripped = roundTrip(new V3LockFile(repos, Set.of(), Set.of(), true)); diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/DownloaderTest.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/DownloaderTest.java index 79aecf731..d762ec7cf 100644 --- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/DownloaderTest.java +++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/DownloaderTest.java @@ -14,6 +14,7 @@ package com.github.bazelbuild.rules_jvm_external.resolver.maven; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import com.github.bazelbuild.rules_jvm_external.Coordinates; @@ -25,6 +26,8 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; import java.util.Map; import java.util.Set; import org.junit.Test; @@ -69,4 +72,36 @@ public void downloaderHandleUndeclaredCharacterEntityInPOM() throws IOException assertTrue(downloadResult.getPath().isEmpty()); } + + @Test + public void downloaderIgnoresKnownPathWithDifferentFilename() throws Exception { + Coordinates coords = new Coordinates("com.example:cached-path:1.0"); + Path repo = MavenRepo.create().add(coords).getPath(); + Path expectedJar = repo.resolve(coords.toRepoPath()); + Path pomPath = + expectedJar.resolveSibling(coords.getArtifactId() + "-" + coords.getVersion() + ".pom"); + Path localRepo = Files.createTempDirectory("local"); + + DownloadResult downloadResult = + new Downloader( + Netrc.fromUserHome(), + localRepo, + Set.of(repo.toUri()), + new NullListener(), + false, + Map.of(coords, pomPath)) + .download(coords); + + assertEquals(expectedJar, downloadResult.getPath().get()); + assertEquals(sha256(expectedJar), downloadResult.getSha256().get()); + } + + private String sha256(Path path) throws IOException, NoSuchAlgorithmException { + byte[] digest = MessageDigest.getInstance("SHA-256").digest(Files.readAllBytes(path)); + StringBuilder hex = new StringBuilder(); + for (byte b : digest) { + hex.append(String.format("%02x", b)); + } + return hex.toString(); + } } diff --git a/tests/unit/v3_lock_file_test.bzl b/tests/unit/v3_lock_file_test.bzl index db2988b53..55f90dac2 100644 --- a/tests/unit/v3_lock_file_test.bzl +++ b/tests/unit/v3_lock_file_test.bzl @@ -1,46 +1,6 @@ load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest") load("//private/rules:v3_lock_file.bzl", "v3_lock_file") -def _get_artifacts_ignores_dependency_targets_without_artifacts_test_impl(ctx): - env = unittest.begin(ctx) - - artifacts = v3_lock_file.get_artifacts({ - "artifacts": { - "com.example:dep": { - "shasums": { - "jar": "def", - }, - "version": "1.0", - }, - "com.example:root": { - "shasums": { - "jar": "abc", - }, - "version": "1.0", - }, - }, - "dependencies": { - "com.example:root": [ - "com.example:dep", - "com.example:missing-native", - ], - }, - "repositories": {}, - "services": {}, - "version": "3", - }) - - # `com.example:missing-native` has no artifact entry, so the dependency edge is dropped rather - # than emitting a dangling target reference. - root = [artifact for artifact in artifacts if artifact["coordinates"] == "com.example:root:1.0"][0] - asserts.equals(env, ["com.example:dep:spoofed-version"], root["deps"]) - - return unittest.end(env) - -get_artifacts_ignores_dependency_targets_without_artifacts_test = unittest.make( - _get_artifacts_ignores_dependency_targets_without_artifacts_test_impl, -) - def _get_artifacts_keeps_classifier_dependency_targets_test_impl(ctx): env = unittest.begin(ctx) @@ -62,7 +22,6 @@ def _get_artifacts_keeps_classifier_dependency_targets_test_impl(ctx): "dependencies": { "com.example:root": [ "com.example:dep:jar:test-fixtures", - "com.example:missing-native", ], }, "repositories": {}, @@ -70,7 +29,6 @@ def _get_artifacts_keeps_classifier_dependency_targets_test_impl(ctx): "version": "3", }) - # A classifier dependency target that does have a matching shasum is preserved. root = [artifact for artifact in artifacts if artifact["coordinates"] == "com.example:root:1.0"][0] asserts.equals(env, ["com.example:dep:spoofed-version:test-fixtures@jar"], root["deps"]) @@ -80,13 +38,9 @@ get_artifacts_keeps_classifier_dependency_targets_test = unittest.make( _get_artifacts_keeps_classifier_dependency_targets_test_impl, ) -def _compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test_impl(ctx): +def _compute_lock_file_hash_accepts_cleaned_aggregator_collision_test_impl(ctx): env = unittest.begin(ctx) - # A binary-less aggregator (`com.example:umbrella`) shares its `group:artifact` key with a - # classified sibling (`com.example:umbrella:jar:unshaded`) that owns the only shasum. The plain - # key still appears in `repositories` and `dependencies` but is never reconstructed into the - # hash's intermediate map, so computing the hash must not fail on the missing key. hashes = v3_lock_file.compute_lock_file_hash({ "artifacts": { "com.example:dep": { @@ -103,13 +57,11 @@ def _compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test_i }, }, "dependencies": { - "com.example:umbrella": ["com.example:dep"], "com.example:umbrella:jar:unshaded": ["com.example:dep"], }, "repositories": { "https://example.com/repo/": [ "com.example:dep", - "com.example:umbrella", "com.example:umbrella:jar:unshaded", ], }, @@ -122,14 +74,13 @@ def _compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test_i return unittest.end(env) -compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test = unittest.make( - _compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test_impl, +compute_lock_file_hash_accepts_cleaned_aggregator_collision_test = unittest.make( + _compute_lock_file_hash_accepts_cleaned_aggregator_collision_test_impl, ) def v3_lock_file_test_suite(): unittest.suite( "v3_lock_file_tests", - get_artifacts_ignores_dependency_targets_without_artifacts_test, get_artifacts_keeps_classifier_dependency_targets_test, - compute_lock_file_hash_tolerates_aggregator_sharing_key_with_sibling_test, + compute_lock_file_hash_accepts_cleaned_aggregator_collision_test, ) From 3c5e2343a0b085a282a3ed6f1def35744d189cf8 Mon Sep 17 00:00:00 2001 From: Simon Mavi Stewart Date: Thu, 11 Jun 2026 17:27:46 +0100 Subject: [PATCH 3/3] Update prebuilts --- .../prebuilt/lock_file_converter_deploy.jar | Bin 7419949 -> 7421018 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/private/tools/prebuilt/lock_file_converter_deploy.jar b/private/tools/prebuilt/lock_file_converter_deploy.jar index 0697a457e6f8fa220dd4ab110d14fe3090dbfa0e..1e61af4a1974378784313abf0ca11a5502c97ec7 100755 GIT binary patch delta 12575 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

H=@geo{ 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 L#dEhymoEJu^Pub!