Skip to content

gh-92897: Ensure venv --copies respects source build property of the creating interpreter (GH-92899) #92899

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

Merged
merged 2 commits into from
Jul 5, 2022
Merged
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
7 changes: 1 addition & 6 deletions Lib/distutils/sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,6 @@
parse_config_h as sysconfig_parse_config_h,

_init_non_posix,
_is_python_source_dir,
_sys_home,

_variable_rx,
_findvar1_rx,
Expand All @@ -52,9 +50,6 @@
# which might not be true in the time of import.
_config_vars = get_config_vars()

if os.name == "nt":
from sysconfig import _fix_pcbuild

warnings.warn(
'The distutils.sysconfig module is deprecated, use sysconfig instead',
DeprecationWarning,
Expand Down Expand Up @@ -287,7 +282,7 @@ def get_python_inc(plat_specific=0, prefix=None):
# must use "srcdir" from the makefile to find the "Include"
# directory.
if plat_specific:
return _sys_home or project_base
return project_base
else:
incdir = os.path.join(get_config_var('srcdir'), 'Include')
return os.path.normpath(incdir)
Expand Down
6 changes: 5 additions & 1 deletion Lib/distutils/tests/test_sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,11 @@ def test_srcdir(self):
# should be a full source checkout.
Python_h = os.path.join(srcdir, 'Include', 'Python.h')
self.assertTrue(os.path.exists(Python_h), Python_h)
self.assertTrue(sysconfig._is_python_source_dir(srcdir))
# <srcdir>/PC/pyconfig.h always exists even if unused on POSIX.
pyconfig_h = os.path.join(srcdir, 'PC', 'pyconfig.h')
self.assertTrue(os.path.exists(pyconfig_h), pyconfig_h)
pyconfig_h_in = os.path.join(srcdir, 'pyconfig.h.in')
self.assertTrue(os.path.exists(pyconfig_h_in), pyconfig_h_in)
elif os.name == 'posix':
self.assertEqual(
os.path.dirname(sysconfig.get_makefile_filename()),
Expand Down
51 changes: 26 additions & 25 deletions Lib/sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -195,37 +195,38 @@ def _safe_realpath(path):
# unable to retrieve the real program name
_PROJECT_BASE = _safe_realpath(os.getcwd())

if (os.name == 'nt' and
_PROJECT_BASE.lower().endswith(('\\pcbuild\\win32', '\\pcbuild\\amd64'))):
_PROJECT_BASE = _safe_realpath(os.path.join(_PROJECT_BASE, pardir, pardir))
# In a virtual environment, `sys._home` gives us the target directory
# `_PROJECT_BASE` for the executable that created it when the virtual
# python is an actual executable ('venv --copies' or Windows).
_sys_home = getattr(sys, '_home', None)
if _sys_home:
_PROJECT_BASE = _sys_home

if os.name == 'nt':
# In a source build, the executable is in a subdirectory of the root
# that we want (<root>\PCbuild\<platname>).
# `_BASE_PREFIX` is used as the base installation is where the source
# will be. The realpath is needed to prevent mount point confusion
# that can occur with just string comparisons.
if _safe_realpath(_PROJECT_BASE).startswith(
_safe_realpath(f'{_BASE_PREFIX}\\PCbuild')):
_PROJECT_BASE = _BASE_PREFIX

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

def _is_python_source_dir(d):
def is_python_build(check_home=None):
if check_home is not None:
import warnings
warnings.warn("check_home argument is deprecated and ignored.",
DeprecationWarning, stacklevel=2)
for fn in ("Setup", "Setup.local"):
if os.path.isfile(os.path.join(d, "Modules", fn)):
if os.path.isfile(os.path.join(_PROJECT_BASE, "Modules", fn)):
return True
return False

_sys_home = getattr(sys, '_home', None)

if os.name == 'nt':
def _fix_pcbuild(d):
if d and os.path.normcase(d).startswith(
os.path.normcase(os.path.join(_PREFIX, "PCbuild"))):
return _PREFIX
return d
_PROJECT_BASE = _fix_pcbuild(_PROJECT_BASE)
_sys_home = _fix_pcbuild(_sys_home)

def is_python_build(check_home=False):
if check_home and _sys_home:
return _is_python_source_dir(_sys_home)
return _is_python_source_dir(_PROJECT_BASE)

_PYTHON_BUILD = is_python_build(True)
_PYTHON_BUILD = is_python_build()

if _PYTHON_BUILD:
for scheme in ('posix_prefix', 'posix_home'):
Expand Down Expand Up @@ -442,7 +443,7 @@ def _parse_makefile(filename, vars=None, keep_unresolved=True):
def get_makefile_filename():
"""Return the path of the Makefile."""
if _PYTHON_BUILD:
return os.path.join(_sys_home or _PROJECT_BASE, "Makefile")
return os.path.join(_PROJECT_BASE, "Makefile")
if hasattr(sys, 'abiflags'):
config_dir_name = f'config-{_PY_VERSION_SHORT}{sys.abiflags}'
else:
Expand Down Expand Up @@ -587,9 +588,9 @@ def get_config_h_filename():
"""Return the path of pyconfig.h."""
if _PYTHON_BUILD:
if os.name == "nt":
inc_dir = os.path.join(_sys_home or _PROJECT_BASE, "PC")
inc_dir = os.path.join(_PROJECT_BASE, "PC")
else:
inc_dir = _sys_home or _PROJECT_BASE
inc_dir = _PROJECT_BASE
else:
inc_dir = get_path('platinclude')
return os.path.join(inc_dir, 'pyconfig.h')
Expand Down
6 changes: 5 additions & 1 deletion Lib/test/test_sysconfig.py
Original file line number Diff line number Diff line change
Expand Up @@ -448,7 +448,11 @@ def test_srcdir(self):
# should be a full source checkout.
Python_h = os.path.join(srcdir, 'Include', 'Python.h')
self.assertTrue(os.path.exists(Python_h), Python_h)
self.assertTrue(sysconfig._is_python_source_dir(srcdir))
# <srcdir>/PC/pyconfig.h always exists even if unused on POSIX.
pyconfig_h = os.path.join(srcdir, 'PC', 'pyconfig.h')
self.assertTrue(os.path.exists(pyconfig_h), pyconfig_h)
pyconfig_h_in = os.path.join(srcdir, 'pyconfig.h.in')
self.assertTrue(os.path.exists(pyconfig_h_in), pyconfig_h_in)
elif os.name == 'posix':
makefile_dir = os.path.dirname(sysconfig.get_makefile_filename())
# Issue #19340: srcdir has been realpath'ed already
Expand Down
46 changes: 39 additions & 7 deletions Lib/test/test_venv.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
import struct
import subprocess
import sys
import sysconfig
import tempfile
from test.support import (captured_stdout, captured_stderr, requires_zlib,
skip_if_broken_multiprocessing_synchronize, verbose,
Expand Down Expand Up @@ -240,18 +241,49 @@ def test_prefixes(self):
self.assertEqual(out.strip(), expected.encode(), prefix)

@requireVenvCreate
def test_sysconfig_preferred_and_default_scheme(self):
def test_sysconfig(self):
"""
Test that the sysconfig preferred(prefix) and default scheme is venv.
Test that the sysconfig functions work in a virtual environment.
"""
rmtree(self.env_dir)
self.run_with_capture(venv.create, self.env_dir)
self.run_with_capture(venv.create, self.env_dir, symlinks=False)
envpy = os.path.join(self.env_dir, self.bindir, self.exe)
cmd = [envpy, '-c', None]
for call in ('get_preferred_scheme("prefix")', 'get_default_scheme()'):
cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
out, err = check_output(cmd)
self.assertEqual(out.strip(), b'venv', err)
for call, expected in (
# installation scheme
('get_preferred_scheme("prefix")', 'venv'),
('get_default_scheme()', 'venv'),
# build environment
('is_python_build()', str(sysconfig.is_python_build())),
('get_makefile_filename()', sysconfig.get_makefile_filename()),
('get_config_h_filename()', sysconfig.get_config_h_filename())):
with self.subTest(call):
cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
out, err = check_output(cmd)
self.assertEqual(out.strip(), expected.encode(), err)

@requireVenvCreate
@unittest.skipUnless(can_symlink(), 'Needs symlinks')
def test_sysconfig_symlinks(self):
"""
Test that the sysconfig functions work in a virtual environment.
"""
rmtree(self.env_dir)
self.run_with_capture(venv.create, self.env_dir, symlinks=True)
envpy = os.path.join(self.env_dir, self.bindir, self.exe)
cmd = [envpy, '-c', None]
for call, expected in (
# installation scheme
('get_preferred_scheme("prefix")', 'venv'),
('get_default_scheme()', 'venv'),
# build environment
('is_python_build()', str(sysconfig.is_python_build())),
('get_makefile_filename()', sysconfig.get_makefile_filename()),
('get_config_h_filename()', sysconfig.get_config_h_filename())):
with self.subTest(call):
cmd[2] = 'import sysconfig; print(sysconfig.%s)' % call
out, err = check_output(cmd)
self.assertEqual(out.strip(), expected.encode(), err)

if sys.platform == 'win32':
ENV_SUBDIRS = (
Expand Down