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

pantsbuild / pants / 20859302922

09 Jan 2026 05:00PM UTC coverage: 80.261% (-0.008%) from 80.269%
20859302922

Pull #22994

github

web-flow
Merge 4a045ad2f into 3782956e6
Pull Request #22994: Fix incorrect use of `PEX_PYTHON`.

3 of 14 new or added lines in 5 files covered. (21.43%)

20 existing lines in 2 files now uncovered.

78787 of 98164 relevant lines covered (80.26%)

3.36 hits per line

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

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

4
from __future__ import annotations
12✔
5

6
import os
12✔
7
from collections.abc import Mapping
12✔
8
from dataclasses import dataclass
12✔
9
from pathlib import PurePath
12✔
10

11
from pants.core.subsystems.python_bootstrap import PythonBootstrap
12✔
12
from pants.core.util_rules import subprocess_environment, system_binaries
12✔
13
from pants.core.util_rules.adhoc_binaries import PythonBuildStandaloneBinary
12✔
14
from pants.core.util_rules.subprocess_environment import SubprocessEnvironmentVars
12✔
15
from pants.core.util_rules.system_binaries import BinaryPath
12✔
16
from pants.engine.engine_aware import EngineAwareReturnType
12✔
17
from pants.engine.rules import collect_rules, rule
12✔
18
from pants.option.global_options import NamedCachesDirOption
12✔
19
from pants.option.option_types import BoolOption, IntOption, StrListOption
12✔
20
from pants.option.subsystem import Subsystem
12✔
21
from pants.util.frozendict import FrozenDict
12✔
22
from pants.util.logging import LogLevel
12✔
23
from pants.util.memo import memoized_property
12✔
24
from pants.util.ordered_set import OrderedSet
12✔
25
from pants.util.strutil import softwrap
12✔
26

27

28
class PexSubsystem(Subsystem):
12✔
29
    options_scope = "pex"
12✔
30
    help = "How Pants uses Pex to run Python subprocesses."
12✔
31

32
    class EnvironmentAware(Subsystem.EnvironmentAware):
12✔
33
        # TODO(#9760): We'll want to deprecate this in favor of a global option which allows for a
34
        #  per-process override.
35

36
        env_vars_used_by_options = ("PATH",)
12✔
37

38
        _executable_search_paths = StrListOption(
12✔
39
            default=["<PATH>"],
40
            help=softwrap(
41
                """
42
                The PATH value that will be used by the PEX subprocess and any subprocesses it
43
                spawns.
44

45
                The special string `"<PATH>"` will expand to the contents of the PATH env var.
46
                """
47
            ),
48
            advanced=True,
49
            metavar="<binary-paths>",
50
        )
51

52
        @memoized_property
12✔
53
        def path(self) -> tuple[str, ...]:
12✔
54
            def iter_path_entries():
×
55
                for entry in self._executable_search_paths:
×
56
                    if entry == "<PATH>":
×
57
                        path = self._options_env.get("PATH")
×
58
                        if path:
×
59
                            yield from path.split(os.pathsep)
×
60
                    else:
61
                        yield entry
×
62

63
            return tuple(OrderedSet(iter_path_entries()))
×
64

65
    _verbosity = IntOption(
12✔
66
        default=0,
67
        help="Set the verbosity level of PEX logging, from 0 (no logging) up to 9 (max logging).",
68
        advanced=True,
69
    )
70
    venv_use_symlinks = BoolOption(
12✔
71
        default=False,
72
        help=softwrap(
73
            """
74
            When possible, use venvs whose site-packages directories are populated with symlinks.
75

76
            Enabling this can save space in the `--named-caches-dir` directory
77
            and lead to slightly faster execution times for Pants Python goals. Some
78
            distributions do not work with symlinked venvs though, so you may not be able to
79
            enable this optimization as a result.
80
            """
81
        ),
82
        advanced=True,
83
    )
84
    emit_warnings = BoolOption(
12✔
85
        default=False,
86
        help=softwrap(
87
            """
88
            If warnings from Pex should be logged by Pants to the console.
89

90
            Note: Pants uses Pex internally in some ways that trigger some warnings at the moment,
91
            so enabling this may result in warnings not related to your code. See
92
            [#20577](https://github.com/pantsbuild/pants/issues/20577) and
93
            [#20586](https://github.com/pantsbuild/pants/issues/20586).
94
            """
95
        ),
96
    )
97

98
    @property
12✔
99
    def verbosity(self) -> int:
12✔
100
        level = self._verbosity
×
101
        if level < 0 or level > 9:
×
102
            raise ValueError("verbosity level must be between 0 and 9")
×
103
        return level
×
104

105

106
@dataclass(frozen=True)
12✔
107
class PythonExecutable(BinaryPath, EngineAwareReturnType):
12✔
108
    """The BinaryPath of a Python executable for user code, along with some extras."""
109

110
    append_only_caches: FrozenDict[str, str] = FrozenDict({})
12✔
111

112
    def __init__(
12✔
113
        self,
114
        path: str,
115
        fingerprint: str | None = None,
116
        append_only_caches: Mapping[str, str] = FrozenDict({}),
117
    ) -> None:
118
        object.__setattr__(self, "append_only_caches", FrozenDict(append_only_caches))
1✔
119
        super().__init__(path, fingerprint)
1✔
120
        self.__post_init__()
1✔
121

122
    def __post_init__(self) -> None:
12✔
123
        if not PurePath(self.path).is_absolute():
1✔
124
            raise ValueError(
×
125
                softwrap(
126
                    f"""
127
                    PythonExecutable expects the path to be absolute. Tools like Pex internalize the
128
                    absolute path to Python (since `sys.executable` is expected to be absolute).
129

130
                    In order to ensure the cache key for Python process is "correct" (especially in
131
                    remote cache/execution situations) we require the Python path is absolute.
132

133
                    Got: {self.path}
134
                    """
135
                )
136
            )
137

138
    def message(self) -> str:
12✔
139
        return f"Selected {self.path} to run PEXes with."
×
140

141

142
@dataclass(frozen=True)
12✔
143
class PexEnvironment:
12✔
144
    path: tuple[str, ...]
12✔
145
    interpreter_search_paths: tuple[str, ...]
12✔
146
    subprocess_environment_dict: FrozenDict[str, str]
12✔
147
    named_caches_dir: PurePath
12✔
148
    bootstrap_python: PythonBuildStandaloneBinary
12✔
149
    venv_use_symlinks: bool = False
12✔
150

151
    _PEX_ROOT_DIRNAME = "pex_root"
12✔
152

153
    def in_sandbox(self, *, working_directory: str | None) -> CompletePexEnvironment:
12✔
154
        pex_root = PurePath(".cache") / self._PEX_ROOT_DIRNAME
×
155
        return CompletePexEnvironment(
×
156
            _pex_environment=self,
157
            pex_root=pex_root,
158
            _working_directory=PurePath(working_directory) if working_directory else None,
159
            append_only_caches=FrozenDict(
160
                **{self._PEX_ROOT_DIRNAME: str(pex_root)},
161
                **self.bootstrap_python.APPEND_ONLY_CACHES,
162
            ),
163
        )
164

165
    def in_workspace(self) -> CompletePexEnvironment:
12✔
166
        # N.B.: When running in the workspace the engine doesn't offer an append_only_caches
167
        # service to setup a symlink to our named cache for us. As such, we point the PEX_ROOT
168
        # directly at the underlying append only cache in that case to re-use results there and
169
        # to keep the workspace from being dirtied by the creation of a new Pex cache rooted
170
        # there.
171
        pex_root = self.named_caches_dir / self._PEX_ROOT_DIRNAME
×
172
        return CompletePexEnvironment(
×
173
            _pex_environment=self,
174
            pex_root=pex_root,
175
            _working_directory=None,
176
            append_only_caches=self.bootstrap_python.APPEND_ONLY_CACHES,
177
        )
178

179
    def venv_site_packages_copies_option(self, use_copies: bool) -> str:
12✔
180
        if self.venv_use_symlinks and not use_copies:
×
181
            return "--no-venv-site-packages-copies"
×
182
        return "--venv-site-packages-copies"
×
183

184

185
@rule(desc="Prepare environment for running PEXes", level=LogLevel.DEBUG)
12✔
186
async def find_pex_python(
12✔
187
    python_bootstrap: PythonBootstrap,
188
    python_binary: PythonBuildStandaloneBinary,
189
    pex_subsystem: PexSubsystem,
190
    pex_environment_aware: PexSubsystem.EnvironmentAware,
191
    subprocess_env_vars: SubprocessEnvironmentVars,
192
    named_caches_dir: NamedCachesDirOption,
193
) -> PexEnvironment:
194
    return PexEnvironment(
×
195
        path=pex_environment_aware.path,
196
        interpreter_search_paths=python_bootstrap.interpreter_search_paths,
197
        subprocess_environment_dict=subprocess_env_vars.vars,
198
        named_caches_dir=named_caches_dir.val,
199
        bootstrap_python=python_binary,
200
        venv_use_symlinks=pex_subsystem.venv_use_symlinks,
201
    )
202

203

204
@dataclass(frozen=True)
12✔
205
class CompletePexEnvironment:
12✔
206
    _pex_environment: PexEnvironment
12✔
207
    pex_root: PurePath
12✔
208
    _working_directory: PurePath | None
12✔
209
    append_only_caches: FrozenDict[str, str]
12✔
210

211
    @property
12✔
212
    def interpreter_search_paths(self) -> tuple[str, ...]:
12✔
213
        return self._pex_environment.interpreter_search_paths
×
214

215
    def create_argv(
12✔
216
        self,
217
        pex_filepath: str,
218
        *args: str,
219
        python: PythonExecutable | PythonBuildStandaloneBinary | None = None,
220
    ) -> tuple[str, ...]:
UNCOV
221
        pex_relpath = (
×
222
            os.path.relpath(pex_filepath, self._working_directory)
223
            if self._working_directory
224
            else pex_filepath
225
        )
NEW
226
        python = python or self._pex_environment.bootstrap_python
×
NEW
227
        if python:
×
NEW
228
            return (python.path, pex_relpath, *args)
×
NEW
229
        if os.path.basename(pex_relpath) == pex_relpath:
×
NEW
230
            return (f"./{pex_relpath}", *args)
×
NEW
231
        return (pex_relpath, *args)
×
232

233
    def environment_dict(self, *, python_configured: bool) -> Mapping[str, str]:
12✔
234
        """The environment to use for running anything with PEX.
235

236
        If the Process is run with a pre-selected Python interpreter, set `python_configured=True`
237
        to avoid PEX from trying to find a new interpreter.
238
        """
239
        path = os.pathsep.join(self._pex_environment.path)
×
240
        subprocess_env_dict = dict(self._pex_environment.subprocess_environment_dict)
×
241

242
        if "PATH" in self._pex_environment.subprocess_environment_dict:
×
243
            path = os.pathsep.join([path, subprocess_env_dict.pop("PATH")])
×
244

245
        d = dict(
×
246
            PATH=path,
247
            PEX_IGNORE_RCFILES="true",
248
            PEX_ROOT=(
249
                os.path.relpath(self.pex_root, self._working_directory)
250
                if self._working_directory
251
                else str(self.pex_root)
252
            ),
253
            **subprocess_env_dict,
254
        )
255
        # NB: We only set `PEX_PYTHON_PATH` if the Python interpreter has not already been
256
        # pre-selected by Pants. Otherwise, Pex would inadvertently try to find another interpreter
257
        # when running PEXes. (Creating a PEX will ignore this env var in favor of `--python-path`.)
NEW
258
        if not python_configured:
×
259
            d["PEX_PYTHON_PATH"] = os.pathsep.join(self.interpreter_search_paths)
×
260
        return d
×
261

262

263
def rules():
12✔
264
    return [
12✔
265
        *collect_rules(),
266
        *subprocess_environment.rules(),
267
        *system_binaries.rules(),
268
    ]
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