• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1"""Shared OS X support functions."""
2
3import os
4import re
5import 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
29def _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
55def _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
74def _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
83def _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
113def _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
120def _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
128def _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
144def _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
206def _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(r'-isysroot\s*\S+', ' ', flags)
215            _save_modified_value(_config_vars, cv, flags)
216
217    return _config_vars
218
219
220def _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
260def _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
277def _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
304def 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 = any(arg for arg in cc_args if arg.startswith('-isysroot'))
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            indices = [i for i,x in enumerate(compiler_so) if x.startswith('-isysroot')]
342            if not indices:
343                break
344            index = indices[0]
345            if compiler_so[index] == '-isysroot':
346                # Strip this argument and the next one:
347                del compiler_so[index:index+2]
348            else:
349                # It's '-isysroot/some/path' in one arg
350                del compiler_so[index:index+1]
351
352    # Check if the SDK that is used during compilation actually exists,
353    # the universal build requires the usage of a universal SDK and not all
354    # users have that installed by default.
355    sysroot = None
356    argvar = cc_args
357    indices = [i for i,x in enumerate(cc_args) if x.startswith('-isysroot')]
358    if not indices:
359        argvar = compiler_so
360        indices = [i for i,x in enumerate(compiler_so) if x.startswith('-isysroot')]
361
362    for idx in indices:
363        if argvar[idx] == '-isysroot':
364            sysroot = argvar[idx+1]
365            break
366        else:
367            sysroot = argvar[idx][len('-isysroot'):]
368            break
369
370    if sysroot and not os.path.isdir(sysroot):
371        from distutils import log
372        log.warn("Compiling with an SDK that doesn't seem to exist: %s",
373                sysroot)
374        log.warn("Please check your Xcode installation")
375
376    return compiler_so
377
378
379def customize_config_vars(_config_vars):
380    """Customize Python build configuration variables.
381
382    Called internally from sysconfig with a mutable mapping
383    containing name/value pairs parsed from the configured
384    makefile used to build this interpreter.  Returns
385    the mapping updated as needed to reflect the environment
386    in which the interpreter is running; in the case of
387    a Python from a binary installer, the installed
388    environment may be very different from the build
389    environment, i.e. different OS levels, different
390    built tools, different available CPU architectures.
391
392    This customization is performed whenever
393    distutils.sysconfig.get_config_vars() is first
394    called.  It may be used in environments where no
395    compilers are present, i.e. when installing pure
396    Python dists.  Customization of compiler paths
397    and detection of unavailable archs is deferred
398    until the first extension module build is
399    requested (in distutils.sysconfig.customize_compiler).
400
401    Currently called from distutils.sysconfig
402    """
403
404    if not _supports_universal_builds():
405        # On Mac OS X before 10.4, check if -arch and -isysroot
406        # are in CFLAGS or LDFLAGS and remove them if they are.
407        # This is needed when building extensions on a 10.3 system
408        # using a universal build of python.
409        _remove_universal_flags(_config_vars)
410
411    # Allow user to override all archs with ARCHFLAGS env var
412    _override_all_archs(_config_vars)
413
414    # Remove references to sdks that are not found
415    _check_for_unavailable_sdk(_config_vars)
416
417    return _config_vars
418
419
420def customize_compiler(_config_vars):
421    """Customize compiler path and configuration variables.
422
423    This customization is performed when the first
424    extension module build is requested
425    in distutils.sysconfig.customize_compiler).
426    """
427
428    # Find a compiler to use for extension module builds
429    _find_appropriate_compiler(_config_vars)
430
431    # Remove ppc arch flags if not supported here
432    _remove_unsupported_archs(_config_vars)
433
434    # Allow user to override all archs with ARCHFLAGS env var
435    _override_all_archs(_config_vars)
436
437    return _config_vars
438
439
440def get_platform_osx(_config_vars, osname, release, machine):
441    """Filter values for get_platform()"""
442    # called from get_platform() in sysconfig and distutils.util
443    #
444    # For our purposes, we'll assume that the system version from
445    # distutils' perspective is what MACOSX_DEPLOYMENT_TARGET is set
446    # to. This makes the compatibility story a bit more sane because the
447    # machine is going to compile and link as if it were
448    # MACOSX_DEPLOYMENT_TARGET.
449
450    macver = _config_vars.get('MACOSX_DEPLOYMENT_TARGET', '')
451    macrelease = _get_system_version() or macver
452    macver = macver or macrelease
453
454    if macver:
455        release = macver
456        osname = "macosx"
457
458        # Use the original CFLAGS value, if available, so that we
459        # return the same machine type for the platform string.
460        # Otherwise, distutils may consider this a cross-compiling
461        # case and disallow installs.
462        cflags = _config_vars.get(_INITPRE+'CFLAGS',
463                                    _config_vars.get('CFLAGS', ''))
464        if macrelease:
465            try:
466                macrelease = tuple(int(i) for i in macrelease.split('.')[0:2])
467            except ValueError:
468                macrelease = (10, 0)
469        else:
470            # assume no universal support
471            macrelease = (10, 0)
472
473        if (macrelease >= (10, 4)) and '-arch' in cflags.strip():
474            # The universal build will build fat binaries, but not on
475            # systems before 10.4
476
477            machine = 'fat'
478
479            archs = re.findall(r'-arch\s+(\S+)', cflags)
480            archs = tuple(sorted(set(archs)))
481
482            if len(archs) == 1:
483                machine = archs[0]
484            elif archs == ('i386', 'ppc'):
485                machine = 'fat'
486            elif archs == ('i386', 'x86_64'):
487                machine = 'intel'
488            elif archs == ('i386', 'ppc', 'x86_64'):
489                machine = 'fat3'
490            elif archs == ('ppc64', 'x86_64'):
491                machine = 'fat64'
492            elif archs == ('i386', 'ppc', 'ppc64', 'x86_64'):
493                machine = 'universal'
494            else:
495                raise ValueError(
496                   "Don't know machine value for archs=%r" % (archs,))
497
498        elif machine == 'i386':
499            # On OSX the machine type returned by uname is always the
500            # 32-bit variant, even if the executable architecture is
501            # the 64-bit variant
502            if sys.maxsize >= 2**32:
503                machine = 'x86_64'
504
505        elif machine in ('PowerPC', 'Power_Macintosh'):
506            # Pick a sane name for the PPC architecture.
507            # See 'i386' case
508            if sys.maxsize >= 2**32:
509                machine = 'ppc64'
510            else:
511                machine = 'ppc'
512
513    return (osname, release, machine)
514