Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Lib/_osx_support.py
12 views
1
"""Shared OS X support functions."""
2
3
import os
4
import re
5
import 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
29
def _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
55
def _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
77
def _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
86
def _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
117
def _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
136
def _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
143
def _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
153
def _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
178
def _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
188
def _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
198
def _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
260
def _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
274
def _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
314
def _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
331
def _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
358
def 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
438
def 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
479
def 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
499
def 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
576