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

pantsbuild / pants / 25443604553

06 May 2026 03:05PM UTC coverage: 92.879% (-0.04%) from 92.915%
25443604553

push

github

web-flow
[pants_ng] Scaffolding for a pants_ng mode. (#23319)

In this mode the command line is parsed as an
NG invocation, and dispatched appropriately.

Of course at the moment there are no
implementations to dispatch to. That will follow.

This does expose a new option, `pants_ng` to users. 
There is a big warning not to set it, but we're not trying
to hide that we're working on a new thing, so I am
comfortable with this.

25 of 76 new or added lines in 9 files covered. (32.89%)

1294 existing lines in 76 files now uncovered.

92234 of 99306 relevant lines covered (92.88%)

4.05 hits per line

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

96.09
/src/python/pants/backend/go/util_rules/build_pkg.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
11✔
4

5
import dataclasses
11✔
6
import hashlib
11✔
7
import os.path
11✔
8
from collections import deque
11✔
9
from collections.abc import Iterable, Mapping
11✔
10
from dataclasses import dataclass
11✔
11
from pathlib import PurePath
11✔
12

13
from pants.backend.go.util_rules import cgo, coverage
11✔
14
from pants.backend.go.util_rules.assembly import (
11✔
15
    AssembleGoAssemblyFilesRequest,
16
    GenerateAssemblySymabisRequest,
17
    assemble_go_assembly_files,
18
    generate_go_assembly_symabisfile,
19
)
20
from pants.backend.go.util_rules.build_opts import GoBuildOptions
11✔
21
from pants.backend.go.util_rules.cgo import (
11✔
22
    CGoCompileRequest,
23
    CGoCompileResult,
24
    CGoCompilerFlags,
25
    cgo_compile_request,
26
)
27
from pants.backend.go.util_rules.coverage import (
11✔
28
    ApplyCodeCoverageRequest,
29
    BuiltGoPackageCodeCoverageMetadata,
30
    FileCodeCoverageMetadata,
31
    go_apply_code_coverage,
32
)
33
from pants.backend.go.util_rules.embedcfg import EmbedConfig
11✔
34
from pants.backend.go.util_rules.goroot import GoRoot
11✔
35
from pants.backend.go.util_rules.import_config import ImportConfigRequest, generate_import_config
11✔
36
from pants.backend.go.util_rules.sdk import GoSdkProcess, GoSdkToolIDRequest, compute_go_tool_id
11✔
37
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
11✔
38
from pants.engine.engine_aware import EngineAwareParameter, EngineAwareReturnType
11✔
39
from pants.engine.fs import (
11✔
40
    EMPTY_DIGEST,
41
    AddPrefix,
42
    CreateDigest,
43
    Digest,
44
    DigestSubset,
45
    FileContent,
46
    FileEntry,
47
    MergeDigests,
48
    PathGlobs,
49
)
50
from pants.engine.intrinsics import (
11✔
51
    add_prefix,
52
    create_digest,
53
    digest_subset_to_digest,
54
    execute_process,
55
    get_digest_entries,
56
    merge_digests,
57
)
58
from pants.engine.process import Process, ProcessResult, execute_process_or_raise
11✔
59
from pants.engine.rules import collect_rules, concurrently, implicitly, rule
11✔
60
from pants.util.frozendict import FrozenDict
11✔
61
from pants.util.logging import LogLevel
11✔
62
from pants.util.resources import read_resource
11✔
63
from pants.util.strutil import path_safe
11✔
64

65

66
class BuildGoPackageRequest(EngineAwareParameter):
11✔
67
    def __init__(
11✔
68
        self,
69
        *,
70
        import_path: str,
71
        pkg_name: str,
72
        digest: Digest,
73
        dir_path: str,
74
        build_opts: GoBuildOptions,
75
        go_files: tuple[str, ...],
76
        s_files: tuple[str, ...],
77
        direct_dependencies: tuple[BuildGoPackageRequest, ...],
78
        import_map: Mapping[str, str] | None = None,
79
        minimum_go_version: str | None,
80
        for_tests: bool = False,
81
        embed_config: EmbedConfig | None = None,
82
        with_coverage: bool = False,
83
        cgo_files: tuple[str, ...] = (),
84
        cgo_flags: CGoCompilerFlags | None = None,
85
        c_files: tuple[str, ...] = (),
86
        header_files: tuple[str, ...] = (),
87
        cxx_files: tuple[str, ...] = (),
88
        objc_files: tuple[str, ...] = (),
89
        fortran_files: tuple[str, ...] = (),
90
        prebuilt_object_files: tuple[str, ...] = (),
91
        pkg_specific_compiler_flags: tuple[str, ...] = (),
92
        pkg_specific_assembler_flags: tuple[str, ...] = (),
93
        is_stdlib: bool = False,
94
    ) -> None:
95
        """Build a package and its dependencies as `__pkg__.a` files.
96

97
        Instances of this class form a structure-shared DAG, and so a hashcode is pre-computed for
98
        the recursive portion.
99
        """
100

101
        if with_coverage and build_opts.coverage_config is None:
10✔
102
            raise ValueError(
×
103
                "BuildGoPackageRequest.with_coverage is set but BuildGoPackageRequest.build_opts.coverage_config is None!"
104
            )
105

106
        self.import_path = import_path
10✔
107
        self.pkg_name = pkg_name
10✔
108
        self.digest = digest
10✔
109
        self.dir_path = dir_path
10✔
110
        self.build_opts = build_opts
10✔
111
        self.go_files = go_files
10✔
112
        self.s_files = s_files
10✔
113
        self.direct_dependencies = direct_dependencies
10✔
114
        self.import_map = FrozenDict(import_map or {})
10✔
115
        self.minimum_go_version = minimum_go_version
10✔
116
        self.for_tests = for_tests
10✔
117
        self.embed_config = embed_config
10✔
118
        self.with_coverage = with_coverage
10✔
119
        self.cgo_files = cgo_files
10✔
120
        self.cgo_flags = cgo_flags
10✔
121
        self.c_files = c_files
10✔
122
        self.header_files = header_files
10✔
123
        self.cxx_files = cxx_files
10✔
124
        self.objc_files = objc_files
10✔
125
        self.fortran_files = fortran_files
10✔
126
        self.prebuilt_object_files = prebuilt_object_files
10✔
127
        self.pkg_specific_compiler_flags = pkg_specific_compiler_flags
10✔
128
        self.pkg_specific_assembler_flags = pkg_specific_assembler_flags
10✔
129
        self.is_stdlib = is_stdlib
10✔
130
        self._hashcode = hash(
10✔
131
            (
132
                self.import_path,
133
                self.pkg_name,
134
                self.digest,
135
                self.dir_path,
136
                self.build_opts,
137
                self.go_files,
138
                self.s_files,
139
                self.direct_dependencies,
140
                self.import_map,
141
                self.minimum_go_version,
142
                self.for_tests,
143
                self.embed_config,
144
                self.with_coverage,
145
                self.cgo_files,
146
                self.cgo_flags,
147
                self.c_files,
148
                self.header_files,
149
                self.cxx_files,
150
                self.objc_files,
151
                self.fortran_files,
152
                self.prebuilt_object_files,
153
                self.pkg_specific_compiler_flags,
154
                self.pkg_specific_assembler_flags,
155
                self.is_stdlib,
156
            )
157
        )
158

159
    def __repr__(self) -> str:
11✔
160
        # NB: We must override the default `__repr__` so that `direct_dependencies` does not
161
        # traverse into transitive dependencies, which was pathologically slow.
162
        return (
×
163
            f"{self.__class__}("
164
            f"import_path={repr(self.import_path)}, "
165
            f"pkg_name={self.pkg_name}, "
166
            f"digest={self.digest}, "
167
            f"dir_path={self.dir_path}, "
168
            f"build_opts={self.build_opts}, "
169
            f"go_files={self.go_files}, "
170
            f"s_files={self.s_files}, "
171
            f"direct_dependencies={[dep.import_path for dep in self.direct_dependencies]}, "
172
            f"import_map={self.import_map}, "
173
            f"minimum_go_version={self.minimum_go_version}, "
174
            f"for_tests={self.for_tests}, "
175
            f"embed_config={self.embed_config}, "
176
            f"with_coverage={self.with_coverage}, "
177
            f"cgo_files={self.cgo_files}, "
178
            f"cgo_flags={self.cgo_flags}, "
179
            f"c_files={self.c_files}, "
180
            f"header_files={self.header_files}, "
181
            f"cxx_files={self.cxx_files}, "
182
            f"objc_files={self.objc_files}, "
183
            f"fortran_files={self.fortran_files}, "
184
            f"prebuilt_object_files={self.prebuilt_object_files}, "
185
            f"pkg_specific_compiler_flags={self.pkg_specific_compiler_flags}, "
186
            f"pkg_specific_assembler_flags={self.pkg_specific_assembler_flags}, "
187
            f"is_stdlib={self.is_stdlib}"
188
            ")"
189
        )
190

191
    def __hash__(self) -> int:
11✔
192
        return self._hashcode
10✔
193

194
    def __eq__(self, other):
11✔
195
        if not isinstance(other, self.__class__):
9✔
196
            return NotImplemented
×
197
        return (
9✔
198
            self._hashcode == other._hashcode
199
            and self.import_path == other.import_path
200
            and self.pkg_name == other.pkg_name
201
            and self.digest == other.digest
202
            and self.dir_path == other.dir_path
203
            and self.build_opts == other.build_opts
204
            and self.import_map == other.import_map
205
            and self.go_files == other.go_files
206
            and self.s_files == other.s_files
207
            and self.minimum_go_version == other.minimum_go_version
208
            and self.for_tests == other.for_tests
209
            and self.embed_config == other.embed_config
210
            and self.with_coverage == other.with_coverage
211
            and self.cgo_files == other.cgo_files
212
            and self.cgo_flags == other.cgo_flags
213
            and self.c_files == other.c_files
214
            and self.header_files == other.header_files
215
            and self.cxx_files == other.cxx_files
216
            and self.objc_files == other.objc_files
217
            and self.fortran_files == other.fortran_files
218
            and self.prebuilt_object_files == other.prebuilt_object_files
219
            and self.pkg_specific_compiler_flags == other.pkg_specific_compiler_flags
220
            and self.pkg_specific_assembler_flags == other.pkg_specific_assembler_flags
221
            and self.is_stdlib == other.is_stdlib
222
            # TODO: Use a recursive memoized __eq__ if this ever shows up in profiles.
223
            and self.direct_dependencies == other.direct_dependencies
224
        )
225

226
    def debug_hint(self) -> str | None:
11✔
227
        return self.import_path
10✔
228

229

230
@dataclass(frozen=True)
11✔
231
class FallibleBuildGoPackageRequest(EngineAwareParameter, EngineAwareReturnType):
11✔
232
    """Request to build a package, but fallible if determining the request metadata failed.
233

234
    When creating "synthetic" packages, use `GoPackageRequest` directly. This type is only intended
235
    for determining the package metadata of user code, which may fail to be analyzed.
236
    """
237

238
    request: BuildGoPackageRequest | None
11✔
239
    import_path: str
11✔
240
    exit_code: int = 0
11✔
241
    stderr: str | None = None
11✔
242
    dependency_failed: bool = False
11✔
243

244
    def level(self) -> LogLevel:
11✔
245
        return (
7✔
246
            LogLevel.ERROR if self.exit_code != 0 and not self.dependency_failed else LogLevel.DEBUG
247
        )
248

249
    def message(self) -> str:
11✔
250
        message = self.import_path
7✔
251
        message += (
7✔
252
            " succeeded." if self.exit_code == 0 else f" failed (exit code {self.exit_code})."
253
        )
254
        if self.stderr:
7✔
255
            message += f"\n{self.stderr}"
3✔
256
        return message
7✔
257

258
    def cacheable(self) -> bool:
11✔
259
        # Failed compile outputs should be re-rendered in every run.
260
        return self.exit_code == 0
7✔
261

262

263
@dataclass(frozen=True)
11✔
264
class FallibleBuiltGoPackage(EngineAwareReturnType):
11✔
265
    """Fallible version of `BuiltGoPackage` with error details."""
266

267
    output: BuiltGoPackage | None
11✔
268
    import_path: str
11✔
269
    exit_code: int = 0
11✔
270
    stdout: str | None = None
11✔
271
    stderr: str | None = None
11✔
272
    dependency_failed: bool = False
11✔
273

274
    def level(self) -> LogLevel:
11✔
275
        return (
10✔
276
            LogLevel.ERROR if self.exit_code != 0 and not self.dependency_failed else LogLevel.DEBUG
277
        )
278

279
    def message(self) -> str:
11✔
280
        message = self.import_path
10✔
281
        message += (
10✔
282
            " succeeded." if self.exit_code == 0 else f" failed (exit code {self.exit_code})."
283
        )
284
        if self.stdout:
10✔
285
            message += f"\n{self.stdout}"
2✔
286
        if self.stderr:
10✔
287
            message += f"\n{self.stderr}"
×
288
        return message
10✔
289

290
    def cacheable(self) -> bool:
11✔
291
        # Failed compile outputs should be re-rendered in every run.
292
        return self.exit_code == 0
10✔
293

294

295
@dataclass(frozen=True)
11✔
296
class BuiltGoPackage:
11✔
297
    """A package and its dependencies compiled as `__pkg__.a` files.
298

299
    The packages are arranged into `__pkgs__/{path_safe(import_path)}/__pkg__.a`.
300
    """
301

302
    digest: Digest
11✔
303
    import_paths_to_pkg_a_files: FrozenDict[str, str]
11✔
304
    coverage_metadata: BuiltGoPackageCodeCoverageMetadata | None = None
11✔
305

306

307
@dataclass(frozen=True)
11✔
308
class RenderEmbedConfigRequest:
11✔
309
    embed_config: EmbedConfig | None
11✔
310

311

312
@dataclass(frozen=True)
11✔
313
class RenderedEmbedConfig:
11✔
314
    digest: Digest
11✔
315
    PATH = "./embedcfg"
11✔
316

317

318
@dataclass(frozen=True)
11✔
319
class GoCompileActionIdRequest:
11✔
320
    build_request: BuildGoPackageRequest
11✔
321

322

323
@dataclass(frozen=True)
11✔
324
class GoCompileActionIdResult:
11✔
325
    action_id: str
11✔
326

327

328
# TODO(#16831): Merge this rule helper and the AssemblyPostCompilationRequest.
329
async def _add_objects_to_archive(
11✔
330
    input_digest: Digest,
331
    pkg_archive_path: str,
332
    obj_file_paths: Iterable[str],
333
) -> ProcessResult:
334
    # Use `go tool asm` tool ID since `go tool pack` does not have a version argument.
335
    asm_tool_id = await compute_go_tool_id(GoSdkToolIDRequest("asm"))
10✔
336
    pack_result = await execute_process_or_raise(
10✔
337
        **implicitly(
338
            GoSdkProcess(
339
                input_digest=input_digest,
340
                command=(
341
                    "tool",
342
                    "pack",
343
                    "r",
344
                    pkg_archive_path,
345
                    *obj_file_paths,
346
                ),
347
                env={
348
                    "__PANTS_GO_ASM_TOOL_ID": asm_tool_id.tool_id,
349
                },
350
                description="Link objects to Go package archive",
351
                output_files=(pkg_archive_path,),
352
            )
353
        ),
354
    )
355
    return pack_result
10✔
356

357

358
@dataclass(frozen=True)
11✔
359
class SetupAsmCheckBinary:
11✔
360
    digest: Digest
11✔
361
    path: str
11✔
362

363

364
# Due to the bootstrap problem, the asm check binary cannot use the `LoadedGoBinaryRequest` rules since
365
# those rules call back into this `build_pkg` package. Instead, just invoke `go build` directly which is fine
366
# since the asm check binary only uses the standard library.
367
@rule
11✔
368
async def setup_golang_asm_check_binary() -> SetupAsmCheckBinary:
11✔
369
    src_file = "asm_check.go"
3✔
370
    content = read_resource("pants.backend.go.go_sources.asm_check", src_file)
3✔
371
    if not content:
3✔
372
        raise AssertionError(f"Unable to find resource for `{src_file}`.")
×
373

374
    sources_digest = await create_digest(CreateDigest([FileContent(src_file, content)]))
3✔
375

376
    binary_name = "__go_asm_check__"
3✔
377
    compile_result = await execute_process_or_raise(
3✔
378
        **implicitly(
379
            GoSdkProcess(
380
                command=("build", "-trimpath", "-o", binary_name, src_file),
381
                input_digest=sources_digest,
382
                output_files=(binary_name,),
383
                env={"CGO_ENABLED": "0"},
384
                description="Build Go assembly check binary",
385
            )
386
        ),
387
    )
388

389
    return SetupAsmCheckBinary(compile_result.output_digest, f"./{binary_name}")
3✔
390

391

392
# Check whether the given files looks like they could be Golang-format assembly language files.
393
@dataclass(frozen=True)
11✔
394
class CheckForGolangAssemblyRequest:
11✔
395
    digest: Digest
11✔
396
    dir_path: str
11✔
397
    s_files: tuple[str, ...]
11✔
398

399

400
@dataclass(frozen=True)
11✔
401
class CheckForGolangAssemblyResult:
11✔
402
    maybe_golang_assembly: bool
11✔
403

404

405
@rule
11✔
406
async def check_for_golang_assembly(
11✔
407
    request: CheckForGolangAssemblyRequest,
408
    asm_check_setup: SetupAsmCheckBinary,
409
) -> CheckForGolangAssemblyResult:
410
    """Return true if any of the given `s_files` look like it could be a Golang-format assembly
411
    language file.
412

413
    This is used by the cgo rules as a heuristic to determine if the user is passing Golang assembly
414
    format instead of gcc assembly format.
415
    """
416
    input_digest = await merge_digests(MergeDigests([request.digest, asm_check_setup.digest]))
3✔
417
    result = await execute_process_or_raise(
3✔
418
        **implicitly(
419
            Process(
420
                argv=(
421
                    asm_check_setup.path,
422
                    *(os.path.join(request.dir_path, s_file) for s_file in request.s_files),
423
                ),
424
                input_digest=input_digest,
425
                level=LogLevel.DEBUG,
426
                description="Check whether assembly language sources are in Go format",
427
            )
428
        ),
429
    )
430
    return CheckForGolangAssemblyResult(len(result.stdout) > 0)
3✔
431

432

433
# Copy header files to names which use platform independent names. For example, defs_linux_amd64.h
434
# becomes defs_GOOS_GOARCH.h.
435
#
436
# See https://github.com/golang/go/blob/1c05968c9a5d6432fc6f30196528f8f37287dd3d/src/cmd/go/internal/work/exec.go#L867-L892
437
# for particulars.
438
async def _maybe_copy_headers_to_platform_independent_names(
11✔
439
    input_digest: Digest,
440
    dir_path: str,
441
    header_files: tuple[str, ...],
442
    goroot: GoRoot,
443
) -> Digest | None:
444
    goos_goarch = f"_{goroot.goos}_{goroot.goarch}"
10✔
445
    goos = f"_{goroot.goos}"
10✔
446
    goarch = f"_{goroot.goarch}"
10✔
447

448
    digest_entries = await get_digest_entries(input_digest)
10✔
449
    digest_entries_by_path: dict[str, FileEntry] = {
10✔
450
        entry.path: entry for entry in digest_entries if isinstance(entry, FileEntry)
451
    }
452

453
    new_digest_entries: list[FileEntry] = []
10✔
454
    for header_file in header_files:
10✔
455
        header_file_path = PurePath(dir_path, header_file)
7✔
456

457
        entry = digest_entries_by_path.get(str(header_file_path))
7✔
458
        if not entry:
7✔
459
            continue
7✔
460

461
        stem = header_file_path.stem
2✔
462
        new_stem: str | None = None
2✔
463
        if stem.endswith(goos_goarch):
2✔
464
            new_stem = stem[0 : -len(goos_goarch)] + "_GOOS_GOARCH"
×
465
        elif stem.endswith(goos):
2✔
UNCOV
466
            new_stem = stem[0 : -len(goos)] + "_GOOS"
1✔
467
        elif stem.endswith(goarch):
1✔
468
            new_stem = stem[0 : -len(goarch)] + "_GOARCH"
×
469

470
        if new_stem:
2✔
UNCOV
471
            new_header_file_path = PurePath(dir_path, f"{new_stem}{header_file_path.suffix}")
1✔
UNCOV
472
            new_digest_entries.append(dataclasses.replace(entry, path=str(new_header_file_path)))
1✔
473

474
    if new_digest_entries:
10✔
UNCOV
475
        digest = await create_digest(CreateDigest(new_digest_entries))
1✔
UNCOV
476
        return digest
1✔
477
    else:
478
        return None
10✔
479

480

481
@rule
11✔
482
async def render_embed_config(request: RenderEmbedConfigRequest) -> RenderedEmbedConfig:
11✔
483
    digest = EMPTY_DIGEST
10✔
484
    if request.embed_config:
10✔
485
        digest = await create_digest(
1✔
486
            CreateDigest(
487
                [FileContent(RenderedEmbedConfig.PATH, request.embed_config.to_embedcfg())]
488
            )
489
        )
490
    return RenderedEmbedConfig(digest)
10✔
491

492

493
# Compute a cache key for the compile action. This computation is intended to capture similar values to the
494
# action ID computed by the `go` tool for its own cache.
495
# For details, see https://github.com/golang/go/blob/21998413ad82655fef1f31316db31e23e0684b21/src/cmd/go/internal/work/exec.go#L216-L403
496
@rule
11✔
497
async def compute_compile_action_id(
11✔
498
    request: GoCompileActionIdRequest, goroot: GoRoot
499
) -> GoCompileActionIdResult:
500
    bq = request.build_request
10✔
501

502
    h = hashlib.sha256()
10✔
503

504
    # All Go action IDs have the full version (as returned by `runtime.Version()`) in the key.
505
    # See https://github.com/golang/go/blob/master/src/cmd/go/internal/cache/hash.go#L32-L46
506
    h.update(goroot.full_version.encode())
10✔
507

508
    h.update(b"compile\n")
10✔
509
    if bq.minimum_go_version:
10✔
510
        h.update(f"go {bq.minimum_go_version}\n".encode())
10✔
511
    h.update(f"goos {goroot.goos} goarch {goroot.goarch}\n".encode())
10✔
512
    h.update(f"import {bq.import_path}\n".encode())
10✔
513
    # TODO: Consider what to do with this information from Go tool:
514
    # fmt.Fprintf(h, "omitdebug %v standard %v local %v prefix %q\n", p.Internal.OmitDebug, p.Standard, p.Internal.Local, p.Internal.LocalPrefix)
515
    # TODO: Inject cgo-related values here.
516
    # TODO: Inject cover mode values here.
517
    # TODO: Inject fuzz instrumentation values here.
518

519
    compile_tool_id = await compute_go_tool_id(GoSdkToolIDRequest("compile"))
10✔
520
    h.update(f"compile {compile_tool_id.tool_id}\n".encode())
10✔
521
    # TODO: Add compiler flags as per `go`'s algorithm. Need to figure out
522
    if bq.s_files:
10✔
523
        asm_tool_id = await compute_go_tool_id(GoSdkToolIDRequest("asm"))
10✔
524
        h.update(f"asm {asm_tool_id.tool_id}\n".encode())
10✔
525
        # TODO: Add asm flags as per `go`'s algorithm.
526
    # TODO: Add micro-architecture into cache key (e.g., GOAMD64 setting).
527
    if "GOEXPERIMENT" in goroot._raw_metadata:
10✔
528
        h.update(f"GOEXPERIMENT={goroot._raw_metadata['GOEXPERIMENT']}".encode())
10✔
529
    # TODO: Maybe handle go "magic" env vars: "GOCLOBBERDEADHASH", "GOSSAFUNC", "GOSSADIR", "GOSSAHASH" ?
530
    # TODO: Handle GSHS_LOGFILE compiler debug option by breaking cache?
531

532
    # Note: Input files are already part of cache key. Thus, this algorithm omits incorporating their
533
    # content hashes into the action ID.
534

535
    return GoCompileActionIdResult(h.hexdigest())
10✔
536

537

538
# Gather transitive prebuilt object files for Cgo. Traverse the provided dependencies and lifts `.syso`
539
# object files into a single `Digest`.
540
async def _gather_transitive_prebuilt_object_files(
11✔
541
    build_request: BuildGoPackageRequest,
542
) -> tuple[Digest, frozenset[str]]:
543
    prebuilt_objects: list[tuple[Digest, list[str]]] = []
3✔
544

545
    queue: deque[BuildGoPackageRequest] = deque([build_request])
3✔
546
    seen: set[BuildGoPackageRequest] = {build_request}
3✔
547
    while queue:
3✔
548
        pkg = queue.popleft()
3✔
549
        unseen = [dd for dd in build_request.direct_dependencies if dd not in seen]
3✔
550
        queue.extend(unseen)
3✔
551
        seen.update(unseen)
3✔
552
        if pkg.prebuilt_object_files:
3✔
UNCOV
553
            prebuilt_objects.append(
1✔
554
                (
555
                    pkg.digest,
556
                    [
557
                        os.path.join(pkg.dir_path, obj_file)
558
                        for obj_file in pkg.prebuilt_object_files
559
                    ],
560
                )
561
            )
562

563
    object_digest = await merge_digests(MergeDigests([digest for digest, _ in prebuilt_objects]))
3✔
564
    object_files = set()
3✔
565
    for _, files in prebuilt_objects:
3✔
UNCOV
566
        object_files.update(files)
1✔
567

568
    return object_digest, frozenset(object_files)
3✔
569

570

571
# NB: We must have a description for the streaming of this rule to work properly
572
# (triggered by `FallibleBuiltGoPackage` subclassing `EngineAwareReturnType`).
573
@rule(desc="Compile with Go", level=LogLevel.DEBUG)
11✔
574
async def build_go_package(
11✔
575
    request: BuildGoPackageRequest, go_root: GoRoot
576
) -> FallibleBuiltGoPackage:
577
    maybe_built_deps = await concurrently(
10✔
578
        build_go_package(build_request, go_root) for build_request in request.direct_dependencies
579
    )
580

581
    import_paths_to_pkg_a_files: dict[str, str] = {}
10✔
582
    dep_digests = []
10✔
583
    for maybe_dep in maybe_built_deps:
10✔
584
        if maybe_dep.output is None:
10✔
585
            return dataclasses.replace(
1✔
586
                maybe_dep, import_path=request.import_path, dependency_failed=True
587
            )
588
        dep = maybe_dep.output
10✔
589
        for dep_import_path, pkg_archive_path in dep.import_paths_to_pkg_a_files.items():
10✔
590
            if dep_import_path not in import_paths_to_pkg_a_files:
10✔
591
                import_paths_to_pkg_a_files[dep_import_path] = pkg_archive_path
10✔
592
                dep_digests.append(dep.digest)
10✔
593

594
    merged_deps_digest, import_config, embedcfg, action_id_result = await concurrently(
10✔
595
        merge_digests(MergeDigests(dep_digests)),
596
        generate_import_config(
597
            ImportConfigRequest(
598
                FrozenDict(import_paths_to_pkg_a_files),
599
                build_opts=request.build_opts,
600
                import_map=request.import_map,
601
            )
602
        ),
603
        render_embed_config(RenderEmbedConfigRequest(request.embed_config)),
604
        compute_compile_action_id(GoCompileActionIdRequest(request), go_root),
605
    )
606

607
    unmerged_input_digests = [
10✔
608
        merged_deps_digest,
609
        import_config.digest,
610
        embedcfg.digest,
611
        request.digest,
612
    ]
613

614
    # If coverage is enabled for this package, then replace the Go source files with versions modified to
615
    # contain coverage code.
616
    go_files = request.go_files
10✔
617
    cgo_files = request.cgo_files
10✔
618
    s_files = list(request.s_files)
10✔
619
    go_files_digest = request.digest
10✔
620
    cover_file_metadatas: tuple[FileCodeCoverageMetadata, ...] | None = None
10✔
621
    if request.with_coverage:
10✔
622
        coverage_config = request.build_opts.coverage_config
2✔
623
        assert coverage_config is not None, "with_coverage=True but coverage_config is None!"
2✔
624
        coverage_result = await go_apply_code_coverage(
2✔
625
            ApplyCodeCoverageRequest(
626
                digest=request.digest,
627
                dir_path=request.dir_path,
628
                go_files=go_files,
629
                cgo_files=cgo_files,
630
                cover_mode=coverage_config.cover_mode,
631
                import_path=request.import_path,
632
            )
633
        )
634
        go_files_digest = coverage_result.digest
2✔
635
        unmerged_input_digests.append(go_files_digest)
2✔
636
        go_files = coverage_result.go_files
2✔
637
        cgo_files = coverage_result.cgo_files
2✔
638
        cover_file_metadatas = coverage_result.cover_file_metadatas
2✔
639

640
    # Track loose object files to link into final package archive. These can come from Cgo outputs, regular
641
    # assembly files, or regular C files.
642
    objects: list[tuple[str, Digest]] = []
10✔
643

644
    # Add any prebuilt object files (".syso" extension) to the list of objects to link into the package.
645
    if request.prebuilt_object_files:
10✔
UNCOV
646
        objects.extend(
1✔
647
            (os.path.join(request.dir_path, prebuilt_object_file), request.digest)
648
            for prebuilt_object_file in request.prebuilt_object_files
649
        )
650

651
    # Process any Cgo files.
652
    cgo_compile_result: CGoCompileResult | None = None
10✔
653
    if cgo_files:
10✔
654
        # Check if any assembly files contain gcc assembly, and not Go assembly. Raise an exception if any are
655
        # likely in Go format since in cgo packages, assembly files are passed to gcc and must be in gcc format.
656
        #
657
        # Exception: When building runtime/cgo itself, only send `gcc_*.s` assembly files to GCC as
658
        # runtime/cgo has both types of files.
659
        if request.is_stdlib and request.import_path == "runtime/cgo":
3✔
660
            gcc_s_files = []
3✔
661
            new_s_files = []
3✔
662
            for s_file in s_files:
3✔
663
                if s_file.startswith("gcc_"):
3✔
664
                    gcc_s_files.append(s_file)
3✔
665
                else:
666
                    new_s_files.append(s_file)
3✔
667
            s_files = new_s_files
3✔
668
        else:
669
            asm_check_result = await check_for_golang_assembly(
3✔
670
                CheckForGolangAssemblyRequest(
671
                    digest=request.digest,
672
                    dir_path=request.dir_path,
673
                    s_files=tuple(s_files),
674
                ),
675
                **implicitly(),
676
            )
677
            if asm_check_result.maybe_golang_assembly:
3✔
678
                raise ValueError(
×
679
                    f"Package {request.import_path} is a cgo package but contains Go assembly files."
680
                )
681
            gcc_s_files = s_files
3✔
682
            s_files = []  # Clear s_files since assembly has already been handled in cgo rules.
3✔
683

684
        # Gather all prebuilt object files transitively and pass them to the Cgo rule for linking into the
685
        # Cgo object output. This is necessary to avoid linking errors.
686
        # See https://github.com/golang/go/blob/6ad27161f8d1b9c5e03fb3415977e1d3c3b11323/src/cmd/go/internal/work/exec.go#L3291-L3311.
687
        transitive_prebuilt_object_files = await _gather_transitive_prebuilt_object_files(request)
3✔
688

689
        assert request.cgo_flags is not None
3✔
690
        cgo_compile_result = await cgo_compile_request(
3✔
691
            CGoCompileRequest(
692
                import_path=request.import_path,
693
                pkg_name=request.pkg_name,
694
                digest=go_files_digest,
695
                build_opts=request.build_opts,
696
                dir_path=request.dir_path,
697
                cgo_files=cgo_files,
698
                cgo_flags=request.cgo_flags,
699
                c_files=request.c_files,
700
                s_files=tuple(gcc_s_files),
701
                cxx_files=request.cxx_files,
702
                objc_files=request.objc_files,
703
                fortran_files=request.fortran_files,
704
                is_stdlib=request.is_stdlib,
705
                transitive_prebuilt_object_files=transitive_prebuilt_object_files,
706
            ),
707
            **implicitly(),
708
        )
709
        assert cgo_compile_result is not None
3✔
710
        unmerged_input_digests.append(cgo_compile_result.digest)
3✔
711
        objects.extend(
3✔
712
            [
713
                (obj_file, cgo_compile_result.digest)
714
                for obj_file in cgo_compile_result.output_obj_files
715
            ]
716
        )
717

718
    # Copy header files with platform-specific values in their name to platform independent names.
719
    # For example, defs_linux_amd64.h becomes defs_GOOS_GOARCH.h.
720
    copied_headers_digest = await _maybe_copy_headers_to_platform_independent_names(
10✔
721
        input_digest=request.digest,
722
        dir_path=request.dir_path,
723
        header_files=request.header_files,
724
        goroot=go_root,
725
    )
726
    if copied_headers_digest:
10✔
UNCOV
727
        unmerged_input_digests.append(copied_headers_digest)
1✔
728

729
    # Merge all of the input digests together.
730
    input_digest = await merge_digests(MergeDigests(unmerged_input_digests))
10✔
731

732
    # If any assembly files are present, generate a "symabis" file containing API metadata about those files.
733
    # The "symabis" file is passed to the Go compiler when building Go code so that the compiler is aware of
734
    # any API exported by the assembly.
735
    #
736
    # Note: The assembly files cannot be assembled at this point because a similar process happens from Go to
737
    # assembly: The Go compiler generates a `go_asm.h` header file with metadata about the Go code in the package.
738
    symabis_path: str | None = None
10✔
739
    extra_assembler_flags = tuple(
10✔
740
        *request.build_opts.assembler_flags, *request.pkg_specific_assembler_flags
741
    )
742
    if s_files:
10✔
743
        symabis_fallible_result = await generate_go_assembly_symabisfile(
10✔
744
            GenerateAssemblySymabisRequest(
745
                compilation_input=input_digest,
746
                s_files=tuple(s_files),
747
                import_path=request.import_path,
748
                dir_path=request.dir_path,
749
                extra_assembler_flags=extra_assembler_flags,
750
            ),
751
            **implicitly(),
752
        )
753
        symabis_result = symabis_fallible_result.result
10✔
754
        if symabis_result is None:
10✔
UNCOV
755
            return FallibleBuiltGoPackage(
1✔
756
                None,
757
                request.import_path,
758
                symabis_fallible_result.exit_code,
759
                stdout=symabis_fallible_result.stdout,
760
                stderr=symabis_fallible_result.stderr,
761
            )
762
        input_digest = await merge_digests(
10✔
763
            MergeDigests([input_digest, symabis_result.symabis_digest])
764
        )
765
        symabis_path = symabis_result.symabis_path
10✔
766

767
    # Build the arguments for compiling the Go code in this package.
768
    compile_args = [
10✔
769
        "tool",
770
        "compile",
771
        "-buildid",
772
        action_id_result.action_id,
773
        "-o",
774
        "__pkg__.a",
775
        "-pack",
776
        "-p",
777
        request.import_path,
778
        "-importcfg",
779
        import_config.CONFIG_PATH,
780
        "-trimpath",
781
        "__PANTS_SANDBOX_ROOT__",
782
    ]
783

784
    # See https://github.com/golang/go/blob/f229e7031a6efb2f23241b5da000c3b3203081d6/src/cmd/go/internal/work/gc.go#L79-L100
785
    # for where this logic comes from.
786
    go_version = go_root.major_version(request.minimum_go_version or "1.16")
10✔
787
    if go_root.is_compatible_version(go_version):
10✔
788
        compile_args.extend(["-lang", f"go{go_version}"])
10✔
789

790
    if request.is_stdlib:
10✔
791
        compile_args.append("-std")
10✔
792

793
    compiling_runtime = request.is_stdlib and request.import_path in (
10✔
794
        "internal/abi",
795
        "internal/bytealg",
796
        "internal/coverage/rtcov",
797
        "internal/cpu",
798
        "internal/goarch",
799
        "internal/goos",
800
        "runtime",
801
        "runtime/internal/atomic",
802
        "runtime/internal/math",
803
        "runtime/internal/sys",
804
        "runtime/internal/syscall",
805
    )
806

807
    # From Go sources:
808
    # runtime compiles with a special gc flag to check for
809
    # memory allocations that are invalid in the runtime package,
810
    # and to implement some special compiler pragmas.
811
    #
812
    # See https://github.com/golang/go/blob/245e95dfabd77f337373bf2d6bb47cd353ad8d74/src/cmd/go/internal/work/gc.go#L107-L112
813
    if compiling_runtime:
10✔
814
        compile_args.append("-+")
10✔
815

816
    if symabis_path:
10✔
817
        compile_args.extend(["-symabis", symabis_path])
10✔
818

819
    # If any assembly files are present, request the compiler write an "assembly header" with API metadata
820
    # about the Go code that can be used by assembly files.
821
    asm_header_path: str | None = None
10✔
822
    if s_files:
10✔
823
        if os.path.isabs(request.dir_path):
10✔
824
            asm_header_path = "go_asm.h"
10✔
825
        else:
UNCOV
826
            asm_header_path = os.path.join(request.dir_path, "go_asm.h")
1✔
827
        compile_args.extend(["-asmhdr", asm_header_path])
10✔
828

829
    if embedcfg.digest != EMPTY_DIGEST:
10✔
830
        compile_args.extend(["-embedcfg", RenderedEmbedConfig.PATH])
1✔
831

832
    if request.build_opts.with_race_detector:
10✔
UNCOV
833
        compile_args.append("-race")
1✔
834

835
    if request.build_opts.with_msan:
10✔
836
        compile_args.append("-msan")
×
837

838
    if request.build_opts.with_asan:
10✔
839
        compile_args.append("-asan")
×
840

841
    # If there are no loose object files to add to the package archive later or assembly files to assemble,
842
    # then pass -complete flag which tells the compiler that the provided Go files constitute the entire package.
843
    if not objects and not s_files:
10✔
844
        # Exceptions: a few standard packages have forward declarations for
845
        # pieces supplied behind-the-scenes by package runtime.
846
        if request.import_path not in (
10✔
847
            "bytes",
848
            "internal/poll",
849
            "net",
850
            "os",
851
            "runtime/metrics",
852
            "runtime/pprof",
853
            "runtime/trace",
854
            "sync",
855
            "syscall",
856
            "time",
857
        ):
858
            compile_args.append("-complete")
10✔
859

860
    # Add any extra compiler flags after the ones added automatically by this rule.
861
    if request.build_opts.compiler_flags:
10✔
862
        compile_args.extend(request.build_opts.compiler_flags)
×
863
    if request.pkg_specific_compiler_flags:
10✔
864
        compile_args.extend(request.pkg_specific_compiler_flags)
×
865

866
    # Remove -N if compiling runtime:
867
    #  It is not possible to build the runtime with no optimizations,
868
    #  because the compiler cannot eliminate enough write barriers.
869
    if compiling_runtime:
10✔
870
        compile_args = [arg for arg in compile_args if arg != "-N"]
10✔
871

872
    go_file_paths = (
10✔
873
        str(PurePath(request.dir_path, go_file)) if request.dir_path else f"./{go_file}"
874
        for go_file in go_files
875
    )
876
    generated_cgo_file_paths = cgo_compile_result.output_go_files if cgo_compile_result else ()
10✔
877

878
    # Put the source file paths into a file and pass that to `go tool compile` via a config file using the
879
    # `@CONFIG_FILE` syntax. This is necessary to avoid command-line argument limits on macOS. The arguments
880
    # may end up to exceed those limits when compiling standard library packages where we append a very long GOROOT
881
    # path to each file name or in packages with large numbers of files.
882
    go_source_file_paths_config = "\n".join([*go_file_paths, *generated_cgo_file_paths])
10✔
883
    go_sources_file_paths_digest = await create_digest(
10✔
884
        CreateDigest([FileContent("__sources__.txt", go_source_file_paths_config.encode())])
885
    )
886
    input_digest = await merge_digests(MergeDigests([input_digest, go_sources_file_paths_digest]))
10✔
887
    compile_args.append("@__sources__.txt")
10✔
888

889
    compile_result = await execute_process(
10✔
890
        **implicitly(
891
            GoSdkProcess(
892
                input_digest=input_digest,
893
                command=tuple(compile_args),
894
                description=f"Compile Go package: {request.import_path}",
895
                output_files=("__pkg__.a", *([asm_header_path] if asm_header_path else [])),
896
                env={"__PANTS_GO_COMPILE_ACTION_ID": action_id_result.action_id},
897
                replace_sandbox_root_in_args=True,
898
            )
899
        )
900
    )
901
    if compile_result.exit_code != 0:
10✔
902
        return FallibleBuiltGoPackage(
1✔
903
            None,
904
            request.import_path,
905
            compile_result.exit_code,
906
            stdout=compile_result.stdout.decode("utf-8"),
907
            stderr=compile_result.stderr.decode("utf-8"),
908
        )
909

910
    compilation_digest = compile_result.output_digest
10✔
911

912
    # TODO: Compile any C files if this package does not use Cgo.
913

914
    # If any assembly files are present, then assemble them. The `compilation_digest` will contain the
915
    # assembly header `go_asm.h` in the object directory.
916
    if s_files:
10✔
917
        # Extract the `go_asm.h` header from the compilation output and merge into the original compilation input.
918
        assert asm_header_path is not None
10✔
919
        asm_header_digest = await digest_subset_to_digest(
10✔
920
            DigestSubset(
921
                compilation_digest,
922
                PathGlobs(
923
                    [asm_header_path],
924
                    glob_match_error_behavior=GlobMatchErrorBehavior.error,
925
                    description_of_origin="the `build_go_package` rule",
926
                ),
927
            )
928
        )
929
        assembly_input_digest = await merge_digests(MergeDigests([input_digest, asm_header_digest]))
10✔
930
        assembly_fallible_result = await assemble_go_assembly_files(
10✔
931
            AssembleGoAssemblyFilesRequest(
932
                input_digest=assembly_input_digest,
933
                s_files=tuple(sorted(s_files)),
934
                dir_path=request.dir_path,
935
                import_path=request.import_path,
936
                extra_assembler_flags=extra_assembler_flags,
937
            ),
938
            **implicitly(),
939
        )
940
        assembly_result = assembly_fallible_result.result
10✔
941
        if assembly_result is None:
10✔
942
            return FallibleBuiltGoPackage(
×
943
                None,
944
                request.import_path,
945
                assembly_fallible_result.exit_code,
946
                stdout=assembly_fallible_result.stdout,
947
                stderr=assembly_fallible_result.stderr,
948
            )
949
        objects.extend(assembly_result.assembly_outputs)
10✔
950

951
    # If there are any loose object files, link them into the package archive.
952
    if objects:
10✔
953
        assembly_link_input_digest = await merge_digests(
10✔
954
            MergeDigests(
955
                [
956
                    compilation_digest,
957
                    *(digest for obj_file, digest in objects),
958
                ]
959
            )
960
        )
961
        assembly_link_result = await _add_objects_to_archive(
10✔
962
            input_digest=assembly_link_input_digest,
963
            pkg_archive_path="__pkg__.a",
964
            obj_file_paths=sorted(obj_file for obj_file, digest in objects),
965
        )
966
        compilation_digest = assembly_link_result.output_digest
10✔
967

968
    path_prefix = os.path.join("__pkgs__", path_safe(request.import_path))
10✔
969
    import_paths_to_pkg_a_files[request.import_path] = os.path.join(path_prefix, "__pkg__.a")
10✔
970
    output_digest = await add_prefix(AddPrefix(compilation_digest, path_prefix))
10✔
971
    merged_result_digest = await merge_digests(MergeDigests([*dep_digests, output_digest]))
10✔
972

973
    # Include the modules sources in the output `Digest` alongside the package archive if the Cgo rules
974
    # detected a potential attempt to link against a static archive (or other reference to `${SRCDIR}` in
975
    # options) which necessitates the linker needing access to module sources.
976
    if cgo_compile_result and cgo_compile_result.include_module_sources_with_output:
10✔
977
        merged_result_digest = await merge_digests(
1✔
978
            MergeDigests([merged_result_digest, request.digest])
979
        )
980

981
    coverage_metadata = (
10✔
982
        BuiltGoPackageCodeCoverageMetadata(
983
            import_path=request.import_path,
984
            cover_file_metadatas=cover_file_metadatas,
985
            sources_digest=request.digest,
986
            sources_dir_path=request.dir_path,
987
        )
988
        if cover_file_metadatas
989
        else None
990
    )
991

992
    output = BuiltGoPackage(
10✔
993
        digest=merged_result_digest,
994
        import_paths_to_pkg_a_files=FrozenDict(import_paths_to_pkg_a_files),
995
        coverage_metadata=coverage_metadata,
996
    )
997
    return FallibleBuiltGoPackage(output, request.import_path)
10✔
998

999

1000
@rule
11✔
1001
async def required_built_go_package(fallible_result: FallibleBuiltGoPackage) -> BuiltGoPackage:
11✔
1002
    if fallible_result.output is not None:
10✔
1003
        return fallible_result.output
10✔
1004
    raise Exception(
×
1005
        f"Failed to compile {fallible_result.import_path}:\n"
1006
        f"{fallible_result.stdout}\n{fallible_result.stderr}"
1007
    )
1008

1009

1010
def rules():
11✔
1011
    return (
11✔
1012
        *collect_rules(),
1013
        *cgo.rules(),
1014
        *coverage.rules(),
1015
    )
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