Support creating a venv for a foreign platform. - #3279
Closed
jasonwbarnett wants to merge 3 commits into
Closed
jasonwbarnett wants to merge 3 commits into
jasonwbarnett wants to merge 3 commits into
Conversation
A venv's layout is fixed by the Python implementation, version and OS; the machine architecture plays no part. So a local interpreter can lay a venv out for a foreign platform it differs from in architecture alone, leaving just the venv Python to point at wherever the foreign interpreter will live. The new `pex3 venv create --link-python` does that pointing, and is useful on its own for a venv built somewhere other than its final resting place. Closes pex-tool#3278 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01JXoAkfaDfspdEqDzgHoTzG
Member
|
@jasonwbarnett can you allow maintainers to edit PR? I have scoping (no 2.7 support, no Windows support) and test (pyvenv.cfg executable is not a thing until 3.11) fixes I want to push: :; git push altana-ai HEAD
remote: Permission to altana-ai/pex.git denied to jsirois.
fatal: unable to access 'https://github.com/altana-ai/pex/': The requested URL returned error: 403Or, apply this please (also attached patch.zip): From 76d1e7e9be96c61c474bc98473d7c45137a852dc Mon Sep 17 00:00:00 2001
From: John Sirois <john.sirois@gmail.com>
Date: Tue, 15 Sep 2026 12:07:03 -0700
Subject: [PATCH] Fix scope (modern Unix venvs) & tests.
---
pex/cli/commands/venv.py | 17 ++-
pex/sysconfig.py | 10 +-
.../cli/commands/test_venv_create.py | 113 +++++++++++++-----
3 files changed, 105 insertions(+), 35 deletions(-)
diff --git a/pex/cli/commands/venv.py b/pex/cli/commands/venv.py
index a794e873..8bb0785b 100644
--- a/pex/cli/commands/venv.py
+++ b/pex/cli/commands/venv.py
@@ -20,6 +20,7 @@ from pex.executor import Executor
from pex.fingerprinted_distribution import FingerprintedDistribution
from pex.interpreter import PythonInterpreter
from pex.orderedset import OrderedSet
+from pex.os import Os
from pex.pex import PEX
from pex.pex_bootstrapper import normalize_path
from pex.pex_info import PexInfo
@@ -292,12 +293,26 @@ class Venv(OutputMixin, JsonMixin, BuildTimeCommand):
if layout is InstallLayout.VENV:
venv_interpreter = target.get_interpreter()
if target.is_foreign:
- if not self.options.link_python:
+ if Os.CURRENT is Os.WINDOWS:
+ return Error(
+ "Cannot create a local venv for foreign platform {platform}.\n"
+ "This is only supported for Unix environments.".format(
+ platform=target.platform
+ )
+ )
+ elif not self.options.link_python:
return Error(
"Cannot create a local venv for foreign platform {platform}.\n"
"Specify --link-python to say where the venv's Python will live in "
"the foreign environment.".format(platform=target.platform)
)
+ elif not target.python_version or target.python_version < (3, 3):
+ return Error(
+ "Cannot create a local venv for foreign platform {platform}.\n"
+ "This is only supported for Python 3.3 and newer.".format(
+ platform=target.platform
+ )
+ )
venv_interpreter = try_(_find_stand_in_interpreter(target, self.options))
venv = Virtualenv.create(
diff --git a/pex/sysconfig.py b/pex/sysconfig.py
index d21257dc..880edf5f 100644
--- a/pex/sysconfig.py
+++ b/pex/sysconfig.py
@@ -17,7 +17,7 @@ from pex.os import Os
from pex.typing import TYPE_CHECKING
if TYPE_CHECKING:
- from typing import Optional, Text, TypeVar
+ from typing import Optional, Text, Tuple, TypeVar, Union
EXE_EXTENSION = get_config_var("EXE") or ""
EXE_EXTENSIONS = (
@@ -159,6 +159,14 @@ class _PlatformValue(Enum.Value):
# type: () -> str
return "Scripts" if self.os is Os.WINDOWS else "bin"
+ def venv_lib_dir(self, version):
+ # type: (Union[Tuple[int, int], Tuple[int, int, int]]) -> str
+ return (
+ "Lib"
+ if self.os is Os.WINDOWS
+ else "lib/python{major}.{minor}".format(major=version[0], minor=version[1])
+ )
+
def binary_name(self, binary_name):
# type: (_Text) -> _Text
return "{binary_name}{extension}".format(
diff --git a/tests/integration/cli/commands/test_venv_create.py b/tests/integration/cli/commands/test_venv_create.py
index c142ab16..f515893c 100644
--- a/tests/integration/cli/commands/test_venv_create.py
+++ b/tests/integration/cli/commands/test_venv_create.py
@@ -5,6 +5,7 @@ from __future__ import absolute_import
import glob
import os.path
+import re
import shutil
import sys
from subprocess import CalledProcessError
@@ -24,14 +25,17 @@ from pex.pep_440 import Version
from pex.pep_503 import ProjectName
from pex.pex import PEX
from pex.resolve import abbreviated_platforms
+from pex.sysconfig import SysPlatform
from pex.typing import TYPE_CHECKING
from pex.venv.bin_path import BinPath
from pex.venv.virtualenv import Virtualenv
from testing import (
IS_ARM_64,
IS_MAC,
+ IS_WINDOWS,
PY39,
PY310,
+ IntegResults,
ensure_python_interpreter,
make_env,
run_pex_command,
@@ -545,51 +549,94 @@ def test_foreign_target(
assert Version("5.9.5") == dist.metadata.version
-@pytest.fixture
-def cross_arch_platform():
- # type: () -> str
+def cross_arch_platform(
+ major, # type: int
+ minor, # type: int
+ abiflags="", # type: str
+):
+ # type: (...) -> str
"""A foreign platform differing from the local one in machine architecture alone."""
+ pyver = "{major}{minor}".format(major=major, minor=minor)
if IS_MAC:
- return "macosx_11_0_{machine}-cp-310-cp310".format(
- machine="x86_64" if IS_ARM_64 else "arm64"
+ return "macosx_11_0_{machine}-cp-{pyver}-cp{pyver}{abiflags}".format(
+ machine="x86_64" if IS_ARM_64 else "arm64", pyver=pyver, abiflags=abiflags
)
- return "linux_{machine}-cp-310-cp310".format(machine="x86_64" if IS_ARM_64 else "aarch64")
+ return "linux_{machine}-cp-{pyver}-cp{pyver}{abiflags}".format(
+ machine="x86_64" if IS_ARM_64 else "aarch64", pyver=pyver, abiflags=abiflags
+ )
+@pytest.mark.skipif(IS_WINDOWS, reason="This feature is not supported on Windows.")
def test_foreign_target_link_python(
- tmpdir, # type: Any
- cross_arch_platform, # type: str
+ tmpdir, # type: Tempdir
+ py310, # type: PythonInterpreter
+ py311, # type: PythonInterpreter
):
# type: (...) -> None
- dest = os.path.join(str(tmpdir), "dest")
- link_python = "/opt/python/bin/python3.10"
- run_pex3(
- "venv",
- "create",
- "psutil==5.9.5",
- "-d",
- dest,
- "--platform",
- cross_arch_platform,
- "--python-path",
- ensure_python_interpreter(PY310),
- "--link-python",
- link_python,
- ).assert_success()
+ venv_dir = tmpdir.join("venv")
+ link_python = "/opt/python/bin/bob"
- assert {link_python} == {
- os.readlink(python) for python in glob.glob(os.path.join(dest, "bin", "python*"))
- }
+ def create_venv(foreign_platform):
+ # type: (str) -> IntegResults
+ return run_pex3(
+ "venv",
+ "create",
+ "psutil==5.9.5",
+ "-d",
+ venv_dir,
+ "--platform",
+ foreign_platform,
+ "--python-path",
+ os.pathsep.join(interpreter.binary for interpreter in (py310, py311)),
+ "--link-python",
+ link_python,
+ "--force",
+ )
- pyvenv_cfg = PyVenvCfg.parse(os.path.join(dest, "pyvenv.cfg"))
- assert os.path.dirname(link_python) == pyvenv_cfg.home
- assert link_python == pyvenv_cfg.config("executable")
+ def assert_foreign_venv(
+ major, # type: int
+ minor, # type: int
+ expect_pyvenv_cfg_executable, # type: Optional[str]
+ ):
+ # type: (...) -> None
+ assert {link_python} == {
+ os.readlink(python) for python in glob.glob(os.path.join(venv_dir, "bin", "python*"))
+ }
+
+ pyvenv_cfg = PyVenvCfg.parse(os.path.join(venv_dir, "pyvenv.cfg"))
+ assert os.path.dirname(link_python) == pyvenv_cfg.home
+ pyvenv_cfg_executable = pyvenv_cfg.config("executable")
+ if expect_pyvenv_cfg_executable:
+ assert pyvenv_cfg_executable == expect_pyvenv_cfg_executable
+ else:
+ assert pyvenv_cfg_executable is None
+
+ site_packages = os.path.join(
+ venv_dir, SysPlatform.CURRENT.venv_lib_dir(version=(major, minor)), "site-packages"
+ )
+ distributions = list(dist_metadata.find_distributions(search_path=[site_packages]))
+ assert 1 == len(distributions)
+ assert ProjectName("psutil") == distributions[0].metadata.project_name
+
+ foreign_platform = cross_arch_platform(2, 7, "mu")
+ create_venv(foreign_platform=cross_arch_platform(2, 7, "mu")).assert_failure(
+ expected_error_re=r".*{msg}$".format(
+ msg=re.escape(
+ "Cannot create a local venv for foreign platform {foreign_platform}.\n"
+ "This is only supported for Python 3.3 and newer.".format(
+ foreign_platform=foreign_platform
+ )
+ )
+ ),
+ re_flags=re.DOTALL,
+ )
- site_packages = os.path.join(dest, "lib", "python3.10", "site-packages")
- distributions = list(dist_metadata.find_distributions(search_path=[site_packages]))
- assert 1 == len(distributions)
- assert ProjectName("psutil") == distributions[0].metadata.project_name
+ create_venv(foreign_platform=cross_arch_platform(3, 10)).assert_success()
+ assert_foreign_venv(3, 10, expect_pyvenv_cfg_executable=None)
+
+ create_venv(foreign_platform=cross_arch_platform(3, 11)).assert_success()
+ assert_foreign_venv(3, 11, expect_pyvenv_cfg_executable=link_python)
def test_venv_update_target_mismatch(
--
2.55.0 |
jsirois
approved these changes
Sep 16, 2026
jsirois
approved these changes
Sep 16, 2026
Member
Member
|
Merged the old fashioned way: e06795b |
This was referenced Sep 17, 2026
jsirois
added a commit
that referenced
this pull request
Sep 18, 2026
`pex3 venv create --link-python` for a foreign platform fails when the
venv is installed from a `--pex-repository`:
```
A distribution for psutil could not be resolved for <local python>.
Found 1 distribution for psutil that does not apply:
1.) The wheel tags for psutil 5.9.5 are cp36-abi3-manylinux_2_12_x86_64, ... which do not match
the supported tags of <local python>: cp313-cp313-manylinux_2_39_aarch64 ...
```
The distributions themselves install fine — it is populating the venv's
*sources* that fails. `_populate_first_party` re-resolves the repository
PEX to build the `pex-repl` script's `activated_dists`, and that resolve
runs against a local interpreter, which a PEX built for the foreign
platform alone holds nothing for. `_install_from_pex` has already
resolved the distributions for the target, so this passes those down
instead; the `PEX_TOOLS=1 ... venv` path keeps re-resolving, where the
PEX and the interpreter always agree.
This is the combination Pants uses — it always invokes `pex3 venv
create` with `--pex-repository` — so #3279 is unreachable from Pants
without it.
Repro and verification, on a linux aarch64 host:
```
pex psutil==5.9.5 --platform linux_x86_64-cp-313-cp313 -o psutil.pex
pex3 venv create -d venv --pex-repository psutil.pex --platform linux_x86_64-cp-313-cp313 \
--python-path $(command -v python3.13) --link-python /opt/python/bin/bob
```
Before: the error above. After: `bin/python`, `bin/python3` and
`bin/python3.13` all link to `/opt/python/bin/bob`, and
`lib/python3.13/site-packages/psutil/_psutil_linux.abi3.so` is `ELF
64-bit LSB shared object, x86-64`.
`test_foreign_target_link_python_from_pex_repository` covers it. I could
not run it locally — the `py310` fixture's CPython 3.10.7 build fails in
my environment (`ensurepip` exits non-zero) — so CI is its first run;
the commands above are the same shape by hand. `uv run dev-cmd format
lint typecheck` is green.
Written with Claude Code, as was #3279.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
https://claude.ai/code/session_012imZdyz1fhVXRNKPjbhSDw
---------
Co-authored-by: Jason Barnett <jason.barnett@altana.ai>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

Closes #3278.
A venv's layout is fixed by the Python implementation, version and OS; the
machine architecture plays no part. So a local interpreter can lay a venv out
for a foreign platform it differs from in architecture alone, leaving just the
venv Python to point at wherever the foreign interpreter will live.
The new
pex3 venv create --link-pythondoes that pointing. The path need notexist when the venv is created, which is what makes the foreign case work: the
venv, its scripts and the foreign wheels all land locally and the Python link
only resolves once the venv reaches its final resting place. That is also
useful on its own for a venv built somewhere other than where it will run.
When the target is foreign,
--link-pythonis required and a stand-ininterpreter is searched for on
--python-path($PATHby default), matchingthe target's interpreter tag, ABI and
platform_system. Nothing matching meansa fail fast naming what was needed.
--compilenow uses the venv's own Pythonrather than the one running Pex, so a foreign venv gets bytecode for its own
version.
Built and verified on
linux-aarch64forlinux_x86_64-cp-311-cp311:N.B.:
tests/integration/cli/commands/test_venv_create.pygrew a cross-archcase that leans on
ensure_python_interpreter(PY310); it could not run where Iwrote it, since
pyenv install 3.10.7will not build on Ubuntu 24.04. Itsassertions were checked against a real foreign venv built with 3.11 instead.
🤖 Generated with Claude Code
https://claude.ai/code/session_01JXoAkfaDfspdEqDzgHoTzG