diff --git a/Doc/library/site.rst b/Doc/library/site.rst
index e2ad3c48f9754e..6962fdd0b3e364 100644
--- a/Doc/library/site.rst
+++ b/Doc/library/site.rst
@@ -32,7 +32,9 @@ It starts by constructing up to four directories from a head and a tail part.
For the head part, it uses ``sys.prefix`` and ``sys.exec_prefix``; empty heads
are skipped. For the tail part, it uses the empty string and then
:file:`lib/site-packages` (on Windows) or
-:file:`lib/python{X.Y}/site-packages` (on Unix and macOS). For each
+:file:`lib/python{X.Y}/site-packages` (on Unix and macOS), and finally
+the ``purelib`` and ``platlib`` paths for each scheme specified in the
+``EXTRA_SITE_INSTALL_SCHEMES`` list variable of the vendor config. For each
of the distinct head-tail combinations, it sees if it refers to an existing
directory, and if so, adds it to ``sys.path`` and also inspects the newly
added path for configuration files.
@@ -40,6 +42,10 @@ added path for configuration files.
.. versionchanged:: 3.5
Support for the "site-python" directory has been removed.
+.. versionchanged:: 3.11
+ Extra site install schemes specified in the vendor config
+ (``--with-vendor-config`` configure option) will also be loaded.
+
If a file named "pyvenv.cfg" exists one directory above sys.executable,
sys.prefix and sys.exec_prefix are set to that directory and
it is also checked for site-packages (sys.base_prefix and
diff --git a/Doc/library/sysconfig.rst b/Doc/library/sysconfig.rst
index 6327318eb108da..67b09e22395828 100644
--- a/Doc/library/sysconfig.rst
+++ b/Doc/library/sysconfig.rst
@@ -85,6 +85,10 @@ Python currently supports seven schemes:
- *nt*: scheme for NT platforms like Windows.
- *nt_user*: scheme for NT platforms, when the *user* option is used.
+Additionally to these, Python also supports vendor schemes specified in the
+``EXTRA_INSTALL_SCHEMES`` dictionary variable of the vendor config
+(``--with-vendor-config`` configure option).
+
Each scheme is itself composed of a series of paths and each path has a unique
identifier. Python currently uses eight paths:
@@ -129,21 +133,6 @@ identifier. Python currently uses eight paths:
.. versionadded:: 3.10
-.. function:: _get_preferred_schemes()
-
- Return a dict containing preferred scheme names on the current platform.
- Python implementers and redistributors may add their preferred schemes to
- the ``_INSTALL_SCHEMES`` module-level global value, and modify this function
- to return those scheme names, to e.g. provide different schemes for system
- and language package managers to use, so packages installed by either do not
- mix with those by the other.
-
- End users should not use this function, but :func:`get_default_scheme` and
- :func:`get_preferred_scheme()` instead.
-
- .. versionadded:: 3.10
-
-
.. function:: get_path_names()
Return a tuple containing all path names currently supported in
diff --git a/Doc/using/configure.rst b/Doc/using/configure.rst
index 75f572c61877fe..aef768a93e31a1 100644
--- a/Doc/using/configure.rst
+++ b/Doc/using/configure.rst
@@ -116,6 +116,32 @@ General Options
.. versionadded:: 3.10
+.. cmdoption:: --with-vendor-config=config.py
+
+ Path to the vendor config (none by default).
+
+ The vendor config is a Python file that allows configuring some aspects of
+ the Python distribution.
+
+ A ``EXTRA_INSTALL_SCHEMES`` dictionary variable can be specified in the
+ config to add extra install schemes. These schemes will be picked up by the
+ :mod:`sysconfig` module.
+
+ A ``EXTRA_SITE_INSTALL_SCHEMES`` list variable can be specified in the config
+ to add extra schemes to the :mod:`site` module initialization. This options
+ allow Python distributors to define custom locations to use for their Python
+ packages.
+
+ A ``get_preferred_schemes()`` function can be specified in the config. This
+ function should return a dict containing preferred scheme names on the
+ current platform. Python implementers and redistributors may add their
+ preferred schemes to the ``EXTRA_INSTALL_SCHEMES`` vendor config variable,
+ and modify this function to return those scheme names, to e.g. provide
+ different schemes for system and language package managers to use, so
+ packages installed by either do not mix with those by the other.
+
+ .. versionadded:: 3.11
+
Install Options
---------------
diff --git a/Lib/_sysconfig.py b/Lib/_sysconfig.py
new file mode 100644
index 00000000000000..f066849e155216
--- /dev/null
+++ b/Lib/_sysconfig.py
@@ -0,0 +1,378 @@
+"""Bits of sysconfig that are required at startup (by the site module)"""
+
+import os
+import sys
+
+
+# helpers
+
+
+def _safe_realpath(path):
+ try:
+ return os.path.realpath(path)
+ except OSError:
+ return path
+
+
+def _is_python_source_dir(d):
+ for fn in ("Setup", "Setup.local"):
+ if os.path.isfile(os.path.join(d, "Modules", fn)):
+ return True
+ return False
+
+
+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
+
+
+# constants
+
+_BASE_INSTALL_SCHEMES = {
+ 'posix_prefix': {
+ 'stdlib': '{installed_base}/{platlibdir}/python{py_version_short}',
+ 'platstdlib': '{platbase}/{platlibdir}/python{py_version_short}',
+ 'purelib': '{base}/lib/python{py_version_short}/site-packages',
+ 'platlib': '{platbase}/{platlibdir}/python{py_version_short}/site-packages',
+ 'include':
+ '{installed_base}/include/python{py_version_short}{abiflags}',
+ 'platinclude':
+ '{installed_platbase}/include/python{py_version_short}{abiflags}',
+ 'scripts': '{base}/bin',
+ 'data': '{base}',
+ },
+ 'posix_home': {
+ 'stdlib': '{installed_base}/lib/python',
+ 'platstdlib': '{base}/lib/python',
+ 'purelib': '{base}/lib/python',
+ 'platlib': '{base}/lib/python',
+ 'include': '{installed_base}/include/python',
+ 'platinclude': '{installed_base}/include/python',
+ 'scripts': '{base}/bin',
+ 'data': '{base}',
+ },
+ 'nt': {
+ 'stdlib': '{installed_base}/Lib',
+ 'platstdlib': '{base}/Lib',
+ 'purelib': '{base}/Lib/site-packages',
+ 'platlib': '{base}/Lib/site-packages',
+ 'include': '{installed_base}/Include',
+ 'platinclude': '{installed_base}/Include',
+ 'scripts': '{base}/Scripts',
+ 'data': '{base}',
+ },
+ }
+_USER_INSTALL_SCHEMES = {
+ # NOTE: When modifying "purelib" scheme, update site._get_path() too.
+ 'nt_user': {
+ 'stdlib': '{userbase}/Python{py_version_nodot_plat}',
+ 'platstdlib': '{userbase}/Python{py_version_nodot_plat}',
+ 'purelib': '{userbase}/Python{py_version_nodot_plat}/site-packages',
+ 'platlib': '{userbase}/Python{py_version_nodot_plat}/site-packages',
+ 'include': '{userbase}/Python{py_version_nodot_plat}/Include',
+ 'scripts': '{userbase}/Python{py_version_nodot_plat}/Scripts',
+ 'data': '{userbase}',
+ },
+ 'posix_user': {
+ 'stdlib': '{userbase}/{platlibdir}/python{py_version_short}',
+ 'platstdlib': '{userbase}/{platlibdir}/python{py_version_short}',
+ 'purelib': '{userbase}/lib/python{py_version_short}/site-packages',
+ 'platlib': '{userbase}/lib/python{py_version_short}/site-packages',
+ 'include': '{userbase}/include/python{py_version_short}',
+ 'scripts': '{userbase}/bin',
+ 'data': '{userbase}',
+ },
+ 'osx_framework_user': {
+ 'stdlib': '{userbase}/lib/python',
+ 'platstdlib': '{userbase}/lib/python',
+ 'purelib': '{userbase}/lib/python/site-packages',
+ 'platlib': '{userbase}/lib/python/site-packages',
+ 'include': '{userbase}/include/python{py_version_short}',
+ 'scripts': '{userbase}/bin',
+ 'data': '{userbase}',
+ },
+ }
+
+_SCHEME_KEYS = ('stdlib', 'platstdlib', 'purelib', 'platlib', 'include',
+ 'scripts', 'data')
+
+_PY_VERSION = sys.version.split()[0]
+_PY_VERSION_SHORT = f'{sys.version_info[0]}.{sys.version_info[1]}'
+_PY_VERSION_SHORT_NO_DOT = f'{sys.version_info[0]}{sys.version_info[1]}'
+_PREFIX = os.path.normpath(sys.prefix)
+_BASE_PREFIX = os.path.normpath(sys.base_prefix)
+_EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
+_BASE_EXEC_PREFIX = os.path.normpath(sys.base_exec_prefix)
+
+_CHEAP_SCHEME_CONFIG_VARS = {
+ 'prefix': _PREFIX,
+ 'exec_prefix': _EXEC_PREFIX,
+ 'py_version': _PY_VERSION,
+ 'py_version_short': _PY_VERSION_SHORT,
+ 'py_version_nodot': _PY_VERSION_SHORT_NO_DOT,
+ 'installed_base': _BASE_PREFIX,
+ 'base': _PREFIX,
+ 'installed_platbase': _BASE_EXEC_PREFIX,
+ 'platbase': _EXEC_PREFIX,
+ 'platlibdir': sys.platlibdir,
+ 'abiflags': getattr(sys, 'abiflags', ''),
+ 'py_version_nodot_plat': getattr(sys, 'winver', '').replace('.', '')
+}
+
+_MODULE = sys.modules[__name__]
+
+# uninitialized
+
+_get_preferred_schemes = None
+
+
+# lazy constants
+
+
+# NOTE: site.py has copy of this function.
+# Sync it when modify this function.
+def _getuserbase():
+ env_base = os.environ.get("PYTHONUSERBASE", None)
+ if env_base:
+ return env_base
+
+ # VxWorks has no home directories
+ if sys.platform == "vxworks":
+ return None
+
+ def joinuser(*args):
+ return os.path.expanduser(os.path.join(*args))
+
+ if os.name == "nt":
+ base = os.environ.get("APPDATA") or "~"
+ return joinuser(base, "Python")
+
+ if sys.platform == "darwin" and sys._framework:
+ return joinuser("~", "Library", sys._framework,
+ f"{sys.version_info[0]}.{sys.version_info[1]}")
+
+ return joinuser("~", ".local")
+
+
+def is_python_build(check_home=False):
+ if check_home and _MODULE._SYS_HOME:
+ return _is_python_source_dir(_SYS_HOME)
+ return _is_python_source_dir(_MODULE._PROJECT_BASE)
+
+
+def __getattr__(name):
+ match name:
+ case '_BUILD_TIME_VARS':
+ _sysconfigdata = __import__(_get_sysconfigdata_name(), globals(), locals(), ['build_time_vars'], 0)
+ value = _sysconfigdata.build_time_vars
+ case '_HAS_USER_BASE':
+ value = (_getuserbase() is not None)
+ case '_PROJECT_BASE':
+ # set for cross builds
+ if "_PYTHON_PROJECT_BASE" in os.environ:
+ value = _safe_realpath(os.environ["_PYTHON_PROJECT_BASE"])
+ else:
+ if sys.executable:
+ value = os.path.dirname(_safe_realpath(sys.executable))
+ else:
+ # sys.executable can be empty if argv[0] has been changed and Python is
+ # unable to retrieve the real program name
+ value = _safe_realpath(os.getcwd())
+
+ if (os.name == 'nt' and
+ value.lower().endswith(('\\pcbuild\\win32', '\\pcbuild\\amd64'))):
+ value = _safe_realpath(os.path.join(value, os.path.pardir, os.path.pardir))
+
+ if os.name == 'nt':
+ value = _fix_pcbuild(value)
+ case '_SRCDIR':
+ if os.name == 'posix':
+ if _MODULE._PYTHON_BUILD:
+ # If srcdir is a relative path (typically '.' or '..')
+ # then it should be interpreted relative to the directory
+ # containing Makefile.
+ if 'srcdir' in _MODULE._BUILD_TIME_VARS:
+ value = _MODULE._BUILD_TIME_VARS['srcdir']
+ else:
+ value = _MODULE._PROJECT_BASE
+ base = os.path.dirname(get_makefile_filename())
+ value = os.path.join(base, value)
+ else:
+ # srcdir is not meaningful since the installation is
+ # spread about the filesystem. We choose the
+ # directory containing the Makefile since we know it
+ # exists.
+ value = os.path.dirname(get_makefile_filename())
+ else:
+ value = _MODULE._PROJECT_BASE
+ value = _safe_realpath(value)
+ case '_SYS_HOME':
+ value = getattr(sys, '_home', None)
+ if os.name == 'nt':
+ value = _fix_pcbuild(value)
+ case '_PYTHON_BUILD':
+ value = is_python_build(check_home=True)
+ case '_SCHEME_CONFIG_VARS':
+ value = _CHEAP_SCHEME_CONFIG_VARS.copy()
+ value['projectbase'] = _MODULE._PROJECT_BASE
+ case _:
+ raise AttributeError(f"module '{__name__}' has no attribute '{name}'")
+ setattr(_MODULE, name, value)
+ return value
+
+
+# methods
+
+
+def _get_raw_scheme_paths(scheme):
+ # lazy loading of install schemes -- only run the code paths we need to
+
+ # check base schemes
+ if scheme in _BASE_INSTALL_SCHEMES:
+ if scheme in ('posix_prefix', 'posix_home') and _MODULE._PYTHON_BUILD:
+ # On POSIX-y platforms, Python will:
+ # - Build from .h files in 'headers' (which is only added to the
+ # scheme when building CPython)
+ # - Install .h files to 'include'
+ scheme = _BASE_INSTALL_SCHEMES[scheme]
+ scheme['headers'] = scheme['include']
+ scheme['include'] = '{srcdir}/Include'
+ scheme['platinclude'] = '{projectbase}/.'
+ return scheme
+ return _BASE_INSTALL_SCHEMES[scheme]
+
+ if scheme in _USER_INSTALL_SCHEMES and _MODULE._HAS_USER_BASE:
+ return _USER_INSTALL_SCHEMES[scheme]
+
+ # check vendor schemes
+ try:
+ import _vendor.config
+
+ vendor_schemes = _vendor.config.EXTRA_INSTALL_SCHEMES
+ except (ModuleNotFoundError, AttributeError):
+ pass
+ return vendor_schemes[scheme]
+
+
+def _get_sysconfigdata_name():
+ multiarch = getattr(sys.implementation, '_multiarch', '')
+ return os.environ.get(
+ '_PYTHON_SYSCONFIGDATA_NAME',
+ f'_sysconfigdata_{sys.abiflags}_{sys.platform}_{multiarch}',
+ )
+
+
+def get_makefile_filename():
+ """Return the path of the Makefile."""
+ if _MODULE._PYTHON_BUILD:
+ return os.path.join(_MODULE._SYS_HOME or _MODULE._PROJECT_BASE, "Makefile")
+ if hasattr(sys, 'abiflags'):
+ config_dir_name = f'config-{_PY_VERSION_SHORT}{sys.abiflags}'
+ else:
+ config_dir_name = 'config'
+ if hasattr(sys.implementation, '_multiarch'):
+ config_dir_name += f'-{sys.implementation._multiarch}'
+ stdlib = _get_paths(get_default_scheme())['stdlib']
+ return os.path.join(stdlib, config_dir_name, 'Makefile')
+
+
+def _subst_vars(s, local_vars):
+ try:
+ return s.format(**local_vars)
+ except KeyError as var:
+ try:
+ return s.format(**os.environ)
+ except KeyError:
+ raise AttributeError(f'{var}') from None
+
+
+def _expand_vars(scheme, vars):
+ if vars is None:
+ vars = {}
+ vars.update(_CHEAP_SCHEME_CONFIG_VARS)
+
+ res = {}
+ for key, value in _get_raw_scheme_paths(scheme).items():
+ if os.name in ('posix', 'nt'):
+ value = os.path.expanduser(value)
+
+ # these are an expensive and uncommon config vars, let's only load them if we need to
+ if '{projectbase}' in value:
+ vars['projectbase'] = _MODULE._PROJECT_BASE
+ if '{srcdir}' in value:
+ vars['srcdir'] = _MODULE._SRCDIR
+
+ res[key] = os.path.normpath(_subst_vars(value, vars))
+ return res
+
+
+def _get_preferred_schemes_default():
+ if os.name == 'nt':
+ return {
+ 'prefix': 'nt',
+ 'home': 'posix_home',
+ 'user': 'nt_user',
+ }
+ if sys.platform == 'darwin' and sys._framework:
+ return {
+ 'prefix': 'posix_prefix',
+ 'home': 'posix_home',
+ 'user': 'osx_framework_user',
+ }
+ return {
+ 'prefix': 'posix_prefix',
+ 'home': 'posix_home',
+ 'user': 'posix_user',
+ }
+
+
+def get_preferred_scheme(key):
+ global _get_preferred_schemes
+ if not _get_preferred_schemes:
+ try:
+ import _vendor.config
+
+ _get_preferred_schemes = _vendor.config.get_preferred_schemes
+ except (ModuleNotFoundError, AttributeError):
+ _get_preferred_schemes = _get_preferred_schemes_default
+
+ scheme = _get_preferred_schemes()[key]
+
+ # check our schemes
+ if scheme in _BASE_INSTALL_SCHEMES or scheme in _USER_INSTALL_SCHEMES:
+ return scheme
+
+ # check vendor schemes
+ try:
+ import _vendor.config
+
+ vendor_schemes = _vendor.config.EXTRA_INSTALL_SCHEMES
+ except (ModuleNotFoundError, AttributeError):
+ pass
+ else:
+ if scheme in vendor_schemes:
+ return scheme
+
+ raise ValueError(
+ f"{key!r} returned {scheme!r}, which is not a valid scheme "
+ f"on this platform"
+ )
+
+
+def get_default_scheme():
+ return get_preferred_scheme('prefix')
+
+
+def _get_paths(scheme=get_default_scheme(), vars=None, expand=True):
+ """Return a mapping containing an install scheme.
+
+ ``scheme`` is the install scheme name. If not provided, it will
+ return the default scheme for the current platform.
+ """
+ if expand:
+ return _expand_vars(scheme, vars)
+ else:
+ return _get_raw_scheme_paths(scheme)
diff --git a/Lib/_vendor/__init__.py b/Lib/_vendor/__init__.py
new file mode 100644
index 00000000000000..e69de29bb2d1d6
diff --git a/Lib/distutils/sysconfig.py b/Lib/distutils/sysconfig.py
index 3414a761e76b99..c67fca5b8aee0e 100644
--- a/Lib/distutils/sysconfig.py
+++ b/Lib/distutils/sysconfig.py
@@ -19,19 +19,23 @@
from .errors import DistutilsPlatformError
-from sysconfig import (
+from _sysconfig import (
_PREFIX as PREFIX,
_BASE_PREFIX as BASE_PREFIX,
_EXEC_PREFIX as EXEC_PREFIX,
_BASE_EXEC_PREFIX as BASE_EXEC_PREFIX,
_PROJECT_BASE as project_base,
_PYTHON_BUILD as python_build,
+ _SYS_HOME as _sys_home,
+
+ _is_python_source_dir,
+)
+
+from sysconfig import (
_init_posix as sysconfig_init_posix,
parse_config_h as sysconfig_parse_config_h,
_init_non_posix,
- _is_python_source_dir,
- _sys_home,
_variable_rx,
_findvar1_rx,
@@ -53,7 +57,7 @@
_config_vars = get_config_vars()
if os.name == "nt":
- from sysconfig import _fix_pcbuild
+ from _sysconfig import _fix_pcbuild
warnings.warn(
'The distutils.sysconfig module is deprecated, use sysconfig instead',
diff --git a/Lib/site.py b/Lib/site.py
index e129f3b4851f3d..f94d8ca59dd265 100644
--- a/Lib/site.py
+++ b/Lib/site.py
@@ -87,6 +87,8 @@
USER_SITE = None
USER_BASE = None
+_VENDOR_SCHEMES = None
+
def _trace(message):
if sys.flags.verbose:
@@ -350,6 +352,7 @@ def getsitepackages(prefixes=None):
this function will find its `site-packages` subdirectory depending on the
system environment, and will return a list of full paths.
"""
+ global _VENDOR_SCHEMES
sitepackages = []
seen = set()
@@ -377,6 +380,23 @@ def getsitepackages(prefixes=None):
for libdir in libdirs:
path = os.path.join(prefix, libdir, "site-packages")
sitepackages.append(path)
+
+ if _VENDOR_SCHEMES is None: # delayed execution
+ try:
+ import _vendor.config
+
+ _VENDOR_SCHEMES = _vendor.config.EXTRA_SITE_INSTALL_SCHEMES
+ except (ModuleNotFoundError, AttributeError):
+ _VENDOR_SCHEMES = []
+
+ # vendor site schemes
+ if _VENDOR_SCHEMES:
+ import _sysconfig
+
+ for scheme in _VENDOR_SCHEMES:
+ paths = _sysconfig._get_paths(scheme)
+ sitepackages += list({paths['purelib'], paths['platlib']})
+
return sitepackages
def addsitepackages(known_paths, prefixes=None):
diff --git a/Lib/sysconfig.py b/Lib/sysconfig.py
index daf9f000060a35..ec228547b2af41 100644
--- a/Lib/sysconfig.py
+++ b/Lib/sysconfig.py
@@ -1,8 +1,18 @@
-"""Access to Python's configuration information."""
+"""Access to Python's configuration information.
+
+This module is split into _sysconfig, with the bits that are required at startup
+(by the site module), and sysconfig, with the rest of the module functionality.
+"""
import os
import sys
-from os.path import pardir, realpath
+from _sysconfig import (
+ _get_paths, _getuserbase, _get_sysconfigdata_name, _safe_realpath,
+ get_default_scheme, get_preferred_scheme, get_makefile_filename,
+ is_python_build, _HAS_USER_BASE, _BASE_INSTALL_SCHEMES,
+ _USER_INSTALL_SCHEMES, _PROJECT_BASE, _PYTHON_BUILD, _PY_VERSION_SHORT,
+ _PY_VERSION_SHORT_NO_DOT, _SCHEME_CONFIG_VARS, _SCHEME_KEYS, _SYS_HOME,
+)
__all__ = [
'get_config_h_filename',
@@ -13,8 +23,10 @@
'get_path_names',
'get_paths',
'get_platform',
+ 'get_preferred_scheme',
'get_python_version',
'get_scheme_names',
+ 'is_python_build',
'parse_config_h',
]
@@ -23,239 +35,37 @@
'MACOSX_DEPLOYMENT_TARGET',
}
-_INSTALL_SCHEMES = {
- 'posix_prefix': {
- 'stdlib': '{installed_base}/{platlibdir}/python{py_version_short}',
- 'platstdlib': '{platbase}/{platlibdir}/python{py_version_short}',
- 'purelib': '{base}/lib/python{py_version_short}/site-packages',
- 'platlib': '{platbase}/{platlibdir}/python{py_version_short}/site-packages',
- 'include':
- '{installed_base}/include/python{py_version_short}{abiflags}',
- 'platinclude':
- '{installed_platbase}/include/python{py_version_short}{abiflags}',
- 'scripts': '{base}/bin',
- 'data': '{base}',
- },
- 'posix_home': {
- 'stdlib': '{installed_base}/lib/python',
- 'platstdlib': '{base}/lib/python',
- 'purelib': '{base}/lib/python',
- 'platlib': '{base}/lib/python',
- 'include': '{installed_base}/include/python',
- 'platinclude': '{installed_base}/include/python',
- 'scripts': '{base}/bin',
- 'data': '{base}',
- },
- 'nt': {
- 'stdlib': '{installed_base}/Lib',
- 'platstdlib': '{base}/Lib',
- 'purelib': '{base}/Lib/site-packages',
- 'platlib': '{base}/Lib/site-packages',
- 'include': '{installed_base}/Include',
- 'platinclude': '{installed_base}/Include',
- 'scripts': '{base}/Scripts',
- 'data': '{base}',
- },
- }
-
-
-# NOTE: site.py has copy of this function.
-# Sync it when modify this function.
-def _getuserbase():
- env_base = os.environ.get("PYTHONUSERBASE", None)
- if env_base:
- return env_base
-
- # VxWorks has no home directories
- if sys.platform == "vxworks":
- return None
-
- def joinuser(*args):
- return os.path.expanduser(os.path.join(*args))
-
- if os.name == "nt":
- base = os.environ.get("APPDATA") or "~"
- return joinuser(base, "Python")
-
- if sys.platform == "darwin" and sys._framework:
- return joinuser("~", "Library", sys._framework,
- f"{sys.version_info[0]}.{sys.version_info[1]}")
-
- return joinuser("~", ".local")
-
-_HAS_USER_BASE = (_getuserbase() is not None)
-
-if _HAS_USER_BASE:
- _INSTALL_SCHEMES |= {
- # NOTE: When modifying "purelib" scheme, update site._get_path() too.
- 'nt_user': {
- 'stdlib': '{userbase}/Python{py_version_nodot_plat}',
- 'platstdlib': '{userbase}/Python{py_version_nodot_plat}',
- 'purelib': '{userbase}/Python{py_version_nodot_plat}/site-packages',
- 'platlib': '{userbase}/Python{py_version_nodot_plat}/site-packages',
- 'include': '{userbase}/Python{py_version_nodot_plat}/Include',
- 'scripts': '{userbase}/Python{py_version_nodot_plat}/Scripts',
- 'data': '{userbase}',
- },
- 'posix_user': {
- 'stdlib': '{userbase}/{platlibdir}/python{py_version_short}',
- 'platstdlib': '{userbase}/{platlibdir}/python{py_version_short}',
- 'purelib': '{userbase}/lib/python{py_version_short}/site-packages',
- 'platlib': '{userbase}/lib/python{py_version_short}/site-packages',
- 'include': '{userbase}/include/python{py_version_short}',
- 'scripts': '{userbase}/bin',
- 'data': '{userbase}',
- },
- 'osx_framework_user': {
- 'stdlib': '{userbase}/lib/python',
- 'platstdlib': '{userbase}/lib/python',
- 'purelib': '{userbase}/lib/python/site-packages',
- 'platlib': '{userbase}/lib/python/site-packages',
- 'include': '{userbase}/include/python{py_version_short}',
- 'scripts': '{userbase}/bin',
- 'data': '{userbase}',
- },
- }
-
-_SCHEME_KEYS = ('stdlib', 'platstdlib', 'purelib', 'platlib', 'include',
- 'scripts', 'data')
-
-_PY_VERSION = sys.version.split()[0]
-_PY_VERSION_SHORT = f'{sys.version_info[0]}.{sys.version_info[1]}'
-_PY_VERSION_SHORT_NO_DOT = f'{sys.version_info[0]}{sys.version_info[1]}'
-_PREFIX = os.path.normpath(sys.prefix)
-_BASE_PREFIX = os.path.normpath(sys.base_prefix)
-_EXEC_PREFIX = os.path.normpath(sys.exec_prefix)
-_BASE_EXEC_PREFIX = os.path.normpath(sys.base_exec_prefix)
-_CONFIG_VARS = None
-_USER_BASE = None
+_INSTALL_SCHEMES = None
-# Regexes needed for parsing Makefile (and similar syntaxes,
-# like old-style Setup files).
-_variable_rx = r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)"
-_findvar1_rx = r"\$\(([A-Za-z][A-Za-z0-9_]*)\)"
-_findvar2_rx = r"\${([A-Za-z][A-Za-z0-9_]*)}"
+def _reload_schemes():
+ global _INSTALL_SCHEMES
-def _safe_realpath(path):
- try:
- return realpath(path)
- except OSError:
- return path
-
-if sys.executable:
- _PROJECT_BASE = os.path.dirname(_safe_realpath(sys.executable))
-else:
- # sys.executable can be empty if argv[0] has been changed and Python is
- # 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))
-
-# 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):
- for fn in ("Setup", "Setup.local"):
- if os.path.isfile(os.path.join(d, "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)
-
-if _PYTHON_BUILD:
- for scheme in ('posix_prefix', 'posix_home'):
- # On POSIX-y platforms, Python will:
- # - Build from .h files in 'headers' (which is only added to the
- # scheme when building CPython)
- # - Install .h files to 'include'
- scheme = _INSTALL_SCHEMES[scheme]
- scheme['headers'] = scheme['include']
- scheme['include'] = '{srcdir}/Include'
- scheme['platinclude'] = '{projectbase}/.'
-
-
-def _subst_vars(s, local_vars):
+ # our schemes
+ _INSTALL_SCHEMES = _BASE_INSTALL_SCHEMES.copy()
+ if _HAS_USER_BASE:
+ _INSTALL_SCHEMES |= _USER_INSTALL_SCHEMES
+
+ # vendor schemes
try:
- return s.format(**local_vars)
- except KeyError as var:
- try:
- return s.format(**os.environ)
- except KeyError:
- raise AttributeError(f'{var}') from None
+ import _vendor.config
-def _extend_dict(target_dict, other_dict):
- target_keys = target_dict.keys()
- for key, value in other_dict.items():
- if key in target_keys:
- continue
- target_dict[key] = value
+ # make sure we do not let the vendor install schemes override ours
+ _INSTALL_SCHEMES = _vendor.config.EXTRA_INSTALL_SCHEMES | _INSTALL_SCHEMES
+ except (ModuleNotFoundError, AttributeError):
+ pass
-def _expand_vars(scheme, vars):
- res = {}
- if vars is None:
- vars = {}
- _extend_dict(vars, get_config_vars())
+_reload_schemes()
- for key, value in _INSTALL_SCHEMES[scheme].items():
- if os.name in ('posix', 'nt'):
- value = os.path.expanduser(value)
- res[key] = os.path.normpath(_subst_vars(value, vars))
- return res
+_CONFIG_VARS = None
-def _get_preferred_schemes():
- if os.name == 'nt':
- return {
- 'prefix': 'nt',
- 'home': 'posix_home',
- 'user': 'nt_user',
- }
- if sys.platform == 'darwin' and sys._framework:
- return {
- 'prefix': 'posix_prefix',
- 'home': 'posix_home',
- 'user': 'osx_framework_user',
- }
- return {
- 'prefix': 'posix_prefix',
- 'home': 'posix_home',
- 'user': 'posix_user',
- }
-
-
-def get_preferred_scheme(key):
- scheme = _get_preferred_schemes()[key]
- if scheme not in _INSTALL_SCHEMES:
- raise ValueError(
- f"{key!r} returned {scheme!r}, which is not a valid scheme "
- f"on this platform"
- )
- return scheme
-
-
-def get_default_scheme():
- return get_preferred_scheme('prefix')
+# Regexes needed for parsing Makefile (and similar syntaxes,
+# like old-style Setup files).
+_variable_rx = r"([a-zA-Z][a-zA-Z0-9_]+)\s*=\s*(.*)"
+_findvar1_rx = r"\$\(([A-Za-z][A-Za-z0-9_]*)\)"
+_findvar2_rx = r"\${([A-Za-z][A-Za-z0-9_]*)}"
def _parse_makefile(filename, vars=None, keep_unresolved=True):
@@ -386,27 +196,6 @@ def _parse_makefile(filename, vars=None, keep_unresolved=True):
return vars
-def get_makefile_filename():
- """Return the path of the Makefile."""
- if _PYTHON_BUILD:
- return os.path.join(_sys_home or _PROJECT_BASE, "Makefile")
- if hasattr(sys, 'abiflags'):
- config_dir_name = f'config-{_PY_VERSION_SHORT}{sys.abiflags}'
- else:
- config_dir_name = 'config'
- if hasattr(sys.implementation, '_multiarch'):
- config_dir_name += f'-{sys.implementation._multiarch}'
- return os.path.join(get_path('stdlib'), config_dir_name, 'Makefile')
-
-
-def _get_sysconfigdata_name():
- multiarch = getattr(sys.implementation, '_multiarch', '')
- return os.environ.get(
- '_PYTHON_SYSCONFIGDATA_NAME',
- f'_sysconfigdata_{sys.abiflags}_{sys.platform}_{multiarch}',
- )
-
-
def _generate_posix_vars():
"""Generate the Python module containing build-time variables."""
import pprint
@@ -470,6 +259,7 @@ def _generate_posix_vars():
with open('pybuilddir.txt', 'w', encoding='utf8') as f:
f.write(pybuilddir)
+
def _init_posix(vars):
"""Initialize the module as appropriate for POSIX systems."""
# _sysconfigdata is generated at build time, see _generate_posix_vars()
@@ -478,6 +268,7 @@ def _init_posix(vars):
build_time_vars = _temp.build_time_vars
vars.update(build_time_vars)
+
def _init_non_posix(vars):
"""Initialize the module as appropriate for NT"""
# set basic install directories
@@ -491,6 +282,14 @@ def _init_non_posix(vars):
vars['BINDIR'] = os.path.dirname(_safe_realpath(sys.executable))
vars['TZPATH'] = ''
+
+def _extend_dict(target_dict, other_dict):
+ target_keys = target_dict.keys()
+ for key, value in other_dict.items():
+ if key in target_keys:
+ continue
+ target_dict[key] = value
+
#
# public APIs
#
@@ -534,9 +333,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(_SYS_HOME or _PROJECT_BASE, "PC")
else:
- inc_dir = _sys_home or _PROJECT_BASE
+ inc_dir = _SYS_HOME or _PROJECT_BASE
else:
inc_dir = get_path('platinclude')
return os.path.join(inc_dir, 'pyconfig.h')
@@ -558,10 +357,10 @@ def get_paths(scheme=get_default_scheme(), vars=None, expand=True):
``scheme`` is the install scheme name. If not provided, it will
return the default scheme for the current platform.
"""
- if expand:
- return _expand_vars(scheme, vars)
- else:
- return _INSTALL_SCHEMES[scheme]
+ if vars is None:
+ vars = {}
+ _extend_dict(vars, get_config_vars())
+ return _get_paths(scheme, vars, expand)
def get_path(name, scheme=get_default_scheme(), vars=None, expand=True):
@@ -584,30 +383,10 @@ def get_config_vars(*args):
"""
global _CONFIG_VARS
if _CONFIG_VARS is None:
- _CONFIG_VARS = {}
+ _CONFIG_VARS = _SCHEME_CONFIG_VARS
# Normalized versions of prefix and exec_prefix are handy to have;
# in fact, these are the standard versions used most places in the
# Distutils.
- _CONFIG_VARS['prefix'] = _PREFIX
- _CONFIG_VARS['exec_prefix'] = _EXEC_PREFIX
- _CONFIG_VARS['py_version'] = _PY_VERSION
- _CONFIG_VARS['py_version_short'] = _PY_VERSION_SHORT
- _CONFIG_VARS['py_version_nodot'] = _PY_VERSION_SHORT_NO_DOT
- _CONFIG_VARS['installed_base'] = _BASE_PREFIX
- _CONFIG_VARS['base'] = _PREFIX
- _CONFIG_VARS['installed_platbase'] = _BASE_EXEC_PREFIX
- _CONFIG_VARS['platbase'] = _EXEC_PREFIX
- _CONFIG_VARS['projectbase'] = _PROJECT_BASE
- _CONFIG_VARS['platlibdir'] = sys.platlibdir
- try:
- _CONFIG_VARS['abiflags'] = sys.abiflags
- except AttributeError:
- # sys.abiflags may not be defined on all platforms.
- _CONFIG_VARS['abiflags'] = ''
- try:
- _CONFIG_VARS['py_version_nodot_plat'] = sys.winver.replace('.', '')
- except AttributeError:
- _CONFIG_VARS['py_version_nodot_plat'] = ''
if os.name == 'nt':
_init_non_posix(_CONFIG_VARS)
diff --git a/Lib/test/support/__init__.py b/Lib/test/support/__init__.py
index 85fd74126b5f47..1ce6ecda67f61f 100644
--- a/Lib/test/support/__init__.py
+++ b/Lib/test/support/__init__.py
@@ -15,6 +15,7 @@
import unittest
import warnings
+import test.vendor_config
from .testresult import get_test_runner
@@ -55,6 +56,7 @@
"run_with_tz", "PGO", "missing_compiler_executable",
"ALWAYS_EQ", "NEVER_EQ", "LARGEST", "SMALLEST",
"LOOPBACK_TIMEOUT", "INTERNET_TIMEOUT", "SHORT_TIMEOUT", "LONG_TIMEOUT",
+ "with_test_vendor_config",
]
@@ -2091,3 +2093,27 @@ def clear_ignored_deprecations(*tokens: object) -> None:
if warnings.filters != new_filters:
warnings.filters[:] = new_filters
warnings._filters_mutated()
+
+
+@contextlib.contextmanager
+def with_test_vendor_config():
+ # this is needed because we are mocking package module
+ try:
+ import _vendor.config
+ old_config = _vendor.config
+ except ModuleNotFoundError:
+ old_config = None
+
+ with unittest.mock.patch.dict(sys.modules, {'_vendor.config': test.vendor_config}):
+ import _vendor
+
+ _vendor.config = test.vendor_config
+ sysconfig._reload_schemes()
+
+ yield
+
+ if old_config:
+ _vendor.config = old_config
+ else:
+ delattr(_vendor, 'config')
+ sysconfig._reload_schemes()
diff --git a/Lib/test/test_site.py b/Lib/test/test_site.py
index 5f06a0d4b03725..f2dfa451272ab5 100644
--- a/Lib/test/test_site.py
+++ b/Lib/test/test_site.py
@@ -10,6 +10,7 @@
from test.support import os_helper
from test.support import socket_helper
from test.support import captured_stderr
+from test.support import with_test_vendor_config
from test.support.os_helper import TESTFN, EnvironmentVarGuard, change_cwd
import builtins
import encodings
@@ -20,6 +21,7 @@
import shutil
import subprocess
import sys
+import _sysconfig
import sysconfig
import tempfile
import urllib.error
@@ -69,8 +71,11 @@ def setUp(self):
self.old_base = site.USER_BASE
self.old_site = site.USER_SITE
self.old_prefixes = site.PREFIXES
+ self.old_vendor_schemes = site._VENDOR_SCHEMES
self.original_vars = sysconfig._CONFIG_VARS
self.old_vars = copy(sysconfig._CONFIG_VARS)
+ self.original_schemes = sysconfig._INSTALL_SCHEMES
+ self.old_schemes = copy(sysconfig._INSTALL_SCHEMES)
def tearDown(self):
"""Restore sys.path"""
@@ -78,11 +83,15 @@ def tearDown(self):
site.USER_BASE = self.old_base
site.USER_SITE = self.old_site
site.PREFIXES = self.old_prefixes
+ site._VENDOR_SCHEMES = self.old_vendor_schemes
sysconfig._CONFIG_VARS = self.original_vars
# _CONFIG_VARS is None before get_config_vars() is called
if sysconfig._CONFIG_VARS is not None:
sysconfig._CONFIG_VARS.clear()
sysconfig._CONFIG_VARS.update(self.old_vars)
+ sysconfig._INSTALL_SCHEMES = self.original_schemes
+ sysconfig._INSTALL_SCHEMES.clear()
+ sysconfig._INSTALL_SCHEMES.update(self.old_schemes)
def test_makepath(self):
# Test makepath() have an absolute path for its first return value
@@ -302,6 +311,36 @@ def test_getsitepackages(self):
wanted = os.path.join('xoxo', 'lib', 'site-packages')
self.assertEqual(dirs[1], wanted)
+ @with_test_vendor_config()
+ def test_getsitepackages_vendor(self):
+ # force re-load of vendor schemes
+ site._VENDOR_SCHEMES = None
+
+ site.PREFIXES = ['xoxo']
+ dirs = site.getsitepackages()
+ if os.sep == '/':
+ # OS X, Linux, FreeBSD, etc
+ if sys.platlibdir != "lib":
+ self.assertEqual(len(dirs), 2)
+ wanted = os.path.join('xoxo', sys.platlibdir,
+ 'python%d.%d' % sys.version_info[:2],
+ 'site-packages')
+ self.assertEqual(dirs[0], wanted)
+ else:
+ self.assertEqual(len(dirs), 3)
+ wanted = os.path.join('xoxo', 'lib',
+ 'python%d.%d' % sys.version_info[:2],
+ 'site-packages')
+ self.assertEqual(dirs[-3], wanted)
+ self.assertEqual(sorted(dirs[-2:]), ['vendor-plat-packages', 'vendor-pure-packages'])
+ else:
+ # other platforms
+ self.assertEqual(len(dirs), 4)
+ self.assertEqual(dirs[0], 'xoxo')
+ wanted = os.path.join('xoxo', 'lib', 'site-packages')
+ self.assertEqual(dirs[1], wanted)
+ self.assertEqual(sorted(dirs[2:]), ['vendor-plat-packages', 'vendor-pure-packages'])
+
@unittest.skipUnless(HAS_USER_SITE, 'need user site')
def test_no_home_directory(self):
# bpo-10496: getuserbase() and getusersitepackages() must not fail if
diff --git a/Lib/test/test_sysconfig.py b/Lib/test/test_sysconfig.py
index 9408657c918863..55038540da3593 100644
--- a/Lib/test/test_sysconfig.py
+++ b/Lib/test/test_sysconfig.py
@@ -1,21 +1,24 @@
import unittest
+import unittest.mock
import sys
import os
import subprocess
import shutil
from copy import copy
-from test.support import (captured_stdout, PythonSymlink)
+from test.support import (captured_stdout, with_test_vendor_config, PythonSymlink)
from test.support.import_helper import import_module
from test.support.os_helper import (TESTFN, unlink, skip_unless_symlink,
change_cwd)
from test.support.warnings_helper import check_warnings
+import _sysconfig
import sysconfig
+from _sysconfig import _get_preferred_schemes_default, _expand_vars
from sysconfig import (get_paths, get_platform, get_config_vars,
- get_path, get_path_names, _INSTALL_SCHEMES,
+ get_path, get_path_names, _INSTALL_SCHEMES, _SCHEME_KEYS,
get_default_scheme, get_scheme_names, get_config_var,
- _expand_vars, _get_preferred_schemes, _main)
+ get_preferred_scheme, _main)
import _osx_support
@@ -43,7 +46,9 @@ def setUp(self):
self.join = os.path.join
self.isabs = os.path.isabs
self.splitdrive = os.path.splitdrive
+ self._get_preferred_schemes = _sysconfig._get_preferred_schemes
self._config_vars = sysconfig._CONFIG_VARS, copy(sysconfig._CONFIG_VARS)
+ self._schemes = sysconfig._INSTALL_SCHEMES, copy(sysconfig._INSTALL_SCHEMES)
self._added_envvars = []
self._changed_envvars = []
for var in ('MACOSX_DEPLOYMENT_TARGET', 'PATH'):
@@ -66,9 +71,13 @@ def tearDown(self):
os.path.join = self.join
os.path.isabs = self.isabs
os.path.splitdrive = self.splitdrive
+ _sysconfig._get_preferred_schemes = self._get_preferred_schemes
sysconfig._CONFIG_VARS = self._config_vars[0]
sysconfig._CONFIG_VARS.clear()
sysconfig._CONFIG_VARS.update(self._config_vars[1])
+ sysconfig._INSTALL_SCHEMES = self._schemes[0]
+ sysconfig._INSTALL_SCHEMES.clear()
+ sysconfig._INSTALL_SCHEMES.update(self._schemes[1])
for var, value in self._changed_envvars:
os.environ[var] = value
for var in self._added_envvars:
@@ -103,7 +112,7 @@ def test_get_paths(self):
def test_get_path(self):
config_vars = get_config_vars()
for scheme in _INSTALL_SCHEMES:
- for name in _INSTALL_SCHEMES[scheme]:
+ for name in _SCHEME_KEYS:
expected = _INSTALL_SCHEMES[scheme][name].format(**config_vars)
self.assertEqual(
os.path.normpath(get_path(name, scheme)),
@@ -113,18 +122,24 @@ def test_get_path(self):
def test_get_default_scheme(self):
self.assertIn(get_default_scheme(), _INSTALL_SCHEMES)
- def test_get_preferred_schemes(self):
+ @with_test_vendor_config()
+ def test_get_preferred_schemes_vendor(self):
+ _sysconfig._get_preferred_schemes = None
+
+ self.assertEqual(get_preferred_scheme('prefix'), 'some_vendor')
+
+ def test_get_preferred_schemes_default(self):
expected_schemes = {'prefix', 'home', 'user'}
# Windows.
os.name = 'nt'
- schemes = _get_preferred_schemes()
+ schemes = _get_preferred_schemes_default()
self.assertIsInstance(schemes, dict)
self.assertEqual(set(schemes), expected_schemes)
# Mac and Linux, shared library build.
os.name = 'posix'
- schemes = _get_preferred_schemes()
+ schemes = _get_preferred_schemes_default()
self.assertIsInstance(schemes, dict)
self.assertEqual(set(schemes), expected_schemes)
@@ -263,10 +278,17 @@ def test_get_config_h_filename(self):
self.assertTrue(os.path.isfile(config_h), config_h)
def test_get_scheme_names(self):
- wanted = ['nt', 'posix_home', 'posix_prefix']
+ wanted = {'nt', 'posix_home', 'posix_prefix'}
+ if HAS_USER_BASE:
+ wanted |= {'nt_user', 'osx_framework_user', 'posix_user'}
+ self.assertEqual(set(get_scheme_names()), wanted)
+
+ @with_test_vendor_config()
+ def test_get_scheme_names_vendor(self):
+ wanted = {'nt', 'posix_home', 'posix_prefix', 'some_vendor'}
if HAS_USER_BASE:
- wanted.extend(['nt_user', 'osx_framework_user', 'posix_user'])
- self.assertEqual(get_scheme_names(), tuple(sorted(wanted)))
+ wanted |= {'nt_user', 'osx_framework_user', 'posix_user'}
+ self.assertEqual(set(sysconfig.get_scheme_names()), wanted)
@skip_unless_symlink
def test_symlink(self): # Issue 7880
@@ -376,7 +398,7 @@ 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))
+ self.assertTrue(_sysconfig._is_python_source_dir(srcdir))
elif os.name == 'posix':
makefile_dir = os.path.dirname(sysconfig.get_makefile_filename())
# Issue #19340: srcdir has been realpath'ed already
diff --git a/Lib/test/vendor_config.py b/Lib/test/vendor_config.py
new file mode 100644
index 00000000000000..0c9f484bd89c06
--- /dev/null
+++ b/Lib/test/vendor_config.py
@@ -0,0 +1,26 @@
+EXTRA_INSTALL_SCHEMES = {
+ 'some_vendor': {
+ 'stdlib': '{installed_base}/{platlibdir}/python{py_version_short}',
+ 'platstdlib': '{platbase}/{platlibdir}/python{py_version_short}',
+ 'include':
+ '{installed_base}/include/python{py_version_short}{abiflags}',
+ 'platinclude':
+ '{installed_platbase}/include/python{py_version_short}{abiflags}',
+ 'purelib': 'vendor-pure-packages',
+ 'platlib': 'vendor-plat-packages',
+ 'scripts': 'vendor-scripts',
+ 'data': 'vendor-data',
+ },
+}
+
+EXTRA_SITE_INSTALL_SCHEMES = [
+ 'some_vendor',
+]
+
+
+def get_preferred_schemes():
+ return {
+ 'prefix': 'some_vendor',
+ 'home': 'some_vendor',
+ 'user': 'some_vendor',
+ }
diff --git a/Makefile.pre.in b/Makefile.pre.in
index f03f535f6faa60..d7e888b6622720 100644
--- a/Makefile.pre.in
+++ b/Makefile.pre.in
@@ -746,6 +746,7 @@ FROZEN_FILES_IN = \
Lib/io.py \
Lib/_collections_abc.py \
Lib/_sitebuiltins.py \
+ Lib/_sysconfig.py \
Lib/genericpath.py \
Lib/ntpath.py \
Lib/posixpath.py \
@@ -768,6 +769,7 @@ FROZEN_FILES_OUT = \
Python/frozen_modules/io.h \
Python/frozen_modules/_collections_abc.h \
Python/frozen_modules/_sitebuiltins.h \
+ Python/frozen_modules/_sysconfig.h \
Python/frozen_modules/genericpath.h \
Python/frozen_modules/ntpath.h \
Python/frozen_modules/posixpath.h \
@@ -813,6 +815,9 @@ Python/frozen_modules/_collections_abc.h: Programs/_freeze_module Lib/_collectio
Python/frozen_modules/_sitebuiltins.h: Programs/_freeze_module Lib/_sitebuiltins.py
Programs/_freeze_module _sitebuiltins $(srcdir)/Lib/_sitebuiltins.py $(srcdir)/Python/frozen_modules/_sitebuiltins.h
+Python/frozen_modules/_sysconfig.h: Programs/_freeze_module Lib/_sysconfig.py
+ Programs/_freeze_module _sysconfig $(srcdir)/Lib/_sysconfig.py $(srcdir)/Python/frozen_modules/_sysconfig.h
+
Python/frozen_modules/genericpath.h: Programs/_freeze_module Lib/genericpath.py
Programs/_freeze_module genericpath $(srcdir)/Lib/genericpath.py $(srcdir)/Python/frozen_modules/genericpath.h
@@ -1517,7 +1522,8 @@ maninstall: altmaninstall
# Install the library
XMLLIBSUBDIRS= xml xml/dom xml/etree xml/parsers xml/sax
-LIBSUBDIRS= asyncio \
+LIBSUBDIRS= _vendor \
+ asyncio \
collections \
concurrent concurrent/futures \
csv \
@@ -1629,6 +1635,7 @@ TESTSUBDIRS= ctypes/test \
unittest/test unittest/test/testmock
TEST_MODULES=@TEST_MODULES@
+VENDOR_CONFIG=@VENDOR_CONFIG@
libinstall: build_all $(srcdir)/Modules/xxmodule.c
@for i in $(SCRIPTDIR) $(LIBDEST); \
do \
@@ -1664,6 +1671,9 @@ libinstall: build_all $(srcdir)/Modules/xxmodule.c
echo $(INSTALL_DATA) $$i $(LIBDEST); \
fi; \
done
+ @if test ! -z "$(VENDOR_CONFIG)"; then \
+ $(INSTALL_SCRIPT) $(VENDOR_CONFIG) $(DESTDIR)$(LIBDEST)/_vendor.config.py; \
+ fi
@if test "$(TEST_MODULES)" = yes; then \
subdirs="$(LIBSUBDIRS) $(TESTSUBDIRS)"; \
else \
diff --git a/Misc/NEWS.d/next/Build/2021-04-28-21-57-32.bpo-41282.xgQ6Cn.rst b/Misc/NEWS.d/next/Build/2021-04-28-21-57-32.bpo-41282.xgQ6Cn.rst
new file mode 100644
index 00000000000000..3543b0cec115eb
--- /dev/null
+++ b/Misc/NEWS.d/next/Build/2021-04-28-21-57-32.bpo-41282.xgQ6Cn.rst
@@ -0,0 +1,2 @@
+Introduced support for Python distributors to specify a vendor config, via
+--with-vendor-config, which allows them to add custom install schemes.
diff --git a/PCbuild/_freeze_module.vcxproj b/PCbuild/_freeze_module.vcxproj
index 12bdde2af84d9a..ed4ee78b61681f 100644
--- a/PCbuild/_freeze_module.vcxproj
+++ b/PCbuild/_freeze_module.vcxproj
@@ -270,6 +270,11 @@
$(IntDir)_sitebuiltins.g.h
$(PySourcePath)Python\frozen_modules\_sitebuiltins.h
+
+ _sysconfig
+ $(IntDir)_sysconfig.g.h
+ $(PySourcePath)Python\frozen_modules\_sysconfig.h
+
genericpath
$(IntDir)genericpath.g.h
diff --git a/PCbuild/_freeze_module.vcxproj.filters b/PCbuild/_freeze_module.vcxproj.filters
index 5894909e0fbe1e..4328c6a9f73a38 100644
--- a/PCbuild/_freeze_module.vcxproj.filters
+++ b/PCbuild/_freeze_module.vcxproj.filters
@@ -40,6 +40,9 @@
Python Files
+
+ Python Files
+
Python Files
diff --git a/Python/frozen.c b/Python/frozen.c
index 499b3b99570573..92a8ea375201db 100644
--- a/Python/frozen.c
+++ b/Python/frozen.c
@@ -47,6 +47,7 @@
#include "frozen_modules/io.h"
#include "frozen_modules/_collections_abc.h"
#include "frozen_modules/_sitebuiltins.h"
+#include "frozen_modules/_sysconfig.h"
#include "frozen_modules/genericpath.h"
#include "frozen_modules/ntpath.h"
#include "frozen_modules/posixpath.h"
@@ -80,6 +81,7 @@ static const struct _frozen _PyImport_FrozenModules[] = {
{"_collections_abc", _Py_M___collections_abc,
(int)sizeof(_Py_M___collections_abc)},
{"_sitebuiltins", _Py_M___sitebuiltins, (int)sizeof(_Py_M___sitebuiltins)},
+ {"_sysconfig", _Py_M___sysconfig, (int)sizeof(_Py_M___sysconfig)},
{"genericpath", _Py_M__genericpath, (int)sizeof(_Py_M__genericpath)},
{"ntpath", _Py_M__ntpath, (int)sizeof(_Py_M__ntpath)},
{"posixpath", _Py_M__posixpath, (int)sizeof(_Py_M__posixpath)},
diff --git a/Python/stdlib_module_names.h b/Python/stdlib_module_names.h
index 1743292593f363..601cde7d60c6c4 100644
--- a/Python/stdlib_module_names.h
+++ b/Python/stdlib_module_names.h
@@ -77,6 +77,7 @@ static const char* _Py_stdlib_module_names[] = {
"_strptime",
"_struct",
"_symtable",
+"_sysconfig",
"_thread",
"_threading_local",
"_tkinter",
@@ -84,6 +85,7 @@ static const char* _Py_stdlib_module_names[] = {
"_tracemalloc",
"_typing",
"_uuid",
+"_vendor",
"_warnings",
"_weakref",
"_weakrefset",
diff --git a/Tools/scripts/freeze_modules.py b/Tools/scripts/freeze_modules.py
index 5c7eee42952896..0dff681fdf1a0d 100644
--- a/Tools/scripts/freeze_modules.py
+++ b/Tools/scripts/freeze_modules.py
@@ -84,6 +84,7 @@ def find_tool():
('stdlib - startup, with site', [
'_collections_abc',
'_sitebuiltins',
+ '_sysconfig',
'genericpath',
'ntpath',
'posixpath',
diff --git a/configure b/configure
index 81ee4282d9412b..58ba1c64040f13 100755
--- a/configure
+++ b/configure
@@ -623,6 +623,7 @@ ac_includes_default="\
#endif"
ac_subst_vars='LTLIBOBJS
+VENDOR_CONFIG
TEST_MODULES
LIBRARY_DEPS
STATIC_LIBPYTHON
@@ -864,6 +865,7 @@ with_builtin_hashlib_hashes
with_experimental_isolated_subinterpreters
with_static_libpython
enable_test_modules
+with_vendor_config
'
ac_precious_vars='build_alias
host_alias
@@ -1630,6 +1632,9 @@ Optional Packages:
--without-static-libpython
do not build libpythonMAJOR.MINOR.a and do not
install python.o (default is yes)
+ --with-vendor-config=
+ use a vendor config to customize the certain details of the Python installation
+
Some influential environment variables:
MACHDEP name for machine-dependent library files
@@ -18106,6 +18111,49 @@ $as_echo "no" >&6; }
fi
+# --with-vendor-config
+VENDOR_CONFIG=''
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for --with-vendor-config" >&5
+$as_echo_n "checking for --with-vendor-config... " >&6; }
+
+# Check whether --with-vendor-config was given.
+if test "${with_vendor_config+set}" = set; then :
+ withval=$with_vendor_config;
+ as_ac_File=`$as_echo "ac_cv_file_"$withval"" | $as_tr_sh`
+{ $as_echo "$as_me:${as_lineno-$LINENO}: checking for \"$withval\"" >&5
+$as_echo_n "checking for \"$withval\"... " >&6; }
+if eval \${$as_ac_File+:} false; then :
+ $as_echo_n "(cached) " >&6
+else
+ test "$cross_compiling" = yes &&
+ as_fn_error $? "cannot check for file existence when cross compiling" "$LINENO" 5
+if test -r ""$withval""; then
+ eval "$as_ac_File=yes"
+else
+ eval "$as_ac_File=no"
+fi
+fi
+eval ac_res=\$$as_ac_File
+ { $as_echo "$as_me:${as_lineno-$LINENO}: result: $ac_res" >&5
+$as_echo "$ac_res" >&6; }
+if eval test \"x\$"$as_ac_File"\" = x"yes"; then :
+
+ if ( echo "$withval" | grep '.*\.py$' > /dev/null); then
+ VENDOR_CONFIG="$withval"
+ else
+ as_fn_error $? "--with-vendor-config requires a Python file" "$LINENO" 5
+ fi
+
+else
+
+ as_fn_error $? "--with-vendor-config requires a valid file" "$LINENO" 5
+
+fi
+
+
+fi
+
+
# generate output files
ac_config_files="$ac_config_files Makefile.pre Misc/python.pc Misc/python-embed.pc Misc/python-config.sh"
diff --git a/configure.ac b/configure.ac
index ab3fc2839d4f8b..ab9692328db0e0 100644
--- a/configure.ac
+++ b/configure.ac
@@ -6008,6 +6008,27 @@ else
fi
AC_SUBST(TEST_MODULES)
+# --with-vendor-config
+VENDOR_CONFIG=''
+AC_MSG_CHECKING(for --with-vendor-config)
+AC_ARG_WITH(vendor-config,
+ AS_HELP_STRING([--with-vendor-config=]
+ [use a vendor config to customize the certain details of the Python installation]),
+[
+ AC_CHECK_FILE("$withval",
+ [
+ if ( echo "$withval" | grep '.*\.py$' > /dev/null); then
+ VENDOR_CONFIG="$withval"
+ else
+ AC_MSG_ERROR([--with-vendor-config requires a Python file])
+ fi
+ ],
+ [
+ AC_MSG_ERROR([--with-vendor-config requires a valid file])
+ ])
+],
+[])
+AC_SUBST(VENDOR_CONFIG)
# generate output files
AC_CONFIG_FILES(Makefile.pre Misc/python.pc Misc/python-embed.pc Misc/python-config.sh)