Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
allendowney
GitHub Repository: allendowney/cpython
Path: blob/main/Mac/BuildScript/build-installer.py
12 views
1
#!/usr/bin/env python
2
"""
3
This script is used to build "official" universal installers on macOS.
4
5
NEW for 3.10 and backports:
6
- support universal2 variant with arm64 and x86_64 archs
7
- enable clang optimizations when building on 10.15+
8
9
NEW for 3.9.0 and backports:
10
- 2.7 end-of-life issues:
11
- Python 3 installs now update the Current version link
12
in /Library/Frameworks/Python.framework/Versions
13
- fully support running under Python 3 as well as 2.7
14
- support building on newer macOS systems with SIP
15
- fully support building on macOS 10.9+
16
- support 10.6+ on best effort
17
- support bypassing docs build by supplying a prebuilt
18
docs html tarball in the third-party source library,
19
in the format and filename conventional of those
20
downloadable from python.org:
21
python-3.x.y-docs-html.tar.bz2
22
23
NEW for 3.7.0:
24
- support Intel 64-bit-only () and 32-bit-only installer builds
25
- build and use internal Tcl/Tk 8.6 for 10.6+ builds
26
- deprecate use of explicit SDK (--sdk-path=) since all but the oldest
27
versions of Xcode support implicit setting of an SDK via environment
28
variables (SDKROOT and friends, see the xcrun man page for more info).
29
The SDK stuff was primarily needed for building universal installers
30
for 10.4; so as of 3.7.0, building installers for 10.4 is no longer
31
supported with build-installer.
32
- use generic "gcc" as compiler (CC env var) rather than "gcc-4.2"
33
34
TODO:
35
- test building with SDKROOT and DEVELOPER_DIR xcrun env variables
36
37
Usage: see USAGE variable in the script.
38
"""
39
import platform, os, sys, getopt, textwrap, shutil, stat, time, pwd, grp
40
try:
41
import urllib2 as urllib_request
42
except ImportError:
43
import urllib.request as urllib_request
44
45
STAT_0o755 = ( stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
46
| stat.S_IRGRP | stat.S_IXGRP
47
| stat.S_IROTH | stat.S_IXOTH )
48
49
STAT_0o775 = ( stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
50
| stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP
51
| stat.S_IROTH | stat.S_IXOTH )
52
53
INCLUDE_TIMESTAMP = 1
54
VERBOSE = 1
55
56
RUNNING_ON_PYTHON2 = sys.version_info.major == 2
57
58
if RUNNING_ON_PYTHON2:
59
from plistlib import writePlist
60
else:
61
from plistlib import dump
62
def writePlist(path, plist):
63
with open(plist, 'wb') as fp:
64
dump(path, fp)
65
66
def shellQuote(value):
67
"""
68
Return the string value in a form that can safely be inserted into
69
a shell command.
70
"""
71
return "'%s'"%(value.replace("'", "'\"'\"'"))
72
73
def grepValue(fn, variable):
74
"""
75
Return the unquoted value of a variable from a file..
76
QUOTED_VALUE='quotes' -> str('quotes')
77
UNQUOTED_VALUE=noquotes -> str('noquotes')
78
"""
79
variable = variable + '='
80
for ln in open(fn, 'r'):
81
if ln.startswith(variable):
82
value = ln[len(variable):].strip()
83
return value.strip("\"'")
84
raise RuntimeError("Cannot find variable %s" % variable[:-1])
85
86
_cache_getVersion = None
87
88
def getVersion():
89
global _cache_getVersion
90
if _cache_getVersion is None:
91
_cache_getVersion = grepValue(
92
os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
93
return _cache_getVersion
94
95
def getVersionMajorMinor():
96
return tuple([int(n) for n in getVersion().split('.', 2)])
97
98
_cache_getFullVersion = None
99
100
def getFullVersion():
101
global _cache_getFullVersion
102
if _cache_getFullVersion is not None:
103
return _cache_getFullVersion
104
fn = os.path.join(SRCDIR, 'Include', 'patchlevel.h')
105
for ln in open(fn):
106
if 'PY_VERSION' in ln:
107
_cache_getFullVersion = ln.split()[-1][1:-1]
108
return _cache_getFullVersion
109
raise RuntimeError("Cannot find full version??")
110
111
FW_PREFIX = ["Library", "Frameworks", "Python.framework"]
112
FW_VERSION_PREFIX = "--undefined--" # initialized in parseOptions
113
FW_SSL_DIRECTORY = "--undefined--" # initialized in parseOptions
114
115
# The directory we'll use to create the build (will be erased and recreated)
116
WORKDIR = "/tmp/_py"
117
118
# The directory we'll use to store third-party sources. Set this to something
119
# else if you don't want to re-fetch required libraries every time.
120
DEPSRC = os.path.join(WORKDIR, 'third-party')
121
DEPSRC = os.path.expanduser('~/Universal/other-sources')
122
123
universal_opts_map = { 'universal2': ('arm64', 'x86_64'),
124
'32-bit': ('i386', 'ppc',),
125
'64-bit': ('x86_64', 'ppc64',),
126
'intel': ('i386', 'x86_64'),
127
'intel-32': ('i386',),
128
'intel-64': ('x86_64',),
129
'3-way': ('ppc', 'i386', 'x86_64'),
130
'all': ('i386', 'ppc', 'x86_64', 'ppc64',) }
131
default_target_map = {
132
'universal2': '10.9',
133
'64-bit': '10.5',
134
'3-way': '10.5',
135
'intel': '10.5',
136
'intel-32': '10.4',
137
'intel-64': '10.5',
138
'all': '10.5',
139
}
140
141
UNIVERSALOPTS = tuple(universal_opts_map.keys())
142
143
UNIVERSALARCHS = '32-bit'
144
145
ARCHLIST = universal_opts_map[UNIVERSALARCHS]
146
147
# Source directory (assume we're in Mac/BuildScript)
148
SRCDIR = os.path.dirname(
149
os.path.dirname(
150
os.path.dirname(
151
os.path.abspath(__file__
152
))))
153
154
# $MACOSX_DEPLOYMENT_TARGET -> minimum OS X level
155
DEPTARGET = '10.5'
156
157
def getDeptargetTuple():
158
return tuple([int(n) for n in DEPTARGET.split('.')[0:2]])
159
160
def getBuildTuple():
161
return tuple([int(n) for n in platform.mac_ver()[0].split('.')[0:2]])
162
163
def getTargetCompilers():
164
target_cc_map = {
165
'10.4': ('gcc-4.0', 'g++-4.0'),
166
'10.5': ('gcc', 'g++'),
167
'10.6': ('gcc', 'g++'),
168
'10.7': ('gcc', 'g++'),
169
'10.8': ('gcc', 'g++'),
170
}
171
return target_cc_map.get(DEPTARGET, ('clang', 'clang++') )
172
173
CC, CXX = getTargetCompilers()
174
175
PYTHON_3 = getVersionMajorMinor() >= (3, 0)
176
177
USAGE = textwrap.dedent("""\
178
Usage: build_python [options]
179
180
Options:
181
-? or -h: Show this message
182
-b DIR
183
--build-dir=DIR: Create build here (default: %(WORKDIR)r)
184
--third-party=DIR: Store third-party sources here (default: %(DEPSRC)r)
185
--sdk-path=DIR: Location of the SDK (deprecated, use SDKROOT env variable)
186
--src-dir=DIR: Location of the Python sources (default: %(SRCDIR)r)
187
--dep-target=10.n macOS deployment target (default: %(DEPTARGET)r)
188
--universal-archs=x universal architectures (options: %(UNIVERSALOPTS)r, default: %(UNIVERSALARCHS)r)
189
""")% globals()
190
191
# Dict of object file names with shared library names to check after building.
192
# This is to ensure that we ended up dynamically linking with the shared
193
# library paths and versions we expected. For example:
194
# EXPECTED_SHARED_LIBS['_tkinter.so'] = [
195
# '/Library/Frameworks/Tcl.framework/Versions/8.5/Tcl',
196
# '/Library/Frameworks/Tk.framework/Versions/8.5/Tk']
197
EXPECTED_SHARED_LIBS = {}
198
199
# Are we building and linking with our own copy of Tcl/TK?
200
# For now, do so if deployment target is 10.6+.
201
def internalTk():
202
return getDeptargetTuple() >= (10, 6)
203
204
# Do we use 8.6.8 when building our own copy
205
# of Tcl/Tk or a modern version.
206
# We use the old version when building on
207
# old versions of macOS due to build issues.
208
def useOldTk():
209
return getBuildTuple() < (10, 15)
210
211
212
def tweak_tcl_build(basedir, archList):
213
with open("Makefile", "r") as fp:
214
contents = fp.readlines()
215
216
# For reasons I don't understand the tcl configure script
217
# decides that some stdlib symbols aren't present, before
218
# deciding that strtod is broken.
219
new_contents = []
220
for line in contents:
221
if line.startswith("COMPAT_OBJS"):
222
# note: the space before strtod.o is intentional,
223
# the detection of a broken strtod results in
224
# "fixstrod.o" on this line.
225
for nm in ("strstr.o", "strtoul.o", " strtod.o"):
226
line = line.replace(nm, "")
227
new_contents.append(line)
228
229
with open("Makefile", "w") as fp:
230
fp.writelines(new_contents)
231
232
# List of names of third party software built with this installer.
233
# The names will be inserted into the rtf version of the License.
234
THIRD_PARTY_LIBS = []
235
236
# Instructions for building libraries that are necessary for building a
237
# batteries included python.
238
# [The recipes are defined here for convenience but instantiated later after
239
# command line options have been processed.]
240
def library_recipes():
241
result = []
242
243
# Since Apple removed the header files for the deprecated system
244
# OpenSSL as of the Xcode 7 release (for OS X 10.10+), we do not
245
# have much choice but to build our own copy here, too.
246
247
result.extend([
248
dict(
249
name="OpenSSL 1.1.1u",
250
url="https://www.openssl.org/source/openssl-1.1.1u.tar.gz",
251
checksum='e2f8d84b523eecd06c7be7626830370300fbcc15386bf5142d72758f6963ebc6',
252
buildrecipe=build_universal_openssl,
253
configure=None,
254
install=None,
255
),
256
])
257
258
if internalTk():
259
if useOldTk():
260
tcl_tk_ver='8.6.8'
261
tcl_checksum='81656d3367af032e0ae6157eff134f89'
262
263
tk_checksum='5e0faecba458ee1386078fb228d008ba'
264
tk_patches = ['tk868_on_10_8_10_9.patch']
265
266
else:
267
tcl_tk_ver='8.6.13'
268
tcl_checksum='43a1fae7412f61ff11de2cfd05d28cfc3a73762f354a417c62370a54e2caf066'
269
270
tk_checksum='2e65fa069a23365440a3c56c556b8673b5e32a283800d8d9b257e3f584ce0675'
271
tk_patches = [ ]
272
273
274
base_url = "https://prdownloads.sourceforge.net/tcl/{what}{version}-src.tar.gz"
275
result.extend([
276
dict(
277
name="Tcl %s"%(tcl_tk_ver,),
278
url=base_url.format(what="tcl", version=tcl_tk_ver),
279
checksum=tcl_checksum,
280
buildDir="unix",
281
configure_pre=[
282
'--enable-shared',
283
'--enable-threads',
284
'--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib'%(getVersion(),),
285
],
286
useLDFlags=False,
287
buildrecipe=tweak_tcl_build,
288
install='make TCL_LIBRARY=%(TCL_LIBRARY)s && make install TCL_LIBRARY=%(TCL_LIBRARY)s DESTDIR=%(DESTDIR)s'%{
289
"DESTDIR": shellQuote(os.path.join(WORKDIR, 'libraries')),
290
"TCL_LIBRARY": shellQuote('/Library/Frameworks/Python.framework/Versions/%s/lib/tcl8.6'%(getVersion())),
291
},
292
),
293
dict(
294
name="Tk %s"%(tcl_tk_ver,),
295
url=base_url.format(what="tk", version=tcl_tk_ver),
296
checksum=tk_checksum,
297
patches=tk_patches,
298
buildDir="unix",
299
configure_pre=[
300
'--enable-aqua',
301
'--enable-shared',
302
'--enable-threads',
303
'--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib'%(getVersion(),),
304
],
305
useLDFlags=False,
306
install='make TCL_LIBRARY=%(TCL_LIBRARY)s TK_LIBRARY=%(TK_LIBRARY)s && make install TCL_LIBRARY=%(TCL_LIBRARY)s TK_LIBRARY=%(TK_LIBRARY)s DESTDIR=%(DESTDIR)s'%{
307
"DESTDIR": shellQuote(os.path.join(WORKDIR, 'libraries')),
308
"TCL_LIBRARY": shellQuote('/Library/Frameworks/Python.framework/Versions/%s/lib/tcl8.6'%(getVersion())),
309
"TK_LIBRARY": shellQuote('/Library/Frameworks/Python.framework/Versions/%s/lib/tk8.6'%(getVersion())),
310
},
311
),
312
])
313
314
if PYTHON_3:
315
result.extend([
316
dict(
317
name="XZ 5.2.3",
318
url="http://tukaani.org/xz/xz-5.2.3.tar.gz",
319
checksum='ef68674fb47a8b8e741b34e429d86e9d',
320
configure_pre=[
321
'--disable-dependency-tracking',
322
]
323
),
324
])
325
326
result.extend([
327
dict(
328
name="NCurses 5.9",
329
url="http://ftp.gnu.org/pub/gnu/ncurses/ncurses-5.9.tar.gz",
330
checksum='8cb9c412e5f2d96bc6f459aa8c6282a1',
331
configure_pre=[
332
"--enable-widec",
333
"--without-cxx",
334
"--without-cxx-binding",
335
"--without-ada",
336
"--without-curses-h",
337
"--enable-shared",
338
"--with-shared",
339
"--without-debug",
340
"--without-normal",
341
"--without-tests",
342
"--without-manpages",
343
"--datadir=/usr/share",
344
"--sysconfdir=/etc",
345
"--sharedstatedir=/usr/com",
346
"--with-terminfo-dirs=/usr/share/terminfo",
347
"--with-default-terminfo-dir=/usr/share/terminfo",
348
"--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib"%(getVersion(),),
349
],
350
patchscripts=[
351
("ftp://ftp.invisible-island.net/ncurses//5.9/ncurses-5.9-20120616-patch.sh.bz2",
352
"f54bf02a349f96a7c4f0d00922f3a0d4"),
353
],
354
useLDFlags=False,
355
install='make && make install DESTDIR=%s && cd %s/usr/local/lib && ln -fs ../../../Library/Frameworks/Python.framework/Versions/%s/lib/lib* .'%(
356
shellQuote(os.path.join(WORKDIR, 'libraries')),
357
shellQuote(os.path.join(WORKDIR, 'libraries')),
358
getVersion(),
359
),
360
),
361
dict(
362
name="SQLite 3.42.0",
363
url="https://sqlite.org/2023/sqlite-autoconf-3420000.tar.gz",
364
checksum="0c5a92bc51cf07cae45b4a1e94653dea",
365
extra_cflags=('-Os '
366
'-DSQLITE_ENABLE_FTS5 '
367
'-DSQLITE_ENABLE_FTS4 '
368
'-DSQLITE_ENABLE_FTS3_PARENTHESIS '
369
'-DSQLITE_ENABLE_RTREE '
370
'-DSQLITE_OMIT_AUTOINIT '
371
'-DSQLITE_TCL=0 '
372
),
373
configure_pre=[
374
'--enable-threadsafe',
375
'--enable-shared=no',
376
'--enable-static=yes',
377
'--disable-readline',
378
'--disable-dependency-tracking',
379
]
380
),
381
])
382
383
if not PYTHON_3:
384
result.extend([
385
dict(
386
name="Sleepycat DB 4.7.25",
387
url="http://download.oracle.com/berkeley-db/db-4.7.25.tar.gz",
388
checksum='ec2b87e833779681a0c3a814aa71359e',
389
buildDir="build_unix",
390
configure="../dist/configure",
391
configure_pre=[
392
'--includedir=/usr/local/include/db4',
393
]
394
),
395
])
396
397
return result
398
399
def compilerCanOptimize():
400
"""
401
Return True iff the default Xcode version can use PGO and LTO
402
"""
403
# bpo-42235: The version check is pretty conservative, can be
404
# adjusted after testing
405
mac_ver = tuple(map(int, platform.mac_ver()[0].split('.')))
406
return mac_ver >= (10, 15)
407
408
# Instructions for building packages inside the .mpkg.
409
def pkg_recipes():
410
unselected_for_python3 = ('selected', 'unselected')[PYTHON_3]
411
result = [
412
dict(
413
name="PythonFramework",
414
long_name="Python Framework",
415
source="/Library/Frameworks/Python.framework",
416
readme="""\
417
This package installs Python.framework, that is the python
418
interpreter and the standard library.
419
""",
420
postflight="scripts/postflight.framework",
421
selected='selected',
422
),
423
dict(
424
name="PythonApplications",
425
long_name="GUI Applications",
426
source="/Applications/Python %(VER)s",
427
readme="""\
428
This package installs IDLE (an interactive Python IDE),
429
Python Launcher and Build Applet (create application bundles
430
from python scripts).
431
432
It also installs a number of examples and demos.
433
""",
434
required=False,
435
selected='selected',
436
),
437
dict(
438
name="PythonUnixTools",
439
long_name="UNIX command-line tools",
440
source="/usr/local/bin",
441
readme="""\
442
This package installs the unix tools in /usr/local/bin for
443
compatibility with older releases of Python. This package
444
is not necessary to use Python.
445
""",
446
required=False,
447
selected='selected',
448
),
449
dict(
450
name="PythonDocumentation",
451
long_name="Python Documentation",
452
topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
453
source="/pydocs",
454
readme="""\
455
This package installs the python documentation at a location
456
that is usable for pydoc and IDLE.
457
""",
458
postflight="scripts/postflight.documentation",
459
required=False,
460
selected='selected',
461
),
462
dict(
463
name="PythonProfileChanges",
464
long_name="Shell profile updater",
465
readme="""\
466
This packages updates your shell profile to make sure that
467
the Python tools are found by your shell in preference of
468
the system provided Python tools.
469
470
If you don't install this package you'll have to add
471
"/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
472
to your PATH by hand.
473
""",
474
postflight="scripts/postflight.patch-profile",
475
topdir="/Library/Frameworks/Python.framework",
476
source="/empty-dir",
477
required=False,
478
selected='selected',
479
),
480
dict(
481
name="PythonInstallPip",
482
long_name="Install or upgrade pip",
483
readme="""\
484
This package installs (or upgrades from an earlier version)
485
pip, a tool for installing and managing Python packages.
486
""",
487
postflight="scripts/postflight.ensurepip",
488
topdir="/Library/Frameworks/Python.framework",
489
source="/empty-dir",
490
required=False,
491
selected='selected',
492
),
493
]
494
495
return result
496
497
def fatal(msg):
498
"""
499
A fatal error, bail out.
500
"""
501
sys.stderr.write('FATAL: ')
502
sys.stderr.write(msg)
503
sys.stderr.write('\n')
504
sys.exit(1)
505
506
def fileContents(fn):
507
"""
508
Return the contents of the named file
509
"""
510
return open(fn, 'r').read()
511
512
def runCommand(commandline):
513
"""
514
Run a command and raise RuntimeError if it fails. Output is suppressed
515
unless the command fails.
516
"""
517
fd = os.popen(commandline, 'r')
518
data = fd.read()
519
xit = fd.close()
520
if xit is not None:
521
sys.stdout.write(data)
522
raise RuntimeError("command failed: %s"%(commandline,))
523
524
if VERBOSE:
525
sys.stdout.write(data); sys.stdout.flush()
526
527
def captureCommand(commandline):
528
fd = os.popen(commandline, 'r')
529
data = fd.read()
530
xit = fd.close()
531
if xit is not None:
532
sys.stdout.write(data)
533
raise RuntimeError("command failed: %s"%(commandline,))
534
535
return data
536
537
def getTclTkVersion(configfile, versionline):
538
"""
539
search Tcl or Tk configuration file for version line
540
"""
541
try:
542
f = open(configfile, "r")
543
except OSError:
544
fatal("Framework configuration file not found: %s" % configfile)
545
546
for l in f:
547
if l.startswith(versionline):
548
f.close()
549
return l
550
551
fatal("Version variable %s not found in framework configuration file: %s"
552
% (versionline, configfile))
553
554
def checkEnvironment():
555
"""
556
Check that we're running on a supported system.
557
"""
558
559
if sys.version_info[0:2] < (2, 7):
560
fatal("This script must be run with Python 2.7 (or later)")
561
562
if platform.system() != 'Darwin':
563
fatal("This script should be run on a macOS 10.5 (or later) system")
564
565
if int(platform.release().split('.')[0]) < 8:
566
fatal("This script should be run on a macOS 10.5 (or later) system")
567
568
# Because we only support dynamic load of only one major/minor version of
569
# Tcl/Tk, if we are not using building and using our own private copy of
570
# Tcl/Tk, ensure:
571
# 1. there is a user-installed framework (usually ActiveTcl) in (or linked
572
# in) SDKROOT/Library/Frameworks. As of Python 3.7.0, we no longer
573
# enforce that the version of the user-installed framework also
574
# exists in the system-supplied Tcl/Tk frameworks. Time to support
575
# Tcl/Tk 8.6 even if Apple does not.
576
if not internalTk():
577
frameworks = {}
578
for framework in ['Tcl', 'Tk']:
579
fwpth = 'Library/Frameworks/%s.framework/Versions/Current' % framework
580
libfw = os.path.join('/', fwpth)
581
usrfw = os.path.join(os.getenv('HOME'), fwpth)
582
frameworks[framework] = os.readlink(libfw)
583
if not os.path.exists(libfw):
584
fatal("Please install a link to a current %s %s as %s so "
585
"the user can override the system framework."
586
% (framework, frameworks[framework], libfw))
587
if os.path.exists(usrfw):
588
fatal("Please rename %s to avoid possible dynamic load issues."
589
% usrfw)
590
591
if frameworks['Tcl'] != frameworks['Tk']:
592
fatal("The Tcl and Tk frameworks are not the same version.")
593
594
print(" -- Building with external Tcl/Tk %s frameworks"
595
% frameworks['Tk'])
596
597
# add files to check after build
598
EXPECTED_SHARED_LIBS['_tkinter.so'] = [
599
"/Library/Frameworks/Tcl.framework/Versions/%s/Tcl"
600
% frameworks['Tcl'],
601
"/Library/Frameworks/Tk.framework/Versions/%s/Tk"
602
% frameworks['Tk'],
603
]
604
else:
605
print(" -- Building private copy of Tcl/Tk")
606
print("")
607
608
# Remove inherited environment variables which might influence build
609
environ_var_prefixes = ['CPATH', 'C_INCLUDE_', 'DYLD_', 'LANG', 'LC_',
610
'LD_', 'LIBRARY_', 'PATH', 'PYTHON']
611
for ev in list(os.environ):
612
for prefix in environ_var_prefixes:
613
if ev.startswith(prefix) :
614
print("INFO: deleting environment variable %s=%s" % (
615
ev, os.environ[ev]))
616
del os.environ[ev]
617
618
base_path = '/bin:/sbin:/usr/bin:/usr/sbin'
619
if 'SDK_TOOLS_BIN' in os.environ:
620
base_path = os.environ['SDK_TOOLS_BIN'] + ':' + base_path
621
# Xcode 2.5 on OS X 10.4 does not include SetFile in its usr/bin;
622
# add its fixed location here if it exists
623
OLD_DEVELOPER_TOOLS = '/Developer/Tools'
624
if os.path.isdir(OLD_DEVELOPER_TOOLS):
625
base_path = base_path + ':' + OLD_DEVELOPER_TOOLS
626
os.environ['PATH'] = base_path
627
print("Setting default PATH: %s"%(os.environ['PATH']))
628
629
def parseOptions(args=None):
630
"""
631
Parse arguments and update global settings.
632
"""
633
global WORKDIR, DEPSRC, SRCDIR, DEPTARGET
634
global UNIVERSALOPTS, UNIVERSALARCHS, ARCHLIST, CC, CXX
635
global FW_VERSION_PREFIX
636
global FW_SSL_DIRECTORY
637
638
if args is None:
639
args = sys.argv[1:]
640
641
try:
642
options, args = getopt.getopt(args, '?hb',
643
[ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir=',
644
'dep-target=', 'universal-archs=', 'help' ])
645
except getopt.GetoptError:
646
print(sys.exc_info()[1])
647
sys.exit(1)
648
649
if args:
650
print("Additional arguments")
651
sys.exit(1)
652
653
deptarget = None
654
for k, v in options:
655
if k in ('-h', '-?', '--help'):
656
print(USAGE)
657
sys.exit(0)
658
659
elif k in ('-d', '--build-dir'):
660
WORKDIR=v
661
662
elif k in ('--third-party',):
663
DEPSRC=v
664
665
elif k in ('--sdk-path',):
666
print(" WARNING: --sdk-path is no longer supported")
667
668
elif k in ('--src-dir',):
669
SRCDIR=v
670
671
elif k in ('--dep-target', ):
672
DEPTARGET=v
673
deptarget=v
674
675
elif k in ('--universal-archs', ):
676
if v in UNIVERSALOPTS:
677
UNIVERSALARCHS = v
678
ARCHLIST = universal_opts_map[UNIVERSALARCHS]
679
if deptarget is None:
680
# Select alternate default deployment
681
# target
682
DEPTARGET = default_target_map.get(v, '10.5')
683
else:
684
raise NotImplementedError(v)
685
686
else:
687
raise NotImplementedError(k)
688
689
SRCDIR=os.path.abspath(SRCDIR)
690
WORKDIR=os.path.abspath(WORKDIR)
691
DEPSRC=os.path.abspath(DEPSRC)
692
693
CC, CXX = getTargetCompilers()
694
695
FW_VERSION_PREFIX = FW_PREFIX[:] + ["Versions", getVersion()]
696
FW_SSL_DIRECTORY = FW_VERSION_PREFIX[:] + ["etc", "openssl"]
697
698
print("-- Settings:")
699
print(" * Source directory: %s" % SRCDIR)
700
print(" * Build directory: %s" % WORKDIR)
701
print(" * Third-party source: %s" % DEPSRC)
702
print(" * Deployment target: %s" % DEPTARGET)
703
print(" * Universal archs: %s" % str(ARCHLIST))
704
print(" * C compiler: %s" % CC)
705
print(" * C++ compiler: %s" % CXX)
706
print("")
707
print(" -- Building a Python %s framework at patch level %s"
708
% (getVersion(), getFullVersion()))
709
print("")
710
711
def extractArchive(builddir, archiveName):
712
"""
713
Extract a source archive into 'builddir'. Returns the path of the
714
extracted archive.
715
716
XXX: This function assumes that archives contain a toplevel directory
717
that is has the same name as the basename of the archive. This is
718
safe enough for almost anything we use. Unfortunately, it does not
719
work for current Tcl and Tk source releases where the basename of
720
the archive ends with "-src" but the uncompressed directory does not.
721
For now, just special case Tcl and Tk tar.gz downloads.
722
"""
723
curdir = os.getcwd()
724
try:
725
os.chdir(builddir)
726
if archiveName.endswith('.tar.gz'):
727
retval = os.path.basename(archiveName[:-7])
728
if ((retval.startswith('tcl') or retval.startswith('tk'))
729
and retval.endswith('-src')):
730
retval = retval[:-4]
731
# Strip rcxx suffix from Tcl/Tk release candidates
732
retval_rc = retval.find('rc')
733
if retval_rc > 0:
734
retval = retval[:retval_rc]
735
if os.path.exists(retval):
736
shutil.rmtree(retval)
737
fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')
738
739
elif archiveName.endswith('.tar.bz2'):
740
retval = os.path.basename(archiveName[:-8])
741
if os.path.exists(retval):
742
shutil.rmtree(retval)
743
fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')
744
745
elif archiveName.endswith('.tar'):
746
retval = os.path.basename(archiveName[:-4])
747
if os.path.exists(retval):
748
shutil.rmtree(retval)
749
fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')
750
751
elif archiveName.endswith('.zip'):
752
retval = os.path.basename(archiveName[:-4])
753
if os.path.exists(retval):
754
shutil.rmtree(retval)
755
fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')
756
757
data = fp.read()
758
xit = fp.close()
759
if xit is not None:
760
sys.stdout.write(data)
761
raise RuntimeError("Cannot extract %s"%(archiveName,))
762
763
return os.path.join(builddir, retval)
764
765
finally:
766
os.chdir(curdir)
767
768
def downloadURL(url, fname):
769
"""
770
Download the contents of the url into the file.
771
"""
772
fpIn = urllib_request.urlopen(url)
773
fpOut = open(fname, 'wb')
774
block = fpIn.read(10240)
775
try:
776
while block:
777
fpOut.write(block)
778
block = fpIn.read(10240)
779
fpIn.close()
780
fpOut.close()
781
except:
782
try:
783
os.unlink(fname)
784
except OSError:
785
pass
786
787
def verifyThirdPartyFile(url, checksum, fname):
788
"""
789
Download file from url to filename fname if it does not already exist.
790
Abort if file contents does not match supplied md5 checksum.
791
"""
792
name = os.path.basename(fname)
793
if os.path.exists(fname):
794
print("Using local copy of %s"%(name,))
795
else:
796
print("Did not find local copy of %s"%(name,))
797
print("Downloading %s"%(name,))
798
downloadURL(url, fname)
799
print("Archive for %s stored as %s"%(name, fname))
800
if len(checksum) == 32:
801
algo = 'md5'
802
elif len(checksum) == 64:
803
algo = 'sha256'
804
else:
805
raise ValueError(checksum)
806
if os.system(
807
'CHECKSUM=$(openssl %s %s) ; test "${CHECKSUM##*= }" = "%s"'
808
% (algo, shellQuote(fname), checksum) ):
809
fatal('%s checksum mismatch for file %s' % (algo, fname))
810
811
def build_universal_openssl(basedir, archList):
812
"""
813
Special case build recipe for universal build of openssl.
814
815
The upstream OpenSSL build system does not directly support
816
OS X universal builds. We need to build each architecture
817
separately then lipo them together into fat libraries.
818
"""
819
820
# OpenSSL fails to build with Xcode 2.5 (on OS X 10.4).
821
# If we are building on a 10.4.x or earlier system,
822
# unilaterally disable assembly code building to avoid the problem.
823
no_asm = int(platform.release().split(".")[0]) < 9
824
825
def build_openssl_arch(archbase, arch):
826
"Build one architecture of openssl"
827
arch_opts = {
828
"i386": ["darwin-i386-cc"],
829
"x86_64": ["darwin64-x86_64-cc", "enable-ec_nistp_64_gcc_128"],
830
"arm64": ["darwin64-arm64-cc"],
831
"ppc": ["darwin-ppc-cc"],
832
"ppc64": ["darwin64-ppc-cc"],
833
}
834
835
# Somewhere between OpenSSL 1.1.0j and 1.1.1c, changes cause the
836
# "enable-ec_nistp_64_gcc_128" option to get compile errors when
837
# building on our 10.6 gcc-4.2 environment. There have been other
838
# reports of projects running into this when using older compilers.
839
# So, for now, do not try to use "enable-ec_nistp_64_gcc_128" when
840
# building for 10.6.
841
if getDeptargetTuple() == (10, 6):
842
arch_opts['x86_64'].remove('enable-ec_nistp_64_gcc_128')
843
844
configure_opts = [
845
"no-idea",
846
"no-mdc2",
847
"no-rc5",
848
"no-zlib",
849
"no-ssl3",
850
# "enable-unit-test",
851
"shared",
852
"--prefix=%s"%os.path.join("/", *FW_VERSION_PREFIX),
853
"--openssldir=%s"%os.path.join("/", *FW_SSL_DIRECTORY),
854
]
855
if no_asm:
856
configure_opts.append("no-asm")
857
runCommand(" ".join(["perl", "Configure"]
858
+ arch_opts[arch] + configure_opts))
859
runCommand("make depend")
860
runCommand("make all")
861
runCommand("make install_sw DESTDIR=%s"%shellQuote(archbase))
862
# runCommand("make test")
863
return
864
865
srcdir = os.getcwd()
866
universalbase = os.path.join(srcdir, "..",
867
os.path.basename(srcdir) + "-universal")
868
os.mkdir(universalbase)
869
archbasefws = []
870
for arch in archList:
871
# fresh copy of the source tree
872
archsrc = os.path.join(universalbase, arch, "src")
873
shutil.copytree(srcdir, archsrc, symlinks=True)
874
# install base for this arch
875
archbase = os.path.join(universalbase, arch, "root")
876
os.mkdir(archbase)
877
# Python framework base within install_prefix:
878
# the build will install into this framework..
879
# This is to ensure that the resulting shared libs have
880
# the desired real install paths built into them.
881
archbasefw = os.path.join(archbase, *FW_VERSION_PREFIX)
882
883
# build one architecture
884
os.chdir(archsrc)
885
build_openssl_arch(archbase, arch)
886
os.chdir(srcdir)
887
archbasefws.append(archbasefw)
888
889
# copy arch-independent files from last build into the basedir framework
890
basefw = os.path.join(basedir, *FW_VERSION_PREFIX)
891
shutil.copytree(
892
os.path.join(archbasefw, "include", "openssl"),
893
os.path.join(basefw, "include", "openssl")
894
)
895
896
shlib_version_number = grepValue(os.path.join(archsrc, "Makefile"),
897
"SHLIB_VERSION_NUMBER")
898
# e.g. -> "1.0.0"
899
libcrypto = "libcrypto.dylib"
900
libcrypto_versioned = libcrypto.replace(".", "."+shlib_version_number+".")
901
# e.g. -> "libcrypto.1.0.0.dylib"
902
libssl = "libssl.dylib"
903
libssl_versioned = libssl.replace(".", "."+shlib_version_number+".")
904
# e.g. -> "libssl.1.0.0.dylib"
905
906
try:
907
os.mkdir(os.path.join(basefw, "lib"))
908
except OSError:
909
pass
910
911
# merge the individual arch-dependent shared libs into a fat shared lib
912
archbasefws.insert(0, basefw)
913
for (lib_unversioned, lib_versioned) in [
914
(libcrypto, libcrypto_versioned),
915
(libssl, libssl_versioned)
916
]:
917
runCommand("lipo -create -output " +
918
" ".join(shellQuote(
919
os.path.join(fw, "lib", lib_versioned))
920
for fw in archbasefws))
921
# and create an unversioned symlink of it
922
os.symlink(lib_versioned, os.path.join(basefw, "lib", lib_unversioned))
923
924
# Create links in the temp include and lib dirs that will be injected
925
# into the Python build so that setup.py can find them while building
926
# and the versioned links so that the setup.py post-build import test
927
# does not fail.
928
relative_path = os.path.join("..", "..", "..", *FW_VERSION_PREFIX)
929
for fn in [
930
["include", "openssl"],
931
["lib", libcrypto],
932
["lib", libssl],
933
["lib", libcrypto_versioned],
934
["lib", libssl_versioned],
935
]:
936
os.symlink(
937
os.path.join(relative_path, *fn),
938
os.path.join(basedir, "usr", "local", *fn)
939
)
940
941
return
942
943
def buildRecipe(recipe, basedir, archList):
944
"""
945
Build software using a recipe. This function does the
946
'configure;make;make install' dance for C software, with a possibility
947
to customize this process, basically a poor-mans DarwinPorts.
948
"""
949
curdir = os.getcwd()
950
951
name = recipe['name']
952
THIRD_PARTY_LIBS.append(name)
953
url = recipe['url']
954
configure = recipe.get('configure', './configure')
955
buildrecipe = recipe.get('buildrecipe', None)
956
install = recipe.get('install', 'make && make install DESTDIR=%s'%(
957
shellQuote(basedir)))
958
959
archiveName = os.path.split(url)[-1]
960
sourceArchive = os.path.join(DEPSRC, archiveName)
961
962
if not os.path.exists(DEPSRC):
963
os.mkdir(DEPSRC)
964
965
verifyThirdPartyFile(url, recipe['checksum'], sourceArchive)
966
print("Extracting archive for %s"%(name,))
967
buildDir=os.path.join(WORKDIR, '_bld')
968
if not os.path.exists(buildDir):
969
os.mkdir(buildDir)
970
971
workDir = extractArchive(buildDir, sourceArchive)
972
os.chdir(workDir)
973
974
for patch in recipe.get('patches', ()):
975
if isinstance(patch, tuple):
976
url, checksum = patch
977
fn = os.path.join(DEPSRC, os.path.basename(url))
978
verifyThirdPartyFile(url, checksum, fn)
979
else:
980
# patch is a file in the source directory
981
fn = os.path.join(curdir, patch)
982
runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
983
shellQuote(fn),))
984
985
for patchscript in recipe.get('patchscripts', ()):
986
if isinstance(patchscript, tuple):
987
url, checksum = patchscript
988
fn = os.path.join(DEPSRC, os.path.basename(url))
989
verifyThirdPartyFile(url, checksum, fn)
990
else:
991
# patch is a file in the source directory
992
fn = os.path.join(curdir, patchscript)
993
if fn.endswith('.bz2'):
994
runCommand('bunzip2 -fk %s' % shellQuote(fn))
995
fn = fn[:-4]
996
runCommand('sh %s' % shellQuote(fn))
997
os.unlink(fn)
998
999
if 'buildDir' in recipe:
1000
os.chdir(recipe['buildDir'])
1001
1002
if configure is not None:
1003
configure_args = [
1004
"--prefix=/usr/local",
1005
"--enable-static",
1006
"--disable-shared",
1007
#"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
1008
]
1009
1010
if 'configure_pre' in recipe:
1011
args = list(recipe['configure_pre'])
1012
if '--disable-static' in args:
1013
configure_args.remove('--enable-static')
1014
if '--enable-shared' in args:
1015
configure_args.remove('--disable-shared')
1016
configure_args.extend(args)
1017
1018
if recipe.get('useLDFlags', 1):
1019
configure_args.extend([
1020
"CFLAGS=%s-mmacosx-version-min=%s -arch %s "
1021
"-I%s/usr/local/include"%(
1022
recipe.get('extra_cflags', ''),
1023
DEPTARGET,
1024
' -arch '.join(archList),
1025
shellQuote(basedir)[1:-1],),
1026
"LDFLAGS=-mmacosx-version-min=%s -L%s/usr/local/lib -arch %s"%(
1027
DEPTARGET,
1028
shellQuote(basedir)[1:-1],
1029
' -arch '.join(archList)),
1030
])
1031
else:
1032
configure_args.extend([
1033
"CFLAGS=%s-mmacosx-version-min=%s -arch %s "
1034
"-I%s/usr/local/include"%(
1035
recipe.get('extra_cflags', ''),
1036
DEPTARGET,
1037
' -arch '.join(archList),
1038
shellQuote(basedir)[1:-1],),
1039
])
1040
1041
if 'configure_post' in recipe:
1042
configure_args = configure_args + list(recipe['configure_post'])
1043
1044
configure_args.insert(0, configure)
1045
configure_args = [ shellQuote(a) for a in configure_args ]
1046
1047
print("Running configure for %s"%(name,))
1048
runCommand(' '.join(configure_args) + ' 2>&1')
1049
1050
if buildrecipe is not None:
1051
# call special-case build recipe, e.g. for openssl
1052
buildrecipe(basedir, archList)
1053
1054
if install is not None:
1055
print("Running install for %s"%(name,))
1056
runCommand('{ ' + install + ' ;} 2>&1')
1057
1058
print("Done %s"%(name,))
1059
print("")
1060
1061
os.chdir(curdir)
1062
1063
def buildLibraries():
1064
"""
1065
Build our dependencies into $WORKDIR/libraries/usr/local
1066
"""
1067
print("")
1068
print("Building required libraries")
1069
print("")
1070
universal = os.path.join(WORKDIR, 'libraries')
1071
os.mkdir(universal)
1072
os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
1073
os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))
1074
1075
for recipe in library_recipes():
1076
buildRecipe(recipe, universal, ARCHLIST)
1077
1078
1079
1080
def buildPythonDocs():
1081
# This stores the documentation as Resources/English.lproj/Documentation
1082
# inside the framework. pydoc and IDLE will pick it up there.
1083
print("Install python documentation")
1084
rootDir = os.path.join(WORKDIR, '_root')
1085
buildDir = os.path.join('../../Doc')
1086
docdir = os.path.join(rootDir, 'pydocs')
1087
curDir = os.getcwd()
1088
os.chdir(buildDir)
1089
runCommand('make clean')
1090
1091
# Search third-party source directory for a pre-built version of the docs.
1092
# Use the naming convention of the docs.python.org html downloads:
1093
# python-3.9.0b1-docs-html.tar.bz2
1094
doctarfiles = [ f for f in os.listdir(DEPSRC)
1095
if f.startswith('python-'+getFullVersion())
1096
if f.endswith('-docs-html.tar.bz2') ]
1097
if doctarfiles:
1098
doctarfile = doctarfiles[0]
1099
if not os.path.exists('build'):
1100
os.mkdir('build')
1101
# if build directory existed, it was emptied by make clean, above
1102
os.chdir('build')
1103
# Extract the first archive found for this version into build
1104
runCommand('tar xjf %s'%shellQuote(os.path.join(DEPSRC, doctarfile)))
1105
# see if tar extracted a directory ending in -docs-html
1106
archivefiles = [ f for f in os.listdir('.')
1107
if f.endswith('-docs-html')
1108
if os.path.isdir(f) ]
1109
if archivefiles:
1110
archivefile = archivefiles[0]
1111
# make it our 'Docs/build/html' directory
1112
print(' -- using pre-built python documentation from %s'%archivefile)
1113
os.rename(archivefile, 'html')
1114
os.chdir(buildDir)
1115
1116
htmlDir = os.path.join('build', 'html')
1117
if not os.path.exists(htmlDir):
1118
# Create virtual environment for docs builds with blurb and sphinx
1119
runCommand('make venv')
1120
runCommand('make html PYTHON=venv/bin/python')
1121
os.rename(htmlDir, docdir)
1122
os.chdir(curDir)
1123
1124
1125
def buildPython():
1126
print("Building a universal python for %s architectures" % UNIVERSALARCHS)
1127
1128
buildDir = os.path.join(WORKDIR, '_bld', 'python')
1129
rootDir = os.path.join(WORKDIR, '_root')
1130
1131
if os.path.exists(buildDir):
1132
shutil.rmtree(buildDir)
1133
if os.path.exists(rootDir):
1134
shutil.rmtree(rootDir)
1135
os.makedirs(buildDir)
1136
os.makedirs(rootDir)
1137
os.makedirs(os.path.join(rootDir, 'empty-dir'))
1138
curdir = os.getcwd()
1139
os.chdir(buildDir)
1140
1141
# Extract the version from the configure file, needed to calculate
1142
# several paths.
1143
version = getVersion()
1144
1145
# Since the extra libs are not in their installed framework location
1146
# during the build, augment the library path so that the interpreter
1147
# will find them during its extension import sanity checks.
1148
1149
print("Running configure...")
1150
runCommand("%s -C --enable-framework --enable-universalsdk=/ "
1151
"--with-universal-archs=%s "
1152
"%s "
1153
"%s "
1154
"%s "
1155
"%s "
1156
"%s "
1157
"%s "
1158
"LDFLAGS='-g -L%s/libraries/usr/local/lib' "
1159
"CFLAGS='-g -I%s/libraries/usr/local/include' 2>&1"%(
1160
shellQuote(os.path.join(SRCDIR, 'configure')),
1161
UNIVERSALARCHS,
1162
(' ', '--with-computed-gotos ')[PYTHON_3],
1163
(' ', '--without-ensurepip ')[PYTHON_3],
1164
(' ', "--with-openssl='%s/libraries/usr/local'"%(
1165
shellQuote(WORKDIR)[1:-1],))[PYTHON_3],
1166
(' ', "--enable-optimizations --with-lto")[compilerCanOptimize()],
1167
(' ', "TCLTK_CFLAGS='-I%s/libraries/usr/local/include'"%(
1168
shellQuote(WORKDIR)[1:-1],))[internalTk()],
1169
(' ', "TCLTK_LIBS='-L%s/libraries/usr/local/lib -ltcl8.6 -ltk8.6'"%(
1170
shellQuote(WORKDIR)[1:-1],))[internalTk()],
1171
shellQuote(WORKDIR)[1:-1],
1172
shellQuote(WORKDIR)[1:-1]))
1173
1174
# As of macOS 10.11 with SYSTEM INTEGRITY PROTECTION, DYLD_*
1175
# environment variables are no longer automatically inherited
1176
# by child processes from their parents. We used to just set
1177
# DYLD_LIBRARY_PATH, pointing to the third-party libs,
1178
# in build-installer.py's process environment and it was
1179
# passed through the make utility into the environment of
1180
# setup.py. Instead, we now append DYLD_LIBRARY_PATH to
1181
# the existing RUNSHARED configuration value when we call
1182
# make for extension module builds.
1183
1184
runshared_for_make = "".join([
1185
" RUNSHARED=",
1186
"'",
1187
grepValue("Makefile", "RUNSHARED"),
1188
' DYLD_LIBRARY_PATH=',
1189
os.path.join(WORKDIR, 'libraries', 'usr', 'local', 'lib'),
1190
"'" ])
1191
1192
# Look for environment value BUILDINSTALLER_BUILDPYTHON_MAKE_EXTRAS
1193
# and, if defined, append its value to the make command. This allows
1194
# us to pass in version control tags, like GITTAG, to a build from a
1195
# tarball rather than from a vcs checkout, thus eliminating the need
1196
# to have a working copy of the vcs program on the build machine.
1197
#
1198
# A typical use might be:
1199
# export BUILDINSTALLER_BUILDPYTHON_MAKE_EXTRAS=" \
1200
# GITVERSION='echo 123456789a' \
1201
# GITTAG='echo v3.6.0' \
1202
# GITBRANCH='echo 3.6'"
1203
1204
make_extras = os.getenv("BUILDINSTALLER_BUILDPYTHON_MAKE_EXTRAS")
1205
if make_extras:
1206
make_cmd = "make " + make_extras + runshared_for_make
1207
else:
1208
make_cmd = "make" + runshared_for_make
1209
print("Running " + make_cmd)
1210
runCommand(make_cmd)
1211
1212
make_cmd = "make install DESTDIR=%s %s"%(
1213
shellQuote(rootDir),
1214
runshared_for_make)
1215
print("Running " + make_cmd)
1216
runCommand(make_cmd)
1217
1218
make_cmd = "make frameworkinstallextras DESTDIR=%s %s"%(
1219
shellQuote(rootDir),
1220
runshared_for_make)
1221
print("Running " + make_cmd)
1222
runCommand(make_cmd)
1223
1224
print("Copying required shared libraries")
1225
if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
1226
build_lib_dir = os.path.join(
1227
WORKDIR, 'libraries', 'Library', 'Frameworks',
1228
'Python.framework', 'Versions', getVersion(), 'lib')
1229
fw_lib_dir = os.path.join(
1230
WORKDIR, '_root', 'Library', 'Frameworks',
1231
'Python.framework', 'Versions', getVersion(), 'lib')
1232
if internalTk():
1233
# move Tcl and Tk pkgconfig files
1234
runCommand("mv %s/pkgconfig/* %s/pkgconfig"%(
1235
shellQuote(build_lib_dir),
1236
shellQuote(fw_lib_dir) ))
1237
runCommand("rm -r %s/pkgconfig"%(
1238
shellQuote(build_lib_dir), ))
1239
runCommand("mv %s/* %s"%(
1240
shellQuote(build_lib_dir),
1241
shellQuote(fw_lib_dir) ))
1242
1243
frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
1244
frmDirVersioned = os.path.join(frmDir, 'Versions', version)
1245
path_to_lib = os.path.join(frmDirVersioned, 'lib', 'python%s'%(version,))
1246
# create directory for OpenSSL certificates
1247
sslDir = os.path.join(frmDirVersioned, 'etc', 'openssl')
1248
os.makedirs(sslDir)
1249
1250
print("Fix file modes")
1251
gid = grp.getgrnam('admin').gr_gid
1252
1253
shared_lib_error = False
1254
for dirpath, dirnames, filenames in os.walk(frmDir):
1255
for dn in dirnames:
1256
os.chmod(os.path.join(dirpath, dn), STAT_0o775)
1257
os.chown(os.path.join(dirpath, dn), -1, gid)
1258
1259
for fn in filenames:
1260
if os.path.islink(fn):
1261
continue
1262
1263
# "chmod g+w $fn"
1264
p = os.path.join(dirpath, fn)
1265
st = os.stat(p)
1266
os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
1267
os.chown(p, -1, gid)
1268
1269
if fn in EXPECTED_SHARED_LIBS:
1270
# check to see that this file was linked with the
1271
# expected library path and version
1272
data = captureCommand("otool -L %s" % shellQuote(p))
1273
for sl in EXPECTED_SHARED_LIBS[fn]:
1274
if ("\t%s " % sl) not in data:
1275
print("Expected shared lib %s was not linked with %s"
1276
% (sl, p))
1277
shared_lib_error = True
1278
1279
if shared_lib_error:
1280
fatal("Unexpected shared library errors.")
1281
1282
if PYTHON_3:
1283
LDVERSION=None
1284
VERSION=None
1285
ABIFLAGS=None
1286
1287
fp = open(os.path.join(buildDir, 'Makefile'), 'r')
1288
for ln in fp:
1289
if ln.startswith('VERSION='):
1290
VERSION=ln.split()[1]
1291
if ln.startswith('ABIFLAGS='):
1292
ABIFLAGS=ln.split()
1293
ABIFLAGS=ABIFLAGS[1] if len(ABIFLAGS) > 1 else ''
1294
if ln.startswith('LDVERSION='):
1295
LDVERSION=ln.split()[1]
1296
fp.close()
1297
1298
LDVERSION = LDVERSION.replace('$(VERSION)', VERSION)
1299
LDVERSION = LDVERSION.replace('$(ABIFLAGS)', ABIFLAGS)
1300
config_suffix = '-' + LDVERSION
1301
if getVersionMajorMinor() >= (3, 6):
1302
config_suffix = config_suffix + '-darwin'
1303
else:
1304
config_suffix = '' # Python 2.x
1305
1306
# We added some directories to the search path during the configure
1307
# phase. Remove those because those directories won't be there on
1308
# the end-users system. Also remove the directories from _sysconfigdata.py
1309
# (added in 3.3) if it exists.
1310
1311
include_path = '-I%s/libraries/usr/local/include' % (WORKDIR,)
1312
lib_path = '-L%s/libraries/usr/local/lib' % (WORKDIR,)
1313
1314
# fix Makefile
1315
path = os.path.join(path_to_lib, 'config' + config_suffix, 'Makefile')
1316
fp = open(path, 'r')
1317
data = fp.read()
1318
fp.close()
1319
1320
for p in (include_path, lib_path):
1321
data = data.replace(" " + p, '')
1322
data = data.replace(p + " ", '')
1323
1324
fp = open(path, 'w')
1325
fp.write(data)
1326
fp.close()
1327
1328
# fix _sysconfigdata
1329
#
1330
# TODO: make this more robust! test_sysconfig_module of
1331
# distutils.tests.test_sysconfig.SysconfigTestCase tests that
1332
# the output from get_config_var in both sysconfig and
1333
# distutils.sysconfig is exactly the same for both CFLAGS and
1334
# LDFLAGS. The fixing up is now complicated by the pretty
1335
# printing in _sysconfigdata.py. Also, we are using the
1336
# pprint from the Python running the installer build which
1337
# may not cosmetically format the same as the pprint in the Python
1338
# being built (and which is used to originally generate
1339
# _sysconfigdata.py).
1340
1341
import pprint
1342
if getVersionMajorMinor() >= (3, 6):
1343
# XXX this is extra-fragile
1344
path = os.path.join(path_to_lib,
1345
'_sysconfigdata_%s_darwin_darwin.py' % (ABIFLAGS,))
1346
else:
1347
path = os.path.join(path_to_lib, '_sysconfigdata.py')
1348
fp = open(path, 'r')
1349
data = fp.read()
1350
fp.close()
1351
# create build_time_vars dict
1352
if RUNNING_ON_PYTHON2:
1353
exec(data)
1354
else:
1355
g_dict = {}
1356
l_dict = {}
1357
exec(data, g_dict, l_dict)
1358
build_time_vars = l_dict['build_time_vars']
1359
vars = {}
1360
for k, v in build_time_vars.items():
1361
if isinstance(v, str):
1362
for p in (include_path, lib_path):
1363
v = v.replace(' ' + p, '')
1364
v = v.replace(p + ' ', '')
1365
vars[k] = v
1366
1367
fp = open(path, 'w')
1368
# duplicated from sysconfig._generate_posix_vars()
1369
fp.write('# system configuration generated and used by'
1370
' the sysconfig module\n')
1371
fp.write('build_time_vars = ')
1372
pprint.pprint(vars, stream=fp)
1373
fp.close()
1374
1375
# Add symlinks in /usr/local/bin, using relative links
1376
usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
1377
to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
1378
'Python.framework', 'Versions', version, 'bin')
1379
if os.path.exists(usr_local_bin):
1380
shutil.rmtree(usr_local_bin)
1381
os.makedirs(usr_local_bin)
1382
for fn in os.listdir(
1383
os.path.join(frmDir, 'Versions', version, 'bin')):
1384
os.symlink(os.path.join(to_framework, fn),
1385
os.path.join(usr_local_bin, fn))
1386
1387
os.chdir(curdir)
1388
1389
def patchFile(inPath, outPath):
1390
data = fileContents(inPath)
1391
data = data.replace('$FULL_VERSION', getFullVersion())
1392
data = data.replace('$VERSION', getVersion())
1393
data = data.replace('$MACOSX_DEPLOYMENT_TARGET', ''.join((DEPTARGET, ' or later')))
1394
data = data.replace('$ARCHITECTURES', ", ".join(universal_opts_map[UNIVERSALARCHS]))
1395
data = data.replace('$INSTALL_SIZE', installSize())
1396
data = data.replace('$THIRD_PARTY_LIBS', "\\\n".join(THIRD_PARTY_LIBS))
1397
1398
# This one is not handy as a template variable
1399
data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
1400
fp = open(outPath, 'w')
1401
fp.write(data)
1402
fp.close()
1403
1404
def patchScript(inPath, outPath):
1405
major, minor = getVersionMajorMinor()
1406
data = fileContents(inPath)
1407
data = data.replace('@PYMAJOR@', str(major))
1408
data = data.replace('@PYVER@', getVersion())
1409
fp = open(outPath, 'w')
1410
fp.write(data)
1411
fp.close()
1412
os.chmod(outPath, STAT_0o755)
1413
1414
1415
1416
def packageFromRecipe(targetDir, recipe):
1417
curdir = os.getcwd()
1418
try:
1419
# The major version (such as 2.5) is included in the package name
1420
# because having two version of python installed at the same time is
1421
# common.
1422
pkgname = '%s-%s'%(recipe['name'], getVersion())
1423
srcdir = recipe.get('source')
1424
pkgroot = recipe.get('topdir', srcdir)
1425
postflight = recipe.get('postflight')
1426
readme = textwrap.dedent(recipe['readme'])
1427
isRequired = recipe.get('required', True)
1428
1429
print("- building package %s"%(pkgname,))
1430
1431
# Substitute some variables
1432
textvars = dict(
1433
VER=getVersion(),
1434
FULLVER=getFullVersion(),
1435
)
1436
readme = readme % textvars
1437
1438
if pkgroot is not None:
1439
pkgroot = pkgroot % textvars
1440
else:
1441
pkgroot = '/'
1442
1443
if srcdir is not None:
1444
srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
1445
srcdir = srcdir % textvars
1446
1447
if postflight is not None:
1448
postflight = os.path.abspath(postflight)
1449
1450
packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
1451
os.makedirs(packageContents)
1452
1453
if srcdir is not None:
1454
os.chdir(srcdir)
1455
runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
1456
runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
1457
runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
1458
1459
fn = os.path.join(packageContents, 'PkgInfo')
1460
fp = open(fn, 'w')
1461
fp.write('pmkrpkg1')
1462
fp.close()
1463
1464
rsrcDir = os.path.join(packageContents, "Resources")
1465
os.mkdir(rsrcDir)
1466
fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
1467
fp.write(readme)
1468
fp.close()
1469
1470
if postflight is not None:
1471
patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
1472
1473
vers = getFullVersion()
1474
major, minor = getVersionMajorMinor()
1475
pl = dict(
1476
CFBundleGetInfoString="Python.%s %s"%(pkgname, vers,),
1477
CFBundleIdentifier='org.python.Python.%s'%(pkgname,),
1478
CFBundleName='Python.%s'%(pkgname,),
1479
CFBundleShortVersionString=vers,
1480
IFMajorVersion=major,
1481
IFMinorVersion=minor,
1482
IFPkgFormatVersion=0.10000000149011612,
1483
IFPkgFlagAllowBackRev=False,
1484
IFPkgFlagAuthorizationAction="RootAuthorization",
1485
IFPkgFlagDefaultLocation=pkgroot,
1486
IFPkgFlagFollowLinks=True,
1487
IFPkgFlagInstallFat=True,
1488
IFPkgFlagIsRequired=isRequired,
1489
IFPkgFlagOverwritePermissions=False,
1490
IFPkgFlagRelocatable=False,
1491
IFPkgFlagRestartAction="NoRestart",
1492
IFPkgFlagRootVolumeOnly=True,
1493
IFPkgFlagUpdateInstalledLangauges=False,
1494
)
1495
writePlist(pl, os.path.join(packageContents, 'Info.plist'))
1496
1497
pl = dict(
1498
IFPkgDescriptionDescription=readme,
1499
IFPkgDescriptionTitle=recipe.get('long_name', "Python.%s"%(pkgname,)),
1500
IFPkgDescriptionVersion=vers,
1501
)
1502
writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
1503
1504
finally:
1505
os.chdir(curdir)
1506
1507
1508
def makeMpkgPlist(path):
1509
1510
vers = getFullVersion()
1511
major, minor = getVersionMajorMinor()
1512
1513
pl = dict(
1514
CFBundleGetInfoString="Python %s"%(vers,),
1515
CFBundleIdentifier='org.python.Python',
1516
CFBundleName='Python',
1517
CFBundleShortVersionString=vers,
1518
IFMajorVersion=major,
1519
IFMinorVersion=minor,
1520
IFPkgFlagComponentDirectory="Contents/Packages",
1521
IFPkgFlagPackageList=[
1522
dict(
1523
IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
1524
IFPkgFlagPackageSelection=item.get('selected', 'selected'),
1525
)
1526
for item in pkg_recipes()
1527
],
1528
IFPkgFormatVersion=0.10000000149011612,
1529
IFPkgFlagBackgroundScaling="proportional",
1530
IFPkgFlagBackgroundAlignment="left",
1531
IFPkgFlagAuthorizationAction="RootAuthorization",
1532
)
1533
1534
writePlist(pl, path)
1535
1536
1537
def buildInstaller():
1538
1539
# Zap all compiled files
1540
for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
1541
for fn in filenames:
1542
if fn.endswith('.pyc') or fn.endswith('.pyo'):
1543
os.unlink(os.path.join(dirpath, fn))
1544
1545
outdir = os.path.join(WORKDIR, 'installer')
1546
if os.path.exists(outdir):
1547
shutil.rmtree(outdir)
1548
os.mkdir(outdir)
1549
1550
pkgroot = os.path.join(outdir, 'Python.mpkg', 'Contents')
1551
pkgcontents = os.path.join(pkgroot, 'Packages')
1552
os.makedirs(pkgcontents)
1553
for recipe in pkg_recipes():
1554
packageFromRecipe(pkgcontents, recipe)
1555
1556
rsrcDir = os.path.join(pkgroot, 'Resources')
1557
1558
fn = os.path.join(pkgroot, 'PkgInfo')
1559
fp = open(fn, 'w')
1560
fp.write('pmkrpkg1')
1561
fp.close()
1562
1563
os.mkdir(rsrcDir)
1564
1565
makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
1566
pl = dict(
1567
IFPkgDescriptionTitle="Python",
1568
IFPkgDescriptionVersion=getVersion(),
1569
)
1570
1571
writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
1572
for fn in os.listdir('resources'):
1573
if fn == '.svn': continue
1574
if fn.endswith('.jpg'):
1575
shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
1576
else:
1577
patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
1578
1579
1580
def installSize(clear=False, _saved=[]):
1581
if clear:
1582
del _saved[:]
1583
if not _saved:
1584
data = captureCommand("du -ks %s"%(
1585
shellQuote(os.path.join(WORKDIR, '_root'))))
1586
_saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
1587
return _saved[0]
1588
1589
1590
def buildDMG():
1591
"""
1592
Create DMG containing the rootDir.
1593
"""
1594
outdir = os.path.join(WORKDIR, 'diskimage')
1595
if os.path.exists(outdir):
1596
shutil.rmtree(outdir)
1597
1598
# We used to use the deployment target as the last characters of the
1599
# installer file name. With the introduction of weaklinked installer
1600
# variants, we may have two variants with the same file name, i.e.
1601
# both ending in '10.9'. To avoid this, we now use the major/minor
1602
# version numbers of the macOS version we are building on.
1603
# Also, as of macOS 11, operating system version numbering has
1604
# changed from three components to two, i.e.
1605
# 10.14.1, 10.14.2, ...
1606
# 10.15.1, 10.15.2, ...
1607
# 11.1, 11.2, ...
1608
# 12.1, 12.2, ...
1609
# (A further twist is that, when running on macOS 11, binaries built
1610
# on older systems may be shown an operating system version of 10.16
1611
# instead of 11. We should not run into that situation here.)
1612
# Also we should use "macos" instead of "macosx" going forward.
1613
#
1614
# To maintain compatibility for legacy variants, the file name for
1615
# builds on macOS 10.15 and earlier remains:
1616
# python-3.x.y-macosx10.z.{dmg->pkg}
1617
# e.g. python-3.9.4-macosx10.9.{dmg->pkg}
1618
# and for builds on macOS 11+:
1619
# python-3.x.y-macosz.{dmg->pkg}
1620
# e.g. python-3.9.4-macos11.{dmg->pkg}
1621
1622
build_tuple = getBuildTuple()
1623
if build_tuple[0] < 11:
1624
os_name = 'macosx'
1625
build_system_version = '%s.%s' % build_tuple
1626
else:
1627
os_name = 'macos'
1628
build_system_version = str(build_tuple[0])
1629
imagepath = os.path.join(outdir,
1630
'python-%s-%s%s'%(getFullVersion(),os_name,build_system_version))
1631
if INCLUDE_TIMESTAMP:
1632
imagepath = imagepath + '-%04d-%02d-%02d'%(time.localtime()[:3])
1633
imagepath = imagepath + '.dmg'
1634
1635
os.mkdir(outdir)
1636
1637
# Try to mitigate race condition in certain versions of macOS, e.g. 10.9,
1638
# when hdiutil create fails with "Resource busy". For now, just retry
1639
# the create a few times and hope that it eventually works.
1640
1641
volname='Python %s'%(getFullVersion())
1642
cmd = ("hdiutil create -format UDRW -volname %s -srcfolder %s -size 100m %s"%(
1643
shellQuote(volname),
1644
shellQuote(os.path.join(WORKDIR, 'installer')),
1645
shellQuote(imagepath + ".tmp.dmg" )))
1646
for i in range(5):
1647
fd = os.popen(cmd, 'r')
1648
data = fd.read()
1649
xit = fd.close()
1650
if not xit:
1651
break
1652
sys.stdout.write(data)
1653
print(" -- retrying hdiutil create")
1654
time.sleep(5)
1655
else:
1656
raise RuntimeError("command failed: %s"%(cmd,))
1657
1658
if not os.path.exists(os.path.join(WORKDIR, "mnt")):
1659
os.mkdir(os.path.join(WORKDIR, "mnt"))
1660
runCommand("hdiutil attach %s -mountroot %s"%(
1661
shellQuote(imagepath + ".tmp.dmg"), shellQuote(os.path.join(WORKDIR, "mnt"))))
1662
1663
# Custom icon for the DMG, shown when the DMG is mounted.
1664
shutil.copy("../Icons/Disk Image.icns",
1665
os.path.join(WORKDIR, "mnt", volname, ".VolumeIcon.icns"))
1666
runCommand("SetFile -a C %s/"%(
1667
shellQuote(os.path.join(WORKDIR, "mnt", volname)),))
1668
1669
runCommand("hdiutil detach %s"%(shellQuote(os.path.join(WORKDIR, "mnt", volname))))
1670
1671
setIcon(imagepath + ".tmp.dmg", "../Icons/Disk Image.icns")
1672
runCommand("hdiutil convert %s -format UDZO -o %s"%(
1673
shellQuote(imagepath + ".tmp.dmg"), shellQuote(imagepath)))
1674
setIcon(imagepath, "../Icons/Disk Image.icns")
1675
1676
os.unlink(imagepath + ".tmp.dmg")
1677
1678
return imagepath
1679
1680
1681
def setIcon(filePath, icnsPath):
1682
"""
1683
Set the custom icon for the specified file or directory.
1684
"""
1685
1686
dirPath = os.path.normpath(os.path.dirname(__file__))
1687
toolPath = os.path.join(dirPath, "seticon.app/Contents/MacOS/seticon")
1688
if not os.path.exists(toolPath) or os.stat(toolPath).st_mtime < os.stat(dirPath + '/seticon.m').st_mtime:
1689
# NOTE: The tool is created inside an .app bundle, otherwise it won't work due
1690
# to connections to the window server.
1691
appPath = os.path.join(dirPath, "seticon.app/Contents/MacOS")
1692
if not os.path.exists(appPath):
1693
os.makedirs(appPath)
1694
runCommand("cc -o %s %s/seticon.m -framework Cocoa"%(
1695
shellQuote(toolPath), shellQuote(dirPath)))
1696
1697
runCommand("%s %s %s"%(shellQuote(os.path.abspath(toolPath)), shellQuote(icnsPath),
1698
shellQuote(filePath)))
1699
1700
def main():
1701
# First parse options and check if we can perform our work
1702
parseOptions()
1703
checkEnvironment()
1704
1705
os.environ['MACOSX_DEPLOYMENT_TARGET'] = DEPTARGET
1706
os.environ['CC'] = CC
1707
os.environ['CXX'] = CXX
1708
1709
if os.path.exists(WORKDIR):
1710
shutil.rmtree(WORKDIR)
1711
os.mkdir(WORKDIR)
1712
1713
os.environ['LC_ALL'] = 'C'
1714
1715
# Then build third-party libraries such as sleepycat DB4.
1716
buildLibraries()
1717
1718
# Now build python itself
1719
buildPython()
1720
1721
# And then build the documentation
1722
# Remove the Deployment Target from the shell
1723
# environment, it's no longer needed and
1724
# an unexpected build target can cause problems
1725
# when Sphinx and its dependencies need to
1726
# be (re-)installed.
1727
del os.environ['MACOSX_DEPLOYMENT_TARGET']
1728
buildPythonDocs()
1729
1730
1731
# Prepare the applications folder
1732
folder = os.path.join(WORKDIR, "_root", "Applications", "Python %s"%(
1733
getVersion(),))
1734
fn = os.path.join(folder, "License.rtf")
1735
patchFile("resources/License.rtf", fn)
1736
fn = os.path.join(folder, "ReadMe.rtf")
1737
patchFile("resources/ReadMe.rtf", fn)
1738
fn = os.path.join(folder, "Update Shell Profile.command")
1739
patchScript("scripts/postflight.patch-profile", fn)
1740
fn = os.path.join(folder, "Install Certificates.command")
1741
patchScript("resources/install_certificates.command", fn)
1742
os.chmod(folder, STAT_0o755)
1743
setIcon(folder, "../Icons/Python Folder.icns")
1744
1745
# Create the installer
1746
buildInstaller()
1747
1748
# And copy the readme into the directory containing the installer
1749
patchFile('resources/ReadMe.rtf',
1750
os.path.join(WORKDIR, 'installer', 'ReadMe.rtf'))
1751
1752
# Ditto for the license file.
1753
patchFile('resources/License.rtf',
1754
os.path.join(WORKDIR, 'installer', 'License.rtf'))
1755
1756
fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
1757
fp.write("# BUILD INFO\n")
1758
fp.write("# Date: %s\n" % time.ctime())
1759
fp.write("# By: %s\n" % pwd.getpwuid(os.getuid()).pw_gecos)
1760
fp.close()
1761
1762
# And copy it to a DMG
1763
buildDMG()
1764
1765
if __name__ == "__main__":
1766
main()
1767
1768