Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -764,12 +764,17 @@ dev_maven.install(
"androidx.annotation:annotation:1.6.0",
# https://github.com/bazel-contrib/rules_jvm_external/issues/1409
"com.squareup.okhttp3:okhttp:4.12.0",
# Versioned snapshot pinning support: https://github.com/bazel-contrib/rules_jvm_external/pull/1412
"com.google.guava:guava:999.0.0-HEAD-jre-SNAPSHOT",
# Non-versioned snapshot pinning support: https://github.com/bazel-contrib/rules_jvm_external/pull/1412
"org.seleniumhq.selenium:selenium-java:4.35.0-SNAPSHOT",
],
generate_compat_repositories = True,
lock_file = "//tests/custom_maven_install:regression_testing_gradle_install.json",
repositories = [
"https://repo1.maven.org/maven2",
"https://maven.google.com",
"https://central.sonatype.com/repository/maven-snapshots",
],
resolver = "gradle",
)
Expand Down
5 changes: 5 additions & 0 deletions WORKSPACE
Original file line number Diff line number Diff line change
Expand Up @@ -388,13 +388,18 @@ maven_install(
"androidx.annotation:annotation:1.6.0",
# https://github.com/bazel-contrib/rules_jvm_external/issues/1409
"com.squareup.okhttp3:okhttp:4.12.0",
# Versioned snapshot pinning support: https://github.com/bazel-contrib/rules_jvm_external/pull/1412
"com.google.guava:guava:999.0.0-HEAD-jre-SNAPSHOT",
# Non-versioned snapshot pinning support: https://github.com/bazel-contrib/rules_jvm_external/pull/1412
"org.seleniumhq.selenium:selenium-java:4.35.0-SNAPSHOT",
],
generate_compat_repositories = True,
maven_install_json = "//tests/custom_maven_install:regression_testing_gradle_install.json",
repin_instructions = "Please run `REPIN=1 bazel run @regression_testing_gradle//:pin` to refresh the lock file.",
repositories = [
"https://repo1.maven.org/maven2",
"https://maven.google.com",
"https://central.sonatype.com/repository/maven-snapshots",
],
resolver = "gradle",
)
Expand Down
4 changes: 3 additions & 1 deletion private/extensions/download_pinned_deps.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,9 @@ def download_pinned_deps(mctx, artifacts, http_files, has_m2local):

http_file(
name = http_file_repository_name,
sha256 = artifact["sha256"],
# sha256 is optional: non-versioned snapshots may not have it
# See: https://github.com/bazel-contrib/rules_jvm_external/pull/1412
sha256 = artifact.get("sha256"),
urls = urls,
# https://github.com/bazelbuild/rules_jvm_external/issues/1028
downloaded_file_path = "v1/%s" % artifact["file"] if artifact["file"] else artifact["file"],
Expand Down
4 changes: 3 additions & 1 deletion private/rules/coursier.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -599,7 +599,9 @@ def _pinned_coursier_fetch_impl(repository_ctx):
http_files.extend([
" http_file(",
" name = \"%s\"," % http_file_repository_name,
" sha256 = \"%s\"," % artifact["sha256"],
# sha256 is optional: non-versioned snapshots may not have it
# See: https://github.com/bazel-contrib/rules_jvm_external/pull/1412
" sha256 = %s," % repr(artifact.get("sha256", None)),
# repository_ctx should point to external/$repository_ctx.name
# The http_file should point to external/$http_file_repository_name
# File-path is relative defined from http_file traveling to repository_ctx.
Expand Down
4 changes: 3 additions & 1 deletion private/rules/v2_lock_file.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -48,10 +48,11 @@ def _compute_lock_file_hash(lock_file_contents):
return hash(repr(to_hash))

def _to_m2_path(unpacked):
path = "{group}/{artifact}/{version}/{artifact}-{version}".format(
path = "{group}/{artifact}/{version}/{artifact}-{version_revision}".format(
artifact = unpacked["artifact"],
group = unpacked["group"].replace(".", "/"),
version = unpacked["version"],
version_revision = unpacked.get("version_revision") or unpacked["version"]
)

classifier = unpacked.get("classifier", "jar")
Expand Down Expand Up @@ -138,6 +139,7 @@ def _get_artifacts(lock_file_contents):
"group": parts[0],
"artifact": parts[1],
"version": data["version"],
"version_revision": data.get("version_revision"),
}
if len(parts) > 2:
root_unpacked["packaging"] = parts[2]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ public class Coordinates implements Comparable<Coordinates> {
private final String groupId;
private final String artifactId;
private final String version;
private final String versionRevision;
private final String classifier;
private final String extension;

Expand Down Expand Up @@ -59,16 +60,23 @@ public Coordinates(String coordinates) {
classifier = "jar".equals(parts[3]) ? "" : parts[3];
version = parts[4];
}
this.versionRevision = null;
}

public Coordinates(
String groupId, String artifactId, String extension, String classifier, String version) {
this(groupId, artifactId, extension, classifier, version, null);
}

public Coordinates(
String groupId, String artifactId, String extension, String classifier, String version, String versionRevision) {
this.groupId = Objects.requireNonNull(groupId, "Group ID");
this.artifactId = Objects.requireNonNull(artifactId, "Artifact ID");
this.extension = extension == null || extension.isEmpty() ? "jar" : extension;
this.classifier =
classifier == null || classifier.isEmpty() || "jar".equals(classifier) ? "" : classifier;
this.version = version == null || version.isEmpty() ? "" : version;
this.versionRevision = versionRevision;
}

public String getGroupId() {
Expand All @@ -88,21 +96,29 @@ public String getClassifier() {
}

public Coordinates setClassifier(String classifier) {
return new Coordinates(getGroupId(), getArtifactId(), getExtension(), classifier, getVersion());
return new Coordinates(getGroupId(), getArtifactId(), getExtension(), classifier, getVersion(), getVersionRevision());
}

public Coordinates setExtension(String extension) {
return new Coordinates(getGroupId(), getArtifactId(), extension, getClassifier(), getVersion());
return new Coordinates(getGroupId(), getArtifactId(), extension, getClassifier(), getVersion(), getVersionRevision());
}

public Coordinates setVersion(String version) {
return new Coordinates(getGroupId(), getArtifactId(), getExtension(), getClassifier(), version);
return new Coordinates(getGroupId(), getArtifactId(), getExtension(), getClassifier(), version, getVersionRevision());
}

public Coordinates setVersionRevision(String versionRevision) {
return new Coordinates(getGroupId(), getArtifactId(), getExtension(), getClassifier(), getVersion(), versionRevision);
}

public String getExtension() {
return extension;
}

public String getVersionRevision() {
return versionRevision;
}

public String asKey() {
StringBuilder coords = new StringBuilder();
coords.append(groupId).append(":").append(artifactId);
Expand Down Expand Up @@ -133,7 +149,7 @@ public String toRepoPath() {
.append("/")
.append(getArtifactId())
.append("-")
.append(getVersion());
.append(isNullOrEmpty(getVersionRevision()) ? getVersion() : getVersionRevision());

String classifier = getClassifier();

Expand Down Expand Up @@ -178,14 +194,15 @@ public boolean equals(Object o) {
return getGroupId().equals(that.getGroupId())
&& getArtifactId().equals(that.getArtifactId())
&& Objects.equals(getVersion(), that.getVersion())
&& Objects.equals(getVersionRevision(), that.getVersionRevision())
&& Objects.equals(getClassifier(), that.getClassifier())
&& Objects.equals(getExtension(), that.getExtension());
}

@Override
public int hashCode() {
return Objects.hash(
getGroupId(), getArtifactId(), getVersion(), getClassifier(), getExtension());
getGroupId(), getArtifactId(), getVersion(), getVersionRevision(), getClassifier(), getExtension());
}

private boolean isNullOrEmpty(String value) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -181,7 +181,8 @@ private ResolutionResult parseDependencies(
gradleCoordinates.getArtifactId(),
extension,
classifier,
gradleCoordinates.getVersion());
gradleCoordinates.getVersion(),
dependency.getVersionRevision());
addDependency(graph, coordinates, dependency, conflicts, requestedDeps, visited);
// if there's a conflict and the conflicting version isn't one that's actually requested
// then it's an actual conflict we want to report
Expand Down Expand Up @@ -275,7 +276,8 @@ private void addDependency(
childCoordinates.getArtifactId(),
extension,
childCoordinates.getClassifier(),
childCoordinates.getVersion());
childCoordinates.getVersion(),
childInfo.getVersionRevision());
graph.addNode(child);
graph.putEdge(parent, child);
// if there's a conflict and the conflicting version isn't one that's actually requested
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,10 @@ public interface GradleResolvedDependency {

void setVersion(String version);

String getVersionRevision();

void setVersionRevision(String versionRevision);

Set<String> getRequestedVersions();

void addRequestedVersion(String requestedVersion);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ public class GradleResolvedDependencyImpl implements Serializable, GradleResolve
private String group;
private String name;
private String version;
private String versionRevision;
private Set<String> requestedVersions;
private boolean conflict;
private List<GradleResolvedDependency> children;
Expand Down Expand Up @@ -61,6 +62,14 @@ public void setVersion(String version) {
this.version = version;
}

public String getVersionRevision() {
return versionRevision;
}

public void setVersionRevision(String versionRevision) {
this.versionRevision = versionRevision;
}

public Set<String> getRequestedVersions() {
return requestedVersions;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -269,6 +269,12 @@ private GradleResolvedDependency walkResolvedComponent(
info.setGroup(component.getModuleVersion().getGroup());
info.setName(component.getModuleVersion().getName());
info.setVersion(component.getModuleVersion().getVersion());

// For versioned snapshot dependencies, extract the timestamped version
if (GradleSnapshotUtil.isVersionedSnapshot(component)) {
String snapshotId = GradleSnapshotUtil.extractSnapshotId(component);
info.setVersionRevision(snapshotId);
}
}

info.addRequestedVersion(info.getVersion()); // add a new version that may have been requested
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
// Copyright 2025 The Bazel Authors. All rights reserved.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package com.github.bazelbuild.rules_jvm_external.resolver.gradle.plugin;

import java.util.regex.Pattern;
import org.gradle.api.artifacts.result.ResolvedComponentResult;

/** Utility class for SNAPSHOT version detection in gradle dependencies.
*
* This class is inspired by the implementation details in the maven resolver
* See: https://github.com/apache/maven-resolver/blob/c9ee9e113f424ac41339ea25313ecceff946960b/maven-resolver-api/src/main/java/org/eclipse/aether/artifact/AbstractArtifact.java#L45
*/
final class GradleSnapshotUtil {
/* A regex to identify when a gradle ModuleComponentidentifer is a versioned snapshot (with timestamp)
*
* See: https://github.com/gradle/gradle/blob/1c1143f9b850f11cb6fef8f7b28405cb5ede45dc/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/repositories/resolver/MavenUniqueSnapshotComponentIdentifier.java#L55
*/
private static final Pattern GRADLE_SNAPSHOT_WITH_TIMESTAMP = Pattern.compile("^.*SNAPSHOT:([0-9]{8}\\.[0-9]{6}-[0-9]+)$");

private GradleSnapshotUtil() {
// Utility class - prevent instantiation
}

/**
* Determines if a component represents a versioned SNAPSHOT dependency
* that follows the gradle identifier convention.
* Example: com.google.guava:guava:999.0.0-HEAD-jre-SNAPSHOT:20250623.150948-114
*/
static boolean isVersionedSnapshot(ResolvedComponentResult component) {
return GRADLE_SNAPSHOT_WITH_TIMESTAMP.matcher(component.getId().toString()).matches();
}

/**
* Extracts the timestamped version from a gradle versioned snapshot component.
* Example: for a component with version "999.0.0-HEAD-jre-SNAPSHOT" and identifier
* "com.google.guava:guava:999.0.0-HEAD-jre-SNAPSHOT:20250623.150948-114"
* returns "999.0.0-HEAD-jre-20250623.150948-114"
* See: https://github.com/gradle/gradle/blob/1c1143f9b850f11cb6fef8f7b28405cb5ede45dc/platforms/software/dependency-management/src/main/java/org/gradle/api/internal/artifacts/repositories/resolver/MavenUniqueSnapshotComponentIdentifier.java#L55
*/
static String extractSnapshotId(ResolvedComponentResult component) {
String version = component.getModuleVersion().getVersion();
String baseVersion = version.substring(0, version.indexOf("-SNAPSHOT"));
String timestamp = component.getId().toString().split(":")[3];
return baseVersion + "-" + timestamp;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,11 @@ public Map<String, Object> render() {
Map<String, Object> artifactValue =
artifacts.computeIfAbsent(shortKey, k -> new TreeMap<>());
artifactValue.put("version", coords.getVersion());

// Add version_revision for reproducible builds when available
if (coords.getVersionRevision() != null && !coords.getVersionRevision().isEmpty()) {
artifactValue.put("version_revision", coords.getVersionRevision());
}

String classifier;
if (coords.getClassifier() == null || coords.getClassifier().isEmpty()) {
Expand All @@ -226,7 +231,15 @@ public Map<String, Object> render() {
@SuppressWarnings("unchecked")
Map<String, String> shasums =
(Map<String, String>) artifactValue.computeIfAbsent("shasums", k -> new TreeMap<>());
info.getSha256().ifPresent(sha -> shasums.put(classifier, sha));

// For non-versioned snapshots, their content can change any moment, so we need to avoid storing the SHA256
boolean isNonVersionedSnapshot = coords.getVersion().endsWith("-SNAPSHOT") && coords.getVersionRevision() == null;
if (isNonVersionedSnapshot) {
// Classifier indicates the files associated to the dependency: store it even if the sha is not present
shasums.put(classifier, null);
} else {
info.getSha256().ifPresent(sha -> shasums.put(classifier, sha));
}

info.getRepositories()
.forEach(
Expand Down
32 changes: 23 additions & 9 deletions tests/bazel_run_tests.sh
Original file line number Diff line number Diff line change
Expand Up @@ -312,31 +312,43 @@ function test_when_both_pom_and_jar_artifact_are_dependencies_jar_artifact_is_pr
function test_gradle_metadata_is_resolved_correctly_for_aar_artifact {
# This artifact in maven_install only has gradle metadata, but it should then automatically resolve to the right aar artifact
# and make it available
bazel query @regression_testing_gradle//:androidx_compose_foundation_foundation_layout_android >> "$TEST_LOG" 2>&1
bazel query --output=label_kind @regression_testing_gradle//:androidx_compose_foundation_foundation_layout_android >> "$TEST_LOG" 2>&1

expect_log "@regression_testing_gradle//:androidx_compose_foundation_foundation_layout_android"
expect_log "aar_import rule @regression_testing_gradle//:androidx_compose_foundation_foundation_layout_android"
}

function test_gradle_metadata_is_resolved_correctly_for_jvm_artifact {
# This artifact in maven_install only has gradle metadata, but it should then automatically resolve to the right jvm artifact
# and make it available
bazel query @regression_testing_gradle//:androidx_annotation_annotation_jvm >> "$TEST_LOG" 2>&1
bazel query --output=label_kind @regression_testing_gradle//:androidx_annotation_annotation_jvm >> "$TEST_LOG" 2>&1

expect_log "@regression_testing_gradle//:androidx_annotation_annotation_jvm"
expect_log "jvm_import rule @regression_testing_gradle//:androidx_annotation_annotation_jvm"

# This is KMP artifact which is a transitive dependency
# and the JAR for this coordinate will just be a dummy jar/placeholder (in some cases a klib file)
# as gradle will use metadata to resolve the right one.
# Regardless we'll want to pull this in because the actual artifacts will be its children
# in the resolved graph with gradle
bazel query @regression_testing_gradle//:com_squareup_okio_okio >> "$TEST_LOG" 2>&1
bazel query --output=label_kind @regression_testing_gradle//:com_squareup_okio_okio >> "$TEST_LOG" 2>&1

expect_log "@regression_testing_gradle//:com_squareup_okio_okio"
expect_log "jvm_import rule @regression_testing_gradle//:com_squareup_okio_okio"

# This is the actual JVM artifact which will have the jar for the KMP artifact
bazel query @regression_testing_gradle//:com_squareup_okio_okio_jvm >> "$TEST_LOG" 2>&1
bazel query --output=label_kind @regression_testing_gradle//:com_squareup_okio_okio_jvm >> "$TEST_LOG" 2>&1

expect_log "@regression_testing_gradle//:com_squareup_okio_okio_jvm"
expect_log "jvm_import rule @regression_testing_gradle//:com_squareup_okio_okio_jvm"
}

function test_gradle_resolves_correctly_a_versioned_snapshot {
bazel query --output=label_kind @regression_testing_gradle//:com_google_guava_guava >> "$TEST_LOG" 2>&1

expect_log "jvm_import rule @regression_testing_gradle//:com_google_guava_guava"
}

function test_gradle_resolves_correctly_a_non_versioned_snapshot {
bazel query --output=label_kind @regression_testing_gradle//:org_seleniumhq_selenium_selenium_java >> "$TEST_LOG" 2>&1

expect_log "jvm_import rule @regression_testing_gradle//:org_seleniumhq_selenium_selenium_java"
}

function test_gradle_versions_catalog {
Expand Down Expand Up @@ -367,8 +379,10 @@ TESTS=(
"test_transitive_dependency_with_type_of_pom"
"test_when_both_pom_and_jar_artifact_are_available_jar_artifact_is_present"
"test_when_both_pom_and_jar_artifact_are_dependencies_jar_artifact_is_present"
# "test_gradle_metadata_is_resolved_correctly_for_aar_artifact"
"test_gradle_metadata_is_resolved_correctly_for_aar_artifact"
"test_gradle_metadata_is_resolved_correctly_for_jvm_artifact"
"test_gradle_resolves_correctly_a_versioned_snapshot"
"test_gradle_resolves_correctly_a_non_versioned_snapshot"
"test_gradle_versions_catalog"
)

Expand Down
Loading