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
39 changes: 39 additions & 0 deletions MODULE.bazel
Original file line number Diff line number Diff line change
Expand Up @@ -738,6 +738,35 @@ dev_maven.install(
],
)

# These repos are used for testing the artifact hash functions. See
# `ArtifactsHashTest.java`
dev_maven.install(
name = "artifacts_hash_no_deps",
artifacts = [
# Has no dependencies
"org.hamcrest:hamcrest-core:3.0",
],
fail_if_repin_required = True,
lock_file = "//tests/custom_maven_install:artifacts_hash_no_deps_install.json",
)
dev_maven.install(
name = "artifacts_hash_with_deps",
artifacts = [
"com.google.code.gson:gson:2.11.0",
],
fail_if_repin_required = True,
lock_file = "//tests/custom_maven_install:artifacts_hash_with_deps_install.json",
)
dev_maven.install(
name = "artifacts_hash_with_deps_from_maven",
artifacts = [
"com.google.guava:guava:33.3.1-jre",
],
fail_if_repin_required = True,
lock_file = "//tests/custom_maven_install:artifacts_hash_with_deps_from_maven_install.json",
resolver = "maven",
)

# Where there are file locks, the pinned and unpinned repos are listed
# next to each other. Where compat repositories are created, they are
# listed next to the repo that created them. The list is otherwise kept
Expand All @@ -746,6 +775,9 @@ dev_maven.install(
# want it to
use_repo(
dev_maven,
"artifacts_hash_no_deps",
"artifacts_hash_with_deps",
"artifacts_hash_with_deps_from_maven",
"duplicate_version_warning",
"duplicate_version_warning_same_version",
"exclusion_testing",
Expand Down Expand Up @@ -837,6 +869,7 @@ use_repo(

http_file(
name = "com.google.ar.sceneform_rendering",
dev_dependency = True,
downloaded_file_path = "rendering-1.10.0.aar",
sha256 = "d2f6cd1d54eee0d5557518d1edcf77a3ba37494ae94f9bb862e570ee426a3431",
urls = [
Expand All @@ -846,6 +879,7 @@ http_file(

http_file(
name = "hamcrest_core_for_test",
dev_dependency = True,
downloaded_file_path = "hamcrest-core-1.3.jar",
sha256 = "66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9",
urls = [
Expand All @@ -855,6 +889,7 @@ http_file(

http_file(
name = "hamcrest_core_srcs_for_test",
dev_dependency = True,
downloaded_file_path = "hamcrest-core-1.3-sources.jar",
sha256 = "e223d2d8fbafd66057a8848cc94222d63c3cedd652cc48eddc0ab5c39c0f84df",
urls = [
Expand All @@ -864,6 +899,7 @@ http_file(

http_file(
name = "gson_for_test",
dev_dependency = True,
downloaded_file_path = "gson-2.9.0.jar",
sha256 = "c96d60551331a196dac54b745aa642cd078ef89b6f267146b705f2c2cbef052d",
urls = [
Expand All @@ -873,6 +909,7 @@ http_file(

http_file(
name = "junit_platform_commons_for_test",
dev_dependency = True,
downloaded_file_path = "junit-platform-commons-1.8.2.jar",
sha256 = "d2e015fca7130e79af2f4608dc54415e4b10b592d77333decb4b1a274c185050",
urls = [
Expand All @@ -883,6 +920,7 @@ http_file(
# https://github.com/bazelbuild/rules_jvm_external/issues/865
http_file(
name = "google_api_services_compute_javadoc_for_test",
dev_dependency = True,
downloaded_file_path = "google-api-services-compute-v1-rev235-1.25.0-javadoc.jar",
sha256 = "b03be5ee8effba3bfbaae53891a9c01d70e2e3bd82ad8889d78e641b22bd76c2",
urls = [
Expand All @@ -892,6 +930,7 @@ http_file(

http_file(
name = "lombok_for_test",
dev_dependency = True,
downloaded_file_path = "lombok-1.18.22.jar",
sha256 = "ecef1581411d7a82cc04281667ee0bac5d7c0a5aae74cfc38430396c91c31831",
urls = [
Expand Down
38 changes: 33 additions & 5 deletions private/lib/coordinates.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def unpack_coordinates(coords):

# If we're using BOMs, the version is optional. That means at this point
# we could be dealing with g:a:p or g:a:v
is_gradle = pieces[2][0].isdigit()
is_gradle = len(pieces[2]) and pieces[2][0].isdigit()

if len(pieces) == 3:
if is_gradle:
Expand Down Expand Up @@ -57,17 +57,30 @@ def unpack_coordinates(coords):
def _is_version_number(part):
return part[0].isdigit()

def _unpack_if_necessary(coords):
if type(coords) == "string":
unpacked = unpack_coordinates(coords)
elif type(coords) == "dict":
unpacked = struct(
group = coords.get("group"),
artifact = coords.get("artifact"),
version = coords.get("version", None),
classifier = coords.get("classifier", None),
extension = coords.get("extension", None),
)
else:
unpacked = coords

return unpacked

def to_external_form(coords):
"""Formats `coords` as a string suitable for use by tools such as Gradle.

The returned format matches Gradle's "external dependency" short-form
syntax: `group:name:version:classifier@packaging`
"""

if type(coords) == "string":
unpacked = unpack_coordinates(coords)
else:
unpacked = coords
unpacked = _unpack_if_necessary(coords)

to_return = "%s:%s:%s" % (unpacked.group, unpacked.artifact, unpacked.version)

Expand All @@ -81,6 +94,21 @@ def to_external_form(coords):

return to_return

# This matches the `Coordinates#asKey` method in the Java tree, and the
# implementations must be kept in sync.
def to_key(coords):
unpacked = _unpack_if_necessary(coords)

key = unpacked.group + ":" + unpacked.artifact

if unpacked.classifier and "jar" != unpacked.classifier:
extension = unpacked.packaging if unpacked.packaging else "jar"
key += ":" + unpacked.packaging + ":" + unpacked.classifier
elif unpacked.packaging and "jar" != unpacked.packaging:
key += ":" + unpacked.packaging

return key

_DEFAULT_PURL_REPOS = [
"https://repo.maven.apache.org/maven2",
"https://repo.maven.apache.org/maven2/",
Expand Down
2 changes: 1 addition & 1 deletion private/rules/coursier.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -531,7 +531,7 @@ def _pinned_coursier_fetch_impl(repository_ctx):
"This feature ensures that the file is not modified manually. To generate this " +
"signature, run 'bazel run %s'." % pin_target,
)
elif importer.compute_lock_file_hash(maven_install_json_content) != dep_tree_signature:
elif not importer.validate_lock_file_hash(maven_install_json_content, dep_tree_signature):
# Then, validate that the signature provided matches the contents of the dependency_tree.
# This is to stop users from manually modifying maven_install.json.
if _get_fail_if_repin_required(repository_ctx):
Expand Down
4 changes: 4 additions & 0 deletions private/rules/v1_lock_file.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,9 @@ def _compute_lock_file_hash(lock_file_contents):
signature_inputs.append(":".join(artifact_group))
return hash(repr(sorted(signature_inputs)))

def _validate_lock_file_hash(lock_file_contents, expected_hash):
return _compute_lock_file_hash(lock_file_contents) == expected_hash

def create_dependency(dep):
url = dep.get("url")
if url:
Expand Down Expand Up @@ -139,6 +142,7 @@ v1_lock_file = struct(
get_input_artifacts_hash = _get_input_artifacts_hash,
get_lock_file_hash = _get_lock_file_hash,
compute_lock_file_hash = _compute_lock_file_hash,
validate_lock_file_hash = _validate_lock_file_hash,
get_artifacts = _get_artifacts,
get_netrc_entries = _get_netrc_entries,
has_m2local = _has_m2local,
Expand Down
28 changes: 27 additions & 1 deletion private/rules/v2_lock_file.bzl
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and

load("//private/lib:coordinates.bzl", "to_external_form", "to_key")

_REQUIRED_KEYS = ["artifacts", "dependencies", "repositories"]

def _is_valid_lock_file(lock_file_contents):
Expand All @@ -35,7 +37,7 @@ def _get_input_artifacts_hash(lock_file_contents):
def _get_lock_file_hash(lock_file_contents):
return lock_file_contents.get("__RESOLVED_ARTIFACTS_HASH")

def _compute_lock_file_hash(lock_file_contents):
def _original_compute_lock_file_hash(lock_file_contents):
to_hash = {}
for key in sorted(_REQUIRED_KEYS):
value = lock_file_contents.get(key)
Expand All @@ -45,6 +47,29 @@ def _compute_lock_file_hash(lock_file_contents):
to_hash.update({key: json.decode(json.encode(value))})
return hash(repr(to_hash))

def _compute_lock_file_hash(lock_file_contents):
# Note: this function is exactly equivalent to the one in `ArtifactsHash.java` if you
# make a change there please make it here, and vice versa.
lines = []
artifacts = _get_artifacts(lock_file_contents)

for artifact in artifacts:
line = "%s | %s | " % (to_external_form(artifact["coordinates"]), artifact["sha256"] if artifact["sha256"] else "")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Probably want to mention that the format should also be updated in tandem with ArtifactsHash.java.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. Also added a very similar comment to ArtifactsHash.java

deps = []
for dep in artifact["deps"]:
deps.append(to_key(dep))
line += ",".join(sorted(deps))

lines.append(line)

lines = sorted(lines)
to_hash = "\n".join(lines)

return hash(to_hash)

def _validate_lock_file_hash(lock_file_contents, expected_hash):
return _compute_lock_file_hash(lock_file_contents) == expected_hash or _original_compute_lock_file_hash(lock_file_contents) == expected_hash

def _to_m2_path(unpacked):
path = "{group}/{artifact}/{version}/{artifact}-{version}".format(
artifact = unpacked["artifact"],
Expand Down Expand Up @@ -192,6 +217,7 @@ v2_lock_file = struct(
get_input_artifacts_hash = _get_input_artifacts_hash,
get_lock_file_hash = _get_lock_file_hash,
compute_lock_file_hash = _compute_lock_file_hash,
validate_lock_file_hash = _validate_lock_file_hash,
get_artifacts = _get_artifacts,
get_netrc_entries = _get_netrc_entries,
render_lock_file = _render_lock_file,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,26 @@ public class Coordinates implements Comparable<Coordinates> {
private final String classifier;
private final String extension;

/**
* Converts a `String` in one of two formats and extracts the information from it.
*
* <p>The two supported formats are:
*
* <ol>
* <li>group:artifact:version:classifier@extension
* <li>group:artifact:extension:classifier:version.
* </ol>
*
* The first of these matches Gradle's <a
* href="https://docs.gradle.org/8.11.1/dsl/org.gradle.api.artifacts.dsl.DependencyHandler.html#N1739F">
* external dependencies</a> form, and is the preferred format to use since it matches
* expectations of users of other tools. The second format is the one used within
* `rules_jvm_external` since its inception.
*
* <p>Note that there is potential confusion when only three segments are given (that is, the
* value could be one of `group:artifact:version` or `group:artifact:extension`) In this case, we
* assume the value is `group:artifact:version` as this is far more widely used.
*/
public Coordinates(String coordinates) {
Objects.requireNonNull(coordinates, "Coordinates");

Expand All @@ -36,29 +56,64 @@ public Coordinates(String coordinates) {
"Bad artifact coordinates "
+ coordinates
+ ", expected format is"
+ " <groupId>:<artifactId>[:<extension>[:<classifier>][:<version>]");
+ " <groupId>:<artifactId>[:<version>][:<classifier>][@<extension>");
}

groupId = Objects.requireNonNull(parts[0]);
artifactId = Objects.requireNonNull(parts[1]);

boolean isGradle =
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please document this constructor formally on what formats are accepted. It's getting a bit harder to understand this code, and AFAIUC, we are trying to use this to canonicalize the format, so some documentation will be useful.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done. I have added a little white-lie about how we handle a:b:c, but I'll create a follow up PR to make that true.

coordinates.contains("@")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

can non-gradle coordinates contain @?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Theoretically, a non-gradle coordinate can contain an @ character, but the Maven folks recommend against it: https://maven.apache.org/guides/mini/guide-naming-conventions.html

I've never seen one in the wild, and I think it's reasonable to tell people to use the Gradle format if it's something they really need.

|| (parts.length > 2 && !parts[2].isEmpty() && Character.isDigit(parts[2].charAt(0)));

String version = null;
String extension = "jar";
String classifier = "jar";

if (parts.length == 2) {
extension = "jar";
classifier = "";
version = "";
} else if (parts.length == 3) {
extension = "jar";
classifier = "";
version = parts[2];
} else if (parts.length == 4) {
extension = parts[2];
classifier = "";
version = parts[3];
} else {
} else if (parts.length == 5) { // Unambiguously the original format
extension = "".equals(parts[2]) ? "jar" : parts[2];
classifier = "jar".equals(parts[3]) ? "" : parts[3];
version = parts[4];
} else if (parts.length == 3) {
// Could either be g:a:e or g:a:v or g:a:v@e
if (isGradle) {
classifier = "";

if (parts[2].contains("@")) {
String[] subparts = parts[2].split("@", 2);
version = subparts[0];
extension = subparts[1];
} else {
extension = "jar";
version = parts[2];
}
}
} else {
// Could be either g:a:e:c or g:a:v:c or g:a:v:c@e
if (isGradle) {
version = parts[2];
if (parts[3].contains("@")) {
String[] subparts = parts[3].split("@", 2);
classifier = subparts[0];
extension = subparts[1];
} else {
classifier = parts[3];
extension = "jar";
}
} else {
extension = parts[2];
classifier = "";
version = parts[3];
}
}

this.version = version;
this.classifier = classifier;
this.extension = extension;
}

public Coordinates(
Expand Down Expand Up @@ -103,6 +158,7 @@ public String getExtension() {
return extension;
}

// This method matches `coordinates.bzl#to_key`. Any changes here must be matched there.
public String asKey() {
StringBuilder coords = new StringBuilder();
coords.append(groupId).append(":").append(artifactId);
Expand Down Expand Up @@ -155,13 +211,23 @@ public int compareTo(Coordinates o) {
}

public String toString() {
String versionless = asKey();
StringBuilder builder = new StringBuilder();

builder.append(getGroupId()).append(":").append(getArtifactId());

if (getVersion() != null && !getVersion().isEmpty()) {
builder.append(":").append(getVersion());
}

if (getClassifier() != null && !getClassifier().isEmpty() && !"jar".equals(getClassifier())) {
builder.append(":").append(getClassifier());
}

if (version != null && !version.isEmpty()) {
return versionless + ":" + version;
if (getExtension() != null && !getExtension().isEmpty() && !"jar".equals(getExtension())) {
builder.append("@").append(getExtension());
}

return versionless;
return builder.toString();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -100,7 +100,7 @@ public static void main(String[] args) {
Set<DependencyInfo> infos = converter.getDependencies();
Set<Conflict> conflicts = converter.getConflicts();

Map<String, Object> rendered = new V2LockFile(repositories, infos, conflicts).render();
Map<String, Object> rendered = new V2LockFile(-1, repositories, infos, conflicts).render();

String converted =
new GsonBuilder().setPrettyPrinting().serializeNulls().create().toJson(rendered);
Expand Down
Loading