Provide a mechanism to list all resolved direct deps for a workspace (#1510)
You can do this by running `bazel run @maven//:direct_deps`
diff --git a/private/lib/coordinates.bzl b/private/lib/coordinates.bzl
index afc4ed1..9bbb40d 100644
--- a/private/lib/coordinates.bzl
+++ b/private/lib/coordinates.bzl
@@ -37,6 +37,10 @@
classifier = coords.get("classifier", None),
)
+ if type(coords) != "string":
+ # Already a struct, return as-is
+ return coords
+
pieces = coords.split(":")
if len(pieces) < 2:
fail("Could not parse maven coordinate: %s" % coords)
@@ -101,6 +105,34 @@
fail("Could not parse maven coordinate: %s" % coords)
+def to_key(coords):
+ """Converts coordinates to a versionless key for lookups.
+
+ The key format is `group:artifact[:packaging[:classifier]]` where packaging
+ and classifier are only included if they differ from "jar".
+
+ Args:
+ coords: Either a string in Gradle External format, a dict with "group",
+ "artifact", and optional "packaging"/"classifier" keys, or a
+ struct from unpack_coordinates.
+
+ Returns:
+ A string key suitable for artifact lookups.
+ """
+ unpacked = unpack_coordinates(coords)
+
+ key = "%s:%s" % (unpacked.group, unpacked.artifact)
+
+ packaging = getattr(unpacked, "packaging", None) or "jar"
+ classifier = getattr(unpacked, "classifier", None) or "jar"
+
+ if classifier != "jar":
+ key += ":%s:%s" % (packaging, classifier)
+ elif packaging != "jar":
+ key += ":%s" % packaging
+
+ return key
+
def to_external_form(coords):
"""Formats `coords` as a string suitable for use by tools such as Gradle.
diff --git a/private/rules/coursier.bzl b/private/rules/coursier.bzl
index 22b3a6a..84b83c8 100644
--- a/private/rules/coursier.bzl
+++ b/private/rules/coursier.bzl
@@ -30,6 +30,7 @@
"COURSIER_CLI_GITHUB_ASSET_URL",
"COURSIER_CLI_SHA256",
)
+load("//private/lib:coordinates.bzl", "to_key", "unpack_coordinates")
load("//private/lib:urls.bzl", "remove_auth_from_url")
load("//private/rules:v1_lock_file.bzl", "v1_lock_file")
load("//private/rules:v2_lock_file.bzl", "v2_lock_file")
@@ -83,6 +84,14 @@
)
"""
+_BUILD_DIRECT_DEPS = """
+sh_binary(
+ name = "direct_deps",
+ srcs = ["direct_deps.sh"],
+ visibility = ["//visibility:public"],
+)
+"""
+
_BUILD_OUTDATED = """
sh_binary(
name = "outdated",
@@ -422,6 +431,71 @@
executable = True,
)
+def get_direct_dependencies(all_artifacts, input_artifacts):
+ """Returns the resolved coordinates for the given input (direct) artifacts.
+
+ Args:
+ all_artifacts: The list of all resolved artifacts from importer.get_artifacts(),
+ each with a "coordinates" field in Gradle External format.
+ input_artifacts: A list of dicts with "group", "artifact", and optionally
+ "classifier" and "packaging" keys representing the direct
+ dependencies the user requested.
+
+ Returns:
+ A sorted list of resolved coordinates in Gradle External format.
+ """
+
+ # Build a lookup from versionless key to full coordinates.
+ # We store both the full key and a simplified group:artifact key
+ # to handle cases where input doesn't specify packaging but the
+ # resolved artifact has non-jar packaging (e.g., pom, aar).
+ resolved_lookup = {}
+ simple_lookup = {}
+ for artifact in all_artifacts:
+ coords = artifact.get("coordinates", "")
+ if coords:
+ full_key = to_key(coords)
+ resolved_lookup[full_key] = coords
+ # Also store by simple group:artifact for fallback matching
+ unpacked = unpack_coordinates(coords)
+ simple_key = "%s:%s" % (unpacked.group, unpacked.artifact)
+ # Only use simple key if no classifier (classifiers are intentional)
+ classifier = getattr(unpacked, "classifier", None)
+ if not classifier:
+ simple_lookup[simple_key] = coords
+
+ direct_deps = {}
+ for input_artifact in input_artifacts:
+ key = to_key(input_artifact)
+ resolved = resolved_lookup.get(key)
+ if not resolved:
+ # Fallback: try simple group:artifact lookup for artifacts where
+ # user didn't specify packaging but resolution found non-jar packaging
+ unpacked = unpack_coordinates(input_artifact)
+ simple_key = "%s:%s" % (unpacked.group, unpacked.artifact)
+ resolved = simple_lookup.get(simple_key)
+ if resolved:
+ direct_deps[resolved] = True
+
+ return sorted(direct_deps.keys())
+
+def _add_direct_deps_files(repository_ctx, direct_deps):
+ """Creates the direct_deps.sh script file.
+
+ Args:
+ repository_ctx: The repository context.
+ direct_deps: A list of resolved coordinates in Gradle External format.
+ """
+ script_content = "#!/bin/bash\n"
+ for dep in direct_deps:
+ script_content += "echo '%s'\n" % dep
+
+ repository_ctx.file(
+ "direct_deps.sh",
+ script_content,
+ executable = True,
+ )
+
def is_repin_required(repository_ctx):
env_var_names = repository_ctx.os.environ.keys()
return "RULES_JVM_EXTERNAL_REPIN" not in env_var_names and "REPIN" not in env_var_names
@@ -683,9 +757,13 @@
pin_target = generate_pin_target(repository_ctx, unpinned_pin_target)
+ all_artifacts = importer.get_artifacts(maven_install_json_content)
+ direct_deps = get_direct_dependencies(all_artifacts, artifacts)
+ _add_direct_deps_files(repository_ctx, direct_deps)
+
repository_ctx.file(
"BUILD",
- (_BUILD + _BUILD_OUTDATED).format(
+ (_BUILD + _BUILD_OUTDATED + _BUILD_DIRECT_DEPS).format(
visibilities = ",".join(["\"%s\"" % s for s in (["//visibility:public"] if not repository_ctx.attr.strict_visibility else repository_ctx.attr.strict_visibility_value)]),
repository_name = repository_ctx.name,
imports = generated_imports,
@@ -1373,9 +1451,13 @@
outdated_build_file_content = _BUILD_OUTDATED
_add_outdated_files(repository_ctx, artifacts, boms, repositories)
+ all_artifacts = v2_lock_file.get_artifacts(lock_file_contents)
+ direct_deps = get_direct_dependencies(all_artifacts, artifacts)
+ _add_direct_deps_files(repository_ctx, direct_deps)
+
repository_ctx.file(
"BUILD",
- (_BUILD + _BUILD_PIN + outdated_build_file_content).format(
+ (_BUILD + _BUILD_PIN + outdated_build_file_content + _BUILD_DIRECT_DEPS).format(
visibilities = ",".join(["\"%s\"" % s for s in (["//visibility:public"] if not repository_ctx.attr.strict_visibility else repository_ctx.attr.strict_visibility_value)]),
repository_name = repository_name,
imports = generated_imports,
diff --git a/private/rules/v2_lock_file.bzl b/private/rules/v2_lock_file.bzl
index 5e5eaa0..a6168cc 100644
--- a/private/rules/v2_lock_file.bzl
+++ b/private/rules/v2_lock_file.bzl
@@ -11,7 +11,7 @@
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
-load("//private/lib:coordinates.bzl", "to_external_form")
+load("//private/lib:coordinates.bzl", "to_external_form", "to_key")
_REQUIRED_KEYS = ["artifacts", "dependencies", "repositories"]
@@ -85,23 +85,6 @@
return coords
-def _to_key(unpacked):
- coords = "%s:%s" % (unpacked["group"], unpacked["artifact"])
-
- extension = unpacked.get("packaging", "jar")
- if not extension:
- extension = "jar"
- classifier = unpacked.get("classifier", "jar")
- if not classifier:
- classifier = "jar"
-
- if classifier != "jar":
- coords += ":%s:%s" % (extension, classifier)
- elif extension != "jar":
- coords += ":%s" % extension
-
- return coords
-
def _from_key(key, spoofed_version):
expected = "%s:%s" % (key, spoofed_version)
@@ -147,7 +130,7 @@
for (classifier, shasum) in data.get("shasums", {}).items():
root_unpacked["classifier"] = classifier
coordinates = to_external_form(root_unpacked)
- key = _to_key(root_unpacked)
+ key = to_key(root_unpacked)
urls = []
for (repo, artifacts_within_repo) in repositories.items():
diff --git a/tests/integration/BUILD b/tests/integration/BUILD
index 75ec593..10709ae 100644
--- a/tests/integration/BUILD
+++ b/tests/integration/BUILD
@@ -27,6 +27,7 @@
"@testonly_testing//:com_google_auto_value_auto_value_annotations_1_6_3",
"@testonly_testing//:com_google_auto_value_auto_value_annotations",
"@testonly_testing//:defs",
+ "@testonly_testing//:direct_deps",
"@testonly_testing//:outdated",
"@testonly_testing//:pin",
],
diff --git a/tests/unit/coursier_test.bzl b/tests/unit/coursier_test.bzl
index 2c66e61..acbc6ec 100644
--- a/tests/unit/coursier_test.bzl
+++ b/tests/unit/coursier_test.bzl
@@ -4,6 +4,7 @@
"//private/rules:coursier.bzl",
"compute_dependency_inputs_signature",
"get_coursier_cache_or_default",
+ "get_direct_dependencies",
"get_netrc_lines_from_entries",
infer = "infer_artifact_path_from_primary_and_repos",
)
@@ -619,6 +620,43 @@
calculate_inputs_hash_uses_excluded_artifacts_test = add_test(_calculate_inputs_hash_uses_excluded_artifacts_test)
+def _get_direct_dependencies_test_impl(ctx):
+ env = unittest.begin(ctx)
+
+ all_artifacts = [
+ {"coordinates": "com.google.guava:guava:31.0-jre"},
+ {"coordinates": "com.google.code.gson:gson:2.10.1"},
+ {"coordinates": "junit:junit:4.13.2"},
+ {"coordinates": "io.netty:netty-tcnative:2.0.61.Final"},
+ {"coordinates": "io.netty:netty-tcnative:2.0.61.Final:linux-x86_64"},
+ {"coordinates": "com.example:lib:1.0@aar"},
+ ]
+
+ input_artifacts = [
+ # Basic: should resolve to 31.0-jre even though 30.0-jre was requested
+ {"group": "com.google.guava", "artifact": "guava", "version": "30.0-jre"},
+ # Duplicate: same artifact requested again, should be deduplicated
+ {"group": "com.google.guava", "artifact": "guava", "version": "29.0-jre"},
+ # With classifier: should match the linux-x86_64 variant
+ {"group": "io.netty", "artifact": "netty-tcnative", "version": "2.0.60.Final", "classifier": "linux-x86_64"},
+ # With packaging: should match the aar
+ {"group": "com.example", "artifact": "lib", "version": "1.0", "packaging": "aar"},
+ # Missing: should be skipped
+ {"group": "com.example", "artifact": "not-present", "version": "1.0"},
+ ]
+
+ result = get_direct_dependencies(all_artifacts, input_artifacts)
+
+ # Should have 3 results: guava (deduplicated), netty with classifier, and aar
+ asserts.equals(env, 3, len(result))
+ asserts.true(env, "com.google.guava:guava:31.0-jre" in result)
+ asserts.true(env, "io.netty:netty-tcnative:2.0.61.Final:linux-x86_64" in result)
+ asserts.true(env, "com.example:lib:1.0@aar" in result)
+
+ return unittest.end(env)
+
+get_direct_dependencies_test = add_test(_get_direct_dependencies_test_impl)
+
def coursier_test_suite():
unittest.suite(
"coursier_tests",