Skip to content

Commit a1f9a33

Browse files
authored
bpo-35854: Fix EnvBuilder and --symlinks in venv on Windows (GH-11700)
1 parent 40ebe94 commit a1f9a33

File tree

5 files changed

+63
-27
lines changed

5 files changed

+63
-27
lines changed

Doc/library/venv.rst

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -109,8 +109,7 @@ creation according to their needs, the :class:`EnvBuilder` class.
109109
any existing target directory, before creating the environment.
110110

111111
* ``symlinks`` -- a Boolean value indicating whether to attempt to symlink the
112-
Python binary (and any necessary DLLs or other binaries,
113-
e.g. ``pythonw.exe``), rather than copying.
112+
Python binary rather than copying.
114113

115114
* ``upgrade`` -- a Boolean value which, if true, will upgrade an existing
116115
environment with the running Python - for use when that Python has been
@@ -176,15 +175,15 @@ creation according to their needs, the :class:`EnvBuilder` class.
176175

177176
.. method:: setup_python(context)
178177

179-
Creates a copy of the Python executable in the environment on POSIX
180-
systems. If a specific executable ``python3.x`` was used, symlinks to
181-
``python`` and ``python3`` will be created pointing to that executable,
182-
unless files with those names already exist.
178+
Creates a copy or symlink to the Python executable in the environment.
179+
On POSIX systems, if a specific executable ``python3.x`` was used,
180+
symlinks to ``python`` and ``python3`` will be created pointing to that
181+
executable, unless files with those names already exist.
183182

184183
.. method:: setup_scripts(context)
185184

186185
Installs activation scripts appropriate to the platform into the virtual
187-
environment. On Windows, also installs the ``python[w].exe`` scripts.
186+
environment.
188187

189188
.. method:: post_setup(context)
190189

@@ -194,8 +193,13 @@ creation according to their needs, the :class:`EnvBuilder` class.
194193

195194
.. versionchanged:: 3.7.2
196195
Windows now uses redirector scripts for ``python[w].exe`` instead of
197-
copying the actual binaries, and so :meth:`setup_python` does nothing
198-
unless running from a build in the source tree.
196+
copying the actual binaries. In 3.7.2 only :meth:`setup_python` does
197+
nothing unless running from a build in the source tree.
198+
199+
.. versionchanged:: 3.7.3
200+
Windows copies the redirector scripts as part of :meth:`setup_python`
201+
instead of :meth:`setup_scripts`. This was not the case in 3.7.2.
202+
When using symlinks, the original executables will be linked.
199203

200204
In addition, :class:`EnvBuilder` provides this utility method that can be
201205
called from :meth:`setup_scripts` or :meth:`post_setup` in subclasses to

Doc/using/venv-create.inc

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -70,6 +70,11 @@ The command, if run with ``-h``, will show the available options::
7070
In earlier versions, if the target directory already existed, an error was
7171
raised, unless the ``--clear`` or ``--upgrade`` option was provided.
7272
73+
.. note::
74+
While symlinks are supported on Windows, they are not recommended. Of
75+
particular note is that double-clicking ``python.exe`` in File Explorer
76+
will resolve the symlink eagerly and ignore the virtual environment.
77+
7378
The created ``pyvenv.cfg`` file also includes the
7479
``include-system-site-packages`` key, set to ``true`` if ``venv`` is
7580
run with the ``--system-site-packages`` option, ``false`` otherwise.

Lib/test/test_venv.py

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,6 @@ def test_isolation(self):
243243
self.assertIn('include-system-site-packages = %s\n' % s, data)
244244

245245
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
246-
@unittest.skipIf(os.name == 'nt', 'Symlinks are never used on Windows')
247246
def test_symlinking(self):
248247
"""
249248
Test symlinking works as expected

Lib/venv/__init__.py

Lines changed: 44 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -64,11 +64,10 @@ def create(self, env_dir):
6464
self.system_site_packages = False
6565
self.create_configuration(context)
6666
self.setup_python(context)
67-
if not self.upgrade:
68-
self.setup_scripts(context)
6967
if self.with_pip:
7068
self._setup_pip(context)
7169
if not self.upgrade:
70+
self.setup_scripts(context)
7271
self.post_setup(context)
7372
if true_system_site_packages:
7473
# We had set it to False before, now
@@ -176,6 +175,23 @@ def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
176175
logger.warning('Unable to symlink %r to %r', src, dst)
177176
force_copy = True
178177
if force_copy:
178+
if os.name == 'nt':
179+
# On Windows, we rewrite symlinks to our base python.exe into
180+
# copies of venvlauncher.exe
181+
basename, ext = os.path.splitext(os.path.basename(src))
182+
if basename.endswith('_d'):
183+
ext = '_d' + ext
184+
basename = basename[:-2]
185+
if sysconfig.is_python_build(True):
186+
if basename == 'python':
187+
basename = 'venvlauncher'
188+
elif basename == 'pythonw':
189+
basename = 'venvwlauncher'
190+
scripts = os.path.dirname(src)
191+
else:
192+
scripts = os.path.join(os.path.dirname(__file__), "scripts", "nt")
193+
src = os.path.join(scripts, basename + ext)
194+
179195
shutil.copyfile(src, dst)
180196

181197
def setup_python(self, context):
@@ -202,23 +218,31 @@ def setup_python(self, context):
202218
if not os.path.islink(path):
203219
os.chmod(path, 0o755)
204220
else:
205-
# For normal cases, the venvlauncher will be copied from
206-
# our scripts folder. For builds, we need to copy it
207-
# manually.
208-
if sysconfig.is_python_build(True):
209-
suffix = '.exe'
210-
if context.python_exe.lower().endswith('_d.exe'):
211-
suffix = '_d.exe'
212-
213-
src = os.path.join(dirname, "venvlauncher" + suffix)
214-
dst = os.path.join(binpath, context.python_exe)
215-
copier(src, dst)
221+
if self.symlinks:
222+
# For symlinking, we need a complete copy of the root directory
223+
# If symlinks fail, you'll get unnecessary copies of files, but
224+
# we assume that if you've opted into symlinks on Windows then
225+
# you know what you're doing.
226+
suffixes = [
227+
f for f in os.listdir(dirname) if
228+
os.path.normcase(os.path.splitext(f)[1]) in ('.exe', '.dll')
229+
]
230+
if sysconfig.is_python_build(True):
231+
suffixes = [
232+
f for f in suffixes if
233+
os.path.normcase(f).startswith(('python', 'vcruntime'))
234+
]
235+
else:
236+
suffixes = ['python.exe', 'python_d.exe', 'pythonw.exe',
237+
'pythonw_d.exe']
216238

217-
src = os.path.join(dirname, "venvwlauncher" + suffix)
218-
dst = os.path.join(binpath, "pythonw" + suffix)
219-
copier(src, dst)
239+
for suffix in suffixes:
240+
src = os.path.join(dirname, suffix)
241+
if os.path.exists(src):
242+
copier(src, os.path.join(binpath, suffix))
220243

221-
# copy init.tcl over
244+
if sysconfig.is_python_build(True):
245+
# copy init.tcl
222246
for root, dirs, files in os.walk(context.python_dir):
223247
if 'init.tcl' in files:
224248
tcldir = os.path.basename(root)
@@ -304,6 +328,9 @@ def install_scripts(self, context, path):
304328
dirs.remove(d)
305329
continue # ignore files in top level
306330
for f in files:
331+
if (os.name == 'nt' and f.startswith('python')
332+
and f.endswith(('.exe', '.pdb'))):
333+
continue
307334
srcfile = os.path.join(root, f)
308335
suffix = root[plen:].split(os.sep)[2:]
309336
if not suffix:
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Fix EnvBuilder and --symlinks in venv on Windows

0 commit comments

Comments
 (0)