Skip to content

gh-106045: Fix venv creation from a python executable symlink #115237

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 43 additions & 0 deletions Lib/test/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -892,6 +892,49 @@ def test_venvwlauncher(self):
except subprocess.CalledProcessError:
self.fail("venvwlauncher.exe did not run %s" % exename)

@requires_subprocess()
@unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
@unittest.skipUnless(sysconfig.get_config_var('HAVE_READLINK'), "Requires HAVE_READLINK support")
def test_executable_symlink(self):
"""
Test creation using a symlink to python executable.
"""
rmtree(self.env_dir)
exe = pathlib.Path(sys.executable).absolute()
with tempfile.TemporaryDirectory() as tmp_dir:
symlink_dir = pathlib.Path(tmp_dir).resolve(strict=True)
exe_symlink = symlink_dir / exe.name
exe_symlink.symlink_to(exe)
cmd = [exe_symlink, "-m", "venv", "--without-pip", self.env_dir]
subprocess.check_call(cmd)
data = self.get_text_file_contents('pyvenv.cfg')
path = os.path.dirname(os.path.abspath(sys._base_executable))
self.assertIn('home = %s' % path, data)
self.assertIn('executable = %s' % exe.resolve(), data)

@requires_subprocess()
@unittest.skipIf(os.name == 'nt', 'not relevant on Windows')
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
@requireVenvCreate
def test_tree_symlink(self):
"""
Test creation using a symlink to python tree.
"""
rmtree(self.env_dir)
exe = pathlib.Path(sys._base_executable).absolute()
tree = exe.parent.parent
with tempfile.TemporaryDirectory() as tmp_dir:
symlink_dir = pathlib.Path(tmp_dir).resolve(strict=True)
tree_symlink = symlink_dir / tree.name
exe_symlink = tree_symlink / exe.relative_to(tree)
tree_symlink.symlink_to(tree)
cmd = [exe_symlink, "-m", "venv", "--without-pip", self.env_dir]
subprocess.check_call(cmd)
data = self.get_text_file_contents('pyvenv.cfg')
self.assertIn('home = %s' % tree_symlink, data)
self.assertIn('executable = %s' % exe.resolve(), data)


@requireVenvCreate
class EnsurePipTest(BaseTest):
Expand Down
37 changes: 36 additions & 1 deletion Lib/venv/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,40 @@ def _same_path(cls, path1, path2):
else:
return path1 == path2

@classmethod
def _getpath_realpath(cls, path):
"""Mimics getpath.realpath

It only mimics it for HAVE_READLINK.
There are a few differences listed here:
- we ensure that we have a resolvable abspath first
(i.e. exists and no symlink loop)
- we stop if a candidate does not resolve to the same file
(this can happen with normpath)
"""
result = os.path.abspath(path)
Copy link
Author

Choose a reason for hiding this comment

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

os.path.abspath is done first as was used before. It does normalize the path which may end-up with an invalid path in case of symlinks (e.g. "symlink_dir/../segment").

try:
real_path = os.path.realpath(result, strict=True)
except OSError:
logger.warning('Unable to resolve %r real path', result)
return result
if sysconfig.get_config_var('HAVE_READLINK'):
while os.path.islink(result):
link = os.readlink(result)
if os.path.isabs(link):
candidate = link
else:
candidate = os.path.join(os.path.dirname(result), link)
candidate = os.path.normpath(candidate)
# shall exists and be the same file as the original one
valid = os.path.exists(candidate) and os.path.samefile(real_path, candidate)
if not valid:
logger.warning('Stopped resolving %r because %r is not the same file',
result, candidate)
break
result = candidate
return result

def ensure_directories(self, env_dir):
"""
Create the directories for the environment.
Expand Down Expand Up @@ -163,7 +197,8 @@ def create_if_needed(d):
'Python interpreter. Provide an explicit path or '
'check that your PATH environment variable is '
'correctly set.')
dirname, exename = os.path.split(os.path.abspath(executable))
# only resolve executable symlinks, not the full chain, see gh-106045
dirname, exename = os.path.split(self._getpath_realpath(executable))
if sys.platform == 'win32':
# Always create the simplest name in the venv. It will either be a
# link back to executable, or a copy of the appropriate launcher
Expand Down
1 change: 1 addition & 0 deletions Misc/ACKS
Original file line number Diff line number Diff line change
Expand Up @@ -419,6 +419,7 @@ Eric Daniel
Scott David Daniels
Derzsi Dániel
Lawrence D'Anna
Matthieu Darbois
Ben Darnell
Kushal Das
Jonathan Dasteel
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
Fix ``venv`` creation from a python executable symlink. Patch by Matthieu
Darbois.
Loading