Home | History | Annotate | Download | only in Lib
      1 """Shared OS X support functions."""
      2 
      3 import os
      4 import re
      5 import sys
      6 
      7 __all__ = [
      8     'compiler_fixup',
      9     'customize_config_vars',
     10     'customize_compiler',
     11     'get_platform_osx',
     12 ]
     13 
     14 # configuration variables that may contain universal build flags,
     15 # like "-arch" or "-isdkroot", that may need customization for
     16 # the user environment
     17 _UNIVERSAL_CONFIG_VARS = ('CFLAGS', 'LDFLAGS', 'CPPFLAGS', 'BASECFLAGS',
     18                             'BLDSHARED', 'LDSHARED', 'CC', 'CXX',
     19                             'PY_CFLAGS', 'PY_LDFLAGS', 'PY_CPPFLAGS',
     20                             'PY_CORE_CFLAGS', 'PY_CORE_LDFLAGS')
     21 
     22 # configuration variables that may contain compiler calls
     23 _COMPILER_CONFIG_VARS = ('BLDSHARED', 'LDSHARED', 'CC', 'CXX')
     24 
     25 # prefix added to original configuration variable names
     26 _INITPRE = '_OSX_SUPPORT_INITIAL_'
     27 
     28 
     29 def _find_executable(executable, path=None):
     30     """Tries to find 'executable' in the directories listed in 'path'.
     31 
     32     A string listing directories separated by 'os.pathsep'; defaults to
     33     os.environ['PATH'].  Returns the complete filename or None if not found.
     34     """
     35     if path is None:
     36         path = os.environ['PATH']
     37 
     38     paths = path.split(os.pathsep)
     39     base, ext = os.path.splitext(executable)
     40 
     41     if (sys.platform == 'win32') and (ext != '.exe'):
     42         executable = executable + '.exe'
     43 
     44     if not os.path.isfile(executable):
     45         for p in paths:
     46             f = os.path.join(p, executable)
     47             if os.path.isfile(f):
     48                 # the file exists, we have a shot at spawn working
     49                 return f
     50         return None
     51     else:
     52         return executable
     53 
     54 
     55 def _read_output(commandstring):
     56     """Output from successful command execution or None"""
     57     # Similar to os.popen(commandstring, "r").read(),
     58     # but without actually using os.popen because that
     59     # function is not usable during python bootstrap.
     60     # tempfile is also not available then.
     61     import contextlib
     62     try:
     63         import tempfile
     64         fp = tempfile.NamedTemporaryFile()
     65     except ImportError:
     66         fp = open("/tmp/_osx_support.%s"%(
     67             os.getpid(),), "w+b")
     68 
     69     with contextlib.closing(fp) as fp:
     70         cmd = "%s 2>/dev/null >'%s'" % (commandstring, fp.name)
     71         return fp.read().decode('utf-8').strip() if not os.system(cmd) else None
     72 
     73 
     74 def _find_build_tool(toolname):
     75     """Find a build tool on current path or using xcrun"""
     76     return (_find_executable(toolname)
     77                 or _read_output("/usr/bin/xcrun -find %s" % (toolname,))
     78                 or ''
     79             )
     80 
     81 _SYSTEM_VERSION = None
     82 
     83 def _get_system_version():
     84     """Return the OS X system version as a string"""
     85     # Reading this plist is a documented way to get the system
     86     # version (see the documentation for the Gestalt Manager)
     87     # We avoid using platform.mac_ver to avoid possible bootstrap issues during
     88     # the build of Python itself (distutils is used to build standard library
     89     # extensions).
     90 
     91     global _SYSTEM_VERSION
     92 
     93     if _SYSTEM_VERSION is None:
     94         _SYSTEM_VERSION = ''
     95         try:
     96             f = open('/System/Library/CoreServices/SystemVersion.plist')
     97         except OSError:
     98             # We're on a plain darwin box, fall back to the default
     99             # behaviour.
    100             pass
    101         else:
    102             try:
    103                 m = re.search(r'<key>ProductUserVisibleVersion</key>\s*'
    104                               r'<string>(.*?)</string>', f.read())
    105             finally:
    106                 f.close()
    107             if m is not None:
    108                 _SYSTEM_VERSION = '.'.join(m.group(1).split('.')[:2])
    109             # else: fall back to the default behaviour
    110 
    111     return _SYSTEM_VERSION
    112 
    113 def _remove_original_values(_config_vars):
    114     """Remove original unmodified values for testing"""
    115     # This is needed for higher-level cross-platform tests of get_platform.
    116     for k in list(_config_vars):
    117         if k.startswith(_INITPRE):
    118             del _config_vars[k]
    119 
    120 def _save_modified_value(_config_vars, cv, newvalue):
    121     """Save modified and original unmodified value of configuration var"""
    122 
    123     oldvalue = _config_vars.get(cv, '')
    124     if (oldvalue != newvalue) and (_INITPRE + cv not in _config_vars):
    125         _config_vars[_INITPRE + cv] = oldvalue
    126     _config_vars[cv] = newvalue
    127 
    128 def _supports_universal_builds():
    129     """Returns True if universal builds are supported on this system"""
    130     # As an approximation, we assume that if we are running on 10.4 or above,
    131     # then we are running with an Xcode environment that supports universal
    132     # builds, in particular -isysroot and -arch arguments to the compiler. This
    133     # is in support of allowing 10.4 universal builds to run on 10.3.x systems.
    134 
    135     osx_version = _get_system_version()
    136     if osx_version:
    137         try:
    138             osx_version = tuple(int(i) for i in osx_version.split('.'))
    139         except ValueError:
    140             osx_version = ''
    141     return bool(osx_version >= (10, 4)) if osx_version else False
    142 
    143 
    144 def _find_appropriate_compiler(_config_vars):
    145     """Find appropriate C compiler for extension module builds"""
    146 
    147     # Issue #13590:
    148     #    The OSX location for the compiler varies between OSX
    149     #    (or rather Xcode) releases.  With older releases (up-to 10.5)
    150     #    the compiler is in /usr/bin, with newer releases the compiler
    151     #    can only be found inside Xcode.app if the "Command Line Tools"
    152     #    are not installed.
    153     #
    154     #    Furthermore, the compiler that can be used varies between
    155     #    Xcode releases. Up to Xcode 4 it was possible to use 'gcc-4.2'
    156     #    as the compiler, after that 'clang' should be used because
    157     #    gcc-4.2 is either not present, or a copy of 'llvm-gcc' that
    158     #    miscompiles Python.
    159 
    160     # skip checks if the compiler was overridden with a CC env variable
    161     if 'CC' in os.environ:
    162         return _config_vars
    163 
    164     # The CC config var might contain additional arguments.
    165     # Ignore them while searching.
    166     cc = oldcc = _config_vars['CC'].split()[0]
    167     if not _find_executable(cc):
    168         # Compiler is not found on the shell search PATH.
    169         # Now search for clang, first on PATH (if the Command LIne
    170         # Tools have been installed in / or if the user has provided
    171         # another location via CC).  If not found, try using xcrun
    172         # to find an uninstalled clang (within a selected Xcode).
    173 
    174         # NOTE: Cannot use subprocess here because of bootstrap
    175         # issues when building Python itself (and os.popen is
    176         # implemented on top of subprocess and is therefore not
    177         # usable as well)
    178 
    179         cc = _find_build_tool('clang')
    180 
    181     elif os.path.basename(cc).startswith('gcc'):
    182         # Compiler is GCC, check if it is LLVM-GCC
    183         data = _read_output("'%s' --version"
    184                              % (cc.replace("'", "'\"'\"'"),))
    185         if data and 'llvm-gcc' in data:
    186             # Found LLVM-GCC, fall back to clang
    187             cc = _find_build_tool('clang')
    188 
    189     if not cc:
    190         raise SystemError(
    191                "Cannot locate working compiler")
    192 
    193     if cc != oldcc:
    194         # Found a replacement compiler.
    195         # Modify config vars using new compiler, if not already explicitly
    196         # overridden by an env variable, preserving additional arguments.
    197         for cv in _COMPILER_CONFIG_VARS:
    198             if cv in _config_vars and cv not in os.environ:
    199                 cv_split = _config_vars[cv].split()
    200                 cv_split[0] = cc if cv != 'CXX' else cc + '++'
    201                 _save_modified_value(_config_vars, cv, ' '.join(cv_split))
    202 
    203     return _config_vars
    204 
    205 
    206 def _remove_universal_flags(_config_vars):
    207     """Remove all universal build arguments from config vars"""
    208 
    209     for cv in _UNIVERSAL_CONFIG_VARS:
    210         # Do not alter a config var explicitly overridden by env var
    211         if cv in _config_vars and cv not in os.environ:
    212             flags = _config_vars[cv]
    213             flags = re.sub(r'-arch\s+\w+\s', ' ', flags, flags=re.ASCII)
    214             flags = re.sub('-isysroot [^ \t]*', ' ', flags)
    215             _save_modified_value(_config_vars, cv, flags)
    216 
    217     return _config_vars
    218 
    219 
    220 def _remove_unsupported_archs(_config_vars):
    221     """Remove any unsupported archs from config vars"""
    222     # Different Xcode releases support different sets for '-arch'
    223     # flags. In particular, Xcode 4.x no longer supports the
    224     # PPC architectures.
    225     #
    226     # This code automatically removes '-arch ppc' and '-arch ppc64'
    227     # when these are not supported. That makes it possible to
    228     # build extensions on OSX 10.7 and later with the prebuilt
    229     # 32-bit installer on the python.org website.
    230 
    231     # skip checks if the compiler was overridden with a CC env variable
    232     if 'CC' in os.environ:
    233         return _config_vars
    234 
    235     if re.search(r'-arch\s+ppc', _config_vars['CFLAGS']) is not None:
    236         # NOTE: Cannot use subprocess here because of bootstrap
    237         # issues when building Python itself
    238         status = os.system(
    239             """echo 'int main{};' | """
    240             """'%s' -c -arch ppc -x c -o /dev/null /dev/null 2>/dev/null"""
    241             %(_config_vars['CC'].replace("'", "'\"'\"'"),))
    242         if status:
    243             # The compile failed for some reason.  Because of differences
    244             # across Xcode and compiler versions, there is no reliable way
    245             # to be sure why it failed.  Assume here it was due to lack of
    246             # PPC support and remove the related '-arch' flags from each
    247             # config variables not explicitly overridden by an environment
    248             # variable.  If the error was for some other reason, we hope the
    249             # failure will show up again when trying to compile an extension
    250             # module.
    251             for cv in _UNIVERSAL_CONFIG_VARS:
    252                 if cv in _config_vars and cv not in os.environ:
    253                     flags = _config_vars[cv]
    254                     flags = re.sub(r'-arch\s+ppc\w*\s', ' ', flags)
    255                     _save_modified_value(_config_vars, cv, flags)
    256 
    257     return _config_vars
    258 
    259 
    260 def _override_all_archs(_config_vars):
    261     """Allow override of all archs with ARCHFLAGS env var"""
    262     # NOTE: This name was introduced by Apple in OSX 10.5 and
    263     # is used by several scripting languages distributed with
    264     # that OS release.
    265     if 'ARCHFLAGS' in os.environ:
    266         arch = os.environ['ARCHFLAGS']
    267         for cv in _UNIVERSAL_CONFIG_VARS:
    268             if cv in _config_vars and '-arch' in _config_vars[cv]:
    269                 flags = _config_vars[cv]
    270                 flags = re.sub(r'-arch\s+\w+\s', ' ', flags)
    271                 flags = flags + ' ' + arch
    272                 _save_modified_value(_config_vars, cv, flags)
    273 
    274     return _config_vars
    275 
    276 
    277 def _check_for_unavailable_sdk(_config_vars):
    278     """Remove references to any SDKs not available"""
    279     # If we're on OSX 10.5 or later and the user tries to
    280     # compile an extension using an SDK that is not present
    281     # on the current machine it is better to not use an SDK
    282     # than to fail.  This is particularly important with
    283     # the standalone Command Line Tools alternative to a
    284     # full-blown Xcode install since the CLT packages do not
    285     # provide SDKs.  If the SDK is not present, it is assumed
    286     # that the header files and dev libs have been installed
    287     # to /usr and /System/Library by either a standalone CLT
    288     # package or the CLT component within Xcode.
    289     cflags = _config_vars.get('CFLAGS', '')
    290     m = re.search(r'-isysroot\s+(\S+)', cflags)
    291     if m is not None:
    292         sdk = m.group(1)
    293         if not os.path.exists(sdk):
    294             for cv in _UNIVERSAL_CONFIG_VARS:
    295                 # Do not alter a config var explicitly overridden by env var
    296                 if cv in _config_vars and cv not in os.environ:
    297                     flags = _config_vars[cv]
    298                     flags = re.sub(r'-isysroot\s+\S+(?:\s|$)', ' ', flags)
    299                     _save_modified_value(_config_vars, cv, flags)
    300 
    301     return _config_vars
    302 
    303 
    304 def compiler_fixup(compiler_so, cc_args):
    305     """
    306     This function will strip '-isysroot PATH' and '-arch ARCH' from the
    307     compile flags if the user has specified one them in extra_compile_flags.
    308 
    309     This is needed because '-arch ARCH' adds another architecture to the
    310     build, without a way to remove an architecture. Furthermore GCC will
    311     barf if multiple '-isysroot' arguments are present.
    312     """
    313     stripArch = stripSysroot = False
    314 
    315     compiler_so = list(compiler_so)
    316 
    317     if not _supports_universal_builds():
    318         # OSX before 10.4.0, these don't support -arch and -isysroot at
    319         # all.
    320         stripArch = stripSysroot = True
    321     else:
    322         stripArch = '-arch' in cc_args
    323         stripSysroot = '-isysroot' in cc_args
    324 
    325     if stripArch or 'ARCHFLAGS' in os.environ:
    326         while True:
    327             try:
    328                 index = compiler_so.index('-arch')
    329                 # Strip this argument and the next one:
    330                 del compiler_so[index:index+2]
    331             except ValueError:
    332                 break
    333 
    334     if 'ARCHFLAGS' in os.environ and not stripArch:
    335         # User specified different -arch flags in the environ,
    336         # see also distutils.sysconfig
    337         compiler_so = compiler_so + os.environ['ARCHFLAGS'].split()
    338 
    339     if stripSysroot:
    340         while True:
    341             try:
    342                 index = compiler_so.index('-isysroot')
    343                 # Strip this argument and the next one:
    344                 del compiler_so[index:index+2]
    345             except ValueError:
    346                 break
    347 
    348     # Check if the SDK that is used during compilation actually exists,
    349     # the universal build requires the usage of a universal SDK and not all
    350     # users have that installed by default.
    351     sysroot = None
    352     if '-isysroot' in cc_args:
    353         idx = cc_args.index('-isysroot')
    354         sysroot = cc_args[idx+1]
    355     elif '-isysroot' in compiler_so:
    356         idx = compiler_so.index('-isysroot')
    357         sysroot = compiler_so[idx+1]
    358 
    359     if sysroot and not os.path.isdir(sysroot):
    360         from distutils import log
    361         log.warn("Compiling with an SDK that doesn't seem to exist: %s",
    362                 sysroot)
    363         log.warn("Please check your Xcode installation")
    364 
    365     return compiler_so
    366 
    367 
    368 def customize_config_vars(_config_vars):
    369     """Customize Python build configuration variables.
    370 
    371     Called internally from sysconfig with a mutable mapping
    372     containing name/value pairs parsed from the configured
    373     makefile used to build this interpreter.  Returns
    374     the mapping updated as needed to reflect the environment
    375     in which the interpreter is running; in the case of
    376     a Python from a binary installer, the installed
    377     environment may be very different from the build
    378     environment, i.e. different OS levels, different
    379     built tools, different available CPU architectures.
    380 
    381     This customization is performed whenever
    382     distutils.sysconfig.get_config_vars() is first
    383     called.  It may be used in environments where no
    384     compilers are present, i.e. when installing pure
    385     Python dists.  Customization of compiler paths
    386     and detection of unavailable archs is deferred
    387     until the first extension module build is
    388     requested (in distutils.sysconfig.customize_compiler).
    389 
    390     Currently called from distutils.sysconfig
    391     """
    392 
    393     if not _supports_universal_builds():
    394         # On Mac OS X before 10.4, check if -arch and -isysroot
    395         # are in CFLAGS or LDFLAGS and remove them if they are.
    396         # This is needed when building extensions on a 10.3 system
    397         # using a universal build of python.
    398         _remove_universal_flags(_config_vars)
    399 
    400     # Allow user to override all archs with ARCHFLAGS env var
    401     _override_all_archs(_config_vars)
    402 
    403     # Remove references to sdks that are not found
    404     _check_for_unavailable_sdk(_config_vars)
    405 
    406     return _config_vars
    407 
    408 
    409 def customize_compiler(_config_vars):
    410     """Customize compiler path and configuration variables.
    411 
    412     This customization is performed when the first
    413     extension module build is requested
    414     in distutils.sysconfig.customize_compiler).
    415     """
    416 
    417     # Find a compiler to use for extension module builds
    418     _find_appropriate_compiler(_config_vars)
    419 
    420     # Remove ppc arch flags if not supported here
    421     _remove_unsupported_archs(_config_vars)
    422 
    423     # Allow user to override all archs with ARCHFLAGS env var
    424     _override_all_archs(_config_vars)
    425 
    426     return _config_vars
    427 
    428 
    429 def get_platform_osx(_config_vars, osname, release, machine):
    430     """Filter values for get_platform()"""
    431     # called from get_platform() in sysconfig and distutils.util
    432     #
    433     # For our purposes, we'll assume that the system version from
    434     # distutils' perspective is what MACOSX_DEPLOYMENT_TARGET is set
    435     # to. This makes the compatibility story a bit more sane because the
    436     # machine is going to compile and link as if it were
    437     # MACOSX_DEPLOYMENT_TARGET.
    438 
    439     macver = _config_vars.get('MACOSX_DEPLOYMENT_TARGET', '')
    440     macrelease = _get_system_version() or macver
    441     macver = macver or macrelease
    442 
    443     if macver:
    444         release = macver
    445         osname = "macosx"
    446 
    447         # Use the original CFLAGS value, if available, so that we
    448         # return the same machine type for the platform string.
    449         # Otherwise, distutils may consider this a cross-compiling
    450         # case and disallow installs.
    451         cflags = _config_vars.get(_INITPRE+'CFLAGS',
    452                                     _config_vars.get('CFLAGS', ''))
    453         if macrelease:
    454             try:
    455                 macrelease = tuple(int(i) for i in macrelease.split('.')[0:2])
    456             except ValueError:
    457                 macrelease = (10, 0)
    458         else:
    459             # assume no universal support
    460             macrelease = (10, 0)
    461 
    462         if (macrelease >= (10, 4)) and '-arch' in cflags.strip():
    463             # The universal build will build fat binaries, but not on
    464             # systems before 10.4
    465 
    466             machine = 'fat'
    467 
    468             archs = re.findall(r'-arch\s+(\S+)', cflags)
    469             archs = tuple(sorted(set(archs)))
    470 
    471             if len(archs) == 1:
    472                 machine = archs[0]
    473             elif archs == ('i386', 'ppc'):
    474                 machine = 'fat'
    475             elif archs == ('i386', 'x86_64'):
    476                 machine = 'intel'
    477             elif archs == ('i386', 'ppc', 'x86_64'):
    478                 machine = 'fat3'
    479             elif archs == ('ppc64', 'x86_64'):
    480                 machine = 'fat64'
    481             elif archs == ('i386', 'ppc', 'ppc64', 'x86_64'):
    482                 machine = 'universal'
    483             else:
    484                 raise ValueError(
    485                    "Don't know machine value for archs=%r" % (archs,))
    486 
    487         elif machine == 'i386':
    488             # On OSX the machine type returned by uname is always the
    489             # 32-bit variant, even if the executable architecture is
    490             # the 64-bit variant
    491             if sys.maxsize >= 2**32:
    492                 machine = 'x86_64'
    493 
    494         elif machine in ('PowerPC', 'Power_Macintosh'):
    495             # Pick a sane name for the PPC architecture.
    496             # See 'i386' case
    497             if sys.maxsize >= 2**32:
    498                 machine = 'ppc64'
    499             else:
    500                 machine = 'ppc'
    501 
    502     return (osname, release, machine)
    503