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

pantsbuild / pants / 20961185705

13 Jan 2026 02:52PM UTC coverage: 80.274% (-0.003%) from 80.277%
20961185705

push

github

web-flow
plumb through more of Pex's --scie flags (#22996)

* --scie-pbs-free-threaded / --scie-pbs-debug which are new since when I
started #22866
* --scie-load-dotenv
https://github.com/pex-tool/pex/releases/tag/v2.75.0
* revised entrypoint
https://github.com/pex-tool/pex/releases/tag/v2.76.0

Since there has not been a relase with these yet, I've changed the name
without doing a depreciation cycle

46 of 59 new or added lines in 4 files covered. (77.97%)

2 existing lines in 1 file now uncovered.

78840 of 98214 relevant lines covered (80.27%)

3.36 hits per line

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

49.47
/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
11✔
5
import itertools
11✔
6
import logging
11✔
7
import os
11✔
8
from collections.abc import Iterable
11✔
9
from dataclasses import dataclass
11✔
10

11
from pants.backend.python.target_types import (
11✔
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
    PexScieField,
32
    PexScieHashAlgField,
33
    PexScieLoadDotenvField,
34
    PexScieNameStyleField,
35
    PexSciePbsDebug,
36
    PexSciePbsFreeThreaded,
37
    PexSciePbsReleaseField,
38
    PexSciePbsStripped,
39
    PexSciePexEntrypointEnvPassthrough,
40
    PexSciePlatformField,
41
    PexSciePythonVersion,
42
    PexScriptField,
43
    PexShBootField,
44
    PexShebangField,
45
    PexStripEnvField,
46
    PexVenvHermeticScripts,
47
    PexVenvSitePackagesCopies,
48
    ResolvePexEntryPointRequest,
49
    ScieNameStyle,
50
)
51
from pants.backend.python.target_types_rules import resolve_pex_entry_point
11✔
52
from pants.backend.python.util_rules.pex import create_pex, digest_complete_platforms
11✔
53
from pants.backend.python.util_rules.pex_from_targets import (
11✔
54
    PexFromTargetsRequest,
55
    create_pex_from_targets,
56
)
57
from pants.core.environments.target_types import EnvironmentField
11✔
58
from pants.core.goals.package import (
11✔
59
    BuiltPackage,
60
    BuiltPackageArtifact,
61
    OutputPathField,
62
    PackageFieldSet,
63
)
64
from pants.core.goals.run import RunFieldSet, RunInSandboxBehavior
11✔
65
from pants.engine.platform import Platform
11✔
66
from pants.engine.rules import collect_rules, implicitly, rule
11✔
67
from pants.engine.unions import UnionRule
11✔
68
from pants.util.frozendict import FrozenDict
11✔
69
from pants.util.logging import LogLevel
11✔
70

71
logger = logging.getLogger(__name__)
11✔
72

73

74
@dataclass(frozen=True)
11✔
75
class PexBinaryFieldSet(PackageFieldSet, RunFieldSet):
11✔
76
    run_in_sandbox_behavior = RunInSandboxBehavior.RUN_REQUEST_HERMETIC
11✔
77

78
    required_fields = (PexEntryPointField,)
11✔
79

80
    entry_point: PexEntryPointField
11✔
81
    script: PexScriptField
11✔
82
    executable: PexExecutableField
11✔
83
    args: PexArgsField
11✔
84
    env: PexEnvField
11✔
85

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

105
    scie: PexScieField
11✔
106
    scie_load_dotenv: PexScieLoadDotenvField
11✔
107
    scie_name_style: PexScieNameStyleField
11✔
108
    scie_busybox: PexScieBusyBox
11✔
109
    scie_pex_entrypoint_env_passthrough: PexSciePexEntrypointEnvPassthrough
11✔
110
    scie_platform: PexSciePlatformField
11✔
111
    scie_pbs_release: PexSciePbsReleaseField
11✔
112
    scie_python_version: PexSciePythonVersion
11✔
113
    scie_hash_alg: PexScieHashAlgField
11✔
114
    scie_pbs_free_threaded: PexSciePbsFreeThreaded
11✔
115
    scie_pbs_debug: PexSciePbsDebug
11✔
116
    scie_pbs_stripped: PexSciePbsStripped
11✔
117

118
    def builds_pex_and_scie(self) -> bool:
11✔
119
        return self.scie.value is not None
×
120

121
    @property
11✔
122
    def _execution_mode(self) -> PexExecutionMode:
11✔
123
        return PexExecutionMode(self.execution_mode.value)
×
124

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

155
    def generate_scie_args(
11✔
156
        self,
157
    ) -> tuple[str, ...]:
158
        args = []
×
159
        if self.scie.value is not None:
×
160
            args.append(f"--scie={self.scie.value}")
×
NEW
161
        if self.scie_load_dotenv.value is not None:
×
NEW
162
            if self.scie_load_dotenv.value:
×
NEW
163
                args.append("--scie-load-dotenv")
×
164
            else:
NEW
165
                args.append("--no-scie-load-dotenv")
×
166
        if self.scie_name_style.value is not None:
×
167
            args.append(f"--scie-name-style={self.scie_name_style.value}")
×
168
        if self.scie_busybox.value is not None:
×
169
            args.append(f"--scie-busybox={self.scie_busybox.value}")
×
NEW
170
        if self.scie_pex_entrypoint_env_passthrough.value is True:
×
171
            args.append("--scie-busybox-pex-entrypoint-env-passthrough")
×
172
        if self.scie_platform.value is not None:
×
173
            args.extend([f"--scie-platform={platform}" for platform in self.scie_platform.value])
×
174
        if self.scie_pbs_release.value is not None:
×
175
            args.append(f"--scie-pbs-release={self.scie_pbs_release.value}")
×
176
        if self.scie_python_version.value is not None:
×
177
            args.append(f"--scie-python-version={self.scie_python_version.value}")
×
178
        if self.scie_hash_alg.value is not None:
×
179
            args.append(f"--scie-hash-alg={self.scie_hash_alg.value}")
×
NEW
180
        if self.scie_pbs_debug.value is not None:
×
NEW
181
            if self.scie_pbs_debug.value:
×
NEW
182
                args.append("--scie-pbs-debug")
×
183
            else:
NEW
184
                args.append("--no-scie-pbs-debug")
×
NEW
185
        if self.scie_pbs_free_threaded.value is not None:
×
NEW
186
            if self.scie_pbs_free_threaded.value:
×
NEW
187
                args.append("--scie-pbs-free-threaded")
×
188
            else:
NEW
189
                args.append("--no-scie-pbs-free-threaded")
×
190
        if self.scie_pbs_stripped.value is True:
×
191
            args.append("--scie-pbs-stripped")
×
192

193
        return tuple(args)
×
194

195
    def output_pex_filename(self) -> str:
11✔
196
        return self.output_path.value_or_default(file_ending="pex")
×
197

198
    def scie_output_filenames(self) -> tuple[str, ...] | None:
11✔
199
        if not self.builds_pex_and_scie():
×
200
            return None
×
201
        return _scie_output_filenames(
×
202
            self.output_path.value_or_default(file_ending=None),
203
            self.scie_name_style.value
204
            if self.scie_name_style.value
205
            else self.scie_name_style.default,
206
            self.scie_platform.value,
207
            self.scie_hash_alg.value,
208
        )
209

210
    def scie_output_directories(self) -> tuple[str, ...] | None:
11✔
211
        if not self.builds_pex_and_scie():
×
212
            return None
×
213
        return _scie_output_directories(
×
214
            self.output_path.value_or_default(file_ending=None),
215
            self.scie_name_style.value
216
            if self.scie_name_style.value
217
            else self.scie_name_style.default,
218
            self.scie_platform.value,
219
        )
220

221

222
# Stand alone functions for ease of testing
223
def _current_scie_platform() -> str:
11✔
224
    # This is only a subset of the platforms that Pex can produce
225
    # scies for.  While Pants can produce foreign platform scies, the
226
    # "current" platform can only be one Pants itself can run on.
227
    platform = Platform.create_for_localhost().replace("_", "-")
×
228
    if platform == Platform.linux_arm64:
×
229
        return platform.replace("arm64", "aarch64")
×
230
    else:
231
        return platform
×
232

233

234
def _scie_output_filenames(
11✔
235
    no_suffix_output_path: str,
236
    scie_name_style: str,
237
    scie_platform: Iterable[str] | None,
238
    scie_hash_alg: str | None,
239
) -> tuple[str, ...] | None:
240
    filenames: list[str] = []
1✔
241

242
    if scie_name_style == ScieNameStyle.DYNAMIC:
1✔
243
        filenames = [no_suffix_output_path]
1✔
244
    elif scie_name_style == ScieNameStyle.PLATFORM_PARENT_DIR:
1✔
245
        return None  # handed by output_directories
1✔
246
    elif scie_name_style == ScieNameStyle.PLATFORM_FILE_SUFFIX:
1✔
247
        if scie_platform:
1✔
248
            filenames = [no_suffix_output_path + f"-{platform}" for platform in scie_platform]
1✔
249
        else:
250
            filenames = [no_suffix_output_path + f"-{_current_scie_platform()}"]
×
251

252
    if scie_hash_alg is None:
1✔
253
        return tuple(filenames)
1✔
254
    else:
255
        return tuple(
1✔
256
            itertools.chain.from_iterable(
257
                [(fname, f"{fname}.{scie_hash_alg}") for fname in filenames]
258
            )
259
        )
260

261

262
def _scie_output_directories(
11✔
263
    no_suffix_output_path: str,
264
    scie_name_style: str,
265
    scie_platform: Iterable[str] | None,
266
) -> tuple[str, ...] | None:
267
    if scie_name_style != ScieNameStyle.PLATFORM_PARENT_DIR:
1✔
268
        return None
1✔
269

270
    if scie_platform:
1✔
271
        return tuple(
1✔
272
            [
273
                os.path.join(os.path.dirname(no_suffix_output_path), platform)
274
                for platform in scie_platform
275
            ]
276
        )
277
    else:
278
        return tuple(os.path.join(os.path.dirname(no_suffix_output_path), _current_scie_platform()))
×
279

280

281
def _scie_build_package_artifacts(field_set: PexBinaryFieldSet) -> tuple[BuiltPackageArtifact, ...]:
11✔
282
    artifacts = []
×
283

284
    scie_output_filenames = field_set.scie_output_filenames()
×
285
    if scie_output_filenames is not None:
×
286
        artifacts.extend(
×
287
            [BuiltPackageArtifact(scie_filename) for scie_filename in scie_output_filenames]
288
        )
289
    scie_output_directories = field_set.scie_output_directories()
×
290
    if scie_output_directories is not None:
×
291
        artifacts.extend([BuiltPackageArtifact(scie_dir) for scie_dir in scie_output_directories])
×
292
    return tuple(artifacts)
×
293

294

295
@dataclass(frozen=True)
11✔
296
class PexFromTargetsRequestForBuiltPackage:
11✔
297
    """An intermediate class that gives consumers access to the data used to create a
298
    `PexFromTargetsRequest` to fulfil a `BuiltPackage` request.
299

300
    This class is used directly by `run_pex_binary`, but should be handled transparently by direct
301
    `BuiltPackage` requests.
302
    """
303

304
    request: PexFromTargetsRequest
11✔
305

306

307
@rule(level=LogLevel.DEBUG)
11✔
308
async def package_pex_binary(
11✔
309
    field_set: PexBinaryFieldSet,
310
    pex_binary_defaults: PexBinaryDefaults,
311
) -> PexFromTargetsRequestForBuiltPackage:
312
    resolved_entry_point = await resolve_pex_entry_point(
×
313
        ResolvePexEntryPointRequest(field_set.entry_point)
314
    )
315

316
    output_filename = field_set.output_pex_filename()
×
317

318
    complete_platforms = await digest_complete_platforms(field_set.complete_platforms)
×
319

320
    request = PexFromTargetsRequest(
×
321
        addresses=[field_set.address],
322
        internal_only=False,
323
        main=resolved_entry_point.val or field_set.script.value or field_set.executable.value,
324
        inject_args=field_set.args.value or [],
325
        inject_env=field_set.env.value or FrozenDict[str, str](),
326
        complete_platforms=complete_platforms,
327
        output_filename=output_filename,
328
        layout=PexLayout(field_set.layout.value),
329
        additional_args=field_set.generate_additional_args(pex_binary_defaults),
330
        include_requirements=field_set.include_requirements.value,
331
        include_source_files=field_set.include_sources.value,
332
        include_local_dists=True,
333
        warn_for_transitive_files_targets=True,
334
    )
335

336
    return PexFromTargetsRequestForBuiltPackage(request)
×
337

338

339
@rule
11✔
340
async def built_package_for_pex_from_targets_request(
11✔
341
    field_set: PexBinaryFieldSet,
342
) -> BuiltPackage:
343
    pft_request = await package_pex_binary(field_set, **implicitly())
×
344

345
    if field_set.builds_pex_and_scie():
×
346
        pex_request = dataclasses.replace(
×
347
            await create_pex_from_targets(**implicitly(pft_request.request)),
348
            additional_args=(*pft_request.request.additional_args, *field_set.generate_scie_args()),
349
            scie_output_files=field_set.scie_output_filenames(),
350
            scie_output_directories=field_set.scie_output_directories(),
351
        )
352
        artifacts = (
×
353
            BuiltPackageArtifact(
354
                pex_request.output_filename,
355
            ),
356
            *_scie_build_package_artifacts(field_set),
357
        )
358

359
    else:
360
        pex_request = await create_pex_from_targets(**implicitly(pft_request.request))
×
361
        artifacts = (
×
362
            BuiltPackageArtifact(
363
                pex_request.output_filename,
364
            ),
365
        )
366

367
    pex = await create_pex(**implicitly(pex_request))
×
368

369
    return BuiltPackage(pex.digest, artifacts)
×
370

371

372
def rules():
11✔
373
    return [*collect_rules(), UnionRule(PackageFieldSet, PexBinaryFieldSet)]
10✔
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