• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

pantsbuild / pants / 23177125175

17 Mar 2026 03:32AM UTC coverage: 52.677% (-40.3%) from 92.932%
23177125175

Pull #23177

github

web-flow
Merge 1824dfbf4 into 0b9fdfb0e
Pull Request #23177: Bump the gha-deps group across 1 directory with 4 updates

31687 of 60153 relevant lines covered (52.68%)

1.05 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.64
/src/python/pants/backend/go/util_rules/go_mod.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3
from __future__ import annotations
2✔
4

5
import json
2✔
6
import logging
2✔
7
import os
2✔
8
from dataclasses import dataclass
2✔
9

10
from pants.backend.go.target_types import (
2✔
11
    GoBinaryMainPackageField,
12
    GoModDependenciesField,
13
    GoModSourcesField,
14
    GoModTarget,
15
    GoOwningGoModAddressField,
16
    GoPackageSourcesField,
17
    GoThirdPartyPackageDependenciesField,
18
)
19
from pants.backend.go.util_rules import binary
2✔
20
from pants.backend.go.util_rules.binary import (
2✔
21
    GoBinaryMainPackageRequest,
22
    determine_main_pkg_for_go_binary,
23
)
24
from pants.backend.go.util_rules.sdk import GoSdkProcess
2✔
25
from pants.base.specs import AncestorGlobSpec, RawSpecs
2✔
26
from pants.build_graph.address import Address, AddressInput
2✔
27
from pants.engine.engine_aware import EngineAwareParameter
2✔
28
from pants.engine.fs import Digest
2✔
29
from pants.engine.internals.build_files import resolve_address
2✔
30
from pants.engine.internals.graph import hydrate_sources, resolve_target, resolve_unexpanded_targets
2✔
31
from pants.engine.process import fallible_to_exec_result_or_raise
2✔
32
from pants.engine.rules import collect_rules, implicitly, rule
2✔
33
from pants.engine.target import (
2✔
34
    AllUnexpandedTargets,
35
    HydrateSourcesRequest,
36
    InvalidTargetException,
37
    Targets,
38
    WrappedTargetRequest,
39
)
40
from pants.util.docutil import bin_name
2✔
41
from pants.util.logging import LogLevel
2✔
42

43
logger = logging.getLogger(__name__)
2✔
44

45

46
@dataclass(frozen=True)
2✔
47
class OwningGoModRequest(EngineAwareParameter):
2✔
48
    address: Address
2✔
49

50
    def debug_hint(self) -> str:
2✔
51
        return self.address.spec
×
52

53

54
@dataclass(frozen=True)
2✔
55
class OwningGoMod:
2✔
56
    address: Address
2✔
57

58

59
@dataclass(frozen=True)
2✔
60
class NearestAncestorGoModRequest(EngineAwareParameter):
2✔
61
    address: Address
2✔
62

63
    def debug_hint(self) -> str | None:
2✔
64
        return self.address.spec
×
65

66

67
@dataclass(frozen=True)
2✔
68
class NearestAncestorGoModResult:
2✔
69
    address: Address
2✔
70

71

72
class AllGoModTargets(Targets):
2✔
73
    pass
2✔
74

75

76
@rule(desc="Find all `go_mod` targets in project", level=LogLevel.DEBUG)
2✔
77
async def find_all_go_mod_targets(targets: AllUnexpandedTargets) -> AllGoModTargets:
2✔
78
    return AllGoModTargets(tgt for tgt in targets if tgt.has_field(GoModDependenciesField))
2✔
79

80

81
@rule
2✔
82
async def find_nearest_ancestor_go_mod(
2✔
83
    request: NearestAncestorGoModRequest,
84
) -> NearestAncestorGoModResult:
85
    # We don't expect `go_mod` targets to be generated, so we can use UnexpandedTargets.
86
    candidate_targets = await resolve_unexpanded_targets(
2✔
87
        **implicitly(
88
            RawSpecs(
89
                ancestor_globs=(AncestorGlobSpec(request.address.spec_path),),
90
                description_of_origin="the `OwningGoMod` rule",
91
            )
92
        )
93
    )
94

95
    # Sort by address.spec_path in descending order so the nearest go_mod target is sorted first.
96
    go_mod_targets = sorted(
2✔
97
        (tgt for tgt in candidate_targets if tgt.has_field(GoModSourcesField)),
98
        key=lambda tgt: tgt.address.spec_path,
99
        reverse=True,
100
    )
101

102
    if not go_mod_targets:
2✔
103
        raise InvalidTargetException(
×
104
            f"The target {request.address} does not have a `go_mod` target in its BUILD file or "
105
            "any ancestor BUILD files. To fix, please make sure your project has a `go.mod` file "
106
            f"and add a `go_mod` target (you can run `{bin_name()} tailor` to do this)."
107
        )
108

109
    return NearestAncestorGoModResult(go_mod_targets[0].address)
2✔
110

111

112
async def _find_explict_owning_go_mod_address(
2✔
113
    address: Address,
114
    field: GoOwningGoModAddressField,
115
    alias: str,
116
    all_go_mod_targets: AllGoModTargets,
117
) -> Address:
118
    # If no value is specified, then see if there is only one `go_mod` target in this repository.
119
    if field.value is None:
×
120
        # If so, that is the default.
121
        if len(all_go_mod_targets) == 1:
×
122
            return all_go_mod_targets[0].address
×
123

124
        # Otherwise error and inform user to specify the owning `go_mod` target's address.
125
        if not all_go_mod_targets:
×
126
            raise InvalidTargetException(
×
127
                f"The `{alias}` target `{address}` requires that the address of an owning `{GoModTarget.alias}` "
128
                f"target be given via the `{GoOwningGoModAddressField.alias}` field since it is "
129
                "a dependency of a Go target. However, there are no `go_mod` targets in this repository."
130
            )
131

132
        raise InvalidTargetException(
×
133
            f"The `{alias}` target `{address}` requires that the address of an owning `{GoModTarget.alias}` "
134
            f"target be given via the `{GoOwningGoModAddressField.alias}` field since it is "
135
            "a dependency of a Go target. However, there are multiple `go_mod` targets in this repository "
136
            "which makes the choice of which `go_mod` target to use ambiguous. Please specify which of the "
137
            "following addresses to use or consider using the `parametrize` builtin to specify more than "
138
            "one of these addresses if this target will be used in multiple Go modules: "
139
            f"{', '.join([str(tgt.address) for tgt in all_go_mod_targets])}"
140
        )
141

142
    # If a value is specified, then resolve it as an address.
143
    address_input = AddressInput.parse(
×
144
        field.value,
145
        relative_to=address.spec_path,
146
        description_of_origin=f"the `{GoOwningGoModAddressField.alias}` field of target `{address}`",
147
    )
148
    candidate_go_mod_address = await resolve_address(**implicitly({address_input: AddressInput}))
×
149
    wrapped_target = await resolve_target(
×
150
        WrappedTargetRequest(
151
            candidate_go_mod_address,
152
            description_of_origin=f"the `{GoOwningGoModAddressField.alias}` field of target `{address}`",
153
        ),
154
        **implicitly(),
155
    )
156
    if not wrapped_target.target.has_field(GoModDependenciesField):
×
157
        raise InvalidTargetException(
×
158
            f"The `{alias}` target `{address}` requires that the address of an owning `{GoModTarget.alias}` "
159
            f"target be given via the `{GoOwningGoModAddressField.alias}` field since it is "
160
            f"a dependency of a Go target. However, the provided address `{field.value}` does not refer to "
161
            f"a `{GoModTarget.alias}` target. Please specify which of the following addresses to use or consider "
162
            "using the `parametrize` builtin to specify more than one of these addresses if this target will be "
163
            f"used in multiple Go modules: {', '.join([str(tgt.address) for tgt in all_go_mod_targets])}"
164
        )
165
    return candidate_go_mod_address
×
166

167

168
@rule
2✔
169
async def find_owning_go_mod(
2✔
170
    request: OwningGoModRequest, all_go_mod_targets: AllGoModTargets
171
) -> OwningGoMod:
172
    wrapped_target = await resolve_target(
2✔
173
        WrappedTargetRequest(request.address, description_of_origin="the `OwningGoMod` rule"),
174
        **implicitly(),
175
    )
176
    target = wrapped_target.target
2✔
177

178
    if target.has_field(GoModDependenciesField):
2✔
179
        return OwningGoMod(request.address)
2✔
180

181
    if target.has_field(GoPackageSourcesField):
2✔
182
        nearest_go_mod_result = await find_nearest_ancestor_go_mod(
2✔
183
            NearestAncestorGoModRequest(request.address)
184
        )
185
        return OwningGoMod(nearest_go_mod_result.address)
2✔
186

187
    if target.has_field(GoThirdPartyPackageDependenciesField):
×
188
        # For `go_third_party_package` targets, use the generator which is the owning `go_mod` target.
189
        generator_address = target.address.maybe_convert_to_target_generator()
×
190
        return OwningGoMod(generator_address)
×
191

192
    if target.has_field(GoBinaryMainPackageField):
×
193
        main_pkg = await determine_main_pkg_for_go_binary(
×
194
            GoBinaryMainPackageRequest(target.get(GoBinaryMainPackageField))
195
        )
196
        owning_go_mod_for_main_pkg = await find_owning_go_mod(
×
197
            OwningGoModRequest(main_pkg.address), **implicitly()
198
        )
199
        return owning_go_mod_for_main_pkg
×
200

201
    if target.has_field(GoOwningGoModAddressField):
×
202
        # Otherwise, find any explicitly defined go_mod address (e.g., for `protobuf_sources` targets).
203
        explicit_go_mod_address = await _find_explict_owning_go_mod_address(
×
204
            address=request.address,
205
            field=target.get(GoOwningGoModAddressField),
206
            alias=target.alias,
207
            all_go_mod_targets=all_go_mod_targets,
208
        )
209
        return OwningGoMod(explicit_go_mod_address)
×
210

211
    raise AssertionError(
×
212
        f"Internal error: Unable to determine how to determine the owning `{GoModTarget.alias}` target "
213
        f"for `{target.alias}` target `{target.address}`. Please file an issue at "
214
        "https://github.com/pantsbuild/pants/issues/new."
215
    )
216

217

218
@dataclass(frozen=True)
2✔
219
class GoModInfo:
2✔
220
    # Import path of the Go module, based on the `module` in `go.mod`.
221
    import_path: str
2✔
222
    digest: Digest
2✔
223
    mod_path: str
2✔
224
    minimum_go_version: str | None
2✔
225

226

227
@dataclass(frozen=True)
2✔
228
class GoModInfoRequest(EngineAwareParameter):
2✔
229
    source: Address | GoModSourcesField
2✔
230

231
    def debug_hint(self) -> str:
2✔
232
        if isinstance(self.source, Address):
×
233
            return self.source.spec
×
234
        else:
235
            return self.source.address.spec
×
236

237

238
@rule
2✔
239
async def determine_go_mod_info(
2✔
240
    request: GoModInfoRequest,
241
) -> GoModInfo:
242
    if isinstance(request.source, Address):
2✔
243
        wrapped_target = await resolve_target(
2✔
244
            WrappedTargetRequest(request.source, description_of_origin="<go mod info rule>"),
245
            **implicitly(),
246
        )
247
        sources_field = wrapped_target.target[GoModSourcesField]
2✔
248
    else:
249
        sources_field = request.source
2✔
250
    go_mod_path = sources_field.go_mod_path
2✔
251
    go_mod_dir = os.path.dirname(go_mod_path)
2✔
252

253
    # Get the `go.mod` (and `go.sum`) and strip so the file has no directory prefix.
254
    hydrated_sources = await hydrate_sources(HydrateSourcesRequest(sources_field), **implicitly())
2✔
255
    sources_digest = hydrated_sources.snapshot.digest
2✔
256

257
    mod_json = await fallible_to_exec_result_or_raise(
2✔
258
        **implicitly(
259
            GoSdkProcess(
260
                command=("mod", "edit", "-json"),
261
                input_digest=sources_digest,
262
                working_dir=go_mod_dir,
263
                description=f"Parse {go_mod_path}",
264
            )
265
        )
266
    )
267
    module_metadata = json.loads(mod_json.stdout)
2✔
268
    return GoModInfo(
2✔
269
        import_path=module_metadata["Module"]["Path"],
270
        digest=sources_digest,
271
        mod_path=go_mod_path,
272
        minimum_go_version=module_metadata.get("Go"),
273
    )
274

275

276
def rules():
2✔
277
    return (
2✔
278
        *collect_rules(),
279
        *binary.rules(),
280
    )
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc