Encapsulate bzlmod dependency layering (#1618)
diff --git a/MODULE.bazel b/MODULE.bazel
index f9dd02b..22e50a9 100644
--- a/MODULE.bazel
+++ b/MODULE.bazel
@@ -204,6 +204,18 @@
neverlink = "on",
)
dev_maven.install(
+ name = "duplicate_version_error",
+ artifacts = [
+ "com.fasterxml.jackson.core:jackson-annotations:2.10.1",
+ "com.fasterxml.jackson.core:jackson-annotations:2.12.1",
+ ],
+ duplicate_version_warning = "error",
+ repositories = [
+ "https://repo1.maven.org/maven2",
+ "https://maven.google.com",
+ ],
+)
+dev_maven.install(
name = "duplicate_version_warning",
artifacts = [
"com.fasterxml.jackson.core:jackson-annotations:2.10.1",
@@ -1054,6 +1066,23 @@
)
dev_maven.install(
+ name = "coursier_layering",
+ artifacts = [
+ "com.google.code.findbugs:jsr305:3.0.1",
+ ],
+ known_contributing_modules = [
+ "coursier_higher_layer",
+ ],
+ resolver = "coursier",
+)
+
+bazel_dep(name = "coursier_higher_layer", version = "0.0.0", dev_dependency = True)
+local_path_override(
+ module_name = "coursier_higher_layer",
+ path = "tests/integration/coursier_higher_layer",
+)
+
+dev_maven.install(
name = "root_module_can_override",
artifacts = ["com.squareup:javapoet:1.11.1"],
known_contributing_modules = [
@@ -1087,6 +1116,8 @@
dev_maven,
"amend_artifacts",
"bom_only_pinning",
+ "coursier_layering",
+ "duplicate_version_error",
"duplicate_version_warning",
"duplicate_version_warning_same_version",
"exclusion_testing",
diff --git a/docs/bzlmod.md b/docs/bzlmod.md
index b07de4a..edce4bc 100644
--- a/docs/bzlmod.md
+++ b/docs/bzlmod.md
@@ -172,43 +172,168 @@
## Module dependency layering
-In order to allow modules to collaborate on required dependencies, the `bzlmod` extension will
-collect the artifacts from all tags with the same `name` attribute together before performing a
-dependency resolution. You'll know this is happening because a message will be printed to inform
-you which modules are contributing to which namespace:
+The extension collects declarations from all tags with the same `name` before resolving them. Each
+name is an independent Maven repository namespace. Declarations in one namespace never affect
+another namespace.
+
+The root module and its dependencies have different roles during layering. The root contributes
+the declarations that belong to the current Bazel project. Every other module is a non-root
+contributor. Coordinates are conceptually matched by `group:artifact:packaging:classifier`,
+meaning that a classified JAR layers independently of its unclassified JAR.
+
+When performing [duplicate coordinate checks](#diagnostics), the declarations are keyed by
+`group:artifact:classifier`, but not packaging. Packaging-distinct declarations at different
+versions can therefore warn or fail even though the extension layers them independently. It also
+continues to check multiple root declarations. Ordinary cross-module conflicts with the same
+layering key no longer reach this check.
+
+### Version precedence
+
+For conflicts between modules, layering selects one complete declaration for each coordinate. The
+selected declaration supplies its exclusions, `neverlink`, `testonly`, `force_version`, packaging,
+classifier, and other fields. Fields from discarded declarations are not merged into it. Layering
+does not deduplicate within the root module, so repeated root declarations for one coordinate reach
+the existing repository-level duplicate check, which warns or fails according to
+`duplicate_version_warning`. Forcing is the exception: if any module, the root included, sets
+`force_version` on the same coordinate at two different versions, layering will fail with an
+error message.
+
+The surviving declaration is chosen by these rules:
+
+1. A forced version in the root module always wins.
+2. Otherwise, a forced declaration beats any unforced one, whatever the versions.
+3. Otherwise, the highest version wins, regardless of which module declared it.
+
+On ties and conflicts:
+
+- Two non-root modules that force different versions is an error and fails before resolution. The root can settle it by forcing the version itself.
+- On a tie (equal versions, or the same forced version from more than one module) the first module's declaration is kept; the root counts as first.
+- A non-root artifact marked `testonly` is dropped.
+
+Be aware that non-default packaging and classifiers remain independent of each other and of the
+plain versioned coordinate. This may lead to some surprises when resolution is complete.
+
+"Highest" uses the Maven `ComparableVersion` ordering implemented by
+`private/rules/maven_version.bzl`, not lexical string ordering.
+
+`version_conflict_policy = "pinned"` changes this interaction. For the Gradle and Maven resolvers,
+root artifacts are marked as `force_version` before layering. The duplicate-force check applies to
+declared forces before this policy is applied. Maven then marks every versioned root declaration.
+Gradle first selects one version for each root `group:artifact`: an unclassified declaration takes
+precedence over classified declarations, and Maven `ComparableVersion` order selects among
+declarations with the same classification status. Every root declaration for that module at the
+selected version is then marked forced, including classified declarations. The root consequently
+wins because it now forces the coordinate. For Coursier, layering is unchanged and the one
+surviving direct version is later passed as a `--force-version` argument. A higher non-root
+version can therefore displace the root under Coursier and then be pinned.
+
+The `force_version` flag can be set by an `artifact` tag, an `amend_artifact` tag, or a regular
+artifact read by `from_toml`. Coordinates in `install.artifacts` cannot carry the flag. BOMs use the
+same extension-layer precedence rules as artifacts.
+
+### Contributors and configuration
+
+When the root and other modules contribute artifacts to the same namespace, the extension prints a
+message such as:
`The maven repository 'multiple_lock_files' has contributions from multiple bzlmod modules, and will be resolved together: ["bzlmod_lock_files", "rules_jvm_external"]`
-In the root module, if this is expected and known, you can disable this warning by adding
-the list of modules to the `known_contributing_modules` attribute of the `install` tag. The entry
-to add will be printed for you as part of the warning. Once you set a value for `known_contributing_modules` then only those modules will be allowed to contribute dependencies.
+If those contributions are expected, set `known_contributing_modules` on the root `install` tag.
+The warning includes the value to add. Once this attribute is non-empty, only listed modules may
+contribute artifacts or BOMs to that namespace. A module that contributes only BOMs triggers the
+same contribution warning and can be acknowledged through the same attribute.
-The default name used is `maven`. Modules that are expected to be included via a `bazel_dep` should
-avoid using the default name, and should always set their own (eg. `rules_jvm_external` uses
-`rules_jvm_external_deps` for its own dependencies) The exception to this is where a module provides
-functionality that would otherwise be obtained using a maven dependency.
+After dependencies are layered, scalar `install` attributes from the root module take precedence.
+List attributes are combined root-first, while preserving their existing deduplication or
+concatenation behaviour.
-Put another way, only projects that are only ever going to be used as root modules should use the
-default name.
+The default namespace is `maven`. A module intended for use through `bazel_dep` should normally use
+its own name, such as the `rules_jvm_external_deps` namespace used by this project. The default is
+appropriate when a module deliberately contributes functionality that would otherwise be supplied
+as a Maven dependency, or when the project is only used as the root module.
-The message is printed so that should you need to understand why a particular dependency or
-transitive dependency is at an unexpected version you'll have the information you need to diagnose
-the problem.
+### <a id="diagnostics"></a>Diagnostics
-When dependencies are layered in this way, you may see a warning similar to:
+Layering keeps the following diagnostics so that unexpected versions can be traced to their
+contributing module. Each entry shows the message a user sees and how to resolve it. Several are
+governed by [`duplicate_version_warning`](bzlmod-api.md#maven.install-duplicate_version_warning),
+which is `"error"` to fail, `"warn"` (the default) to print and continue, or `"none"` to stay
+silent.
+
+#### Which modules are contributing to this repository?
+
+An unacknowledged non-root module contributing artifacts or BOMs always prints the contribution
+warning:
```
-"WARNING: The following maven modules appear in multiple sub-modules with potentially different versions. Consider adding one of these to your root module to ensure consistent versions:
- com.google.guava:guava (31.1-jre, 33.2.1-jre)
+The maven repository 'my-project' has contributions from multiple bzlmod modules, and will be resolved together: ["my-project", "some-other-module"]
```
-The resolver will use the highest version artifact from the root and sub-modules. If the root version is not the highest you will see a warning during repinning similar to:
+**Remedy:** if the contributions are expected, set `known_contributing_modules` on the root
+`install` tag to the module names in the message; otherwise remove the contributing module. When
+`known_contributing_modules` instead excludes a contributor, an `INFO` message is printed when
+`RJE_VERBOSE` is set.
+
+#### Why is my forced version rejected?
+
+One module forcing the same coordinate at two different versions fails:
+
+```
+Module 'my_module' forces dependency 'com.google.guava:guava' at different versions: 31.1-jre and 33.0.0-jre.
+```
+
+**Remedy:** keep a single version for the coordinate within that module.
+
+Non-root modules forcing different versions of a coordinate that the root does not force fails with:
+
+```
+Conflicting forced versions for dependency 'com.google.guava:guava': module_a wants 31.1-jre, module_b wants 33.0.0-jre. Add an `artifact` tag to the root module at the version you want and set `force_version = True`.
+```
+
+**Remedy:** add an `artifact` tag to the root module at the version you want and set
+`force_version = True` on it.
+
+#### Which version will be selected?
+
+When layering selects a version different from the root version, the version-selection warning is:
```
WARNING: For dependency 'com.google.protobuf:protobuf-java' the root @maven repo wants version 3.25.5, but got 4.27.2 from the bazel_worker_java bazel dep. Please update the version in your MODULE.bazel or set `force_version = True`.
```
-You can either update the version in the root module to the highest version or set `force_version = True` in the root module to ensure that version will be the one used in the dependency resolution.
+`duplicate_version_warning` controls whether this warns, fails, or stays silent.
+
+**Remedy:** update the version in the root module to the highest version, or set
+`force_version = True` in the root module to ensure that version is the one used
+in dependency resolution.
+
+You only see this when the version that ends up being used differs from the one declared in your
+root module. For example, a `bazel_dep` may pull in a higher version of a dependency you also
+declare in the root. If the resolved version already matches your root declaration, there is
+nothing to act on and no warning is printed. Coordinates that only a `bazel_dep` declares (and
+your root does not) do not produce this warning either; they are covered by the contribution
+warning above instead.
+
+#### Which versions are reaching the repository?
+
+When more than one version of the same dependency makes it into the repository, whether declared
+twice in one module or contributed by several modules, the message is:
+
+```
+Found duplicate artifact versions
+ com.google.guava:guava has multiple versions 31.1-jre, 33.0.0-jre
+Please remove duplicate artifacts from the artifact list so you do not get unexpected artifact versions
+```
+
+`duplicate_version_warning` controls whether this warns, fails, or stays silent. **Remedy:** remove
+duplicate artifacts from the artifact list.
+
+A non-root-only coordinate is reported as an `INFO` message when a repin variable and `RJE_VERBOSE`
+are both set:
+
+```
+INFO: The @maven repo is getting the additional artifact com.google.guava:guava:33.0.0-jre from the module_a bazel dep.
+```
## Known issues
diff --git a/private/extensions/maven.bzl b/private/extensions/maven.bzl
index d64709a..503e6c2 100644
--- a/private/extensions/maven.bzl
+++ b/private/extensions/maven.bzl
@@ -9,9 +9,15 @@
"escape",
"strip_packaging_and_classifier_and_version",
)
-load("//private/lib:coordinates.bzl", "to_external_form", "to_key", "unpack_coordinates")
+load("//private/lib:coordinates.bzl", "to_external_form", "unpack_coordinates")
+load(
+ "//private/lib:layering.bzl",
+ "DEFAULT_NAME",
+ "layer_maven_namespace",
+ "remove_empty_fields",
+ "should_print_diagnostic",
+)
load("//private/rules:coursier.bzl", "DEFAULT_AAR_IMPORT_LABEL", "coursier_fetch", "pinned_coursier_fetch")
-load("//private/rules:maven_version.bzl", "compare_maven_versions")
load("//private/rules:unpinned_maven_pin_command_alias.bzl", "unpinned_maven_pin_command_alias")
load("//private/rules:v1_lock_file.bzl", "v1_lock_file")
load("//private/rules:v3_lock_file.bzl", "v2_lock_file", "v3_lock_file")
@@ -21,8 +27,6 @@
"https://repo1.maven.org/maven2",
]
-DEFAULT_NAME = "maven"
-
_DEFAULT_RESOLVER = "coursier"
artifact = tag_class(
@@ -77,7 +81,7 @@
"additional_netrc_lines": attr.string_list(doc = "Additional lines prepended to the netrc file used by `http_file` (with `maven_install_json` only).", default = []),
"use_credentials_from_home_netrc_file": attr.bool(doc = "Whether to pass machine login credentials from the ~/.netrc file to coursier.", default = False),
"duplicate_version_warning": attr.string(
- doc = """What to do if there are duplicate artifacts
+ doc = """What to do if layering selects a non-root version instead of the root version, or if the root module declares multiple versions of the same coordinate
If "error", then print a message and fail the build.
If "warn", then print a warning and continue.
@@ -181,24 +185,6 @@
to_return.append(exclusion)
return to_return
-def _warn_if_multiple_contributing_modules(repo, repo_name, non_root_bazel_dep_to_artifacts):
- known_contributing_modules = repo.get("known_contributing_modules", sets.make())
- contributing_module_names = non_root_bazel_dep_to_artifacts.keys()
- new_contributing_modules = sets.difference(sets.make(contributing_module_names), known_contributing_modules)
- if sets.length(new_contributing_modules) > 0:
- print("The maven repository '%s' has contributions from multiple bzlmod modules, and will be resolved together: %s." % (
- repo_name,
- sorted(contributing_module_names),
- ) + "\nSee https://github.com/bazel-contrib/rules_jvm_external/blob/master/docs/bzlmod.md#module-dependency-layering" +
- " for more information. \n" +
- " To suppress this warning review the contributions from the other modules and add the following attribute" +
- " in the root MODULE.bazel file: \n" +
- "maven.install(\n" +
- (" name = \"{0}\"\n".format(repo_name) if repo_name != DEFAULT_NAME else "") +
- " known_contributing_modules = {0},\n".format(sorted(contributing_module_names)) +
- " ...\n" +
- ")")
-
def _generate_compat_repos(name, existing_compat_repos, artifacts):
seen = []
@@ -217,68 +203,6 @@
return seen
-def _deduplicate_non_root_artifacts(bazel_dep_to_non_root_artifacts, return_only_artifacts = False):
- coordinate_to_artifact = {}
- for bazel_dep_name in bazel_dep_to_non_root_artifacts:
- for artifact in bazel_dep_to_non_root_artifacts.get(bazel_dep_name, []):
- if not getattr(artifact, "testonly", False):
- artifact_key = to_key(artifact)
-
- # prioritize highest version
- if artifact_key in coordinate_to_artifact:
- _bazel_dep_name, current_artifact = coordinate_to_artifact[artifact_key]
- if compare_maven_versions(current_artifact.version, artifact.version) == -1:
- coordinate_to_artifact[artifact_key] = (bazel_dep_name, artifact)
- else:
- coordinate_to_artifact[artifact_key] = (bazel_dep_name, artifact)
-
- if return_only_artifacts:
- return [v[1] for v in coordinate_to_artifact.values()]
- else:
- return coordinate_to_artifact
-
-# Each bzlmod module may contribute jars to different rules_jvm_external maven repo namespaces.
-# We emit a warning to the user if a module overrides an artifact version in the root maven repo.
-#
-# This can be typical for the default @maven namespace, if a bzlmod dependency
-# wishes to contribute to the users' jars.
-def _deduplicate_artifacts_with_root_priority(name, root_artifacts, bazel_dep_to_non_root_artifacts, repin_env_var, rje_verbose_env_var):
- """Deduplicate artifacts, giving priority to root module artifacts with force_version set."""
- non_root_coordinate_to_artifact = _deduplicate_non_root_artifacts(bazel_dep_to_non_root_artifacts)
-
- duplicate_artifact_warning = ""
- filtered_non_root_artifacts = []
- for root_artifact in root_artifacts:
- artifact_key = to_key(root_artifact)
- if artifact_key in non_root_coordinate_to_artifact:
- bazel_dep_name, non_root_artifact = non_root_coordinate_to_artifact.pop(artifact_key)
- if not getattr(root_artifact, "force_version", False):
- # prioritize highest version
- if compare_maven_versions(root_artifact.version, non_root_artifact.version) == -1:
- filtered_non_root_artifacts.append(non_root_artifact)
- duplicate_artifact_warning = duplicate_artifact_warning + (
- "\nWARNING: For dependency '%s:%s' the root @%s repo wants version %s, " % (root_artifact.group, root_artifact.artifact, name, root_artifact.version) +
- "but got %s from the %s bazel dep. " % (non_root_artifact.version, bazel_dep_name) +
- "Please update the version in your MODULE.bazel or set `force_version = True`."
- )
-
- # Add any remaining non root artifacts that weren't found in the root artifact list
- addtional_artifact_message = ""
- for bazel_dep_name, non_root_artifact in non_root_coordinate_to_artifact.values():
- addtional_artifact_message = addtional_artifact_message + (
- "\nINFO: The @%s repo is getting the additional artifact %s:%s:%s from the %s bazel dep." % (name, non_root_artifact.group, non_root_artifact.artifact, non_root_artifact.version, bazel_dep_name)
- )
- filtered_non_root_artifacts.append(non_root_artifact)
-
- if repin_env_var:
- if duplicate_artifact_warning != "":
- print(duplicate_artifact_warning)
- if rje_verbose_env_var:
- if addtional_artifact_message != "":
- print(addtional_artifact_message)
-
- return root_artifacts + filtered_non_root_artifacts
-
def _get_tri_state_bool(amend_val, original_val):
if amend_val in ["true", "on"]:
return True
@@ -590,62 +514,10 @@
"""
return root_list + non_root_list
-def remove_fields(s):
- """Used for reducing an artifact struct down to only those fields that have values"""
- return {
- k: getattr(s, k)
- for k in dir(s)
- if k != "to_json" and k != "to_proto" and getattr(s, k, None)
- } | {"version": getattr(s, "version", "")}
-
-def _defines_gradle_module_version(candidate, current):
- """Whether candidate should force the Gradle module version instead of current."""
- candidate_classified = bool(getattr(candidate, "classifier", None))
- current_classified = bool(getattr(current, "classifier", None))
- if candidate_classified != current_classified:
- # An unclassified root defines the module version.
- return current_classified
- return compare_maven_versions(candidate.version, current.version) == 1
-
-def _select_gradle_forced_versions(artifacts):
- """Selects the single version to force for each Gradle group:artifact module.
-
- Gradle resolves one version per module regardless of classifier, so forcing
- two versions of the same module (for example a main jar and its
- test-fixtures jar) makes resolution unsatisfiable.
- """
- winners = {}
- for artifact in artifacts:
- if not getattr(artifact, "version", None):
- continue
- key = "%s:%s" % (artifact.group, artifact.artifact)
- current = winners.get(key)
- if current == None or _defines_gradle_module_version(artifact, current):
- winners[key] = artifact
- return {key: winner.version for key, winner in winners.items()}
-
-def _forces_gradle_module_version(artifact, forced_versions):
- version = getattr(artifact, "version", None)
- if not version:
- return False
- return version == forced_versions.get("%s:%s" % (artifact.group, artifact.artifact))
-
-def apply_root_version_conflict_policy(artifacts, resolver, version_conflict_policy):
- """Applies the install-level conflict policy to root module artifacts."""
- if resolver not in ["gradle", "maven"] or version_conflict_policy != "pinned":
- return artifacts
-
- if resolver == "gradle":
- forced_versions = _select_gradle_forced_versions(artifacts)
- return [
- struct(**(remove_fields(artifact) | {"force_version": True})) if _forces_gradle_module_version(artifact, forced_versions) else artifact
- for artifact in artifacts
- ]
-
- return [
- struct(**(remove_fields(artifact) | {"force_version": True})) if getattr(artifact, "version", None) else artifact
- for artifact in artifacts
- ]
+def _print_layering_diagnostics(diagnostics, repin_env_var, rje_verbose_env_var):
+ for diagnostic in diagnostics:
+ if should_print_diagnostic(diagnostic, repin_env_var, rje_verbose_env_var):
+ print(diagnostic.text)
def maven_impl(mctx):
repos = {}
@@ -701,62 +573,21 @@
merged_repo.update(non_root_repo)
merged_repo.update(root_repo)
- # Special handling for artifacts and boms - deduplicate with root priority
- root_artifacts = apply_root_version_conflict_policy(
- root_repo.get("artifacts", []),
- root_repo.get("resolver", _DEFAULT_RESOLVER),
- root_repo.get("version_conflict_policy", "default"),
+ layered_artifacts_and_boms = layer_maven_namespace(
+ name = repo_name,
+ root_present = repo_name in root_module_repos,
+ root_artifacts = root_repo.get("artifacts", []),
+ root_boms = root_repo.get("boms", []),
+ resolver = root_repo.get("resolver", _DEFAULT_RESOLVER),
+ version_conflict_policy = root_repo.get("version_conflict_policy", "default"),
+ duplicate_version_warning = root_repo.get("duplicate_version_warning") or "warn",
+ known_contributing_modules = root_repo.get("known_contributing_modules", sets.make()),
+ bazel_dep_to_non_root_artifacts = non_root_repo.get("bazel_dep_to_artifacts", {}),
+ bazel_dep_to_non_root_boms = non_root_repo.get("bazel_dep_to_boms", {}),
)
- bazel_dep_to_non_root_artifacts = non_root_repo.get("bazel_dep_to_artifacts", {})
- root_boms = root_repo.get("boms", [])
- bazel_dep_to_non_root_boms = non_root_repo.get("bazel_dep_to_boms", {})
-
- if repo_name in root_module_repos.keys():
- known_contributing_modules = root_repo.get("known_contributing_modules", sets.make())
- if sets.length(known_contributing_modules) == 0:
- # Warn users if multiple modules contribute to the same maven `name`
- _warn_if_multiple_contributing_modules(root_repo, repo_name, bazel_dep_to_non_root_artifacts)
- else:
- # Filter results so only modules in the known_contributing_modules add artifacts or boms
- all_non_root_artifact_modules = bazel_dep_to_non_root_artifacts.keys()
- bazel_dep_to_non_root_artifacts = {
- k: bazel_dep_to_non_root_artifacts[k]
- for k in sets.to_list(known_contributing_modules)
- if k in bazel_dep_to_non_root_artifacts
- }
- if rje_verbose_env_var:
- for k in all_non_root_artifact_modules:
- if k not in bazel_dep_to_non_root_artifacts.keys():
- print("\nINFO: The @%s repo is not using deps from %s because it is not in the known_contributing_modules" % (repo_name, k))
- all_non_root_bom_modules = bazel_dep_to_non_root_boms.keys()
- bazel_dep_to_non_root_boms = {
- k: bazel_dep_to_non_root_boms[k]
- for k in sets.to_list(known_contributing_modules)
- if k in bazel_dep_to_non_root_boms
- }
- if rje_verbose_env_var:
- for k in all_non_root_bom_modules:
- if k not in bazel_dep_to_non_root_boms.keys():
- print("\nINFO: The @%s repo is not using boms from %s because it is not in the known_contributing_modules" % (repo_name, k))
-
- merged_repo["artifacts"] = _deduplicate_artifacts_with_root_priority(
- repo_name,
- root_artifacts,
- bazel_dep_to_non_root_artifacts,
- repin_env_var,
- rje_verbose_env_var,
- )
-
- merged_repo["boms"] = _deduplicate_artifacts_with_root_priority(
- repo_name,
- root_boms,
- bazel_dep_to_non_root_boms,
- repin_env_var,
- rje_verbose_env_var,
- )
- else:
- merged_repo["artifacts"] = _deduplicate_non_root_artifacts(bazel_dep_to_non_root_artifacts, True)
- merged_repo["boms"] = _deduplicate_non_root_artifacts(bazel_dep_to_non_root_boms, True)
+ merged_repo["artifacts"] = layered_artifacts_and_boms.artifacts
+ merged_repo["boms"] = layered_artifacts_and_boms.boms
+ _print_layering_diagnostics(layered_artifacts_and_boms.diagnostics, repin_env_var, rje_verbose_env_var)
# For list attributes, concatenate but avoid duplicates (root items first)
for list_attr in ["repositories", "excluded_artifacts", "additional_netrc_lines"]:
@@ -818,8 +649,8 @@
existing_repos = []
for (name, repo) in repos.items():
- boms_json = [json.encode(remove_fields(b)) for b in repo.get("boms", [])]
- artifacts_json = [json.encode(remove_fields(a)) for a in repo.get("artifacts", [])]
+ boms_json = [json.encode(remove_empty_fields(b)) for b in repo.get("boms", [])]
+ artifacts_json = [json.encode(remove_empty_fields(a)) for a in repo.get("artifacts", [])]
excluded_artifacts = parse.parse_exclusion_spec_list(repo.get("excluded_artifacts", []))
excluded_artifacts_json = [_json.write_exclusion_spec(a) for a in excluded_artifacts]
diff --git a/private/lib/layering.bzl b/private/lib/layering.bzl
new file mode 100644
index 0000000..f1c51fb
--- /dev/null
+++ b/private/lib/layering.bzl
@@ -0,0 +1,335 @@
+"""Support for layering Maven dependencies contributed by bzlmod modules."""
+
+load("@bazel_skylib//lib:new_sets.bzl", "sets")
+load("//private/lib:coordinates.bzl", "to_key")
+load("//private/rules:maven_version.bzl", "compare_maven_versions")
+
+DEFAULT_NAME = "maven"
+
+def _diagnostic(text, gate):
+ return struct(text = text, gate = gate)
+
+def should_print_diagnostic(diagnostic, repin, verbose):
+ """Whether a layering diagnostic is enabled for the current environment."""
+ return (
+ diagnostic.gate == "always" or
+ (diagnostic.gate == "repin" and repin) or
+ (diagnostic.gate == "verbose" and verbose) or
+ (diagnostic.gate == "repin_verbose" and repin and verbose)
+ )
+
+def contributing_modules_warning(repo_name, known_contributing_modules, non_root_bazel_dep_to_items):
+ """Returns the warning for contributions from modules not acknowledged by the root."""
+ contributing_module_names = non_root_bazel_dep_to_items.keys()
+ new_contributing_modules = sets.difference(sets.make(contributing_module_names), known_contributing_modules)
+ if sets.length(new_contributing_modules) > 0:
+ return (
+ "The maven repository '%s' has contributions from multiple bzlmod modules, and will be resolved together: %s." % (
+ repo_name,
+ sorted(contributing_module_names),
+ ) + "\nSee https://github.com/bazel-contrib/rules_jvm_external/blob/master/docs/bzlmod.md#module-dependency-layering" +
+ " for more information. \n" +
+ " To suppress this warning review the contributions from the other modules and add the following attribute" +
+ " in the root MODULE.bazel file: \n" +
+ "maven.install(\n" +
+ (" name = \"{0}\"\n".format(repo_name) if repo_name != DEFAULT_NAME else "") +
+ " known_contributing_modules = {0},\n".format(sorted(contributing_module_names)) +
+ " ...\n" +
+ ")"
+ )
+ return None
+
+def _candidate_takes_precedence(current_artifact, candidate_artifact):
+ if current_artifact == None:
+ return True
+
+ current_forced = getattr(current_artifact, "force_version", False)
+ candidate_forced = getattr(candidate_artifact, "force_version", False)
+ if current_forced != candidate_forced:
+ return candidate_forced
+
+ return compare_maven_versions(current_artifact.version, candidate_artifact.version) == -1
+
+def _fail_if_conflicting_forces(module_name, artifacts):
+ coordinate_to_forced_artifact = {}
+ for artifact in artifacts:
+ if not getattr(artifact, "force_version", False):
+ continue
+
+ artifact_key = to_key(artifact)
+ previous_artifact = coordinate_to_forced_artifact.get(artifact_key)
+ if previous_artifact != None and compare_maven_versions(previous_artifact.version, artifact.version) != 0:
+ fail(
+ "Module '%s' forces dependency '%s' at different versions: %s and %s." % (
+ module_name,
+ artifact_key,
+ previous_artifact.version,
+ artifact.version,
+ ),
+ )
+ coordinate_to_forced_artifact[artifact_key] = artifact
+
+def deduplicate_non_root_artifacts(
+ bazel_dep_to_non_root_artifacts,
+ return_only_artifacts = False,
+ root_forced_artifact_keys = None):
+ root_forced_artifact_keys = root_forced_artifact_keys or {}
+ coordinate_to_artifact = {}
+ coordinate_to_forced_artifact = {}
+ for bazel_dep_name in bazel_dep_to_non_root_artifacts:
+ module_coordinate_to_artifact = {}
+ module_artifacts = bazel_dep_to_non_root_artifacts.get(bazel_dep_name, [])
+ _fail_if_conflicting_forces(bazel_dep_name, module_artifacts)
+ for artifact in module_artifacts:
+ if not getattr(artifact, "testonly", False):
+ artifact_key = to_key(artifact)
+ if _candidate_takes_precedence(module_coordinate_to_artifact.get(artifact_key), artifact):
+ module_coordinate_to_artifact[artifact_key] = artifact
+
+ for artifact_key, artifact in module_coordinate_to_artifact.items():
+ if getattr(artifact, "force_version", False) and artifact_key not in root_forced_artifact_keys:
+ previous_force = coordinate_to_forced_artifact.get(artifact_key)
+ if previous_force:
+ previous_bazel_dep_name, previous_artifact = previous_force
+ if compare_maven_versions(previous_artifact.version, artifact.version) != 0:
+ fail(
+ "Conflicting forced versions for dependency '%s': %s wants %s, %s wants %s. " % (
+ artifact_key,
+ previous_bazel_dep_name,
+ previous_artifact.version,
+ bazel_dep_name,
+ artifact.version,
+ ) +
+ "Add an `artifact` tag to the root module at the version you want and set `force_version = True`.",
+ )
+ else:
+ coordinate_to_forced_artifact[artifact_key] = (bazel_dep_name, artifact)
+
+ current = coordinate_to_artifact.get(artifact_key)
+ current_artifact = current[1] if current else None
+ if _candidate_takes_precedence(current_artifact, artifact):
+ coordinate_to_artifact[artifact_key] = (bazel_dep_name, artifact)
+
+ if return_only_artifacts:
+ return [v[1] for v in coordinate_to_artifact.values()]
+ else:
+ return coordinate_to_artifact
+
+# Each bzlmod module may contribute jars to different rules_jvm_external maven repo namespaces.
+# We emit a warning to the user if a module overrides an artifact version in the root maven repo.
+#
+# This can be typical for the default @maven namespace, if a bzlmod dependency
+# wishes to contribute to the users' jars.
+def merge_with_root_priority(
+ name,
+ root_artifacts,
+ bazel_dep_to_non_root_artifacts,
+ duplicate_version_warning = "warn"):
+ """Deduplicate artifacts, giving priority to root module artifacts with force_version set."""
+ root_forced_artifact_keys = {
+ to_key(artifact): True
+ for artifact in root_artifacts
+ if getattr(artifact, "force_version", False)
+ }
+ non_root_coordinate_to_artifact = deduplicate_non_root_artifacts(
+ bazel_dep_to_non_root_artifacts,
+ root_forced_artifact_keys = root_forced_artifact_keys,
+ )
+
+ duplicate_artifact_warning = ""
+ filtered_root_artifacts = []
+ filtered_non_root_artifacts = []
+ for root_artifact in root_artifacts:
+ keep_root_artifact = True
+ artifact_key = to_key(root_artifact)
+ if artifact_key in non_root_coordinate_to_artifact:
+ bazel_dep_name, non_root_artifact = non_root_coordinate_to_artifact.pop(artifact_key)
+ if not getattr(root_artifact, "force_version", False):
+ comparison = compare_maven_versions(root_artifact.version, non_root_artifact.version)
+ non_root_forced = getattr(non_root_artifact, "force_version", False)
+ if non_root_forced or comparison == -1:
+ keep_root_artifact = False
+ filtered_non_root_artifacts.append(non_root_artifact)
+
+ if comparison != 0 and (non_root_forced or comparison == -1):
+ message = (
+ "For dependency '%s:%s' the root @%s repo wants version %s, " % (root_artifact.group, root_artifact.artifact, name, root_artifact.version) +
+ "but got %s from the %s bazel dep. " % (non_root_artifact.version, bazel_dep_name) +
+ "Please update the version in your MODULE.bazel or set `force_version = True`."
+ )
+ if duplicate_version_warning == "error":
+ fail(message)
+ elif duplicate_version_warning == "warn":
+ duplicate_artifact_warning = duplicate_artifact_warning + "\nWARNING: " + message
+ if keep_root_artifact:
+ filtered_root_artifacts.append(root_artifact)
+
+ # Add any remaining non root artifacts that weren't found in the root artifact list
+ addtional_artifact_message = ""
+ for bazel_dep_name, non_root_artifact in non_root_coordinate_to_artifact.values():
+ addtional_artifact_message = addtional_artifact_message + (
+ "\nINFO: The @%s repo is getting the additional artifact %s:%s:%s from the %s bazel dep." % (name, non_root_artifact.group, non_root_artifact.artifact, non_root_artifact.version, bazel_dep_name)
+ )
+ filtered_non_root_artifacts.append(non_root_artifact)
+
+ diagnostics = []
+ if duplicate_artifact_warning != "":
+ diagnostics.append(_diagnostic(duplicate_artifact_warning, "always"))
+ if addtional_artifact_message != "":
+ diagnostics.append(_diagnostic(addtional_artifact_message, "repin_verbose"))
+
+ return struct(
+ artifacts = filtered_root_artifacts + filtered_non_root_artifacts,
+ diagnostics = diagnostics,
+ )
+
+def filter_known_contributing_modules(name, known_contributing_modules, bazel_dep_to_items, item_kind):
+ """Filters contributions to modules acknowledged by the root."""
+ all_non_root_modules = bazel_dep_to_items.keys()
+ filtered = {
+ module: bazel_dep_to_items[module]
+ for module in sets.to_list(known_contributing_modules)
+ if module in bazel_dep_to_items
+ }
+ diagnostics = []
+ for module in all_non_root_modules:
+ if module not in filtered:
+ diagnostics.append(_diagnostic(
+ "\nINFO: The @%s repo is not using %s from %s because it is not in the known_contributing_modules" % (name, item_kind, module),
+ "verbose",
+ ))
+ return struct(filtered = filtered, diagnostics = diagnostics)
+
+def layer_maven_namespace(
+ name,
+ root_present,
+ root_artifacts,
+ root_boms,
+ resolver,
+ version_conflict_policy,
+ duplicate_version_warning,
+ known_contributing_modules,
+ bazel_dep_to_non_root_artifacts,
+ bazel_dep_to_non_root_boms):
+ """Layers the dependency declarations for one Maven repository namespace."""
+ _fail_if_conflicting_forces("root", root_artifacts)
+ _fail_if_conflicting_forces("root", root_boms)
+ root_artifacts = apply_root_version_conflict_policy(
+ root_artifacts,
+ resolver,
+ version_conflict_policy,
+ )
+ diagnostics = []
+
+ if not root_present:
+ return struct(
+ artifacts = deduplicate_non_root_artifacts(bazel_dep_to_non_root_artifacts, True),
+ boms = deduplicate_non_root_artifacts(bazel_dep_to_non_root_boms, True),
+ diagnostics = diagnostics,
+ )
+
+ if sets.length(known_contributing_modules) == 0:
+ warning = contributing_modules_warning(
+ name,
+ known_contributing_modules,
+ bazel_dep_to_non_root_artifacts | bazel_dep_to_non_root_boms,
+ )
+ if warning:
+ diagnostics.append(_diagnostic(warning, "always"))
+ else:
+ filtered_artifacts = filter_known_contributing_modules(
+ name,
+ known_contributing_modules,
+ bazel_dep_to_non_root_artifacts,
+ "deps",
+ )
+ bazel_dep_to_non_root_artifacts = filtered_artifacts.filtered
+ diagnostics.extend(filtered_artifacts.diagnostics)
+
+ filtered_boms = filter_known_contributing_modules(
+ name,
+ known_contributing_modules,
+ bazel_dep_to_non_root_boms,
+ "boms",
+ )
+ bazel_dep_to_non_root_boms = filtered_boms.filtered
+ diagnostics.extend(filtered_boms.diagnostics)
+
+ artifacts = merge_with_root_priority(
+ name,
+ root_artifacts,
+ bazel_dep_to_non_root_artifacts,
+ duplicate_version_warning,
+ )
+ diagnostics.extend(artifacts.diagnostics)
+
+ boms = merge_with_root_priority(
+ name,
+ root_boms,
+ bazel_dep_to_non_root_boms,
+ duplicate_version_warning,
+ )
+ diagnostics.extend(boms.diagnostics)
+
+ return struct(
+ artifacts = artifacts.artifacts,
+ boms = boms.artifacts,
+ diagnostics = diagnostics,
+ )
+
+def remove_empty_fields(s):
+ """Used for reducing an artifact struct down to only those fields that have values"""
+ return {
+ k: getattr(s, k)
+ for k in dir(s)
+ if k != "to_json" and k != "to_proto" and getattr(s, k, None)
+ } | {"version": getattr(s, "version", "")}
+
+def _defines_gradle_module_version(candidate, current):
+ """Whether candidate should force the Gradle module version instead of current."""
+ candidate_classified = bool(getattr(candidate, "classifier", None))
+ current_classified = bool(getattr(current, "classifier", None))
+ if candidate_classified != current_classified:
+ # An unclassified root defines the module version.
+ return current_classified
+ return compare_maven_versions(candidate.version, current.version) == 1
+
+def _select_gradle_forced_versions(artifacts):
+ """Selects the single version to force for each Gradle group:artifact module.
+
+ Gradle resolves one version per module regardless of classifier, so forcing
+ two versions of the same module (for example a main jar and its
+ test-fixtures jar) makes resolution unsatisfiable.
+ """
+ winners = {}
+ for artifact in artifacts:
+ if not getattr(artifact, "version", None):
+ continue
+ key = "%s:%s" % (artifact.group, artifact.artifact)
+ current = winners.get(key)
+ if current == None or _defines_gradle_module_version(artifact, current):
+ winners[key] = artifact
+ return {key: winner.version for key, winner in winners.items()}
+
+def _forces_gradle_module_version(artifact, forced_versions):
+ version = getattr(artifact, "version", None)
+ if not version:
+ return False
+ return version == forced_versions.get("%s:%s" % (artifact.group, artifact.artifact))
+
+def apply_root_version_conflict_policy(artifacts, resolver, version_conflict_policy):
+ """Applies the install-level conflict policy to root module artifacts."""
+ if resolver not in ["gradle", "maven"] or version_conflict_policy != "pinned":
+ return artifacts
+
+ if resolver == "gradle":
+ forced_versions = _select_gradle_forced_versions(artifacts)
+ return [
+ struct(**(remove_empty_fields(artifact) | {"force_version": True})) if _forces_gradle_module_version(artifact, forced_versions) else artifact
+ for artifact in artifacts
+ ]
+
+ return [
+ struct(**(remove_empty_fields(artifact) | {"force_version": True})) if getattr(artifact, "version", None) else artifact
+ for artifact in artifacts
+ ]
diff --git a/private/rules/coursier.bzl b/private/rules/coursier.bzl
index 8f20e49..e5a9cb8 100644
--- a/private/rules/coursier.bzl
+++ b/private/rules/coursier.bzl
@@ -578,10 +578,10 @@
repositories = [json.decode(repository) for repository in repository_ctx.attr.repositories]
artifacts = [json.decode(artifact) for artifact in repository_ctx.attr.artifacts]
- _check_artifacts_are_unique(artifacts, repository_ctx.attr.duplicate_version_warning)
+ check_artifacts_are_unique(artifacts, repository_ctx.attr.duplicate_version_warning)
boms = [json.decode(bom) for bom in repository_ctx.attr.boms]
- _check_artifacts_are_unique(boms, repository_ctx.attr.duplicate_version_warning)
+ check_artifacts_are_unique(boms, repository_ctx.attr.duplicate_version_warning)
# Read Coursier state from maven_install.json.
repository_ctx.symlink(
@@ -937,7 +937,7 @@
break
return primary_artifact_path
-def _check_artifacts_are_unique(artifacts, duplicate_version_warning):
+def check_artifacts_are_unique(artifacts, duplicate_version_warning):
if duplicate_version_warning == "none":
return
seen_artifacts = {}
@@ -962,7 +962,9 @@
if duplicate_version_warning == "error":
fail("\n".join(msg_parts))
else:
- print("\n".join(msg_parts))
+ message = "\n".join(msg_parts)
+ print(message)
+ return message
def get_coursier_sha256(environ, default_sha256):
return environ.get("COURSIER_SHA256", default_sha256)
@@ -1284,10 +1286,10 @@
for artifact in repository_ctx.attr.artifacts:
artifacts.append(json.decode(artifact))
- _check_artifacts_are_unique(artifacts, repository_ctx.attr.duplicate_version_warning)
+ check_artifacts_are_unique(artifacts, repository_ctx.attr.duplicate_version_warning)
boms = [json.decode(bom) for bom in repository_ctx.attr.boms]
- _check_artifacts_are_unique(boms, repository_ctx.attr.duplicate_version_warning)
+ check_artifacts_are_unique(boms, repository_ctx.attr.duplicate_version_warning)
excluded_artifacts = []
for artifact in repository_ctx.attr.excluded_artifacts:
diff --git a/tests/bazel_run_tests.sh b/tests/bazel_run_tests.sh
index 655edef..1166aa2 100755
--- a/tests/bazel_run_tests.sh
+++ b/tests/bazel_run_tests.sh
@@ -32,6 +32,17 @@
expect_log "Successfully pinned resolved artifacts"
}
+function test_duplicate_version_error() {
+ if bazel run @duplicate_version_error//:pin >> "$TEST_LOG" 2>&1; then
+ printf "Expected duplicate artifact versions to fail the build\n" >> "$TEST_LOG"
+ return 1
+ fi
+
+ expect_log "Found duplicate artifact versions"
+ expect_log "com.fasterxml.jackson.core:jackson-annotations has multiple versions"
+ expect_not_log "Successfully pinned resolved artifacts"
+}
+
function test_duplicate_version_warning_same_version() {
bazel run @duplicate_version_warning_same_version//:pin >> "$TEST_LOG" 2>&1
rm -f *duplicate_version_warning_same_version_install.json
@@ -450,6 +461,7 @@
"test_coursier_resolution_with_boms"
"test_maven_resolution"
"test_dependency_aggregation"
+ "test_duplicate_version_error"
"test_duplicate_version_warning"
"test_duplicate_version_warning_same_version"
"test_outdated"
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 c7449bd..8308563 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
@@ -66,6 +66,19 @@
}
@Test
+ public void duplicateDirectDependenciesUseHighestVersion() {
+ Coordinates lower = new Coordinates("com.example:library:1.0");
+ Coordinates higher = new Coordinates("com.example:library:2.0");
+ Path repo = MavenRepo.create().add(lower).add(higher).getPath();
+
+ Graph<Coordinates> resolved =
+ resolver.resolve(prepareRequestFor(repo.toUri(), lower, higher)).getResolution();
+
+ assertFalse(resolved.nodes().contains(lower));
+ assertTrue(resolved.nodes().contains(higher));
+ }
+
+ @Test
public void resolvesSimpleJvmVariant() throws IOException, XMLStreamException {
// This test validates gradle can resolve a artifact using only gradle module metadata
// In this case, there's a root artifact com.example.sample which points to
diff --git a/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/MavenResolverTest.java b/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/MavenResolverTest.java
index 08d7862..663cbdf 100644
--- a/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/MavenResolverTest.java
+++ b/tests/com/github/bazelbuild/rules_jvm_external/resolver/maven/MavenResolverTest.java
@@ -14,6 +14,8 @@
package com.github.bazelbuild.rules_jvm_external.resolver.maven;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.junit.Assert.fail;
import com.github.bazelbuild.rules_jvm_external.Coordinates;
@@ -35,6 +37,18 @@
}
@Test
+ public void duplicateDirectDependenciesUseFirstVersion() {
+ Coordinates first = new Coordinates("com.example:library:1.0");
+ Coordinates second = new Coordinates("com.example:library:2.0");
+ Path repo = MavenRepo.create().add(first).add(second).getPath();
+
+ var resolved = resolver.resolve(prepareRequestFor(repo.toUri(), first, second)).getResolution();
+
+ assertTrue(resolved.nodes().contains(first));
+ assertFalse(resolved.nodes().contains(second));
+ }
+
+ @Test
public void shouldSuccessfullyResolveNettyStaticClasses() {
Coordinates main = new Coordinates("com.example:root:1.0.0");
Coordinates x86Dep = new Coordinates("com.example", "root", null, "linux-x86_64", "1.0.0");
diff --git a/tests/integration/BUILD b/tests/integration/BUILD
index 21848b1..71654c7 100644
--- a/tests/integration/BUILD
+++ b/tests/integration/BUILD
@@ -154,6 +154,31 @@
tags = [] if is_bzlmod_enabled() else ["manual"],
)
+genquery(
+ name = "coursier-layering-deps",
+ expression = "deps(@coursier_layering//:com_google_code_findbugs_jsr305)",
+ opts = [
+ "--nohost_deps",
+ "--noimplicit_deps",
+ ],
+ scope = ["@coursier_layering//:com_google_code_findbugs_jsr305"],
+)
+
+genrule(
+ name = "coursier-layering-deps-sorted",
+ testonly = 1,
+ srcs = [":coursier-layering-deps"],
+ outs = ["coursier-layering-deps-sorted.txt"],
+ cmd = "cat $< | grep coursier_layering | sed -e 's|^@@|@|g; s|\\r||g' | sed -e 's|^@[^/]*[+~]|@|g; s|\\r||g' | sort > $@",
+)
+
+diff_test(
+ name = "coursier-layering-deps-test",
+ file1 = "coursier-layering-deps.golden.unix",
+ file2 = ":coursier-layering-deps-sorted.txt",
+ tags = [] if is_bzlmod_enabled() else ["manual"],
+)
+
# This target will fail to build if we're not handling merging of maven.install
# tags properly, and if we don't handle multiple lock files properly.
java_library(
diff --git a/tests/integration/coursier-layering-deps.golden.unix b/tests/integration/coursier-layering-deps.golden.unix
new file mode 100644
index 0000000..115469f
--- /dev/null
+++ b/tests/integration/coursier-layering-deps.golden.unix
@@ -0,0 +1,2 @@
+@coursier_layering//:com_google_code_findbugs_jsr305
+@coursier_layering//:v1/https/repo1.maven.org/maven2/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar
diff --git a/tests/integration/coursier_higher_layer/MODULE.bazel b/tests/integration/coursier_higher_layer/MODULE.bazel
new file mode 100644
index 0000000..16f3d13
--- /dev/null
+++ b/tests/integration/coursier_higher_layer/MODULE.bazel
@@ -0,0 +1,16 @@
+module(name = "coursier_higher_layer")
+
+bazel_dep(name = "rules_jvm_external", version = "0.0")
+local_path_override(
+ module_name = "rules_jvm_external",
+ path = "../../../",
+)
+
+maven = use_extension("@rules_jvm_external//:extensions.bzl", "maven")
+maven.install(
+ name = "coursier_layering",
+ artifacts = [
+ "com.google.code.findbugs:jsr305:3.0.2",
+ ],
+)
+use_repo(maven, "coursier_layering")
diff --git a/tests/unit/BUILD b/tests/unit/BUILD
index f50a066..e62bfea 100644
--- a/tests/unit/BUILD
+++ b/tests/unit/BUILD
@@ -6,6 +6,7 @@
load(":coursier_utilities_test.bzl", "coursier_utilities_test_suite")
load(":dependency_tree_parser_test.bzl", "dependency_tree_parser_test_suite")
load(":java_utilities_test.bzl", "java_utilities_test_suite")
+load(":layering_test.bzl", "layering_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")
@@ -31,6 +32,8 @@
java_utilities_test_suite()
+layering_test_suite()
+
maven_version_test_suite()
proxy_test_suite()
diff --git a/tests/unit/coursier_test.bzl b/tests/unit/coursier_test.bzl
index a542aaa..62a09c9 100644
--- a/tests/unit/coursier_test.bzl
+++ b/tests/unit/coursier_test.bzl
@@ -2,6 +2,7 @@
load("//private/lib:urls.bzl", "extract_netrc_from_auth_url", "remove_auth_from_url", "split_url")
load(
"//private/rules:coursier.bzl",
+ "check_artifacts_are_unique",
"compute_dependency_inputs_signature",
"get_coursier_cache_or_default",
"get_coursier_environment",
@@ -20,6 +21,35 @@
ALL_TESTS.append(test)
return test
+def _packaging_does_not_distinguish_duplicate_versions_test_impl(ctx):
+ env = unittest.begin(ctx)
+ artifacts = [
+ {
+ "group": "com.example",
+ "artifact": "library",
+ "version": "1.0",
+ "packaging": "jar",
+ "classifier": None,
+ },
+ {
+ "group": "com.example",
+ "artifact": "library",
+ "version": "2.0",
+ "packaging": "aar",
+ "classifier": None,
+ },
+ ]
+
+ asserts.equals(
+ env,
+ "Found duplicate artifact versions\n com.example:library has multiple versions 1.0, 2.0\nPlease remove duplicate artifacts from the artifact list so you do not get unexpected artifact versions",
+ check_artifacts_are_unique(artifacts, "warn"),
+ )
+
+ return unittest.end(env)
+
+packaging_does_not_distinguish_duplicate_versions_test = add_test(_packaging_does_not_distinguish_duplicate_versions_test_impl)
+
def _infer_doc_example_test_impl(ctx):
env = unittest.begin(ctx)
asserts.equals(
diff --git a/tests/unit/layering_test.bzl b/tests/unit/layering_test.bzl
new file mode 100644
index 0000000..1abb461
--- /dev/null
+++ b/tests/unit/layering_test.bzl
@@ -0,0 +1,878 @@
+"""Tests for dependency layering across bzlmod modules."""
+
+load("@bazel_skylib//lib:new_sets.bzl", "sets")
+load("@bazel_skylib//lib:partial.bzl", "partial")
+load("@bazel_skylib//lib:unittest.bzl", "analysistest", "asserts", "unittest")
+load(
+ "//private/lib:layering.bzl",
+ "deduplicate_non_root_artifacts",
+ "filter_known_contributing_modules",
+ "layer_maven_namespace",
+ "merge_with_root_priority",
+ "should_print_diagnostic",
+)
+
+def _artifact(
+ version,
+ force_version = False,
+ testonly = False,
+ neverlink = False,
+ exclusions = None,
+ packaging = None,
+ classifier = None,
+ group = "com.example",
+ artifact = "library"):
+ return struct(
+ group = group,
+ artifact = artifact,
+ version = version,
+ packaging = packaging,
+ classifier = classifier,
+ force_version = force_version,
+ testonly = testonly,
+ neverlink = neverlink,
+ exclusions = exclusions or [],
+ )
+
+def _merge_result(
+ root_artifacts,
+ bazel_dep_to_non_root_artifacts,
+ duplicate_version_warning = "warn"):
+ return merge_with_root_priority(
+ "maven",
+ root_artifacts,
+ bazel_dep_to_non_root_artifacts,
+ duplicate_version_warning,
+ )
+
+def _merge(root_artifacts, bazel_dep_to_non_root_artifacts):
+ return _merge_result(root_artifacts, bazel_dep_to_non_root_artifacts).artifacts
+
+def _layer(
+ name = "maven",
+ root_present = True,
+ root_artifacts = None,
+ root_boms = None,
+ resolver = "coursier",
+ version_conflict_policy = "default",
+ duplicate_version_warning = "warn",
+ known_contributing_modules = None,
+ non_root_artifacts = None,
+ non_root_boms = None):
+ return layer_maven_namespace(
+ name = name,
+ root_present = root_present,
+ root_artifacts = root_artifacts or [],
+ root_boms = root_boms or [],
+ resolver = resolver,
+ version_conflict_policy = version_conflict_policy,
+ duplicate_version_warning = duplicate_version_warning,
+ known_contributing_modules = known_contributing_modules or sets.make(),
+ bazel_dep_to_non_root_artifacts = non_root_artifacts or {},
+ bazel_dep_to_non_root_boms = non_root_boms or {},
+ )
+
+def _root_only_resolves_root_version_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("1.0")
+
+ asserts.equals(env, [root], _merge([root], {}))
+
+ return unittest.end(env)
+
+root_only_resolves_root_version_test = unittest.make(_root_only_resolves_root_version_impl)
+
+def _nonroot_lower_version_is_dropped_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("2.0")
+
+ result = _merge_result([root], {"dep": [_artifact("1.0")]})
+
+ asserts.equals(env, [root], result.artifacts)
+ asserts.equals(env, [], result.diagnostics)
+
+ return unittest.end(env)
+
+nonroot_lower_version_is_dropped_test = unittest.make(_nonroot_lower_version_is_dropped_impl)
+
+def _equal_unforced_nonroot_keeps_root_metadata_without_warning_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact(
+ "1.0",
+ neverlink = True,
+ exclusions = [struct(group = "excluded", artifact = "root")],
+ )
+
+ result = _merge_result(
+ [root],
+ {"dep": [_artifact(
+ "1.0",
+ exclusions = [struct(group = "excluded", artifact = "non-root")],
+ )]},
+ )
+
+ asserts.equals(env, [root], result.artifacts)
+ asserts.equals(env, [], result.diagnostics)
+
+ return unittest.end(env)
+
+equal_unforced_nonroot_keeps_root_metadata_without_warning_test = unittest.make(_equal_unforced_nonroot_keeps_root_metadata_without_warning_impl)
+
+def _nonroot_higher_version_wins_and_warns_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("1.0")
+ non_root = _artifact("2.0")
+
+ result = _merge_result([root], {"dep": [non_root]})
+
+ asserts.equals(env, [non_root], result.artifacts)
+ asserts.equals(
+ env,
+ [struct(
+ text = "\nWARNING: For dependency 'com.example:library' the root @maven repo wants version 1.0, but got 2.0 from the dep bazel dep. Please update the version in your MODULE.bazel or set `force_version = True`.",
+ gate = "always",
+ )],
+ result.diagnostics,
+ )
+
+ return unittest.end(env)
+
+nonroot_higher_version_wins_and_warns_test = unittest.make(_nonroot_higher_version_wins_and_warns_impl)
+
+def _nonroot_override_warning_can_be_disabled_impl(ctx):
+ env = unittest.begin(ctx)
+ non_root = _artifact("2.0")
+
+ result = _layer(
+ root_artifacts = [_artifact("1.0")],
+ duplicate_version_warning = "none",
+ known_contributing_modules = sets.make(["dep"]),
+ non_root_artifacts = {"dep": [non_root]},
+ )
+
+ asserts.equals(env, [non_root], result.artifacts)
+ asserts.equals(env, [], result.diagnostics)
+
+ return unittest.end(env)
+
+nonroot_override_warning_can_be_disabled_test = unittest.make(_nonroot_override_warning_can_be_disabled_impl)
+
+def _higher_nonroot_force_beats_unforced_root_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("1.0")
+ non_root = _artifact("2.0", force_version = True)
+
+ result = _merge_result([root], {"dep": [non_root]})
+
+ asserts.equals(env, [non_root], result.artifacts)
+ asserts.true(env, result.artifacts[0].force_version)
+ asserts.equals(env, "always", result.diagnostics[0].gate)
+
+ return unittest.end(env)
+
+higher_nonroot_force_beats_unforced_root_test = unittest.make(_higher_nonroot_force_beats_unforced_root_impl)
+
+def _lower_nonroot_force_beats_unforced_root_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("2.0")
+ non_root = _artifact("1.0", force_version = True)
+
+ result = _merge_result([root], {"dep": [non_root]})
+
+ asserts.equals(env, [non_root], result.artifacts)
+ asserts.equals(env, "always", result.diagnostics[0].gate)
+
+ return unittest.end(env)
+
+lower_nonroot_force_beats_unforced_root_test = unittest.make(_lower_nonroot_force_beats_unforced_root_impl)
+
+def _equal_nonroot_force_retains_transitive_pin_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("1.0")
+ non_root = _artifact("1.0", force_version = True)
+
+ result = _merge_result([root], {"dep": [non_root]})
+
+ asserts.equals(env, [non_root], result.artifacts)
+ asserts.true(env, result.artifacts[0].force_version)
+ asserts.equals(env, [], result.diagnostics)
+
+ return unittest.end(env)
+
+equal_nonroot_force_retains_transitive_pin_test = unittest.make(_equal_nonroot_force_retains_transitive_pin_impl)
+
+def _both_forced_root_wins_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("1.0", force_version = True)
+
+ asserts.equals(
+ env,
+ [root],
+ _merge([root], {"dep": [_artifact("2.0", force_version = True)]}),
+ )
+
+ return unittest.end(env)
+
+both_forced_root_wins_test = unittest.make(_both_forced_root_wins_impl)
+
+def _root_force_beats_conflicting_nonroot_forces_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("3.0", force_version = True)
+
+ asserts.equals(
+ env,
+ [root],
+ _merge([root], _conflicting_nonroot_forces()),
+ )
+
+ return unittest.end(env)
+
+root_force_beats_conflicting_nonroot_forces_test = unittest.make(_root_force_beats_conflicting_nonroot_forces_impl)
+
+def _single_nonroot_survives_impl(ctx):
+ env = unittest.begin(ctx)
+ non_root = _artifact("1.0")
+
+ asserts.equals(
+ env,
+ [non_root],
+ deduplicate_non_root_artifacts({"dep": [non_root]}, return_only_artifacts = True),
+ )
+
+ return unittest.end(env)
+
+single_nonroot_survives_test = unittest.make(_single_nonroot_survives_impl)
+
+def _testonly_nonroot_is_filtered_impl(ctx):
+ env = unittest.begin(ctx)
+
+ asserts.equals(
+ env,
+ [],
+ deduplicate_non_root_artifacts(
+ {"dep": [_artifact("1.0", testonly = True)]},
+ return_only_artifacts = True,
+ ),
+ )
+
+ return unittest.end(env)
+
+testonly_nonroot_is_filtered_test = unittest.make(_testonly_nonroot_is_filtered_impl)
+
+def _multiple_nonroot_highest_wins_impl(ctx):
+ env = unittest.begin(ctx)
+ higher = _artifact("2.0")
+
+ asserts.equals(
+ env,
+ [higher],
+ deduplicate_non_root_artifacts(
+ {"first": [_artifact("1.0")], "second": [higher]},
+ return_only_artifacts = True,
+ ),
+ )
+
+ return unittest.end(env)
+
+multiple_nonroot_highest_wins_test = unittest.make(_multiple_nonroot_highest_wins_impl)
+
+def _equal_version_tie_keeps_first_module_metadata_impl(ctx):
+ env = unittest.begin(ctx)
+ first = _artifact(
+ "1.0",
+ force_version = True,
+ neverlink = True,
+ exclusions = [struct(group = "excluded", artifact = "first")],
+ )
+ second = _artifact(
+ "1.0",
+ exclusions = [struct(group = "excluded", artifact = "second")],
+ )
+
+ asserts.equals(
+ env,
+ [first],
+ deduplicate_non_root_artifacts(
+ {"first": [first], "second": [second]},
+ return_only_artifacts = True,
+ ),
+ )
+
+ return unittest.end(env)
+
+equal_version_tie_keeps_first_module_metadata_test = unittest.make(_equal_version_tie_keeps_first_module_metadata_impl)
+
+def _root_force_beats_higher_unforced_nonroot_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("1.0", force_version = True)
+
+ asserts.equals(env, [root], _merge([root], {"dep": [_artifact("2.0")]}))
+
+ return unittest.end(env)
+
+root_force_beats_higher_unforced_nonroot_test = unittest.make(_root_force_beats_higher_unforced_nonroot_impl)
+
+def _conflicting_nonroot_forces():
+ return {
+ "first": [_artifact("1.0", force_version = True)],
+ "second": [_artifact("2.0", force_version = True)],
+ }
+
+def _conflicting_nonroot_forces_target_impl(_ctx):
+ deduplicate_non_root_artifacts(_conflicting_nonroot_forces(), return_only_artifacts = True)
+ return []
+
+conflicting_nonroot_forces_target = rule(implementation = _conflicting_nonroot_forces_target_impl)
+
+def _same_module_forces_target_impl(ctx):
+ artifacts = [
+ _artifact("1.0", force_version = True),
+ _artifact(ctx.attr.second_version, force_version = True),
+ ]
+ if ctx.attr.root:
+ _layer(root_artifacts = artifacts)
+ else:
+ deduplicate_non_root_artifacts({"dep": artifacts}, return_only_artifacts = True)
+ return []
+
+same_module_forces_target = rule(
+ implementation = _same_module_forces_target_impl,
+ attrs = {
+ "root": attr.bool(),
+ "second_version": attr.string(mandatory = True),
+ },
+)
+
+def _nonroot_override_error_target_impl(_ctx):
+ _layer(
+ root_artifacts = [_artifact("1.0")],
+ duplicate_version_warning = "error",
+ known_contributing_modules = sets.make(["dep"]),
+ non_root_artifacts = {"dep": [_artifact("2.0")]},
+ )
+ return []
+
+nonroot_override_error_target = rule(implementation = _nonroot_override_error_target_impl)
+
+def _unforced_root_with_conflicting_nonroot_forces_target_impl(_ctx):
+ merge_with_root_priority(
+ "maven",
+ [_artifact("3.0")],
+ _conflicting_nonroot_forces(),
+ )
+ return []
+
+unforced_root_with_conflicting_nonroot_forces_target = rule(
+ implementation = _unforced_root_with_conflicting_nonroot_forces_target_impl,
+)
+
+def _conflicting_nonroot_forces_fail_impl(ctx):
+ env = analysistest.begin(ctx)
+
+ asserts.expect_failure(
+ env,
+ "Conflicting forced versions for dependency 'com.example:library': first wants 1.0, second wants 2.0. Add an `artifact` tag to the root module at the version you want and set `force_version = True`.",
+ )
+
+ return analysistest.end(env)
+
+conflicting_nonroot_forces_fail_test = analysistest.make(
+ _conflicting_nonroot_forces_fail_impl,
+ expect_failure = True,
+)
+
+unforced_root_with_conflicting_nonroot_forces_fail_test = analysistest.make(
+ _conflicting_nonroot_forces_fail_impl,
+ expect_failure = True,
+)
+
+def _nonroot_override_error_fails_impl(ctx):
+ env = analysistest.begin(ctx)
+
+ asserts.expect_failure(
+ env,
+ "For dependency 'com.example:library' the root @maven repo wants version 1.0, but got 2.0 from the dep bazel dep.",
+ )
+
+ return analysistest.end(env)
+
+nonroot_override_error_fails_test = analysistest.make(
+ _nonroot_override_error_fails_impl,
+ expect_failure = True,
+)
+
+def _same_module_forces_fail_impl(ctx):
+ env = analysistest.begin(ctx)
+
+ asserts.expect_failure(env, ctx.attr.expected_message)
+
+ return analysistest.end(env)
+
+different_forced_versions_from_same_nonroot_module_fail_test = analysistest.make(
+ _same_module_forces_fail_impl,
+ attrs = {"expected_message": attr.string(mandatory = True)},
+ expect_failure = True,
+)
+
+different_forced_versions_from_root_module_fail_test = analysistest.make(
+ _same_module_forces_fail_impl,
+ attrs = {"expected_message": attr.string(mandatory = True)},
+ expect_failure = True,
+)
+
+def _matching_nonroot_forces_keep_first_module_impl(ctx):
+ env = unittest.begin(ctx)
+ first = _artifact("1.0", force_version = True, neverlink = True)
+
+ asserts.equals(
+ env,
+ [first],
+ deduplicate_non_root_artifacts(
+ {
+ "first": [first],
+ "second": [_artifact("1.0", force_version = True)],
+ },
+ return_only_artifacts = True,
+ ),
+ )
+
+ return unittest.end(env)
+
+matching_nonroot_forces_keep_first_module_test = unittest.make(_matching_nonroot_forces_keep_first_module_impl)
+
+def _matching_forced_duplicates_from_same_nonroot_module_keep_first_impl(ctx):
+ env = unittest.begin(ctx)
+ first = _artifact("1.0", force_version = True, neverlink = True)
+
+ asserts.equals(
+ env,
+ [first],
+ deduplicate_non_root_artifacts(
+ {"dep": [first, _artifact("1.0", force_version = True)]},
+ return_only_artifacts = True,
+ ),
+ )
+
+ return unittest.end(env)
+
+matching_forced_duplicates_from_same_nonroot_module_keep_first_test = unittest.make(_matching_forced_duplicates_from_same_nonroot_module_keep_first_impl)
+
+def _matching_forced_duplicates_from_root_module_remain_for_repository_check_impl(ctx):
+ env = unittest.begin(ctx)
+ first = _artifact("1.0", force_version = True, neverlink = True)
+ second = _artifact("1.0", force_version = True)
+
+ asserts.equals(env, [first, second], _layer(root_artifacts = [first, second]).artifacts)
+
+ return unittest.end(env)
+
+matching_forced_duplicates_from_root_module_remain_for_repository_check_test = unittest.make(_matching_forced_duplicates_from_root_module_remain_for_repository_check_impl)
+
+def _forced_nonroot_beats_higher_unforced_nonroot_impl(ctx):
+ env = unittest.begin(ctx)
+ forced = _artifact("1.0", force_version = True)
+
+ asserts.equals(
+ env,
+ [forced],
+ deduplicate_non_root_artifacts(
+ {
+ "first": [_artifact("2.0")],
+ "second": [forced],
+ },
+ return_only_artifacts = True,
+ ),
+ )
+
+ return unittest.end(env)
+
+forced_nonroot_beats_higher_unforced_nonroot_test = unittest.make(_forced_nonroot_beats_higher_unforced_nonroot_impl)
+
+def _forced_nonroot_beats_higher_duplicate_from_same_module_impl(ctx):
+ env = unittest.begin(ctx)
+ forced = _artifact("1.0", force_version = True)
+
+ asserts.equals(
+ env,
+ [forced],
+ deduplicate_non_root_artifacts(
+ {"dep": [_artifact("2.0"), forced]},
+ return_only_artifacts = True,
+ ),
+ )
+
+ return unittest.end(env)
+
+forced_nonroot_beats_higher_duplicate_from_same_module_test = unittest.make(_forced_nonroot_beats_higher_duplicate_from_same_module_impl)
+
+def _forced_classifiers_from_same_module_layer_independently_impl(ctx):
+ env = unittest.begin(ctx)
+ plain = _artifact("1.0", force_version = True)
+ classified = _artifact("2.0", force_version = True, classifier = "tests")
+
+ asserts.equals(
+ env,
+ [plain, classified],
+ deduplicate_non_root_artifacts(
+ {"dep": [plain, classified]},
+ return_only_artifacts = True,
+ ),
+ )
+
+ return unittest.end(env)
+
+forced_classifiers_from_same_module_layer_independently_test = unittest.make(_forced_classifiers_from_same_module_layer_independently_impl)
+
+def _pinned_maven_synthesized_root_forces_do_not_conflict_impl(ctx):
+ env = unittest.begin(ctx)
+
+ result = _layer(
+ root_artifacts = [_artifact("1.0"), _artifact("2.0")],
+ resolver = "maven",
+ version_conflict_policy = "pinned",
+ )
+
+ asserts.equals(env, ["1.0", "2.0"], [artifact.version for artifact in result.artifacts])
+ asserts.equals(env, [True, True], [artifact.force_version for artifact in result.artifacts])
+
+ return unittest.end(env)
+
+pinned_maven_synthesized_root_forces_do_not_conflict_test = unittest.make(_pinned_maven_synthesized_root_forces_do_not_conflict_impl)
+
+def _namespace_without_root_uses_nonroot_dedup_impl(ctx):
+ env = unittest.begin(ctx)
+ higher = _artifact("2.0")
+
+ result = _layer(
+ root_present = False,
+ non_root_artifacts = {
+ "first": [_artifact("1.0")],
+ "second": [higher],
+ },
+ )
+
+ asserts.equals(env, [higher], result.artifacts)
+ asserts.equals(env, [], result.boms)
+ asserts.equals(env, [], result.diagnostics)
+
+ return unittest.end(env)
+
+namespace_without_root_uses_nonroot_dedup_test = unittest.make(_namespace_without_root_uses_nonroot_dedup_impl)
+
+def _known_contributors_filter_deps_and_boms_impl(ctx):
+ env = unittest.begin(ctx)
+ kept_artifact = _artifact("1.0")
+ kept_bom = _artifact("1.0", packaging = "pom", artifact = "bom")
+
+ artifacts = filter_known_contributing_modules(
+ "custom",
+ sets.make(["kept"]),
+ {"kept": [kept_artifact], "excluded-dep": [_artifact("2.0")]},
+ "deps",
+ )
+ boms = filter_known_contributing_modules(
+ "custom",
+ sets.make(["kept"]),
+ {"kept": [kept_bom], "excluded-bom": [_artifact("2.0", packaging = "pom", artifact = "bom")]},
+ "boms",
+ )
+
+ asserts.equals(env, {"kept": [kept_artifact]}, artifacts.filtered)
+ asserts.equals(env, {"kept": [kept_bom]}, boms.filtered)
+ asserts.equals(
+ env,
+ [struct(
+ text = "\nINFO: The @custom repo is not using deps from excluded-dep because it is not in the known_contributing_modules",
+ gate = "verbose",
+ )],
+ artifacts.diagnostics,
+ )
+ asserts.equals(
+ env,
+ [struct(
+ text = "\nINFO: The @custom repo is not using boms from excluded-bom because it is not in the known_contributing_modules",
+ gate = "verbose",
+ )],
+ boms.diagnostics,
+ )
+
+ return unittest.end(env)
+
+known_contributors_filter_deps_and_boms_test = unittest.make(_known_contributors_filter_deps_and_boms_impl)
+
+def _bom_only_contributor_warns_about_modules_impl(ctx):
+ env = unittest.begin(ctx)
+ bom = _artifact("1.0", packaging = "pom", artifact = "bom")
+
+ result = _layer(non_root_boms = {"dep": [bom]})
+
+ asserts.equals(env, [bom], result.boms)
+ asserts.equals(
+ env,
+ [struct(
+ text = "The maven repository 'maven' has contributions from multiple bzlmod modules, and will be resolved together: [\"dep\"].\nSee https://github.com/bazel-contrib/rules_jvm_external/blob/master/docs/bzlmod.md#module-dependency-layering for more information. \n To suppress this warning review the contributions from the other modules and add the following attribute in the root MODULE.bazel file: \nmaven.install(\n known_contributing_modules = [\"dep\"],\n ...\n)",
+ gate = "always",
+ ), struct(
+ text = "\nINFO: The @maven repo is getting the additional artifact com.example:bom:1.0 from the dep bazel dep.",
+ gate = "repin_verbose",
+ )],
+ result.diagnostics,
+ )
+
+ return unittest.end(env)
+
+bom_only_contributor_warns_about_modules_test = unittest.make(_bom_only_contributor_warns_about_modules_impl)
+
+def _boms_merge_with_root_priority_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("1.0", packaging = "pom", artifact = "bom", force_version = True)
+
+ result = _layer(
+ root_boms = [root],
+ non_root_boms = {"dep": [_artifact("2.0", packaging = "pom", artifact = "bom")]},
+ )
+
+ asserts.equals(env, [root], result.boms)
+
+ return unittest.end(env)
+
+boms_merge_with_root_priority_test = unittest.make(_boms_merge_with_root_priority_impl)
+
+def _classifier_and_packaging_layer_independently_impl(ctx):
+ env = unittest.begin(ctx)
+ plain = _artifact("1.0")
+ classified = _artifact("2.0", classifier = "tests")
+ pom = _artifact("3.0", packaging = "pom")
+
+ result = _layer(
+ root_present = False,
+ non_root_artifacts = {"dep": [plain, classified, pom]},
+ )
+
+ asserts.equals(env, [plain, classified, pom], result.artifacts)
+
+ return unittest.end(env)
+
+classifier_and_packaging_layer_independently_test = unittest.make(_classifier_and_packaging_layer_independently_impl)
+
+def _pinned_gradle_root_beats_higher_nonroot_force_impl(ctx):
+ env = unittest.begin(ctx)
+ root = _artifact("1.0")
+
+ result = _layer(
+ root_artifacts = [root],
+ resolver = "gradle",
+ version_conflict_policy = "pinned",
+ non_root_artifacts = {"dep": [_artifact("2.0", force_version = True)]},
+ )
+
+ asserts.equals(env, ["1.0"], [artifact.version for artifact in result.artifacts])
+ asserts.true(env, result.artifacts[0].force_version)
+
+ return unittest.end(env)
+
+pinned_gradle_root_beats_higher_nonroot_force_test = unittest.make(_pinned_gradle_root_beats_higher_nonroot_force_impl)
+
+def _namespaces_are_layered_independently_impl(ctx):
+ env = unittest.begin(ctx)
+
+ first = _layer(
+ name = "first",
+ root_present = False,
+ non_root_artifacts = {"dep": [_artifact("1.0")]},
+ )
+ second = _layer(
+ name = "second",
+ root_present = False,
+ non_root_artifacts = {"dep": [_artifact("2.0")]},
+ )
+
+ asserts.equals(env, ["1.0"], [artifact.version for artifact in first.artifacts])
+ asserts.equals(env, ["2.0"], [artifact.version for artifact in second.artifacts])
+
+ return unittest.end(env)
+
+namespaces_are_layered_independently_test = unittest.make(_namespaces_are_layered_independently_impl)
+
+def _diagnostics_preserve_text_gates_and_order_impl(ctx):
+ env = unittest.begin(ctx)
+ root_artifact = _artifact("1.0")
+ root_bom = _artifact("1.0", packaging = "pom", artifact = "bom")
+ kept_artifacts = [
+ _artifact("2.0"),
+ _artifact("1.0", artifact = "additional"),
+ ]
+ kept_boms = [
+ _artifact("2.0", packaging = "pom", artifact = "bom"),
+ _artifact("1.0", packaging = "pom", artifact = "additional-bom"),
+ ]
+
+ result = _layer(
+ name = "custom",
+ root_artifacts = [root_artifact],
+ root_boms = [root_bom],
+ known_contributing_modules = sets.make(["kept"]),
+ non_root_artifacts = {
+ "kept": kept_artifacts,
+ "excluded-dep": [_artifact("3.0")],
+ },
+ non_root_boms = {
+ "kept": kept_boms,
+ "excluded-bom": [_artifact("3.0", packaging = "pom", artifact = "bom")],
+ },
+ )
+
+ asserts.equals(
+ env,
+ [
+ struct(text = "\nINFO: The @custom repo is not using deps from excluded-dep because it is not in the known_contributing_modules", gate = "verbose"),
+ struct(text = "\nINFO: The @custom repo is not using boms from excluded-bom because it is not in the known_contributing_modules", gate = "verbose"),
+ struct(text = "\nWARNING: For dependency 'com.example:library' the root @custom repo wants version 1.0, but got 2.0 from the kept bazel dep. Please update the version in your MODULE.bazel or set `force_version = True`.", gate = "always"),
+ struct(text = "\nINFO: The @custom repo is getting the additional artifact com.example:additional:1.0 from the kept bazel dep.", gate = "repin_verbose"),
+ struct(text = "\nWARNING: For dependency 'com.example:bom' the root @custom repo wants version 1.0, but got 2.0 from the kept bazel dep. Please update the version in your MODULE.bazel or set `force_version = True`.", gate = "always"),
+ struct(text = "\nINFO: The @custom repo is getting the additional artifact com.example:additional-bom:1.0 from the kept bazel dep.", gate = "repin_verbose"),
+ ],
+ result.diagnostics,
+ )
+
+ return unittest.end(env)
+
+diagnostics_preserve_text_gates_and_order_test = unittest.make(_diagnostics_preserve_text_gates_and_order_impl)
+
+def _default_namespace_contribution_warning_is_preserved_impl(ctx):
+ env = unittest.begin(ctx)
+
+ result = _layer(non_root_artifacts = {"dep": [_artifact("1.0")]})
+
+ asserts.equals(
+ env,
+ [struct(
+ text = "The maven repository 'maven' has contributions from multiple bzlmod modules, and will be resolved together: [\"dep\"].\nSee https://github.com/bazel-contrib/rules_jvm_external/blob/master/docs/bzlmod.md#module-dependency-layering for more information. \n To suppress this warning review the contributions from the other modules and add the following attribute in the root MODULE.bazel file: \nmaven.install(\n known_contributing_modules = [\"dep\"],\n ...\n)",
+ gate = "always",
+ ), struct(
+ text = "\nINFO: The @maven repo is getting the additional artifact com.example:library:1.0 from the dep bazel dep.",
+ gate = "repin_verbose",
+ )],
+ result.diagnostics,
+ )
+
+ return unittest.end(env)
+
+default_namespace_contribution_warning_is_preserved_test = unittest.make(_default_namespace_contribution_warning_is_preserved_impl)
+
+def _diagnostic_gates_match_environment_flags_impl(ctx):
+ env = unittest.begin(ctx)
+ diagnostics = {
+ gate: struct(text = gate, gate = gate)
+ for gate in ["always", "repin", "verbose", "repin_verbose"]
+ }
+
+ asserts.equals(
+ env,
+ [True, False, False, False],
+ [should_print_diagnostic(diagnostics[gate], False, False) for gate in diagnostics],
+ )
+ asserts.equals(
+ env,
+ [True, True, False, False],
+ [should_print_diagnostic(diagnostics[gate], True, False) for gate in diagnostics],
+ )
+ asserts.equals(
+ env,
+ [True, False, True, False],
+ [should_print_diagnostic(diagnostics[gate], False, True) for gate in diagnostics],
+ )
+ asserts.equals(
+ env,
+ [True, True, True, True],
+ [should_print_diagnostic(diagnostics[gate], True, True) for gate in diagnostics],
+ )
+
+ return unittest.end(env)
+
+diagnostic_gates_match_environment_flags_test = unittest.make(_diagnostic_gates_match_environment_flags_impl)
+
+def layering_test_suite():
+ conflicting_nonroot_forces_target(
+ name = "conflicting_nonroot_forces_target",
+ # This target must only be analysed through the expected-failure test.
+ tags = ["manual"],
+ )
+ nonroot_override_error_target(
+ name = "nonroot_override_error_target",
+ # This target must only be analysed through the expected-failure test.
+ tags = ["manual"],
+ )
+ unforced_root_with_conflicting_nonroot_forces_target(
+ name = "unforced_root_with_conflicting_nonroot_forces_target",
+ # This target must only be analysed through the expected-failure test.
+ tags = ["manual"],
+ )
+ same_module_forces_target(
+ name = "different_forced_versions_from_same_nonroot_module_target",
+ second_version = "2.0",
+ tags = ["manual"],
+ )
+ same_module_forces_target(
+ name = "different_forced_versions_from_root_module_target",
+ root = True,
+ second_version = "2.0",
+ tags = ["manual"],
+ )
+ unittest.suite(
+ "layering_tests",
+ partial.make(root_only_resolves_root_version_test, size = "small"),
+ partial.make(nonroot_lower_version_is_dropped_test, size = "small"),
+ partial.make(equal_unforced_nonroot_keeps_root_metadata_without_warning_test, size = "small"),
+ partial.make(nonroot_higher_version_wins_and_warns_test, size = "small"),
+ partial.make(nonroot_override_warning_can_be_disabled_test, size = "small"),
+ partial.make(higher_nonroot_force_beats_unforced_root_test, size = "small"),
+ partial.make(lower_nonroot_force_beats_unforced_root_test, size = "small"),
+ partial.make(equal_nonroot_force_retains_transitive_pin_test, size = "small"),
+ partial.make(both_forced_root_wins_test, size = "small"),
+ partial.make(root_force_beats_conflicting_nonroot_forces_test, size = "small"),
+ partial.make(single_nonroot_survives_test, size = "small"),
+ partial.make(testonly_nonroot_is_filtered_test, size = "small"),
+ partial.make(multiple_nonroot_highest_wins_test, size = "small"),
+ partial.make(equal_version_tie_keeps_first_module_metadata_test, size = "small"),
+ partial.make(root_force_beats_higher_unforced_nonroot_test, size = "small"),
+ partial.make(
+ conflicting_nonroot_forces_fail_test,
+ target_under_test = ":conflicting_nonroot_forces_target",
+ ),
+ partial.make(
+ unforced_root_with_conflicting_nonroot_forces_fail_test,
+ target_under_test = ":unforced_root_with_conflicting_nonroot_forces_target",
+ ),
+ partial.make(
+ nonroot_override_error_fails_test,
+ target_under_test = ":nonroot_override_error_target",
+ ),
+ partial.make(
+ different_forced_versions_from_same_nonroot_module_fail_test,
+ expected_message = "Module 'dep' forces dependency 'com.example:library' at different versions: 1.0 and 2.0.",
+ target_under_test = ":different_forced_versions_from_same_nonroot_module_target",
+ ),
+ partial.make(
+ different_forced_versions_from_root_module_fail_test,
+ expected_message = "Module 'root' forces dependency 'com.example:library' at different versions: 1.0 and 2.0.",
+ target_under_test = ":different_forced_versions_from_root_module_target",
+ ),
+ partial.make(matching_nonroot_forces_keep_first_module_test, size = "small"),
+ partial.make(matching_forced_duplicates_from_same_nonroot_module_keep_first_test, size = "small"),
+ partial.make(matching_forced_duplicates_from_root_module_remain_for_repository_check_test, size = "small"),
+ partial.make(forced_nonroot_beats_higher_unforced_nonroot_test, size = "small"),
+ partial.make(forced_nonroot_beats_higher_duplicate_from_same_module_test, size = "small"),
+ partial.make(forced_classifiers_from_same_module_layer_independently_test, size = "small"),
+ partial.make(pinned_maven_synthesized_root_forces_do_not_conflict_test, size = "small"),
+ partial.make(namespace_without_root_uses_nonroot_dedup_test, size = "small"),
+ partial.make(known_contributors_filter_deps_and_boms_test, size = "small"),
+ partial.make(bom_only_contributor_warns_about_modules_test, size = "small"),
+ partial.make(boms_merge_with_root_priority_test, size = "small"),
+ partial.make(classifier_and_packaging_layer_independently_test, size = "small"),
+ partial.make(pinned_gradle_root_beats_higher_nonroot_force_test, size = "small"),
+ partial.make(namespaces_are_layered_independently_test, size = "small"),
+ partial.make(diagnostics_preserve_text_gates_and_order_test, size = "small"),
+ partial.make(default_namespace_contribution_warning_is_preserved_test, size = "small"),
+ partial.make(diagnostic_gates_match_environment_flags_test, size = "small"),
+ )
diff --git a/tests/unit/version_conflict_policy_test.bzl b/tests/unit/version_conflict_policy_test.bzl
index b4210a9..891a434 100644
--- a/tests/unit/version_conflict_policy_test.bzl
+++ b/tests/unit/version_conflict_policy_test.bzl
@@ -2,8 +2,8 @@
load("@bazel_skylib//lib:partial.bzl", "partial")
load("@bazel_skylib//lib:unittest.bzl", "asserts", "unittest")
-load("//private/extensions:maven.bzl", "apply_root_version_conflict_policy")
load("//private/lib:coordinates.bzl", "unpack_coordinates")
+load("//private/lib:layering.bzl", "apply_root_version_conflict_policy")
def _pinned_policy_forces_versioned_root_artifacts_impl(ctx):
env = unittest.begin(ctx)