Skip to content

Commit 759455d

Browse files
authored
fix: always use copies for the isolated venv on Windows (#1176)
The broken probe fixed in #1118 (1.6.0, broken previously) wasn't actually supposed to be there; `python -m venv` doesn't check for symlink support. Even if symlinks are supported, python don't always work symlinked on Windows. This PR removes the check. Fixes #1175. :robot: _AI text below_ :robot: Version 1.6.0 fixed the symlink probe in #1118. Before that fix the probe always failed on Windows, so every isolated environment was built from copies. After the fix, GitHub Windows runners report symlink support, and `venv` started to symlink `python.exe`. A symlinked interpreter cannot find the DLLs of some Python builds, most visibly conda, and pip fails with `DLL load failed while importing _ssl` / `_ctypes`. Projects such as xgboost, LightGBM, and partcad have pinned `build<1.6` to work around this. This change removes the probe and hardcodes `symlinks=os.name != 'nt'`, which is the `venv` CLI default and the behavior all releases before 1.6.0 had in practice. The two tests for the probe are removed with it.
1 parent b0a276c commit 759455d

3 files changed

Lines changed: 6 additions & 76 deletions

File tree

docs/changelog/1175.bugfix.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Avoid trying to detect symlinks on Windows, regression in 1.6.0 - by :user:`henryiii` (:issue:`1175`)

src/build/env.py

Lines changed: 5 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,9 @@ class _DistArgs(typing.TypedDict, total=False):
6666

6767
INSTALLERS: tuple[Installer, ...] = typing.get_args(Installer)
6868

69+
# Match the ``venv`` CLI default. Symlinked interpreters on Windows can fail, see #1175.
70+
_USE_SYMLINKS = os.name != 'nt'
71+
6972

7073
class IsolatedEnv(typing.Protocol):
7174
"""Isolated build environment ABC."""
@@ -361,7 +364,7 @@ def create(self, path: str) -> None:
361364
with_pip = not self._has_valid_outer_pip
362365

363366
try:
364-
venv.EnvBuilder(symlinks=_fs_supports_symlink(), with_pip=with_pip).create(path)
367+
venv.EnvBuilder(symlinks=_USE_SYMLINKS, with_pip=with_pip).create(path)
365368
except subprocess.CalledProcessError as exc:
366369
_ctx.log_subprocess_error(exc)
367370
raise FailedProcessError(exc, 'Failed to create venv. Maybe try installing virtualenv.') from None
@@ -455,7 +458,7 @@ def create(self, path: str) -> None:
455458
_ctx.log(f'Using external uv from {uv_bin}')
456459
self._uv_bin = uv_bin
457460

458-
venv.EnvBuilder(symlinks=_fs_supports_symlink(), with_pip=False).create(self._env_path)
461+
venv.EnvBuilder(symlinks=_USE_SYMLINKS, with_pip=False).create(self._env_path)
459462
self.python_executable, self.scripts_dir, self.purelib = _find_executable_and_scripts(self._env_path)
460463

461464
def install_dependencies( # pragma: no cover -- uv tests are skipped on PyPy, covered on CPython
@@ -486,24 +489,6 @@ def display_name(self) -> str:
486489
return 'venv+uv'
487490

488491

489-
@functools.cache
490-
def _fs_supports_symlink() -> bool:
491-
"""Return True if symlinks are supported"""
492-
# Using definition used by venv.main()
493-
if os.name != 'nt':
494-
return True # pragma: win32 no cover
495-
496-
# Windows may support symlinks (setting in Windows 10)
497-
with tempfile.NamedTemporaryFile(prefix='build-symlink-') as tmp_file: # pragma: win32 cover
498-
dest = f'{tmp_file.name}-b'
499-
try:
500-
os.symlink(tmp_file.name, dest)
501-
os.unlink(dest)
502-
except (OSError, NotImplementedError, AttributeError):
503-
return False
504-
return True
505-
506-
507492
def _find_executable_and_scripts(path: str) -> tuple[str, str, str]:
508493
"""
509494
Detect the Python executable and script folder of a virtual environment.

tests/test_env.py

Lines changed: 0 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -212,62 +212,6 @@ def test_pip_needs_upgrade_mac_os_11(
212212
)
213213

214214

215-
@pytest.mark.parametrize('has_symlink', [True, False] if sys.platform.startswith('win') else [True])
216-
def test_venv_symlink(
217-
mocker: pytest_mock.MockerFixture,
218-
has_symlink: bool,
219-
) -> None:
220-
if has_symlink:
221-
mocker.patch('os.symlink')
222-
mocker.patch('os.unlink')
223-
else: # pragma: win32 cover
224-
mocker.patch('os.symlink', side_effect=OSError())
225-
226-
# Cache must be cleared to rerun
227-
build.env._fs_supports_symlink.cache_clear()
228-
supports_symlink = build.env._fs_supports_symlink()
229-
build.env._fs_supports_symlink.cache_clear()
230-
231-
assert supports_symlink is has_symlink
232-
233-
234-
def test_fs_supports_symlink_windows_dest_is_tmp_file_path(
235-
mocker: pytest_mock.MockerFixture,
236-
) -> None:
237-
"""Regression test for the Windows-only branch of ``_fs_supports_symlink``.
238-
239-
``dest`` must be built from ``tmp_file.name``, not from the ``NamedTemporaryFile`` object itself: interpolating the
240-
object yields its repr (containing ``<`` and ``>``, which are invalid in Windows filenames), so ``os.symlink`` would
241-
always fail and the function would always report symlinks as unsupported, even when Windows Developer Mode enables
242-
them.
243-
244-
"""
245-
mocker.patch('os.name', 'nt')
246-
247-
# Avoid exercising the real tempfile module: on some platforms tempfile's own
248-
# internals branch on os.name, which we're patching to 'nt' above.
249-
fake_tmp_file = mocker.MagicMock()
250-
fake_tmp_file.name = r'C:\Users\test\AppData\Local\Temp\build-symlink-abc123'
251-
fake_tmp_file.__enter__.return_value = fake_tmp_file
252-
fake_tmp_file.__exit__.return_value = False
253-
mocker.patch('build.env.tempfile.NamedTemporaryFile', return_value=fake_tmp_file)
254-
255-
recorded_dest = {}
256-
257-
def fake_symlink(_src: str, dst: str) -> None:
258-
recorded_dest['dst'] = dst
259-
260-
mocker.patch('os.symlink', side_effect=fake_symlink)
261-
mocker.patch('os.unlink')
262-
263-
build.env._fs_supports_symlink.cache_clear()
264-
supports_symlink = build.env._fs_supports_symlink()
265-
build.env._fs_supports_symlink.cache_clear()
266-
267-
assert supports_symlink is True
268-
assert recorded_dest['dst'] == f'{fake_tmp_file.name}-b'
269-
270-
271215
def test_install_short_circuits(
272216
mocker: pytest_mock.MockerFixture,
273217
) -> None:

0 commit comments

Comments
 (0)