mirror of https://github.com/python/cpython
575 lines
21 KiB
Python
575 lines
21 KiB
Python
"""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)
|