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', encoding="utf-8") 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 sys.stderr.write(f"Compiling with an SDK that doesn't seem to exist: {sysroot}\n") 432 sys.stderr.write("Please check your Xcode installation\n") 433 sys.stderr.flush() 434 435 return compiler_so 436 437 438def customize_config_vars(_config_vars): 439 """Customize Python build configuration variables. 440 441 Called internally from sysconfig with a mutable mapping 442 containing name/value pairs parsed from the configured 443 makefile used to build this interpreter. Returns 444 the mapping updated as needed to reflect the environment 445 in which the interpreter is running; in the case of 446 a Python from a binary installer, the installed 447 environment may be very different from the build 448 environment, i.e. different OS levels, different 449 built tools, different available CPU architectures. 450 451 This customization is performed whenever 452 distutils.sysconfig.get_config_vars() is first 453 called. It may be used in environments where no 454 compilers are present, i.e. when installing pure 455 Python dists. Customization of compiler paths 456 and detection of unavailable archs is deferred 457 until the first extension module build is 458 requested (in distutils.sysconfig.customize_compiler). 459 460 Currently called from distutils.sysconfig 461 """ 462 463 if not _supports_universal_builds(): 464 # On Mac OS X before 10.4, check if -arch and -isysroot 465 # are in CFLAGS or LDFLAGS and remove them if they are. 466 # This is needed when building extensions on a 10.3 system 467 # using a universal build of python. 468 _remove_universal_flags(_config_vars) 469 470 # Allow user to override all archs with ARCHFLAGS env var 471 _override_all_archs(_config_vars) 472 473 # Remove references to sdks that are not found 474 _check_for_unavailable_sdk(_config_vars) 475 476 return _config_vars 477 478 479def customize_compiler(_config_vars): 480 """Customize compiler path and configuration variables. 481 482 This customization is performed when the first 483 extension module build is requested 484 in distutils.sysconfig.customize_compiler. 485 """ 486 487 # Find a compiler to use for extension module builds 488 _find_appropriate_compiler(_config_vars) 489 490 # Remove ppc arch flags if not supported here 491 _remove_unsupported_archs(_config_vars) 492 493 # Allow user to override all archs with ARCHFLAGS env var 494 _override_all_archs(_config_vars) 495 496 return _config_vars 497 498 499def get_platform_osx(_config_vars, osname, release, machine): 500 """Filter values for get_platform()""" 501 # called from get_platform() in sysconfig and distutils.util 502 # 503 # For our purposes, we'll assume that the system version from 504 # distutils' perspective is what MACOSX_DEPLOYMENT_TARGET is set 505 # to. This makes the compatibility story a bit more sane because the 506 # machine is going to compile and link as if it were 507 # MACOSX_DEPLOYMENT_TARGET. 508 509 macver = _config_vars.get('MACOSX_DEPLOYMENT_TARGET', '') 510 macrelease = _get_system_version() or macver 511 macver = macver or macrelease 512 513 if macver: 514 release = macver 515 osname = "macosx" 516 517 # Use the original CFLAGS value, if available, so that we 518 # return the same machine type for the platform string. 519 # Otherwise, distutils may consider this a cross-compiling 520 # case and disallow installs. 521 cflags = _config_vars.get(_INITPRE+'CFLAGS', 522 _config_vars.get('CFLAGS', '')) 523 if macrelease: 524 try: 525 macrelease = tuple(int(i) for i in macrelease.split('.')[0:2]) 526 except ValueError: 527 macrelease = (10, 3) 528 else: 529 # assume no universal support 530 macrelease = (10, 3) 531 532 if (macrelease >= (10, 4)) and '-arch' in cflags.strip(): 533 # The universal build will build fat binaries, but not on 534 # systems before 10.4 535 536 machine = 'fat' 537 538 archs = re.findall(r'-arch\s+(\S+)', cflags) 539 archs = tuple(sorted(set(archs))) 540 541 if len(archs) == 1: 542 machine = archs[0] 543 elif archs == ('arm64', 'x86_64'): 544 machine = 'universal2' 545 elif archs == ('i386', 'ppc'): 546 machine = 'fat' 547 elif archs == ('i386', 'x86_64'): 548 machine = 'intel' 549 elif archs == ('i386', 'ppc', 'x86_64'): 550 machine = 'fat3' 551 elif archs == ('ppc64', 'x86_64'): 552 machine = 'fat64' 553 elif archs == ('i386', 'ppc', 'ppc64', 'x86_64'): 554 machine = 'universal' 555 else: 556 raise ValueError( 557 "Don't know machine value for archs=%r" % (archs,)) 558 559 elif machine == 'i386': 560 # On OSX the machine type returned by uname is always the 561 # 32-bit variant, even if the executable architecture is 562 # the 64-bit variant 563 if sys.maxsize >= 2**32: 564 machine = 'x86_64' 565 566 elif machine in ('PowerPC', 'Power_Macintosh'): 567 # Pick a sane name for the PPC architecture. 568 # See 'i386' case 569 if sys.maxsize >= 2**32: 570 machine = 'ppc64' 571 else: 572 machine = 'ppc' 573 574 return (osname, release, machine) 575