From 4df1d2a82b5cec747766e4d66edcdea84cbf53ea Mon Sep 17 00:00:00 2001 From: John Sirois Date: Mon, 29 Dec 2025 16:04:28 -0800 Subject: [PATCH 01/23] Split Pex `.whl` into two `.whl`s. We now pulish two wheels: 1. `pex-.py27.py35.py36.py37.py38.py39.py310.py311-none-any.whl`: Targets exactly the Pythons in the python tag. 2. `pex-.py3.py312-none-any.whl`: Targets Python>=3.12. The 1st wheel carries the same contents as the existing `pex-.py2.py3.py312-none-any.whl` wheel and the 2nd wheel (`pex-.py3.py312-none-any.whl`) ships without vendored `pip`, `setuptools`, `toml` or `tomli` since these are either unusable or un-needed under Python>=3.12. Fixes #2785. --- .github/workflows/ci.yml | 3 +- .github/workflows/release.yml | 11 +- build-backend/pex_build/__init__.py | 7 + pex/environment.py | 2 +- pex/pep_425.py | 4 +- pex/pep_427.py | 4 +- pex/pex_builder.py | 6 +- pex/targets.py | 39 +- pex/third_party/__init__.py | 35 +- pex/vendor/__init__.py | 75 +- pex/wheel.py | 2 + scripts/create-packages.py | 71 +- testing/__init__.py | 25 +- testing/cli.py | 21 +- ...mplete_platform_almalinux:8.10_py3.11.json | 732 ++++++++++++++++++ testing/pex_dist.py | 75 +- tests/conftest.py | 8 +- tests/integration/cli/commands/test_run.py | 9 +- tests/integration/resolve/test_issue_2532.py | 26 +- tests/integration/test_integration.py | 16 +- tests/integration/tools/commands/test_venv.py | 16 +- tests/integration/venv_ITs/test_issue_1745.py | 1 + tests/integration/venv_ITs/test_issue_2248.py | 1 + 23 files changed, 1070 insertions(+), 119 deletions(-) create mode 100644 testing/data/platforms/complete_platform_almalinux:8.10_py3.11.json diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3ab82dcba..799f89c0b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -72,7 +72,8 @@ jobs: run: | uv run dev-cmd package -- \ --additional-format sdist \ - --additional-format wheel \ + --additional-format whl \ + --additional-format whl-3.12-plus \ --embed-docs \ --clean-docs \ --scies \ diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index d03504fe8..76ee6ecfd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -64,8 +64,15 @@ jobs: python-version: "3.11" - name: Install uv uses: astral-sh/setup-uv@v5 - - name: Build sdist and wheel - run: uv run dev-cmd package -- --no-pex --additional-format sdist --additional-format wheel --embed-docs --clean-docs + - name: Build sdist and wheels + run: | + uv run dev-cmd package -- \ + --no-pex \ + --additional-format sdist \ + --additional-format whl \ + --additional-format whl-3.12-plus \ + --embed-docs \ + --clean-docs - name: Publish Pex ${{ needs.determine-tag.outputs.release-tag }} uses: pypa/gh-action-pypi-publish@release/v1 with: diff --git a/build-backend/pex_build/__init__.py b/build-backend/pex_build/__init__.py index c0b6c61ed..6ed837c48 100644 --- a/build-backend/pex_build/__init__.py +++ b/build-backend/pex_build/__init__.py @@ -14,11 +14,13 @@ from pex.common import safe_mkdir, safe_mkdtemp from pex.third_party.packaging.markers import Marker from pex.typing import TYPE_CHECKING +from pex.vendor import iter_vendor_specs if TYPE_CHECKING: from typing import Callable, Iterator, Optional INCLUDE_DOCS = os.environ.get("__PEX_BUILD_INCLUDE_DOCS__", "False").lower() in ("1", "true") +WHEEL_3_12_PLUS = os.environ.get("__PEX_BUILD_WHL_3_12_PLUS__", "False").lower() in ("1", "true") @contextmanager @@ -128,3 +130,8 @@ def modify_wheel( out_dir, ] ) + if WHEEL_3_12_PLUS: + for vendor_spec in iter_vendor_specs(): + if vendor_spec.key in ("pip", "setuptools", "toml", "tomli"): + shutil.rmtree(os.path.join(wheel_dir, vendor_spec.relpath)) + print("Removed un-needed vendored library", vendor_spec.relpath, file=sys.stderr) diff --git a/pex/environment.py b/pex/environment.py index 76169c1b5..e207d34db 100644 --- a/pex/environment.py +++ b/pex/environment.py @@ -59,7 +59,7 @@ def _import_pkg_resources(): except ImportError: from pex import third_party - third_party.install(expose=["setuptools"]) + third_party.install(expose_if_available=["setuptools"]) try: import pkg_resources # vendor:skip diff --git a/pex/pep_425.py b/pex/pep_425.py index 2b76a4cee..d5a5b3bb4 100644 --- a/pex/pep_425.py +++ b/pex/pep_425.py @@ -49,7 +49,9 @@ class RankedTag(object): rank = attr.ib() # type: TagRank def select_higher_rank(self, other): - # type: (RankedTag) -> RankedTag + # type: (Optional[RankedTag]) -> RankedTag + if other is None: + return self return Rank.select_highest_rank( self, other, extract_rank=lambda ranked_tag: ranked_tag.rank ) diff --git a/pex/pep_427.py b/pex/pep_427.py index ccf67a254..e5cd9578c 100644 --- a/pex/pep_427.py +++ b/pex/pep_427.py @@ -76,8 +76,8 @@ class InstallableType(Enum["InstallableType.Value"]): class Value(Enum.Value): pass - INSTALLED_WHEEL_CHROOT = Value("installed wheel chroot") - WHEEL_FILE = Value(".whl file") + INSTALLED_WHEEL_CHROOT = Value("installed-wheel-chroot") + WHEEL_FILE = Value(".whl-file") InstallableType.seal() diff --git a/pex/pex_builder.py b/pex/pex_builder.py index 14e399332..8cd7f6195 100644 --- a/pex/pex_builder.py +++ b/pex/pex_builder.py @@ -543,7 +543,11 @@ def _prepare_bootstrap(self): # NB: We use pip here in the builder, but that's only at build time, and # although we don't use pyparsing directly, packaging.markers, which we # do use at runtime, does. - root_module_names = ["appdirs", "attr", "colors", "packaging", "pkg_resources", "pyparsing"] + root_module_names = ["appdirs", "attr", "colors", "packaging", "pyparsing"] + for vendor_spec in vendor.iter_vendor_specs(): + if vendor_spec.key == "setuptools": + root_module_names.append("pkg_resources") + prepared_sources = vendor.vendor_runtime( chroot=self._chroot, dest_basedir=self._pex_info.bootstrap, diff --git a/pex/targets.py b/pex/targets.py index 7c861c72f..68824a709 100644 --- a/pex/targets.py +++ b/pex/targets.py @@ -6,6 +6,7 @@ import os from pex.dist_metadata import Constraint, Distribution +from pex.dist_metadata import requires_python as dist_requires_python from pex.interpreter import PythonInterpreter from pex.interpreter_implementation import InterpreterImplementation from pex.orderedset import OrderedSet @@ -31,6 +32,22 @@ class RequiresPythonError(Exception): @attr.s(frozen=True) class WheelEvaluation(object): + @classmethod + def select_best_match(cls, evals): + # type: (Iterable[WheelEvaluation]) -> Optional[WheelEvaluation] + match = None # type: Optional[WheelEvaluation] + for wheel_eval in evals: + if not wheel_eval.applies: + continue + if match is None: + match = wheel_eval + elif wheel_eval.best_match and ( + wheel_eval.best_match.select_higher_rank(match.best_match) == wheel_eval.best_match + ): + match = wheel_eval + return match + + wheel = attr.ib() # type: str tags = attr.ib() # type: Tuple[Tag, ...] best_match = attr.ib() # type: Optional[RankedTag] requires_python = attr.ib() # type: Optional[SpecifierSet] @@ -182,20 +199,30 @@ def requirement_applies( return False def wheel_applies(self, wheel): - # type: (Distribution) -> WheelEvaluation + # type: (Union[str, Distribution]) -> WheelEvaluation + wheel_tags = CompatibilityTags.from_wheel(wheel) ranked_tag = self.supported_tags.best_match(wheel_tags) + + requires_python = None # type: Optional[SpecifierSet] + if ranked_tag: + requires_python = ( + wheel.metadata.requires_python + if isinstance(wheel, Distribution) + else dist_requires_python(wheel) + ) + + wheel_location = wheel.location if isinstance(wheel, Distribution) else wheel return WheelEvaluation( + wheel=wheel_location, tags=tuple(wheel_tags), best_match=ranked_tag, - requires_python=wheel.metadata.requires_python, + requires_python=requires_python, applies=( ranked_tag is not None and ( - not wheel.metadata.requires_python - or self.requires_python_applies( - wheel.metadata.requires_python, source=wheel.location - ) + not requires_python + or self.requires_python_applies(requires_python, source=wheel_location) ) ), ) diff --git a/pex/third_party/__init__.py b/pex/third_party/__init__.py index c4e4aa12b..e04ce3c3b 100644 --- a/pex/third_party/__init__.py +++ b/pex/third_party/__init__.py @@ -244,6 +244,7 @@ def install_vendored( prefix, # type: str root=None, # type: Optional[str] expose=None, # type: Optional[Iterable[str]] + expose_if_available=None, # type: Optional[Iterable[str]] ): # type: (...) -> None """Install an importer for all vendored code with the given import prefix. @@ -255,7 +256,8 @@ def install_vendored( :param root: The root path of the distribution containing the vendored code. NB: This is the path to the pex code, which serves as the root under which code is vendored at ``pex/vendor/_vendored``. - :param expose: Optional names of distributions to expose for direct, un-prefixed import. + :param expose: Names of distributions to expose for direct, un-prefixed import. + :param expose: Names of distributions to expose for direct, un-prefixed import only if available. :raise: :class:`ValueError` if any distributions to expose cannot be found. """ root = cls._abs_root(root) @@ -274,14 +276,17 @@ def install_vendored( uninstallable=True, prefix=prefix, path_items=cls._vendored_path_items(), root=root ) + # Only expose the bits needed. + exposed_paths = [] if expose: - # But only expose the bits needed. - exposed_paths = [] for path in cls.expose(expose, root): sys.path.insert(0, path) exposed_paths.append(os.path.relpath(path, root)) - - vendor_importer._expose(exposed_paths) + if expose_if_available: + for path in cls.expose(expose_if_available, root, optional=True): + sys.path.insert(0, path) + exposed_paths.append(os.path.relpath(path, root)) + vendor_importer._expose(exposed_paths) @classmethod def expose( @@ -289,6 +294,7 @@ def expose( dists, # type: Iterable[str] root=None, # type: Optional[str] interpreter=None, # type: Optional[PythonInterpreter] + optional=False, # type: bool ): # type: (...) -> Iterator[str] from pex import vendor @@ -304,13 +310,14 @@ def iter_available(): (key, relpath) for key, relpath in iter_available() if key in dists ) - unexposed = set(dists) - set(path_by_key.keys()) - if unexposed: - raise ValueError( - "The following vendored dists are not available to expose: {}".format( - ", ".join(sorted(unexposed)) + if not optional: + unexposed = set(dists) - set(path_by_key.keys()) + if unexposed: + raise ValueError( + "The following vendored dists are not available to expose: {}".format( + ", ".join(sorted(unexposed)) + ) ) - ) exposed_paths = path_by_key.values() for exposed_path in exposed_paths: @@ -568,7 +575,7 @@ def import_prefix(): return __name__ -def install(root=None, expose=None): +def install(root=None, expose=None, expose_if_available=None): """Installs the default :class:`VendorImporter` for PEX vendored code. Any distributions listed in ``expose`` will also be exposed for direct import; ie: @@ -610,7 +617,9 @@ def install(root=None, expose=None): :type expose: list of str :raise: :class:`ValueError` if any distributions to expose cannot be found. """ - VendorImporter.install_vendored(prefix=import_prefix(), root=root, expose=expose) + VendorImporter.install_vendored( + prefix=import_prefix(), root=root, expose=expose, expose_if_available=expose_if_available + ) def exposed(root=None): diff --git a/pex/vendor/__init__.py b/pex/vendor/__init__.py index d9fe809a5..48852f931 100644 --- a/pex/vendor/__init__.py +++ b/pex/vendor/__init__.py @@ -9,23 +9,53 @@ import sys from textwrap import dedent -from pex.common import Chroot, is_pyc_dir, is_pyc_file, safe_mkdtemp, touch +from pex.common import Chroot, is_pyc_dir, is_pyc_file, open_zip, safe_mkdtemp, touch +from pex.exceptions import production_assert from pex.tracer import TRACER from pex.typing import TYPE_CHECKING if TYPE_CHECKING: - from typing import Iterable, Iterator, Optional, Sequence, Set, Text, Tuple, Union + from typing import Iterable, Iterator, List, Optional, Sequence, Set, Text, Tuple, Union from pex.interpreter import PythonInterpreter _PACKAGE_COMPONENTS = __name__.split(".") +class EnclosingZip(collections.namedtuple("EnclosingZip", ["path", "entries"])): + pass + + +class VendorRoot(collections.namedtuple("VendorRoot", ["path", "enclosing_zip"])): + pass + + def _root(): + # type: () -> VendorRoot + path = os.path.dirname(os.path.abspath(__file__)) for _ in _PACKAGE_COMPONENTS: path = os.path.dirname(path) - return path + + if os.path.isdir(path): + return VendorRoot(path, enclosing_zip=None) + + import zipfile + + enclosing_zip = path + while not zipfile.is_zipfile(enclosing_zip): + parent = os.path.dirname(enclosing_zip) + production_assert( + parent != enclosing_zip, + "Expected to find enclosing PEX or .whl zip for vendor root: {root}", + root=path, + ) + enclosing_zip = parent + + with open_zip(enclosing_zip) as zf: + return VendorRoot( + path, enclosing_zip=EnclosingZip(path=enclosing_zip, entries=frozenset(zf.namelist())) + ) class VendorSpec( @@ -49,7 +79,7 @@ class VendorSpec( case of pex, which is a py2.py3 platform-agnostic wheel, vendored libraries should be as well. """ - ROOT = _root() + ROOT, _ENCLOSING_ZIP = _root() _VENDOR_DIR = "_vendored" @@ -122,16 +152,31 @@ def prepare(self): @property def _subpath_components(self): + # type: () -> List[str] return [self._VENDOR_DIR, self.import_path] @property def relpath(self): - return os.path.join(*(_PACKAGE_COMPONENTS + self._subpath_components)) + # type: () -> str + return self._relpath() + + def _relpath(self, sep=os.sep): + # type: (str) -> str + return sep.join(_PACKAGE_COMPONENTS + self._subpath_components) @property def target_dir(self): return os.path.join(self.ROOT, self.relpath) + @property + def exists(self): + # type: () -> bool + if self._ENCLOSING_ZIP: + prefix = self.ROOT[len(self._ENCLOSING_ZIP.path) + len(os.sep) :] + target_dir = prefix + "/" + self._relpath("/") + "/" + return target_dir in self._ENCLOSING_ZIP.entries + return os.path.isdir(self.target_dir) + def prepare(self): return self.requirement @@ -264,24 +309,33 @@ def iter_vendor_specs(filter_requires_python=None): # Modern packaging for everyone else. yield VendorSpec.pinned("packaging", "25.0", import_path="packaging_25_0") + # N.B.: All vendored items below are optional and may not be present in Pex distributions + # targeting newer Pythons. + # We use toml to read pyproject.toml when building sdists from local source projects. # The toml project provides compatibility back to Python 2.7, but is frozen in time in 2020 # with bugs - notably no support for heterogeneous lists. We add the more modern tomli for # other Pythons and just use tomllib for Python 3.11+. if not python_major_minor or python_major_minor < (3, 7): - yield VendorSpec.pinned("toml", "0.10.2") + vendored_toml = VendorSpec.pinned("toml", "0.10.2") + if vendored_toml.exists: + yield vendored_toml if not python_major_minor or (3, 7) <= python_major_minor < (3, 11): - yield VendorSpec.pinned("tomli", "2.0.1") + vendored_tomli = VendorSpec.pinned("tomli", "2.0.1") + if vendored_tomli.exists: + yield vendored_tomli # We shell out to pip at build-time to resolve and install dependencies. - yield PIP_SPEC + if not python_major_minor or python_major_minor < (3, 12): + if PIP_SPEC.exists: + yield PIP_SPEC # We expose this to pip at build-time for legacy builds, but we also use pkg_resources via # pex.third_party at runtime to inject pkg_resources style namespace packages if needed. # N.B.: 44.0.0 is the last setuptools version compatible with Python 2 and we use a fork of that # with patches needed to support Pex on the v44.0.0/patches/pex-2.x branch. pex_tool_setuptools_commit = "3acb925dd708430aeaf197ea53ac8a752f7c1863" - yield VendorSpec.git( + vendored_setuptools = VendorSpec.git( repo="https://github.com/pex-tool/setuptools", commit=pex_tool_setuptools_commit, project_name="setuptools", @@ -313,6 +367,9 @@ def iter_vendor_specs(filter_requires_python=None): ).format(commit=pex_tool_setuptools_commit), ], ) + if not python_major_minor or python_major_minor < (3, 12): + if vendored_setuptools.exists: + yield vendored_setuptools def vendor_runtime( diff --git a/pex/wheel.py b/pex/wheel.py index 1ace2d29c..644c92020 100644 --- a/pex/wheel.py +++ b/pex/wheel.py @@ -86,6 +86,8 @@ def load( ) wheel = cls.from_metadata_files(metadata_files) cls._CACHE[(location, project_name)] = wheel + if project_name is None: + cls._CACHE[(location, wheel.files.metadata.project_name)] = wheel return wheel @classmethod diff --git a/scripts/create-packages.py b/scripts/create-packages.py index 55db1ef78..b2bc7c06f 100755 --- a/scripts/create-packages.py +++ b/scripts/create-packages.py @@ -12,11 +12,12 @@ import sys import tempfile from argparse import Action, ArgumentDefaultsHelpFormatter, ArgumentError, ArgumentParser +from dataclasses import dataclass from email.parser import Parser from enum import Enum, unique from http.server import HTTPServer, SimpleHTTPRequestHandler from pathlib import Path, PurePath -from typing import Dict, Iterator, Optional, Tuple, cast +from typing import Dict, Iterator, Optional, Tuple from package.scie_config import PlatformConfig, ScieConfig from pex.common import safe_mkdtemp @@ -191,16 +192,46 @@ def describe_file(path: Path) -> Tuple[str, int]: return hasher.hexdigest(), size +@dataclass +class _FormatData: + _name: str + build_args: Tuple[str, ...] + build_env: Tuple[Tuple[str, str], ...] + + @unique -class Format(Enum): - SDIST = "sdist" - WHEEL = "wheel" +class Format(_FormatData, Enum): + @classmethod + def for_name(cls, name: str): + for member in cls: + if name in (member._name, member.name): + return member + raise ValueError(f"Not a recognized format: {name}") + + SDIST = ("sdist", ("--sdist",), ()) + WHEEL = ( + "whl", + ( + "--wheel", + "--config-setting=--build-option=--python-tag=py2.py35.py36.py37.py38.py39.py310.py311", + ), + (), + ) + WHEEL_3_12_PLUS = ( + "whl-3.12-plus", + ("--wheel", "--config-setting=--build-option=--python-tag=py3"), + (("__PEX_BUILD_WHL_3_12_PLUS__", "1"),), + ) - def __str__(self) -> str: - return cast(str, self.value) + def add_build_env(self, env: Optional[Dict[str, str]] = None) -> Optional[Dict[str, str]]: + if not env and not self.build_env: + return None + build_env = dict(env or os.environ) + build_env.update(self.build_env) + return build_env - def build_arg(self) -> str: - return f"--{self.value}" + def __str__(self) -> str: + return self._name def build_pex_dists( @@ -217,20 +248,14 @@ def build_pex_dists( output = None if verbose else subprocess.DEVNULL - subprocess.run( - args=[ - sys.executable, - "-m", - "build", - "--outdir", - out_dir, - *[fmt.build_arg() for fmt in [dist_fmt, *additional_dist_fmts]], - ], - env=env, - stdout=output, - stderr=output, - check=True, - ) + for fmt in [dist_fmt, *additional_dist_fmts]: + subprocess.run( + args=[sys.executable, "-m", "build", "--outdir", out_dir, *fmt.build_args], + env=fmt.add_build_env(env), + stdout=output, + stderr=output, + check=True, + ) for dist in os.listdir(out_dir): built = dist_dir / dist @@ -359,7 +384,7 @@ def __call__(self, parser, namespace, values, option_string=None): "--additional-format", dest="additional_formats", choices=list(Format), - type=Format, + type=Format.for_name, action="append", help="Package Pex in additional formats.", ) diff --git a/testing/__init__.py b/testing/__init__.py index b324973fd..3e63b283b 100644 --- a/testing/__init__.py +++ b/testing/__init__.py @@ -25,7 +25,7 @@ from pex.interpreter import PythonInterpreter from pex.interpreter_implementation import InterpreterImplementation from pex.os import LINUX, MAC, WINDOWS -from pex.pep_427 import install_wheel_chroot +from pex.pep_427 import install_wheel_chroot, install_wheel_interpreter from pex.pex import PEX from pex.pex_builder import PEXBuilder from pex.pex_info import PexInfo @@ -33,6 +33,7 @@ from pex.pip.version import PipVersion, PipVersionValue from pex.resolve.configured_resolver import ConfiguredResolver from pex.sysconfig import SCRIPT_DIR, script_name +from pex.targets import LocalInterpreter from pex.typing import TYPE_CHECKING, cast from pex.util import named_temporary_file from pex.venv.virtualenv import InstallationChoice, Virtualenv @@ -401,6 +402,7 @@ def re_exact(text): class IntegResults(object): """Convenience object to return integration run results.""" + cmd = attr.ib() # type: Tuple[str, ...] output = attr.ib() # type: Text error = attr.ib() # type: Text return_code = attr.ib() # type: int @@ -450,7 +452,7 @@ def create_pex_command( pex_module="pex", # type: str ): # type: (...) -> List[str] - cmd = [python or sys.executable, "-m", pex_module] + cmd = [installed_pex_wheel_venv_python(python or sys.executable), "-m", pex_module] if pex_module == "pex" and not quiet: cmd.append("-v") if args: @@ -478,7 +480,9 @@ def run_pex_command( cmd=cmd, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) output, error = process.communicate() - return IntegResults(output.decode("utf-8"), error.decode("utf-8"), process.returncode) + return IntegResults( + tuple(cmd), output.decode("utf-8"), error.decode("utf-8"), process.returncode + ) def run_simple_pex( @@ -905,3 +909,18 @@ def _rotate(self, counter_key, x): return x rotate_by = self._counter[counter_key] % len(x) return x[-rotate_by:] + x[:-rotate_by] + + +def installed_pex_wheel_venv_python(python): + # type: (str) -> str + + from testing.pex_dist import wheel + + interpreter = PythonInterpreter.from_binary(python) + pex_wheel = wheel(LocalInterpreter.create(interpreter=interpreter)) + pex_venv_dir = os.path.join(PEX_TEST_DEV_ROOT, "pex_venvs", "0", str(interpreter.identity)) + with atomic_directory(pex_venv_dir) as atomic_dir: + if not atomic_dir.is_finalized(): + venv = Virtualenv.create(atomic_dir.work_dir, interpreter=interpreter) + install_wheel_interpreter(pex_wheel, interpreter=venv.interpreter) + return Virtualenv(pex_venv_dir).interpreter.binary diff --git a/testing/cli.py b/testing/cli.py index 04f09a933..e10b4a34e 100644 --- a/testing/cli.py +++ b/testing/cli.py @@ -6,12 +6,10 @@ import subprocess import sys -from pex.compatibility import to_unicode -from pex.typing import TYPE_CHECKING, cast -from testing import IntegResults +from pex.typing import TYPE_CHECKING +from testing import IntegResults, installed_pex_wheel_venv_python if TYPE_CHECKING: - from typing import Text # noqa from typing import Any @@ -21,14 +19,13 @@ def run_pex3( ): # type: (...) -> IntegResults - python = cast("Text", kwargs.pop("python", None) or to_unicode(sys.executable)) - process = subprocess.Popen( - args=[python, "-mpex.cli"] + list(args), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - **kwargs - ) + python = installed_pex_wheel_venv_python(kwargs.pop("python", None) or sys.executable) + cmd = [python, "-mpex.cli"] + list(args) + process = subprocess.Popen(args=cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) stdout, stderr = process.communicate() return IntegResults( - output=stdout.decode("utf-8"), error=stderr.decode("utf-8"), return_code=process.returncode + cmd=tuple(cmd), + output=stdout.decode("utf-8"), + error=stderr.decode("utf-8"), + return_code=process.returncode, ) diff --git a/testing/data/platforms/complete_platform_almalinux:8.10_py3.11.json b/testing/data/platforms/complete_platform_almalinux:8.10_py3.11.json new file mode 100644 index 000000000..11ae57591 --- /dev/null +++ b/testing/data/platforms/complete_platform_almalinux:8.10_py3.11.json @@ -0,0 +1,732 @@ +{ + "path": "/.venv/bin/python3.11", + "compatible_tags": [ + "cp311-cp311-manylinux_2_28_x86_64", + "cp311-cp311-manylinux_2_27_x86_64", + "cp311-cp311-manylinux_2_26_x86_64", + "cp311-cp311-manylinux_2_25_x86_64", + "cp311-cp311-manylinux_2_24_x86_64", + "cp311-cp311-manylinux_2_23_x86_64", + "cp311-cp311-manylinux_2_22_x86_64", + "cp311-cp311-manylinux_2_21_x86_64", + "cp311-cp311-manylinux_2_20_x86_64", + "cp311-cp311-manylinux_2_19_x86_64", + "cp311-cp311-manylinux_2_18_x86_64", + "cp311-cp311-manylinux_2_17_x86_64", + "cp311-cp311-manylinux2014_x86_64", + "cp311-cp311-manylinux_2_16_x86_64", + "cp311-cp311-manylinux_2_15_x86_64", + "cp311-cp311-manylinux_2_14_x86_64", + "cp311-cp311-manylinux_2_13_x86_64", + "cp311-cp311-manylinux_2_12_x86_64", + "cp311-cp311-manylinux2010_x86_64", + "cp311-cp311-manylinux_2_11_x86_64", + "cp311-cp311-manylinux_2_10_x86_64", + "cp311-cp311-manylinux_2_9_x86_64", + "cp311-cp311-manylinux_2_8_x86_64", + "cp311-cp311-manylinux_2_7_x86_64", + "cp311-cp311-manylinux_2_6_x86_64", + "cp311-cp311-manylinux_2_5_x86_64", + "cp311-cp311-manylinux1_x86_64", + "cp311-cp311-linux_x86_64", + "cp311-abi3-manylinux_2_28_x86_64", + "cp311-abi3-manylinux_2_27_x86_64", + "cp311-abi3-manylinux_2_26_x86_64", + "cp311-abi3-manylinux_2_25_x86_64", + "cp311-abi3-manylinux_2_24_x86_64", + "cp311-abi3-manylinux_2_23_x86_64", + "cp311-abi3-manylinux_2_22_x86_64", + "cp311-abi3-manylinux_2_21_x86_64", + "cp311-abi3-manylinux_2_20_x86_64", + "cp311-abi3-manylinux_2_19_x86_64", + "cp311-abi3-manylinux_2_18_x86_64", + "cp311-abi3-manylinux_2_17_x86_64", + "cp311-abi3-manylinux2014_x86_64", + "cp311-abi3-manylinux_2_16_x86_64", + "cp311-abi3-manylinux_2_15_x86_64", + "cp311-abi3-manylinux_2_14_x86_64", + "cp311-abi3-manylinux_2_13_x86_64", + "cp311-abi3-manylinux_2_12_x86_64", + "cp311-abi3-manylinux2010_x86_64", + "cp311-abi3-manylinux_2_11_x86_64", + "cp311-abi3-manylinux_2_10_x86_64", + "cp311-abi3-manylinux_2_9_x86_64", + "cp311-abi3-manylinux_2_8_x86_64", + "cp311-abi3-manylinux_2_7_x86_64", + "cp311-abi3-manylinux_2_6_x86_64", + "cp311-abi3-manylinux_2_5_x86_64", + "cp311-abi3-manylinux1_x86_64", + "cp311-abi3-linux_x86_64", + "cp311-none-manylinux_2_28_x86_64", + "cp311-none-manylinux_2_27_x86_64", + "cp311-none-manylinux_2_26_x86_64", + "cp311-none-manylinux_2_25_x86_64", + "cp311-none-manylinux_2_24_x86_64", + "cp311-none-manylinux_2_23_x86_64", + "cp311-none-manylinux_2_22_x86_64", + "cp311-none-manylinux_2_21_x86_64", + "cp311-none-manylinux_2_20_x86_64", + "cp311-none-manylinux_2_19_x86_64", + "cp311-none-manylinux_2_18_x86_64", + "cp311-none-manylinux_2_17_x86_64", + "cp311-none-manylinux2014_x86_64", + "cp311-none-manylinux_2_16_x86_64", + "cp311-none-manylinux_2_15_x86_64", + "cp311-none-manylinux_2_14_x86_64", + "cp311-none-manylinux_2_13_x86_64", + "cp311-none-manylinux_2_12_x86_64", + "cp311-none-manylinux2010_x86_64", + "cp311-none-manylinux_2_11_x86_64", + "cp311-none-manylinux_2_10_x86_64", + "cp311-none-manylinux_2_9_x86_64", + "cp311-none-manylinux_2_8_x86_64", + "cp311-none-manylinux_2_7_x86_64", + "cp311-none-manylinux_2_6_x86_64", + "cp311-none-manylinux_2_5_x86_64", + "cp311-none-manylinux1_x86_64", + "cp311-none-linux_x86_64", + "cp310-abi3-manylinux_2_28_x86_64", + "cp310-abi3-manylinux_2_27_x86_64", + "cp310-abi3-manylinux_2_26_x86_64", + "cp310-abi3-manylinux_2_25_x86_64", + "cp310-abi3-manylinux_2_24_x86_64", + "cp310-abi3-manylinux_2_23_x86_64", + "cp310-abi3-manylinux_2_22_x86_64", + "cp310-abi3-manylinux_2_21_x86_64", + "cp310-abi3-manylinux_2_20_x86_64", + "cp310-abi3-manylinux_2_19_x86_64", + "cp310-abi3-manylinux_2_18_x86_64", + "cp310-abi3-manylinux_2_17_x86_64", + "cp310-abi3-manylinux2014_x86_64", + "cp310-abi3-manylinux_2_16_x86_64", + "cp310-abi3-manylinux_2_15_x86_64", + "cp310-abi3-manylinux_2_14_x86_64", + "cp310-abi3-manylinux_2_13_x86_64", + "cp310-abi3-manylinux_2_12_x86_64", + "cp310-abi3-manylinux2010_x86_64", + "cp310-abi3-manylinux_2_11_x86_64", + "cp310-abi3-manylinux_2_10_x86_64", + "cp310-abi3-manylinux_2_9_x86_64", + "cp310-abi3-manylinux_2_8_x86_64", + "cp310-abi3-manylinux_2_7_x86_64", + "cp310-abi3-manylinux_2_6_x86_64", + "cp310-abi3-manylinux_2_5_x86_64", + "cp310-abi3-manylinux1_x86_64", + "cp310-abi3-linux_x86_64", + "cp39-abi3-manylinux_2_28_x86_64", + "cp39-abi3-manylinux_2_27_x86_64", + "cp39-abi3-manylinux_2_26_x86_64", + "cp39-abi3-manylinux_2_25_x86_64", + "cp39-abi3-manylinux_2_24_x86_64", + "cp39-abi3-manylinux_2_23_x86_64", + "cp39-abi3-manylinux_2_22_x86_64", + "cp39-abi3-manylinux_2_21_x86_64", + "cp39-abi3-manylinux_2_20_x86_64", + "cp39-abi3-manylinux_2_19_x86_64", + "cp39-abi3-manylinux_2_18_x86_64", + "cp39-abi3-manylinux_2_17_x86_64", + "cp39-abi3-manylinux2014_x86_64", + "cp39-abi3-manylinux_2_16_x86_64", + "cp39-abi3-manylinux_2_15_x86_64", + "cp39-abi3-manylinux_2_14_x86_64", + "cp39-abi3-manylinux_2_13_x86_64", + "cp39-abi3-manylinux_2_12_x86_64", + "cp39-abi3-manylinux2010_x86_64", + "cp39-abi3-manylinux_2_11_x86_64", + "cp39-abi3-manylinux_2_10_x86_64", + "cp39-abi3-manylinux_2_9_x86_64", + "cp39-abi3-manylinux_2_8_x86_64", + "cp39-abi3-manylinux_2_7_x86_64", + "cp39-abi3-manylinux_2_6_x86_64", + "cp39-abi3-manylinux_2_5_x86_64", + "cp39-abi3-manylinux1_x86_64", + "cp39-abi3-linux_x86_64", + "cp38-abi3-manylinux_2_28_x86_64", + "cp38-abi3-manylinux_2_27_x86_64", + "cp38-abi3-manylinux_2_26_x86_64", + "cp38-abi3-manylinux_2_25_x86_64", + "cp38-abi3-manylinux_2_24_x86_64", + "cp38-abi3-manylinux_2_23_x86_64", + "cp38-abi3-manylinux_2_22_x86_64", + "cp38-abi3-manylinux_2_21_x86_64", + "cp38-abi3-manylinux_2_20_x86_64", + "cp38-abi3-manylinux_2_19_x86_64", + "cp38-abi3-manylinux_2_18_x86_64", + "cp38-abi3-manylinux_2_17_x86_64", + "cp38-abi3-manylinux2014_x86_64", + "cp38-abi3-manylinux_2_16_x86_64", + "cp38-abi3-manylinux_2_15_x86_64", + "cp38-abi3-manylinux_2_14_x86_64", + "cp38-abi3-manylinux_2_13_x86_64", + "cp38-abi3-manylinux_2_12_x86_64", + "cp38-abi3-manylinux2010_x86_64", + "cp38-abi3-manylinux_2_11_x86_64", + "cp38-abi3-manylinux_2_10_x86_64", + "cp38-abi3-manylinux_2_9_x86_64", + "cp38-abi3-manylinux_2_8_x86_64", + "cp38-abi3-manylinux_2_7_x86_64", + "cp38-abi3-manylinux_2_6_x86_64", + "cp38-abi3-manylinux_2_5_x86_64", + "cp38-abi3-manylinux1_x86_64", + "cp38-abi3-linux_x86_64", + "cp37-abi3-manylinux_2_28_x86_64", + "cp37-abi3-manylinux_2_27_x86_64", + "cp37-abi3-manylinux_2_26_x86_64", + "cp37-abi3-manylinux_2_25_x86_64", + "cp37-abi3-manylinux_2_24_x86_64", + "cp37-abi3-manylinux_2_23_x86_64", + "cp37-abi3-manylinux_2_22_x86_64", + "cp37-abi3-manylinux_2_21_x86_64", + "cp37-abi3-manylinux_2_20_x86_64", + "cp37-abi3-manylinux_2_19_x86_64", + "cp37-abi3-manylinux_2_18_x86_64", + "cp37-abi3-manylinux_2_17_x86_64", + "cp37-abi3-manylinux2014_x86_64", + "cp37-abi3-manylinux_2_16_x86_64", + "cp37-abi3-manylinux_2_15_x86_64", + "cp37-abi3-manylinux_2_14_x86_64", + "cp37-abi3-manylinux_2_13_x86_64", + "cp37-abi3-manylinux_2_12_x86_64", + "cp37-abi3-manylinux2010_x86_64", + "cp37-abi3-manylinux_2_11_x86_64", + "cp37-abi3-manylinux_2_10_x86_64", + "cp37-abi3-manylinux_2_9_x86_64", + "cp37-abi3-manylinux_2_8_x86_64", + "cp37-abi3-manylinux_2_7_x86_64", + "cp37-abi3-manylinux_2_6_x86_64", + "cp37-abi3-manylinux_2_5_x86_64", + "cp37-abi3-manylinux1_x86_64", + "cp37-abi3-linux_x86_64", + "cp36-abi3-manylinux_2_28_x86_64", + "cp36-abi3-manylinux_2_27_x86_64", + "cp36-abi3-manylinux_2_26_x86_64", + "cp36-abi3-manylinux_2_25_x86_64", + "cp36-abi3-manylinux_2_24_x86_64", + "cp36-abi3-manylinux_2_23_x86_64", + "cp36-abi3-manylinux_2_22_x86_64", + "cp36-abi3-manylinux_2_21_x86_64", + "cp36-abi3-manylinux_2_20_x86_64", + "cp36-abi3-manylinux_2_19_x86_64", + "cp36-abi3-manylinux_2_18_x86_64", + "cp36-abi3-manylinux_2_17_x86_64", + "cp36-abi3-manylinux2014_x86_64", + "cp36-abi3-manylinux_2_16_x86_64", + "cp36-abi3-manylinux_2_15_x86_64", + "cp36-abi3-manylinux_2_14_x86_64", + "cp36-abi3-manylinux_2_13_x86_64", + "cp36-abi3-manylinux_2_12_x86_64", + "cp36-abi3-manylinux2010_x86_64", + "cp36-abi3-manylinux_2_11_x86_64", + "cp36-abi3-manylinux_2_10_x86_64", + "cp36-abi3-manylinux_2_9_x86_64", + "cp36-abi3-manylinux_2_8_x86_64", + "cp36-abi3-manylinux_2_7_x86_64", + "cp36-abi3-manylinux_2_6_x86_64", + "cp36-abi3-manylinux_2_5_x86_64", + "cp36-abi3-manylinux1_x86_64", + "cp36-abi3-linux_x86_64", + "cp35-abi3-manylinux_2_28_x86_64", + "cp35-abi3-manylinux_2_27_x86_64", + "cp35-abi3-manylinux_2_26_x86_64", + "cp35-abi3-manylinux_2_25_x86_64", + "cp35-abi3-manylinux_2_24_x86_64", + "cp35-abi3-manylinux_2_23_x86_64", + "cp35-abi3-manylinux_2_22_x86_64", + "cp35-abi3-manylinux_2_21_x86_64", + "cp35-abi3-manylinux_2_20_x86_64", + "cp35-abi3-manylinux_2_19_x86_64", + "cp35-abi3-manylinux_2_18_x86_64", + "cp35-abi3-manylinux_2_17_x86_64", + "cp35-abi3-manylinux2014_x86_64", + "cp35-abi3-manylinux_2_16_x86_64", + "cp35-abi3-manylinux_2_15_x86_64", + "cp35-abi3-manylinux_2_14_x86_64", + "cp35-abi3-manylinux_2_13_x86_64", + "cp35-abi3-manylinux_2_12_x86_64", + "cp35-abi3-manylinux2010_x86_64", + "cp35-abi3-manylinux_2_11_x86_64", + "cp35-abi3-manylinux_2_10_x86_64", + "cp35-abi3-manylinux_2_9_x86_64", + "cp35-abi3-manylinux_2_8_x86_64", + "cp35-abi3-manylinux_2_7_x86_64", + "cp35-abi3-manylinux_2_6_x86_64", + "cp35-abi3-manylinux_2_5_x86_64", + "cp35-abi3-manylinux1_x86_64", + "cp35-abi3-linux_x86_64", + "cp34-abi3-manylinux_2_28_x86_64", + "cp34-abi3-manylinux_2_27_x86_64", + "cp34-abi3-manylinux_2_26_x86_64", + "cp34-abi3-manylinux_2_25_x86_64", + "cp34-abi3-manylinux_2_24_x86_64", + "cp34-abi3-manylinux_2_23_x86_64", + "cp34-abi3-manylinux_2_22_x86_64", + "cp34-abi3-manylinux_2_21_x86_64", + "cp34-abi3-manylinux_2_20_x86_64", + "cp34-abi3-manylinux_2_19_x86_64", + "cp34-abi3-manylinux_2_18_x86_64", + "cp34-abi3-manylinux_2_17_x86_64", + "cp34-abi3-manylinux2014_x86_64", + "cp34-abi3-manylinux_2_16_x86_64", + "cp34-abi3-manylinux_2_15_x86_64", + "cp34-abi3-manylinux_2_14_x86_64", + "cp34-abi3-manylinux_2_13_x86_64", + "cp34-abi3-manylinux_2_12_x86_64", + "cp34-abi3-manylinux2010_x86_64", + "cp34-abi3-manylinux_2_11_x86_64", + "cp34-abi3-manylinux_2_10_x86_64", + "cp34-abi3-manylinux_2_9_x86_64", + "cp34-abi3-manylinux_2_8_x86_64", + "cp34-abi3-manylinux_2_7_x86_64", + "cp34-abi3-manylinux_2_6_x86_64", + "cp34-abi3-manylinux_2_5_x86_64", + "cp34-abi3-manylinux1_x86_64", + "cp34-abi3-linux_x86_64", + "cp33-abi3-manylinux_2_28_x86_64", + "cp33-abi3-manylinux_2_27_x86_64", + "cp33-abi3-manylinux_2_26_x86_64", + "cp33-abi3-manylinux_2_25_x86_64", + "cp33-abi3-manylinux_2_24_x86_64", + "cp33-abi3-manylinux_2_23_x86_64", + "cp33-abi3-manylinux_2_22_x86_64", + "cp33-abi3-manylinux_2_21_x86_64", + "cp33-abi3-manylinux_2_20_x86_64", + "cp33-abi3-manylinux_2_19_x86_64", + "cp33-abi3-manylinux_2_18_x86_64", + "cp33-abi3-manylinux_2_17_x86_64", + "cp33-abi3-manylinux2014_x86_64", + "cp33-abi3-manylinux_2_16_x86_64", + "cp33-abi3-manylinux_2_15_x86_64", + "cp33-abi3-manylinux_2_14_x86_64", + "cp33-abi3-manylinux_2_13_x86_64", + "cp33-abi3-manylinux_2_12_x86_64", + "cp33-abi3-manylinux2010_x86_64", + "cp33-abi3-manylinux_2_11_x86_64", + "cp33-abi3-manylinux_2_10_x86_64", + "cp33-abi3-manylinux_2_9_x86_64", + "cp33-abi3-manylinux_2_8_x86_64", + "cp33-abi3-manylinux_2_7_x86_64", + "cp33-abi3-manylinux_2_6_x86_64", + "cp33-abi3-manylinux_2_5_x86_64", + "cp33-abi3-manylinux1_x86_64", + "cp33-abi3-linux_x86_64", + "cp32-abi3-manylinux_2_28_x86_64", + "cp32-abi3-manylinux_2_27_x86_64", + "cp32-abi3-manylinux_2_26_x86_64", + "cp32-abi3-manylinux_2_25_x86_64", + "cp32-abi3-manylinux_2_24_x86_64", + "cp32-abi3-manylinux_2_23_x86_64", + "cp32-abi3-manylinux_2_22_x86_64", + "cp32-abi3-manylinux_2_21_x86_64", + "cp32-abi3-manylinux_2_20_x86_64", + "cp32-abi3-manylinux_2_19_x86_64", + "cp32-abi3-manylinux_2_18_x86_64", + "cp32-abi3-manylinux_2_17_x86_64", + "cp32-abi3-manylinux2014_x86_64", + "cp32-abi3-manylinux_2_16_x86_64", + "cp32-abi3-manylinux_2_15_x86_64", + "cp32-abi3-manylinux_2_14_x86_64", + "cp32-abi3-manylinux_2_13_x86_64", + "cp32-abi3-manylinux_2_12_x86_64", + "cp32-abi3-manylinux2010_x86_64", + "cp32-abi3-manylinux_2_11_x86_64", + "cp32-abi3-manylinux_2_10_x86_64", + "cp32-abi3-manylinux_2_9_x86_64", + "cp32-abi3-manylinux_2_8_x86_64", + "cp32-abi3-manylinux_2_7_x86_64", + "cp32-abi3-manylinux_2_6_x86_64", + "cp32-abi3-manylinux_2_5_x86_64", + "cp32-abi3-manylinux1_x86_64", + "cp32-abi3-linux_x86_64", + "py311-none-manylinux_2_28_x86_64", + "py311-none-manylinux_2_27_x86_64", + "py311-none-manylinux_2_26_x86_64", + "py311-none-manylinux_2_25_x86_64", + "py311-none-manylinux_2_24_x86_64", + "py311-none-manylinux_2_23_x86_64", + "py311-none-manylinux_2_22_x86_64", + "py311-none-manylinux_2_21_x86_64", + "py311-none-manylinux_2_20_x86_64", + "py311-none-manylinux_2_19_x86_64", + "py311-none-manylinux_2_18_x86_64", + "py311-none-manylinux_2_17_x86_64", + "py311-none-manylinux2014_x86_64", + "py311-none-manylinux_2_16_x86_64", + "py311-none-manylinux_2_15_x86_64", + "py311-none-manylinux_2_14_x86_64", + "py311-none-manylinux_2_13_x86_64", + "py311-none-manylinux_2_12_x86_64", + "py311-none-manylinux2010_x86_64", + "py311-none-manylinux_2_11_x86_64", + "py311-none-manylinux_2_10_x86_64", + "py311-none-manylinux_2_9_x86_64", + "py311-none-manylinux_2_8_x86_64", + "py311-none-manylinux_2_7_x86_64", + "py311-none-manylinux_2_6_x86_64", + "py311-none-manylinux_2_5_x86_64", + "py311-none-manylinux1_x86_64", + "py311-none-linux_x86_64", + "py3-none-manylinux_2_28_x86_64", + "py3-none-manylinux_2_27_x86_64", + "py3-none-manylinux_2_26_x86_64", + "py3-none-manylinux_2_25_x86_64", + "py3-none-manylinux_2_24_x86_64", + "py3-none-manylinux_2_23_x86_64", + "py3-none-manylinux_2_22_x86_64", + "py3-none-manylinux_2_21_x86_64", + "py3-none-manylinux_2_20_x86_64", + "py3-none-manylinux_2_19_x86_64", + "py3-none-manylinux_2_18_x86_64", + "py3-none-manylinux_2_17_x86_64", + "py3-none-manylinux2014_x86_64", + "py3-none-manylinux_2_16_x86_64", + "py3-none-manylinux_2_15_x86_64", + "py3-none-manylinux_2_14_x86_64", + "py3-none-manylinux_2_13_x86_64", + "py3-none-manylinux_2_12_x86_64", + "py3-none-manylinux2010_x86_64", + "py3-none-manylinux_2_11_x86_64", + "py3-none-manylinux_2_10_x86_64", + "py3-none-manylinux_2_9_x86_64", + "py3-none-manylinux_2_8_x86_64", + "py3-none-manylinux_2_7_x86_64", + "py3-none-manylinux_2_6_x86_64", + "py3-none-manylinux_2_5_x86_64", + "py3-none-manylinux1_x86_64", + "py3-none-linux_x86_64", + "py310-none-manylinux_2_28_x86_64", + "py310-none-manylinux_2_27_x86_64", + "py310-none-manylinux_2_26_x86_64", + "py310-none-manylinux_2_25_x86_64", + "py310-none-manylinux_2_24_x86_64", + "py310-none-manylinux_2_23_x86_64", + "py310-none-manylinux_2_22_x86_64", + "py310-none-manylinux_2_21_x86_64", + "py310-none-manylinux_2_20_x86_64", + "py310-none-manylinux_2_19_x86_64", + "py310-none-manylinux_2_18_x86_64", + "py310-none-manylinux_2_17_x86_64", + "py310-none-manylinux2014_x86_64", + "py310-none-manylinux_2_16_x86_64", + "py310-none-manylinux_2_15_x86_64", + "py310-none-manylinux_2_14_x86_64", + "py310-none-manylinux_2_13_x86_64", + "py310-none-manylinux_2_12_x86_64", + "py310-none-manylinux2010_x86_64", + "py310-none-manylinux_2_11_x86_64", + "py310-none-manylinux_2_10_x86_64", + "py310-none-manylinux_2_9_x86_64", + "py310-none-manylinux_2_8_x86_64", + "py310-none-manylinux_2_7_x86_64", + "py310-none-manylinux_2_6_x86_64", + "py310-none-manylinux_2_5_x86_64", + "py310-none-manylinux1_x86_64", + "py310-none-linux_x86_64", + "py39-none-manylinux_2_28_x86_64", + "py39-none-manylinux_2_27_x86_64", + "py39-none-manylinux_2_26_x86_64", + "py39-none-manylinux_2_25_x86_64", + "py39-none-manylinux_2_24_x86_64", + "py39-none-manylinux_2_23_x86_64", + "py39-none-manylinux_2_22_x86_64", + "py39-none-manylinux_2_21_x86_64", + "py39-none-manylinux_2_20_x86_64", + "py39-none-manylinux_2_19_x86_64", + "py39-none-manylinux_2_18_x86_64", + "py39-none-manylinux_2_17_x86_64", + "py39-none-manylinux2014_x86_64", + "py39-none-manylinux_2_16_x86_64", + "py39-none-manylinux_2_15_x86_64", + "py39-none-manylinux_2_14_x86_64", + "py39-none-manylinux_2_13_x86_64", + "py39-none-manylinux_2_12_x86_64", + "py39-none-manylinux2010_x86_64", + "py39-none-manylinux_2_11_x86_64", + "py39-none-manylinux_2_10_x86_64", + "py39-none-manylinux_2_9_x86_64", + "py39-none-manylinux_2_8_x86_64", + "py39-none-manylinux_2_7_x86_64", + "py39-none-manylinux_2_6_x86_64", + "py39-none-manylinux_2_5_x86_64", + "py39-none-manylinux1_x86_64", + "py39-none-linux_x86_64", + "py38-none-manylinux_2_28_x86_64", + "py38-none-manylinux_2_27_x86_64", + "py38-none-manylinux_2_26_x86_64", + "py38-none-manylinux_2_25_x86_64", + "py38-none-manylinux_2_24_x86_64", + "py38-none-manylinux_2_23_x86_64", + "py38-none-manylinux_2_22_x86_64", + "py38-none-manylinux_2_21_x86_64", + "py38-none-manylinux_2_20_x86_64", + "py38-none-manylinux_2_19_x86_64", + "py38-none-manylinux_2_18_x86_64", + "py38-none-manylinux_2_17_x86_64", + "py38-none-manylinux2014_x86_64", + "py38-none-manylinux_2_16_x86_64", + "py38-none-manylinux_2_15_x86_64", + "py38-none-manylinux_2_14_x86_64", + "py38-none-manylinux_2_13_x86_64", + "py38-none-manylinux_2_12_x86_64", + "py38-none-manylinux2010_x86_64", + "py38-none-manylinux_2_11_x86_64", + "py38-none-manylinux_2_10_x86_64", + "py38-none-manylinux_2_9_x86_64", + "py38-none-manylinux_2_8_x86_64", + "py38-none-manylinux_2_7_x86_64", + "py38-none-manylinux_2_6_x86_64", + "py38-none-manylinux_2_5_x86_64", + "py38-none-manylinux1_x86_64", + "py38-none-linux_x86_64", + "py37-none-manylinux_2_28_x86_64", + "py37-none-manylinux_2_27_x86_64", + "py37-none-manylinux_2_26_x86_64", + "py37-none-manylinux_2_25_x86_64", + "py37-none-manylinux_2_24_x86_64", + "py37-none-manylinux_2_23_x86_64", + "py37-none-manylinux_2_22_x86_64", + "py37-none-manylinux_2_21_x86_64", + "py37-none-manylinux_2_20_x86_64", + "py37-none-manylinux_2_19_x86_64", + "py37-none-manylinux_2_18_x86_64", + "py37-none-manylinux_2_17_x86_64", + "py37-none-manylinux2014_x86_64", + "py37-none-manylinux_2_16_x86_64", + "py37-none-manylinux_2_15_x86_64", + "py37-none-manylinux_2_14_x86_64", + "py37-none-manylinux_2_13_x86_64", + "py37-none-manylinux_2_12_x86_64", + "py37-none-manylinux2010_x86_64", + "py37-none-manylinux_2_11_x86_64", + "py37-none-manylinux_2_10_x86_64", + "py37-none-manylinux_2_9_x86_64", + "py37-none-manylinux_2_8_x86_64", + "py37-none-manylinux_2_7_x86_64", + "py37-none-manylinux_2_6_x86_64", + "py37-none-manylinux_2_5_x86_64", + "py37-none-manylinux1_x86_64", + "py37-none-linux_x86_64", + "py36-none-manylinux_2_28_x86_64", + "py36-none-manylinux_2_27_x86_64", + "py36-none-manylinux_2_26_x86_64", + "py36-none-manylinux_2_25_x86_64", + "py36-none-manylinux_2_24_x86_64", + "py36-none-manylinux_2_23_x86_64", + "py36-none-manylinux_2_22_x86_64", + "py36-none-manylinux_2_21_x86_64", + "py36-none-manylinux_2_20_x86_64", + "py36-none-manylinux_2_19_x86_64", + "py36-none-manylinux_2_18_x86_64", + "py36-none-manylinux_2_17_x86_64", + "py36-none-manylinux2014_x86_64", + "py36-none-manylinux_2_16_x86_64", + "py36-none-manylinux_2_15_x86_64", + "py36-none-manylinux_2_14_x86_64", + "py36-none-manylinux_2_13_x86_64", + "py36-none-manylinux_2_12_x86_64", + "py36-none-manylinux2010_x86_64", + "py36-none-manylinux_2_11_x86_64", + "py36-none-manylinux_2_10_x86_64", + "py36-none-manylinux_2_9_x86_64", + "py36-none-manylinux_2_8_x86_64", + "py36-none-manylinux_2_7_x86_64", + "py36-none-manylinux_2_6_x86_64", + "py36-none-manylinux_2_5_x86_64", + "py36-none-manylinux1_x86_64", + "py36-none-linux_x86_64", + "py35-none-manylinux_2_28_x86_64", + "py35-none-manylinux_2_27_x86_64", + "py35-none-manylinux_2_26_x86_64", + "py35-none-manylinux_2_25_x86_64", + "py35-none-manylinux_2_24_x86_64", + "py35-none-manylinux_2_23_x86_64", + "py35-none-manylinux_2_22_x86_64", + "py35-none-manylinux_2_21_x86_64", + "py35-none-manylinux_2_20_x86_64", + "py35-none-manylinux_2_19_x86_64", + "py35-none-manylinux_2_18_x86_64", + "py35-none-manylinux_2_17_x86_64", + "py35-none-manylinux2014_x86_64", + "py35-none-manylinux_2_16_x86_64", + "py35-none-manylinux_2_15_x86_64", + "py35-none-manylinux_2_14_x86_64", + "py35-none-manylinux_2_13_x86_64", + "py35-none-manylinux_2_12_x86_64", + "py35-none-manylinux2010_x86_64", + "py35-none-manylinux_2_11_x86_64", + "py35-none-manylinux_2_10_x86_64", + "py35-none-manylinux_2_9_x86_64", + "py35-none-manylinux_2_8_x86_64", + "py35-none-manylinux_2_7_x86_64", + "py35-none-manylinux_2_6_x86_64", + "py35-none-manylinux_2_5_x86_64", + "py35-none-manylinux1_x86_64", + "py35-none-linux_x86_64", + "py34-none-manylinux_2_28_x86_64", + "py34-none-manylinux_2_27_x86_64", + "py34-none-manylinux_2_26_x86_64", + "py34-none-manylinux_2_25_x86_64", + "py34-none-manylinux_2_24_x86_64", + "py34-none-manylinux_2_23_x86_64", + "py34-none-manylinux_2_22_x86_64", + "py34-none-manylinux_2_21_x86_64", + "py34-none-manylinux_2_20_x86_64", + "py34-none-manylinux_2_19_x86_64", + "py34-none-manylinux_2_18_x86_64", + "py34-none-manylinux_2_17_x86_64", + "py34-none-manylinux2014_x86_64", + "py34-none-manylinux_2_16_x86_64", + "py34-none-manylinux_2_15_x86_64", + "py34-none-manylinux_2_14_x86_64", + "py34-none-manylinux_2_13_x86_64", + "py34-none-manylinux_2_12_x86_64", + "py34-none-manylinux2010_x86_64", + "py34-none-manylinux_2_11_x86_64", + "py34-none-manylinux_2_10_x86_64", + "py34-none-manylinux_2_9_x86_64", + "py34-none-manylinux_2_8_x86_64", + "py34-none-manylinux_2_7_x86_64", + "py34-none-manylinux_2_6_x86_64", + "py34-none-manylinux_2_5_x86_64", + "py34-none-manylinux1_x86_64", + "py34-none-linux_x86_64", + "py33-none-manylinux_2_28_x86_64", + "py33-none-manylinux_2_27_x86_64", + "py33-none-manylinux_2_26_x86_64", + "py33-none-manylinux_2_25_x86_64", + "py33-none-manylinux_2_24_x86_64", + "py33-none-manylinux_2_23_x86_64", + "py33-none-manylinux_2_22_x86_64", + "py33-none-manylinux_2_21_x86_64", + "py33-none-manylinux_2_20_x86_64", + "py33-none-manylinux_2_19_x86_64", + "py33-none-manylinux_2_18_x86_64", + "py33-none-manylinux_2_17_x86_64", + "py33-none-manylinux2014_x86_64", + "py33-none-manylinux_2_16_x86_64", + "py33-none-manylinux_2_15_x86_64", + "py33-none-manylinux_2_14_x86_64", + "py33-none-manylinux_2_13_x86_64", + "py33-none-manylinux_2_12_x86_64", + "py33-none-manylinux2010_x86_64", + "py33-none-manylinux_2_11_x86_64", + "py33-none-manylinux_2_10_x86_64", + "py33-none-manylinux_2_9_x86_64", + "py33-none-manylinux_2_8_x86_64", + "py33-none-manylinux_2_7_x86_64", + "py33-none-manylinux_2_6_x86_64", + "py33-none-manylinux_2_5_x86_64", + "py33-none-manylinux1_x86_64", + "py33-none-linux_x86_64", + "py32-none-manylinux_2_28_x86_64", + "py32-none-manylinux_2_27_x86_64", + "py32-none-manylinux_2_26_x86_64", + "py32-none-manylinux_2_25_x86_64", + "py32-none-manylinux_2_24_x86_64", + "py32-none-manylinux_2_23_x86_64", + "py32-none-manylinux_2_22_x86_64", + "py32-none-manylinux_2_21_x86_64", + "py32-none-manylinux_2_20_x86_64", + "py32-none-manylinux_2_19_x86_64", + "py32-none-manylinux_2_18_x86_64", + "py32-none-manylinux_2_17_x86_64", + "py32-none-manylinux2014_x86_64", + "py32-none-manylinux_2_16_x86_64", + "py32-none-manylinux_2_15_x86_64", + "py32-none-manylinux_2_14_x86_64", + "py32-none-manylinux_2_13_x86_64", + "py32-none-manylinux_2_12_x86_64", + "py32-none-manylinux2010_x86_64", + "py32-none-manylinux_2_11_x86_64", + "py32-none-manylinux_2_10_x86_64", + "py32-none-manylinux_2_9_x86_64", + "py32-none-manylinux_2_8_x86_64", + "py32-none-manylinux_2_7_x86_64", + "py32-none-manylinux_2_6_x86_64", + "py32-none-manylinux_2_5_x86_64", + "py32-none-manylinux1_x86_64", + "py32-none-linux_x86_64", + "py31-none-manylinux_2_28_x86_64", + "py31-none-manylinux_2_27_x86_64", + "py31-none-manylinux_2_26_x86_64", + "py31-none-manylinux_2_25_x86_64", + "py31-none-manylinux_2_24_x86_64", + "py31-none-manylinux_2_23_x86_64", + "py31-none-manylinux_2_22_x86_64", + "py31-none-manylinux_2_21_x86_64", + "py31-none-manylinux_2_20_x86_64", + "py31-none-manylinux_2_19_x86_64", + "py31-none-manylinux_2_18_x86_64", + "py31-none-manylinux_2_17_x86_64", + "py31-none-manylinux2014_x86_64", + "py31-none-manylinux_2_16_x86_64", + "py31-none-manylinux_2_15_x86_64", + "py31-none-manylinux_2_14_x86_64", + "py31-none-manylinux_2_13_x86_64", + "py31-none-manylinux_2_12_x86_64", + "py31-none-manylinux2010_x86_64", + "py31-none-manylinux_2_11_x86_64", + "py31-none-manylinux_2_10_x86_64", + "py31-none-manylinux_2_9_x86_64", + "py31-none-manylinux_2_8_x86_64", + "py31-none-manylinux_2_7_x86_64", + "py31-none-manylinux_2_6_x86_64", + "py31-none-manylinux_2_5_x86_64", + "py31-none-manylinux1_x86_64", + "py31-none-linux_x86_64", + "py30-none-manylinux_2_28_x86_64", + "py30-none-manylinux_2_27_x86_64", + "py30-none-manylinux_2_26_x86_64", + "py30-none-manylinux_2_25_x86_64", + "py30-none-manylinux_2_24_x86_64", + "py30-none-manylinux_2_23_x86_64", + "py30-none-manylinux_2_22_x86_64", + "py30-none-manylinux_2_21_x86_64", + "py30-none-manylinux_2_20_x86_64", + "py30-none-manylinux_2_19_x86_64", + "py30-none-manylinux_2_18_x86_64", + "py30-none-manylinux_2_17_x86_64", + "py30-none-manylinux2014_x86_64", + "py30-none-manylinux_2_16_x86_64", + "py30-none-manylinux_2_15_x86_64", + "py30-none-manylinux_2_14_x86_64", + "py30-none-manylinux_2_13_x86_64", + "py30-none-manylinux_2_12_x86_64", + "py30-none-manylinux2010_x86_64", + "py30-none-manylinux_2_11_x86_64", + "py30-none-manylinux_2_10_x86_64", + "py30-none-manylinux_2_9_x86_64", + "py30-none-manylinux_2_8_x86_64", + "py30-none-manylinux_2_7_x86_64", + "py30-none-manylinux_2_6_x86_64", + "py30-none-manylinux_2_5_x86_64", + "py30-none-manylinux1_x86_64", + "py30-none-linux_x86_64", + "cp311-none-any", + "py311-none-any", + "py3-none-any", + "py310-none-any", + "py39-none-any", + "py38-none-any", + "py37-none-any", + "py36-none-any", + "py35-none-any", + "py34-none-any", + "py33-none-any", + "py32-none-any", + "py31-none-any", + "py30-none-any" + ], + "marker_environment": { + "implementation_name": "cpython", + "implementation_version": "3.11.13", + "os_name": "posix", + "platform_machine": "x86_64", + "platform_python_implementation": "CPython", + "platform_release": "6.17.0-8-generic", + "platform_system": "Linux", + "platform_version": "#8-Ubuntu SMP PREEMPT_DYNAMIC Fri Nov 14 21:44:46 UTC 2025", + "python_full_version": "3.11.13", + "python_version": "3.11", + "sys_platform": "linux" + } +} diff --git a/testing/pex_dist.py b/testing/pex_dist.py index 07b1def75..d5fec6aad 100644 --- a/testing/pex_dist.py +++ b/testing/pex_dist.py @@ -1,25 +1,29 @@ # Copyright 2025 Pex project contributors. # Licensed under the Apache License, Version 2.0 (see LICENSE). -from __future__ import absolute_import +from __future__ import absolute_import, print_function +import glob import hashlib import json import os +import subprocess from pex import hashing from pex.atomic_directory import atomic_directory from pex.compatibility import ConfigParser -from pex.pip.version import PipVersion -from pex.typing import cast +from pex.targets import LocalInterpreter, Target, WheelEvaluation +from pex.typing import TYPE_CHECKING, cast from pex.util import CacheHelper -from pex.venv.virtualenv import InstallationChoice, Virtualenv from pex.version import __version__ from testing import PEX_TEST_DEV_ROOT, pex_project_dir +if TYPE_CHECKING: + from typing import Iterable, List -def wheel(): - # type: () -> str + +def wheels(): + # type: () -> List[str] hasher = hashlib.sha1 pex_dir = pex_project_dir() @@ -49,32 +53,45 @@ def dir_hash(rel_path): ).encode("utf-8") ).hexdigest() - pex_wheel_dir = os.path.join(PEX_TEST_DEV_ROOT, "pex_wheels", pex_wheel_inputs_fingerprint) - with atomic_directory(pex_wheel_dir, source="dist") as atomic_dir: + pex_wheel_dir = os.path.join(PEX_TEST_DEV_ROOT, "pex_wheels", "0", pex_wheel_inputs_fingerprint) + with atomic_directory(pex_wheel_dir) as atomic_dir: if not atomic_dir.is_finalized(): - venv = Virtualenv.create( - os.path.join(atomic_dir.work_dir, "venv"), - install_pip=( - InstallationChoice.YES - if PipVersion.DEFAULT is PipVersion.VENDORED - else InstallationChoice.UPGRADED - ), - install_wheel=( - InstallationChoice.YES - if PipVersion.DEFAULT is PipVersion.VENDORED - else InstallationChoice.NO - ), + subprocess.check_call( + args=[ + "uv", + "run", + "dev-cmd", + "package", + "--", + "--no-pex", + "--additional-format", + "whl", + "--additional-format", + "whl-3.12-plus", + "--dist-dir", + atomic_dir.work_dir, + ] ) - dist_dir = os.path.join(atomic_dir.work_dir, "dist") - if PipVersion.DEFAULT is PipVersion.VENDORED: - venv.interpreter.execute( - args=[os.path.join(pex_dir, "setup.py"), "bdist_wheel", "-d", dist_dir] - ) - else: - venv.interpreter.execute(args=["-m", "pip", "wheel", pex_dir, "-w", dist_dir]) - return os.path.join( - pex_wheel_dir, "pex-{version}-py2.py3-none-any.whl".format(version=__version__) + return glob.glob(os.path.join(pex_wheel_dir, "pex-{version}-*.whl".format(version=__version__))) + + +def select_best_wheel( + whls, # type: Iterable[str] + target=LocalInterpreter.create(), # type: Target +): + # type: (...) -> str + wheel_eval = WheelEvaluation.select_best_match(target.wheel_applies(whl) for whl in whls) + assert ( + wheel_eval is not None + ), "Expected a wheel from {wheels} to be compatible with {interpreter}".format( + wheels=wheels, interpreter=target.render_description() ) + return wheel_eval.wheel + + +def wheel(target=LocalInterpreter.create()): + # type: (Target) -> str + return select_best_wheel(wheels(), target=target) def requires_python(): diff --git a/tests/conftest.py b/tests/conftest.py index 3637c7d14..00faa3acd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -17,7 +17,7 @@ from testing.pytest_utils import tmp, track_status_hook if TYPE_CHECKING: - from typing import Iterator + from typing import Iterator, List from _pytest.fixtures import FixtureRequest @@ -34,6 +34,12 @@ def pex_wheel(): return pex_dist.wheel() +@pytest.fixture(scope="session") +def pex_wheels(): + # type: () -> List[str] + return pex_dist.wheels() + + @pytest.fixture(scope="session") def pex_requires_python(): # type: () -> str diff --git a/tests/integration/cli/commands/test_run.py b/tests/integration/cli/commands/test_run.py index 9aae24791..e93e108ba 100644 --- a/tests/integration/cli/commands/test_run.py +++ b/tests/integration/cli/commands/test_run.py @@ -556,7 +556,7 @@ def test_pexec(tmpdir): dist_dir, "--scie", "--additional-format", - "wheel", + "whl-3.12-plus" if sys.version_info[:2] >= (3, 12) else "whl", ] ) @@ -568,9 +568,10 @@ def test_pexec(tmpdir): "-m", "pip", "install", - os.path.join( - dist_dir, "pex-{version}-py2.py3-none-any.whl".format(version=__version__) - ), + "--no-index", + "--find-links", + dist_dir, + "pex=={version}".format(version=__version__), ] ) assert b"| Moo! |" in subprocess.check_output( diff --git a/tests/integration/resolve/test_issue_2532.py b/tests/integration/resolve/test_issue_2532.py index 5d228370f..83b8b9eab 100644 --- a/tests/integration/resolve/test_issue_2532.py +++ b/tests/integration/resolve/test_issue_2532.py @@ -3,27 +3,47 @@ from __future__ import absolute_import +import json import os import shutil from textwrap import dedent +import pytest + +from pex.pep_425 import CompatibilityTags +from pex.pep_508 import MarkerEnvironment +from pex.targets import CompletePlatform, Target from pex.typing import TYPE_CHECKING -from testing import subprocess +from testing import data, pex_dist, subprocess from testing.docker import skip_unless_docker if TYPE_CHECKING: - from typing import Any + from typing import Any, List + + +@pytest.fixture +def docker_python(): + # type: () -> Target + with open(data.path("platforms", "complete_platform_almalinux:8.10_py3.11.json")) as fp: + complete_platform_data = json.load(fp) + + return CompletePlatform.create( + marker_environment=MarkerEnvironment(**complete_platform_data["marker_environment"]), + supported_tags=CompatibilityTags.from_strings(complete_platform_data["compatible_tags"]), + ) @skip_unless_docker def test_resolved_wheel_tag_platform_mismatch_warns( tmpdir, # type: Any - pex_wheel, # type: str + pex_wheels, # type: List[str] + docker_python, # type: Target ): # type: (...) -> None context = os.path.join(str(tmpdir), "context") os.mkdir(context) + pex_wheel = pex_dist.select_best_wheel(pex_wheels, target=docker_python) shutil.copy(pex_wheel, context) with open(os.path.join(context, "Dockerfile"), "w") as fp: fp.write( diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index b7326536e..5ce81e4f1 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -31,6 +31,7 @@ from pex.pex_info import PexInfo from pex.pip.version import PipVersion from pex.requirements import LogicalLine, PyPIRequirement, parse_requirement_file +from pex.targets import LocalInterpreter from pex.typing import TYPE_CHECKING, cast from pex.util import named_temporary_file from pex.variables import ENV, unzip_dir, venv_dir @@ -50,6 +51,7 @@ ensure_python_interpreter, get_dep_dist_names_from_pex, make_env, + pex_dist, run_pex_command, run_simple_pex, run_simple_pex_test, @@ -132,13 +134,18 @@ def test_pex_root_build(): @skip_if_only_vendored_pip_supported def test_pex_root_run( - pex_wheel, # type: str + pex_wheels, # type: str tmpdir, # type: Any ): # type: (...) -> None python39 = ensure_python_interpreter(PY39) python311 = ensure_python_interpreter(PY311) - + wheels = [ + pex_dist.select_best_wheel( + pex_wheels, LocalInterpreter.create(PythonInterpreter.from_binary(binary)) + ) + for binary in (python39, python311) + ] runtime_pex_root = safe_mkdir(os.path.join(str(tmpdir), "runtime_pex_root")) home = safe_mkdir(os.path.join(str(tmpdir), "home")) @@ -151,7 +158,6 @@ def test_pex_root_run( args = [ "--pip-version", PipVersion.LATEST_COMPATIBLE.value, - pex_wheel, "-o", pex_pex, "-c", @@ -160,7 +166,7 @@ def test_pex_root_run( "--pex-root={}".format(buildtime_pex_root), "--runtime-pex-root={}".format(runtime_pex_root), "--interpreter-constraint=CPython=={version}".format(version=PY311), - ] + ] + wheels results = run_pex_command(args=args, env=pex_env, python=python311) results.assert_success() assert ["pex.pex"] == os.listdir(output_dir), "Expected built pex file." @@ -1825,7 +1831,7 @@ def test_seed_verbose( assert pex_root == verbose_info.pop("pex_root") python = verbose_info.pop("python") - assert PythonInterpreter.get() == PythonInterpreter.from_binary(python) + assert PythonInterpreter.from_binary(results.cmd[0]) == PythonInterpreter.from_binary(python) verbose_info.pop("pex") assert {} == verbose_info diff --git a/tests/integration/tools/commands/test_venv.py b/tests/integration/tools/commands/test_venv.py index 274717f51..768f65cde 100644 --- a/tests/integration/tools/commands/test_venv.py +++ b/tests/integration/tools/commands/test_venv.py @@ -11,7 +11,13 @@ from pex.typing import TYPE_CHECKING from pex.util import CacheHelper from pex.venv.virtualenv import Virtualenv -from testing import IntegResults, make_env, run_pex_command, subprocess +from testing import ( + IntegResults, + installed_pex_wheel_venv_python, + make_env, + run_pex_command, + subprocess, +) from testing.venv import assert_venv_site_packages_copy_mode if TYPE_CHECKING: @@ -21,14 +27,18 @@ def run_pex_tools(*args): # type: (*str) -> IntegResults + cmd = [installed_pex_wheel_venv_python(sys.executable), "-m", "pex.tools"] + list(args) process = subprocess.Popen( - args=[sys.executable, "-mpex.tools"] + list(args), + args=cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, ) stdout, stderr = process.communicate() return IntegResults( - output=stdout.decode("utf-8"), error=stderr.decode("utf-8"), return_code=process.returncode + tuple(cmd), + output=stdout.decode("utf-8"), + error=stderr.decode("utf-8"), + return_code=process.returncode, ) diff --git a/tests/integration/venv_ITs/test_issue_1745.py b/tests/integration/venv_ITs/test_issue_1745.py index 03c8417a5..0c6f378e9 100644 --- a/tests/integration/venv_ITs/test_issue_1745.py +++ b/tests/integration/venv_ITs/test_issue_1745.py @@ -131,6 +131,7 @@ def execute_pex(disable_assertions): ) stdout, stderr = process.communicate(input=execution_configuration.stdin) return IntegResults( + cmd=tuple(args), output=stdout.decode("utf-8"), error=stderr.decode("utf-8"), return_code=process.returncode, diff --git a/tests/integration/venv_ITs/test_issue_2248.py b/tests/integration/venv_ITs/test_issue_2248.py index 09f5eaabd..021c0ad2a 100644 --- a/tests/integration/venv_ITs/test_issue_2248.py +++ b/tests/integration/venv_ITs/test_issue_2248.py @@ -73,6 +73,7 @@ def execute_pex(disable_assertions): ) stdout, stderr = process.communicate(input=repl_commands.encode("utf-8")) return IntegResults( + cmd=tuple(args), output=stdout.decode("utf-8"), error=stderr.decode("utf-8"), return_code=process.returncode, From 873bdd9656dceed3fcca503531b0a2f52cba4780 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Mon, 29 Dec 2025 16:15:57 -0800 Subject: [PATCH 02/23] Prepare a release. --- CHANGES.md | 12 ++++++++++++ pex/version.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index 68fe0ed0c..bab31cd29 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,5 +1,17 @@ # Release Notes +## 2.77.0 + +This release has no fixes or new features per-se, but just changes the set of distributions that +Pex releases to PyPI. Previously Pex released an sdist and a universal (`py2.py3-none-any`) `.whl`. +Pex now releases two wheels in addition to the sdist. The `py3.py312-none-any.whl` targets +Python>=3.12 and has un-needed vendored libraries elided making it bith a smaller `.whl` and less +prone to false-positive security scan issues since unused vendored code is now omitted. The other +wheel carries the same contents as prior and supports creating PEXes for Python 2.7 and +Python>=3.5,<3.12. + +* Split Pex `.whl` into two `.whl`s. (#3057) + ## 2.76.1 This release fixes bootstrapping of Pips specified via `--pip-version` to respect Pex Pip diff --git a/pex/version.py b/pex/version.py index fdd12227d..9b1e2e40a 100644 --- a/pex/version.py +++ b/pex/version.py @@ -1,4 +1,4 @@ # Copyright 2015 Pex project contributors. # Licensed under the Apache License, Version 2.0 (see LICENSE). -__version__ = "2.76.1" +__version__ = "2.77.0" From 20f3e79b648e15c37e94c88560344c2a0138374a Mon Sep 17 00:00:00 2001 From: John Sirois Date: Mon, 29 Dec 2025 16:23:22 -0800 Subject: [PATCH 03/23] Fixes + I <3 Windows. --- scripts/create-packages.py | 2 +- ...py3.11.json => complete_platform_almalinux-8.10_py3.11.json} | 0 tests/integration/resolve/test_issue_2532.py | 2 +- 3 files changed, 2 insertions(+), 2 deletions(-) rename testing/data/platforms/{complete_platform_almalinux:8.10_py3.11.json => complete_platform_almalinux-8.10_py3.11.json} (100%) diff --git a/scripts/create-packages.py b/scripts/create-packages.py index b2bc7c06f..153dc518c 100755 --- a/scripts/create-packages.py +++ b/scripts/create-packages.py @@ -219,7 +219,7 @@ def for_name(cls, name: str): ) WHEEL_3_12_PLUS = ( "whl-3.12-plus", - ("--wheel", "--config-setting=--build-option=--python-tag=py3"), + ("--wheel", "--config-setting=--build-option=--python-tag=py3.py312"), (("__PEX_BUILD_WHL_3_12_PLUS__", "1"),), ) diff --git a/testing/data/platforms/complete_platform_almalinux:8.10_py3.11.json b/testing/data/platforms/complete_platform_almalinux-8.10_py3.11.json similarity index 100% rename from testing/data/platforms/complete_platform_almalinux:8.10_py3.11.json rename to testing/data/platforms/complete_platform_almalinux-8.10_py3.11.json diff --git a/tests/integration/resolve/test_issue_2532.py b/tests/integration/resolve/test_issue_2532.py index 83b8b9eab..75aeea413 100644 --- a/tests/integration/resolve/test_issue_2532.py +++ b/tests/integration/resolve/test_issue_2532.py @@ -24,7 +24,7 @@ @pytest.fixture def docker_python(): # type: () -> Target - with open(data.path("platforms", "complete_platform_almalinux:8.10_py3.11.json")) as fp: + with open(data.path("platforms", "complete_platform_almalinux-8.10_py3.11.json")) as fp: complete_platform_data = json.load(fp) return CompletePlatform.create( From 60962ef36149f9ad64a9a3177f175f6484f67244 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Tue, 30 Dec 2025 13:18:37 -0800 Subject: [PATCH 04/23] Fix `uvrc vendor`. --- pex/vendor/__init__.py | 16 ++++++++++------ pex/vendor/__main__.py | 2 +- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/pex/vendor/__init__.py b/pex/vendor/__init__.py index 48852f931..eb0f5f416 100644 --- a/pex/vendor/__init__.py +++ b/pex/vendor/__init__.py @@ -258,12 +258,16 @@ def create_packages(self): ) -def iter_vendor_specs(filter_requires_python=None): - # type: (Optional[Union[Tuple[int, int], PythonInterpreter]]) -> Iterator[VendorSpec] +def iter_vendor_specs( + filter_requires_python=None, # type: Optional[Union[Tuple[int, int], PythonInterpreter]] + filter_exists=True, # type: bool +): + # type: (...) -> Iterator[VendorSpec] """Iterate specifications for code vendored by pex. :param filter_requires_python: An optional interpreter (or its major and minor version) to tailor the vendor specs to. + :param filter_exists: Whether to exclude vendor specs whose vendored content is not present. :return: An iterator over specs of all vendored code. """ python_major_minor = None # type: Optional[Tuple[int, int]] @@ -318,16 +322,16 @@ def iter_vendor_specs(filter_requires_python=None): # other Pythons and just use tomllib for Python 3.11+. if not python_major_minor or python_major_minor < (3, 7): vendored_toml = VendorSpec.pinned("toml", "0.10.2") - if vendored_toml.exists: + if not filter_exists or vendored_toml.exists: yield vendored_toml if not python_major_minor or (3, 7) <= python_major_minor < (3, 11): vendored_tomli = VendorSpec.pinned("tomli", "2.0.1") - if vendored_tomli.exists: + if not filter_exists or vendored_tomli.exists: yield vendored_tomli # We shell out to pip at build-time to resolve and install dependencies. if not python_major_minor or python_major_minor < (3, 12): - if PIP_SPEC.exists: + if not filter_exists or PIP_SPEC.exists: yield PIP_SPEC # We expose this to pip at build-time for legacy builds, but we also use pkg_resources via @@ -368,7 +372,7 @@ def iter_vendor_specs(filter_requires_python=None): ], ) if not python_major_minor or python_major_minor < (3, 12): - if vendored_setuptools.exists: + if not filter_exists or vendored_setuptools.exists: yield vendored_setuptools diff --git a/pex/vendor/__main__.py b/pex/vendor/__main__.py index d48e55a7d..a05ec82a0 100644 --- a/pex/vendor/__main__.py +++ b/pex/vendor/__main__.py @@ -577,7 +577,7 @@ def vendorize(root_dir, vendor_specs, prefix, update): os.umask(0o022) vendorize( root_dir=root_directory, - vendor_specs=list(iter_vendor_specs()), + vendor_specs=list(iter_vendor_specs(filter_exists=False)), prefix="pex.third_party", update=options.update, ) From 15a46a857da653a06cde7ce2f0eab00926da544d Mon Sep 17 00:00:00 2001 From: John Sirois Date: Tue, 30 Dec 2025 13:41:04 -0800 Subject: [PATCH 05/23] Fix some tests. --- pex/targets.py | 11 +++++-- testing/__init__.py | 33 ++++++++++++++++++-- tests/integration/cli/commands/test_lock.py | 5 ++- tests/integration/resolve/test_issue_2412.py | 8 ++--- tests/integration/test_integration.py | 6 ++-- tests/integration/test_issue_1031.py | 1 + tests/integration/test_issue_2432.py | 10 +++--- tests/integration/test_reproducible.py | 4 +-- tests/resolve/lockfile/test_lockfile.py | 3 +- 9 files changed, 57 insertions(+), 24 deletions(-) diff --git a/pex/targets.py b/pex/targets.py index 68824a709..f5bcce670 100644 --- a/pex/targets.py +++ b/pex/targets.py @@ -244,8 +244,15 @@ def __repr__(self): class LocalInterpreter(Target): @classmethod def create(cls, interpreter=None): - # type: (Optional[PythonInterpreter]) -> LocalInterpreter - python_interpreter = interpreter or PythonInterpreter.get() + # type: (Optional[Union[str, PythonInterpreter]]) -> LocalInterpreter + + if not interpreter: + python_interpreter = PythonInterpreter.get() + elif isinstance(interpreter, PythonInterpreter): + python_interpreter = interpreter + else: + python_interpreter = PythonInterpreter.from_binary(interpreter) + return cls( id=python_interpreter.binary.replace(os.sep, ".").lstrip("."), platform=python_interpreter.platform, diff --git a/testing/__init__.py b/testing/__init__.py index 3e63b283b..ae7dde4ef 100644 --- a/testing/__init__.py +++ b/testing/__init__.py @@ -407,6 +407,26 @@ class IntegResults(object): error = attr.ib() # type: Text return_code = attr.ib() # type: int + @property + def exe(self): + # type: () -> str + return self.cmd[0] + + @property + def interpreter(self): + # type: () -> PythonInterpreter + return PythonInterpreter.from_binary(self.exe) + + @property + def target(self): + # type: () -> LocalInterpreter + return LocalInterpreter.create(self.exe) + + @property + def pex(self): + # type: () -> PEX + return PEX(self.exe) + def assert_success( self, expected_output_re=None, # type: Optional[str] @@ -450,9 +470,15 @@ def create_pex_command( python=None, # type: Optional[str] quiet=None, # type: Optional[bool] pex_module="pex", # type: str + use_pex_whl_venv=True, # type: bool ): # type: (...) -> List[str] - cmd = [installed_pex_wheel_venv_python(python or sys.executable), "-m", pex_module] + python_exe = python or sys.executable + cmd = [ + installed_pex_wheel_venv_python(python_exe) if use_pex_whl_venv else python_exe, + "-m", + pex_module, + ] if pex_module == "pex" and not quiet: cmd.append("-v") if args: @@ -467,6 +493,7 @@ def run_pex_command( quiet=None, # type: Optional[bool] cwd=None, # type: Optional[str] pex_module="pex", # type: str + use_pex_whl_venv=True, # type: bool ): # type: (...) -> IntegResults """Simulate running pex command for integration testing. @@ -475,7 +502,9 @@ def run_pex_command( generated pex. This is useful for testing end to end runs with specific command line arguments or env options. """ - cmd = create_pex_command(args, python=python, quiet=quiet, pex_module=pex_module) + cmd = create_pex_command( + args, python=python, quiet=quiet, pex_module=pex_module, use_pex_whl_venv=use_pex_whl_venv + ) process = Executor.open_process( cmd=cmd, env=env, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE ) diff --git a/tests/integration/cli/commands/test_lock.py b/tests/integration/cli/commands/test_lock.py index 51ce7c2d6..32e9d94e0 100644 --- a/tests/integration/cli/commands/test_lock.py +++ b/tests/integration/cli/commands/test_lock.py @@ -15,7 +15,6 @@ from pex.cache.dirs import CacheDir from pex.common import safe_open from pex.dist_metadata import Constraint, Requirement -from pex.interpreter import PythonInterpreter from pex.interpreter_constraints import InterpreterConstraint from pex.pep_440 import Version from pex.pep_503 import ProjectName @@ -902,7 +901,7 @@ def test_update_targeted_impossible( ] == error_lines[:11] assert re.match( r"^1\.\) {platform}: pid [\d]+ -> ".format( - platform=LocalInterpreter.create(PythonInterpreter.from_binary(py310)).platform.tag + platform=LocalInterpreter.create(py310).platform.tag ), error_lines[11], ) @@ -1015,7 +1014,7 @@ def test_update_add_impossible( ] == error_lines[:12] assert re.match( r"^1\.\) {platform}: pid [\d]+ -> ".format( - platform=LocalInterpreter.create(PythonInterpreter.from_binary(py310)).platform.tag + platform=LocalInterpreter.create(py310).platform.tag ), error_lines[12], ) diff --git a/tests/integration/resolve/test_issue_2412.py b/tests/integration/resolve/test_issue_2412.py index b0abb9a32..d851954cd 100644 --- a/tests/integration/resolve/test_issue_2412.py +++ b/tests/integration/resolve/test_issue_2412.py @@ -13,7 +13,6 @@ from colors import color # vendor:skip from pex.common import open_zip, safe_open, touch -from pex.targets import LocalInterpreter from pex.typing import TYPE_CHECKING from pex.util import CacheHelper from testing import run_pex_command, subprocess @@ -271,8 +270,7 @@ def assert_pex( # If the project is updated in a way incompatible with the lock, building a # `pex --project ... --lock ...` should fail. write_setup(cowsay_requirement="cowsay==5.0") - target = LocalInterpreter.create() - run_pex_command( + result = run_pex_command( args=[ "--pex-root", pex_root, @@ -287,7 +285,9 @@ def assert_pex( "-o", pex3, ] - ).assert_failure( + ) + target = result.target + result.assert_failure( expected_error_re=r".*{message}$".format( message=re.escape( "Failed to resolve compatible artifacts from lock {lock} for 1 target:\n" diff --git a/tests/integration/test_integration.py b/tests/integration/test_integration.py index 5ce81e4f1..9835518ea 100644 --- a/tests/integration/test_integration.py +++ b/tests/integration/test_integration.py @@ -141,9 +141,7 @@ def test_pex_root_run( python39 = ensure_python_interpreter(PY39) python311 = ensure_python_interpreter(PY311) wheels = [ - pex_dist.select_best_wheel( - pex_wheels, LocalInterpreter.create(PythonInterpreter.from_binary(binary)) - ) + pex_dist.select_best_wheel(pex_wheels, LocalInterpreter.create(binary)) for binary in (python39, python311) ] runtime_pex_root = safe_mkdir(os.path.join(str(tmpdir), "runtime_pex_root")) @@ -1831,7 +1829,7 @@ def test_seed_verbose( assert pex_root == verbose_info.pop("pex_root") python = verbose_info.pop("python") - assert PythonInterpreter.from_binary(results.cmd[0]) == PythonInterpreter.from_binary(python) + assert results.interpreter == PythonInterpreter.from_binary(python) verbose_info.pop("pex") assert {} == verbose_info diff --git a/tests/integration/test_issue_1031.py b/tests/integration/test_issue_1031.py index 8f6ac100e..eb27f0f2a 100644 --- a/tests/integration/test_issue_1031.py +++ b/tests/integration/test_issue_1031.py @@ -88,6 +88,7 @@ def get_system_site_packages_pex_sys_path( args=args + ("--", "-c", print_sys_path_code), python=system_site_packages_venv_python, env=make_env(**env), + use_pex_whl_venv=False, ) result.assert_success() return OrderedSet(result.output.strip().splitlines()) diff --git a/tests/integration/test_issue_2432.py b/tests/integration/test_issue_2432.py index 94ba7f3fb..50a2946b1 100644 --- a/tests/integration/test_issue_2432.py +++ b/tests/integration/test_issue_2432.py @@ -5,7 +5,6 @@ from textwrap import dedent from pex.compatibility import safe_commonpath -from pex.targets import LocalInterpreter from pex.typing import TYPE_CHECKING from testing import run_pex_command from testing.cli import run_pex3 @@ -53,7 +52,10 @@ def assert_pex_from_lock( result = run_pex_command(args=list(extra_args) + args) if expected_error: result.assert_failure() - assert expected_error in result.error + assert ( + expected_error.format(target_description=result.target.render_description()) + in result.error + ) else: result.assert_success() assert pex_root == safe_commonpath((pex_root, result.output.strip())) @@ -71,7 +73,7 @@ def assert_pex_from_lock( extra_args=["--only-build", "ansicolors"], expected_error=dedent( """\ - Failed to resolve all requirements for {target_description} from {lock}: + Failed to resolve all requirements for {{target_description}} from {lock}: Configured with: build: True @@ -81,5 +83,5 @@ def assert_pex_from_lock( Dependency on ansicolors not satisfied, 1 incompatible candidate found: 1.) ansicolors 1.1.8 (via: ansicolors==1.1.8) does not have any compatible artifacts: """ - ).format(lock=lock, target_description=LocalInterpreter.create().render_description()), + ).format(lock=lock), ) diff --git a/tests/integration/test_reproducible.py b/tests/integration/test_reproducible.py index 2f56483e0..283c0750d 100644 --- a/tests/integration/test_reproducible.py +++ b/tests/integration/test_reproducible.py @@ -42,9 +42,7 @@ def compatible_pip_version(pythons): # type: (Iterable[str]) -> PipVersionValue for pip_version in sorted(PipVersion.values(), key=lambda v: v.version, reverse=True): if all( - pip_version.requires_python_applies( - LocalInterpreter.create(PythonInterpreter.from_binary(python)) - ) + pip_version.requires_python_applies(LocalInterpreter.create(python)) for python in pythons ): return pip_version diff --git a/tests/resolve/lockfile/test_lockfile.py b/tests/resolve/lockfile/test_lockfile.py index adefce3df..46a071cbf 100644 --- a/tests/resolve/lockfile/test_lockfile.py +++ b/tests/resolve/lockfile/test_lockfile.py @@ -7,7 +7,6 @@ import pytest -from pex.interpreter import PythonInterpreter from pex.resolve.lockfile import json_codec from pex.targets import LocalInterpreter, Target from pex.typing import TYPE_CHECKING @@ -19,7 +18,7 @@ def create_target(python): # type: (str) -> Target - return LocalInterpreter.create(PythonInterpreter.from_binary(python)) + return LocalInterpreter.create(python) @pytest.fixture From 1b5f97acac9fd37f7610037e2a85fd73b664d19c Mon Sep 17 00:00:00 2001 From: John Sirois Date: Tue, 30 Dec 2025 15:25:24 -0800 Subject: [PATCH 06/23] More test fixes. --- testing/__init__.py | 10 ++++-- .../cli/commands/test_interpreter_inspect.py | 32 +++++++++++-------- tests/integration/scie/test_pex_scie.py | 5 +-- tests/integration/test_issue_1560.py | 6 ++-- tests/integration/test_issue_2186.py | 13 ++++++-- 5 files changed, 40 insertions(+), 26 deletions(-) diff --git a/testing/__init__.py b/testing/__init__.py index ae7dde4ef..47703d847 100644 --- a/testing/__init__.py +++ b/testing/__init__.py @@ -25,7 +25,7 @@ from pex.interpreter import PythonInterpreter from pex.interpreter_implementation import InterpreterImplementation from pex.os import LINUX, MAC, WINDOWS -from pex.pep_427 import install_wheel_chroot, install_wheel_interpreter +from pex.pep_427 import install_wheel_chroot from pex.pex import PEX from pex.pex_builder import PEXBuilder from pex.pex_info import PexInfo @@ -950,6 +950,10 @@ def installed_pex_wheel_venv_python(python): pex_venv_dir = os.path.join(PEX_TEST_DEV_ROOT, "pex_venvs", "0", str(interpreter.identity)) with atomic_directory(pex_venv_dir) as atomic_dir: if not atomic_dir.is_finalized(): - venv = Virtualenv.create(atomic_dir.work_dir, interpreter=interpreter) - install_wheel_interpreter(pex_wheel, interpreter=venv.interpreter) + Virtualenv.create_atomic( + atomic_dir, + interpreter=interpreter, + install_pip=InstallationChoice.UPGRADED, + other_installs=[pex_wheel], + ) return Virtualenv(pex_venv_dir).interpreter.binary diff --git a/tests/integration/cli/commands/test_interpreter_inspect.py b/tests/integration/cli/commands/test_interpreter_inspect.py index ff853f6be..8ed22e5c5 100644 --- a/tests/integration/cli/commands/test_interpreter_inspect.py +++ b/tests/integration/cli/commands/test_interpreter_inspect.py @@ -13,7 +13,7 @@ from testing.cli import run_pex3 if TYPE_CHECKING: - from typing import Any, Dict, Text + from typing import Any, Dict, Optional def inspect( @@ -28,16 +28,17 @@ def assert_inspect( *args, # type: str **popen_kwargs # type: str ): - # type: (...) -> Text + # type: (...) -> IntegResults result = inspect(*args, **popen_kwargs) result.assert_success() - return result.output + return result -def test_inspect_default(current_interpreter): - # type: (PythonInterpreter) -> None - assert current_interpreter.binary == assert_inspect().strip() +def test_inspect_default(): + # type: () -> None + result = assert_inspect() + assert result.interpreter.binary == result.output.strip() def assert_default_verbose_data( @@ -46,18 +47,19 @@ def assert_default_verbose_data( ): # type: (...) -> Dict[str, Any] - return assert_verbose_data(PythonInterpreter.get(), *args, **popen_kwargs) + return assert_verbose_data(None, *args, **popen_kwargs) def assert_verbose_data( - interpreter, # type: PythonInterpreter + interpreter, # type: Optional[PythonInterpreter] *args, # type: str **popen_kwargs # type: str ): # type: (...) -> Dict[str, Any] - data = json.loads(assert_inspect(*args, **popen_kwargs)) - assert interpreter.binary == data.pop("path") + result = assert_inspect(*args, **popen_kwargs) + data = json.loads(result.output) + assert (interpreter or result.interpreter).binary == data.pop("path") return cast("Dict[str, Any]", data) @@ -111,7 +113,9 @@ def test_inspect_default_combined(): def test_inspect_all(): # type: () -> None - assert [pi.binary for pi in PythonInterpreter.all()] == assert_inspect("--all").splitlines() + assert [pi.binary for pi in PythonInterpreter.all()] == assert_inspect( + "--all" + ).output.splitlines() def test_inspect_interpreter_selection( @@ -123,18 +127,18 @@ def test_inspect_interpreter_selection( assert [py27.binary, py311.binary] == assert_inspect( "--python", py27.binary, "--python", py311.binary - ).splitlines() + ).output.splitlines() assert [py39.binary, py311.binary] == assert_inspect( "--all", "--python-path", os.pathsep.join([os.path.dirname(py39.binary), py311.binary]) - ).splitlines() + ).output.splitlines() assert [py39.binary] == assert_inspect( "--interpreter-constraint", "<3.11", "--python-path", os.pathsep.join([os.path.dirname(py39.binary), py311.binary]), - ).splitlines() + ).output.splitlines() def test_inspect_distributions(tmpdir): diff --git a/tests/integration/scie/test_pex_scie.py b/tests/integration/scie/test_pex_scie.py index ecd78b681..7cbb03ed9 100644 --- a/tests/integration/scie/test_pex_scie.py +++ b/tests/integration/scie/test_pex_scie.py @@ -24,7 +24,6 @@ from pex.pip.version import PipVersion from pex.scie import ScieStyle from pex.sysconfig import SysPlatform -from pex.targets import LocalInterpreter from pex.typing import TYPE_CHECKING from pex.version import __version__ from testing import IS_PYPY, PY_VER, make_env, run_pex_command, subprocess @@ -80,9 +79,7 @@ def test_basic( message=re.escape( "You selected `--scie {style}`, but none of the selected targets have " "compatible interpreters that can be embedded to form a scie:\n" - "{target}".format( - style=scie_style, target=LocalInterpreter.create().render_description() - ) + "{target}".format(style=scie_style, target=result.target.render_description()) ) ), re_flags=re.DOTALL | re.MULTILINE, diff --git a/tests/integration/test_issue_1560.py b/tests/integration/test_issue_1560.py index db865a745..d731c9bd3 100644 --- a/tests/integration/test_issue_1560.py +++ b/tests/integration/test_issue_1560.py @@ -58,7 +58,9 @@ def test_build_isolation( python, pip = venv_factory.create_venv() # N.B.: Pip 25.0 introduces a new message to check for here. - run_pex_command(args=[project_dir, "--no-build-isolation"], python=python).assert_failure( + run_pex_command( + args=[project_dir, "--no-build-isolation"], python=python, use_pex_whl_venv=False + ).assert_failure( expected_error_re=r".*(?:{old_message}|{new_message}).*".format( old_message=re.escape("ModuleNotFoundError: No module named 'flit_core'"), new_message=re.escape("BackendUnavailable: Cannot import 'flit_core.buildapi'"), @@ -70,7 +72,7 @@ def test_build_isolation( pex = os.path.join(str(tmpdir), "pex") run_pex_command( - args=[project_dir, "--no-build-isolation", "-o", pex], python=python + args=[project_dir, "--no-build-isolation", "-o", pex], python=python, use_pex_whl_venv=False ).assert_success() subprocess.check_call(args=[python, pex, "-c", "import foo"]) diff --git a/tests/integration/test_issue_2186.py b/tests/integration/test_issue_2186.py index 11d6d7156..d991f04f0 100644 --- a/tests/integration/test_issue_2186.py +++ b/tests/integration/test_issue_2186.py @@ -8,6 +8,7 @@ from pex import targets from pex.interpreter import PythonInterpreter from pex.pip.version import PipVersion, PipVersionValue +from pex.targets import Target from pex.typing import TYPE_CHECKING from testing import IntegResults, run_pex_command @@ -52,6 +53,7 @@ def incompatible_pip_version(): def expected_incompatible_pip_message( incompatible_pip_version, # type: PipVersionValue warning, # type: bool + target, # type: Target ): # type: (...) -> str header = ( @@ -69,7 +71,7 @@ def expected_incompatible_pip_message( header=header, pip_version=incompatible_pip_version, python_req=incompatible_pip_version.requires_python, - target=targets.current(), + target=target, ) ) @@ -80,7 +82,10 @@ def test_incompatible_resolve_warning(incompatible_pip_version): result = pex_execute_cowsay("--pip-version", str(incompatible_pip_version)) result.assert_success() assert ( - expected_incompatible_pip_message(incompatible_pip_version, warning=True) in result.error + expected_incompatible_pip_message( + incompatible_pip_version, warning=True, target=result.target + ) + in result.error ), result.error assert "Moo!" in result.output, result.output @@ -93,5 +98,7 @@ def test_incompatible_resolve_error(incompatible_pip_version): ) result.assert_failure() assert result.error.endswith( - expected_incompatible_pip_message(incompatible_pip_version, warning=False) + expected_incompatible_pip_message( + incompatible_pip_version, warning=False, target=result.target + ) ), result.error From 1ff2ff8df6420153e25dfbc180be95ac58491817 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Tue, 30 Dec 2025 16:03:18 -0800 Subject: [PATCH 07/23] More test fixes. Importantly, Pex without vendored Pip can now materialize vendored Pip from its VCS requirement. --- pex/pip/installation.py | 13 +++---------- pex/pip/tool.py | 4 ++-- pex/pip/version.py | 9 ++++++--- testing/__init__.py | 6 ++++-- tests/integration/test_issue_2885.py | 7 ++++--- 5 files changed, 19 insertions(+), 20 deletions(-) diff --git a/pex/pip/installation.py b/pex/pip/installation.py index af877adb2..4fe915255 100644 --- a/pex/pip/installation.py +++ b/pex/pip/installation.py @@ -278,14 +278,7 @@ def bootstrap_pip(): try: bootstrap_pip_version = PipVersion.for_value(pip_version.raw) except ValueError: - # Backstop with the version of Pip CPython 3.12.0 shipped with. This should be the - # oldest Pip we need in the Pip bootstrap process (which is only required for - # Python >= 3.12 which have distutils removed rendering vendored Pip unusable). - bootstrap_pip_version = PipVersion.v23_2 - for rev in sorted(PipVersion.values(), reverse=True): - if pip_version > rev.version: - bootstrap_pip_version = rev - break + bootstrap_pip_version = PipVersionValue(pip_version.raw) pip = BootstrapPip( pip_venv=PipVenv( @@ -388,7 +381,7 @@ def _resolved_installation( warn=False, ) ) - if bootstrap_pip_version is PipVersion.VENDORED: + if bootstrap_pip_version is PipVersion.VENDORED and PipVersion.VENDORED.available: def resolve_distribution_locations(): for resolved_distribution in resolver.resolve_requirements( @@ -555,7 +548,7 @@ def get_pip( pip = _PIP.get(installation) if pip is None: installation.check_python_applies() - if installation.version is PipVersion.VENDORED: + if installation.version is PipVersion.VENDORED and PipVersion.VENDORED.available: pip = _vendored_installation( interpreter=interpreter, resolver=resolver, diff --git a/pex/pip/tool.py b/pex/pip/tool.py index fdf960da8..3151fc55e 100644 --- a/pex/pip/tool.py +++ b/pex/pip/tool.py @@ -365,7 +365,7 @@ def _calculate_resolver_version_args( if ( resolver_version == ResolverVersion.PIP_2020 and interpreter.version[0] == 2 - and self.version.version < PipVersion.v22_3.version + and PipVersion.VENDORED <= self.version < PipVersion.v22_3 ): yield "--use-feature" yield "2020-resolver" @@ -398,7 +398,7 @@ def _spawn_pip_isolated( # We are not interactive. "--no-input", ] - if self.version < PipVersion.v25_0: + if PipVersion.VENDORED <= self.version < PipVersion.v25_0: # If we want to warn about a version of python we support, we should do it, not pip. # That said, the option does nothing in Pip 25.0 and is deprecated and slated for # removal. diff --git a/pex/pip/version.py b/pex/pip/version.py index 2f259b407..93eb8d663 100644 --- a/pex/pip/version.py +++ b/pex/pip/version.py @@ -47,7 +47,7 @@ def overridden(cls): @staticmethod def _to_requirement( project_name, # type: str - project_version, # type: Union[str, Version] + project_version=None, # type: Optional[Union[str, Version]] ): # type: (...) -> Requirement @@ -63,13 +63,14 @@ def _to_requirement( def __init__( self, version, # type: str - setuptools_version, # type: str - wheel_version, # type: str + setuptools_version=None, # type: Optional[str] + wheel_version=None, # type: Optional[str] requires_python=None, # type: Optional[str] name=None, # type: Optional[str] requirement=None, # type: Optional[str] setuptools_requirement=None, # type: Optional[str] hidden=False, # type: bool + available=True, # type: bool ): # type: (...) -> None super(PipVersionValue, self).__init__(name or version, enum_type=PipVersionValue) @@ -87,6 +88,7 @@ def __init__( self.wheel_version = wheel_version self.wheel_requirement = self._to_requirement("wheel", wheel_version) self.hidden = hidden + self.available = available def cache_dir_name(self): # type: () -> str @@ -283,6 +285,7 @@ def latest_compatible(cls, target=None): setuptools_requirement="setuptools", wheel_version="0.37.1", requires_python="<3.12", + available=vendor.PIP_SPEC.exists, ) v22_2_2 = PipVersionValue( diff --git a/testing/__init__.py b/testing/__init__.py index 47703d847..00df02e32 100644 --- a/testing/__init__.py +++ b/testing/__init__.py @@ -35,7 +35,7 @@ from pex.sysconfig import SCRIPT_DIR, script_name from pex.targets import LocalInterpreter from pex.typing import TYPE_CHECKING, cast -from pex.util import named_temporary_file +from pex.util import CacheHelper, named_temporary_file from pex.venv.virtualenv import InstallationChoice, Virtualenv # Explicitly re-export subprocess to enable a transparent substitution in tests that supports @@ -947,7 +947,9 @@ def installed_pex_wheel_venv_python(python): interpreter = PythonInterpreter.from_binary(python) pex_wheel = wheel(LocalInterpreter.create(interpreter=interpreter)) - pex_venv_dir = os.path.join(PEX_TEST_DEV_ROOT, "pex_venvs", "0", str(interpreter.identity)) + pex_venv_dir = os.path.join( + PEX_TEST_DEV_ROOT, "pex_venvs", "0", str(interpreter.identity), CacheHelper.hash(pex_wheel) + ) with atomic_directory(pex_venv_dir) as atomic_dir: if not atomic_dir.is_finalized(): Virtualenv.create_atomic( diff --git a/tests/integration/test_issue_2885.py b/tests/integration/test_issue_2885.py index edb4d00f7..5a9c13f7f 100644 --- a/tests/integration/test_issue_2885.py +++ b/tests/integration/test_issue_2885.py @@ -12,7 +12,7 @@ import pytest -from pex import targets, toml +from pex import toml from pex.common import safe_mkdir, safe_open from pex.dist_metadata import DistMetadata, Requirement from pex.orderedset import OrderedSet @@ -302,7 +302,8 @@ def test_dependencies_nominal_lock_not_spec_compliant_ambiguous_install( # https://packaging.python.org/en/latest/specifications/pylock-toml/#installation attr.evolve(wheel_b2, marker=B1_DEP_MARKER), ) - pex_from_lock(lock.source).assert_failure( + result = pex_from_lock(lock.source) + result.assert_failure( expected_error_re=re.escape( "Failed to resolve compatible artifacts from lock {lock_file} created by {creator} for " "1 target:\n" @@ -311,7 +312,7 @@ def test_dependencies_nominal_lock_not_spec_compliant_ambiguous_install( " b 1 wheel\n" " b 2 wheel\n" "Pex resolves must produce a unique package per project.".format( - lock_file=lock.source, creator=lock.created_by, target=targets.current() + lock_file=lock.source, creator=lock.created_by, target=result.target ) ) ) From 3af259ee69031b6b6bc6225b8178df3a49787aed Mon Sep 17 00:00:00 2001 From: John Sirois Date: Tue, 30 Dec 2025 20:06:50 -0800 Subject: [PATCH 08/23] Fixup pex wheel venv to not include Pip. --- testing/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/testing/__init__.py b/testing/__init__.py index 00df02e32..fdc0de03f 100644 --- a/testing/__init__.py +++ b/testing/__init__.py @@ -25,7 +25,7 @@ from pex.interpreter import PythonInterpreter from pex.interpreter_implementation import InterpreterImplementation from pex.os import LINUX, MAC, WINDOWS -from pex.pep_427 import install_wheel_chroot +from pex.pep_427 import install_wheel_chroot, install_wheel_interpreter from pex.pex import PEX from pex.pex_builder import PEXBuilder from pex.pex_info import PexInfo @@ -952,10 +952,11 @@ def installed_pex_wheel_venv_python(python): ) with atomic_directory(pex_venv_dir) as atomic_dir: if not atomic_dir.is_finalized(): - Virtualenv.create_atomic( - atomic_dir, - interpreter=interpreter, - install_pip=InstallationChoice.UPGRADED, - other_installs=[pex_wheel], - ) + venv = Virtualenv.create_atomic(atomic_dir, interpreter=interpreter) + install_wheel_interpreter(pex_wheel, venv.interpreter) + for _ in venv.rewrite_scripts( + python=venv.interpreter.binary.replace(atomic_dir.work_dir, atomic_dir.target_dir) + ): + # Just ensure the re-writing iterator is driven to completion. + pass return Virtualenv(pex_venv_dir).interpreter.binary From 4905125ba5cee55690736e92eddb12f1bea92c8f Mon Sep 17 00:00:00 2001 From: John Sirois Date: Tue, 30 Dec 2025 21:11:25 -0800 Subject: [PATCH 09/23] Fix more tests. --- tests/integration/cli/commands/test_cache_prune.py | 1 + tests/integration/scie/test_pex_scie.py | 4 +++- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/integration/cli/commands/test_cache_prune.py b/tests/integration/cli/commands/test_cache_prune.py index 69ade5d9a..b75f5aadf 100644 --- a/tests/integration/cli/commands/test_cache_prune.py +++ b/tests/integration/cli/commands/test_cache_prune.py @@ -180,6 +180,7 @@ def create_ansicolors_pex( run_pex_command( args=["ansicolors==1.1.8", "-D", "src", "-m" "app", "-o", pex] + list(extra_args), cwd=tmpdir.path, + use_pex_whl_venv=False, ).assert_success() return AnsicolorsPex(pex) diff --git a/tests/integration/scie/test_pex_scie.py b/tests/integration/scie/test_pex_scie.py index 7cbb03ed9..f4b692881 100644 --- a/tests/integration/scie/test_pex_scie.py +++ b/tests/integration/scie/test_pex_scie.py @@ -1342,7 +1342,9 @@ def test_free_threaded_scie_auto_detected(tmpdir): scie = tmpdir.join("scie") run_pex_command( - args=["--scie", "eager", "psutil", "--scie-only", "-o", scie], python=free_threaded_python + args=["--scie", "eager", "psutil", "--scie-only", "-o", scie], + python=free_threaded_python, + use_pex_whl_venv=False, ).assert_success() assert ( From 708fa5b2b828e0d161ee241ff48696029bf76fde Mon Sep 17 00:00:00 2001 From: John Sirois Date: Wed, 31 Dec 2025 10:27:58 -0800 Subject: [PATCH 10/23] Fix `uv` lock timeout and fix more tests. --- testing/pex_dist.py | 9 ++++++--- tests/integration/cli/commands/test_pep_751.py | 4 ++-- tests/integration/test_issue_1179.py | 3 +-- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/testing/pex_dist.py b/testing/pex_dist.py index d5fec6aad..5b74328b1 100644 --- a/testing/pex_dist.py +++ b/testing/pex_dist.py @@ -15,6 +15,7 @@ from pex.targets import LocalInterpreter, Target, WheelEvaluation from pex.typing import TYPE_CHECKING, cast from pex.util import CacheHelper +from pex.venv.virtualenv import Virtualenv from pex.version import __version__ from testing import PEX_TEST_DEV_ROOT, pex_project_dir @@ -56,11 +57,13 @@ def dir_hash(rel_path): pex_wheel_dir = os.path.join(PEX_TEST_DEV_ROOT, "pex_wheels", "0", pex_wheel_inputs_fingerprint) with atomic_directory(pex_wheel_dir) as atomic_dir: if not atomic_dir.is_finalized(): + # The package command can be slow to run which locks up uv; so we just ensure a synced + # uv venv (fast), then run the dev-cmd console script directly to avoid uv lock + # timeouts in CI. + subprocess.check_call(args=["uv", "sync"]) subprocess.check_call( args=[ - "uv", - "run", - "dev-cmd", + Virtualenv(venv_dir=".venv").bin_path("dev-cmd"), "package", "--", "--no-pex", diff --git a/tests/integration/cli/commands/test_pep_751.py b/tests/integration/cli/commands/test_pep_751.py index 4fc1a53d5..ba05c4d97 100644 --- a/tests/integration/cli/commands/test_pep_751.py +++ b/tests/integration/cli/commands/test_pep_751.py @@ -16,7 +16,7 @@ import pytest import testing -from pex import targets, toml +from pex import toml from pex.atomic_directory import atomic_directory from pex.common import CopyMode, iter_copytree, safe_copy from pex.compatibility import string @@ -726,7 +726,7 @@ def assert_pdm_less_than_39_failure( "support the current target.\n" "The supported environments are:\n" '+ python_version >= "3.9"\n'.format( - pylock=pdm_exported_pylock_toml, target=targets.current() + pylock=pdm_exported_pylock_toml, target=result.target ) ) ) diff --git a/tests/integration/test_issue_1179.py b/tests/integration/test_issue_1179.py index 20733b031..0ba15a30e 100644 --- a/tests/integration/test_issue_1179.py +++ b/tests/integration/test_issue_1179.py @@ -7,7 +7,6 @@ import pytest from pex.pip.version import PipVersion -from pex.targets import LocalInterpreter from testing import run_pex_command @@ -39,7 +38,7 @@ def test_pip_2020_resolver_engaged(): boto3 1.15.6 requires botocore<1.19.0,>=1.18.6 but 1 incompatible dist was resolved: botocore-1.19.63-py2.py3-none-any.whl """.format( - target=LocalInterpreter.create().render_description() + target=results.target.render_description() ) ) in results.error From 02360408161b4d557bedf91390f88bf03d45b7f1 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Wed, 31 Dec 2025 11:45:22 -0800 Subject: [PATCH 11/23] Fix more tests. --- testing/cli.py | 7 ++++++- tests/integration/cli/commands/test_cache_prune.py | 13 ++++++++++--- tests/integration/test_issue_2088.py | 3 ++- tests/integration/test_sh_boot.py | 3 ++- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/testing/cli.py b/testing/cli.py index e10b4a34e..69c73e4ad 100644 --- a/testing/cli.py +++ b/testing/cli.py @@ -19,7 +19,12 @@ def run_pex3( ): # type: (...) -> IntegResults - python = installed_pex_wheel_venv_python(kwargs.pop("python", None) or sys.executable) + python_exe = kwargs.pop("python", sys.executable) + python = ( + installed_pex_wheel_venv_python(python_exe) + if kwargs.pop("use_pex_whl_venv", True) + else python_exe + ) cmd = [python, "-mpex.cli"] + list(args) process = subprocess.Popen(args=cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, **kwargs) stdout, stderr = process.communicate() diff --git a/tests/integration/cli/commands/test_cache_prune.py b/tests/integration/cli/commands/test_cache_prune.py index b75f5aadf..7b758d7e2 100644 --- a/tests/integration/cli/commands/test_cache_prune.py +++ b/tests/integration/cli/commands/test_cache_prune.py @@ -335,8 +335,11 @@ def test_zipapp_prune_shared_code( write_app_py(tmpdir.join("app.py")) no_colors_pex = tmpdir.join("no-colors.pex") + use_pex_whl_venv = sys.version_info[0] == 3 run_pex_command( - args=["-M" "app", "-m", "app", "-o", no_colors_pex], cwd=tmpdir.path + args=["-M" "app", "-m", "app", "-o", no_colors_pex], + cwd=tmpdir.path, + use_pex_whl_venv=use_pex_whl_venv, ).assert_success() assert b"Hello Cache!\n" == subprocess.check_output(args=[no_colors_pex]) assert all_user_code == list( @@ -344,12 +347,16 @@ def test_zipapp_prune_shared_code( ), "Expected the shared code cache to be re-used since the code is the same for both PEXes." set_last_access_one_day_ago(ansicolors_zipapp_pex.path) - run_pex3("cache", "prune", "--older-than", "1 hour").assert_success() + run_pex3( + "cache", "prune", "--older-than", "1 hour", use_pex_whl_venv=use_pex_whl_venv + ).assert_success() assert all_user_code == list( UserCodeDir.iter_all() ), "Expected the shared code cache to be un-pruned since no_colors_pex still needs it." - run_pex3("cache", "prune", "--older-than", "0 seconds").assert_success() + run_pex3( + "cache", "prune", "--older-than", "0 seconds", use_pex_whl_venv=use_pex_whl_venv + ).assert_success() assert len(list(UserCodeDir.iter_all())) == 0, ( "Expected the shared code cache to be pruned since the last remaining user, no_colors_pex," "is now pruned." diff --git a/tests/integration/test_issue_2088.py b/tests/integration/test_issue_2088.py index c41554872..6a947d4e5 100644 --- a/tests/integration/test_issue_2088.py +++ b/tests/integration/test_issue_2088.py @@ -53,7 +53,8 @@ def test_venv_symlink_site_packages( "--seed", "-o", pex, - ] + ], + use_pex_whl_venv=sys.version_info[0] == 3, ) result.assert_success() venv_pex_path = str(result.output.strip()) diff --git a/tests/integration/test_sh_boot.py b/tests/integration/test_sh_boot.py index e3cad344a..6b2d8b04e 100644 --- a/tests/integration/test_sh_boot.py +++ b/tests/integration/test_sh_boot.py @@ -80,7 +80,8 @@ def test_issue_1881( "--runtime-pex-root", pex_root, ] - + execution_mode_args + + execution_mode_args, + use_pex_whl_venv=sys.version_info[0] == 3, ).assert_success() # simulate pex_root writable at runtime. os.chmod(pex_root, 0o777) From bf07cbc963f18f840a2ebb801c441f5952518fd0 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Wed, 31 Dec 2025 12:17:06 -0800 Subject: [PATCH 12/23] Run `uv` under `--frozen` where possible. Also apply package command CI timeout fix in a test that packages the Pex PEX scie. --- pyproject.toml | 3 +++ testing/devpi.py | 1 + testing/pex_dist.py | 4 ++-- tests/integration/cli/commands/test_run.py | 8 +++++--- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 7b8361a44..63f09094c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -14,6 +14,7 @@ plugins = ["pex_build"] command = [ "uv", "export", + "--frozen", "--format", "pylock.toml", "--no-header", @@ -414,6 +415,7 @@ extra-cache-keys = ["uv.lock"] 3rdparty-export-command = [ "uv", "export", "-q", + "--frozen", "--no-emit-project", # Both uv and Pip can't handle VCS URLs in hashed requirements; so we handle this ourselves. "--no-emit-package", "attrs", @@ -441,6 +443,7 @@ when = "platform_machine == 'armv7l' or platform_machine == 'riscv64'" 3rdparty-export-command = [ "uv", "export", "-q", + "--frozen", "--no-emit-project", # We get some wheels from https://www.piwheels.org/simple and # https://gitlab.com/api/v4/projects/riseproject%2Fpython%2Fwheel_builder/packages/pypi/simple diff --git a/testing/devpi.py b/testing/devpi.py index 01bfd53f8..38552d777 100644 --- a/testing/devpi.py +++ b/testing/devpi.py @@ -140,6 +140,7 @@ def ensure_devpi_server(): args=[ "uv", "export", + "--frozen", "-q", "--only-group", "devpi-server", diff --git a/testing/pex_dist.py b/testing/pex_dist.py index 5b74328b1..7bd12db37 100644 --- a/testing/pex_dist.py +++ b/testing/pex_dist.py @@ -60,10 +60,10 @@ def dir_hash(rel_path): # The package command can be slow to run which locks up uv; so we just ensure a synced # uv venv (fast), then run the dev-cmd console script directly to avoid uv lock # timeouts in CI. - subprocess.check_call(args=["uv", "sync"]) + subprocess.check_call(args=["uv", "sync", "--frozen"]) subprocess.check_call( args=[ - Virtualenv(venv_dir=".venv").bin_path("dev-cmd"), + Virtualenv(".venv").bin_path("dev-cmd"), "package", "--", "--no-pex", diff --git a/tests/integration/cli/commands/test_run.py b/tests/integration/cli/commands/test_run.py index e93e108ba..07b9377da 100644 --- a/tests/integration/cli/commands/test_run.py +++ b/tests/integration/cli/commands/test_run.py @@ -545,11 +545,13 @@ def test_pexec(tmpdir): # type: (Tempdir) -> None dist_dir = tmpdir.join("dist") + # The package command can be slow to run which locks up uv; so we just ensure a synced + # uv venv (fast), then run the dev-cmd console script directly to avoid uv lock + # timeouts in CI. + subprocess.check_call(args=["uv", "sync", "--frozen"]) subprocess.check_call( args=[ - "uv", - "run", - "dev-cmd", + Virtualenv(".venv").bin_path("dev-cmd"), "package", "--", "--dist-dir", From c81887aae150468858a79327d9ca7b785edf0de3 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Wed, 31 Dec 2025 21:22:13 -0800 Subject: [PATCH 13/23] Fix `run_pex3` utility. --- testing/cli.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/testing/cli.py b/testing/cli.py index 69c73e4ad..8502ca568 100644 --- a/testing/cli.py +++ b/testing/cli.py @@ -19,7 +19,7 @@ def run_pex3( ): # type: (...) -> IntegResults - python_exe = kwargs.pop("python", sys.executable) + python_exe = kwargs.pop("python", None) or sys.executable python = ( installed_pex_wheel_venv_python(python_exe) if kwargs.pop("use_pex_whl_venv", True) From 76984106bb6e095d36252f2b39a3bfdb47e3aee5 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 08:30:41 -0800 Subject: [PATCH 14/23] Fix another test. --- tests/integration/test_issue_1232.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/integration/test_issue_1232.py b/tests/integration/test_issue_1232.py index 1557fcb49..da14786b7 100644 --- a/tests/integration/test_issue_1232.py +++ b/tests/integration/test_issue_1232.py @@ -100,6 +100,7 @@ def vendored_toplevel(isolated_dir): "pyproject.toml", "setup.cfg", "setup.py", + "uv.lock", ): shutil.copy(build_file, os.path.join(modified_pex_src, build_file)) From 5adce27051577a65d9e64131f62ba02e47d74f95 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 10:43:12 -0800 Subject: [PATCH 15/23] Fix some stray bare `get_pip`s in tests. --- tests/integration/test_issue_2299.py | 5 +++-- tests/integration/test_issue_2998.py | 3 ++- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/integration/test_issue_2299.py b/tests/integration/test_issue_2299.py index 7e64df6c5..e1ebe596d 100644 --- a/tests/integration/test_issue_2299.py +++ b/tests/integration/test_issue_2299.py @@ -15,6 +15,7 @@ from pex.pep_440 import Version from pex.pep_503 import ProjectName from pex.pip.installation import get_pip +from pex.resolve.configured_resolver import ConfiguredResolver from pex.resolve.resolver_configuration import BuildConfiguration from pex.util import CacheHelper from pex.wheel import Wheel @@ -48,7 +49,7 @@ def test_repository_extract_wheels_with_data( ) wheels_dir = tmpdir.join("wheels") - get_pip().spawn_download_distributions( + get_pip(resolver=ConfiguredResolver.default()).spawn_download_distributions( download_dir=wheels_dir, requirements=[greenlet_requirement], build_configuration=BuildConfiguration.create(allow_builds=False), @@ -97,7 +98,7 @@ def test_venv_repository_resolve_whls(tmpdir): # type: (Tempdir) -> None download_dir = tmpdir.join("wheels") - get_pip().spawn_download_distributions( + get_pip(resolver=ConfiguredResolver.default()).spawn_download_distributions( download_dir=download_dir, requirements=["ansicolors==1.1.8"] ).wait() pypi_wheels = glob.glob(os.path.join(download_dir, "*.whl")) diff --git a/tests/integration/test_issue_2998.py b/tests/integration/test_issue_2998.py index f6947169b..3a6e6e0ab 100644 --- a/tests/integration/test_issue_2998.py +++ b/tests/integration/test_issue_2998.py @@ -11,6 +11,7 @@ from pex.common import open_zip, safe_rmtree from pex.compatibility import commonpath from pex.pip.installation import get_pip +from pex.resolve.configured_resolver import ConfiguredResolver from pex.resolve.resolver_configuration import BuildConfiguration from pex.util import CacheHelper from pex.venv.virtualenv import InstallationChoice, Virtualenv @@ -44,7 +45,7 @@ def test_record_directory_entries_whl_round_trip(tmpdir): pex_root = tmpdir.join("pex-root") download_dir = tmpdir.join("downloads") - get_pip().spawn_download_distributions( + get_pip(resolver=ConfiguredResolver.default()).spawn_download_distributions( download_dir=download_dir, requirements=["cmake==3.26.3"], build_configuration=BuildConfiguration.create(allow_builds=False), From 5328f7026c7cf7cb56fd6454344b7dfaec919aa1 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 10:44:22 -0800 Subject: [PATCH 16/23] Fix requires_python to always be calculated. This need not rely on a ranked tag. --- pex/targets.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/pex/targets.py b/pex/targets.py index f5bcce670..c3d6296b3 100644 --- a/pex/targets.py +++ b/pex/targets.py @@ -203,16 +203,13 @@ def wheel_applies(self, wheel): wheel_tags = CompatibilityTags.from_wheel(wheel) ranked_tag = self.supported_tags.best_match(wheel_tags) - - requires_python = None # type: Optional[SpecifierSet] - if ranked_tag: - requires_python = ( - wheel.metadata.requires_python - if isinstance(wheel, Distribution) - else dist_requires_python(wheel) - ) - + requires_python = ( + wheel.metadata.requires_python + if isinstance(wheel, Distribution) + else dist_requires_python(wheel) + ) wheel_location = wheel.location if isinstance(wheel, Distribution) else wheel + return WheelEvaluation( wheel=wheel_location, tags=tuple(wheel_tags), From c9390cccf281e81c8b03800b20793c4aedfda83e Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 11:22:23 -0800 Subject: [PATCH 17/23] Fix installing `.whl`s from Pex symlink venvs. An incidental bug noticed stress testing Pex split `.whl`s. --- pex/pep_427.py | 2 +- tests/integration/test_pep_427.py | 68 ++++++++++++++++++++++++++++--- 2 files changed, 64 insertions(+), 6 deletions(-) diff --git a/pex/pep_427.py b/pex/pep_427.py index e5cd9578c..13bfe6312 100644 --- a/pex/pep_427.py +++ b/pex/pep_427.py @@ -974,7 +974,7 @@ def is_entry_point_script(script_path): if not compile and installed_file.path.endswith(".pyc"): continue - src_file = os.path.realpath(os.path.join(wheel.location, installed_file.path)) + src_file = os.path.join(wheel.location, installed_file.path) if not os.path.exists(src_file): if not warned_bad_record: pex_warnings.warn( diff --git a/tests/integration/test_pep_427.py b/tests/integration/test_pep_427.py index ad5be5173..15d498717 100644 --- a/tests/integration/test_pep_427.py +++ b/tests/integration/test_pep_427.py @@ -1,22 +1,26 @@ # Copyright 2023 Pex project contributors. # Licensed under the Apache License, Version 2.0 (see LICENSE). - +import json import os.path import re from glob import glob from textwrap import dedent -from pex.common import safe_open +import pytest + +from pex.common import CopyMode, safe_open from pex.os import is_exe -from pex.pep_427 import install_wheel_interpreter +from pex.pep_427 import InstallableWheel, InstallPaths, ZipMetadata, install_wheel_interpreter from pex.pip.installation import get_pip from pex.resolve.configured_resolver import ConfiguredResolver from pex.typing import TYPE_CHECKING from pex.venv.virtualenv import InstallationChoice, Virtualenv -from testing import WheelBuilder, make_env, subprocess +from pex.wheel import Wheel +from testing import WheelBuilder, make_env, run_pex_command, subprocess +from testing.pytest_utils.tmp import Tempdir if TYPE_CHECKING: - from typing import Any + from typing import Any, List def test_install_wheel_interpreter(tmpdir): @@ -114,3 +118,57 @@ def do_some_preprocessing(): assert re.match( br"^Done some preprocessing\nNow starting an interactive session\n>>>>?$", output.strip() ), output + + +@pytest.mark.parametrize( + "pex_venv_args", + [pytest.param([], id="symlinks"), pytest.param(["--venv-site-packages-copies"], id="copies")], +) +@pytest.mark.parametrize( + "copy_mode", [pytest.param(copy_mode, id=str(copy_mode)) for copy_mode in CopyMode.values()] +) +def test_install_pex_venv_distributions( + tmpdir, # type: Tempdir + pex_venv_args, # type: List[str] + copy_mode, # type: CopyMode.Value +): + # type: (...) -> None + pex_root = tmpdir.join("pex-root") + pex = tmpdir.join("cowsay.pex") + result = run_pex_command( + args=[ + "--pex-root", + pex_root, + "--runtime-pex-root", + pex_root, + "cowsay<6", + "-c", + "cowsay", + "-o", + pex, + "--seed", + "verbose", + "--venv", + ] + + pex_venv_args + ) + result.assert_success() + src_vnv = Virtualenv(venv_dir=os.path.dirname(json.loads(result.output)["pex"])) + + dst_venv = Virtualenv.create(tmpdir.join("venv")) + for dist in src_vnv.iter_distributions(): + wheel = Wheel.load(dist.location, project_name=dist.metadata.project_name) + installable_wheel = InstallableWheel( + wheel=wheel, + install_paths=InstallPaths.interpreter( + interpreter=src_vnv.interpreter, + project_name=wheel.project_name, + root_is_purelib=wheel.root_is_purelib, + ), + zip_metadata=ZipMetadata.read(wheel), + ) + install_wheel_interpreter( + wheel=installable_wheel, interpreter=dst_venv.interpreter, copy_mode=copy_mode + ) + + assert b"| Moo! |" in subprocess.check_output(args=[pex, "Moo!"]) From b65ac04b21ee88ca6b340552d5eecdfda04ce08f Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 12:09:53 -0800 Subject: [PATCH 18/23] Add an explicit test for vendored Pip bootstrap. The one potentially tricky case in the split `.whl` regime is bootstrapping what would normally be vendored Pip to satisfy older `--python`s and / or explicitly requested `--pip-version vendored`. --- tests/integration/resolve/test_issue_2785.py | 57 ++++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/integration/resolve/test_issue_2785.py diff --git a/tests/integration/resolve/test_issue_2785.py b/tests/integration/resolve/test_issue_2785.py new file mode 100644 index 000000000..5c40da2c9 --- /dev/null +++ b/tests/integration/resolve/test_issue_2785.py @@ -0,0 +1,57 @@ +# Copyright 2026 Pex project contributors. +# Licensed under the Apache License, Version 2.0 (see LICENSE). + +from __future__ import absolute_import + +import subprocess +import sys + +import pytest + +from pex.common import safe_mkdir +from testing import PY27, PY311, ensure_python_interpreter, run_pex_command +from testing.pytest_utils.tmp import Tempdir + + +@pytest.mark.skipif( + sys.version_info[:2] < (3, 12), + reason="Vendored Pip bootstrapping only occurs when using Pex installed via a Python 3.12+ whl.", +) +@pytest.mark.parametrize( + "old_python", + [ + pytest.param(ensure_python_interpreter(version), id="py" + version) + for version in (PY27, PY311) + ], +) +def test_bootstrap_vendored_pip( + tmpdir, # type: Tempdir + old_python, # type: str +): + # type: (...) -> None + + pex_root = tmpdir.join("pex-root") + pex = tmpdir.join("cowsay.pex") + run_pex_command( + args=[ + "--pex-root", + pex_root, + "--runtime-pex-root", + pex_root, + "--python", + old_python, + "--pip-version", + "vendored", + "--no-allow-pip-version-fallback", + "cowsay<6", + "-c", + "cowsay", + "-o", + pex, + ], + use_pex_whl_venv=True, + # N.B.: This ensures we don't pick up vendored Pip from the default CWD of the Pex repo + # root. + cwd=safe_mkdir(tmpdir.join("empty-pythonpath")), + ).assert_success() + assert b"| Moo! |" in subprocess.check_output(args=[pex, "Moo!"]) From f0bda060ccd835076cce214e4e4f104e4e6e720b Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 12:36:14 -0800 Subject: [PATCH 19/23] I <3 macOS. --- pex/pep_427.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/pex/pep_427.py b/pex/pep_427.py index 13bfe6312..4feedf6a1 100644 --- a/pex/pep_427.py +++ b/pex/pep_427.py @@ -975,7 +975,8 @@ def is_entry_point_script(script_path): continue src_file = os.path.join(wheel.location, installed_file.path) - if not os.path.exists(src_file): + src_file_realpath = os.path.realpath(src_file) + if not os.path.exists(src_file_realpath): if not warned_bad_record: pex_warnings.warn( "The wheel {whl} has a bad RECORD. Skipping install of non-existent file " @@ -989,17 +990,24 @@ def is_entry_point_script(script_path): dst_components = None # type: Optional[Tuple[Text, Text, bool]] for path_name, installed_path in wheel.iter_install_paths_by_name(): installed_path = os.path.realpath(installed_path) - if installed_path == commonpath((installed_path, src_file)): + + src_path = None # type: Optional[Text] + if installed_path == commonpath((installed_path, src_file_realpath)): + src_path = src_file_realpath + elif installed_path == commonpath((installed_path, src_file)): + src_path = src_file + + if src_path: rewrite_script = False if "scripts" == path_name: - if is_entry_point_script(src_file): + if is_entry_point_script(src_path): # This entry point script will be installed afresh below as needed. break rewrite_script = interpreter is not None and is_python_script( - src_file, check_executable=False + src_path, check_executable=False ) - dst_rel_path = os.path.relpath(src_file, installed_path) + dst_rel_path = os.path.relpath(src_path, installed_path) dst_components = path_name, dst_rel_path, rewrite_script break else: From 0cc283be83391f6b406606b4acffc0a61628a4a0 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 13:49:17 -0800 Subject: [PATCH 20/23] Fixup `install_wheel` for non-`realpath` case. --- pex/pep_427.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pex/pep_427.py b/pex/pep_427.py index 4feedf6a1..7554c1997 100644 --- a/pex/pep_427.py +++ b/pex/pep_427.py @@ -974,7 +974,7 @@ def is_entry_point_script(script_path): if not compile and installed_file.path.endswith(".pyc"): continue - src_file = os.path.join(wheel.location, installed_file.path) + src_file = os.path.normpath(os.path.join(wheel.location, installed_file.path)) src_file_realpath = os.path.realpath(src_file) if not os.path.exists(src_file_realpath): if not warned_bad_record: From 2d739e23503acdee6ef560e28e8d6cfc687c2c19 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 15:58:33 -0800 Subject: [PATCH 21/23] DRY --- build-backend/pex_build/__init__.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/build-backend/pex_build/__init__.py b/build-backend/pex_build/__init__.py index 6ed837c48..001d2ce34 100644 --- a/build-backend/pex_build/__init__.py +++ b/build-backend/pex_build/__init__.py @@ -19,8 +19,20 @@ if TYPE_CHECKING: from typing import Callable, Iterator, Optional -INCLUDE_DOCS = os.environ.get("__PEX_BUILD_INCLUDE_DOCS__", "False").lower() in ("1", "true") -WHEEL_3_12_PLUS = os.environ.get("__PEX_BUILD_WHL_3_12_PLUS__", "False").lower() in ("1", "true") + +def read_bool_env( + name, # type: str + default, # type: bool +): + # type: (...) -> bool + value = os.environ.get(name) + if value is None: + return default + return value.lower() in ("1", "true") + + +INCLUDE_DOCS = read_bool_env("__PEX_BUILD_INCLUDE_DOCS__", default=False) +WHEEL_3_12_PLUS = read_bool_env("__PEX_BUILD_WHL_3_12_PLUS__", default=False) @contextmanager From 80b520916b69f863b7e7a13561efa370065accb4 Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 15:58:52 -0800 Subject: [PATCH 22/23] Fix doc. --- pex/third_party/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pex/third_party/__init__.py b/pex/third_party/__init__.py index e04ce3c3b..97ff4753d 100644 --- a/pex/third_party/__init__.py +++ b/pex/third_party/__init__.py @@ -257,7 +257,8 @@ def install_vendored( path to the pex code, which serves as the root under which code is vendored at ``pex/vendor/_vendored``. :param expose: Names of distributions to expose for direct, un-prefixed import. - :param expose: Names of distributions to expose for direct, un-prefixed import only if available. + :param expose_if_available: Names of distributions to expose, if available, for direct, + un-prefixed import. :raise: :class:`ValueError` if any distributions to expose cannot be found. """ root = cls._abs_root(root) From b527b57f8336001f41a7e2aeb2305ecadfe1863c Mon Sep 17 00:00:00 2001 From: John Sirois Date: Thu, 1 Jan 2026 15:59:02 -0800 Subject: [PATCH 23/23] Fix test. --- tests/integration/resolve/test_issue_2785.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/integration/resolve/test_issue_2785.py b/tests/integration/resolve/test_issue_2785.py index 5c40da2c9..09ffaf1b1 100644 --- a/tests/integration/resolve/test_issue_2785.py +++ b/tests/integration/resolve/test_issue_2785.py @@ -54,4 +54,4 @@ def test_bootstrap_vendored_pip( # root. cwd=safe_mkdir(tmpdir.join("empty-pythonpath")), ).assert_success() - assert b"| Moo! |" in subprocess.check_output(args=[pex, "Moo!"]) + assert b"| Moo! |" in subprocess.check_output(args=[old_python, pex, "Moo!"])