• 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

32.81
/src/python/pants/backend/go/util_rules/cgo.py
1
# Copyright 2022 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 logging
11✔
7
import os
11✔
8
import shlex
11✔
9
import textwrap
11✔
10
from collections.abc import Iterable
11✔
11
from dataclasses import dataclass
11✔
12
from pathlib import PurePath
11✔
13

14
from pants.backend.go.subsystems.golang import GolangSubsystem
11✔
15
from pants.backend.go.util_rules import cgo_binaries, cgo_pkgconfig
11✔
16
from pants.backend.go.util_rules.build_opts import GoBuildOptions
11✔
17
from pants.backend.go.util_rules.cgo_binaries import CGoBinaryPathRequest, find_cgo_binary_path
11✔
18
from pants.backend.go.util_rules.cgo_pkgconfig import (
11✔
19
    CGoPkgConfigFlagsRequest,
20
    resolve_cgo_pkg_config_args,
21
)
22
from pants.backend.go.util_rules.cgo_security import check_linker_flags
11✔
23
from pants.backend.go.util_rules.goroot import GoRoot
11✔
24
from pants.backend.go.util_rules.sdk import GoSdkProcess
11✔
25
from pants.base.glob_match_error_behavior import GlobMatchErrorBehavior
11✔
26
from pants.core.util_rules.env_vars import environment_vars_subset
11✔
27
from pants.core.util_rules.system_binaries import BinaryPathTest, get_bash
11✔
28
from pants.engine.engine_aware import EngineAwareParameter
11✔
29
from pants.engine.env_vars import EnvironmentVarsRequest
11✔
30
from pants.engine.fs import CreateDigest, DigestSubset, Directory, FileContent, PathGlobs
11✔
31
from pants.engine.internals.native_engine import EMPTY_DIGEST, Digest, MergeDigests
11✔
32
from pants.engine.internals.selectors import concurrently
11✔
33
from pants.engine.intrinsics import (
11✔
34
    create_digest,
35
    execute_process,
36
    get_digest_contents,
37
    merge_digests,
38
)
39
from pants.engine.process import FallibleProcessResult, Process, fallible_to_exec_result_or_raise
11✔
40
from pants.engine.rules import collect_rules, implicitly, rule
11✔
41
from pants.util.logging import LogLevel
11✔
42

43
_logger = logging.getLogger(__name__)
11✔
44

45

46
# Adapted from the Go toolchain.
47
# See generally https://github.com/golang/go/blob/master/src/cmd/go/internal/work/exec.go.
48
#
49
# Original copyright:
50
#   // Copyright 2011 The Go Authors. All rights reserved.
51
#   // Use of this source code is governed by a BSD-style
52
#   // license that can be found in the LICENSE file.
53

54

55
@dataclass(frozen=True)
11✔
56
class CGoCompileRequest(EngineAwareParameter):
11✔
57
    import_path: str
11✔
58
    pkg_name: str
11✔
59
    digest: Digest
11✔
60
    build_opts: GoBuildOptions
11✔
61
    dir_path: str
11✔
62
    cgo_files: tuple[str, ...]
11✔
63
    cgo_flags: CGoCompilerFlags
11✔
64
    c_files: tuple[str, ...] = ()
11✔
65
    cxx_files: tuple[str, ...] = ()
11✔
66
    objc_files: tuple[str, ...] = ()
11✔
67
    fortran_files: tuple[str, ...] = ()
11✔
68
    s_files: tuple[str, ...] = ()
11✔
69
    is_stdlib: bool = False
11✔
70
    transitive_prebuilt_object_files: tuple[Digest, frozenset[str]] | None = None
11✔
71

72
    def debug_hint(self) -> str | None:
11✔
73
        return self.import_path
×
74

75

76
@dataclass(frozen=True)
11✔
77
class CGoCompileResult:
11✔
78
    digest: Digest
11✔
79
    output_go_files: tuple[str, ...]
11✔
80
    output_obj_files: tuple[str, ...]
11✔
81

82
    # If True, then include the module sources in the same Digest as the package archive. This supports
83
    # cgo usages where the package wants to link with a static archive embedded in the module, for example,
84
    # https://github.com/confluentinc/confluent-kafka-go.
85
    include_module_sources_with_output: bool
11✔
86

87

88
@dataclass(frozen=True)
11✔
89
class CGoCompilerFlags:
11✔
90
    cflags: tuple[str, ...]
11✔
91
    cppflags: tuple[str, ...]
11✔
92
    cxxflags: tuple[str, ...]
11✔
93
    fflags: tuple[str, ...]
11✔
94
    ldflags: tuple[str, ...]
11✔
95
    pkg_config: tuple[str, ...]
11✔
96

97
    @classmethod
11✔
98
    def empty(cls) -> CGoCompilerFlags:
11✔
99
        return cls(
×
100
            cflags=(),
101
            cppflags=(),
102
            cxxflags=(),
103
            fflags=(),
104
            ldflags=(),
105
            pkg_config=(),
106
        )
107

108

109
@dataclass(frozen=True)
11✔
110
class CheckCompilerSupportsFlagRequest:
11✔
111
    cc: str
11✔
112
    flag: str
11✔
113

114

115
@dataclass(frozen=True)
11✔
116
class CheckCompilerSupportsOptionResult:
11✔
117
    supports_flag: bool
11✔
118

119

120
# Logic and comments in this rule come from `go` at:
121
# https://github.com/golang/go/blob/7eaad60737bc507596c56cec4951b089596ccc9e/src/cmd/go/internal/work/exec.go#L2570
122
@rule
11✔
123
async def check_compiler_supports_flag(
11✔
124
    request: CheckCompilerSupportsFlagRequest, goroot: GoRoot
125
) -> CheckCompilerSupportsOptionResult:
126
    input_digest = EMPTY_DIGEST
×
127
    tmp_file = "/dev/null"
×
128
    if goroot.goos == "windows":
×
129
        input_digest = await create_digest(CreateDigest([FileContent("grok.c", b"")]))
×
130
        tmp_file = "grok.c"
×
131

132
    # We used to write an empty C file, but that gets complicated with
133
    # go build -n. We tried using a file that does not exist, but that
134
    # fails on systems with GCC version 4.2.1; that is the last GPLv2
135
    # version of GCC, so some systems have frozen on it.
136
    # Now we pass an empty file on stdin, which should work at least for
137
    # GCC and clang.
138

139
    result = await execute_process(
×
140
        Process(
141
            [request.cc, request.flag, "-c", "-x", "c", "-", "-o", tmp_file],
142
            input_digest=input_digest,
143
            env={
144
                "LC_ALL": "C",
145
            },
146
            description=f"Check whether compiler `{request.cc}` for Cgo supports flag `{request.flag}`",
147
            level=LogLevel.DEBUG,
148
        ),
149
        **implicitly(),
150
    )
151

152
    # GCC says "unrecognized command line option".
153
    # clang says "unknown argument".
154
    # Older versions of GCC say "unrecognised debug output level".
155
    # For -fsplit-stack GCC says "'-fsplit-stack' is not supported".
156
    combined_output = result.stdout + result.stderr
×
157
    supported = (
×
158
        b"unrecognized" not in combined_output
159
        and b"unknown" not in combined_output
160
        and b"unrecognised" not in combined_output
161
        and b"is not supported" not in combined_output
162
    )
163
    return CheckCompilerSupportsOptionResult(supported)
×
164

165

166
@dataclass(frozen=True)
11✔
167
class SetupCompilerCmdRequest:
11✔
168
    compiler: tuple[str, ...]
11✔
169
    include_dir: str
11✔
170

171

172
@dataclass(frozen=True)
11✔
173
class SetupCompilerCmdResult:
11✔
174
    args: tuple[str, ...]
11✔
175

176

177
# Logic and comments in this rule come from `go` toolchain.
178
# Note: Commented-out Go code remains in this function because it was not clear yet how to adapt that code.
179
def _gcc_arch_args(goroot: GoRoot) -> list[str]:
11✔
180
    goarch = goroot.goarch
×
181
    if goarch == "386":
×
182
        return ["-m32"]
×
183
    elif goarch == "amd64":
×
184
        if goroot.goos == "darwin":
×
185
            return ["-arch", "x86_64", "-m64"]
×
186
        return ["-m64"]
×
187
    elif goarch == "arm64":
×
188
        if goroot.goos == "darwin":
×
189
            return ["-arch", "arm64"]
×
190
    elif goarch == "arm":
×
191
        return ["-marm"]  # not thumb
×
192
    elif goarch == "s390x":
×
193
        return ["-m64", "-march=z196"]
×
194
    elif goarch in ("mips64", "mips64le"):
×
195
        args = ["-mabi=64"]
×
196
        # if cfg.GOMIPS64 == "hardfloat" {
197
        # return append(args, "-mhard-float")
198
        # } else if cfg.GOMIPS64 == "softfloat" {
199
        # return append(args, "-msoft-float")
200
        # }
201
        return args
×
202
    elif goarch in ("mips", "mipsle"):
×
203
        args = ["-mabi=32", "-march=mips32"]
×
204
        # if cfg.GOMIPS == "hardfloat" {
205
        #     return append(args, "-mhard-float", "-mfp32", "-mno-odd-spreg")
206
        # } else if cfg.GOMIPS == "softfloat" {
207
        #     return append(args, "-msoft-float")
208
        # }
209
        return args
×
210
    elif goarch == "ppc64":
×
211
        if goroot.goos == "aix":
×
212
            return ["-maix64"]
×
213
    return []
×
214

215

216
# Note: This function is adapted mostly from the Go toolchain. Comments are generally from the adapted
217
# function. Portions that did not make sense to adapt yet have been commented out.
218
@rule
11✔
219
async def setup_compiler_cmd(
11✔
220
    request: SetupCompilerCmdRequest, goroot: GoRoot
221
) -> SetupCompilerCmdResult:
222
    args = [*request.compiler, "-I", request.include_dir]
×
223

224
    # Definitely want -fPIC but on Windows gcc complains
225
    # "-fPIC ignored for target (all code is position independent)"
226
    if goroot.goos != "windows":
×
227
        args.append("-fPIC")
×
228
    args.extend(_gcc_arch_args(goroot))
×
229

230
    # gcc-4.5 and beyond require explicit "-pthread" flag
231
    # for multithreading with pthread library.
232
    # TODO: Disable this if cgo disabled?
233
    #   `go` code has conditional: if cfg.BuildContext.CgoEnabled
234
    #   but this file is cgo only
235
    if goroot.goos == "windows":
×
236
        args.append("-mthreads")
×
237
    else:
238
        args.append("-pthread")
×
239

240
    if goroot.goos == "aix":
×
241
        # mcmodel=large must always be enabled to allow large TOC.
242
        args.append("-mcmodel=large")
×
243

244
    # disable ASCII art in clang errors, if possible
245
    supports_no_caret_diagnostics = await check_compiler_supports_flag(
×
246
        CheckCompilerSupportsFlagRequest(request.compiler[0], "-fno-caret-diagnostics"),
247
        **implicitly(),
248
    )
249
    if supports_no_caret_diagnostics.supports_flag:
×
250
        args.append("-fno-caret-diagnostics")
×
251
    # clang is too smart about command-line arguments
252
    supports_unused_arguments = await check_compiler_supports_flag(
×
253
        CheckCompilerSupportsFlagRequest(request.compiler[0], "-Qunused-arguments"), **implicitly()
254
    )
255
    if supports_unused_arguments.supports_flag:
×
256
        args.append("-Qunused-arguments")
×
257

258
    # disable word wrapping in error messages
259
    args.append("-fmessage-length=0")
×
260

261
    # Tell gcc not to include the work directory in object files.
262
    # if b.gccSupportsFlag(compiler, "-fdebug-prefix-map=a=b") {
263
    # if workdir == "" {
264
    # workdir = b.WorkDir
265
    # }
266
    # workdir = strings.TrimSuffix(workdir, string(filepath.Separator))
267
    # a = append(a, "-fdebug-prefix-map="+workdir+"=/tmp/go-build")
268
    # }
269

270
    # Tell gcc not to include flags in object files, which defeats the
271
    # point of -fdebug-prefix-map above.
272
    supports_no_record_gcc_switches = await check_compiler_supports_flag(
×
273
        CheckCompilerSupportsFlagRequest(request.compiler[0], "-gno-record-gcc-switches"),
274
        **implicitly(),
275
    )
276
    if supports_no_record_gcc_switches.supports_flag:
×
277
        args.append("-gno-record-gcc-switches")
×
278

279
    # On OS X, some of the compilers behave as if -fno-common
280
    # is always set, and the Mach-O linker in 6l/8l assumes this.
281
    # See https://golang.org/issue/3253.
282
    if goroot.goos == "darwin" or goroot.goos == "ios":
×
283
        args.append("-fno-common")
×
284

285
    return SetupCompilerCmdResult(tuple(args))
×
286

287

288
@dataclass(frozen=True)
11✔
289
class CGoCompilerWrapperScript:
11✔
290
    digest: Digest
11✔
291

292

293
@rule
11✔
294
async def make_cgo_compile_wrapper_script() -> CGoCompilerWrapperScript:
11✔
295
    digest = await create_digest(
×
296
        CreateDigest(
297
            [
298
                FileContent(
299
                    path="wrapper",
300
                    content=textwrap.dedent(
301
                        """\
302
                sandbox_root="$(/bin/pwd)"
303
                args=("${@//__PANTS_SANDBOX_ROOT__/$sandbox_root}")
304
                exec "${args[@]}"
305
                """
306
                    ).encode(),
307
                    is_executable=True,
308
                )
309
            ]
310
        )
311
    )
312
    return CGoCompilerWrapperScript(digest=digest)
×
313

314

315
async def _cc(
11✔
316
    binary_name: str,
317
    input_digest: Digest,
318
    dir_path: str,
319
    src_file: str,
320
    flags: Iterable[str],
321
    obj_file: str,
322
    description: str,
323
    golang_env_aware: GolangSubsystem.EnvironmentAware,
324
) -> Process:
325
    compiler_path, bash, wrapper_script = await concurrently(
×
326
        find_cgo_binary_path(
327
            CGoBinaryPathRequest(
328
                binary_name=binary_name,
329
                binary_path_test=BinaryPathTest(["--version"]),
330
            ),
331
            **implicitly(),
332
        ),
333
        get_bash(**implicitly()),
334
        make_cgo_compile_wrapper_script(),
335
    )
336
    compiler_args_result, env, input_digest = await concurrently(
×
337
        setup_compiler_cmd(
338
            SetupCompilerCmdRequest((compiler_path.path,), dir_path), **implicitly()
339
        ),
340
        environment_vars_subset(
341
            EnvironmentVarsRequest(golang_env_aware.env_vars_to_pass_to_subprocesses),
342
            **implicitly(),
343
        ),
344
        merge_digests(MergeDigests([input_digest, wrapper_script.digest])),
345
    )
346
    replaced_flags = _replace_srcdir_in_flags(flags, dir_path)
×
347
    args = [
×
348
        bash.path,
349
        "./wrapper",
350
        *compiler_args_result.args,
351
        *replaced_flags,
352
        "-o",
353
        obj_file,
354
        "-c",
355
        src_file,
356
    ]
357
    return Process(
×
358
        argv=args,
359
        env={"TERM": "dumb", **env},
360
        input_digest=input_digest,
361
        output_files=(obj_file,),
362
        description=description,
363
        level=LogLevel.DEBUG,
364
    )
365

366

367
async def _gccld(
11✔
368
    binary_name: str,
369
    input_digest: Digest,
370
    dir_path: str,
371
    outfile: str,
372
    flags: Iterable[str],
373
    objs: Iterable[str],
374
    description: str,
375
) -> FallibleProcessResult:
376
    compiler_path, bash, wrapper_script = await concurrently(
×
377
        find_cgo_binary_path(
378
            CGoBinaryPathRequest(
379
                binary_name=binary_name,
380
                binary_path_test=BinaryPathTest(["--version"]),
381
            ),
382
            **implicitly(),
383
        ),
384
        get_bash(**implicitly()),
385
        make_cgo_compile_wrapper_script(),
386
    )
387

388
    compiler_args_result, env, input_digest = await concurrently(
×
389
        setup_compiler_cmd(
390
            SetupCompilerCmdRequest((compiler_path.path,), dir_path), **implicitly()
391
        ),
392
        environment_vars_subset(EnvironmentVarsRequest(["PATH"]), **implicitly()),
393
        merge_digests(MergeDigests([input_digest, wrapper_script.digest])),
394
    )
395

396
    replaced_flags_in_compiler_args = _replace_srcdir_in_flags(compiler_args_result.args, dir_path)
×
397
    replaced_other_flags = _replace_srcdir_in_flags(flags, dir_path)
×
398

399
    args = [
×
400
        bash.path,
401
        "./wrapper",
402
        *replaced_flags_in_compiler_args,
403
        "-o",
404
        outfile,
405
        *objs,
406
        *replaced_other_flags,
407
    ]
408

409
    result = await execute_process(
×
410
        Process(
411
            argv=args,
412
            env={"TERM": "dumb", **env},
413
            input_digest=input_digest,
414
            output_files=(outfile,),
415
            description=description,
416
            level=LogLevel.DEBUG,
417
        ),
418
        **implicitly(),
419
    )
420

421
    # TODO(#16828): Filter out output with irrelevant warnings just like `go` tool does.
422

423
    return result
×
424

425

426
@dataclass(frozen=True)
11✔
427
class _DynImportResult:
11✔
428
    digest: Digest
11✔
429
    dyn_out_go: str | None  # if not empty, is a new Go file to build as part of the package.
11✔
430
    dyn_out_obj: str | None  # if not empty, is a new file to add to the generated archive.
11✔
431
    use_external_link: bool
11✔
432

433

434
# From Go comments:
435
#   dynimport creates a Go source file named importGo containing
436
#   //go:cgo_import_dynamic directives for each symbol or library
437
#   dynamically imported by the object files outObj.
438
#   dynOutObj, if not empty, is a new file to add to the generated archive.'
439
#
440
# see https://github.com/golang/go/blob/f28fa952b5f81a63afd96c9c58dceb99cc7d1dbf/src/cmd/go/internal/work/exec.go#L3020
441
#
442
# Note: Commented-out Go code remains in this function because it was not clear yet how to adapt that code.
443
async def _dynimport(
11✔
444
    import_path: str,
445
    input_digest: Digest,
446
    obj_files: Iterable[str],
447
    dir_path: str,
448
    obj_dir_path: str,
449
    cflags: Iterable[str],
450
    ldflags: Iterable[str],
451
    pkg_name: str,
452
    goroot: GoRoot,
453
    import_go_path: str,
454
    golang_env_aware: GolangSubsystem.EnvironmentAware,
455
    use_cxx_linker: bool,
456
    transitive_prebuilt_objects_digest: Digest,
457
    transitive_prebuilt_objects: frozenset[str],
458
) -> _DynImportResult:
459
    cgo_main_compile_process = await _cc(
×
460
        binary_name=golang_env_aware.cgo_gcc_binary_name,
461
        input_digest=input_digest,
462
        dir_path=dir_path,
463
        src_file=os.path.join(obj_dir_path, "_cgo_main.c"),
464
        flags=cflags,
465
        obj_file=os.path.join(obj_dir_path, "_cgo_main.o"),
466
        description=f"Compile _cgo_main.c ({import_path})",
467
        golang_env_aware=golang_env_aware,
468
    )
469
    cgo_main_compile_result = await fallible_to_exec_result_or_raise(
×
470
        **implicitly(cgo_main_compile_process)
471
    )
472
    obj_digest = await merge_digests(
×
473
        MergeDigests(
474
            [
475
                input_digest,
476
                cgo_main_compile_result.output_digest,
477
                transitive_prebuilt_objects_digest,
478
            ]
479
        ),
480
    )
481

482
    dynobj = os.path.join(obj_dir_path, "_cgo_.o")
×
483
    ldflags = list(ldflags)
×
484
    if (goroot.goarch == "arm" and goroot.goos == "linux") or goroot.goos == "android":
×
485
        if "-no-pie" not in ldflags:
×
486
            # we need to use -pie for Linux/ARM to get accurate imported sym (added in https://golang.org/cl/5989058)
487
            # this seems to be outdated, but we don't want to break existing builds depending on this (Issue 45940)
488
            ldflags.append("-pie")
×
489
        if "-pie" in ldflags and "-static" in ldflags:
×
490
            # -static -pie doesn't make sense, and causes link errors.
491
            # Issue 26197.
492
            ldflags = [arg for arg in ldflags if arg != "-static"]
×
493

494
    linker_binary_name = (
×
495
        golang_env_aware.cgo_gxx_binary_name
496
        if use_cxx_linker
497
        else golang_env_aware.cgo_gcc_binary_name
498
    )
499

500
    cgo_binary_link_result = await _gccld(
×
501
        binary_name=linker_binary_name,
502
        input_digest=obj_digest,
503
        dir_path=dir_path,
504
        outfile=dynobj,
505
        flags=ldflags,
506
        objs=[
507
            *obj_files,
508
            os.path.join(obj_dir_path, "_cgo_main.o"),
509
            *sorted(transitive_prebuilt_objects),
510
        ],
511
        description=f"Link _cgo_.o ({import_path})",
512
    )
513
    if cgo_binary_link_result.exit_code != 0:
×
514
        # From `go` source:
515
        #   We only need this information for internal linking.
516
        #   If this link fails, mark the object as requiring
517
        #   external linking. This link can fail for things like
518
        #   syso files that have unexpected dependencies.
519
        #   cmd/link explicitly looks for the name "dynimportfail".
520
        #   See issue #52863.
521
        _logger.info(
×
522
            f"cgo binary link failed:\n"
523
            f"stdout:\n{cgo_binary_link_result.stdout.decode()}\n"
524
            f"stderr:\n{cgo_binary_link_result.stderr.decode()}\n"
525
        )
526
        # return _DynImportResult(digest=EMPTY_DIGEST, dyn_out_go=None, dyn_out_obj=None, use_external_link=True)
527
        # If linking the binary for cgo fails, this is usually because the
528
        # object files reference external symbols that can't be resolved yet.
529
        # Since the binary is only produced to have its symbols read by the cgo
530
        # command, there is no harm in trying to build it allowing unresolved
531
        # symbols - the real link that happens at the end will fail if they
532
        # rightfully can't be resolved.
533
        if goroot.goos == "windows":
×
534
            # MinGW's linker doesn't seem to support --unresolved-symbols
535
            # and MSVC isn't supported at all.
536
            raise ValueError("link error - no workaround on Windows")
×
537
        elif goroot.goos in ("darwin", "ios"):
×
538
            allow_unresolved_symbols_ldflag = "-Wl,-undefined,dynamic_lookup"
×
539
        else:
540
            allow_unresolved_symbols_ldflag = "-Wl,--unresolved-symbols=ignore-all"
×
541
        # Print and return the original error if we can't link the binary with
542
        # the additional linker flags as they may simply be incorrect for the
543
        # particular compiler/linker pair and would obscure the true reason for
544
        # the failure of the original command.
545
        cgo_binary_link_result = await _gccld(
×
546
            binary_name=linker_binary_name,
547
            input_digest=obj_digest,
548
            dir_path=dir_path,
549
            outfile=dynobj,
550
            flags=[*ldflags, allow_unresolved_symbols_ldflag],
551
            objs=obj_files,
552
            description=f"Link _cgo_.o ({import_path})",
553
        )
554
        if cgo_binary_link_result.exit_code != 0:
×
555
            raise ValueError(
×
556
                f"cgo binary link failed:\n"
557
                f"stdout:\n{cgo_binary_link_result.stdout.decode()}\n"
558
                f"stderr:\n{cgo_binary_link_result.stderr.decode()}\n"
559
            )
560

561
    # cgo -dynimport
562
    dynimport_process_result = await fallible_to_exec_result_or_raise(
×
563
        **implicitly(
564
            GoSdkProcess(
565
                command=[
566
                    "tool",
567
                    "cgo",
568
                    # record path to dynamic linker
569
                    *(["-dynlinker"] if import_path == "runtime/cgo" else []),
570
                    "-trimpath",
571
                    "__PANTS_SANDBOX_ROOT__",
572
                    "-dynpackage",
573
                    pkg_name,
574
                    "-dynimport",
575
                    dynobj,
576
                    "-dynout",
577
                    import_go_path,
578
                ],
579
                description="Gather cgo dynimport data.",
580
                env={"TERM": "dumb"},
581
                input_digest=cgo_binary_link_result.output_digest,
582
                output_files=(import_go_path,),
583
                replace_sandbox_root_in_args=True,
584
            ),
585
        )
586
    )
587
    return _DynImportResult(
×
588
        digest=dynimport_process_result.output_digest,
589
        dyn_out_go=import_go_path,
590
        dyn_out_obj=None,
591
        use_external_link=False,
592
    )
593

594

595
# Note: Comments are mostly from the original function in Go toolchain sources.
596
def _check_link_args_in_content(src: bytes):
11✔
597
    cgo_ldflag_directive = b"//go:cgo_ldflag"
×
598
    idx = src.find(cgo_ldflag_directive)
×
599
    flags = []
×
600
    while idx >= 0:
×
601
        # We are looking at //go:cgo_ldflag.
602
        # Find start of line.
603
        start = src[:idx].rfind(b"\n")
×
604
        if start == -1:
×
605
            start = 0
×
606

607
        # Find end of line.
608
        end = src[idx:].find(b"\n")
×
609
        if end == -1:
×
610
            end = len(src)
×
611
        else:
612
            end += idx
×
613

614
        # Check for first line comment in line.
615
        # We don't worry about /* */ comments,
616
        # which normally won't appear in files
617
        # generated by cgo.
618
        comment_start = src[start:].find(b"//")
×
619
        comment_start += start
×
620
        # If that line comment is //go:cgo_ldflag,
621
        # it's a match.
622
        if src[comment_start:].startswith(cgo_ldflag_directive):
×
623
            # Pull out the flag, and unquote it.
624
            # This is what the compiler does.
625
            flag = src[idx + len(cgo_ldflag_directive) : end].decode()
×
626
            flag = flag.strip()
×
627
            flag = flag.strip('"')
×
628
            flags.append(flag)
×
629

630
        src = src[end:]
×
631
        idx = src.find(cgo_ldflag_directive)
×
632

633
    check_linker_flags(flags, "go:cgo_ldflag")
×
634

635

636
async def _ensure_only_allowed_link_args(
11✔
637
    digest: Digest, dir_path: str, go_files: Iterable[str]
638
) -> None:
639
    cgo_go_files = [
×
640
        os.path.join(dir_path, go_file) for go_file in go_files if go_file.startswith("_cgo_")
641
    ]
642
    digest_contents = await get_digest_contents(
×
643
        **implicitly(
644
            DigestSubset(
645
                digest,
646
                PathGlobs(
647
                    globs=cgo_go_files,
648
                    glob_match_error_behavior=GlobMatchErrorBehavior.error,
649
                    description_of_origin="cgo-related go_files",
650
                ),
651
            )
652
        )
653
    )
654

655
    for entry in digest_contents:
×
656
        _check_link_args_in_content(entry.content)
×
657

658

659
def _replace_srcdir_in_arg(flag: str, dir_path: str) -> str:
11✔
660
    if "${SRCDIR}" in flag:
×
661
        return flag.replace("${SRCDIR}", f"__PANTS_SANDBOX_ROOT__/{dir_path}")
×
662
    else:
663
        return flag
×
664

665

666
def _replace_srcdir_in_flags(flags: Iterable[str], dir_path: str) -> tuple[str, ...]:
11✔
667
    return tuple(_replace_srcdir_in_arg(flag, dir_path) for flag in flags)
×
668

669

670
@rule
11✔
671
async def cgo_compile_request(
11✔
672
    request: CGoCompileRequest, goroot: GoRoot, golang_env_aware: GolangSubsystem.EnvironmentAware
673
) -> CGoCompileResult:
674
    dir_path = request.dir_path if request.dir_path else "."
×
675

676
    obj_dir_path = (
×
677
        f"__go_stdlib_obj__/{request.import_path}" if os.path.isabs(dir_path) else dir_path
678
    )
679
    cgo_input_digest = request.digest
×
680
    if os.path.isabs(dir_path):
×
681
        mkdir_digest = await create_digest(CreateDigest([Directory(obj_dir_path)]))
×
682
        cgo_input_digest = await merge_digests(MergeDigests([cgo_input_digest, mkdir_digest]))
×
683

684
    # Extract the cgo flags instance from the request so it can be updated as necessary.
685
    flags = request.cgo_flags
×
686

687
    # Prepend the default compiler options (`-g -O2`) before any package-specific options extracted from cgo
688
    # directives.
689
    flags = dataclasses.replace(
×
690
        flags,
691
        cflags=golang_env_aware.cgo_c_flags + flags.cflags,
692
        cxxflags=golang_env_aware.cgo_cxx_flags + flags.cxxflags,
693
        fflags=golang_env_aware.cgo_fortran_flags + flags.fflags,
694
        ldflags=golang_env_aware.cgo_linker_flags + flags.ldflags,
695
    )
696

697
    # Resolve pkg-config flags into compiler and linker flags.
698
    if request.cgo_flags.pkg_config:
×
699
        pkg_config_flags = await resolve_cgo_pkg_config_args(
×
700
            CGoPkgConfigFlagsRequest(
701
                pkg_config_args=request.cgo_flags.pkg_config,
702
            )
703
        )
704
        flags = dataclasses.replace(
×
705
            flags,
706
            cppflags=flags.cppflags + pkg_config_flags.cflags,
707
            ldflags=flags.ldflags + pkg_config_flags.ldflags,
708
        )
709

710
    # If compiling C++, then link against C++ standard library.
711
    if request.cxx_files:
×
712
        flags = dataclasses.replace(flags, ldflags=flags.ldflags + ("-lstdc++",))
×
713

714
    # If we are compiling Objective-C code, then we need to link against libobjc
715
    if request.objc_files:
×
716
        flags = dataclasses.replace(flags, ldflags=flags.ldflags + ("-lobjc",))
×
717

718
    # Likewise for Fortran, except there are many Fortran compilers.
719
    # Support gfortran out of the box and let others pass the correct link options
720
    # via CGO_LDFLAGS
721
    if request.fortran_files and "gfortran" in golang_env_aware.cgo_fortran_binary_name:
×
722
        flags = dataclasses.replace(flags, ldflags=flags.ldflags + ("-lgfortran",))
×
723

724
    if request.build_opts.with_msan:
×
725
        flags = dataclasses.replace(
×
726
            flags,
727
            cflags=flags.cflags + ("-fsanitize=memory",),
728
            ldflags=flags.ldflags + ("-fsanitize=memory",),
729
        )
730

731
    if request.build_opts.with_asan:
×
732
        flags = dataclasses.replace(
×
733
            flags,
734
            cflags=flags.cflags + ("-fsanitize=address",),
735
            ldflags=flags.ldflags + ("-fsanitize=address",),
736
        )
737

738
    # Allows including _cgo_export.h, as well as the user's .h files,
739
    # from .[ch] files in the package.
740
    flags = dataclasses.replace(flags, cflags=flags.cflags + ("-I", dir_path))
×
741

742
    # Replace `${SRCDIR}` in LDFLAGS with the path to the source directory within the sandbox.
743
    # From Go docs:
744
    #   When the cgo directives are parsed, any occurrence of the string ${SRCDIR} will be replaced by the
745
    #   absolute path to the directory containing the source file. This allows pre-compiled static libraries
746
    #   to be included in the package directory and linked properly. For example if package foo is in the
747
    #   directory /go/src/foo:
748
    flags = CGoCompilerFlags(
×
749
        cflags=_replace_srcdir_in_flags(flags.cflags, dir_path),
750
        cppflags=_replace_srcdir_in_flags(flags.cppflags, dir_path),
751
        cxxflags=_replace_srcdir_in_flags(flags.cxxflags, dir_path),
752
        fflags=_replace_srcdir_in_flags(flags.fflags, dir_path),
753
        ldflags=_replace_srcdir_in_flags(flags.ldflags, dir_path),
754
        pkg_config=flags.pkg_config,
755
    )
756

757
    go_files: list[str] = [os.path.join(obj_dir_path, "_cgo_gotypes.go")]
×
758
    gcc_files: list[str] = [
×
759
        os.path.join(obj_dir_path, "_cgo_export.c"),
760
        *(os.path.join(dir_path, c_file) for c_file in request.c_files),
761
        *(os.path.join(dir_path, s_file) for s_file in request.s_files),
762
    ]
763
    for cgo_file in request.cgo_files:
×
764
        cgo_file_path = PurePath(cgo_file)
×
765
        stem = cgo_file_path.stem
×
766
        go_files.append(os.path.join(obj_dir_path, f"{stem}.cgo1.go"))
×
767
        gcc_files.append(os.path.join(obj_dir_path, f"{stem}.cgo2.c"))
×
768

769
    # When building certain parts of the standard library, disable certain imports in generated code.
770
    maybe_disable_imports_flags: list[str] = []
×
771
    if request.is_stdlib and request.import_path == "runtime/cgo":
×
772
        maybe_disable_imports_flags.append("-import_runtime_cgo=false")
×
773
    if request.is_stdlib and request.import_path in (
×
774
        "runtime/race",
775
        "runtime/msan",
776
        "runtime/cgo",
777
        "runtime/asan",
778
    ):
779
        maybe_disable_imports_flags.append("-import_syscall=false")
×
780

781
    # Update CGO_LDFLAGS with the configured linker flags.
782
    #
783
    # From Go sources:
784
    #   These flags are recorded in the generated _cgo_gotypes.go file
785
    #   using //go:cgo_ldflag directives, the compiler records them in the
786
    #   object file for the package, and then the Go linker passes them
787
    #   along to the host linker. At this point in the code, cgoLDFLAGS
788
    #   consists of the original $CGO_LDFLAGS (unchecked) and all the
789
    #   flags put together from source code (checked).
790
    #
791
    # Note: Some packages, e.g. https://github.com/confluentinc/confluent-kafka-go, try to link a static archive
792
    # emedded in the module into the package archive. If so, mark this package so that the module sources are
793
    # included in the output digest for the build of this package. We assume that this is needed if an earlier
794
    # replacement of `${SRCDIR}` resulted in `__PANTS_SANDBOX_ROOT__` appearing in the flags. The
795
    # `__PANTS_SANDBOX_ROOT__` will be replaced by the external linker wrapper configured in `link.py`.
796
    cgo_env = {"CGO_ENABLED": "1", "TERM": "dumb"}
×
797
    include_module_sources_with_output = False
×
798
    if flags.ldflags:
×
799
        for arg in flags.ldflags:
×
800
            if "__PANTS_SANDBOX_ROOT__" in arg:
×
801
                include_module_sources_with_output = True
×
802
        cgo_env["CGO_LDFLAGS"] = shlex.join(flags.ldflags)
×
803

804
    # Note: If Pants supported building C static or shared archives, then we would need to direct cgo here to
805
    # produce a header file via the `-exportheader` option. Not necessary since Pants does not support that.
806

807
    # Invoke cgo.
808
    cgo_result = await fallible_to_exec_result_or_raise(
×
809
        **implicitly(
810
            GoSdkProcess(
811
                [
812
                    "tool",
813
                    "cgo",
814
                    "-objdir",
815
                    obj_dir_path,
816
                    "-importpath",
817
                    request.import_path,
818
                    *maybe_disable_imports_flags,
819
                    "-trimpath",
820
                    "__PANTS_SANDBOX_ROOT__",
821
                    "--",
822
                    *flags.cppflags,
823
                    *flags.cflags,
824
                    *(os.path.join(dir_path, f) for f in request.cgo_files),
825
                ],
826
                env=cgo_env,
827
                description=f"Generate Go and C files from CGo files ({request.import_path})",
828
                input_digest=cgo_input_digest,
829
                output_directories=(obj_dir_path,),
830
                replace_sandbox_root_in_args=True,
831
            ),
832
        )
833
    )
834

835
    out_obj_files: list[str] = []
×
836
    oseq = 0
×
837
    compile_process_gets = []
×
838

839
    # C files
840
    cflags = [*flags.cppflags, *flags.cflags]
×
841
    for gcc_file in gcc_files:
×
842
        ofile = os.path.join(obj_dir_path, f"_x{oseq:03}.o")
×
843
        oseq = oseq + 1
×
844
        out_obj_files.append(ofile)
×
845

846
        compile_process = await _cc(
×
847
            binary_name=golang_env_aware.cgo_gcc_binary_name,
848
            input_digest=cgo_result.output_digest,
849
            dir_path=dir_path,
850
            src_file=gcc_file,
851
            flags=cflags,
852
            obj_file=ofile,
853
            description=f"Compile cgo source: {gcc_file}",
854
            golang_env_aware=golang_env_aware,
855
        )
856
        compile_process_gets.append(
×
857
            fallible_to_exec_result_or_raise(**implicitly({compile_process: Process}))
858
        )
859

860
    # C++ files
861
    cxxflags = [*flags.cppflags, *flags.cxxflags]
×
862
    for cxx_file in (os.path.join(dir_path, cxx_file) for cxx_file in request.cxx_files):
×
863
        ofile = os.path.join(obj_dir_path, f"_x{oseq:03}.o")
×
864
        oseq = oseq + 1
×
865
        out_obj_files.append(ofile)
×
866

867
        compile_process = await _cc(
×
868
            binary_name=golang_env_aware.cgo_gxx_binary_name,
869
            input_digest=cgo_result.output_digest,
870
            dir_path=dir_path,
871
            src_file=cxx_file,
872
            flags=cxxflags,
873
            obj_file=ofile,
874
            description=f"Compile cgo C++ source: {cxx_file}",
875
            golang_env_aware=golang_env_aware,
876
        )
877
        compile_process_gets.append(
×
878
            fallible_to_exec_result_or_raise(**implicitly({compile_process: Process}))
879
        )
880

881
    # Objective-C files
882
    for objc_file in (os.path.join(dir_path, objc_file) for objc_file in request.objc_files):
×
883
        ofile = os.path.join(obj_dir_path, f"_x{oseq:03}.o")
×
884
        oseq = oseq + 1
×
885
        out_obj_files.append(ofile)
×
886

887
        compile_process = await _cc(
×
888
            binary_name=golang_env_aware.cgo_gcc_binary_name,
889
            input_digest=cgo_result.output_digest,
890
            dir_path=dir_path,
891
            src_file=objc_file,
892
            flags=cflags,
893
            obj_file=ofile,
894
            description=f"Compile cgo Objective-C source: {objc_file}",
895
            golang_env_aware=golang_env_aware,
896
        )
897
        compile_process_gets.append(
×
898
            fallible_to_exec_result_or_raise(**implicitly({compile_process: Process}))
899
        )
900

901
    fflags = [*flags.cppflags, *flags.fflags]
×
902
    for fortran_file in (
×
903
        os.path.join(dir_path, fortran_file) for fortran_file in request.fortran_files
904
    ):
905
        ofile = os.path.join(obj_dir_path, f"_x{oseq:03}.o")
×
906
        oseq = oseq + 1
×
907
        out_obj_files.append(ofile)
×
908

909
        compile_process = await _cc(
×
910
            binary_name=golang_env_aware.cgo_fortran_binary_name,
911
            input_digest=cgo_result.output_digest,
912
            dir_path=dir_path,
913
            src_file=fortran_file,
914
            flags=fflags,
915
            obj_file=ofile,
916
            description=f"Compile cgo Fortran source: {fortran_file}",
917
            golang_env_aware=golang_env_aware,
918
        )
919
        compile_process_gets.append(
×
920
            fallible_to_exec_result_or_raise(**implicitly({compile_process: Process}))
921
        )
922

923
    # Dispatch all of the compilation requests.
924
    compile_results = await concurrently(compile_process_gets)
×
925
    out_obj_files_digest = await merge_digests(
×
926
        MergeDigests([r.output_digest for r in compile_results])
927
    )
928

929
    # Run dynimport process to create a Go source file named importGo containing
930
    # //go:cgo_import_dynamic directives for each symbol or library
931
    # dynamically imported by the object files outObj.
932
    dynimport_input_digest = await merge_digests(
×
933
        MergeDigests(
934
            [
935
                cgo_result.output_digest,
936
                out_obj_files_digest,
937
            ]
938
        )
939
    )
940
    transitive_prebuilt_objects_digest: Digest = EMPTY_DIGEST
×
941
    transitive_prebuilt_objects: frozenset[str] = frozenset()
×
942
    if request.transitive_prebuilt_object_files:
×
943
        transitive_prebuilt_objects_digest = request.transitive_prebuilt_object_files[0]
×
944
        transitive_prebuilt_objects = request.transitive_prebuilt_object_files[1]
×
945

946
    dynimport_result = await _dynimport(
×
947
        import_path=request.import_path,
948
        input_digest=dynimport_input_digest,
949
        dir_path=dir_path,
950
        obj_dir_path=obj_dir_path,
951
        obj_files=out_obj_files,
952
        cflags=cflags,
953
        ldflags=request.cgo_flags.ldflags,
954
        pkg_name=request.pkg_name,
955
        goroot=goroot,
956
        import_go_path=os.path.join(obj_dir_path, "_cgo_import.go"),
957
        golang_env_aware=golang_env_aware,
958
        use_cxx_linker=bool(request.cxx_files),
959
        transitive_prebuilt_objects_digest=transitive_prebuilt_objects_digest,
960
        transitive_prebuilt_objects=transitive_prebuilt_objects,
961
    )
962
    if dynimport_result.dyn_out_go:
×
963
        go_files.append(dynimport_result.dyn_out_go)
×
964
    if dynimport_result.dyn_out_obj:
×
965
        out_obj_files.append(dynimport_result.dyn_out_obj)
×
966

967
    # Double check the //go:cgo_ldflag comments in the generated files.
968
    # The compiler only permits such comments in files whose base name
969
    # starts with "_cgo_". Make sure that the comments in those files
970
    # are safe. This is a backstop against people somehow smuggling
971
    # such a comment into a file generated by cgo.
972
    await _ensure_only_allowed_link_args(cgo_result.output_digest, dir_path, go_files)
×
973

974
    output_digest = await merge_digests(
×
975
        MergeDigests(
976
            [
977
                cgo_result.output_digest,
978
                out_obj_files_digest,
979
                dynimport_result.digest,
980
            ]
981
        )
982
    )
983
    return CGoCompileResult(
×
984
        digest=output_digest,
985
        output_go_files=tuple(go_files),
986
        output_obj_files=tuple(out_obj_files),
987
        include_module_sources_with_output=include_module_sources_with_output,
988
    )
989

990

991
def rules():
11✔
992
    return (
11✔
993
        *collect_rules(),
994
        *cgo_binaries.rules(),
995
        *cgo_pkgconfig.rules(),
996
    )
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