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