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

pantsbuild / pants / 20332790708

18 Dec 2025 09:48AM UTC coverage: 64.992% (-15.3%) from 80.295%
20332790708

Pull #22949

github

web-flow
Merge f730a56cd into 407284c67
Pull Request #22949: Add experimental uv resolver for Python lockfiles

54 of 97 new or added lines in 5 files covered. (55.67%)

8270 existing lines in 295 files now uncovered.

48990 of 75379 relevant lines covered (64.99%)

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

11
from pants.backend.python.target_types import (
5✔
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
5✔
49
from pants.backend.python.util_rules.pex import create_pex, digest_complete_platforms
5✔
50
from pants.backend.python.util_rules.pex_from_targets import (
5✔
51
    PexFromTargetsRequest,
52
    create_pex_from_targets,
53
)
54
from pants.core.environments.target_types import EnvironmentField
5✔
55
from pants.core.goals.package import (
5✔
56
    BuiltPackage,
57
    BuiltPackageArtifact,
58
    OutputPathField,
59
    PackageFieldSet,
60
)
61
from pants.core.goals.run import RunFieldSet, RunInSandboxBehavior
5✔
62
from pants.engine.platform import Platform
5✔
63
from pants.engine.rules import collect_rules, implicitly, rule
5✔
64
from pants.engine.unions import UnionRule
5✔
65
from pants.util.frozendict import FrozenDict
5✔
66
from pants.util.logging import LogLevel
5✔
67

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

70

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

75
    required_fields = (PexEntryPointField,)
5✔
76

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

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

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

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

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

119
    def generate_additional_args(self, pex_binary_defaults: PexBinaryDefaults) -> tuple[str, ...]:
5✔
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(
5✔
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:
5✔
175
        return self.output_path.value_or_default(file_ending="pex")
×
176

177
    def scie_output_filenames(self) -> tuple[str, ...] | None:
5✔
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:
5✔
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:
5✔
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(
5✔
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:
UNCOV
219
    filenames: list[str] = []
×
220

UNCOV
221
    if scie_name_style == ScieNameStyle.DYNAMIC:
×
UNCOV
222
        filenames = [no_suffix_output_path]
×
UNCOV
223
    elif scie_name_style == ScieNameStyle.PLATFORM_PARENT_DIR:
×
UNCOV
224
        return None  # handed by output_directories
×
UNCOV
225
    elif scie_name_style == ScieNameStyle.PLATFORM_FILE_SUFFIX:
×
UNCOV
226
        if scie_platform:
×
UNCOV
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

UNCOV
231
    if scie_hash_alg is None:
×
UNCOV
232
        return tuple(filenames)
×
233
    else:
UNCOV
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(
5✔
242
    no_suffix_output_path: str,
243
    scie_name_style: str,
244
    scie_platform: Iterable[str] | None,
245
) -> tuple[str, ...] | None:
UNCOV
246
    if scie_name_style != ScieNameStyle.PLATFORM_PARENT_DIR:
×
UNCOV
247
        return None
×
248

UNCOV
249
    if scie_platform:
×
UNCOV
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, ...]:
5✔
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)
5✔
275
class PexFromTargetsRequestForBuiltPackage:
5✔
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
5✔
284

285

286
@rule(level=LogLevel.DEBUG)
5✔
287
async def package_pex_binary(
5✔
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
5✔
319
async def built_package_for_pex_from_targets_request(
5✔
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():
5✔
352
    return [*collect_rules(), UnionRule(PackageFieldSet, PexBinaryFieldSet)]
5✔
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

© 2025 Coveralls, Inc