Skip to content

Commit b72a436

Browse files
committed
GH-127178: improve compatibility in _sysconfig_vars_(...).json
This patch improves environment and platform compatibility - Data now matches `sysconfig.get_config_vars` after install - `userbase` now correctly reflects the target platform when cross-compiling - `test_sysconfigdata_json` now takes into account the following situations: - Running with a non-default sys.executable path - Running under virtual environments - Running under relocatable installs To simplify the detection of relocatable installs, which needs to look at `_sysconfigdata_(...)`, this module is now saved in `sys.modules`. As part of this change, the code to import the module from a specific directory was refactored to use `PathFinder`, simplifying the implementation. Signed-off-by: Filipe Laíns <[email protected]
1 parent f826bec commit b72a436

File tree

3 files changed

+155
-121
lines changed

3 files changed

+155
-121
lines changed

Lib/sysconfig/__init__.py

+138-118
Original file line numberDiff line numberDiff line change
@@ -109,15 +109,114 @@
109109
def _get_implementation():
110110
return 'Python'
111111

112+
def get_platform():
113+
"""Return a string that identifies the current platform.
114+
115+
This is used mainly to distinguish platform-specific build directories and
116+
platform-specific built distributions. Typically includes the OS name and
117+
version and the architecture (as supplied by 'os.uname()'), although the
118+
exact information included depends on the OS; on Linux, the kernel version
119+
isn't particularly important.
120+
121+
Examples of returned values:
122+
linux-i586
123+
linux-alpha (?)
124+
solaris-2.6-sun4u
125+
126+
Windows will return one of:
127+
win-amd64 (64bit Windows on AMD64 (aka x86_64, Intel64, EM64T, etc)
128+
win32 (all others - specifically, sys.platform is returned)
129+
130+
For other non-POSIX platforms, currently just returns 'sys.platform'.
131+
132+
"""
133+
if os.name == 'nt':
134+
if 'amd64' in sys.version.lower():
135+
return 'win-amd64'
136+
if '(arm)' in sys.version.lower():
137+
return 'win-arm32'
138+
if '(arm64)' in sys.version.lower():
139+
return 'win-arm64'
140+
return sys.platform
141+
142+
if os.name != "posix" or not hasattr(os, 'uname'):
143+
# XXX what about the architecture? NT is Intel or Alpha
144+
return sys.platform
145+
146+
# Set for cross builds explicitly
147+
if "_PYTHON_HOST_PLATFORM" in os.environ:
148+
return os.environ["_PYTHON_HOST_PLATFORM"]
149+
150+
# Try to distinguish various flavours of Unix
151+
osname, host, release, version, machine = os.uname()
152+
153+
# Convert the OS name to lowercase, remove '/' characters, and translate
154+
# spaces (for "Power Macintosh")
155+
osname = osname.lower().replace('/', '')
156+
machine = machine.replace(' ', '_')
157+
machine = machine.replace('/', '-')
158+
159+
if osname[:5] == "linux":
160+
if sys.platform == "android":
161+
osname = "android"
162+
release = get_config_var("ANDROID_API_LEVEL")
163+
164+
# Wheel tags use the ABI names from Android's own tools.
165+
machine = {
166+
"x86_64": "x86_64",
167+
"i686": "x86",
168+
"aarch64": "arm64_v8a",
169+
"armv7l": "armeabi_v7a",
170+
}[machine]
171+
else:
172+
# At least on Linux/Intel, 'machine' is the processor --
173+
# i386, etc.
174+
# XXX what about Alpha, SPARC, etc?
175+
return f"{osname}-{machine}"
176+
elif osname[:5] == "sunos":
177+
if release[0] >= "5": # SunOS 5 == Solaris 2
178+
osname = "solaris"
179+
release = f"{int(release[0]) - 3}.{release[2:]}"
180+
# We can't use "platform.architecture()[0]" because a
181+
# bootstrap problem. We use a dict to get an error
182+
# if some suspicious happens.
183+
bitness = {2147483647:"32bit", 9223372036854775807:"64bit"}
184+
machine += f".{bitness[sys.maxsize]}"
185+
# fall through to standard osname-release-machine representation
186+
elif osname[:3] == "aix":
187+
from _aix_support import aix_platform
188+
return aix_platform()
189+
elif osname[:6] == "cygwin":
190+
osname = "cygwin"
191+
import re
192+
rel_re = re.compile(r'[\d.]+')
193+
m = rel_re.match(release)
194+
if m:
195+
release = m.group()
196+
elif osname[:6] == "darwin":
197+
if sys.platform == "ios":
198+
release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
199+
osname = sys.platform
200+
machine = sys.implementation._multiarch
201+
else:
202+
import _osx_support
203+
osname, release, machine = _osx_support.get_platform_osx(
204+
get_config_vars(),
205+
osname, release, machine)
206+
207+
return f"{osname}-{release}-{machine}"
208+
112209
# NOTE: site.py has copy of this function.
113210
# Sync it when modify this function.
114211
def _getuserbase():
115212
env_base = os.environ.get("PYTHONUSERBASE", None)
116213
if env_base:
117214
return env_base
118215

119-
# Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories
120-
if sys.platform in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
216+
# Emscripten, iOS, tvOS, VxWorks, WASI, and watchOS have no home directories.
217+
# Use sysconfig.get_platform() to get the correct platform when cross-compiling.
218+
system_name = get_platform().split('-')[0]
219+
if system_name in {"emscripten", "ios", "tvos", "vxworks", "wasi", "watchos"}:
121220
return None
122221

123222
def joinuser(*args):
@@ -335,34 +434,53 @@ def get_makefile_filename():
335434
return os.path.join(get_path('stdlib'), config_dir_name, 'Makefile')
336435

337436

437+
def _import_from_directory(path, name):
438+
if name not in sys.modules:
439+
import importlib.machinery
440+
import importlib.util
441+
442+
spec = importlib.machinery.PathFinder.find_spec(name, [path])
443+
module = importlib.util.module_from_spec(spec)
444+
spec.loader.exec_module(module)
445+
sys.modules[name] = module
446+
return sys.modules[name]
447+
448+
338449
def _get_sysconfigdata_name():
339450
multiarch = getattr(sys.implementation, '_multiarch', '')
340451
return os.environ.get(
341452
'_PYTHON_SYSCONFIGDATA_NAME',
342453
f'_sysconfigdata_{sys.abiflags}_{sys.platform}_{multiarch}',
343454
)
344455

345-
def _init_posix(vars):
346-
"""Initialize the module as appropriate for POSIX systems."""
347-
# _sysconfigdata is generated at build time, see _generate_posix_vars()
456+
457+
def _get_sysconfigdata():
458+
import importlib
459+
348460
name = _get_sysconfigdata_name()
461+
path = os.environ.get('_PYTHON_SYSCONFIGDATA_PATH')
462+
module = _import_from_directory(path, name) if path else importlib.import_module(name)
349463

350-
# For cross builds, the path to the target's sysconfigdata must be specified
351-
# so it can be imported. It cannot be in PYTHONPATH, as foreign modules in
352-
# sys.path can cause crashes when loaded by the host interpreter.
353-
# Rely on truthiness as a valueless env variable is still an empty string.
354-
# See OS X note in _generate_posix_vars re _sysconfigdata.
355-
if (path := os.environ.get('_PYTHON_SYSCONFIGDATA_PATH')):
356-
from importlib.machinery import FileFinder, SourceFileLoader, SOURCE_SUFFIXES
357-
from importlib.util import module_from_spec
358-
spec = FileFinder(path, (SourceFileLoader, SOURCE_SUFFIXES)).find_spec(name)
359-
_temp = module_from_spec(spec)
360-
spec.loader.exec_module(_temp)
361-
else:
362-
_temp = __import__(name, globals(), locals(), ['build_time_vars'], 0)
363-
build_time_vars = _temp.build_time_vars
464+
return module.build_time_vars
465+
466+
467+
def _installation_is_relocated():
468+
"""Is the Python installation running from a different prefix than what was targetted when building?"""
469+
if os.name != 'posix':
470+
raise NotImplementedError('sysconfig._installation_is_relocated() is currently only supported on POSIX')
471+
472+
data = _get_sysconfigdata()
473+
return (
474+
data['prefix'] != getattr(sys, 'base_prefix', '')
475+
or data['exec_prefix'] != getattr(sys, 'base_exec_prefix', '')
476+
)
477+
478+
479+
def _init_posix(vars):
480+
"""Initialize the module as appropriate for POSIX systems."""
364481
# GH-126920: Make sure we don't overwrite any of the keys already set
365-
vars.update(build_time_vars | vars)
482+
vars.update(_get_sysconfigdata() | vars)
483+
366484

367485
def _init_non_posix(vars):
368486
"""Initialize the module as appropriate for NT"""
@@ -601,104 +719,6 @@ def get_config_var(name):
601719
return get_config_vars().get(name)
602720

603721

604-
def get_platform():
605-
"""Return a string that identifies the current platform.
606-
607-
This is used mainly to distinguish platform-specific build directories and
608-
platform-specific built distributions. Typically includes the OS name and
609-
version and the architecture (as supplied by 'os.uname()'), although the
610-
exact information included depends on the OS; on Linux, the kernel version
611-
isn't particularly important.
612-
613-
Examples of returned values:
614-
linux-i586
615-
linux-alpha (?)
616-
solaris-2.6-sun4u
617-
618-
Windows will return one of:
619-
win-amd64 (64bit Windows on AMD64 (aka x86_64, Intel64, EM64T, etc)
620-
win32 (all others - specifically, sys.platform is returned)
621-
622-
For other non-POSIX platforms, currently just returns 'sys.platform'.
623-
624-
"""
625-
if os.name == 'nt':
626-
if 'amd64' in sys.version.lower():
627-
return 'win-amd64'
628-
if '(arm)' in sys.version.lower():
629-
return 'win-arm32'
630-
if '(arm64)' in sys.version.lower():
631-
return 'win-arm64'
632-
return sys.platform
633-
634-
if os.name != "posix" or not hasattr(os, 'uname'):
635-
# XXX what about the architecture? NT is Intel or Alpha
636-
return sys.platform
637-
638-
# Set for cross builds explicitly
639-
if "_PYTHON_HOST_PLATFORM" in os.environ:
640-
return os.environ["_PYTHON_HOST_PLATFORM"]
641-
642-
# Try to distinguish various flavours of Unix
643-
osname, host, release, version, machine = os.uname()
644-
645-
# Convert the OS name to lowercase, remove '/' characters, and translate
646-
# spaces (for "Power Macintosh")
647-
osname = osname.lower().replace('/', '')
648-
machine = machine.replace(' ', '_')
649-
machine = machine.replace('/', '-')
650-
651-
if osname[:5] == "linux":
652-
if sys.platform == "android":
653-
osname = "android"
654-
release = get_config_var("ANDROID_API_LEVEL")
655-
656-
# Wheel tags use the ABI names from Android's own tools.
657-
machine = {
658-
"x86_64": "x86_64",
659-
"i686": "x86",
660-
"aarch64": "arm64_v8a",
661-
"armv7l": "armeabi_v7a",
662-
}[machine]
663-
else:
664-
# At least on Linux/Intel, 'machine' is the processor --
665-
# i386, etc.
666-
# XXX what about Alpha, SPARC, etc?
667-
return f"{osname}-{machine}"
668-
elif osname[:5] == "sunos":
669-
if release[0] >= "5": # SunOS 5 == Solaris 2
670-
osname = "solaris"
671-
release = f"{int(release[0]) - 3}.{release[2:]}"
672-
# We can't use "platform.architecture()[0]" because a
673-
# bootstrap problem. We use a dict to get an error
674-
# if some suspicious happens.
675-
bitness = {2147483647:"32bit", 9223372036854775807:"64bit"}
676-
machine += f".{bitness[sys.maxsize]}"
677-
# fall through to standard osname-release-machine representation
678-
elif osname[:3] == "aix":
679-
from _aix_support import aix_platform
680-
return aix_platform()
681-
elif osname[:6] == "cygwin":
682-
osname = "cygwin"
683-
import re
684-
rel_re = re.compile(r'[\d.]+')
685-
m = rel_re.match(release)
686-
if m:
687-
release = m.group()
688-
elif osname[:6] == "darwin":
689-
if sys.platform == "ios":
690-
release = get_config_vars().get("IPHONEOS_DEPLOYMENT_TARGET", "13.0")
691-
osname = sys.platform
692-
machine = sys.implementation._multiarch
693-
else:
694-
import _osx_support
695-
osname, release, machine = _osx_support.get_platform_osx(
696-
get_config_vars(),
697-
osname, release, machine)
698-
699-
return f"{osname}-{release}-{machine}"
700-
701-
702722
def get_python_version():
703723
return _PY_VERSION_SHORT
704724

Lib/sysconfig/__main__.py

+5-1
Original file line numberDiff line numberDiff line change
@@ -232,10 +232,14 @@ def _generate_posix_vars():
232232

233233
print(f'Written {destfile}')
234234

235+
install_vars = get_config_vars()
236+
# Fix config vars to match the values after install (of the default environment)
237+
install_vars['projectbase'] = install_vars['BINDIR']
238+
install_vars['srcdir'] = install_vars['LIBPL']
235239
# Write a JSON file with the output of sysconfig.get_config_vars
236240
jsonfile = os.path.join(pybuilddir, _get_json_data_name())
237241
with open(jsonfile, 'w') as f:
238-
json.dump(get_config_vars(), f, indent=2)
242+
json.dump(install_vars, f, indent=2)
239243

240244
print(f'Written {jsonfile}')
241245

Lib/test/test_sysconfig.py

+12-2
Original file line numberDiff line numberDiff line change
@@ -650,8 +650,18 @@ def test_sysconfigdata_json(self):
650650

651651
system_config_vars = get_config_vars()
652652

653-
# Ignore keys in the check
654-
for key in ('projectbase', 'srcdir'):
653+
ignore_keys = set()
654+
# Keys dependent on the executable location
655+
if os.path.dirname(sys.executable) != system_config_vars['BINDIR']:
656+
ignore_keys |= {'projectbase'}
657+
# Keys dependent on the environment (different inside virtual environments)
658+
if sys.prefix != sys.base_prefix:
659+
ignore_keys |= {'prefix', 'exec_prefix', 'base', 'platbase'}
660+
# Keys dependent on Python being run from the prefix targetted when building (different on relocatable installs)
661+
if sysconfig._installation_is_relocated():
662+
ignore_keys |= {'prefix', 'exec_prefix', 'base', 'platbase', 'installed_base', 'installed_platbase'}
663+
664+
for key in ignore_keys:
655665
json_config_vars.pop(key)
656666
system_config_vars.pop(key)
657667

0 commit comments

Comments
 (0)