Skip to content

Commit 57eba3a

Browse files
zoobaned-deily
authored andcommitted
bpo-37369: Fix venv and test symlinking (GH-14456)
1 parent 3c34ea9 commit 57eba3a

File tree

4 files changed

+96
-47
lines changed

4 files changed

+96
-47
lines changed

Lib/test/test_platform.py

Lines changed: 18 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,14 +16,24 @@ def test_architecture(self):
1616

1717
@support.skip_unless_symlink
1818
def test_architecture_via_symlink(self): # issue3762
19+
if sys.platform == "win32" and not os.path.exists(sys.executable):
20+
# App symlink appears to not exist, but we want the
21+
# real executable here anyway
22+
import _winapi
23+
real = _winapi.GetModuleFileName(0)
24+
else:
25+
real = os.path.realpath(sys.executable)
26+
link = os.path.abspath(support.TESTFN)
27+
os.symlink(real, link)
28+
1929
# On Windows, the EXE needs to know where pythonXY.dll and *.pyd is at
2030
# so we add the directory to the path, PYTHONHOME and PYTHONPATH.
2131
env = None
2232
if sys.platform == "win32":
2333
env = {k.upper(): os.environ[k] for k in os.environ}
2434
env["PATH"] = "{};{}".format(
25-
os.path.dirname(sys.executable), env.get("PATH", ""))
26-
env["PYTHONHOME"] = os.path.dirname(sys.executable)
35+
os.path.dirname(real), env.get("PATH", ""))
36+
env["PYTHONHOME"] = os.path.dirname(real)
2737
if sysconfig.is_python_build(True):
2838
env["PYTHONPATH"] = os.path.dirname(os.__file__)
2939

@@ -40,11 +50,8 @@ def get(python, env=None):
4050
.format(p.returncode))
4151
return r
4252

43-
real = os.path.realpath(sys.executable)
44-
link = os.path.abspath(support.TESTFN)
45-
os.symlink(real, link)
4653
try:
47-
self.assertEqual(get(real), get(link, env=env))
54+
self.assertEqual(get(sys.executable), get(link, env=env))
4855
finally:
4956
os.remove(link)
5057

@@ -280,6 +287,11 @@ def test_libc_ver(self):
280287
os.path.exists(sys.executable+'.exe'):
281288
# Cygwin horror
282289
executable = sys.executable + '.exe'
290+
elif sys.platform == "win32" and not os.path.exists(sys.executable):
291+
# App symlink appears to not exist, but we want the
292+
# real executable here anyway
293+
import _winapi
294+
executable = _winapi.GetModuleFileName(0)
283295
else:
284296
executable = sys.executable
285297
res = platform.libc_ver(executable)

Lib/test/test_sysconfig.py

Lines changed: 12 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -233,16 +233,26 @@ def test_get_scheme_names(self):
233233

234234
@skip_unless_symlink
235235
def test_symlink(self):
236+
if sys.platform == "win32" and not os.path.exists(sys.executable):
237+
# App symlink appears to not exist, but we want the
238+
# real executable here anyway
239+
import _winapi
240+
real = _winapi.GetModuleFileName(0)
241+
else:
242+
real = os.path.realpath(sys.executable)
243+
link = os.path.abspath(TESTFN)
244+
os.symlink(real, link)
245+
236246
# On Windows, the EXE needs to know where pythonXY.dll is at so we have
237247
# to add the directory to the path.
238248
env = None
239249
if sys.platform == "win32":
240250
env = {k.upper(): os.environ[k] for k in os.environ}
241251
env["PATH"] = "{};{}".format(
242-
os.path.dirname(sys.executable), env.get("PATH", ""))
252+
os.path.dirname(real), env.get("PATH", ""))
243253
# Requires PYTHONHOME as well since we locate stdlib from the
244254
# EXE path and not the DLL path (which should be fixed)
245-
env["PYTHONHOME"] = os.path.dirname(sys.executable)
255+
env["PYTHONHOME"] = os.path.dirname(real)
246256
if sysconfig.is_python_build(True):
247257
env["PYTHONPATH"] = os.path.dirname(os.__file__)
248258

@@ -258,9 +268,6 @@ def get(python, env=None):
258268
self.fail('Non-zero return code {0} (0x{0:08X})'
259269
.format(p.returncode))
260270
return out, err
261-
real = os.path.realpath(sys.executable)
262-
link = os.path.abspath(TESTFN)
263-
os.symlink(real, link)
264271
try:
265272
self.assertEqual(get(real), get(link, env))
266273
finally:

Lib/test/test_venv.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,12 @@ def setUp(self):
5858
self.include = 'include'
5959
executable = getattr(sys, '_base_executable', sys.executable)
6060
self.exe = os.path.split(executable)[-1]
61+
if (sys.platform == 'win32'
62+
and os.path.lexists(executable)
63+
and not os.path.exists(executable)):
64+
self.cannot_link_exe = True
65+
else:
66+
self.cannot_link_exe = False
6167

6268
def tearDown(self):
6369
rmtree(self.env_dir)
@@ -248,7 +254,12 @@ def test_symlinking(self):
248254
# symlinked to 'python3.3' in the env, even when symlinking in
249255
# general isn't wanted.
250256
if usl:
251-
self.assertTrue(os.path.islink(fn))
257+
if self.cannot_link_exe:
258+
# Symlinking is skipped when our executable is already a
259+
# special app symlink
260+
self.assertFalse(os.path.islink(fn))
261+
else:
262+
self.assertTrue(os.path.islink(fn))
252263

253264
# If a venv is created from a source build and that venv is used to
254265
# run the test, the pyvenv.cfg in the venv created in the test will

Lib/venv/__init__.py

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -155,47 +155,66 @@ def create_configuration(self, context):
155155
f.write('include-system-site-packages = %s\n' % incl)
156156
f.write('version = %d.%d.%d\n' % sys.version_info[:3])
157157

158-
def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
159-
"""
160-
Try symlinking a file, and if that fails, fall back to copying.
161-
"""
162-
force_copy = not self.symlinks
163-
if not force_copy:
164-
try:
165-
if not os.path.islink(dst): # can't link to itself!
158+
if os.name != 'nt':
159+
def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
160+
"""
161+
Try symlinking a file, and if that fails, fall back to copying.
162+
"""
163+
force_copy = not self.symlinks
164+
if not force_copy:
165+
try:
166+
if not os.path.islink(dst): # can't link to itself!
167+
if relative_symlinks_ok:
168+
assert os.path.dirname(src) == os.path.dirname(dst)
169+
os.symlink(os.path.basename(src), dst)
170+
else:
171+
os.symlink(src, dst)
172+
except Exception: # may need to use a more specific exception
173+
logger.warning('Unable to symlink %r to %r', src, dst)
174+
force_copy = True
175+
if force_copy:
176+
shutil.copyfile(src, dst)
177+
else:
178+
def symlink_or_copy(self, src, dst, relative_symlinks_ok=False):
179+
"""
180+
Try symlinking a file, and if that fails, fall back to copying.
181+
"""
182+
bad_src = os.path.lexists(src) and not os.path.exists(src)
183+
if self.symlinks and not bad_src and not os.path.islink(dst):
184+
try:
166185
if relative_symlinks_ok:
167186
assert os.path.dirname(src) == os.path.dirname(dst)
168187
os.symlink(os.path.basename(src), dst)
169188
else:
170189
os.symlink(src, dst)
171-
except Exception: # may need to use a more specific exception
172-
logger.warning('Unable to symlink %r to %r', src, dst)
173-
force_copy = True
174-
if force_copy:
175-
if os.name == 'nt':
176-
# On Windows, we rewrite symlinks to our base python.exe into
177-
# copies of venvlauncher.exe
178-
basename, ext = os.path.splitext(os.path.basename(src))
179-
srcfn = os.path.join(os.path.dirname(__file__),
180-
"scripts",
181-
"nt",
182-
basename + ext)
183-
# Builds or venv's from builds need to remap source file
184-
# locations, as we do not put them into Lib/venv/scripts
185-
if sysconfig.is_python_build(True) or not os.path.isfile(srcfn):
186-
if basename.endswith('_d'):
187-
ext = '_d' + ext
188-
basename = basename[:-2]
189-
if basename == 'python':
190-
basename = 'venvlauncher'
191-
elif basename == 'pythonw':
192-
basename = 'venvwlauncher'
193-
src = os.path.join(os.path.dirname(src), basename + ext)
194-
else:
195-
src = srcfn
196-
if not os.path.exists(src):
197-
logger.warning('Unable to copy %r', src)
198190
return
191+
except Exception: # may need to use a more specific exception
192+
logger.warning('Unable to symlink %r to %r', src, dst)
193+
194+
# On Windows, we rewrite symlinks to our base python.exe into
195+
# copies of venvlauncher.exe
196+
basename, ext = os.path.splitext(os.path.basename(src))
197+
srcfn = os.path.join(os.path.dirname(__file__),
198+
"scripts",
199+
"nt",
200+
basename + ext)
201+
# Builds or venv's from builds need to remap source file
202+
# locations, as we do not put them into Lib/venv/scripts
203+
if sysconfig.is_python_build(True) or not os.path.isfile(srcfn):
204+
if basename.endswith('_d'):
205+
ext = '_d' + ext
206+
basename = basename[:-2]
207+
if basename == 'python':
208+
basename = 'venvlauncher'
209+
elif basename == 'pythonw':
210+
basename = 'venvwlauncher'
211+
src = os.path.join(os.path.dirname(src), basename + ext)
212+
else:
213+
src = srcfn
214+
if not os.path.exists(src):
215+
if not bad_src:
216+
logger.warning('Unable to copy %r', src)
217+
return
199218

200219
shutil.copyfile(src, dst)
201220

0 commit comments

Comments
 (0)