diff --git a/src/python/pants/backend/go/goals/test.py b/src/python/pants/backend/go/goals/test.py index 61188d6d9bf..57c1e5a8e2c 100644 --- a/src/python/pants/backend/go/goals/test.py +++ b/src/python/pants/backend/go/goals/test.py @@ -7,7 +7,7 @@ from typing import Sequence from pants.backend.go.subsystems.gotest import GoTestSubsystem -from pants.backend.go.target_types import GoFirstPartyPackageSourcesField, GoImportPathField +from pants.backend.go.target_types import GoFirstPartyPackageSourcesField from pants.backend.go.util_rules.build_pkg import ( BuildGoPackageRequest, FallibleBuildGoPackageRequest, @@ -21,13 +21,11 @@ from pants.backend.go.util_rules.import_analysis import ImportConfig, ImportConfigRequest from pants.backend.go.util_rules.link import LinkedGoBinary, LinkGoBinaryRequest from pants.backend.go.util_rules.tests_analysis import GeneratedTestMain, GenerateTestMainRequest -from pants.build_graph.address import Address from pants.core.goals.test import TestDebugRequest, TestFieldSet, TestResult, TestSubsystem from pants.engine.fs import EMPTY_FILE_DIGEST, Digest, MergeDigests -from pants.engine.internals.selectors import Get, MultiGet +from pants.engine.internals.selectors import Get from pants.engine.process import FallibleProcessResult, Process, ProcessCacheScope from pants.engine.rules import collect_rules, rule -from pants.engine.target import WrappedTarget from pants.engine.unions import UnionRule from pants.util.logging import LogLevel from pants.util.ordered_set import FrozenOrderedSet, OrderedSet @@ -118,9 +116,8 @@ def transform_test_args(args: Sequence[str]) -> tuple[str, ...]: async def run_go_tests( field_set: GoTestFieldSet, test_subsystem: TestSubsystem, go_test_subsystem: GoTestSubsystem ) -> TestResult: - maybe_pkg_info, wrapped_target = await MultiGet( - Get(FallibleFirstPartyPkgInfo, FirstPartyPkgInfoRequest(field_set.address)), - Get(WrappedTarget, Address, field_set.address), + maybe_pkg_info = await Get( + FallibleFirstPartyPkgInfo, FirstPartyPkgInfoRequest(field_set.address) ) if maybe_pkg_info.info is None: @@ -135,9 +132,7 @@ async def run_go_tests( output_setting=test_subsystem.output, ) pkg_info = maybe_pkg_info.info - - target = wrapped_target.target - import_path = target[GoImportPathField].value + import_path = pkg_info.import_path testmain = await Get( GeneratedTestMain, diff --git a/src/python/pants/backend/go/target_type_rules.py b/src/python/pants/backend/go/target_type_rules.py index 89c80097aa3..16bde3bc918 100644 --- a/src/python/pants/backend/go/target_type_rules.py +++ b/src/python/pants/backend/go/target_type_rules.py @@ -14,7 +14,6 @@ GoBinaryMainPackageField, GoBinaryMainPackageRequest, GoFirstPartyPackageSourcesField, - GoFirstPartyPackageSubpathField, GoFirstPartyPackageTarget, GoImportPathField, GoModPackageSourcesField, @@ -25,6 +24,8 @@ from pants.backend.go.util_rules import first_party_pkg, import_analysis from pants.backend.go.util_rules.first_party_pkg import ( FallibleFirstPartyPkgInfo, + FirstPartyPkgImportPath, + FirstPartyPkgImportPathRequest, FirstPartyPkgInfoRequest, ) from pants.backend.go.util_rules.go_mod import GoModInfo, GoModInfoRequest @@ -68,7 +69,11 @@ class AllGoTargets(Targets): @rule(desc="Find all Go targets in project", level=LogLevel.DEBUG) def find_all_go_targets(tgts: AllTargets) -> AllGoTargets: - return AllGoTargets(t for t in tgts if t.has_field(GoImportPathField)) + return AllGoTargets( + t + for t in tgts + if t.has_field(GoImportPathField) or t.has_field(GoFirstPartyPackageSourcesField) + ) @dataclass(frozen=True) @@ -79,9 +84,22 @@ class ImportPathToPackages: @rule(desc="Map all Go targets to their import paths", level=LogLevel.DEBUG) async def map_import_paths_to_packages(go_tgts: AllGoTargets) -> ImportPathToPackages: mapping: dict[str, list[Address]] = defaultdict(list) + first_party_addresses = [] + first_party_gets = [] for tgt in go_tgts: - import_path = tgt[GoImportPathField].value - mapping[import_path].append(tgt.address) + if tgt.has_field(GoImportPathField): + import_path = tgt[GoImportPathField].value + mapping[import_path].append(tgt.address) + else: + first_party_addresses.append(tgt.address) + first_party_gets.append( + Get(FirstPartyPkgImportPath, FirstPartyPkgImportPathRequest(tgt.address)) + ) + + first_party_import_paths = await MultiGet(first_party_gets) + for import_path_info, addr in zip(first_party_import_paths, first_party_addresses): + mapping[import_path_info.import_path].append(addr) + frozen_mapping = FrozenDict({ip: tuple(tgts) for ip, tgts in mapping.items()}) return ImportPathToPackages(frozen_mapping) @@ -217,15 +235,12 @@ async def generate_targets_from_go_mod( def create_first_party_package_tgt(dir: str) -> GoFirstPartyPackageTarget: subpath = fast_relpath(dir, generator_addr.spec_path) - import_path = f"{go_mod_info.import_path}/{subpath}" if subpath else go_mod_info.import_path return GoFirstPartyPackageTarget( { - GoImportPathField.alias: import_path, - GoFirstPartyPackageSubpathField.alias: subpath, GoFirstPartyPackageSourcesField.alias: tuple( sorted(os.path.join(subpath, f) for f in dir_to_filenames[dir]) - ), + ) }, # E.g. `src/go:mod#./subdir`. generator_addr.create_generated(f"./{subpath}"), diff --git a/src/python/pants/backend/go/target_type_rules_test.py b/src/python/pants/backend/go/target_type_rules_test.py index 38b28dd51ea..2c1bd59d428 100644 --- a/src/python/pants/backend/go/target_type_rules_test.py +++ b/src/python/pants/backend/go/target_type_rules_test.py @@ -20,7 +20,6 @@ GoBinaryMainPackageRequest, GoBinaryTarget, GoFirstPartyPackageSourcesField, - GoFirstPartyPackageSubpathField, GoFirstPartyPackageTarget, GoImportPathField, GoModTarget, @@ -186,13 +185,7 @@ def test_generate_package_targets(rule_runner: RuleRunner) -> None: def gen_first_party_tgt(rel_dir: str, sources: list[str]) -> GoFirstPartyPackageTarget: return GoFirstPartyPackageTarget( - { - GoImportPathField.alias: ( - os.path.join("example.com/src/go", rel_dir) if rel_dir else "example.com/src/go" - ), - GoFirstPartyPackageSubpathField.alias: rel_dir, - GoFirstPartyPackageSourcesField.alias: tuple(sources), - }, + {GoFirstPartyPackageSourcesField.alias: tuple(sources)}, Address("src/go", generated_name=f"./{rel_dir}"), residence_dir=os.path.join("src/go", rel_dir).rstrip("/"), ) @@ -234,10 +227,7 @@ def gen_third_party_tgt(import_path: str) -> GoThirdPartyPackageTarget: def test_package_targets_cannot_be_manually_created() -> None: with pytest.raises(InvalidTargetException): - GoFirstPartyPackageTarget( - {GoImportPathField.alias: "foo", GoFirstPartyPackageSubpathField.alias: "foo"}, - Address("foo"), - ) + GoFirstPartyPackageTarget({}, Address("foo")) with pytest.raises(InvalidTargetException): GoThirdPartyPackageTarget( {GoImportPathField.alias: "foo"}, diff --git a/src/python/pants/backend/go/target_types.py b/src/python/pants/backend/go/target_types.py index 95a15bf9542..7c956b8be09 100644 --- a/src/python/pants/backend/go/target_types.py +++ b/src/python/pants/backend/go/target_types.py @@ -25,17 +25,6 @@ ) from pants.option.global_options import FilesNotFoundBehavior - -class GoImportPathField(StringField): - alias = "import_path" - help = ( - "Import path in Go code to import this package.\n\n" - "This field should not be overridden; use the value from target generation." - ) - required = True - value: str - - # ----------------------------------------------------------------------------------------------- # `go_mod` target generator # ----------------------------------------------------------------------------------------------- @@ -143,22 +132,10 @@ class GoFirstPartyPackageDependenciesField(Dependencies): pass -class GoFirstPartyPackageSubpathField(StringField, AsyncFieldMixin): - alias = "subpath" - help = ( - "The path from the owning `go.mod` to this package's directory, e.g. `subdir`.\n\n" - "This field should not be overridden; use the value from target generation." - ) - required = True - value: str - - class GoFirstPartyPackageTarget(Target): alias = "go_first_party_package" core_fields = ( *COMMON_TARGET_FIELDS, - GoImportPathField, - GoFirstPartyPackageSubpathField, GoFirstPartyPackageDependenciesField, GoFirstPartyPackageSourcesField, ) @@ -184,6 +161,16 @@ def validate(self) -> None: # ----------------------------------------------------------------------------------------------- +class GoImportPathField(StringField): + alias = "import_path" + help = ( + "Import path in Go code to import this package.\n\n" + "This field should not be overridden; use the value from target generation." + ) + required = True + value: str + + class GoThirdPartyPackageDependenciesField(Dependencies): pass diff --git a/src/python/pants/backend/go/util_rules/build_pkg_target.py b/src/python/pants/backend/go/util_rules/build_pkg_target.py index 5aa3fbd5b62..c1e596c0a0f 100644 --- a/src/python/pants/backend/go/util_rules/build_pkg_target.py +++ b/src/python/pants/backend/go/util_rules/build_pkg_target.py @@ -4,12 +4,10 @@ from __future__ import annotations import dataclasses -import os from dataclasses import dataclass from pants.backend.go.target_types import ( GoFirstPartyPackageSourcesField, - GoFirstPartyPackageSubpathField, GoImportPathField, GoThirdPartyPackageDependenciesField, ) @@ -52,7 +50,6 @@ async def setup_build_go_package_target_request( ) -> FallibleBuildGoPackageRequest: wrapped_target = await Get(WrappedTarget, Address, request.address) target = wrapped_target.target - import_path = target[GoImportPathField].value if target.has_field(GoFirstPartyPackageSourcesField): _maybe_first_party_pkg_info = await Get( @@ -61,16 +58,15 @@ async def setup_build_go_package_target_request( if _maybe_first_party_pkg_info.info is None: return FallibleBuildGoPackageRequest( None, - import_path, + _maybe_first_party_pkg_info.import_path, exit_code=_maybe_first_party_pkg_info.exit_code, stderr=_maybe_first_party_pkg_info.stderr, ) _first_party_pkg_info = _maybe_first_party_pkg_info.info digest = _first_party_pkg_info.digest - subpath = os.path.join( - target.address.spec_path, target[GoFirstPartyPackageSubpathField].value - ) + import_path = _first_party_pkg_info.import_path + subpath = _first_party_pkg_info.subpath minimum_go_version = _first_party_pkg_info.minimum_go_version go_file_names = _first_party_pkg_info.go_files @@ -82,6 +78,8 @@ async def setup_build_go_package_target_request( s_file_names = _first_party_pkg_info.s_files elif target.has_field(GoThirdPartyPackageDependenciesField): + import_path = target[GoImportPathField].value + _go_mod_address = target.address.maybe_convert_to_target_generator() _go_mod_info = await Get(GoModInfo, GoModInfoRequest(_go_mod_address)) _third_party_pkg_info = await Get( diff --git a/src/python/pants/backend/go/util_rules/first_party_pkg.py b/src/python/pants/backend/go/util_rules/first_party_pkg.py index 7af16ca1d6c..47df04a9757 100644 --- a/src/python/pants/backend/go/util_rules/first_party_pkg.py +++ b/src/python/pants/backend/go/util_rules/first_party_pkg.py @@ -10,11 +10,7 @@ from dataclasses import dataclass from typing import ClassVar -from pants.backend.go.target_types import ( - GoFirstPartyPackageSourcesField, - GoFirstPartyPackageSubpathField, - GoImportPathField, -) +from pants.backend.go.target_types import GoFirstPartyPackageSourcesField from pants.backend.go.util_rules.build_pkg import BuildGoPackageRequest, BuiltGoPackage from pants.backend.go.util_rules.go_mod import GoModInfo, GoModInfoRequest from pants.backend.go.util_rules.import_analysis import ImportConfig, ImportConfigRequest @@ -30,18 +26,39 @@ logger = logging.getLogger(__name__) +@dataclass(frozen=True) +class FirstPartyPkgImportPath: + """The derived import path of a first party package, based on its owning go.mod. + + Use `FirstPartyPkgInfo` instead for more detailed information like parsed imports. + """ + + import_path: str + subpath: str + + +@dataclass(frozen=True) +class FirstPartyPkgImportPathRequest(EngineAwareParameter): + address: Address + + def debug_hint(self) -> str: + return self.address.spec + + @dataclass(frozen=True) class FirstPartyPkgInfo: """All the info and digest needed to build a first-party Go package. - The digest does not strip its source files. You must set `working_dir` appropriately to use the - `go_first_party_package` target's `subpath` field. + The digest does not strip its source files. You must set `working_dir` appropriately to use + the `subpath`. + + Use `FirstPartyPkgImportPath` if you only need the derived import path. """ digest: Digest - subpath: str import_path: str + subpath: str imports: tuple[str, ...] test_imports: tuple[str, ...] @@ -78,6 +95,20 @@ def debug_hint(self) -> str: return self.address.spec +@rule +async def compute_first_party_package_import_path( + request: FirstPartyPkgImportPathRequest, +) -> FirstPartyPkgImportPath: + go_mod_address = request.address.maybe_convert_to_target_generator() + + # The generated_name will have been set to `./{subpath}`. + subpath = request.address.generated_name[2:] # type: ignore[index] + + go_mod_info = await Get(GoModInfo, GoModInfoRequest(go_mod_address)) + import_path = f"{go_mod_info.import_path}/{subpath}" if subpath else go_mod_info.import_path + return FirstPartyPkgImportPath(import_path, subpath) + + @dataclass(frozen=True) class PackageAnalyzerSetup: digest: Digest @@ -89,23 +120,22 @@ async def compute_first_party_package_info( request: FirstPartyPkgInfoRequest, analyzer: PackageAnalyzerSetup ) -> FallibleFirstPartyPkgInfo: go_mod_address = request.address.maybe_convert_to_target_generator() - wrapped_target, go_mod_info = await MultiGet( + wrapped_target, import_path_info, go_mod_info = await MultiGet( Get(WrappedTarget, Address, request.address), + Get(FirstPartyPkgImportPath, FirstPartyPkgImportPathRequest(request.address)), Get(GoModInfo, GoModInfoRequest(go_mod_address)), ) - target = wrapped_target.target - import_path = target[GoImportPathField].value - subpath = target[GoFirstPartyPackageSubpathField].value pkg_sources = await Get( - HydratedSources, HydrateSourcesRequest(target[GoFirstPartyPackageSourcesField]) + HydratedSources, + HydrateSourcesRequest(wrapped_target.target[GoFirstPartyPackageSourcesField]), ) input_digest = await Get( Digest, MergeDigests([pkg_sources.snapshot.digest, analyzer.digest]), ) path = request.address.spec_path if request.address.spec_path else "." - path = os.path.join(path, subpath) if subpath else path + path = os.path.join(path, import_path_info.subpath) if import_path_info.subpath else path if not path: path = "." result = await Get( @@ -120,7 +150,7 @@ async def compute_first_party_package_info( if result.exit_code != 0: return FallibleFirstPartyPkgInfo( info=None, - import_path=import_path, + import_path=import_path_info.import_path, exit_code=result.exit_code, stderr=result.stderr.decode("utf-8"), ) @@ -137,7 +167,7 @@ async def compute_first_party_package_info( ) error += "\n" return FallibleFirstPartyPkgInfo( - info=None, import_path=import_path, exit_code=1, stderr=error + info=None, import_path=import_path_info.import_path, exit_code=1, stderr=error ) if "CgoFiles" in metadata: @@ -150,8 +180,8 @@ async def compute_first_party_package_info( info = FirstPartyPkgInfo( digest=pkg_sources.snapshot.digest, - subpath=os.path.join(target.address.spec_path, subpath), - import_path=import_path, + subpath=os.path.join(request.address.spec_path, import_path_info.subpath), + import_path=import_path_info.import_path, imports=tuple(metadata.get("Imports", [])), test_imports=tuple(metadata.get("TestImports", [])), xtest_imports=tuple(metadata.get("XTestImports", [])), @@ -164,7 +194,7 @@ async def compute_first_party_package_info( test_embed_patterns=tuple(metadata.get("TestEmbedPatterns", [])), xtest_embed_patterns=tuple(metadata.get("XTestEmbedPatterns", [])), ) - return FallibleFirstPartyPkgInfo(info, import_path) + return FallibleFirstPartyPkgInfo(info, import_path_info.import_path) @rule diff --git a/src/python/pants/backend/go/util_rules/first_party_pkg_test.py b/src/python/pants/backend/go/util_rules/first_party_pkg_test.py index b3732c18d54..51b0d1f0cb1 100644 --- a/src/python/pants/backend/go/util_rules/first_party_pkg_test.py +++ b/src/python/pants/backend/go/util_rules/first_party_pkg_test.py @@ -21,6 +21,8 @@ ) from pants.backend.go.util_rules.first_party_pkg import ( FallibleFirstPartyPkgInfo, + FirstPartyPkgImportPath, + FirstPartyPkgImportPathRequest, FirstPartyPkgInfoRequest, ) from pants.engine.addresses import Address @@ -42,6 +44,7 @@ def rule_runner() -> RuleRunner: *link.rules(), *assembly.rules(), QueryRule(FallibleFirstPartyPkgInfo, [FirstPartyPkgInfoRequest]), + QueryRule(FirstPartyPkgImportPath, [FirstPartyPkgImportPathRequest]), ], target_types=[GoModTarget], ) @@ -49,6 +52,35 @@ def rule_runner() -> RuleRunner: return rule_runner +@pytest.mark.parametrize("mod_dir", ("", "src/go/")) +def test_import_path(rule_runner: RuleRunner, mod_dir: str) -> None: + rule_runner.write_files( + { + f"{mod_dir}BUILD": "go_mod(name='mod')\n", + f"{mod_dir}go.mod": "module go.example.com/foo", + f"{mod_dir}f.go": "", + f"{mod_dir}dir/f.go": "", + } + ) + info = rule_runner.request( + FirstPartyPkgImportPath, + [FirstPartyPkgImportPathRequest(Address(mod_dir, target_name="mod", generated_name="./"))], + ) + assert info.import_path == "go.example.com/foo" + assert info.subpath == "" + + info = rule_runner.request( + FirstPartyPkgImportPath, + [ + FirstPartyPkgImportPathRequest( + Address(mod_dir, target_name="mod", generated_name="./dir") + ) + ], + ) + assert info.import_path == "go.example.com/foo/dir" + assert info.subpath == "dir" + + def test_package_info(rule_runner: RuleRunner) -> None: rule_runner.write_files( {