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

pantsbuild / pants / 19529437518

20 Nov 2025 07:44AM UTC coverage: 78.884% (-1.4%) from 80.302%
19529437518

push

github

web-flow
nfpm.native_libs: Add RPM package depends from packaged pex_binaries (#22899)

## PR Series Overview

This is the second in a series of PRs that introduces a new backend:
`pants.backend.npm.native_libs`
Initially, the backend will be available as:
`pants.backend.experimental.nfpm.native_libs`

I proposed this new backend (originally named `bindeps`) in discussion
#22396.

This backend will inspect ELF bin/lib files (like `lib*.so`) in packaged
contents (for this PR series, only in `pex_binary` targets) to identify
package dependency metadata and inject that metadata on the relevant
`nfpm_deb_package` or `nfpm_rpm_package` targets. Effectively, it will
provide an approximation of these native packager features:
- `rpm`: `rpmdeps` + `elfdeps`
- `deb`: `dh_shlibdeps` + `dpkg-shlibdeps` (These substitute
`${shlibs:Depends}` in debian control files have)

### Goal: Host-agnostic package builds

This pants backend is designed to be host-agnostic, like
[nFPM](https://nfpm.goreleaser.com/).

Native packaging tools are often restricted to a single release of a
single distro. Unlike native package builders, this new pants backend
does not use any of those distro-specific or distro-release-specific
utilities or local package databases. This new backend should be able to
run (help with building deb and rpm packages) anywhere that pants can
run (MacOS, rpm linux distros, deb linux distros, other linux distros,
docker, ...).

### Previous PRs in series

- #22873

## PR Overview

This PR adds rules in `nfpm.native_libs` to add package dependency
metadata to `nfpm_rpm_package`. The 2 new rules are:

- `inject_native_libs_dependencies_in_package_fields`:

    - An implementation of the polymorphic rule `inject_nfpm_package_fields`.
      This rule is low priority (`priority = 2`) so that in-repo plugins can
      override/augment what it injects. (See #22864)

    - Rule logic overview:
        - find any pex_binaries that will be packaged in an `nfpm_rpm_package`
   ... (continued)

96 of 118 new or added lines in 3 files covered. (81.36%)

910 existing lines in 53 files now uncovered.

73897 of 93678 relevant lines covered (78.88%)

3.21 hits per line

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

0.0
/src/python/pants/backend/codegen/protobuf/go/rules.py
1
# Copyright 2021 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
UNCOV
3
from __future__ import annotations
×
4

UNCOV
5
import logging
×
UNCOV
6
import os
×
UNCOV
7
import re
×
UNCOV
8
import textwrap
×
UNCOV
9
from collections import defaultdict
×
UNCOV
10
from dataclasses import dataclass
×
11

UNCOV
12
from pants.backend.codegen.protobuf import protoc
×
UNCOV
13
from pants.backend.codegen.protobuf.protoc import Protoc
×
UNCOV
14
from pants.backend.codegen.protobuf.target_types import (
×
15
    AllProtobufTargets,
16
    ProtobufGrpcToggleField,
17
    ProtobufSourceField,
18
    ProtobufSourcesGeneratorTarget,
19
    ProtobufSourceTarget,
20
)
UNCOV
21
from pants.backend.go import target_type_rules
×
UNCOV
22
from pants.backend.go.dependency_inference import (
×
23
    GoImportPathsMappingAddressSet,
24
    GoModuleImportPathsMapping,
25
    GoModuleImportPathsMappings,
26
    GoModuleImportPathsMappingsHook,
27
)
UNCOV
28
from pants.backend.go.target_type_rules import (
×
29
    GoImportPathMappingRequest,
30
    map_import_paths_to_packages,
31
)
UNCOV
32
from pants.backend.go.target_types import GoOwningGoModAddressField, GoPackageSourcesField
×
UNCOV
33
from pants.backend.go.util_rules import (
×
34
    assembly,
35
    build_pkg,
36
    build_pkg_target,
37
    first_party_pkg,
38
    go_mod,
39
    link,
40
    sdk,
41
    third_party_pkg,
42
)
UNCOV
43
from pants.backend.go.util_rules.build_opts import GoBuildOptions
×
UNCOV
44
from pants.backend.go.util_rules.build_pkg import (
×
45
    BuildGoPackageRequest,
46
    FallibleBuildGoPackageRequest,
47
)
UNCOV
48
from pants.backend.go.util_rules.build_pkg_target import (
×
49
    BuildGoPackageTargetRequest,
50
    GoCodegenBuildRequest,
51
    required_build_go_package_request,
52
)
UNCOV
53
from pants.backend.go.util_rules.first_party_pkg import FallibleFirstPartyPkgAnalysis
×
UNCOV
54
from pants.backend.go.util_rules.go_mod import OwningGoModRequest, find_owning_go_mod
×
UNCOV
55
from pants.backend.go.util_rules.pkg_analyzer import PackageAnalyzerSetup
×
UNCOV
56
from pants.backend.go.util_rules.sdk import GoSdkProcess
×
UNCOV
57
from pants.backend.python.util_rules import pex
×
UNCOV
58
from pants.build_graph.address import Address
×
UNCOV
59
from pants.core.util_rules.external_tool import download_external_tool
×
UNCOV
60
from pants.core.util_rules.source_files import SourceFilesRequest, determine_source_files
×
UNCOV
61
from pants.core.util_rules.stripped_source_files import strip_source_roots
×
UNCOV
62
from pants.engine.fs import (
×
63
    AddPrefix,
64
    CreateDigest,
65
    Digest,
66
    Directory,
67
    FileContent,
68
    MergeDigests,
69
    RemovePrefix,
70
)
UNCOV
71
from pants.engine.internals.graph import hydrate_sources, resolve_source_paths, transitive_targets
×
UNCOV
72
from pants.engine.internals.native_engine import EMPTY_DIGEST
×
UNCOV
73
from pants.engine.internals.selectors import concurrently
×
UNCOV
74
from pants.engine.intrinsics import (
×
75
    create_digest,
76
    digest_to_snapshot,
77
    execute_process,
78
    get_digest_contents,
79
    merge_digests,
80
    remove_prefix,
81
)
UNCOV
82
from pants.engine.platform import Platform
×
UNCOV
83
from pants.engine.process import Process, fallible_to_exec_result_or_raise
×
UNCOV
84
from pants.engine.rules import collect_rules, implicitly, rule
×
UNCOV
85
from pants.engine.target import (
×
86
    GeneratedSources,
87
    GenerateSourcesRequest,
88
    HydrateSourcesRequest,
89
    SourcesPathsRequest,
90
    TransitiveTargetsRequest,
91
)
UNCOV
92
from pants.engine.unions import UnionRule
×
UNCOV
93
from pants.source.source_root import (
×
94
    SourceRootRequest,
95
    SourceRootsRequest,
96
    get_source_root,
97
    get_source_roots,
98
)
UNCOV
99
from pants.util.dirutil import group_by_dir
×
UNCOV
100
from pants.util.frozendict import FrozenDict
×
UNCOV
101
from pants.util.logging import LogLevel
×
UNCOV
102
from pants.util.strutil import softwrap
×
103

UNCOV
104
_logger = logging.getLogger(__name__)
×
105

106

UNCOV
107
class GoCodegenBuildProtobufRequest(GoCodegenBuildRequest):
×
UNCOV
108
    generate_from = ProtobufSourceField
×
109

110

UNCOV
111
class GenerateGoFromProtobufRequest(GenerateSourcesRequest):
×
UNCOV
112
    input = ProtobufSourceField
×
UNCOV
113
    output = GoPackageSourcesField
×
114

115

UNCOV
116
@dataclass(frozen=True)
×
UNCOV
117
class _SetupGoProtocPlugin:
×
UNCOV
118
    digest: Digest
×
119

120

UNCOV
121
_QUOTE_CHAR = r"(?:'|\")"
×
UNCOV
122
_IMPORT_PATH_RE = re.compile(rf"^\s*option\s+go_package\s+=\s+{_QUOTE_CHAR}(.*){_QUOTE_CHAR};")
×
123

124

UNCOV
125
def parse_go_package_option(content_raw: bytes) -> str | None:
×
UNCOV
126
    content = content_raw.decode()
×
UNCOV
127
    for line in content.splitlines():
×
UNCOV
128
        m = _IMPORT_PATH_RE.match(line)
×
UNCOV
129
        if m:
×
UNCOV
130
            return m.group(1)
×
131
    return None
×
132

133

UNCOV
134
class ProtobufGoModuleImportPathsMappingsHook(GoModuleImportPathsMappingsHook):
×
UNCOV
135
    pass
×
136

137

UNCOV
138
@rule(desc="Map import paths for all Go Protobuf targets.", level=LogLevel.DEBUG)
×
UNCOV
139
async def map_import_paths_of_all_go_protobuf_targets(
×
140
    _request: ProtobufGoModuleImportPathsMappingsHook,
141
    all_protobuf_targets: AllProtobufTargets,
142
) -> GoModuleImportPathsMappings:
143
    sources = await concurrently(
×
144
        hydrate_sources(
145
            HydrateSourcesRequest(
146
                tgt[ProtobufSourceField],
147
                for_sources_types=(ProtobufSourceField,),
148
                enable_codegen=True,
149
            ),
150
            **implicitly(),
151
        )
152
        for tgt in all_protobuf_targets
153
    )
154

155
    all_contents = await concurrently(
×
156
        get_digest_contents(source.snapshot.digest) for source in sources
157
    )
158

159
    go_protobuf_mapping_metadata = []
×
160
    owning_go_mod_gets = []
×
161
    for tgt, contents in zip(all_protobuf_targets, all_contents):
×
162
        if not contents:
×
163
            continue
×
164
        if len(contents) > 1:
×
165
            raise AssertionError(
×
166
                f"Protobuf target `{tgt.address}` mapped to more than one source file."
167
            )
168

169
        import_path = parse_go_package_option(contents[0].content)
×
170
        if not import_path:
×
171
            continue
×
172

173
        owning_go_mod_gets.append(
×
174
            find_owning_go_mod(OwningGoModRequest(tgt.address), **implicitly())
175
        )
176
        go_protobuf_mapping_metadata.append((import_path, tgt.address))
×
177

178
    owning_go_mod_targets = await concurrently(owning_go_mod_gets)
×
179

180
    import_paths_by_module: dict[Address, dict[str, set[Address]]] = defaultdict(
×
181
        lambda: defaultdict(set)
182
    )
183

184
    for owning_go_mod, (import_path, address) in zip(
×
185
        owning_go_mod_targets, go_protobuf_mapping_metadata
186
    ):
187
        import_paths_by_module[owning_go_mod.address][import_path].add(address)
×
188

189
    return GoModuleImportPathsMappings(
×
190
        FrozenDict(
191
            {
192
                go_mod_addr: GoModuleImportPathsMapping(
193
                    mapping=FrozenDict(
194
                        {
195
                            import_path: GoImportPathsMappingAddressSet(
196
                                addresses=tuple(sorted(addresses)), infer_all=True
197
                            )
198
                            for import_path, addresses in import_path_mapping.items()
199
                        }
200
                    ),
201
                    address_to_import_path=FrozenDict(
202
                        {
203
                            address: import_path
204
                            for import_path, addresses in import_path_mapping.items()
205
                            for address in addresses
206
                        }
207
                    ),
208
                )
209
                for go_mod_addr, import_path_mapping in import_paths_by_module.items()
210
            }
211
        )
212
    )
213

214

UNCOV
215
@dataclass(frozen=True)
×
UNCOV
216
class _SetupGoProtobufPackageBuildRequest:
×
217
    """Request type used to trigger setup of a BuildGoPackageRequest for entire generated Go
218
    Protobuf package.
219

220
    This type is separate so that a build of the full package can be cached no matter which one of
221
    its component source files was requested. This occurs because a request to build any one of the
222
    source files will be converted into this type and then built.
223
    """
224

UNCOV
225
    addresses: tuple[Address, ...]
×
UNCOV
226
    import_path: str
×
UNCOV
227
    build_opts: GoBuildOptions
×
228

229

UNCOV
230
@rule
×
UNCOV
231
async def setup_full_package_build_request(
×
232
    request: _SetupGoProtobufPackageBuildRequest,
233
    protoc: Protoc,
234
    go_protoc_plugin: _SetupGoProtocPlugin,
235
    analyzer: PackageAnalyzerSetup,
236
    platform: Platform,
237
) -> FallibleBuildGoPackageRequest:
238
    output_dir = "_generated_files"
×
239
    protoc_relpath = "__protoc"
×
240
    protoc_go_plugin_relpath = "__protoc_gen_go"
×
241

242
    (
×
243
        transitive_targets_for_protobuf_source,
244
        downloaded_protoc_binary,
245
        empty_output_dir,
246
    ) = await concurrently(
247
        transitive_targets(TransitiveTargetsRequest(request.addresses), **implicitly()),
248
        download_external_tool(protoc.get_request(platform)),
249
        create_digest(CreateDigest([Directory(output_dir)])),
250
    )
251

252
    go_mod_addr = await find_owning_go_mod(
×
253
        OwningGoModRequest(transitive_targets_for_protobuf_source.roots[0].address), **implicitly()
254
    )
255
    package_mapping = await map_import_paths_to_packages(
×
256
        GoImportPathMappingRequest(go_mod_addr.address), **implicitly()
257
    )
258

259
    all_sources = await determine_source_files(
×
260
        SourceFilesRequest(
261
            sources_fields=(
262
                tgt[ProtobufSourceField]
263
                for tgt in transitive_targets_for_protobuf_source.closure
264
                if tgt.has_field(ProtobufSourceField)
265
            ),
266
            for_sources_types=(ProtobufSourceField,),
267
            enable_codegen=True,
268
        )
269
    )
270
    source_roots, input_digest = await concurrently(
×
271
        get_source_roots(SourceRootsRequest.for_files(all_sources.files)),
272
        merge_digests(MergeDigests([all_sources.snapshot.digest, empty_output_dir])),
273
    )
274

275
    source_root_paths = sorted({sr.path for sr in source_roots.path_to_root.values()})
×
276

277
    pkg_sources = await concurrently(
×
278
        resolve_source_paths(SourcesPathsRequest(tgt[ProtobufSourceField]), **implicitly())
279
        for tgt in transitive_targets_for_protobuf_source.roots
280
    )
281
    pkg_files = sorted({f for ps in pkg_sources for f in ps.files})
×
282

283
    maybe_grpc_plugin_args = []
×
284
    if any(
×
285
        tgt.get(ProtobufGrpcToggleField).value
286
        for tgt in transitive_targets_for_protobuf_source.roots
287
    ):
288
        maybe_grpc_plugin_args = [
×
289
            f"--go-grpc_out={output_dir}",
290
            "--go-grpc_opt=paths=source_relative",
291
        ]
292

293
    gen_result = await execute_process(
×
294
        Process(
295
            argv=[
296
                os.path.join(protoc_relpath, downloaded_protoc_binary.exe),
297
                f"--plugin=go={os.path.join('.', protoc_go_plugin_relpath, 'protoc-gen-go')}",
298
                f"--plugin=go-grpc={os.path.join('.', protoc_go_plugin_relpath, 'protoc-gen-go-grpc')}",
299
                f"--go_out={output_dir}",
300
                "--go_opt=paths=source_relative",
301
                *(f"--proto_path={source_root}" for source_root in source_root_paths),
302
                *maybe_grpc_plugin_args,
303
                *pkg_files,
304
            ],
305
            # Note: Necessary or else --plugin option needs absolute path.
306
            env={"PATH": protoc_go_plugin_relpath},
307
            input_digest=input_digest,
308
            immutable_input_digests={
309
                protoc_relpath: downloaded_protoc_binary.digest,
310
                protoc_go_plugin_relpath: go_protoc_plugin.digest,
311
            },
312
            description=f"Generating Go sources from {request.import_path}.",
313
            level=LogLevel.DEBUG,
314
            output_directories=(output_dir,),
315
        ),
316
        **implicitly(),
317
    )
318
    if gen_result.exit_code != 0:
×
319
        return FallibleBuildGoPackageRequest(
×
320
            request=None,
321
            import_path=request.import_path,
322
            exit_code=gen_result.exit_code,
323
            stderr=gen_result.stderr.decode(),
324
        )
325

326
    # Ensure that the generated files are in a single package directory.
327
    gen_sources = await digest_to_snapshot(gen_result.output_digest)
×
328
    files_by_dir = group_by_dir(gen_sources.files)
×
329
    if len(files_by_dir) != 1:
×
330
        return FallibleBuildGoPackageRequest(
×
331
            request=None,
332
            import_path=request.import_path,
333
            exit_code=1,
334
            stderr=textwrap.dedent(
335
                f"""
336
                Expected Go files generated from Protobuf sources to be output to a single directory.
337
                - import path: {request.import_path}
338
                - protobuf files: {", ".join(pkg_files)}
339
                """
340
            ).strip(),
341
        )
342
    gen_dir = list(files_by_dir.keys())[0]
×
343

344
    # Analyze the generated sources.
345
    input_digest = await merge_digests(MergeDigests([gen_sources.digest, analyzer.digest]))
×
346
    result = await execute_process(
×
347
        Process(
348
            (analyzer.path, gen_dir),
349
            input_digest=input_digest,
350
            description=f"Determine metadata for generated Go package for {request.import_path}",
351
            level=LogLevel.DEBUG,
352
            env={"CGO_ENABLED": "0"},  # protobuf files should not have cgo!
353
        ),
354
        **implicitly(),
355
    )
356

357
    # Parse the metadata from the analysis.
358
    fallible_analysis = FallibleFirstPartyPkgAnalysis.from_process_result(
×
359
        result,
360
        dir_path=gen_dir,
361
        import_path=request.import_path,
362
        minimum_go_version="",
363
        description_of_source=f"Go package generated from protobuf targets `{', '.join(str(addr) for addr in request.addresses)}`",
364
    )
365
    if not fallible_analysis.analysis:
×
366
        return FallibleBuildGoPackageRequest(
×
367
            request=None,
368
            import_path=request.import_path,
369
            exit_code=fallible_analysis.exit_code,
370
            stderr=fallible_analysis.stderr,
371
        )
372
    analysis = fallible_analysis.analysis
×
373

374
    # Obtain build requests for third-party dependencies.
375
    # TODO: Consider how to merge this code with existing dependency inference code.
376
    dep_build_request_addrs: set[Address] = set()
×
377
    for dep_import_path in (*analysis.imports, *analysis.test_imports, *analysis.xtest_imports):
×
378
        # Infer dependencies on other Go packages.
379
        candidate_addresses = package_mapping.mapping.get(dep_import_path)
×
380
        if candidate_addresses:
×
381
            # TODO: Use explicit dependencies to disambiguate? This should never happen with Go backend though.
382
            if candidate_addresses.infer_all:
×
383
                dep_build_request_addrs.update(candidate_addresses.addresses)
×
384
            else:
385
                if len(candidate_addresses.addresses) > 1:
×
386
                    return FallibleBuildGoPackageRequest(
×
387
                        request=None,
388
                        import_path=request.import_path,
389
                        exit_code=result.exit_code,
390
                        stderr=textwrap.dedent(
391
                            f"""
392
                            Multiple addresses match import of `{dep_import_path}`.
393

394
                            addresses: {", ".join(str(a) for a in candidate_addresses.addresses)}
395
                            """
396
                        ).strip(),
397
                    )
398
                dep_build_request_addrs.update(candidate_addresses.addresses)
×
399

400
    dep_build_requests = await concurrently(
×
401
        required_build_go_package_request(
402
            **implicitly(BuildGoPackageTargetRequest(addr, build_opts=request.build_opts))
403
        )
404
        for addr in sorted(dep_build_request_addrs)
405
    )
406

407
    return FallibleBuildGoPackageRequest(
×
408
        request=BuildGoPackageRequest(
409
            import_path=request.import_path,
410
            pkg_name=analysis.name,
411
            digest=gen_sources.digest,
412
            dir_path=analysis.dir_path,
413
            go_files=analysis.go_files,
414
            s_files=analysis.s_files,
415
            direct_dependencies=dep_build_requests,
416
            minimum_go_version=analysis.minimum_go_version,
417
            build_opts=request.build_opts,
418
        ),
419
        import_path=request.import_path,
420
    )
421

422

UNCOV
423
@rule
×
UNCOV
424
async def setup_build_go_package_request_for_protobuf(
×
425
    request: GoCodegenBuildProtobufRequest,
426
) -> FallibleBuildGoPackageRequest:
427
    # Hydrate the protobuf source to parse for the Go import path.
428
    sources = await hydrate_sources(
×
429
        HydrateSourcesRequest(request.target[ProtobufSourceField]), **implicitly()
430
    )
431
    sources_content = await get_digest_contents(sources.snapshot.digest)
×
432
    assert len(sources_content) == 1
×
433
    import_path = parse_go_package_option(sources_content[0].content)
×
434
    if not import_path:
×
435
        return FallibleBuildGoPackageRequest(
×
436
            request=None,
437
            import_path="",
438
            exit_code=1,
439
            stderr=f"No import path was set in Protobuf file via `option go_package` directive for {request.target.address}.",
440
        )
441

442
    go_mod_addr = await find_owning_go_mod(
×
443
        OwningGoModRequest(request.target.address), **implicitly()
444
    )
445
    package_mapping = await map_import_paths_to_packages(
×
446
        GoImportPathMappingRequest(go_mod_addr.address), **implicitly()
447
    )
448

449
    # Request the full build of the package. This indirection is necessary so that requests for two or more
450
    # Protobuf files in the same Go package result in a single cacheable rule invocation.
451
    protobuf_target_addrs_set_for_import_path = package_mapping.mapping.get(import_path)
×
452
    if not protobuf_target_addrs_set_for_import_path:
×
453
        return FallibleBuildGoPackageRequest(
×
454
            request=None,
455
            import_path=import_path,
456
            exit_code=1,
457
            stderr=softwrap(
458
                f"""
459
                No Protobuf files exists for import path `{import_path}`.
460
                Consider whether the import path was set correctly via the `option go_package` directive.
461
                """
462
            ),
463
        )
464

465
    return await setup_full_package_build_request(
×
466
        _SetupGoProtobufPackageBuildRequest(
467
            addresses=protobuf_target_addrs_set_for_import_path.addresses,
468
            import_path=import_path,
469
            build_opts=request.build_opts,
470
        ),
471
        **implicitly(),
472
    )
473

474

UNCOV
475
@rule(desc="Generate Go source files from Protobuf", level=LogLevel.DEBUG)
×
UNCOV
476
async def generate_go_from_protobuf(
×
477
    request: GenerateGoFromProtobufRequest,
478
    protoc: Protoc,
479
    go_protoc_plugin: _SetupGoProtocPlugin,
480
    platform: Platform,
481
) -> GeneratedSources:
482
    output_dir = "_generated_files"
×
483
    protoc_relpath = "__protoc"
×
484
    protoc_go_plugin_relpath = "__protoc_gen_go"
×
485

486
    (
×
487
        downloaded_protoc_binary,
488
        empty_output_dir,
489
        transitive_targets_for_protobuf_source,
490
    ) = await concurrently(
491
        download_external_tool(protoc.get_request(platform)),
492
        create_digest(CreateDigest([Directory(output_dir)])),
493
        transitive_targets(
494
            TransitiveTargetsRequest([request.protocol_target.address]), **implicitly()
495
        ),
496
    )
497

498
    # NB: By stripping the source roots, we avoid having to set the value `--proto_path`
499
    # for Protobuf imports to be discoverable.
500
    all_sources_stripped, target_sources_stripped = await concurrently(
×
501
        strip_source_roots(
502
            **implicitly(
503
                SourceFilesRequest(
504
                    tgt[ProtobufSourceField]
505
                    for tgt in transitive_targets_for_protobuf_source.closure
506
                    if tgt.has_field(ProtobufSourceField)
507
                )
508
            )
509
        ),
510
        strip_source_roots(
511
            **implicitly(SourceFilesRequest([request.protocol_target[ProtobufSourceField]]))
512
        ),
513
    )
514

515
    input_digest = await merge_digests(
×
516
        MergeDigests([all_sources_stripped.snapshot.digest, empty_output_dir])
517
    )
518

519
    maybe_grpc_plugin_args = []
×
520
    if request.protocol_target.get(ProtobufGrpcToggleField).value:
×
521
        maybe_grpc_plugin_args = [
×
522
            f"--go-grpc_out={output_dir}",
523
            "--go-grpc_opt=paths=source_relative",
524
        ]
525

526
    result = await fallible_to_exec_result_or_raise(
×
527
        **implicitly(
528
            Process(
529
                argv=[
530
                    os.path.join(protoc_relpath, downloaded_protoc_binary.exe),
531
                    f"--plugin=go={os.path.join('.', protoc_go_plugin_relpath, 'protoc-gen-go')}",
532
                    f"--plugin=go-grpc={os.path.join('.', protoc_go_plugin_relpath, 'protoc-gen-go-grpc')}",
533
                    f"--go_out={output_dir}",
534
                    "--go_opt=paths=source_relative",
535
                    *maybe_grpc_plugin_args,
536
                    *target_sources_stripped.snapshot.files,
537
                ],
538
                # Note: Necessary or else --plugin option needs absolute path.
539
                env={"PATH": protoc_go_plugin_relpath},
540
                input_digest=input_digest,
541
                immutable_input_digests={
542
                    protoc_relpath: downloaded_protoc_binary.digest,
543
                    protoc_go_plugin_relpath: go_protoc_plugin.digest,
544
                },
545
                description=f"Generating Go sources from {request.protocol_target.address}.",
546
                level=LogLevel.DEBUG,
547
                output_directories=(output_dir,),
548
            )
549
        )
550
    )
551

552
    normalized_digest, source_root = await concurrently(
×
553
        remove_prefix(RemovePrefix(result.output_digest, output_dir)),
554
        get_source_root(SourceRootRequest.for_target(request.protocol_target)),
555
    )
556

557
    source_root_restored = (
×
558
        await digest_to_snapshot(**implicitly(AddPrefix(normalized_digest, source_root.path)))
559
        if source_root.path != "."
560
        else await digest_to_snapshot(normalized_digest)
561
    )
562
    return GeneratedSources(source_root_restored)
×
563

564

565
# Note: The versions of the Go protoc and gRPC plugins are hard coded in the following go.mod. To update,
566
# copy the following go.mod and go.sum contents to go.mod and go.sum files in a new directory. Then update the
567
# versions and run `go mod download all`. Copy the go.mod and go.sum contents back into these constants,
568
# making sure to replace tabs with `\t`.
569

UNCOV
570
GO_PROTOBUF_GO_MOD = """\
×
571
module org.pantsbuild.backend.go.protobuf
572

573
go 1.17
574

575
require (
576
\tgoogle.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0
577
\tgoogle.golang.org/protobuf v1.27.1
578
)
579

580
require (
581
\tgithub.com/golang/protobuf v1.5.0 // indirect
582
\tgithub.com/google/go-cmp v0.5.5 // indirect
583
\tgolang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect
584
)
585
"""
586

UNCOV
587
GO_PROTOBUF_GO_SUM = """\
×
588
github.com/golang/protobuf v1.5.0 h1:LUVKkCeviFUMKqHa4tXIIij/lbhnMbP7Fn5wKdKkRh4=
589
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
590
github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU=
591
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
592
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4=
593
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
594
google.golang.org/grpc v1.2.0 h1:v8eFdETH8nqZHQ9x+0f2PLuU6W7zo5PFZuVEwH5126Y=
595
google.golang.org/grpc v1.2.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
596
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0 h1:TLkBREm4nIsEcexnCjgQd5GQWaHcqMzwQV0TX9pq8S0=
597
google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.2.0/go.mod h1:DNq5QpG7LJqD2AamLZ7zvKE0DEpVl2BSEVjFycAAjRY=
598
google.golang.org/protobuf v1.27.1 h1:SnqbnDw1V7RiZcXPx5MEeqPv2s79L9i7BJUlG/+RurQ=
599
google.golang.org/protobuf v1.27.1/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
600
"""
601

602

UNCOV
603
@rule
×
UNCOV
604
async def setup_go_protoc_plugin() -> _SetupGoProtocPlugin:
×
605
    go_mod_digest = await create_digest(
×
606
        CreateDigest(
607
            [
608
                FileContent("go.mod", GO_PROTOBUF_GO_MOD.encode()),
609
                FileContent("go.sum", GO_PROTOBUF_GO_SUM.encode()),
610
            ]
611
        )
612
    )
613

614
    download_sources_result = await fallible_to_exec_result_or_raise(
×
615
        **implicitly(
616
            GoSdkProcess(
617
                ["mod", "download", "all"],
618
                input_digest=go_mod_digest,
619
                output_directories=("gopath",),
620
                description="Download Go `protoc` plugin sources.",
621
                allow_downloads=True,
622
            )
623
        )
624
    )
625

626
    go_plugin_build_result, go_grpc_plugin_build_result = await concurrently(
×
627
        fallible_to_exec_result_or_raise(
628
            **implicitly(
629
                GoSdkProcess(
630
                    ["install", "google.golang.org/protobuf/cmd/protoc-gen-go@v1.27.1"],
631
                    input_digest=download_sources_result.output_digest,
632
                    output_files=["gopath/bin/protoc-gen-go"],
633
                    description="Build Go protobuf plugin for `protoc`.",
634
                    # Allow `go` to contact the Go module proxy since it will run its own build.
635
                    allow_downloads=True,
636
                )
637
            )
638
        ),
639
        fallible_to_exec_result_or_raise(
640
            **implicitly(
641
                GoSdkProcess(
642
                    [
643
                        "install",
644
                        "google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.2.0",
645
                    ],
646
                    input_digest=download_sources_result.output_digest,
647
                    output_files=["gopath/bin/protoc-gen-go-grpc"],
648
                    description="Build Go gRPC protobuf plugin for `protoc`.",
649
                    # Allow `go` to contact the Go module proxy since it will run its own build.
650
                    allow_downloads=True,
651
                )
652
            )
653
        ),
654
    )
655
    if go_plugin_build_result.output_digest == EMPTY_DIGEST:
×
656
        raise AssertionError(
×
657
            f"Failed to build protoc-gen-go:\n"
658
            f"stdout:\n{go_plugin_build_result.stdout.decode()}\n\n"
659
            f"stderr:\n{go_plugin_build_result.stderr.decode()}"
660
        )
661
    if go_grpc_plugin_build_result.output_digest == EMPTY_DIGEST:
×
662
        raise AssertionError(
×
663
            f"Failed to build protoc-gen-go-grpc:\n"
664
            f"stdout:\n{go_grpc_plugin_build_result.stdout.decode()}\n\n"
665
            f"stderr:\n{go_grpc_plugin_build_result.stderr.decode()}"
666
        )
667

668
    merged_output_digests = await merge_digests(
×
669
        MergeDigests(
670
            [go_plugin_build_result.output_digest, go_grpc_plugin_build_result.output_digest]
671
        )
672
    )
673
    plugin_digest = await remove_prefix(RemovePrefix(merged_output_digests, "gopath/bin"))
×
674
    return _SetupGoProtocPlugin(plugin_digest)
×
675

676

UNCOV
677
def rules():
×
UNCOV
678
    return (
×
679
        *collect_rules(),
680
        UnionRule(GenerateSourcesRequest, GenerateGoFromProtobufRequest),
681
        UnionRule(GoCodegenBuildRequest, GoCodegenBuildProtobufRequest),
682
        UnionRule(GoModuleImportPathsMappingsHook, ProtobufGoModuleImportPathsMappingsHook),
683
        ProtobufSourcesGeneratorTarget.register_plugin_field(GoOwningGoModAddressField),
684
        ProtobufSourceTarget.register_plugin_field(GoOwningGoModAddressField),
685
        *protoc.rules(),
686
        # Rules needed for this to pass src/python/pants/init/load_backends_integration_test.py:
687
        *assembly.rules(),
688
        *build_pkg.rules(),
689
        *build_pkg_target.rules(),
690
        *first_party_pkg.rules(),
691
        *go_mod.rules(),
692
        *link.rules(),
693
        *sdk.rules(),
694
        *target_type_rules.rules(),
695
        *third_party_pkg.rules(),
696
        *pex.rules(),
697
    )
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

© 2025 Coveralls, Inc