Skip to content

Commit 0675975

Browse files
authored
gh-92897: Ensure venv --copies respects source build property of the creating interpreter (GH-92899)
1 parent e6ec6f5 commit 0675975

File tree

5 files changed

+76
-40
lines changed

5 files changed

+76
-40
lines changed

Lib/distutils/sysconfig.py

Lines changed: 1 addition & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,8 +29,6 @@
2929
parse_config_h as sysconfig_parse_config_h,
3030

3131
_init_non_posix,
32-
_is_python_source_dir,
33-
_sys_home,
3432

3533
_variable_rx,
3634
_findvar1_rx,
@@ -51,9 +49,6 @@
5149
# which might not be true in the time of import.
5250
_config_vars = get_config_vars()
5351

54-
if os.name == "nt":
55-
from sysconfig import _fix_pcbuild
56-
5752
warnings.warn(
5853
'The distutils.sysconfig module is deprecated, use sysconfig instead',
5954
DeprecationWarning,
@@ -286,7 +281,7 @@ def get_python_inc(plat_specific=0, prefix=None):
286281
# must use "srcdir" from the makefile to find the "Include"
287282
# directory.
288283
if plat_specific:
289-
return _sys_home or project_base
284+
return project_base
290285
else:
291286
incdir = os.path.join(get_config_var('srcdir'), 'Include')
292287
return os.path.normpath(incdir)

Lib/distutils/tests/test_sysconfig.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,11 @@ def test_srcdir(self):
6060
# should be a full source checkout.
6161
Python_h = os.path.join(srcdir, 'Include', 'Python.h')
6262
self.assertTrue(os.path.exists(Python_h), Python_h)
63-
self.assertTrue(sysconfig._is_python_source_dir(srcdir))
63+
# <srcdir>/PC/pyconfig.h always exists even if unused on POSIX.
64+
pyconfig_h = os.path.join(srcdir, 'PC', 'pyconfig.h')
65+
self.assertTrue(os.path.exists(pyconfig_h), pyconfig_h)
66+
pyconfig_h_in = os.path.join(srcdir, 'pyconfig.h.in')
67+
self.assertTrue(os.path.exists(pyconfig_h_in), pyconfig_h_in)
6468
elif os.name == 'posix':
6569
self.assertEqual(
6670
os.path.dirname(sysconfig.get_makefile_filename()),

Lib/sysconfig.py

Lines changed: 26 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -195,37 +195,38 @@ def _safe_realpath(path):
195195
# unable to retrieve the real program name
196196
_PROJECT_BASE = _safe_realpath(os.getcwd())
197197

198-
if (os.name == 'nt' and
199-
_PROJECT_BASE.lower().endswith(('\\pcbuild\\win32', '\\pcbuild\\amd64'))):
200-
_PROJECT_BASE = _safe_realpath(os.path.join(_PROJECT_BASE, pardir, pardir))
198+
# In a virtual environment, `sys._home` gives us the target directory
199+
# `_PROJECT_BASE` for the executable that created it when the virtual
200+
# python is an actual executable ('venv --copies' or Windows).
201+
_sys_home = getattr(sys, '_home', None)
202+
if _sys_home:
203+
_PROJECT_BASE = _sys_home
204+
205+
if os.name == 'nt':
206+
# In a source build, the executable is in a subdirectory of the root
207+
# that we want (<root>\PCbuild\<platname>).
208+
# `_BASE_PREFIX` is used as the base installation is where the source
209+
# will be. The realpath is needed to prevent mount point confusion
210+
# that can occur with just string comparisons.
211+
if _safe_realpath(_PROJECT_BASE).startswith(
212+
_safe_realpath(f'{_BASE_PREFIX}\\PCbuild')):
213+
_PROJECT_BASE = _BASE_PREFIX
201214

202215
# set for cross builds
203216
if "_PYTHON_PROJECT_BASE" in os.environ:
204217
_PROJECT_BASE = _safe_realpath(os.environ["_PYTHON_PROJECT_BASE"])
205218

206-
def _is_python_source_dir(d):
219+
def is_python_build(check_home=None):
220+
if check_home is not None:
221+
import warnings
222+
warnings.warn("check_home argument is deprecated and ignored.",
223+
DeprecationWarning, stacklevel=2)
207224
for fn in ("Setup", "Setup.local"):
208-
if os.path.isfile(os.path.join(d, "Modules", fn)):
225+
if os.path.isfile(os.path.join(_PROJECT_BASE, "Modules", fn)):
209226
return True
210227
return False
211228

212-
_sys_home = getattr(sys, '_home', None)
213-
214-
if os.name == 'nt':
215-
def _fix_pcbuild(d):
216-
if d and os.path.normcase(d).startswith(
217-
os.path.normcase(os.path.join(_PREFIX, "PCbuild"))):
218-
return _PREFIX
219-
return d
220-
_PROJECT_BASE = _fix_pcbuild(_PROJECT_BASE)
221-
_sys_home = _fix_pcbuild(_sys_home)
222-
223-
def is_python_build(check_home=False):
224-
if check_home and _sys_home:
225-
return _is_python_source_dir(_sys_home)
226-
return _is_python_source_dir(_PROJECT_BASE)
227-
228-
_PYTHON_BUILD = is_python_build(True)
229+
_PYTHON_BUILD = is_python_build()
229230

230231
if _PYTHON_BUILD:
231232
for scheme in ('posix_prefix', 'posix_home'):
@@ -442,7 +443,7 @@ def _parse_makefile(filename, vars=None, keep_unresolved=True):
442443
def get_makefile_filename():
443444
"""Return the path of the Makefile."""
444445
if _PYTHON_BUILD:
445-
return os.path.join(_sys_home or _PROJECT_BASE, "Makefile")
446+
return os.path.join(_PROJECT_BASE, "Makefile")
446447
if hasattr(sys, 'abiflags'):
447448
config_dir_name = f'config-{_PY_VERSION_SHORT}{sys.abiflags}'
448449
else:
@@ -587,9 +588,9 @@ def get_config_h_filename():
587588
"""Return the path of pyconfig.h."""
588589
if _PYTHON_BUILD:
589590
if os.name == "nt":
590-
inc_dir = os.path.join(_sys_home or _PROJECT_BASE, "PC")
591+
inc_dir = os.path.join(_PROJECT_BASE, "PC")
591592
else:
592-
inc_dir = _sys_home or _PROJECT_BASE
593+
inc_dir = _PROJECT_BASE
593594
else:
594595
inc_dir = get_path('platinclude')
595596
return os.path.join(inc_dir, 'pyconfig.h')

Lib/test/test_sysconfig.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,11 @@ def test_srcdir(self):
450450
# should be a full source checkout.
451451
Python_h = os.path.join(srcdir, 'Include', 'Python.h')
452452
self.assertTrue(os.path.exists(Python_h), Python_h)
453-
self.assertTrue(sysconfig._is_python_source_dir(srcdir))
453+
# <srcdir>/PC/pyconfig.h always exists even if unused on POSIX.
454+
pyconfig_h = os.path.join(srcdir, 'PC', 'pyconfig.h')
455+
self.assertTrue(os.path.exists(pyconfig_h), pyconfig_h)
456+
pyconfig_h_in = os.path.join(srcdir, 'pyconfig.h.in')
457+
self.assertTrue(os.path.exists(pyconfig_h_in), pyconfig_h_in)
454458
elif os.name == 'posix':
455459
makefile_dir = os.path.dirname(sysconfig.get_makefile_filename())
456460
# Issue #19340: srcdir has been realpath'ed already

Lib/test/test_venv.py

Lines changed: 39 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import struct
1616
import subprocess
1717
import sys
18+
import sysconfig
1819
import tempfile
1920
from test.support import (captured_stdout, captured_stderr,
2021
skip_if_broken_multiprocessing_synchronize, verbose,
@@ -254,18 +255,49 @@ def test_prefixes(self):
254255
self.assertEqual(out.strip(), expected.encode(), prefix)
255256

256257
@requireVenvCreate
257-
def test_sysconfig_preferred_and_default_scheme(self):
258+
def test_sysconfig(self):
258259
"""
259-
Test that the sysconfig preferred(prefix) and default scheme is venv.
260+
Test that the sysconfig functions work in a virtual environment.
260261
"""
261262
rmtree(self.env_dir)
262-
self.run_with_capture(venv.create, self.env_dir)
263+
self.run_with_capture(venv.create, self.env_dir, symlinks=False)
263264
envpy = os.path.join(self.env_dir, self.bindir, self.exe)
264265
cmd = [envpy, '-c', None]
265-
for call in ('get_preferred_scheme("prefix")', 'get_default_scheme()'):
266-
cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
267-
out, err = check_output(cmd)
268-
self.assertEqual(out.strip(), b'venv', err)
266+
for call, expected in (
267+
# installation scheme
268+
('get_preferred_scheme("prefix")', 'venv'),
269+
('get_default_scheme()', 'venv'),
270+
# build environment
271+
('is_python_build()', str(sysconfig.is_python_build())),
272+
('get_makefile_filename()', sysconfig.get_makefile_filename()),
273+
('get_config_h_filename()', sysconfig.get_config_h_filename())):
274+
with self.subTest(call):
275+
cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
276+
out, err = check_output(cmd)
277+
self.assertEqual(out.strip(), expected.encode(), err)
278+
279+
@requireVenvCreate
280+
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
281+
def test_sysconfig_symlinks(self):
282+
"""
283+
Test that the sysconfig functions work in a virtual environment.
284+
"""
285+
rmtree(self.env_dir)
286+
self.run_with_capture(venv.create, self.env_dir, symlinks=True)
287+
envpy = os.path.join(self.env_dir, self.bindir, self.exe)
288+
cmd = [envpy, '-c', None]
289+
for call, expected in (
290+
# installation scheme
291+
('get_preferred_scheme("prefix")', 'venv'),
292+
('get_default_scheme()', 'venv'),
293+
# build environment
294+
('is_python_build()', str(sysconfig.is_python_build())),
295+
('get_makefile_filename()', sysconfig.get_makefile_filename()),
296+
('get_config_h_filename()', sysconfig.get_config_h_filename())):
297+
with self.subTest(call):
298+
cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
299+
out, err = check_output(cmd)
300+
self.assertEqual(out.strip(), expected.encode(), err)
269301

270302
if sys.platform == 'win32':
271303
ENV_SUBDIRS = (

0 commit comments

Comments
 (0)