Skip to content

Commit 5c6ef7b

Browse files
esafakgoogle-labs-jules[bot]pre-commit-ci[bot]
authored
fix: Improve symlink check and sysconfig path handling (#2914)
Co-authored-by: google-labs-jules[bot] <161369871+google-labs-jules[bot]@users.noreply.github.com> Co-authored-by: pre-commit-ci[bot] <66853113+pre-commit-ci[bot]@users.noreply.github.com>
1 parent 683e5db commit 5c6ef7b

File tree

5 files changed

+87
-14
lines changed

5 files changed

+87
-14
lines changed

docs/changelog/2786.bugfix.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
Creating a virtual environment on a filesystem without symlink-support would fail even with `--copies`
2+
Make `fs_supports_symlink` perform a real symlink creation check on all platforms.
3+
Contributed by :user:`esafak`.

src/virtualenv/create/via_global_ref/api.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,12 @@ def add_parser_arguments(cls, parser, interpreter, meta, app_data):
6262
help="give the virtual environment access to the system site-packages dir",
6363
)
6464
if not meta.can_symlink and not meta.can_copy:
65-
msg = "neither symlink or copy method supported"
65+
errors = []
66+
if meta.symlink_error:
67+
errors.append(f"symlink: {meta.symlink_error}")
68+
if meta.copy_error:
69+
errors.append(f"copy: {meta.copy_error}")
70+
msg = f"neither symlink or copy method supported: {', '.join(errors)}"
6671
raise RuntimeError(msg)
6772
group = parser.add_mutually_exclusive_group()
6873
if meta.can_symlink:

src/virtualenv/discovery/py_info.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -219,7 +219,9 @@ def is_venv(self):
219219
return self.base_prefix is not None
220220

221221
def sysconfig_path(self, key, config_var=None, sep=os.sep):
222-
pattern = self.sysconfig_paths[key]
222+
pattern = self.sysconfig_paths.get(key)
223+
if pattern is None:
224+
return ""
223225
if config_var is None:
224226
config_var = self.sysconfig_vars
225227
else:

src/virtualenv/info.py

Lines changed: 14 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -34,18 +34,20 @@ def fs_supports_symlink():
3434
if _CAN_SYMLINK is None:
3535
can = False
3636
if hasattr(os, "symlink"):
37-
if IS_WIN:
38-
with tempfile.NamedTemporaryFile(prefix="TmP") as tmp_file:
39-
temp_dir = os.path.dirname(tmp_file.name)
40-
dest = os.path.join(temp_dir, f"{tmp_file.name}-{'b'}")
41-
try:
42-
os.symlink(tmp_file.name, dest)
43-
can = True
44-
except (OSError, NotImplementedError):
45-
pass
46-
LOGGER.debug("symlink on filesystem does%s work", "" if can else " not")
47-
else:
48-
can = True
37+
# Creating a symlink can fail for a variety of reasons, indicating that the filesystem does not support it.
38+
# E.g. on Linux with a VFAT partition mounted.
39+
with tempfile.NamedTemporaryFile(prefix="TmP") as tmp_file:
40+
temp_dir = os.path.dirname(tmp_file.name)
41+
dest = os.path.join(temp_dir, f"{tmp_file.name}-{'b'}")
42+
try:
43+
os.symlink(tmp_file.name, dest)
44+
can = True
45+
except (OSError, NotImplementedError):
46+
pass # symlink is not supported
47+
finally:
48+
if os.path.lexists(dest):
49+
os.remove(dest)
50+
LOGGER.debug("symlink on filesystem does%s work", "" if can else " not")
4951
_CAN_SYMLINK = can
5052
return _CAN_SYMLINK
5153

tests/unit/create/test_creator.py

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from virtualenv.__main__ import run, run_with_catch
2626
from virtualenv.create.creator import DEBUG_SCRIPT, Creator, get_env_debug_info
2727
from virtualenv.create.pyenv_cfg import PyEnvCfg
28+
from virtualenv.create.via_global_ref import api
2829
from virtualenv.create.via_global_ref.builtin.cpython.common import is_macos_brew
2930
from virtualenv.create.via_global_ref.builtin.cpython.cpython3 import CPython3Posix
3031
from virtualenv.discovery.py_info import PythonInfo
@@ -690,3 +691,63 @@ def func(self):
690691

691692
cmd = [str(tmp_path), "--seeder", "app-data", "--without-pip", "--creator", "venv"]
692693
cli_run(cmd)
694+
695+
696+
def test_fallback_to_copies_if_symlink_unsupported(tmp_path, python, mocker):
697+
"""Test that creating a virtual environment falls back to copies when filesystem has no symlink support."""
698+
if is_macos_brew(PythonInfo.from_exe(python)):
699+
pytest.skip("brew python on darwin may not support copies, which is tested separately")
700+
701+
# Given a filesystem that does not support symlinks
702+
mocker.patch("virtualenv.create.via_global_ref.api.fs_supports_symlink", return_value=False)
703+
704+
# When creating a virtual environment (no method specified)
705+
cmd = [
706+
"-v",
707+
"-p",
708+
str(python),
709+
str(tmp_path),
710+
"--without-pip",
711+
"--activators",
712+
"",
713+
]
714+
result = cli_run(cmd)
715+
716+
# Then the creation should succeed and the creator should report it used copies
717+
assert result.creator is not None
718+
assert result.creator.symlinks is False
719+
720+
721+
def test_fail_gracefully_if_no_method_supported(tmp_path, python, mocker):
722+
"""Test that virtualenv fails gracefully when no creation method is supported."""
723+
# Given a filesystem that does not support symlinks
724+
mocker.patch("virtualenv.create.via_global_ref.api.fs_supports_symlink", return_value=False)
725+
726+
# And a creator that does not support copying
727+
if not is_macos_brew(PythonInfo.from_exe(python)):
728+
original_init = api.ViaGlobalRefMeta.__init__
729+
730+
def new_init(self, *args, **kwargs):
731+
original_init(self, *args, **kwargs)
732+
self.copy_error = "copying is not supported"
733+
734+
mocker.patch("virtualenv.create.via_global_ref.api.ViaGlobalRefMeta.__init__", new=new_init)
735+
736+
# When creating a virtual environment
737+
with pytest.raises(RuntimeError) as excinfo:
738+
cli_run(
739+
[
740+
"-p",
741+
str(python),
742+
str(tmp_path),
743+
"--without-pip",
744+
],
745+
)
746+
747+
# Then a RuntimeError should be raised with a detailed message
748+
assert "neither symlink or copy method supported" in str(excinfo.value)
749+
assert "symlink: the filesystem does not supports symlink" in str(excinfo.value)
750+
if is_macos_brew(PythonInfo.from_exe(python)):
751+
assert "copy: Brew disables copy creation" in str(excinfo.value)
752+
else:
753+
assert "copy: copying is not supported" in str(excinfo.value)

0 commit comments

Comments
 (0)