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

pantsbuild / pants / 19000741080

01 Nov 2025 06:16PM UTC coverage: 80.3% (+0.3%) from 80.004%
19000741080

Pull #22837

github

web-flow
Merge 51f49bc90 into da3fb359e
Pull Request #22837: Updated Treesitter dependencies

77994 of 97128 relevant lines covered (80.3%)

3.35 hits per line

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

35.75
/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:
2✔
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
2✔
107
        self.pkg_name = pkg_name
2✔
108
        self.digest = digest
2✔
109
        self.dir_path = dir_path
2✔
110
        self.build_opts = build_opts
2✔
111
        self.go_files = go_files
2✔
112
        self.s_files = s_files
2✔
113
        self.direct_dependencies = direct_dependencies
2✔
114
        self.import_map = FrozenDict(import_map or {})
2✔
115
        self.minimum_go_version = minimum_go_version
2✔
116
        self.for_tests = for_tests
2✔
117
        self.embed_config = embed_config
2✔
118
        self.with_coverage = with_coverage
2✔
119
        self.cgo_files = cgo_files
2✔
120
        self.cgo_flags = cgo_flags
2✔
121
        self.c_files = c_files
2✔
122
        self.header_files = header_files
2✔
123
        self.cxx_files = cxx_files
2✔
124
        self.objc_files = objc_files
2✔
125
        self.fortran_files = fortran_files
2✔
126
        self.prebuilt_object_files = prebuilt_object_files
2✔
127
        self.pkg_specific_compiler_flags = pkg_specific_compiler_flags
2✔
128
        self.pkg_specific_assembler_flags = pkg_specific_assembler_flags
2✔
129
        self.is_stdlib = is_stdlib
2✔
130
        self._hashcode = hash(
2✔
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
3✔
193

194
    def __eq__(self, other):
11✔
195
        if not isinstance(other, self.__class__):
×
196
            return NotImplemented
×
197
        return (
×
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
×
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 (
×
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
×
251
        message += (
×
252
            " succeeded." if self.exit_code == 0 else f" failed (exit code {self.exit_code})."
253
        )
254
        if self.stderr:
×
255
            message += f"\n{self.stderr}"
×
256
        return message
×
257

258
    def cacheable(self) -> bool:
11✔
259
        # Failed compile outputs should be re-rendered in every run.
260
        return self.exit_code == 0
×
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 (
×
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
×
281
        message += (
×
282
            " succeeded." if self.exit_code == 0 else f" failed (exit code {self.exit_code})."
283
        )
284
        if self.stdout:
×
285
            message += f"\n{self.stdout}"
×
286
        if self.stderr:
×
287
            message += f"\n{self.stderr}"
×
288
        return message
×
289

290
    def cacheable(self) -> bool:
11✔
291
        # Failed compile outputs should be re-rendered in every run.
292
        return self.exit_code == 0
×
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"))
×
336
    pack_result = await execute_process_or_raise(
×
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
×
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"
×
370
    content = read_resource("pants.backend.go.go_sources.asm_check", src_file)
×
371
    if not content:
×
372
        raise AssertionError(f"Unable to find resource for `{src_file}`.")
×
373

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

376
    binary_name = "__go_asm_check__"
×
377
    compile_result = await execute_process_or_raise(
×
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}")
×
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]))
×
417
    result = await execute_process_or_raise(
×
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)
×
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}"
×
445
    goos = f"_{goroot.goos}"
×
446
    goarch = f"_{goroot.goarch}"
×
447

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

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

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

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

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

474
    if new_digest_entries:
×
475
        digest = await create_digest(CreateDigest(new_digest_entries))
×
476
        return digest
×
477
    else:
478
        return None
×
479

480

481
@rule
11✔
482
async def render_embed_config(request: RenderEmbedConfigRequest) -> RenderedEmbedConfig:
11✔
483
    digest = EMPTY_DIGEST
×
484
    if request.embed_config:
×
485
        digest = await create_digest(
×
486
            CreateDigest(
487
                [FileContent(RenderedEmbedConfig.PATH, request.embed_config.to_embedcfg())]
488
            )
489
        )
490
    return RenderedEmbedConfig(digest)
×
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
×
501

502
    h = hashlib.sha256()
×
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())
×
507

508
    h.update(b"compile\n")
×
509
    if bq.minimum_go_version:
×
510
        h.update(f"go {bq.minimum_go_version}\n".encode())
×
511
    h.update(f"goos {goroot.goos} goarch {goroot.goarch}\n".encode())
×
512
    h.update(f"import {bq.import_path}\n".encode())
×
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"))
×
520
    h.update(f"compile {compile_tool_id.tool_id}\n".encode())
×
521
    # TODO: Add compiler flags as per `go`'s algorithm. Need to figure out
522
    if bq.s_files:
×
523
        asm_tool_id = await compute_go_tool_id(GoSdkToolIDRequest("asm"))
×
524
        h.update(f"asm {asm_tool_id.tool_id}\n".encode())
×
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:
×
528
        h.update(f"GOEXPERIMENT={goroot._raw_metadata['GOEXPERIMENT']}".encode())
×
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())
×
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]]] = []
×
544

545
    queue: deque[BuildGoPackageRequest] = deque([build_request])
×
546
    seen: set[BuildGoPackageRequest] = {build_request}
×
547
    while queue:
×
548
        pkg = queue.popleft()
×
549
        unseen = [dd for dd in build_request.direct_dependencies if dd not in seen]
×
550
        queue.extend(unseen)
×
551
        seen.update(unseen)
×
552
        if pkg.prebuilt_object_files:
×
553
            prebuilt_objects.append(
×
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]))
×
564
    object_files = set()
×
565
    for _, files in prebuilt_objects:
×
566
        object_files.update(files)
×
567

568
    return object_digest, frozenset(object_files)
×
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(
×
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] = {}
×
582
    dep_digests = []
×
583
    for maybe_dep in maybe_built_deps:
×
584
        if maybe_dep.output is None:
×
585
            return dataclasses.replace(
×
586
                maybe_dep, import_path=request.import_path, dependency_failed=True
587
            )
588
        dep = maybe_dep.output
×
589
        for dep_import_path, pkg_archive_path in dep.import_paths_to_pkg_a_files.items():
×
590
            if dep_import_path not in import_paths_to_pkg_a_files:
×
591
                import_paths_to_pkg_a_files[dep_import_path] = pkg_archive_path
×
592
                dep_digests.append(dep.digest)
×
593

594
    merged_deps_digest, import_config, embedcfg, action_id_result = await concurrently(
×
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 = [
×
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
×
617
    cgo_files = request.cgo_files
×
618
    s_files = list(request.s_files)
×
619
    go_files_digest = request.digest
×
620
    cover_file_metadatas: tuple[FileCodeCoverageMetadata, ...] | None = None
×
621
    if request.with_coverage:
×
622
        coverage_config = request.build_opts.coverage_config
×
623
        assert coverage_config is not None, "with_coverage=True but coverage_config is None!"
×
624
        coverage_result = await go_apply_code_coverage(
×
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
×
635
        unmerged_input_digests.append(go_files_digest)
×
636
        go_files = coverage_result.go_files
×
637
        cgo_files = coverage_result.cgo_files
×
638
        cover_file_metadatas = coverage_result.cover_file_metadatas
×
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]] = []
×
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:
×
646
        objects.extend(
×
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
×
653
    if cgo_files:
×
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":
×
660
            gcc_s_files = []
×
661
            new_s_files = []
×
662
            for s_file in s_files:
×
663
                if s_file.startswith("gcc_"):
×
664
                    gcc_s_files.append(s_file)
×
665
                else:
666
                    new_s_files.append(s_file)
×
667
            s_files = new_s_files
×
668
        else:
669
            asm_check_result = await check_for_golang_assembly(
×
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:
×
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
×
682
            s_files = []  # Clear s_files since assembly has already been handled in cgo rules.
×
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)
×
688

689
        assert request.cgo_flags is not None
×
690
        cgo_compile_result = await cgo_compile_request(
×
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
×
710
        unmerged_input_digests.append(cgo_compile_result.digest)
×
711
        objects.extend(
×
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(
×
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:
×
727
        unmerged_input_digests.append(copied_headers_digest)
×
728

729
    # Merge all of the input digests together.
730
    input_digest = await merge_digests(MergeDigests(unmerged_input_digests))
×
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
×
739
    extra_assembler_flags = tuple(
×
740
        *request.build_opts.assembler_flags, *request.pkg_specific_assembler_flags
741
    )
742
    if s_files:
×
743
        symabis_fallible_result = await generate_go_assembly_symabisfile(
×
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
×
754
        if symabis_result is None:
×
755
            return FallibleBuiltGoPackage(
×
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(
×
763
            MergeDigests([input_digest, symabis_result.symabis_digest])
764
        )
765
        symabis_path = symabis_result.symabis_path
×
766

767
    # Build the arguments for compiling the Go code in this package.
768
    compile_args = [
×
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")
×
787
    if go_root.is_compatible_version(go_version):
×
788
        compile_args.extend(["-lang", f"go{go_version}"])
×
789

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

793
    compiling_runtime = request.is_stdlib and request.import_path in (
×
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:
×
814
        compile_args.append("-+")
×
815

816
    if symabis_path:
×
817
        compile_args.extend(["-symabis", symabis_path])
×
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
×
822
    if s_files:
×
823
        if os.path.isabs(request.dir_path):
×
824
            asm_header_path = "go_asm.h"
×
825
        else:
826
            asm_header_path = os.path.join(request.dir_path, "go_asm.h")
×
827
        compile_args.extend(["-asmhdr", asm_header_path])
×
828

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

832
    if request.build_opts.with_race_detector:
×
833
        compile_args.append("-race")
×
834

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

838
    if request.build_opts.with_asan:
×
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:
×
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 (
×
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")
×
859

860
    # Add any extra compiler flags after the ones added automatically by this rule.
861
    if request.build_opts.compiler_flags:
×
862
        compile_args.extend(request.build_opts.compiler_flags)
×
863
    if request.pkg_specific_compiler_flags:
×
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:
×
870
        compile_args = [arg for arg in compile_args if arg != "-N"]
×
871

872
    go_file_paths = (
×
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 ()
×
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])
×
883
    go_sources_file_paths_digest = await create_digest(
×
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]))
×
887
    compile_args.append("@__sources__.txt")
×
888

889
    compile_result = await execute_process(
×
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:
×
902
        return FallibleBuiltGoPackage(
×
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
×
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:
×
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
×
919
        asm_header_digest = await digest_subset_to_digest(
×
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]))
×
930
        assembly_fallible_result = await assemble_go_assembly_files(
×
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
×
941
        if assembly_result is None:
×
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)
×
950

951
    # If there are any loose object files, link them into the package archive.
952
    if objects:
×
953
        assembly_link_input_digest = await merge_digests(
×
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(
×
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
×
967

968
    path_prefix = os.path.join("__pkgs__", path_safe(request.import_path))
×
969
    import_paths_to_pkg_a_files[request.import_path] = os.path.join(path_prefix, "__pkg__.a")
×
970
    output_digest = await add_prefix(AddPrefix(compilation_digest, path_prefix))
×
971
    merged_result_digest = await merge_digests(MergeDigests([*dep_digests, output_digest]))
×
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:
×
977
        merged_result_digest = await merge_digests(
×
978
            MergeDigests([merged_result_digest, request.digest])
979
        )
980

981
    coverage_metadata = (
×
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(
×
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)
×
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:
×
1003
        return fallible_result.output
×
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