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