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

pantsbuild / pants / 20328535594

18 Dec 2025 06:46AM UTC coverage: 57.969% (-22.3%) from 80.295%
20328535594

Pull #22954

github

web-flow
Merge ccc9c5409 into 407284c67
Pull Request #22954: free up disk space in runner image

39083 of 67421 relevant lines covered (57.97%)

0.91 hits per line

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

43.43
/src/python/pants/backend/python/goals/package_pex_binary.py
1
# Copyright 2019 Pants project contributors (see CONTRIBUTORS.md).
2
# Licensed under the Apache License, Version 2.0 (see LICENSE).
3

4
import dataclasses
1✔
5
import itertools
1✔
6
import logging
1✔
7
import os
1✔
8
from collections.abc import Iterable
1✔
9
from dataclasses import dataclass
1✔
10

11
from pants.backend.python.target_types import (
1✔
12
    PexArgsField,
13
    PexBinaryDefaults,
14
    PexCheckField,
15
    PexCompletePlatformsField,
16
    PexEmitWarningsField,
17
    PexEntryPointField,
18
    PexEnvField,
19
    PexExecutableField,
20
    PexExecutionMode,
21
    PexExecutionModeField,
22
    PexExtraBuildArgsField,
23
    PexIgnoreErrorsField,
24
    PexIncludeRequirementsField,
25
    PexIncludeSourcesField,
26
    PexIncludeToolsField,
27
    PexInheritPathField,
28
    PexLayout,
29
    PexLayoutField,
30
    PexScieBusyBox,
31
    PexScieBusyboxPexEntrypointEnvPassthrough,
32
    PexScieField,
33
    PexScieHashAlgField,
34
    PexScieNameStyleField,
35
    PexSciePbsReleaseField,
36
    PexSciePbsStripped,
37
    PexSciePlatformField,
38
    PexSciePythonVersion,
39
    PexScriptField,
40
    PexShBootField,
41
    PexShebangField,
42
    PexStripEnvField,
43
    PexVenvHermeticScripts,
44
    PexVenvSitePackagesCopies,
45
    ResolvePexEntryPointRequest,
46
    ScieNameStyle,
47
)
48
from pants.backend.python.target_types_rules import resolve_pex_entry_point
1✔
49
from pants.backend.python.util_rules.pex import create_pex, digest_complete_platforms
1✔
50
from pants.backend.python.util_rules.pex_from_targets import (
1✔
51
    PexFromTargetsRequest,
52
    create_pex_from_targets,
53
)
54
from pants.core.environments.target_types import EnvironmentField
1✔
55
from pants.core.goals.package import (
1✔
56
    BuiltPackage,
57
    BuiltPackageArtifact,
58
    OutputPathField,
59
    PackageFieldSet,
60
)
61
from pants.core.goals.run import RunFieldSet, RunInSandboxBehavior
1✔
62
from pants.engine.platform import Platform
1✔
63
from pants.engine.rules import collect_rules, implicitly, rule
1✔
64
from pants.engine.unions import UnionRule
1✔
65
from pants.util.frozendict import FrozenDict
1✔
66
from pants.util.logging import LogLevel
1✔
67

68
logger = logging.getLogger(__name__)
1✔
69

70

71
@dataclass(frozen=True)
1✔
72
class PexBinaryFieldSet(PackageFieldSet, RunFieldSet):
1✔
73
    run_in_sandbox_behavior = RunInSandboxBehavior.RUN_REQUEST_HERMETIC
1✔
74

75
    required_fields = (PexEntryPointField,)
1✔
76

77
    entry_point: PexEntryPointField
1✔
78
    script: PexScriptField
1✔
79
    executable: PexExecutableField
1✔
80
    args: PexArgsField
1✔
81
    env: PexEnvField
1✔
82

83
    output_path: OutputPathField
1✔
84
    emit_warnings: PexEmitWarningsField
1✔
85
    ignore_errors: PexIgnoreErrorsField
1✔
86
    inherit_path: PexInheritPathField
1✔
87
    sh_boot: PexShBootField
1✔
88
    shebang: PexShebangField
1✔
89
    strip_env: PexStripEnvField
1✔
90
    complete_platforms: PexCompletePlatformsField
1✔
91
    layout: PexLayoutField
1✔
92
    execution_mode: PexExecutionModeField
1✔
93
    include_requirements: PexIncludeRequirementsField
1✔
94
    include_sources: PexIncludeSourcesField
1✔
95
    include_tools: PexIncludeToolsField
1✔
96
    venv_site_packages_copies: PexVenvSitePackagesCopies
1✔
97
    venv_hermetic_scripts: PexVenvHermeticScripts
1✔
98
    environment: EnvironmentField
1✔
99
    check: PexCheckField
1✔
100
    extra_build_args: PexExtraBuildArgsField
1✔
101

102
    scie: PexScieField
1✔
103
    scie_name_style: PexScieNameStyleField
1✔
104
    scie_busybox: PexScieBusyBox
1✔
105
    scie_busybox_pex_entrypoint_env_passthrough: PexScieBusyboxPexEntrypointEnvPassthrough
1✔
106
    scie_platform: PexSciePlatformField
1✔
107
    scie_pbs_release: PexSciePbsReleaseField
1✔
108
    scie_python_version: PexSciePythonVersion
1✔
109
    scie_hash_alg: PexScieHashAlgField
1✔
110
    scie_pbs_stripped: PexSciePbsStripped
1✔
111

112
    def builds_pex_and_scie(self) -> bool:
1✔
113
        return self.scie.value is not None
×
114

115
    @property
1✔
116
    def _execution_mode(self) -> PexExecutionMode:
1✔
117
        return PexExecutionMode(self.execution_mode.value)
×
118

119
    def generate_additional_args(self, pex_binary_defaults: PexBinaryDefaults) -> tuple[str, ...]:
1✔
120
        args = []
×
121
        if self.emit_warnings.value_or_global_default(pex_binary_defaults) is False:
×
122
            args.append("--no-emit-warnings")
×
123
        elif self.emit_warnings.value_or_global_default(pex_binary_defaults) is True:
×
124
            args.append("--emit-warnings")
×
125
        if self.ignore_errors.value is True:
×
126
            args.append("--ignore-errors")
×
127
        if self.inherit_path.value is not None:
×
128
            args.append(f"--inherit-path={self.inherit_path.value}")
×
129
        if self.sh_boot.value is True:
×
130
            args.append("--sh-boot")
×
131
        if self.check.value is not None:
×
132
            args.append(f"--check={self.check.value}")
×
133
        if self.shebang.value is not None:
×
134
            args.append(f"--python-shebang={self.shebang.value}")
×
135
        if self.strip_env.value is False:
×
136
            args.append("--no-strip-pex-env")
×
137
        if self._execution_mode is PexExecutionMode.VENV:
×
138
            args.extend(("--venv", "prepend"))
×
139
        if self.include_tools.value is True:
×
140
            args.append("--include-tools")
×
141
        if self.venv_site_packages_copies.value is True:
×
142
            args.append("--venv-site-packages-copies")
×
143
        if self.venv_hermetic_scripts.value is False:
×
144
            args.append("--non-hermetic-venv-scripts")
×
145
        if self.extra_build_args.value:
×
146
            args.extend(self.extra_build_args.value)
×
147
        return tuple(args)
×
148

149
    def generate_scie_args(
1✔
150
        self,
151
    ) -> tuple[str, ...]:
152
        args = []
×
153
        if self.scie.value is not None:
×
154
            args.append(f"--scie={self.scie.value}")
×
155
        if self.scie_name_style.value is not None:
×
156
            args.append(f"--scie-name-style={self.scie_name_style.value}")
×
157
        if self.scie_busybox.value is not None:
×
158
            args.append(f"--scie-busybox={self.scie_busybox.value}")
×
159
        if self.scie_busybox_pex_entrypoint_env_passthrough.value is True:
×
160
            args.append("--scie-busybox-pex-entrypoint-env-passthrough")
×
161
        if self.scie_platform.value is not None:
×
162
            args.extend([f"--scie-platform={platform}" for platform in self.scie_platform.value])
×
163
        if self.scie_pbs_release.value is not None:
×
164
            args.append(f"--scie-pbs-release={self.scie_pbs_release.value}")
×
165
        if self.scie_python_version.value is not None:
×
166
            args.append(f"--scie-python-version={self.scie_python_version.value}")
×
167
        if self.scie_hash_alg.value is not None:
×
168
            args.append(f"--scie-hash-alg={self.scie_hash_alg.value}")
×
169
        if self.scie_pbs_stripped.value is True:
×
170
            args.append("--scie-pbs-stripped")
×
171

172
        return tuple(args)
×
173

174
    def output_pex_filename(self) -> str:
1✔
175
        return self.output_path.value_or_default(file_ending="pex")
×
176

177
    def scie_output_filenames(self) -> tuple[str, ...] | None:
1✔
178
        if not self.builds_pex_and_scie():
×
179
            return None
×
180
        return _scie_output_filenames(
×
181
            self.output_path.value_or_default(file_ending=None),
182
            self.scie_name_style.value
183
            if self.scie_name_style.value
184
            else self.scie_name_style.default,
185
            self.scie_platform.value,
186
            self.scie_hash_alg.value,
187
        )
188

189
    def scie_output_directories(self) -> tuple[str, ...] | None:
1✔
190
        if not self.builds_pex_and_scie():
×
191
            return None
×
192
        return _scie_output_directories(
×
193
            self.output_path.value_or_default(file_ending=None),
194
            self.scie_name_style.value
195
            if self.scie_name_style.value
196
            else self.scie_name_style.default,
197
            self.scie_platform.value,
198
        )
199

200

201
# Stand alone functions for ease of testing
202
def _current_scie_platform() -> str:
1✔
203
    # This is only a subset of the platforms that Pex can produce
204
    # scies for.  While Pants can produce foreign platform scies, the
205
    # "current" platform can only be one Pants itself can run on.
206
    platform = Platform.create_for_localhost().replace("_", "-")
×
207
    if platform == Platform.linux_arm64:
×
208
        return platform.replace("arm64", "aarch64")
×
209
    else:
210
        return platform
×
211

212

213
def _scie_output_filenames(
1✔
214
    no_suffix_output_path: str,
215
    scie_name_style: str,
216
    scie_platform: Iterable[str] | None,
217
    scie_hash_alg: str | None,
218
) -> tuple[str, ...] | None:
219
    filenames: list[str] = []
×
220

221
    if scie_name_style == ScieNameStyle.DYNAMIC:
×
222
        filenames = [no_suffix_output_path]
×
223
    elif scie_name_style == ScieNameStyle.PLATFORM_PARENT_DIR:
×
224
        return None  # handed by output_directories
×
225
    elif scie_name_style == ScieNameStyle.PLATFORM_FILE_SUFFIX:
×
226
        if scie_platform:
×
227
            filenames = [no_suffix_output_path + f"-{platform}" for platform in scie_platform]
×
228
        else:
229
            filenames = [no_suffix_output_path + f"-{_current_scie_platform()}"]
×
230

231
    if scie_hash_alg is None:
×
232
        return tuple(filenames)
×
233
    else:
234
        return tuple(
×
235
            itertools.chain.from_iterable(
236
                [(fname, f"{fname}.{scie_hash_alg}") for fname in filenames]
237
            )
238
        )
239

240

241
def _scie_output_directories(
1✔
242
    no_suffix_output_path: str,
243
    scie_name_style: str,
244
    scie_platform: Iterable[str] | None,
245
) -> tuple[str, ...] | None:
246
    if scie_name_style != ScieNameStyle.PLATFORM_PARENT_DIR:
×
247
        return None
×
248

249
    if scie_platform:
×
250
        return tuple(
×
251
            [
252
                os.path.join(os.path.dirname(no_suffix_output_path), platform)
253
                for platform in scie_platform
254
            ]
255
        )
256
    else:
257
        return tuple(os.path.join(os.path.dirname(no_suffix_output_path), _current_scie_platform()))
×
258

259

260
def _scie_build_package_artifacts(field_set: PexBinaryFieldSet) -> tuple[BuiltPackageArtifact, ...]:
1✔
261
    artifacts = []
×
262

263
    scie_output_filenames = field_set.scie_output_filenames()
×
264
    if scie_output_filenames is not None:
×
265
        artifacts.extend(
×
266
            [BuiltPackageArtifact(scie_filename) for scie_filename in scie_output_filenames]
267
        )
268
    scie_output_directories = field_set.scie_output_directories()
×
269
    if scie_output_directories is not None:
×
270
        artifacts.extend([BuiltPackageArtifact(scie_dir) for scie_dir in scie_output_directories])
×
271
    return tuple(artifacts)
×
272

273

274
@dataclass(frozen=True)
1✔
275
class PexFromTargetsRequestForBuiltPackage:
1✔
276
    """An intermediate class that gives consumers access to the data used to create a
277
    `PexFromTargetsRequest` to fulfil a `BuiltPackage` request.
278

279
    This class is used directly by `run_pex_binary`, but should be handled transparently by direct
280
    `BuiltPackage` requests.
281
    """
282

283
    request: PexFromTargetsRequest
1✔
284

285

286
@rule(level=LogLevel.DEBUG)
1✔
287
async def package_pex_binary(
1✔
288
    field_set: PexBinaryFieldSet,
289
    pex_binary_defaults: PexBinaryDefaults,
290
) -> PexFromTargetsRequestForBuiltPackage:
291
    resolved_entry_point = await resolve_pex_entry_point(
×
292
        ResolvePexEntryPointRequest(field_set.entry_point)
293
    )
294

295
    output_filename = field_set.output_pex_filename()
×
296

297
    complete_platforms = await digest_complete_platforms(field_set.complete_platforms)
×
298

299
    request = PexFromTargetsRequest(
×
300
        addresses=[field_set.address],
301
        internal_only=False,
302
        main=resolved_entry_point.val or field_set.script.value or field_set.executable.value,
303
        inject_args=field_set.args.value or [],
304
        inject_env=field_set.env.value or FrozenDict[str, str](),
305
        complete_platforms=complete_platforms,
306
        output_filename=output_filename,
307
        layout=PexLayout(field_set.layout.value),
308
        additional_args=field_set.generate_additional_args(pex_binary_defaults),
309
        include_requirements=field_set.include_requirements.value,
310
        include_source_files=field_set.include_sources.value,
311
        include_local_dists=True,
312
        warn_for_transitive_files_targets=True,
313
    )
314

315
    return PexFromTargetsRequestForBuiltPackage(request)
×
316

317

318
@rule
1✔
319
async def built_package_for_pex_from_targets_request(
1✔
320
    field_set: PexBinaryFieldSet,
321
) -> BuiltPackage:
322
    pft_request = await package_pex_binary(field_set, **implicitly())
×
323

324
    if field_set.builds_pex_and_scie():
×
325
        pex_request = dataclasses.replace(
×
326
            await create_pex_from_targets(**implicitly(pft_request.request)),
327
            additional_args=(*pft_request.request.additional_args, *field_set.generate_scie_args()),
328
            scie_output_files=field_set.scie_output_filenames(),
329
            scie_output_directories=field_set.scie_output_directories(),
330
        )
331
        artifacts = (
×
332
            BuiltPackageArtifact(
333
                pex_request.output_filename,
334
            ),
335
            *_scie_build_package_artifacts(field_set),
336
        )
337

338
    else:
339
        pex_request = await create_pex_from_targets(**implicitly(pft_request.request))
×
340
        artifacts = (
×
341
            BuiltPackageArtifact(
342
                pex_request.output_filename,
343
            ),
344
        )
345

346
    pex = await create_pex(**implicitly(pex_request))
×
347

348
    return BuiltPackage(pex.digest, artifacts)
×
349

350

351
def rules():
1✔
352
    return [*collect_rules(), UnionRule(PackageFieldSet, PexBinaryFieldSet)]
1✔
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