diff --git a/src/python/pants/backend/experimental/java/register.py b/src/python/pants/backend/experimental/java/register.py index 5592b938b91..ea9b3bebaed 100644 --- a/src/python/pants/backend/experimental/java/register.py +++ b/src/python/pants/backend/experimental/java/register.py @@ -17,7 +17,7 @@ from pants.jvm import classpath, jdk_rules, resources from pants.jvm import util_rules as jvm_util_rules from pants.jvm.dependency_inference import symbol_mapper -from pants.jvm.goals import coursier +from pants.jvm.goals import lockfile from pants.jvm.resolve import coursier_fetch, jvm_tool from pants.jvm.target_types import JvmArtifactTarget from pants.jvm.test import junit @@ -41,7 +41,7 @@ def rules(): *classpath.rules(), *junit.rules(), *deploy_jar.rules(), - *coursier.rules(), + *lockfile.rules(), *coursier_fetch.rules(), *java_parser.rules(), *java_parser_launcher.rules(), diff --git a/src/python/pants/backend/experimental/scala/register.py b/src/python/pants/backend/experimental/scala/register.py index 3251430f4b7..912feb80b20 100644 --- a/src/python/pants/backend/experimental/scala/register.py +++ b/src/python/pants/backend/experimental/scala/register.py @@ -18,7 +18,7 @@ from pants.backend.scala.test import scalatest from pants.jvm import classpath, jdk_rules, resources from pants.jvm import util_rules as jvm_util_rules -from pants.jvm.goals import coursier +from pants.jvm.goals import lockfile from pants.jvm.resolve import coursier_fetch, coursier_setup, jvm_tool from pants.jvm.target_types import JvmArtifactTarget from pants.jvm.test import junit @@ -48,7 +48,7 @@ def rules(): *classpath.rules(), *junit.rules(), *deploy_jar.rules(), - *coursier.rules(), + *lockfile.rules(), *coursier_fetch.rules(), *coursier_setup.rules(), *jvm_util_rules.rules(), diff --git a/src/python/pants/backend/java/compile/javac_test.py b/src/python/pants/backend/java/compile/javac_test.py index f41b93388c0..6a3042bce4b 100644 --- a/src/python/pants/backend/java/compile/javac_test.py +++ b/src/python/pants/backend/java/compile/javac_test.py @@ -24,7 +24,7 @@ from pants.engine.target import CoarsenedTargets, Targets from pants.jvm import jdk_rules, testutil from pants.jvm.compile import ClasspathEntry, CompileResult, FallibleClasspathEntry -from pants.jvm.goals.coursier import rules as coursier_rules +from pants.jvm.goals import lockfile from pants.jvm.resolve.common import ArtifactRequirement, Coordinate, Coordinates from pants.jvm.resolve.coursier_fetch import CoursierLockfileEntry from pants.jvm.resolve.coursier_fetch import rules as coursier_fetch_rules @@ -55,7 +55,7 @@ def rule_runner() -> RuleRunner: *javac_check_rules(), *util_rules(), *target_types_rules(), - *coursier_rules(), + *lockfile.rules(), *jdk_rules.rules(), *java_dep_inf_rules(), *source_files.rules(), diff --git a/src/python/pants/core/goals/generate_lockfiles.py b/src/python/pants/core/goals/generate_lockfiles.py index b234cd66a19..99b2d0b95d7 100644 --- a/src/python/pants/core/goals/generate_lockfiles.py +++ b/src/python/pants/core/goals/generate_lockfiles.py @@ -221,10 +221,6 @@ def determine_resolves_to_generate( sentinel.options_scope: sentinel for sentinel in all_tool_sentinels } - # TODO: check for ambiguity: between tools and user resolves, and across distinct - # `KnownUserResolveNames`s. Update AmbiguousResolveNamesError to say where the resolve - # name is defined, whereas right now we hardcode it to be the `[python]` option. - if not requested_resolve_names: return [ known_resolve_names.requested_resolve_names_cls(known_resolve_names.names) @@ -289,8 +285,7 @@ def filter_tool_lockfile_requests( class GenerateLockfilesSubsystem(GoalSubsystem): name = "generate-lockfiles" help = "Generate lockfiles for Python third-party dependencies." - # TODO: Add back `KnownUserResolveNames` once JVM implements it. - required_union_implementations = (ToolLockfileSentinel,) + required_union_implementations = (ToolLockfileSentinel, KnownUserResolveNamesRequest) @classmethod def register_options(cls, register) -> None: diff --git a/src/python/pants/jvm/compile_test.py b/src/python/pants/jvm/compile_test.py index 016f8e4dcc9..3ccd157ba68 100644 --- a/src/python/pants/jvm/compile_test.py +++ b/src/python/pants/jvm/compile_test.py @@ -45,7 +45,7 @@ ClasspathSourceAmbiguity, ClasspathSourceMissing, ) -from pants.jvm.goals.coursier import rules as coursier_rules +from pants.jvm.goals import lockfile from pants.jvm.resolve.coursier_fetch import CoursierFetchRequest from pants.jvm.resolve.coursier_fetch import rules as coursier_fetch_rules from pants.jvm.resolve.coursier_setup import rules as coursier_setup_rules @@ -66,7 +66,7 @@ def rule_runner() -> RuleRunner: rules=[ *config_files.rules(), *coursier_fetch_rules(), - *coursier_rules(), + *lockfile.rules(), *classpath.rules(), *coursier_setup_rules(), *external_tool_rules(), diff --git a/src/python/pants/jvm/goals/coursier.py b/src/python/pants/jvm/goals/coursier.py deleted file mode 100644 index 859254db12d..00000000000 --- a/src/python/pants/jvm/goals/coursier.py +++ /dev/null @@ -1,170 +0,0 @@ -# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). -# Licensed under the Apache License, Version 2.0 (see LICENSE). - -from __future__ import annotations - -from collections import defaultdict -from dataclasses import dataclass - -from pants.engine.console import Console -from pants.engine.fs import ( - EMPTY_DIGEST, - CreateDigest, - Digest, - DigestContents, - FileContent, - MergeDigests, - PathGlobs, - Snapshot, - Workspace, -) -from pants.engine.goal import Goal, GoalSubsystem -from pants.engine.rules import Get, MultiGet, collect_rules, goal_rule, rule -from pants.engine.target import AllTargets -from pants.jvm.resolve.common import ArtifactRequirement, ArtifactRequirements -from pants.jvm.resolve.coursier_fetch import CoursierError, CoursierResolvedLockfile -from pants.jvm.resolve.lockfile_metadata import JVMLockfileMetadata -from pants.jvm.subsystems import JvmSubsystem -from pants.jvm.target_types import JvmArtifactCompatibleResolvesField -from pants.util.frozendict import FrozenDict - - -class CoursierResolveSubsystem(GoalSubsystem): - name = "coursier-resolve" - help = "Generate a lockfile by resolving JVM dependencies." - - @classmethod - def register_options(cls, register): - super().register_options(register) - register( - "--names", - type=list, - help=( - "A list of resolve names to resolve.\n\n" - "Each name must be defined as a resolve in `[jvm].resolves`.\n\n" - "If not provided, resolve all known resolves." - ), - ) - - -class CoursierResolve(Goal): - subsystem_cls = CoursierResolveSubsystem - - -class JvmResolvesToArtifacts(FrozenDict[str, ArtifactRequirements]): - pass - - -@rule -async def map_resolves_to_consuming_targets( - all_targets: AllTargets, jvm: JvmSubsystem -) -> JvmResolvesToArtifacts: - resolve_to_artifacts = defaultdict(set) - for tgt in all_targets: - if not tgt.has_field(JvmArtifactCompatibleResolvesField): - continue - artifact = ArtifactRequirement.from_jvm_artifact_target(tgt) - for resolve in jvm.resolves_for_target(tgt): - resolve_to_artifacts[resolve].add(artifact) - return JvmResolvesToArtifacts( - (resolve, ArtifactRequirements(artifacts)) - for resolve, artifacts in resolve_to_artifacts.items() - ) - - -@dataclass(frozen=True) -class CoursierGenerateLockfileRequest: - """Regenerate a lockfile from its JVM requirements. - - This request allows a user to manually regenerate their lockfile. This is done for a few reasons: to - generate the lockfile for the first time, to regenerate it because the input JVM requirements - have changed, or to regenerate it to check if the resolve has changed (e.g. due to newer - versions of dependencies being published). - - resolve: The name of the resolve config to compare - """ - - resolve: str - - -@dataclass(frozen=True) -class CoursierGenerateLockfileResult: - digest: Digest - - -@rule -async def coursier_generate_lockfile( - request: CoursierGenerateLockfileRequest, - jvm: JvmSubsystem, - resolves_to_artifacts: JvmResolvesToArtifacts, -) -> CoursierGenerateLockfileResult: - requirements = ArtifactRequirements(resolves_to_artifacts.get(request.resolve, ())) - resolved_lockfile = await Get( - CoursierResolvedLockfile, - # Note that it's legal to have a resolve with no artifacts. - ArtifactRequirements, - requirements, - ) - resolved_serialized_lockfile = resolved_lockfile.to_serialized() - metadata = JVMLockfileMetadata.new(requirements) - resolved_serialized_lockfile = metadata.add_header_to_lockfile( - resolved_serialized_lockfile, regenerate_command="./pants generate-lockfiles" - ) - - lockfile_path = jvm.resolves[request.resolve] - - # If the lockfile hasn't changed, don't overwrite it. - existing_lockfile_digest_contents = await Get(DigestContents, PathGlobs([lockfile_path])) - if ( - existing_lockfile_digest_contents - and resolved_serialized_lockfile == existing_lockfile_digest_contents[0].content - ): - return CoursierGenerateLockfileResult(EMPTY_DIGEST) - - new_lockfile = await Get( - Digest, CreateDigest((FileContent(lockfile_path, resolved_serialized_lockfile),)) - ) - return CoursierGenerateLockfileResult(new_lockfile) - - -@goal_rule -async def coursier_resolve_lockfiles( - console: Console, - resolve_subsystem: CoursierResolveSubsystem, - jvm: JvmSubsystem, - workspace: Workspace, -) -> CoursierResolve: - resolves = resolve_subsystem.options.names - available_resolves = set(jvm.resolves.keys()) - if not resolves: - # Default behaviour is to resolve everything. - resolves = available_resolves - else: - invalid_resolve_names = set(resolves) - available_resolves - if invalid_resolve_names: - raise CoursierError( - "The following resolve names are not defined in `[jvm].resolves`: " - f"{invalid_resolve_names}\n\n" - f"The valid resolve names are: {available_resolves}" - ) - - results = await MultiGet( - Get(CoursierGenerateLockfileResult, CoursierGenerateLockfileRequest(resolve)) - for resolve in resolves - ) - - # For performance reasons, avoid writing out files to the workspace that haven't changed. - results_to_write = tuple(result for result in results if result.digest != EMPTY_DIGEST) - if results_to_write: - merged_snapshot = await Get( - Snapshot, MergeDigests(result.digest for result in results_to_write) - ) - workspace.write_digest(merged_snapshot.digest) - for path in merged_snapshot.files: - console.print_stderr(f"Updated lockfile at: {path}") - - return CoursierResolve(exit_code=0) - - -def rules(): - return [*collect_rules()] diff --git a/src/python/pants/jvm/goals/coursier_integration_test.py b/src/python/pants/jvm/goals/coursier_integration_test.py deleted file mode 100644 index ce2ed0fa72e..00000000000 --- a/src/python/pants/jvm/goals/coursier_integration_test.py +++ /dev/null @@ -1,205 +0,0 @@ -# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). -# Licensed under the Apache License, Version 2.0 (see LICENSE). - -from __future__ import annotations - -from pathlib import Path -from textwrap import dedent - -import pytest - -from pants.core.util_rules import source_files -from pants.core.util_rules.external_tool import rules as external_tool_rules -from pants.engine.fs import FileDigest -from pants.jvm.goals.coursier import CoursierResolve -from pants.jvm.goals.coursier import rules as coursier_goal_rules -from pants.jvm.resolve.common import ArtifactRequirement, Coordinate, Coordinates -from pants.jvm.resolve.coursier_fetch import CoursierLockfileEntry, CoursierResolvedLockfile -from pants.jvm.resolve.coursier_fetch import rules as coursier_fetch_rules -from pants.jvm.resolve.coursier_setup import rules as coursier_setup_rules -from pants.jvm.resolve.lockfile_metadata import JVMLockfileMetadata -from pants.jvm.target_types import JvmArtifactTarget -from pants.jvm.testutil import maybe_skip_jdk_test -from pants.jvm.util_rules import rules as util_rules -from pants.testutil.rule_runner import RuleRunner - -NAME = "hamcrest" -GROUP = "org.hamcrest" -ARTIFACT = "hamcrest-core" -VERSION = "1.3" - -HAMCREST_BUILD_FILE = dedent( - f"""\ - jvm_artifact( - name='{NAME}', - group='{GROUP}', - artifact='{ARTIFACT}', - version="{VERSION}", - ) - """ -) -HAMCREST_EXPECTED_LOCKFILE = CoursierResolvedLockfile( - entries=( - CoursierLockfileEntry( - coord=Coordinate( - group=GROUP, - artifact=ARTIFACT, - version=VERSION, - ), - file_name="org.hamcrest_hamcrest-core_1.3.jar", - direct_dependencies=Coordinates([]), - dependencies=Coordinates([]), - file_digest=FileDigest( - fingerprint="66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9", - serialized_bytes_length=45024, - ), - ), - ) -) -HAMCREST_LOCKFILE_METADATA = JVMLockfileMetadata.new( - [ArtifactRequirement(Coordinate(group=GROUP, artifact=ARTIFACT, version=VERSION))] -) - - -def compare_lockfiles(file_content: bytes, structured: CoursierResolvedLockfile) -> None: - assert CoursierResolvedLockfile.from_serialized(file_content).entries == structured.entries - - -@pytest.fixture -def rule_runner() -> RuleRunner: - rule_runner = RuleRunner( - rules=[ - *coursier_fetch_rules(), - *coursier_goal_rules(), - *coursier_setup_rules(), - *external_tool_rules(), - *source_files.rules(), - *util_rules(), - ], - target_types=[JvmArtifactTarget], - ) - rule_runner.set_options([], env_inherit={"PATH"}) - return rule_runner - - -@maybe_skip_jdk_test -def test_creates_missing_lockfile(rule_runner: RuleRunner) -> None: - rule_runner.write_files({"BUILD": HAMCREST_BUILD_FILE}) - result = rule_runner.run_goal_rule(CoursierResolve, args=[], env_inherit={"PATH"}) - assert result.exit_code == 0 - assert result.stderr == "Updated lockfile at: 3rdparty/jvm/default.lock\n" - compare_lockfiles( - Path(rule_runner.build_root, "3rdparty/jvm/default.lock").read_bytes(), - HAMCREST_EXPECTED_LOCKFILE, - ) - - -@maybe_skip_jdk_test -def test_noop_does_not_touch_lockfile(rule_runner: RuleRunner) -> None: - rule_runner.write_files( - { - "BUILD": HAMCREST_BUILD_FILE, - "3rdparty/jvm/default.lock": HAMCREST_LOCKFILE_METADATA.add_header_to_lockfile( - HAMCREST_EXPECTED_LOCKFILE.to_serialized(), - regenerate_command="./pants generate-lockfiles", - ).decode("utf-8"), - } - ) - result = rule_runner.run_goal_rule(CoursierResolve, args=[], env_inherit={"PATH"}) - assert result.exit_code == 0 - assert result.stderr == "" - compare_lockfiles( - Path(rule_runner.build_root, "3rdparty/jvm/default.lock").read_bytes(), - HAMCREST_EXPECTED_LOCKFILE, - ) - - -@maybe_skip_jdk_test -def test_updates_lockfile(rule_runner: RuleRunner) -> None: - rule_runner.write_files({"BUILD": HAMCREST_BUILD_FILE, "3rdparty/jvm/default.lock": "[]"}) - result = rule_runner.run_goal_rule(CoursierResolve, args=[], env_inherit={"PATH"}) - assert result.exit_code == 0 - assert result.stderr == "Updated lockfile at: 3rdparty/jvm/default.lock\n" - compare_lockfiles( - Path(rule_runner.build_root, "3rdparty/jvm/default.lock").read_bytes(), - HAMCREST_EXPECTED_LOCKFILE, - ) - - -@maybe_skip_jdk_test -def test_multiple_resolves(rule_runner: RuleRunner) -> None: - rule_runner.write_files( - { - "BUILD": dedent( - """\ - jvm_artifact( - name='hamcrest', - group='org.hamcrest', - artifact='hamcrest-core', - version="1.3", - compatible_resolves=["a", "b"], - ) - jvm_artifact( - name='opentest4j', - group='org.opentest4j', - artifact='opentest4j', - version='1.2.0', - compatible_resolves=["a"], - ) - jvm_artifact( - name='apiguardian-api', - group='org.apiguardian', - artifact='apiguardian-api', - version='1.1.0', - compatible_resolves=["b"], - ) - """ - ), - } - ) - result = rule_runner.run_goal_rule( - CoursierResolve, - args=["--jvm-resolves={'a': 'a.lockfile', 'b': 'b.lockfile'}"], - env_inherit={"PATH"}, - ) - assert result.exit_code == 0 - assert "Updated lockfile at: a.lockfile" in result.stderr - assert "Updated lockfile at: b.lockfile" in result.stderr - - expected_lockfile_a = CoursierResolvedLockfile( - entries=( - HAMCREST_EXPECTED_LOCKFILE.entries[0], - CoursierLockfileEntry( - coord=Coordinate(group="org.opentest4j", artifact="opentest4j", version="1.2.0"), - file_name="org.opentest4j_opentest4j_1.2.0.jar", - direct_dependencies=Coordinates([]), - dependencies=Coordinates([]), - file_digest=FileDigest( - fingerprint="58812de60898d976fb81ef3b62da05c6604c18fd4a249f5044282479fc286af2", - serialized_bytes_length=7653, - ), - ), - ) - ) - - compare_lockfiles(Path(rule_runner.build_root, "a.lockfile").read_bytes(), expected_lockfile_a) - - expected_lockfile_b = CoursierResolvedLockfile( - entries=( - CoursierLockfileEntry( - coord=Coordinate( - group="org.apiguardian", artifact="apiguardian-api", version="1.1.0" - ), - file_name="org.apiguardian_apiguardian-api_1.1.0.jar", - direct_dependencies=Coordinates([]), - dependencies=Coordinates([]), - file_digest=FileDigest( - fingerprint="a9aae9ff8ae3e17a2a18f79175e82b16267c246fbbd3ca9dfbbb290b08dcfdd4", - serialized_bytes_length=2387, - ), - ), - HAMCREST_EXPECTED_LOCKFILE.entries[0], - ) - ) - - compare_lockfiles(Path(rule_runner.build_root, "b.lockfile").read_bytes(), expected_lockfile_b) diff --git a/src/python/pants/jvm/goals/lockfile.py b/src/python/pants/jvm/goals/lockfile.py index e28855fdac1..839e7393e2c 100644 --- a/src/python/pants/jvm/goals/lockfile.py +++ b/src/python/pants/jvm/goals/lockfile.py @@ -3,23 +3,31 @@ from __future__ import annotations +from collections import defaultdict from dataclasses import dataclass from pants.core.goals.generate_lockfiles import ( GenerateLockfilesSubsystem, + KnownUserResolveNames, + KnownUserResolveNamesRequest, Lockfile, LockfileRequest, + RequestedUserResolveNames, + UserLockfileRequests, WrappedLockfileRequest, ) from pants.engine.fs import CreateDigest, Digest, FileContent, PathGlobs, Snapshot from pants.engine.rules import Get, collect_rules, rule +from pants.engine.target import AllTargets from pants.engine.unions import UnionRule from pants.jvm.resolve import coursier_fetch, jvm_tool -from pants.jvm.resolve.common import ArtifactRequirements +from pants.jvm.resolve.common import ArtifactRequirement, ArtifactRequirements from pants.jvm.resolve.coursier_fetch import CoursierResolvedLockfile from pants.jvm.resolve.jvm_tool import GatherJvmCoordinatesRequest, JvmToolBase from pants.jvm.resolve.key import CoursierResolveKey from pants.jvm.resolve.lockfile_metadata import JVMLockfileMetadata +from pants.jvm.subsystems import JvmSubsystem +from pants.jvm.target_types import JvmArtifactCompatibleResolvesField from pants.util.logging import LogLevel from pants.util.meta import frozen_after_init from pants.util.ordered_set import FrozenOrderedSet @@ -81,7 +89,7 @@ async def generate_jvm_lockfile( lockfile_digest = await Get( Digest, - CreateDigest([FileContent(request.lockfile_dest, resolved_lockfile.to_serialized())]), + CreateDigest([FileContent(request.lockfile_dest, resolved_lockfile_contents)]), ) return Lockfile(lockfile_digest, request.resolve_name, request.lockfile_dest) @@ -110,10 +118,54 @@ async def load_jvm_lockfile( ) +class RequestedJVMserResolveNames(RequestedUserResolveNames): + pass + + +class KnownJVMUserResolveNamesRequest(KnownUserResolveNamesRequest): + pass + + +@rule +def determine_jvm_user_resolves( + _: KnownJVMUserResolveNamesRequest, jvm_subsystem: JvmSubsystem +) -> KnownUserResolveNames: + return KnownUserResolveNames( + names=tuple(jvm_subsystem.resolves.keys()), + option_name=f"[{jvm_subsystem.options_scope}].resolves", + requested_resolve_names_cls=RequestedJVMserResolveNames, + ) + + +@rule +async def setup_user_lockfile_requests( + requested: RequestedJVMserResolveNames, all_targets: AllTargets, jvm_subsystem: JvmSubsystem +) -> UserLockfileRequests: + resolve_to_artifacts = defaultdict(set) + for tgt in all_targets: + if not tgt.has_field(JvmArtifactCompatibleResolvesField): + continue + artifact = ArtifactRequirement.from_jvm_artifact_target(tgt) + for resolve in jvm_subsystem.resolves_for_target(tgt): + resolve_to_artifacts[resolve].add(artifact) + + return UserLockfileRequests( + JvmLockfileRequest( + # Note that it's legal to have a resolve with no artifacts. + artifacts=ArtifactRequirements(sorted(resolve_to_artifacts.get(resolve, ()))), + resolve_name=resolve, + lockfile_dest=jvm_subsystem.resolves[resolve], + ) + for resolve in requested + ) + + def rules(): return ( *collect_rules(), *coursier_fetch.rules(), *jvm_tool.rules(), UnionRule(LockfileRequest, JvmLockfileRequest), + UnionRule(KnownUserResolveNamesRequest, KnownJVMUserResolveNamesRequest), + UnionRule(RequestedUserResolveNames, RequestedJVMserResolveNames), ) diff --git a/src/python/pants/jvm/goals/lockfile_test.py b/src/python/pants/jvm/goals/lockfile_test.py new file mode 100644 index 00000000000..98cc5db88eb --- /dev/null +++ b/src/python/pants/jvm/goals/lockfile_test.py @@ -0,0 +1,141 @@ +# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md). +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import annotations + +from textwrap import dedent + +import pytest + +from pants.core.goals.generate_lockfiles import Lockfile, UserLockfileRequests +from pants.core.util_rules import source_files +from pants.core.util_rules.external_tool import rules as external_tool_rules +from pants.engine.fs import DigestContents, FileDigest +from pants.jvm.goals import lockfile +from pants.jvm.goals.lockfile import JvmLockfileRequest, RequestedJVMserResolveNames +from pants.jvm.resolve.common import ( + ArtifactRequirement, + ArtifactRequirements, + Coordinate, + Coordinates, +) +from pants.jvm.resolve.coursier_fetch import CoursierLockfileEntry, CoursierResolvedLockfile +from pants.jvm.resolve.coursier_fetch import rules as coursier_fetch_rules +from pants.jvm.resolve.coursier_setup import rules as coursier_setup_rules +from pants.jvm.resolve.lockfile_metadata import JVMLockfileMetadata +from pants.jvm.target_types import JvmArtifactTarget +from pants.jvm.testutil import maybe_skip_jdk_test +from pants.jvm.util_rules import rules as util_rules +from pants.testutil.rule_runner import QueryRule, RuleRunner + + +@pytest.fixture +def rule_runner() -> RuleRunner: + rule_runner = RuleRunner( + rules=[ + *coursier_fetch_rules(), + *lockfile.rules(), + *coursier_setup_rules(), + *external_tool_rules(), + *source_files.rules(), + *util_rules(), + QueryRule(UserLockfileRequests, [RequestedJVMserResolveNames]), + QueryRule(Lockfile, [JvmLockfileRequest]), + ], + target_types=[JvmArtifactTarget], + ) + rule_runner.set_options([], env_inherit={"PATH"}) + return rule_runner + + +@maybe_skip_jdk_test +def test_generate_lockfile(rule_runner: RuleRunner) -> None: + artifacts = ArtifactRequirements( + [ArtifactRequirement(Coordinate("org.hamcrest", "hamcrest-core", "1.3"))] + ) + result = rule_runner.request( + Lockfile, + [JvmLockfileRequest(artifacts=artifacts, resolve_name="test", lockfile_dest="lock.txt")], + ) + digest_contents = rule_runner.request(DigestContents, [result.digest]) + assert len(digest_contents) == 1 + + expected = CoursierResolvedLockfile( + entries=( + CoursierLockfileEntry( + coord=Coordinate( + group="org.hamcrest", + artifact="hamcrest-core", + version="1.3", + ), + file_name="org.hamcrest_hamcrest-core_1.3.jar", + direct_dependencies=Coordinates([]), + dependencies=Coordinates([]), + file_digest=FileDigest( + fingerprint="66fdef91e9739348df7a096aa384a5685f4e875584cce89386a7a47251c4d8e9", + serialized_bytes_length=45024, + ), + ), + ), + metadata=JVMLockfileMetadata.new(artifacts), + ) + assert CoursierResolvedLockfile.from_serialized(digest_contents[0].content) == expected + + +@maybe_skip_jdk_test +def test_multiple_resolves(rule_runner: RuleRunner) -> None: + rule_runner.write_files( + { + "BUILD": dedent( + """\ + jvm_artifact( + name='hamcrest', + group='org.hamcrest', + artifact='hamcrest-core', + version="1.3", + compatible_resolves=["a", "b"], + ) + jvm_artifact( + name='opentest4j', + group='org.opentest4j', + artifact='opentest4j', + version='1.2.0', + compatible_resolves=["a"], + ) + jvm_artifact( + name='apiguardian-api', + group='org.apiguardian', + artifact='apiguardian-api', + version='1.1.0', + compatible_resolves=["b"], + ) + """ + ), + } + ) + rule_runner.set_options(["--jvm-resolves={'a': 'a.lock', 'b': 'b.lock'}"], env_inherit={"PATH"}) + + result = rule_runner.request(UserLockfileRequests, [RequestedJVMserResolveNames(["a", "b"])]) + hamcrest_core = ArtifactRequirement(Coordinate("org.hamcrest", "hamcrest-core", "1.3")) + assert set(result) == { + JvmLockfileRequest( + artifacts=ArtifactRequirements( + [ + hamcrest_core, + ArtifactRequirement(Coordinate("org.opentest4j", "opentest4j", "1.2.0")), + ] + ), + resolve_name="a", + lockfile_dest="a.lock", + ), + JvmLockfileRequest( + artifacts=ArtifactRequirements( + [ + ArtifactRequirement(Coordinate("org.apiguardian", "apiguardian-api", "1.1.0")), + hamcrest_core, + ] + ), + resolve_name="b", + lockfile_dest="b.lock", + ), + } diff --git a/src/python/pants/jvm/resolve/common.py b/src/python/pants/jvm/resolve/common.py index 86328487eaf..42baae50c86 100644 --- a/src/python/pants/jvm/resolve/common.py +++ b/src/python/pants/jvm/resolve/common.py @@ -139,7 +139,7 @@ class Coordinates(DeduplicatedCollection[Coordinate]): """An ordered list of `Coordinate`s.""" -@dataclass(frozen=True) +@dataclass(frozen=True, order=True) class ArtifactRequirement: """A single Maven-style coordinate for a JVM dependency, along with information of how to fetch the dependency if it is not to be fetched from a Maven repository.""" diff --git a/src/python/pants/jvm/resolve/lockfile_metadata.py b/src/python/pants/jvm/resolve/lockfile_metadata.py index b4c75d455f8..799888e70bb 100644 --- a/src/python/pants/jvm/resolve/lockfile_metadata.py +++ b/src/python/pants/jvm/resolve/lockfile_metadata.py @@ -35,7 +35,7 @@ class JVMLockfileMetadata(LockfileMetadata): @staticmethod def new( requirements: Iterable[ArtifactRequirement], - ) -> LockfileMetadata: + ) -> JVMLockfileMetadata: """Call the most recent version of the `LockfileMetadata` class to construct a concrete instance.