diff --git a/Lib/test/test_venv.py b/Lib/test/test_venv.py index 0b09010c69d4ea..cbd8e89beb5d14 100644 --- a/Lib/test/test_venv.py +++ b/Lib/test/test_venv.py @@ -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): diff --git a/Lib/venv/__init__.py b/Lib/venv/__init__.py index dc4c9ef3531991..9a153040316477 100644 --- a/Lib/venv/__init__.py +++ b/Lib/venv/__init__.py @@ -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) + 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. @@ -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 diff --git a/Misc/ACKS b/Misc/ACKS index deda334bee7417..5767d0c4452fe1 100644 --- a/Misc/ACKS +++ b/Misc/ACKS @@ -419,6 +419,7 @@ Eric Daniel Scott David Daniels Derzsi Dániel Lawrence D'Anna +Matthieu Darbois Ben Darnell Kushal Das Jonathan Dasteel diff --git a/Misc/NEWS.d/next/Library/2024-02-10-11-41-42.gh-issue-106045.eVZFt2.rst b/Misc/NEWS.d/next/Library/2024-02-10-11-41-42.gh-issue-106045.eVZFt2.rst new file mode 100644 index 00000000000000..1a3292976bc9d3 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2024-02-10-11-41-42.gh-issue-106045.eVZFt2.rst @@ -0,0 +1,2 @@ +Fix ``venv`` creation from a python executable symlink. Patch by Matthieu +Darbois.