_osx_support.py 22 KB

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