Spaces:
Sleeping
Sleeping
"""Shared OS X support functions.""" | |
import os | |
import re | |
import sys | |
__all__ = [ | |
'compiler_fixup', | |
'customize_config_vars', | |
'customize_compiler', | |
'get_platform_osx', | |
] | |
# configuration variables that may contain universal build flags, | |
# like "-arch" or "-isdkroot", that may need customization for | |
# the user environment | |
_UNIVERSAL_CONFIG_VARS = ('CFLAGS', 'LDFLAGS', 'CPPFLAGS', 'BASECFLAGS', | |
'BLDSHARED', 'LDSHARED', 'CC', 'CXX', | |
'PY_CFLAGS', 'PY_LDFLAGS', 'PY_CPPFLAGS', | |
'PY_CORE_CFLAGS', 'PY_CORE_LDFLAGS') | |
# configuration variables that may contain compiler calls | |
_COMPILER_CONFIG_VARS = ('BLDSHARED', 'LDSHARED', 'CC', 'CXX') | |
# prefix added to original configuration variable names | |
_INITPRE = '_OSX_SUPPORT_INITIAL_' | |
def _find_executable(executable, path=None): | |
"""Tries to find 'executable' in the directories listed in 'path'. | |
A string listing directories separated by 'os.pathsep'; defaults to | |
os.environ['PATH']. Returns the complete filename or None if not found. | |
""" | |
if path is None: | |
path = os.environ['PATH'] | |
paths = path.split(os.pathsep) | |
base, ext = os.path.splitext(executable) | |
if (sys.platform == 'win32') and (ext != '.exe'): | |
executable = executable + '.exe' | |
if not os.path.isfile(executable): | |
for p in paths: | |
f = os.path.join(p, executable) | |
if os.path.isfile(f): | |
# the file exists, we have a shot at spawn working | |
return f | |
return None | |
else: | |
return executable | |
def _read_output(commandstring, capture_stderr=False): | |
"""Output from successful command execution or None""" | |
# Similar to os.popen(commandstring, "r").read(), | |
# but without actually using os.popen because that | |
# function is not usable during python bootstrap. | |
# tempfile is also not available then. | |
import contextlib | |
try: | |
import tempfile | |
fp = tempfile.NamedTemporaryFile() | |
except ImportError: | |
fp = open("/tmp/_osx_support.%s"%( | |
os.getpid(),), "w+b") | |
with contextlib.closing(fp) as fp: | |
if capture_stderr: | |
cmd = "%s >'%s' 2>&1" % (commandstring, fp.name) | |
else: | |
cmd = "%s 2>/dev/null >'%s'" % (commandstring, fp.name) | |
return fp.read().decode('utf-8').strip() if not os.system(cmd) else None | |
def _find_build_tool(toolname): | |
"""Find a build tool on current path or using xcrun""" | |
return (_find_executable(toolname) | |
or _read_output("/usr/bin/xcrun -find %s" % (toolname,)) | |
or '' | |
) | |
_SYSTEM_VERSION = None | |
def _get_system_version(): | |
"""Return the OS X system version as a string""" | |
# Reading this plist is a documented way to get the system | |
# version (see the documentation for the Gestalt Manager) | |
# We avoid using platform.mac_ver to avoid possible bootstrap issues during | |
# the build of Python itself (distutils is used to build standard library | |
# extensions). | |
global _SYSTEM_VERSION | |
if _SYSTEM_VERSION is None: | |
_SYSTEM_VERSION = '' | |
try: | |
f = open('/System/Library/CoreServices/SystemVersion.plist', encoding="utf-8") | |
except OSError: | |
# We're on a plain darwin box, fall back to the default | |
# behaviour. | |
pass | |
else: | |
try: | |
m = re.search(r'<key>ProductUserVisibleVersion</key>\s*' | |
r'<string>(.*?)</string>', f.read()) | |
finally: | |
f.close() | |
if m is not None: | |
_SYSTEM_VERSION = '.'.join(m.group(1).split('.')[:2]) | |
# else: fall back to the default behaviour | |
return _SYSTEM_VERSION | |
_SYSTEM_VERSION_TUPLE = None | |
def _get_system_version_tuple(): | |
""" | |
Return the macOS system version as a tuple | |
The return value is safe to use to compare | |
two version numbers. | |
""" | |
global _SYSTEM_VERSION_TUPLE | |
if _SYSTEM_VERSION_TUPLE is None: | |
osx_version = _get_system_version() | |
if osx_version: | |
try: | |
_SYSTEM_VERSION_TUPLE = tuple(int(i) for i in osx_version.split('.')) | |
except ValueError: | |
_SYSTEM_VERSION_TUPLE = () | |
return _SYSTEM_VERSION_TUPLE | |
def _remove_original_values(_config_vars): | |
"""Remove original unmodified values for testing""" | |
# This is needed for higher-level cross-platform tests of get_platform. | |
for k in list(_config_vars): | |
if k.startswith(_INITPRE): | |
del _config_vars[k] | |
def _save_modified_value(_config_vars, cv, newvalue): | |
"""Save modified and original unmodified value of configuration var""" | |
oldvalue = _config_vars.get(cv, '') | |
if (oldvalue != newvalue) and (_INITPRE + cv not in _config_vars): | |
_config_vars[_INITPRE + cv] = oldvalue | |
_config_vars[cv] = newvalue | |
_cache_default_sysroot = None | |
def _default_sysroot(cc): | |
""" Returns the root of the default SDK for this system, or '/' """ | |
global _cache_default_sysroot | |
if _cache_default_sysroot is not None: | |
return _cache_default_sysroot | |
contents = _read_output('%s -c -E -v - </dev/null' % (cc,), True) | |
in_incdirs = False | |
for line in contents.splitlines(): | |
if line.startswith("#include <...>"): | |
in_incdirs = True | |
elif line.startswith("End of search list"): | |
in_incdirs = False | |
elif in_incdirs: | |
line = line.strip() | |
if line == '/usr/include': | |
_cache_default_sysroot = '/' | |
elif line.endswith(".sdk/usr/include"): | |
_cache_default_sysroot = line[:-12] | |
if _cache_default_sysroot is None: | |
_cache_default_sysroot = '/' | |
return _cache_default_sysroot | |
def _supports_universal_builds(): | |
"""Returns True if universal builds are supported on this system""" | |
# As an approximation, we assume that if we are running on 10.4 or above, | |
# then we are running with an Xcode environment that supports universal | |
# builds, in particular -isysroot and -arch arguments to the compiler. This | |
# is in support of allowing 10.4 universal builds to run on 10.3.x systems. | |
osx_version = _get_system_version_tuple() | |
return bool(osx_version >= (10, 4)) if osx_version else False | |
def _supports_arm64_builds(): | |
"""Returns True if arm64 builds are supported on this system""" | |
# There are two sets of systems supporting macOS/arm64 builds: | |
# 1. macOS 11 and later, unconditionally | |
# 2. macOS 10.15 with Xcode 12.2 or later | |
# For now the second category is ignored. | |
osx_version = _get_system_version_tuple() | |
return osx_version >= (11, 0) if osx_version else False | |
def _find_appropriate_compiler(_config_vars): | |
"""Find appropriate C compiler for extension module builds""" | |
# Issue #13590: | |
# The OSX location for the compiler varies between OSX | |
# (or rather Xcode) releases. With older releases (up-to 10.5) | |
# the compiler is in /usr/bin, with newer releases the compiler | |
# can only be found inside Xcode.app if the "Command Line Tools" | |
# are not installed. | |
# | |
# Furthermore, the compiler that can be used varies between | |
# Xcode releases. Up to Xcode 4 it was possible to use 'gcc-4.2' | |
# as the compiler, after that 'clang' should be used because | |
# gcc-4.2 is either not present, or a copy of 'llvm-gcc' that | |
# miscompiles Python. | |
# skip checks if the compiler was overridden with a CC env variable | |
if 'CC' in os.environ: | |
return _config_vars | |
# The CC config var might contain additional arguments. | |
# Ignore them while searching. | |
cc = oldcc = _config_vars['CC'].split()[0] | |
if not _find_executable(cc): | |
# Compiler is not found on the shell search PATH. | |
# Now search for clang, first on PATH (if the Command LIne | |
# Tools have been installed in / or if the user has provided | |
# another location via CC). If not found, try using xcrun | |
# to find an uninstalled clang (within a selected Xcode). | |
# NOTE: Cannot use subprocess here because of bootstrap | |
# issues when building Python itself (and os.popen is | |
# implemented on top of subprocess and is therefore not | |
# usable as well) | |
cc = _find_build_tool('clang') | |
elif os.path.basename(cc).startswith('gcc'): | |
# Compiler is GCC, check if it is LLVM-GCC | |
data = _read_output("'%s' --version" | |
% (cc.replace("'", "'\"'\"'"),)) | |
if data and 'llvm-gcc' in data: | |
# Found LLVM-GCC, fall back to clang | |
cc = _find_build_tool('clang') | |
if not cc: | |
raise SystemError( | |
"Cannot locate working compiler") | |
if cc != oldcc: | |
# Found a replacement compiler. | |
# Modify config vars using new compiler, if not already explicitly | |
# overridden by an env variable, preserving additional arguments. | |
for cv in _COMPILER_CONFIG_VARS: | |
if cv in _config_vars and cv not in os.environ: | |
cv_split = _config_vars[cv].split() | |
cv_split[0] = cc if cv != 'CXX' else cc + '++' | |
_save_modified_value(_config_vars, cv, ' '.join(cv_split)) | |
return _config_vars | |
def _remove_universal_flags(_config_vars): | |
"""Remove all universal build arguments from config vars""" | |
for cv in _UNIVERSAL_CONFIG_VARS: | |
# Do not alter a config var explicitly overridden by env var | |
if cv in _config_vars and cv not in os.environ: | |
flags = _config_vars[cv] | |
flags = re.sub(r'-arch\s+\w+\s', ' ', flags, flags=re.ASCII) | |
flags = re.sub(r'-isysroot\s*\S+', ' ', flags) | |
_save_modified_value(_config_vars, cv, flags) | |
return _config_vars | |
def _remove_unsupported_archs(_config_vars): | |
"""Remove any unsupported archs from config vars""" | |
# Different Xcode releases support different sets for '-arch' | |
# flags. In particular, Xcode 4.x no longer supports the | |
# PPC architectures. | |
# | |
# This code automatically removes '-arch ppc' and '-arch ppc64' | |
# when these are not supported. That makes it possible to | |
# build extensions on OSX 10.7 and later with the prebuilt | |
# 32-bit installer on the python.org website. | |
# skip checks if the compiler was overridden with a CC env variable | |
if 'CC' in os.environ: | |
return _config_vars | |
if re.search(r'-arch\s+ppc', _config_vars['CFLAGS']) is not None: | |
# NOTE: Cannot use subprocess here because of bootstrap | |
# issues when building Python itself | |
status = os.system( | |
"""echo 'int main{};' | """ | |
"""'%s' -c -arch ppc -x c -o /dev/null /dev/null 2>/dev/null""" | |
%(_config_vars['CC'].replace("'", "'\"'\"'"),)) | |
if status: | |
# The compile failed for some reason. Because of differences | |
# across Xcode and compiler versions, there is no reliable way | |
# to be sure why it failed. Assume here it was due to lack of | |
# PPC support and remove the related '-arch' flags from each | |
# config variables not explicitly overridden by an environment | |
# variable. If the error was for some other reason, we hope the | |
# failure will show up again when trying to compile an extension | |
# module. | |
for cv in _UNIVERSAL_CONFIG_VARS: | |
if cv in _config_vars and cv not in os.environ: | |
flags = _config_vars[cv] | |
flags = re.sub(r'-arch\s+ppc\w*\s', ' ', flags) | |
_save_modified_value(_config_vars, cv, flags) | |
return _config_vars | |
def _override_all_archs(_config_vars): | |
"""Allow override of all archs with ARCHFLAGS env var""" | |
# NOTE: This name was introduced by Apple in OSX 10.5 and | |
# is used by several scripting languages distributed with | |
# that OS release. | |
if 'ARCHFLAGS' in os.environ: | |
arch = os.environ['ARCHFLAGS'] | |
for cv in _UNIVERSAL_CONFIG_VARS: | |
if cv in _config_vars and '-arch' in _config_vars[cv]: | |
flags = _config_vars[cv] | |
flags = re.sub(r'-arch\s+\w+\s', ' ', flags) | |
flags = flags + ' ' + arch | |
_save_modified_value(_config_vars, cv, flags) | |
return _config_vars | |
def _check_for_unavailable_sdk(_config_vars): | |
"""Remove references to any SDKs not available""" | |
# If we're on OSX 10.5 or later and the user tries to | |
# compile an extension using an SDK that is not present | |
# on the current machine it is better to not use an SDK | |
# than to fail. This is particularly important with | |
# the standalone Command Line Tools alternative to a | |
# full-blown Xcode install since the CLT packages do not | |
# provide SDKs. If the SDK is not present, it is assumed | |
# that the header files and dev libs have been installed | |
# to /usr and /System/Library by either a standalone CLT | |
# package or the CLT component within Xcode. | |
cflags = _config_vars.get('CFLAGS', '') | |
m = re.search(r'-isysroot\s*(\S+)', cflags) | |
if m is not None: | |
sdk = m.group(1) | |
if not os.path.exists(sdk): | |
for cv in _UNIVERSAL_CONFIG_VARS: | |
# Do not alter a config var explicitly overridden by env var | |
if cv in _config_vars and cv not in os.environ: | |
flags = _config_vars[cv] | |
flags = re.sub(r'-isysroot\s*\S+(?:\s|$)', ' ', flags) | |
_save_modified_value(_config_vars, cv, flags) | |
return _config_vars | |
def compiler_fixup(compiler_so, cc_args): | |
""" | |
This function will strip '-isysroot PATH' and '-arch ARCH' from the | |
compile flags if the user has specified one them in extra_compile_flags. | |
This is needed because '-arch ARCH' adds another architecture to the | |
build, without a way to remove an architecture. Furthermore GCC will | |
barf if multiple '-isysroot' arguments are present. | |
""" | |
stripArch = stripSysroot = False | |
compiler_so = list(compiler_so) | |
if not _supports_universal_builds(): | |
# OSX before 10.4.0, these don't support -arch and -isysroot at | |
# all. | |
stripArch = stripSysroot = True | |
else: | |
stripArch = '-arch' in cc_args | |
stripSysroot = any(arg for arg in cc_args if arg.startswith('-isysroot')) | |
if stripArch or 'ARCHFLAGS' in os.environ: | |
while True: | |
try: | |
index = compiler_so.index('-arch') | |
# Strip this argument and the next one: | |
del compiler_so[index:index+2] | |
except ValueError: | |
break | |
elif not _supports_arm64_builds(): | |
# Look for "-arch arm64" and drop that | |
for idx in reversed(range(len(compiler_so))): | |
if compiler_so[idx] == '-arch' and compiler_so[idx+1] == "arm64": | |
del compiler_so[idx:idx+2] | |
if 'ARCHFLAGS' in os.environ and not stripArch: | |
# User specified different -arch flags in the environ, | |
# see also distutils.sysconfig | |
compiler_so = compiler_so + os.environ['ARCHFLAGS'].split() | |
if stripSysroot: | |
while True: | |
indices = [i for i,x in enumerate(compiler_so) if x.startswith('-isysroot')] | |
if not indices: | |
break | |
index = indices[0] | |
if compiler_so[index] == '-isysroot': | |
# Strip this argument and the next one: | |
del compiler_so[index:index+2] | |
else: | |
# It's '-isysroot/some/path' in one arg | |
del compiler_so[index:index+1] | |
# Check if the SDK that is used during compilation actually exists, | |
# the universal build requires the usage of a universal SDK and not all | |
# users have that installed by default. | |
sysroot = None | |
argvar = cc_args | |
indices = [i for i,x in enumerate(cc_args) if x.startswith('-isysroot')] | |
if not indices: | |
argvar = compiler_so | |
indices = [i for i,x in enumerate(compiler_so) if x.startswith('-isysroot')] | |
for idx in indices: | |
if argvar[idx] == '-isysroot': | |
sysroot = argvar[idx+1] | |
break | |
else: | |
sysroot = argvar[idx][len('-isysroot'):] | |
break | |
if sysroot and not os.path.isdir(sysroot): | |
sys.stderr.write(f"Compiling with an SDK that doesn't seem to exist: {sysroot}\n") | |
sys.stderr.write("Please check your Xcode installation\n") | |
sys.stderr.flush() | |
return compiler_so | |
def customize_config_vars(_config_vars): | |
"""Customize Python build configuration variables. | |
Called internally from sysconfig with a mutable mapping | |
containing name/value pairs parsed from the configured | |
makefile used to build this interpreter. Returns | |
the mapping updated as needed to reflect the environment | |
in which the interpreter is running; in the case of | |
a Python from a binary installer, the installed | |
environment may be very different from the build | |
environment, i.e. different OS levels, different | |
built tools, different available CPU architectures. | |
This customization is performed whenever | |
distutils.sysconfig.get_config_vars() is first | |
called. It may be used in environments where no | |
compilers are present, i.e. when installing pure | |
Python dists. Customization of compiler paths | |
and detection of unavailable archs is deferred | |
until the first extension module build is | |
requested (in distutils.sysconfig.customize_compiler). | |
Currently called from distutils.sysconfig | |
""" | |
if not _supports_universal_builds(): | |
# On Mac OS X before 10.4, check if -arch and -isysroot | |
# are in CFLAGS or LDFLAGS and remove them if they are. | |
# This is needed when building extensions on a 10.3 system | |
# using a universal build of python. | |
_remove_universal_flags(_config_vars) | |
# Allow user to override all archs with ARCHFLAGS env var | |
_override_all_archs(_config_vars) | |
# Remove references to sdks that are not found | |
_check_for_unavailable_sdk(_config_vars) | |
return _config_vars | |
def customize_compiler(_config_vars): | |
"""Customize compiler path and configuration variables. | |
This customization is performed when the first | |
extension module build is requested | |
in distutils.sysconfig.customize_compiler. | |
""" | |
# Find a compiler to use for extension module builds | |
_find_appropriate_compiler(_config_vars) | |
# Remove ppc arch flags if not supported here | |
_remove_unsupported_archs(_config_vars) | |
# Allow user to override all archs with ARCHFLAGS env var | |
_override_all_archs(_config_vars) | |
return _config_vars | |
def get_platform_osx(_config_vars, osname, release, machine): | |
"""Filter values for get_platform()""" | |
# called from get_platform() in sysconfig and distutils.util | |
# | |
# For our purposes, we'll assume that the system version from | |
# distutils' perspective is what MACOSX_DEPLOYMENT_TARGET is set | |
# to. This makes the compatibility story a bit more sane because the | |
# machine is going to compile and link as if it were | |
# MACOSX_DEPLOYMENT_TARGET. | |
macver = _config_vars.get('MACOSX_DEPLOYMENT_TARGET', '') | |
macrelease = _get_system_version() or macver | |
macver = macver or macrelease | |
if macver: | |
release = macver | |
osname = "macosx" | |
# Use the original CFLAGS value, if available, so that we | |
# return the same machine type for the platform string. | |
# Otherwise, distutils may consider this a cross-compiling | |
# case and disallow installs. | |
cflags = _config_vars.get(_INITPRE+'CFLAGS', | |
_config_vars.get('CFLAGS', '')) | |
if macrelease: | |
try: | |
macrelease = tuple(int(i) for i in macrelease.split('.')[0:2]) | |
except ValueError: | |
macrelease = (10, 3) | |
else: | |
# assume no universal support | |
macrelease = (10, 3) | |
if (macrelease >= (10, 4)) and '-arch' in cflags.strip(): | |
# The universal build will build fat binaries, but not on | |
# systems before 10.4 | |
machine = 'fat' | |
archs = re.findall(r'-arch\s+(\S+)', cflags) | |
archs = tuple(sorted(set(archs))) | |
if len(archs) == 1: | |
machine = archs[0] | |
elif archs == ('arm64', 'x86_64'): | |
machine = 'universal2' | |
elif archs == ('i386', 'ppc'): | |
machine = 'fat' | |
elif archs == ('i386', 'x86_64'): | |
machine = 'intel' | |
elif archs == ('i386', 'ppc', 'x86_64'): | |
machine = 'fat3' | |
elif archs == ('ppc64', 'x86_64'): | |
machine = 'fat64' | |
elif archs == ('i386', 'ppc', 'ppc64', 'x86_64'): | |
machine = 'universal' | |
else: | |
raise ValueError( | |
"Don't know machine value for archs=%r" % (archs,)) | |
elif machine == 'i386': | |
# On OSX the machine type returned by uname is always the | |
# 32-bit variant, even if the executable architecture is | |
# the 64-bit variant | |
if sys.maxsize >= 2**32: | |
machine = 'x86_64' | |
elif machine in ('PowerPC', 'Power_Macintosh'): | |
# Pick a sane name for the PPC architecture. | |
# See 'i386' case | |
if sys.maxsize >= 2**32: | |
machine = 'ppc64' | |
else: | |
machine = 'ppc' | |
return (osname, release, machine) | |