Visitar URL original
feat: Add a venv-based build environment provider by ichard26 · Pull Request #14070 · pypa/pip · GitHub
Skip to content

feat: Add a venv-based build environment provider - #14070

Merged
ichard26 merged 10 commits into
pypa:mainfrom
ichard26:feat/venv-isolation
Jul 7, 2026
Merged

ichard26 merged 10 commits into
pypa:mainfrom
ichard26:feat/venv-isolation

Conversation

@ichard26

@ichard26 ichard26 commented Jun 17, 2026 •

Copy link
Copy Markdown
Member

By using real virtual environments, pip behaves more similarly to python-build and with less buggy isolation. However, since the venv-based environment does differ in numerous respects, we need to introduce this feature in stages. It will be first be available with--use-feature=venv-isolation

A few noteworthy details:

  1. Since the sitecustomize.py trick is gone, we need to call the venv's Python executable directly to pick up the right site-packages directories.

  2. We no longer prepend the environment's site to PYTHONPATH as all of the backend calls will be made with the venv's Python executable. If they spawn more Python subprocesses, the PATH adjustment will ensure that the venv's Python executable is still used. (This doesn't apply for system level Python tools, but those should be able to access their own site paths)

Some known behavioural differences:

  1. Backend calls are now able to use system Python tools from the system environment as isolation doesn't apply to such Python processes anymore (since the sitecustomize.py file was dropped). However, the backend isn't able to import external libraries, of course.

  2. There is no more site-packages layering, one for the static build backend dependencies, and one for the dynamic injected dependencies. They are all installed to the same location since nested virtual environments aren't really a thing. This means all dependencies requested by a backend must be resolvable altogether (which IMO seems fair).

  3. The venv-based build environment plays... poorly with the new inprocess build-dep installer as due to (2), we now need to be able to uninstall conflicting static dependencies while dynamic dependencies are installed. This doesn't work... since pip has no proper support for operating with another Python environment inprocess. This is fixable, but will take some time. There is one xfailing test related to this (build env console scripts have the wrong Python executable).

  4. The build environment's site-packages no longer overrides stdlib modules. This occured with the virtual build environment as it set PYTHONPATH which prepended said site directories to sys.path for backend subprocesses. This shouldn't break anyone, but who knows what can happen.

Regrettably, it's unlikely that we will be able to replace virtual build environments wholesale with venvs as certain platforms do not support or include the venv module (including Android, iOS, probably certain Linux distributions... although not Debian-based distros!)

This will fix #13222, fix #12419, fix #12976, should fix #11812, and should fix #10635.

By using real virtual environments, pip behaves more similarly to
python-build and with less buggy isolation. However, since the
venv-based environment does differ in numerous respects, we need to
introduce this feature in stages. It will be first be available with
`--use-feature=venv-isolation`

A few noteworthy details:

(1) Since the sitecustomize.py trick is gone, we need to call the venv's
  Python executable directly to pick up the right site-packages
  directories.

(2) We no longer prepend the environment's site to PYTHONPATH as all of
  the backend calls will be made with the venv's Python executable. If
  they spawn more Python subprocesses, the PATH adjustment will ensure
  that the venv's Python executable is still used. (This doesn't apply
  for system level Python *tools*, but those should be able to access
  their own site paths)

Some known behavioural differences:

(1) Backend calls are now able to use system Python tools from the
  system environment as isolation doesn't apply to such Python processes
  anymore (since the sitecustomize.py file was dropped). However, the
  backend isn't able to import external libraries, of course.

(2) There is no more site-packages layering, one for the static build
  backend dependencies, and one for the dynamic injected dependencies.
  They are all installed to the same location since nested virtual
  environments aren't really a thing. This means all dependencies
  requested by a backend must be resolvable altogether (which IMO
  seems fair).

(3) The venv-based build environment plays... poorly with the new
  inprocess build-dep installer as due to (2), we now need to be able to
  uninstall conflicting static dependencies while dynamic dependencies
  are installed. This doesn't work... since pip has no proper support
  for operating with another Python environment inprocess. This is
  fixable, but will take some time. There is one xfailing test related
  to this (build env console scripts have the wrong Python executable).

(4) The build environment's site-packages no longer overrides stdlib
  modules. This occured with the virtual build environment as it set
  PYTHONPATH which prepended said site directories to sys.path for
  backend subprocesses. This shouldn't break anyone, but who knows what
  can happen.

Regrettably, it's unlikely that we will be able to replace virtual build
environments wholesale with venvs as certain platforms do not support or
include the venv module (including Android, iOS, probably certain Linux
distributions... although not Debian-based distros!)
@ichard26
ichard26 force-pushed the feat/venv-isolation branch from f1bdd85 to bc9597d Compare June 20, 2026 01:57
@ichard26
ichard26 marked this pull request as ready for review June 20, 2026 02:15
@ichard26

Copy link
Copy Markdown
Member Author

This is finally ready for review. I've left fairly extensive notes in the commit message and PR description. Not all of it is relevant to the review, but it's good context for the transition and what to expect.

@ichard26

Copy link
Copy Markdown
Member Author

Also, why did I decide to name the legacy build environment "virtual" ... it doesn't use virtual environments, LOL. I will fix that in a separate PR later. Maybe I should've gotten the LLM to rename that class instead of me picking the name.

@ichard26

ichard26 commented Jun 21, 2026 •

Copy link
Copy Markdown
Member Author

Sigh, OK, so there ended up being a LOT more subtle issues using venv for build isolation:

  • The context object that venv.ensure_directories returns containing all of the key paths we need to interact with the virtual environment was only documented in Python 3.12. It seems to exist on all CPython versions except for those as old or older than 3.10.1
    • ... but we can't rely on these context attributes existing until 3.12 for alternative Python implementations
    • PyPy is notable since its site-packages path construction is different
  • venv's API isn't a clean mapping to its CLI behaviour, turns out that venv uses symlinks by default on macOS and Linux. This is necessary to support relocatable Python installations (including python-build-standalone)
  • Windows is a nightmare and venv creation can fail in numerous ways, including aggressive AVs that prevent the Python executable from being copied

There is one remaining Windows issue that's unfixable, if the backend spawns a child process which persists after pip itself has finished running, then pip won't be able to delete the temporary virtual environment as its python.exe will still be locked... writing this out I realize that I probably need to add error handling for that in pip's cleanup routine, but I'll do that tomorrow when I boot my Windows machine.

I've tried my best to harden venv initialization as much as possible. I hate how many fallbacks it has, but the consolation prize is that we can remove most of it once we drop Python 3.10 and 3.11.

@ichard26

Copy link
Copy Markdown
Member Author

For context, Damian threw Claude at my feature branch and got it to find as many edge cases in my implementation as possible. I've attached the two reports. I also got Codex to take a look locally, but it is comparably much weaker at finding issues.

Review one

Review: PR #14070 (venv-based build environment)

Two correctness regressions versus the legacy build environment, plus one
low-severity test defect. Both regressions are gated behind
--use-feature=venv-isolation. Verified end-to-end against the PR head
(bc9597d9).

Setup

# PR code in a throwaway worktree (keeps your checkout untouched)
git fetch upstream pull/14070/head
git worktree add /tmp/pip-pr14070 bc9597d9
RUNNER=/tmp/pip-pr14070/src/pip/__pip-runner__.py   # runs the PR's pip from source

# a source tree that needs an isolated setuptools build
mkdir -p /tmp/pkg/sampleproj
cat > /tmp/pkg/pyproject.toml <<'EOF'
[build-system]
requires = ["setuptools>=60"]
build-backend = "setuptools.build_meta"
[project]
name = "sampleproj"
version = "0.1"
EOF
printf 'from setuptools import setup\nsetup()\n' > /tmp/pkg/setup.py
echo 'x = 1' > /tmp/pkg/sampleproj/__init__.py

# a host venv to launch pip from for Bug 1 (pip itself comes from $RUNNER)
python3 -m venv --without-pip /tmp/host

--dry-run suffices; the build env and its deps are set up before the dry-run
report.

Bug 1 (high): PIP_PREFIX / pip.conf prefix= redirects build deps out of the venv

src/pip/_internal/build_env/installer.py,
SubprocessBuildEnvironmentInstaller.install: the new if prefix.venv_executable:
branch drops the --prefix <path> that the legacy branch passes. pip derives the
default for --prefix from PIP_PREFIX and from a pip.conf [install] prefix=.
With no --prefix on the command line, that ambient value wins, so build deps
install to the user's prefix instead of the temp venv. The venv is left empty, so
importing the backend with the venv python fails.

PIP_PREFIX=/tmp/leak /tmp/host/bin/python "$RUNNER" install \
    --use-feature=venv-isolation --dry-run /tmp/pkg
#   Getting requirements to build wheel: finished with status 'done'
# ERROR: Exception:
# pip._vendor.pyproject_hooks._impl.BackendUnavailable: Cannot import 'setuptools.build_meta'

Controls, both succeed (Would install sampleproj-0.1):

env -u PIP_PREFIX /tmp/host/bin/python "$RUNNER" install --use-feature=venv-isolation --dry-run /tmp/pkg
PIP_PREFIX=/tmp/leak /tmp/host/bin/python "$RUNNER" install                        --dry-run /tmp/pkg  # legacy is immune

A pip.conf [install] prefix= feeds the same option and behaves identically. This
is the subprocess installer only; the inprocess installer
(--use-feature=inprocess-build-deps) passes prefix.path directly and ignores
PIP_PREFIX (verified). --target "" still neutralizes PIP_TARGET and
--no-user still covers PIP_USER; only --prefix was dropped. PIP_ROOT
affects both the venv and legacy subprocess branches (neither passes --root), so
it is not introduced here. The comment at installer.py ("The prefix specified two
lines above ...") is now stale for this branch.

Fix: pin the install location in the venv branch. Both are immune to PIP_PREFIX
(verified): add --prefix <venv_root> (in a venv this resolves to the venv's own
site-packages), or add --isolated.

Bug 2 (high on standalone interpreters): EnvBuilder() copies the interpreter, the build venv cannot start

src/pip/_internal/build_env/venv.py: venv.EnvBuilder() uses the constructor
default symlinks=False, so on POSIX it copies the interpreter. python -m venv
symlinks on POSIX. For relocatable CPython (python-build-standalone, used by
uv/rye/mise/hatch and most CI) a copied interpreter looks for its stdlib next to
pyvenv.cfg's home. When pip is launched directly by such an interpreter whose
on-PATH name is a shim (for example ~/.local/bin/python3.12), home is the shim
directory, which has no stdlib, so the copied python cannot boot and every
isolated build fails. Running pip from inside an already-created venv is unaffected
(there home points at the real interpreter directory).

# pip launched directly by a uv/standalone shim (not inside a venv)
~/.local/bin/python3.12 "$RUNNER" install --use-feature=venv-isolation \
    --break-system-packages --dry-run /tmp/pkg
#   Installing build dependencies: finished with status 'error'
#   Could not find platform independent libraries <prefix>
#   Fatal Python error: init_fs_encoding: failed to get the Python codec of the filesystem encoding
#   ModuleNotFoundError: No module named 'encodings'

The same command without --use-feature=venv-isolation builds fine
(Would install sampleproj-0.1). Minimal mechanism:

~/.local/bin/python3.12 -c '
import venv, subprocess
b = venv.EnvBuilder(); c = b.ensure_directories("/tmp/bv"); b.create("/tmp/bv")
print("copied :", subprocess.run([c.env_exec_cmd, "-c", "print(1)"]).returncode)'
# copied : 1   (returncode; the boot fails with: No module named encodings)
# with symlinks=True the returncode is 0

Even when the copy boots, it duplicates the interpreter binary (tens of MB) per
build venv; the legacy env copied nothing.

Fix: venv.EnvBuilder(symlinks=(os.name != "nt")), matching the python -m venv
default.

Bug 3 (low): xfail(strict=True) shebang test can XPASS and fail CI

tests/functional/test_build_env.py,
test_venv_build_env_inprocess_scripts_use_venv_python: the only assertion is
guarded by if "/bin/sh" not in shebang:. distlib emits a /bin/sh wrapper when
the interpreter path contains a space or the shebang exceeds 127 chars. Then no
assertion runs, the test passes, and strict=True turns that XPASS into a
failure. It will not fire on pip's hosted runners (short, space-free paths) but is
fragile elsewhere. Assert against the wrapper's exec line instead of skipping, or
drop strict.

Minor

  • venv.py __enter__ prepends the venv bin (which holds python/python3) to
    PATH, so a backend that shells out to a bare python gets the pip-less venv
    interpreter. Arguably intended ("as if activated"), and the legacy env already
    hides system site-packages, so this is debatable rather than wrong. Worth a doc
    note.
  • test_build_env_can_still_access_python_tools_on_system_path has no skip guard
    and needs a downloadable cmake wheel for the runner arch.
  • venv.py calls ensure_directories() then create() (which re-runs it).
    Harmless.

Checked, no issue

configure_backend/load_pyproject_toml split (no path reaches pep517_backend
while it is None); build_isolation == "off" uses NoOp.python_executable; the
abstract __init__ signature mismatch; env_exec_cmd exists on 3.9+; the
lib_path 3.12 guard; venv disables user-site so dropping PYTHONNOUSERSITE is
fine; an empty PYTHONPATH does not inject cwd; --target "" neutralizes
PIP_TARGET.

Review two

PR #14070 (venv build environment): issues found on Windows

Tested on Windows 11 / CPython 3.13.5, building sdists from stdlib venv, virtualenv,
uv, and conda base interpreters. Issues below.

1. inprocess-build-deps + venv-isolation: console-script build deps break on Windows

With both features on, a console-script build dep gets a Windows .exe launcher embedding
the parent pip's interpreter instead of the temp venv's. The inprocess installer writes
launchers with the running pip's interpreter (sys.executable); the subprocess installer runs
pip as the venv python, so only it targets the venv. The interpreter is baked into the .exe,
so PATH can't redirect it.

Repro:

  • scripttool: a wheel with a console script (scripttool = "scripttool:main") whose main
    imports its own package.
  • usestool: build-system.requires = ["setuptools>=61", "scripttool==1.0"]; its setup.py
    runs subprocess.run([shutil.which("scripttool")], check=True) (invoked during
    get_requires_for_build_wheel).
  • pip wheel usestool --use-feature=venv-isolation --use-feature=inprocess-build-deps --find-links <dir with scripttool wheel> --no-deps

Result: ModuleNotFoundError: No module named 'scripttool' — the .exe runs the parent
interpreter, which lacks the build deps. Dropping --use-feature=inprocess-build-deps (the
default subprocess installer) builds it. The embedded interpreter is the #!"...python.exe"
line just before the PK\x03\x04 zip header in scripttool.exe: parent python under
inprocess, venv python under subprocess.

The PR description notes this limitation and the xfail covers it, but the xfail is skipped on
Windows as "Unix-only test" (test_build_env.py:414). The defect is present on Windows too;
only the shebang-reading technique doesn't apply to an .exe. The Windows skip should be
removed when the fix lands.

2. A failed python.exe copy during venv creation is not detected (Windows)

EnvBuilder.setup_python copies venvlauncher.exe to Scripts/python.exe inside a
try/except OSError that only logs a warning, so create() returns even when no interpreter
was produced. venv.py:64 then stores context.env_exec_cmd without checking it exists.

Repro (deterministic, no real AV needed):

import os, tempfile, shutil, venv
d = tempfile.mkdtemp()
b = venv.EnvBuilder(symlinks=False)
ctx = b.ensure_directories(d)
_orig = shutil.copy2
def blocked(src, dst, *a, **k):
    if os.path.basename(dst).lower() in ("python.exe", "pythonw.exe"):
        raise OSError(13, "blocked")          # emulate AV / lock on the launcher copy
    return _orig(src, dst, *a, **k)
shutil.copy2 = blocked
b.create(d)                                   # returns without raising
print(os.path.exists(ctx.env_exec_cmd))       # -> False

Real triggers: Defender / controlled-folder-access or a sharing violation on the new exe. The
legacy env has no copy step (uses sys.executable), so this is new here. Through pip it
surfaces as a later "file not found" rather than a build-env error; an os.path.exists check
after create() makes it clear (patch below).

3. A surviving child holding the venv python leaks the temp dir (Windows)

The build env now contains a real python.exe, so a grandchild that outlives the build and
holds it open blocks removal of the pip-build-env-* temp dir (Windows can't delete a running
exe). The legacy env runs sys.executable and leaves only text files, so it never hits this.

Repro:

import os, tempfile, shutil, subprocess, venv, time
d = tempfile.mkdtemp()
venv.EnvBuilder(symlinks=False, with_pip=False).create(d)
vpy = os.path.join(d, "Scripts", "python.exe")
p = subprocess.Popen([vpy, "-c", "import time; time.sleep(20)"])
time.sleep(1)
shutil.rmtree(d)        # PermissionError: [WinError 5] Access is denied

Triggered by backends that leave a daemon or test process running. Minor, but new on Windows.

4. Only import venv is guarded

Only import venv is wrapped (venv.py:27-30 → VenvImportError);
EnvBuilder.ensure_directories() / create() can raise OSError/ValueError that fall
through as a raw ERROR: Exception. Example: a ; in TEMP gives ValueError: Refusing to create a venv in <path> because it contains the PATH separator ;. Not a regression (the
legacy env also fails on ; in TEMP, later and differently), but the venv path exposes more
EnvBuilder failure modes without a diagnostic.

5. context.env_exec_cmd is read unconditionally and is missing on 3.10.0

venv.py:64 reads context.env_exec_cmd. That attribute (bpo-45337) is absent in v3.10.0 and
present from v3.10.1 (per the release tags). On 3.10.0, --use-feature=venv-isolation would
raise AttributeError before any build. getattr(context, "env_exec_cmd", context.env_exe)
covers it (patch below). Narrow, but free to guard.

Patch for issues 2 and 5

One spot in venv.py. Verified on Windows: normal build unchanged; the fallback yields a
working interpreter when env_exec_cmd is deleted; a blocked launcher copy now raises.

-        self.python_executable = context.env_exec_cmd
+        # env_exec_cmd was added in bpo-45337 (Python 3.10.1); fall back to env_exe
+        # on 3.10.0.
+        self.python_executable = getattr(context, "env_exec_cmd", context.env_exe)
+        # On Windows, venv.create() only logs a warning if it fails to copy the
+        # launcher to Scripts/python.exe, so confirm the interpreter exists before
+        # relying on it.
+        if not os.path.exists(self.python_executable):
+            raise VenvCreationError(self.python_executable)

VenvCreationError is a small DiagnosticPipError alongside VenvImportError.

6. lib_dirs is wrong for PyPy on POSIX (not Windows)

For Python < 3.12, venv.py:54-60 builds the site-packages path from python{X.Y}. PyPy's
POSIX layout is lib/pypy{X.Y}/site-packages (confirmed via PyPy 3.11's sysconfig), so
lib_dirs points at a nonexistent dir and check_requirements / --check-build-deps
silently find nothing. Windows PyPy uses Lib/site-packages, so it's unaffected. 3.12+ avoids
this via context.lib_path; deriving the < 3.12 path from sysconfig (as the code comment
suggests) would fix PyPy.

By inspection, not reproduced

The venv path drops --ignore-installed and shares one prefix (installer.py:117-121 adds it
only on the non-venv branch), so a dynamic build req can uninstall or upgrade a static one.
Matches the layering change in the PR description; worth a changelog line.

I've fixed all of the issues flagged except for the console script and locked python.exe one as noted.

I still believe that using venv is the right approach since it aligns pip closer with how pypa/build and uv works as they use a virtual environment under the hood. And once the feature has stabilized, it should be less buggy and more maintainable than trying to fix up our custom isolation logic. It just seems the transition is going is bumpy.

Comment on lines +61 to +79
else:
# Otherwise, we need to manually construct all the paths... sigh.
if sys.platform == "win32":
libpath = os.path.join(self._env_path, "Lib", "site-packages")
else:
python = "pypy" if sys.implementation.name == "pypy" else "python"
libpath = os.path.join(
self._env_path,
"lib",
f"{python}{sys.version_info.major}.{sys.version_info.minor}",
"site-packages",
)
self.lib_dirs = [libpath]
# Same reasoning for try-except as for python_executable below.
try:
self._bin_path = context.bin_path
except AttributeError:
scripts_dir = "Scripts" if os.name == "nt" else "bin"
self._bin_path = os.path.join(self._env_path, scripts_dir)

@ichard26 ichard26 Jun 21, 2026 •

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just did check how pypa/build handles this and they seem to rely exclusively on sysconfig, although their else-branch seems similarly dodgy as ours 🙃 https://github.com/pypa/build/blob/f7aaf74017c8e6c29fa3a3b7fc683517ae437aa6/src/build/env.py#L513-L519

I guess we could assume the prefix scheme for Python 3.10 to reduce the amount of manual path construction, but I'd need to think it more through first.

Our Python executable discovery code seems more robust than theirs though... /shrug

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is what using sysconfig on 3.10 would look like:

diff --git a/src/pip/_internal/build_env/venv.py b/src/pip/_internal/build_env/venv.py
index d6d7c5055..2400e759e 100644
--- a/src/pip/_internal/build_env/venv.py
+++ b/src/pip/_internal/build_env/venv.py
@@ -19,12 +19,14 @@ if TYPE_CHECKING:
     from pip._internal.req.req_install import InstallRequirement
 
 
-def _get_venv_path_from_sysconfig(name: str, env_dir: str) -> str:
+def _get_venv_path_from_sysconfig(
+    name: str, env_dir: str, *, scheme: str = "venv"
+) -> str:
     vars = {
         "base": env_dir,
         "platbase": env_dir,
     }
-    return sysconfig.get_path(name, scheme="venv", vars=vars)
+    return sysconfig.get_path(name, scheme=scheme, vars=vars)
 
 
 class VenvBuildEnvironment(BuildEnvironment):
@@ -55,28 +57,19 @@ class VenvBuildEnvironment(BuildEnvironment):
             self.lib_dirs = [context.lib_path]
             self._bin_path = context.bin_path
         elif sys.version_info[:2] == (3, 11):
-            # On Python 3.11, we can use sysconfig.
+            # On Python 3.11, we can use sysconfig's venv scheme
             self.lib_dirs = [_get_venv_path_from_sysconfig("purelib", self._env_path)]
             self._bin_path = _get_venv_path_from_sysconfig("scripts", self._env_path)
         else:
-            # Otherwise, we need to manually construct all the paths... sigh.
-            if sys.platform == "win32":
-                libpath = os.path.join(self._env_path, "Lib", "site-packages")
-            else:
-                python = "pypy" if sys.implementation.name == "pypy" else "python"
-                libpath = os.path.join(
-                    self._env_path,
-                    "lib",
-                    f"{python}{sys.version_info.major}.{sys.version_info.minor}",
-                    "site-packages",
-                )
-            self.lib_dirs = [libpath]
-            # Same reasoning for try-except as for python_executable below.
-            try:
-                self._bin_path = context.bin_path
-            except AttributeError:
-                scripts_dir = "Scripts" if os.name == "nt" else "bin"
-                self._bin_path = os.path.join(self._env_path, scripts_dir)
+            # Otherwise, the best we can do is assume sysconfig's prefix scheme
+            scheme = sysconfig.get_preferred_scheme("prefix")
+            purelib = _get_venv_path_from_sysconfig(
+                "purelib", self._env_path, scheme=scheme
+            )
+            self.lib_dirs = [purelib]
+            self._bin_path = _get_venv_path_from_sysconfig(
+                "scripts", self._env_path, scheme=scheme
+            )
 
         # There are enough ways trying to construct the Python executable path can go
         # wrong that we're better off assuming that the context object has the right

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm going to stick with what I have right now since it seems to work fine, and I don't want to rock the boat further. This is similar (but expanded in this PR) to how the original PR by Pradyun determined environment paths.

This is also similar to how Python 3.10's venv works in practice.

@geofft

geofft commented Jul 4, 2026

Copy link
Copy Markdown

Windows is a nightmare and venv creation can fail in numerous ways, including aggressive AVs that prevent the Python executable from being copied

I think you can play tricks on Windows specifically with setting the __PYVENV_LAUNCHER__ environment variable (which gets unset at Python startup) to the expected pathname for the venv's interpreter, and invoking the real Python executable, without actually creating a file at that pathname. Since the venv is just for your own internal use and you're not having anyone else run it, you can probably get away with having to invoke Python this way. (I believe this trick doesn't work on Linux until Python 3.12, or something like that, but I believe it works on all versions of Python on Windows.)

venv's API isn't a clean mapping to its CLI behaviour, turns out that venv uses symlinks by default on macOS and Linux. This is necessary to support relocatable Python installations (including python-build-standalone)

FWIW, python/cpython#129382 requests updating the API defaults to match the CLI defaults. (It has a patch but just seems to be stalled on getting it CI clean.)

Regrettably, it's unlikely that we will be able to replace virtual build environments wholesale with venvs as certain platforms do not support or include the venv module

You could consider vendoring or reimplementing it; most of the interesting part of venv (as compared to virtualenv and its custom site.py etc.) is now upstream in Python itself. You don't even need all of it, e.g., you can drop support for activate scripts. You mostly just need to create a pyvenv.cfg.

@notatallshaw notatallshaw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I set Claude on this tonight and cherry picked on something that seemed important:

The venv branch of the build-dep install drops --ignore-installed, but install_requirements runs before VenvBuildEnvironment.__enter__ clears PYTHONPATH, so the install subprocess still sees the caller's PYTHONPATH. If a declared build requirement happens to be importable from that ambient PYTHONPATH, pip treats it as already satisfied and never installs it into the venv. The backend then runs with PYTHONPATH="" and can't import it, so the build fails. off and virtual are unaffected since they keep --ignore-installed

Possible fixed could be: keep --ignore-installed on the venv branch, pass unset_environ=["PYTHONPATH"] for the install subprocess, or run the install inside the build_env context.

Reproducer: https://gist.github.com/notatallshaw/7152cf2c16b9b46a8462d21c005ecc7c

@notatallshaw

Copy link
Copy Markdown
Member

You could consider vendoring or reimplementing it;

I support the courageous maintainer who wishes to do that 🙃.

@ichard26 ichard26 added the skip PR template check Silence the PR template check in CI label Jul 5, 2026
Otherwise, pip will think the build backend or the dynamic build
dependencies are already installed if available through an ambient
PYTHONPATH (which we normally patch away).
@ichard26

ichard26 commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

I set Claude on this tonight and cherry picked on something that seemed important:

This is just a pre-existing bug where we forgot to enter the build environment context, but it's worked around by --ignore-installed in the old isolation codepath. Fixed by 0ec17e1. I tried to come up with a test for this, but I couldn't think of a reasonable test because this logic is tied closely to the concrete distribution classes.

Perhaps, I could create the distribution object manually and test it that way...?

@ichard26

ichard26 commented Jul 6, 2026

Copy link
Copy Markdown
Member Author

I've spent some time digging through the issue tracker and found a handful of issues this will/should fix. Seeing it's more than one makes me feel better about how much of a pain this turns out to be. See the updated PR description.

@notatallshaw notatallshaw left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I ran multiple smoke tests tonight across Linux and Windows and generally looks good. No unknown regression and in fact a build that went from failing to working in my environment.

But if it's doable I do think we should make venv-isolation + inprocess-build-deps + build backends that shells out to a pure-Python console-script build work out the gate. I have a commit that seems to fix it: notatallshaw@c9c0d4e. I've smoke tested it also, but only very lightly reviewed it, and worked in all my tests.

@ichard26

ichard26 commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

from @geofft: I think you can play tricks on Windows specifically with setting the PYVENV_LAUNCHER environment variable (which gets unset at Python startup) to the expected pathname for the venv's interpreter, and invoking the real Python executable, without actually creating a file at that pathname.

You could consider vendoring or reimplementing [your own venv]

Getting standard virtual environments to work was already enough of a journey. I have zero desire to play more tricks or maintain yet another isolation mechanism. 🙂

But if it's doable I do think we should make venv-isolation + inprocess-build-deps + build backends that shells out to a pure-Python console-script build work out the gate. I have a commit that seems to fix it: notatallshaw@c9c0d4e. I've smoke tested it also, but only very lightly reviewed it, and worked in all my tests.

Damian and I talked out of band, but TL;DR, I'm okay with this change to support this use-case now, but I'm declaring combining venv-isolation and inprocess-build-deps as only partially supported. There is likely significant refactoring needed to support both features together properly. See points (2) and (3) in the PR description for more.

@ichard26

ichard26 commented Jul 7, 2026

Copy link
Copy Markdown
Member Author

I'm going to merge this since everyone has expressed support for this idea at some point in either the linked issue threads or on this PR explicitly, and I don't expect anyone else to have the capacity to review. This is experimental and will need to be out in the wild for at least one (or more?) release before we can promote it to stable and enable it by default. Before then, we can make adjustments as needed.

In the meanwhile, I'll follow up with affected users and try to get them to test this change.

@ichard26
ichard26 merged commit 5c9ec60 into pypa:main Jul 7, 2026
41 checks passed
@ichard26
ichard26 deleted the feat/venv-isolation branch July 7, 2026 23:56
@github-actions github-actions Bot locked as resolved and limited conversation to collaborators Jul 23, 2026
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.

Labels

bot:chronographer:provided skip PR template check Silence the PR template check in CI

Projects

None yet

3 participants