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

pantsbuild / pants / 20966449192

13 Jan 2026 05:35PM UTC coverage: 80.269% (-0.005%) from 80.274%
20966449192

push

github

web-flow
always include all artifacts in BuildPexResult (#22995)

Followup from #22866, which added fiddly logic to anticipate which
artifacts would be built when the answer wasn't just "a single .pex
file". Upon further reflection and investigation, I agree that it is
safe to drop that logic and always return all the artifacts, provided
that we maintain the invariant that the "regular" PEX is always first.

Co-authored-by: John Sirois <john.sirois@gmail.com>

---------

Co-authored-by: John Sirois <john.sirois@gmail.com>

19 of 36 new or added lines in 3 files covered. (52.78%)

4 existing lines in 2 files now uncovered.

78764 of 98125 relevant lines covered (80.27%)

3.36 hits per line

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

47.02
/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
10✔
5
import logging
10✔
6
import os
10✔
7
from dataclasses import dataclass
10✔
8

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

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

70

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

75
    required_fields = (PexEntryPointField,)
10✔
76

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

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

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

115
    def builds_pex_and_scie(self) -> bool:
10✔
116
        return self.scie.value is not None
×
117

118
    @property
10✔
119
    def _execution_mode(self) -> PexExecutionMode:
10✔
120
        return PexExecutionMode(self.execution_mode.value)
×
121

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

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

190
        return tuple(args)
×
191

192
    def output_pex_filename(self) -> str:
10✔
193
        return self.output_path.value_or_default(file_ending="pex")
×
194

195

196
@dataclass(frozen=True)
10✔
197
class PexFromTargetsRequestForBuiltPackage:
10✔
198
    """An intermediate class that gives consumers access to the data used to create a
199
    `PexFromTargetsRequest` to fulfil a `BuiltPackage` request.
200

201
    This class is used directly by `run_pex_binary`, but should be handled transparently by direct
202
    `BuiltPackage` requests.
203
    """
204

205
    request: PexFromTargetsRequest
10✔
206

207

208
@rule(level=LogLevel.DEBUG)
10✔
209
async def package_pex_binary(
10✔
210
    field_set: PexBinaryFieldSet,
211
    pex_binary_defaults: PexBinaryDefaults,
212
) -> PexFromTargetsRequestForBuiltPackage:
213
    resolved_entry_point = await resolve_pex_entry_point(
×
214
        ResolvePexEntryPointRequest(field_set.entry_point)
215
    )
216

217
    output_filename = field_set.output_pex_filename()
×
218

219
    complete_platforms = await digest_complete_platforms(field_set.complete_platforms)
×
220

221
    request = PexFromTargetsRequest(
×
222
        addresses=[field_set.address],
223
        internal_only=False,
224
        main=resolved_entry_point.val or field_set.script.value or field_set.executable.value,
225
        inject_args=field_set.args.value or [],
226
        inject_env=field_set.env.value or FrozenDict[str, str](),
227
        complete_platforms=complete_platforms,
228
        output_filename=output_filename,
229
        layout=PexLayout(field_set.layout.value),
230
        additional_args=field_set.generate_additional_args(pex_binary_defaults),
231
        include_requirements=field_set.include_requirements.value,
232
        include_source_files=field_set.include_sources.value,
233
        include_local_dists=True,
234
        warn_for_transitive_files_targets=True,
235
    )
236

237
    return PexFromTargetsRequestForBuiltPackage(request)
×
238

239

240
@rule
10✔
241
async def built_package_for_pex_from_targets_request(
10✔
242
    field_set: PexBinaryFieldSet,
243
) -> BuiltPackage:
244
    pft_request = await package_pex_binary(field_set, **implicitly())
×
245

246
    if field_set.builds_pex_and_scie():
×
247
        pex_request = dataclasses.replace(
×
248
            await create_pex_from_targets(**implicitly(pft_request.request)),
249
            additional_args=(*pft_request.request.additional_args, *field_set.generate_scie_args()),
250
        )
251
    else:
UNCOV
252
        pex_request = await create_pex_from_targets(**implicitly(pft_request.request))
×
253

UNCOV
254
    pex = await create_pex(**implicitly(pex_request))
×
NEW
255
    snapshot = await digest_to_snapshot(pex.digest)
×
256

257
    # "The" PEX, and not scie, hashes, or future auxiliary files must be first
NEW
258
    artifacts = [BuiltPackageArtifact(pft_request.request.output_filename)]
×
NEW
259
    if PexLayout.ZIPAPP == pft_request.request.layout:
×
NEW
260
        artifacts.extend(
×
261
            BuiltPackageArtifact(artifact)
262
            for artifact in snapshot.files
263
            if artifact != pft_request.request.output_filename
264
        )
265
    else:
NEW
266
        artifacts.extend(
×
267
            BuiltPackageArtifact(artifact)
268
            for artifact in snapshot.files
269
            if (
270
                pft_request.request.output_filename
271
                != os.path.commonpath((pft_request.request.output_filename, artifact))
272
            )
273
        )
274
    # Make sure the "regular PEX first" invariant explained above is true
NEW
275
    assert artifacts[0].relpath in snapshot.files or artifacts[0].relpath in snapshot.dirs, (
×
276
        "PEX must be first BuiltPackageArtifact"
277
    )
NEW
278
    return BuiltPackage(pex.digest, tuple(artifacts))
×
279

280

281
def rules():
10✔
282
    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