Skip to content

Commit 244b250

Browse files
committed
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 c93ca6f commit 244b250

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
@@ -64,6 +64,9 @@ class _DistArgs(typing.TypedDict, total=False):
6464

6565
INSTALLERS: tuple[Installer, ...] = typing.get_args(Installer)
6666

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

6871
class IsolatedEnv(typing.Protocol):
6972
"""Isolated build environment ABC."""
@@ -359,7 +362,7 @@ def create(self, path: str) -> None:
359362
with_pip = not self._has_valid_outer_pip
360363

361364
try:
362-
venv.EnvBuilder(symlinks=_fs_supports_symlink(), with_pip=with_pip).create(path)
365+
venv.EnvBuilder(symlinks=_USE_SYMLINKS, with_pip=with_pip).create(path)
363366
except subprocess.CalledProcessError as exc:
364367
_ctx.log_subprocess_error(exc)
365368
raise FailedProcessError(exc, 'Failed to create venv. Maybe try installing virtualenv.') from None
@@ -459,7 +462,7 @@ def create(self, path: str) -> None:
459462
_ctx.log(f'Using external uv from {uv_bin}')
460463
self._uv_bin = uv_bin
461464

462-
venv.EnvBuilder(symlinks=_fs_supports_symlink(), with_pip=False).create(self._env_path)
465+
venv.EnvBuilder(symlinks=_USE_SYMLINKS, with_pip=False).create(self._env_path)
463466
self.python_executable, self.scripts_dir, self.purelib = _find_executable_and_scripts(self._env_path)
464467

465468
def install_dependencies( # pragma: no cover -- uv tests are skipped on PyPy, covered on CPython
@@ -497,24 +500,6 @@ def display_name(self) -> str:
497500
return 'venv+uv'
498501

499502

500-
@functools.cache
501-
def _fs_supports_symlink() -> bool:
502-
"""Return True if symlinks are supported"""
503-
# Using definition used by venv.main()
504-
if os.name != 'nt':
505-
return True # pragma: win32 no cover
506-
507-
# Windows may support symlinks (setting in Windows 10)
508-
with tempfile.NamedTemporaryFile(prefix='build-symlink-') as tmp_file: # pragma: win32 cover
509-
dest = f'{tmp_file.name}-b'
510-
try:
511-
os.symlink(tmp_file.name, dest)
512-
os.unlink(dest)
513-
except (OSError, NotImplementedError, AttributeError):
514-
return False
515-
return True
516-
517-
518503
def _find_executable_and_scripts(path: str) -> tuple[str, str, str]:
519504
"""
520505
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)