Skip to content

Commit 9969afc

Browse files
committed
fix: always use copies for the isolated venv on Windows
Version 1.6.0 fixed the symlink probe (#1118), so isolated environments on Windows started using symlinks whenever the filesystem allowed them. A symlinked python.exe cannot locate the DLLs of some interpreters, most visibly conda, which fails with "DLL load failed while importing _ssl". Every earlier release used copies in practice, and copies are the venv CLI default on Windows, so hardcode that instead of probing. Fixes #1175 Assisted-by: ClaudeCode:claude-fable-5-1
1 parent b0a276c commit 9969afc

3 files changed

Lines changed: 8 additions & 74 deletions

File tree

docs/changelog/1175.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Always create the isolated environment with copies instead of symlinks on Windows, matching the ``venv`` default.
2+
Version 1.6.0 started using symlinks when the filesystem allowed them, which broke conda interpreters with ``DLL load
3+
failed while importing _ssl`` / ``_ctypes`` errors - by :user:`henryiii` (:issue:`1175`)

src/build/env.py

Lines changed: 5 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -361,7 +361,7 @@ def create(self, path: str) -> None:
361361
with_pip = not self._has_valid_outer_pip
362362

363363
try:
364-
venv.EnvBuilder(symlinks=_fs_supports_symlink(), with_pip=with_pip).create(path)
364+
venv.EnvBuilder(symlinks=_USE_SYMLINKS, with_pip=with_pip).create(path)
365365
except subprocess.CalledProcessError as exc:
366366
_ctx.log_subprocess_error(exc)
367367
raise FailedProcessError(exc, 'Failed to create venv. Maybe try installing virtualenv.') from None
@@ -455,7 +455,7 @@ def create(self, path: str) -> None:
455455
_ctx.log(f'Using external uv from {uv_bin}')
456456
self._uv_bin = uv_bin
457457

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

461461
def install_dependencies( # pragma: no cover -- uv tests are skipped on PyPy, covered on CPython
@@ -486,22 +486,9 @@ def display_name(self) -> str:
486486
return 'venv+uv'
487487

488488

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
489+
# Match the ``venv`` CLI default. Symlinked interpreters on Windows can fail to
490+
# locate their DLLs (e.g. conda's ``_ssl``/``_ctypes``), see #1175.
491+
_USE_SYMLINKS = os.name != 'nt'
505492

506493

507494
def _find_executable_and_scripts(path: str) -> tuple[str, str, str]:

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)