Make `amend_artifact` also match BOMs (#1586)
`amend_artifact` previously searched only the `artifacts` list, so a BOM
(declared via `install(boms = ...)` or split out of a `from_toml`
`libs.versions.toml` via `bom_modules`) could not be amended. The
amendment failed with "No artifact found matching coordinates" because
BOMs are tracked in a separate list that the amendment never searched.
`amend_artifact` now searches both the artifacts and boms lists, so a
BOM's version and other properties can be adjusted through the same
mechanism as a regular artifact (for example, force-pinning a BOM
version so transitive deps can't silently upgrade it).
Add unit tests covering amendment of artifacts, BOMs, and the
no-match case.
Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
diff --git a/private/extensions/maven.bzl b/private/extensions/maven.bzl
index 3ec6618..6c23e52 100644
--- a/private/extensions/maven.bzl
+++ b/private/extensions/maven.bzl
@@ -314,6 +314,30 @@
return (artifact.group == coords.group and
artifact.artifact == coords.artifact)
+def apply_amendment(amend, artifacts, boms):
+ """Applies an `amend_artifact` to matching entries in `artifacts` and `boms`.
+
+ Matching is by `group:artifact`, and matched entries are amended in place.
+ Both lists are searched: a BOM declared via `install(boms = ...)` or
+ `from_toml` can be amended through the same mechanism as a regular
+ artifact.
+
+ Args:
+ amend: The `amend_artifact` tag whose values should be applied.
+ artifacts: List of artifact structs to amend in place.
+ boms: List of BOM structs to amend in place.
+
+ Returns:
+ `True` if at least one artifact or BOM matched the amendment.
+ """
+ amended = False
+ for collection in (artifacts, boms):
+ for i, entry in enumerate(collection):
+ if _coordinates_match(entry, amend.coordinates):
+ collection[i] = _amend_artifact(entry, amend)
+ amended = True
+ return amended
+
def process_gradle_versions_file(parsed, bom_modules):
artifacts = []
boms = []
@@ -526,19 +550,17 @@
repo = target_repos.get(amend.name, {})
if mod.is_root:
artifacts = repo.get("artifacts", [])
+ boms = repo.get("boms", [])
else:
if not "bazel_dep_to_artifacts" in repo:
repo["bazel_dep_to_artifacts"] = {}
artifacts = repo["bazel_dep_to_artifacts"].get(mod.name, [])
+ if not "bazel_dep_to_boms" in repo:
+ repo["bazel_dep_to_boms"] = {}
+ boms = repo["bazel_dep_to_boms"].get(mod.name, [])
- # Find matching artifacts and amend them
- amended = False
- for i, artifact in enumerate(artifacts):
- if _coordinates_match(artifact, amend.coordinates):
- artifacts[i] = _amend_artifact(artifact, amend)
- amended = True
-
- if not amended:
+ # Amend matching artifacts and BOMs.
+ if not apply_amendment(amend, artifacts, boms):
# If no matching artifact found, this might be an error or we could create a placeholder
fail("No artifact found matching coordinates '%s' for amendment" % amend.coordinates)
diff --git a/tests/unit/BUILD b/tests/unit/BUILD
index 4144fce..f3a6e1d 100644
--- a/tests/unit/BUILD
+++ b/tests/unit/BUILD
@@ -1,3 +1,4 @@
+load(":amend_artifact_test.bzl", "amend_artifact_test_suite")
load(":artifact_utilities_test.bzl", "artifact_utilities_test_suite")
load(":coordinates_test.bzl", "coordinates_test_suite")
load(":coursier_test.bzl", "coursier_test_suite")
@@ -8,6 +9,8 @@
load(":specs_test.bzl", "artifact_specs_test_suite")
load(":version_catalogs_test.bzl", "version_catalogs_test_suite")
+amend_artifact_test_suite()
+
artifact_specs_test_suite()
artifact_utilities_test_suite()
diff --git a/tests/unit/amend_artifact_test.bzl b/tests/unit/amend_artifact_test.bzl
new file mode 100644
index 0000000..88edc9c
--- /dev/null
+++ b/tests/unit/amend_artifact_test.bzl
@@ -0,0 +1,94 @@
+"""Tests for `amend_artifact` matching both artifacts and BOMs."""
+
+load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
+load("//private/extensions:maven.bzl", "apply_amendment")
+load("//private/lib:coordinates.bzl", "unpack_coordinates")
+
+def _amend(coordinates, force_version = None, neverlink = None, testonly = None, exclusions = None):
+ """Builds a struct mirroring an `amend_artifact` tag."""
+ return struct(
+ coordinates = coordinates,
+ force_version = force_version,
+ neverlink = neverlink,
+ testonly = testonly,
+ exclusions = exclusions,
+ )
+
+def _amends_artifact_impl(ctx):
+ env = unittest.begin(ctx)
+
+ artifacts = [unpack_coordinates("com.google.guava:guava:31.1-jre")]
+ boms = []
+
+ matched = apply_amendment(_amend("com.google.guava:guava", testonly = "true"), artifacts, boms)
+
+ asserts.true(env, matched)
+ asserts.equals(env, True, artifacts[0].testonly)
+ asserts.equals(env, "31.1-jre", artifacts[0].version)
+
+ return unittest.end(env)
+
+amends_artifact_test = unittest.make(_amends_artifact_impl)
+
+def _amends_bom_impl(ctx):
+ env = unittest.begin(ctx)
+
+ artifacts = []
+ boms = [unpack_coordinates("com.google.protobuf:protobuf-bom:3.25.5")]
+
+ matched = apply_amendment(_amend("com.google.protobuf:protobuf-bom", force_version = "on"), artifacts, boms)
+
+ asserts.true(env, matched)
+ asserts.equals(env, "com.google.protobuf", boms[0].group)
+ asserts.equals(env, "protobuf-bom", boms[0].artifact)
+ asserts.equals(env, True, boms[0].force_version)
+
+ # Amending the BOM must not change its declared version.
+ asserts.equals(env, "3.25.5", boms[0].version)
+
+ return unittest.end(env)
+
+amends_bom_test = unittest.make(_amends_bom_impl)
+
+def _amends_bom_alongside_artifacts_impl(ctx):
+ env = unittest.begin(ctx)
+
+ artifacts = [unpack_coordinates("com.google.guava:guava:31.1-jre")]
+ boms = [unpack_coordinates("com.google.protobuf:protobuf-bom:3.25.5")]
+
+ matched = apply_amendment(_amend("com.google.protobuf:protobuf-bom", force_version = "on"), artifacts, boms)
+
+ asserts.true(env, matched)
+ asserts.equals(env, True, boms[0].force_version)
+
+ # The artifact that wasn't targeted is left untouched: a raw unpacked
+ # artifact has no `force_version` field, whereas an amended one always does.
+ asserts.false(env, hasattr(artifacts[0], "force_version"))
+
+ return unittest.end(env)
+
+amends_bom_alongside_artifacts_test = unittest.make(_amends_bom_alongside_artifacts_impl)
+
+def _no_match_returns_false_impl(ctx):
+ env = unittest.begin(ctx)
+
+ artifacts = [unpack_coordinates("com.google.guava:guava:31.1-jre")]
+ boms = [unpack_coordinates("com.google.protobuf:protobuf-bom:3.25.5")]
+
+ matched = apply_amendment(_amend("org.example:does-not-exist", force_version = "on"), artifacts, boms)
+
+ # The caller relies on this being False to fail the build with a helpful message.
+ asserts.false(env, matched)
+
+ return unittest.end(env)
+
+no_match_returns_false_test = unittest.make(_no_match_returns_false_impl)
+
+def amend_artifact_test_suite():
+ unittest.suite(
+ "amend_artifact_tests",
+ amends_artifact_test,
+ amends_bom_test,
+ amends_bom_alongside_artifacts_test,
+ no_match_returns_false_test,
+ )