blob: 71079a82e27e817db5a8f4b9e8c7380cd646e554 [file]
"""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 = [
artifact
for artifact in bazel_dep_to_non_root_artifacts.get(bazel_dep_name, [])
if not getattr(artifact, "testonly", False)
]
_fail_if_conflicting_forces(bazel_dep_name, module_artifacts)
for artifact in module_artifacts:
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 = ""
forced_version_info = ""
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
elif non_root_forced:
forced_version_info = forced_version_info + (
"\nINFO: For dependency '%s:%s' the %s bazel dep forces version %s; " % (root_artifact.group, root_artifact.artifact, bazel_dep_name, non_root_artifact.version) +
"its declaration replaces the root module's declaration of the same version."
)
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
additional_artifact_message = ""
for bazel_dep_name, non_root_artifact in non_root_coordinate_to_artifact.values():
additional_artifact_message = additional_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 forced_version_info != "":
diagnostics.append(_diagnostic(forced_version_info, "verbose"))
if additional_artifact_message != "":
diagnostics.append(_diagnostic(additional_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
]