blob: fcd97cd3ed3b681e098a510dc4b619d52edfb14c [file] [log] [blame]
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00001#!/usr/bin/python2.3
2"""
3This script is used to build the "official unofficial" universal build on
4Mac OS X. It requires Mac OS X 10.4, Xcode 2.2 and the 10.4u SDK to do its
5work.
6
7Please ensure that this script keeps working with Python 2.3, to avoid
8bootstrap issues (/usr/bin/python is Python 2.3 on OSX 10.4)
9
10Usage: see USAGE variable in the script.
11"""
12import platform, os, sys, getopt, textwrap, shutil, urllib2, stat, time, pwd
Ronald Oussoren0e258c52007-07-26 21:39:36 +000013import grp, md5
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000014
15INCLUDE_TIMESTAMP=1
16VERBOSE=1
17
18from plistlib import Plist
19
20import MacOS
21import Carbon.File
22import Carbon.Icn
23import Carbon.Res
24from Carbon.Files import kCustomIconResource, fsRdWrPerm, kHasCustomIcon
25from Carbon.Files import kFSCatInfoFinderInfo
26
27try:
28 from plistlib import writePlist
29except ImportError:
30 # We're run using python2.3
31 def writePlist(plist, path):
32 plist.write(path)
33
Ronald Oussoren0e258c52007-07-26 21:39:36 +000034
35
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000036def shellQuote(value):
37 """
Ronald Oussoren72e405b2006-11-07 15:54:38 +000038 Return the string value in a form that can safely be inserted into
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000039 a shell command.
40 """
41 return "'%s'"%(value.replace("'", "'\"'\"'"))
42
43def grepValue(fn, variable):
44 variable = variable + '='
45 for ln in open(fn, 'r'):
46 if ln.startswith(variable):
47 value = ln[len(variable):].strip()
48 return value[1:-1]
49
50def getVersion():
51 return grepValue(os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
52
53def getFullVersion():
54 fn = os.path.join(SRCDIR, 'Include', 'patchlevel.h')
55 for ln in open(fn):
56 if 'PY_VERSION' in ln:
57 return ln.split()[-1][1:-1]
58
59 raise RuntimeError, "Cannot find full version??"
60
Ronald Oussoren72e405b2006-11-07 15:54:38 +000061# The directory we'll use to create the build (will be erased and recreated)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000062WORKDIR="/tmp/_py"
63
Ronald Oussoren72e405b2006-11-07 15:54:38 +000064# The directory we'll use to store third-party sources. Set this to something
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000065# else if you don't want to re-fetch required libraries every time.
66DEPSRC=os.path.join(WORKDIR, 'third-party')
Ronald Oussoren0e258c52007-07-26 21:39:36 +000067DEPSRC=os.path.expanduser('/tmp/other-sources')
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000068
69# Location of the preferred SDK
70SDKPATH="/Developer/SDKs/MacOSX10.4u.sdk"
71#SDKPATH="/"
72
Ronald Oussoren9b8b6192006-06-27 12:53:52 +000073ARCHLIST=('i386', 'ppc',)
74
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000075# Source directory (asume we're in Mac/BuildScript)
76SRCDIR=os.path.dirname(
77 os.path.dirname(
78 os.path.dirname(
79 os.path.abspath(__file__
80 ))))
81
82USAGE=textwrap.dedent("""\
83 Usage: build_python [options]
84
85 Options:
86 -? or -h: Show this message
87 -b DIR
88 --build-dir=DIR: Create build here (default: %(WORKDIR)r)
89 --third-party=DIR: Store third-party sources here (default: %(DEPSRC)r)
90 --sdk-path=DIR: Location of the SDK (default: %(SDKPATH)r)
91 --src-dir=DIR: Location of the Python sources (default: %(SRCDIR)r)
92""")% globals()
93
94
95# Instructions for building libraries that are necessary for building a
96# batteries included python.
97LIBRARY_RECIPES=[
98 dict(
Ronald Oussoren0e258c52007-07-26 21:39:36 +000099 name="Bzip2 1.0.4",
100 url="http://www.bzip.org/1.0.4/bzip2-1.0.4.tar.gz",
101 checksum='fc310b254f6ba5fbb5da018f04533688',
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000102 configure=None,
103 install='make install PREFIX=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
104 shellQuote(os.path.join(WORKDIR, 'libraries')),
105 ' -arch '.join(ARCHLIST),
106 SDKPATH,
107 ),
108 ),
109 dict(
110 name="ZLib 1.2.3",
111 url="http://www.gzip.org/zlib/zlib-1.2.3.tar.gz",
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000112 checksum='debc62758716a169df9f62e6ab2bc634',
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000113 configure=None,
114 install='make install prefix=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
115 shellQuote(os.path.join(WORKDIR, 'libraries')),
116 ' -arch '.join(ARCHLIST),
117 SDKPATH,
118 ),
119 ),
120 dict(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000121 # Note that GNU readline is GPL'd software
122 name="GNU Readline 5.1.4",
123 url="http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz" ,
124 patchlevel='0',
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000125 checksum='7ee5a692db88b30ca48927a13fd60e46',
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000126 patches=[
127 # The readline maintainers don't do actual micro releases, but
128 # just ship a set of patches.
129 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-001',
130 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-002',
131 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-003',
132 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-004',
133 ]
134 ),
135
136 dict(
Martin v. Löwis20fde662008-12-17 18:03:01 +0000137 name="SQLite 3.6.7",
138 url="http://www.sqlite.org/sqlite-3.6.7.tar.gz",
139 checksum='5223d1f459b608ed8c2c54f8847f8e1a',
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000140 configure_pre=[
141 '--enable-threadsafe',
142 '--enable-tempstore',
143 '--enable-shared=no',
144 '--enable-static=yes',
145 '--disable-tcl',
146 ]
147 ),
148
149 dict(
150 name="NCurses 5.5",
151 url="http://ftp.gnu.org/pub/gnu/ncurses/ncurses-5.5.tar.gz",
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000152 checksum='e73c1ac10b4bfc46db43b2ddfd6244ef',
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000153 configure_pre=[
154 "--without-cxx",
155 "--without-ada",
156 "--without-progs",
157 "--without-curses-h",
158 "--enable-shared",
159 "--with-shared",
160 "--datadir=/usr/share",
161 "--sysconfdir=/etc",
162 "--sharedstatedir=/usr/com",
163 "--with-terminfo-dirs=/usr/share/terminfo",
164 "--with-default-terminfo-dir=/usr/share/terminfo",
165 "--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib"%(getVersion(),),
166 "--enable-termcap",
167 ],
168 patches=[
169 "ncurses-5.5.patch",
170 ],
171 useLDFlags=False,
172 install='make && make install DESTDIR=%s && cd %s/usr/local/lib && ln -fs ../../../Library/Frameworks/Python.framework/Versions/%s/lib/lib* .'%(
173 shellQuote(os.path.join(WORKDIR, 'libraries')),
174 shellQuote(os.path.join(WORKDIR, 'libraries')),
175 getVersion(),
176 ),
177 ),
178 dict(
179 name="Sleepycat DB 4.4",
Martin v. Löwis20fde662008-12-17 18:03:01 +0000180 url="http://download.oracle.com/berkeley-db/db-4.4.20.tar.gz",
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000181 checksum='d84dff288a19186b136b0daf7067ade3',
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000182 #name="Sleepycat DB 4.3.29",
183 #url="http://downloads.sleepycat.com/db-4.3.29.tar.gz",
184 buildDir="build_unix",
185 configure="../dist/configure",
186 configure_pre=[
187 '--includedir=/usr/local/include/db4',
188 ]
189 ),
190]
191
192
193# Instructions for building packages inside the .mpkg.
194PKG_RECIPES=[
195 dict(
196 name="PythonFramework",
197 long_name="Python Framework",
198 source="/Library/Frameworks/Python.framework",
199 readme="""\
200 This package installs Python.framework, that is the python
201 interpreter and the standard library. This also includes Python
202 wrappers for lots of Mac OS X API's.
203 """,
204 postflight="scripts/postflight.framework",
205 ),
206 dict(
207 name="PythonApplications",
208 long_name="GUI Applications",
209 source="/Applications/MacPython %(VER)s",
210 readme="""\
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000211 This package installs IDLE (an interactive Python IDE),
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000212 Python Launcher and Build Applet (create application bundles
213 from python scripts).
214
215 It also installs a number of examples and demos.
216 """,
217 required=False,
218 ),
219 dict(
220 name="PythonUnixTools",
221 long_name="UNIX command-line tools",
222 source="/usr/local/bin",
223 readme="""\
224 This package installs the unix tools in /usr/local/bin for
225 compatibility with older releases of MacPython. This package
226 is not necessary to use MacPython.
227 """,
228 required=False,
229 ),
230 dict(
231 name="PythonDocumentation",
232 long_name="Python Documentation",
233 topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
234 source="/pydocs",
235 readme="""\
236 This package installs the python documentation at a location
237 that is useable for pydoc and IDLE. If you have installed Xcode
238 it will also install a link to the documentation in
239 /Developer/Documentation/Python
240 """,
241 postflight="scripts/postflight.documentation",
242 required=False,
243 ),
244 dict(
245 name="PythonProfileChanges",
246 long_name="Shell profile updater",
247 readme="""\
248 This packages updates your shell profile to make sure that
249 the MacPython tools are found by your shell in preference of
250 the system provided Python tools.
251
252 If you don't install this package you'll have to add
253 "/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
254 to your PATH by hand.
255 """,
256 postflight="scripts/postflight.patch-profile",
257 topdir="/Library/Frameworks/Python.framework",
258 source="/empty-dir",
259 required=False,
260 ),
261 dict(
262 name="PythonSystemFixes",
263 long_name="Fix system Python",
264 readme="""\
Tim Petersae6a5a72006-06-07 20:40:06 +0000265 This package updates the system python installation on
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000266 Mac OS X 10.3 to ensure that you can build new python extensions
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000267 using that copy of python after installing this version.
Ronald Oussorenc5555542006-06-11 20:24:45 +0000268 """,
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000269 postflight="../Tools/fixapplepython23.py",
270 topdir="/Library/Frameworks/Python.framework",
271 source="/empty-dir",
272 required=False,
273 )
274]
275
276def fatal(msg):
277 """
278 A fatal error, bail out.
279 """
280 sys.stderr.write('FATAL: ')
281 sys.stderr.write(msg)
282 sys.stderr.write('\n')
283 sys.exit(1)
284
285def fileContents(fn):
286 """
287 Return the contents of the named file
288 """
289 return open(fn, 'rb').read()
290
291def runCommand(commandline):
292 """
293 Run a command and raise RuntimeError if it fails. Output is surpressed
294 unless the command fails.
295 """
296 fd = os.popen(commandline, 'r')
297 data = fd.read()
298 xit = fd.close()
299 if xit != None:
300 sys.stdout.write(data)
301 raise RuntimeError, "command failed: %s"%(commandline,)
302
303 if VERBOSE:
304 sys.stdout.write(data); sys.stdout.flush()
305
306def captureCommand(commandline):
307 fd = os.popen(commandline, 'r')
308 data = fd.read()
309 xit = fd.close()
310 if xit != None:
311 sys.stdout.write(data)
312 raise RuntimeError, "command failed: %s"%(commandline,)
313
314 return data
315
316def checkEnvironment():
317 """
318 Check that we're running on a supported system.
319 """
320
321 if platform.system() != 'Darwin':
322 fatal("This script should be run on a Mac OS X 10.4 system")
323
324 if platform.release() <= '8.':
325 fatal("This script should be run on a Mac OS X 10.4 system")
326
327 if not os.path.exists(SDKPATH):
328 fatal("Please install the latest version of Xcode and the %s SDK"%(
329 os.path.basename(SDKPATH[:-4])))
330
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000331 if os.path.exists('/sw'):
332 fatal("Detected Fink, please remove before building Python")
333
334 if os.path.exists('/opt/local'):
335 fatal("Detected MacPorts, please remove before building Python")
336
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000337def parseOptions(args = None):
338 """
339 Parse arguments and update global settings.
340 """
341 global WORKDIR, DEPSRC, SDKPATH, SRCDIR
342
343 if args is None:
344 args = sys.argv[1:]
345
346 try:
347 options, args = getopt.getopt(args, '?hb',
348 [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir='])
349 except getopt.error, msg:
350 print msg
351 sys.exit(1)
352
353 if args:
354 print "Additional arguments"
355 sys.exit(1)
356
357 for k, v in options:
358 if k in ('-h', '-?'):
359 print USAGE
360 sys.exit(0)
361
362 elif k in ('-d', '--build-dir'):
363 WORKDIR=v
364
365 elif k in ('--third-party',):
366 DEPSRC=v
367
368 elif k in ('--sdk-path',):
369 SDKPATH=v
370
371 elif k in ('--src-dir',):
372 SRCDIR=v
373
374 else:
375 raise NotImplementedError, k
376
377 SRCDIR=os.path.abspath(SRCDIR)
378 WORKDIR=os.path.abspath(WORKDIR)
379 SDKPATH=os.path.abspath(SDKPATH)
380 DEPSRC=os.path.abspath(DEPSRC)
381
382 print "Settings:"
383 print " * Source directory:", SRCDIR
384 print " * Build directory: ", WORKDIR
385 print " * SDK location: ", SDKPATH
386 print " * third-party source:", DEPSRC
387 print ""
388
389
390
391
392def extractArchive(builddir, archiveName):
393 """
394 Extract a source archive into 'builddir'. Returns the path of the
395 extracted archive.
396
397 XXX: This function assumes that archives contain a toplevel directory
398 that is has the same name as the basename of the archive. This is
399 save enough for anything we use.
400 """
401 curdir = os.getcwd()
402 try:
403 os.chdir(builddir)
404 if archiveName.endswith('.tar.gz'):
405 retval = os.path.basename(archiveName[:-7])
406 if os.path.exists(retval):
407 shutil.rmtree(retval)
408 fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')
409
410 elif archiveName.endswith('.tar.bz2'):
411 retval = os.path.basename(archiveName[:-8])
412 if os.path.exists(retval):
413 shutil.rmtree(retval)
414 fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')
415
416 elif archiveName.endswith('.tar'):
417 retval = os.path.basename(archiveName[:-4])
418 if os.path.exists(retval):
419 shutil.rmtree(retval)
420 fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')
421
422 elif archiveName.endswith('.zip'):
423 retval = os.path.basename(archiveName[:-4])
424 if os.path.exists(retval):
425 shutil.rmtree(retval)
426 fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')
427
428 data = fp.read()
429 xit = fp.close()
430 if xit is not None:
431 sys.stdout.write(data)
432 raise RuntimeError, "Cannot extract %s"%(archiveName,)
433
434 return os.path.join(builddir, retval)
435
436 finally:
437 os.chdir(curdir)
438
439KNOWNSIZES = {
440 "http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz": 7952742,
441 "http://downloads.sleepycat.com/db-4.4.20.tar.gz": 2030276,
442}
443
444def downloadURL(url, fname):
445 """
446 Download the contents of the url into the file.
447 """
448 try:
449 size = os.path.getsize(fname)
450 except OSError:
451 pass
452 else:
453 if KNOWNSIZES.get(url) == size:
454 print "Using existing file for", url
455 return
456 fpIn = urllib2.urlopen(url)
457 fpOut = open(fname, 'wb')
458 block = fpIn.read(10240)
459 try:
460 while block:
461 fpOut.write(block)
462 block = fpIn.read(10240)
463 fpIn.close()
464 fpOut.close()
465 except:
466 try:
467 os.unlink(fname)
468 except:
469 pass
470
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000471
472def verifyChecksum(path, checksum):
473 summer = md5.md5()
474 fp = open(path, 'rb')
475 block = fp.read(10240)
476 while block:
477 summer.update(block)
478 block = fp.read(10240)
479
480 return summer.hexdigest() == checksum
481
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000482def buildRecipe(recipe, basedir, archList):
483 """
484 Build software using a recipe. This function does the
485 'configure;make;make install' dance for C software, with a possibility
486 to customize this process, basically a poor-mans DarwinPorts.
487 """
488 curdir = os.getcwd()
489
490 name = recipe['name']
491 url = recipe['url']
492 configure = recipe.get('configure', './configure')
493 install = recipe.get('install', 'make && make install DESTDIR=%s'%(
494 shellQuote(basedir)))
495
496 archiveName = os.path.split(url)[-1]
497 sourceArchive = os.path.join(DEPSRC, archiveName)
498
499 if not os.path.exists(DEPSRC):
500 os.mkdir(DEPSRC)
501
502
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000503 if os.path.exists(sourceArchive) and verifyChecksum(sourceArchive, recipe['checksum']):
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000504 print "Using local copy of %s"%(name,)
505
506 else:
507 print "Downloading %s"%(name,)
508 downloadURL(url, sourceArchive)
509 print "Archive for %s stored as %s"%(name, sourceArchive)
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000510 if not verifyChecksum(sourceArchive, recipe['checksum']):
511 fatal("Download for %s failed: bad checksum"%(url,))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000512
513 print "Extracting archive for %s"%(name,)
514 buildDir=os.path.join(WORKDIR, '_bld')
515 if not os.path.exists(buildDir):
516 os.mkdir(buildDir)
517
518 workDir = extractArchive(buildDir, sourceArchive)
519 os.chdir(workDir)
520 if 'buildDir' in recipe:
521 os.chdir(recipe['buildDir'])
522
523
524 for fn in recipe.get('patches', ()):
525 if fn.startswith('http://'):
526 # Download the patch before applying it.
527 path = os.path.join(DEPSRC, os.path.basename(fn))
528 downloadURL(fn, path)
529 fn = path
530
531 fn = os.path.join(curdir, fn)
532 runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
533 shellQuote(fn),))
534
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000535 if configure is not None:
536 configure_args = [
537 "--prefix=/usr/local",
538 "--enable-static",
539 "--disable-shared",
540 #"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
541 ]
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000542
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000543 if 'configure_pre' in recipe:
544 args = list(recipe['configure_pre'])
545 if '--disable-static' in args:
546 configure_args.remove('--enable-static')
547 if '--enable-shared' in args:
548 configure_args.remove('--disable-shared')
549 configure_args.extend(args)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000550
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000551 if recipe.get('useLDFlags', 1):
552 configure_args.extend([
553 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
554 ' -arch '.join(archList),
555 shellQuote(SDKPATH)[1:-1],
556 shellQuote(basedir)[1:-1],),
557 "LDFLAGS=-syslibroot,%s -L%s/usr/local/lib -arch %s"%(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000558 shellQuote(SDKPATH)[1:-1],
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000559 shellQuote(basedir)[1:-1],
560 ' -arch '.join(archList)),
561 ])
562 else:
563 configure_args.extend([
564 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
565 ' -arch '.join(archList),
566 shellQuote(SDKPATH)[1:-1],
567 shellQuote(basedir)[1:-1],),
568 ])
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000569
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000570 if 'configure_post' in recipe:
571 configure_args = configure_args = list(recipe['configure_post'])
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000572
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000573 configure_args.insert(0, configure)
574 configure_args = [ shellQuote(a) for a in configure_args ]
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000575
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000576 print "Running configure for %s"%(name,)
577 runCommand(' '.join(configure_args) + ' 2>&1')
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000578
579 print "Running install for %s"%(name,)
580 runCommand('{ ' + install + ' ;} 2>&1')
581
582 print "Done %s"%(name,)
583 print ""
584
585 os.chdir(curdir)
586
587def buildLibraries():
588 """
589 Build our dependencies into $WORKDIR/libraries/usr/local
590 """
591 print ""
592 print "Building required libraries"
593 print ""
594 universal = os.path.join(WORKDIR, 'libraries')
595 os.mkdir(universal)
596 os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
597 os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))
598
599 for recipe in LIBRARY_RECIPES:
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000600 buildRecipe(recipe, universal, ARCHLIST)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000601
602
603
604def buildPythonDocs():
605 # This stores the documentation as Resources/English.lproj/Docuentation
606 # inside the framwork. pydoc and IDLE will pick it up there.
607 print "Install python documentation"
608 rootDir = os.path.join(WORKDIR, '_root')
609 version = getVersion()
610 docdir = os.path.join(rootDir, 'pydocs')
611
612 name = 'html-%s.tar.bz2'%(getFullVersion(),)
613 sourceArchive = os.path.join(DEPSRC, name)
614 if os.path.exists(sourceArchive):
615 print "Using local copy of %s"%(name,)
616
617 else:
618 print "Downloading %s"%(name,)
619 downloadURL('http://www.python.org/ftp/python/doc/%s/%s'%(
620 getFullVersion(), name), sourceArchive)
621 print "Archive for %s stored as %s"%(name, sourceArchive)
622
623 extractArchive(os.path.dirname(docdir), sourceArchive)
624 os.rename(
625 os.path.join(
626 os.path.dirname(docdir), 'Python-Docs-%s'%(getFullVersion(),)),
627 docdir)
628
629
630def buildPython():
631 print "Building a universal python"
632
633 buildDir = os.path.join(WORKDIR, '_bld', 'python')
634 rootDir = os.path.join(WORKDIR, '_root')
635
636 if os.path.exists(buildDir):
637 shutil.rmtree(buildDir)
638 if os.path.exists(rootDir):
639 shutil.rmtree(rootDir)
640 os.mkdir(buildDir)
641 os.mkdir(rootDir)
642 os.mkdir(os.path.join(rootDir, 'empty-dir'))
643 curdir = os.getcwd()
644 os.chdir(buildDir)
645
646 # Not sure if this is still needed, the original build script
647 # claims that parts of the install assume python.exe exists.
648 os.symlink('python', os.path.join(buildDir, 'python.exe'))
649
650 # Extract the version from the configure file, needed to calculate
651 # several paths.
652 version = getVersion()
653
654 print "Running configure..."
655 runCommand("%s -C --enable-framework --enable-universalsdk=%s LDFLAGS='-g -L%s/libraries/usr/local/lib' OPT='-g -O3 -I%s/libraries/usr/local/include' 2>&1"%(
656 shellQuote(os.path.join(SRCDIR, 'configure')),
657 shellQuote(SDKPATH), shellQuote(WORKDIR)[1:-1],
658 shellQuote(WORKDIR)[1:-1]))
659
660 print "Running make"
661 runCommand("make")
662
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000663 print "Running make frameworkinstall"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000664 runCommand("make frameworkinstall DESTDIR=%s"%(
665 shellQuote(rootDir)))
666
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000667 print "Running make frameworkinstallextras"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000668 runCommand("make frameworkinstallextras DESTDIR=%s"%(
669 shellQuote(rootDir)))
670
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000671 print "Copying required shared libraries"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000672 if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
673 runCommand("mv %s/* %s"%(
674 shellQuote(os.path.join(
675 WORKDIR, 'libraries', 'Library', 'Frameworks',
676 'Python.framework', 'Versions', getVersion(),
677 'lib')),
678 shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
679 'Python.framework', 'Versions', getVersion(),
680 'lib'))))
681
682 print "Fix file modes"
683 frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
Ronald Oussorena1cf44d2006-10-10 07:54:24 +0000684 gid = grp.getgrnam('admin').gr_gid
685
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000686 for dirpath, dirnames, filenames in os.walk(frmDir):
687 for dn in dirnames:
688 os.chmod(os.path.join(dirpath, dn), 0775)
Ronald Oussorena1cf44d2006-10-10 07:54:24 +0000689 os.chown(os.path.join(dirpath, dn), -1, gid)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000690
691 for fn in filenames:
692 if os.path.islink(fn):
693 continue
694
695 # "chmod g+w $fn"
696 p = os.path.join(dirpath, fn)
697 st = os.stat(p)
Ronald Oussorena1cf44d2006-10-10 07:54:24 +0000698 os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
699 os.chown(p, -1, gid)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000700
701 # We added some directories to the search path during the configure
702 # phase. Remove those because those directories won't be there on
703 # the end-users system.
704 path =os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework',
705 'Versions', version, 'lib', 'python%s'%(version,),
706 'config', 'Makefile')
707 fp = open(path, 'r')
708 data = fp.read()
709 fp.close()
710
711 data = data.replace('-L%s/libraries/usr/local/lib'%(WORKDIR,), '')
712 data = data.replace('-I%s/libraries/usr/local/include'%(WORKDIR,), '')
713 fp = open(path, 'w')
714 fp.write(data)
715 fp.close()
716
717 # Add symlinks in /usr/local/bin, using relative links
718 usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
719 to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
720 'Python.framework', 'Versions', version, 'bin')
721 if os.path.exists(usr_local_bin):
722 shutil.rmtree(usr_local_bin)
723 os.makedirs(usr_local_bin)
724 for fn in os.listdir(
725 os.path.join(frmDir, 'Versions', version, 'bin')):
726 os.symlink(os.path.join(to_framework, fn),
727 os.path.join(usr_local_bin, fn))
728
Ronald Oussoren8d273b02008-02-19 15:29:40 +0000729 # A quick fix that removes a spurious unittest failure when users
730 # upgrade their python2.5 install and then run Python's test suite.
731 # This is needed because one of the test files for the decimal module
732 # changed it name (see issue 2114).
733 test_dir = os.path.join(rootDir, 'Library', 'Frameworks',
734 'Python.framework', 'Versions', version, 'lib',
735 'python%s'%(version,), 'test', 'decimaltestdata')
736 data = open(os.path.join(test_dir, 'reduce.decTest'), 'r').read()
737 open(os.path.join(test_dir, 'normalize.decTest'), 'w').write(data)
738
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000739 os.chdir(curdir)
740
741
742
743def patchFile(inPath, outPath):
744 data = fileContents(inPath)
745 data = data.replace('$FULL_VERSION', getFullVersion())
746 data = data.replace('$VERSION', getVersion())
747 data = data.replace('$MACOSX_DEPLOYMENT_TARGET', '10.3 or later')
748 data = data.replace('$ARCHITECTURES', "i386, ppc")
749 data = data.replace('$INSTALL_SIZE', installSize())
Ronald Oussorenc5555542006-06-11 20:24:45 +0000750
751 # This one is not handy as a template variable
752 data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000753 fp = open(outPath, 'wb')
754 fp.write(data)
755 fp.close()
756
757def patchScript(inPath, outPath):
758 data = fileContents(inPath)
759 data = data.replace('@PYVER@', getVersion())
760 fp = open(outPath, 'wb')
761 fp.write(data)
762 fp.close()
763 os.chmod(outPath, 0755)
764
765
766
767def packageFromRecipe(targetDir, recipe):
768 curdir = os.getcwd()
769 try:
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000770 # The major version (such as 2.5) is included in the package name
771 # because having two version of python installed at the same time is
Ronald Oussorenc5555542006-06-11 20:24:45 +0000772 # common.
773 pkgname = '%s-%s'%(recipe['name'], getVersion())
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000774 srcdir = recipe.get('source')
775 pkgroot = recipe.get('topdir', srcdir)
776 postflight = recipe.get('postflight')
777 readme = textwrap.dedent(recipe['readme'])
778 isRequired = recipe.get('required', True)
779
780 print "- building package %s"%(pkgname,)
781
782 # Substitute some variables
783 textvars = dict(
784 VER=getVersion(),
785 FULLVER=getFullVersion(),
786 )
787 readme = readme % textvars
788
789 if pkgroot is not None:
790 pkgroot = pkgroot % textvars
791 else:
792 pkgroot = '/'
793
794 if srcdir is not None:
795 srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
796 srcdir = srcdir % textvars
797
798 if postflight is not None:
799 postflight = os.path.abspath(postflight)
800
801 packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
802 os.makedirs(packageContents)
803
804 if srcdir is not None:
805 os.chdir(srcdir)
806 runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
807 runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
808 runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
809
810 fn = os.path.join(packageContents, 'PkgInfo')
811 fp = open(fn, 'w')
812 fp.write('pmkrpkg1')
813 fp.close()
814
815 rsrcDir = os.path.join(packageContents, "Resources")
816 os.mkdir(rsrcDir)
817 fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
818 fp.write(readme)
819 fp.close()
820
821 if postflight is not None:
822 patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
823
824 vers = getFullVersion()
825 major, minor = map(int, getVersion().split('.', 2))
826 pl = Plist(
827 CFBundleGetInfoString="MacPython.%s %s"%(pkgname, vers,),
828 CFBundleIdentifier='org.python.MacPython.%s'%(pkgname,),
829 CFBundleName='MacPython.%s'%(pkgname,),
830 CFBundleShortVersionString=vers,
831 IFMajorVersion=major,
832 IFMinorVersion=minor,
833 IFPkgFormatVersion=0.10000000149011612,
834 IFPkgFlagAllowBackRev=False,
835 IFPkgFlagAuthorizationAction="RootAuthorization",
836 IFPkgFlagDefaultLocation=pkgroot,
837 IFPkgFlagFollowLinks=True,
838 IFPkgFlagInstallFat=True,
839 IFPkgFlagIsRequired=isRequired,
840 IFPkgFlagOverwritePermissions=False,
841 IFPkgFlagRelocatable=False,
842 IFPkgFlagRestartAction="NoRestart",
843 IFPkgFlagRootVolumeOnly=True,
844 IFPkgFlagUpdateInstalledLangauges=False,
845 )
846 writePlist(pl, os.path.join(packageContents, 'Info.plist'))
847
848 pl = Plist(
849 IFPkgDescriptionDescription=readme,
850 IFPkgDescriptionTitle=recipe.get('long_name', "MacPython.%s"%(pkgname,)),
851 IFPkgDescriptionVersion=vers,
852 )
853 writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
854
855 finally:
856 os.chdir(curdir)
857
858
859def makeMpkgPlist(path):
860
861 vers = getFullVersion()
862 major, minor = map(int, getVersion().split('.', 2))
863
864 pl = Plist(
865 CFBundleGetInfoString="MacPython %s"%(vers,),
866 CFBundleIdentifier='org.python.MacPython',
867 CFBundleName='MacPython',
868 CFBundleShortVersionString=vers,
869 IFMajorVersion=major,
870 IFMinorVersion=minor,
871 IFPkgFlagComponentDirectory="Contents/Packages",
872 IFPkgFlagPackageList=[
873 dict(
Ronald Oussorenc5555542006-06-11 20:24:45 +0000874 IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000875 IFPkgFlagPackageSelection='selected'
876 )
877 for item in PKG_RECIPES
878 ],
879 IFPkgFormatVersion=0.10000000149011612,
880 IFPkgFlagBackgroundScaling="proportional",
881 IFPkgFlagBackgroundAlignment="left",
Ronald Oussorenc5555542006-06-11 20:24:45 +0000882 IFPkgFlagAuthorizationAction="RootAuthorization",
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000883 )
884
885 writePlist(pl, path)
886
887
888def buildInstaller():
889
890 # Zap all compiled files
891 for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
892 for fn in filenames:
893 if fn.endswith('.pyc') or fn.endswith('.pyo'):
894 os.unlink(os.path.join(dirpath, fn))
895
896 outdir = os.path.join(WORKDIR, 'installer')
897 if os.path.exists(outdir):
898 shutil.rmtree(outdir)
899 os.mkdir(outdir)
900
901 pkgroot = os.path.join(outdir, 'MacPython.mpkg', 'Contents')
902 pkgcontents = os.path.join(pkgroot, 'Packages')
903 os.makedirs(pkgcontents)
904 for recipe in PKG_RECIPES:
905 packageFromRecipe(pkgcontents, recipe)
906
907 rsrcDir = os.path.join(pkgroot, 'Resources')
908
909 fn = os.path.join(pkgroot, 'PkgInfo')
910 fp = open(fn, 'w')
911 fp.write('pmkrpkg1')
912 fp.close()
913
914 os.mkdir(rsrcDir)
915
916 makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
917 pl = Plist(
918 IFPkgDescriptionTitle="Universal MacPython",
919 IFPkgDescriptionVersion=getVersion(),
920 )
921
922 writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
923 for fn in os.listdir('resources'):
924 if fn == '.svn': continue
925 if fn.endswith('.jpg'):
926 shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
927 else:
928 patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
929
Ronald Oussorenc5555542006-06-11 20:24:45 +0000930 shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000931
932
933def installSize(clear=False, _saved=[]):
934 if clear:
935 del _saved[:]
936 if not _saved:
937 data = captureCommand("du -ks %s"%(
938 shellQuote(os.path.join(WORKDIR, '_root'))))
939 _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
940 return _saved[0]
941
942
943def buildDMG():
944 """
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000945 Create DMG containing the rootDir.
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000946 """
947 outdir = os.path.join(WORKDIR, 'diskimage')
948 if os.path.exists(outdir):
949 shutil.rmtree(outdir)
950
951 imagepath = os.path.join(outdir,
952 'python-%s-macosx'%(getFullVersion(),))
953 if INCLUDE_TIMESTAMP:
954 imagepath = imagepath + '%04d-%02d-%02d'%(time.localtime()[:3])
955 imagepath = imagepath + '.dmg'
956
957 os.mkdir(outdir)
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000958 runCommand("hdiutil create -volname 'Universal MacPython %s' -srcfolder %s %s"%(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000959 getFullVersion(),
960 shellQuote(os.path.join(WORKDIR, 'installer')),
961 shellQuote(imagepath)))
962
963 return imagepath
964
965
966def setIcon(filePath, icnsPath):
967 """
968 Set the custom icon for the specified file or directory.
969
970 For a directory the icon data is written in a file named 'Icon\r' inside
971 the directory. For both files and directories write the icon as an 'icns'
972 resource. Furthermore set kHasCustomIcon in the finder flags for filePath.
973 """
974 ref, isDirectory = Carbon.File.FSPathMakeRef(icnsPath)
975 icon = Carbon.Icn.ReadIconFile(ref)
976 del ref
977
978 #
979 # Open the resource fork of the target, to add the icon later on.
980 # For directories we use the file 'Icon\r' inside the directory.
981 #
982
983 ref, isDirectory = Carbon.File.FSPathMakeRef(filePath)
984
985 if isDirectory:
Ronald Oussoren736fc562006-09-17 18:40:15 +0000986 # There is a problem with getting this into the pax(1) archive,
987 # just ignore directory icons for now.
988 return
989
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000990 tmpPath = os.path.join(filePath, "Icon\r")
991 if not os.path.exists(tmpPath):
992 fp = open(tmpPath, 'w')
993 fp.close()
994
995 tmpRef, _ = Carbon.File.FSPathMakeRef(tmpPath)
996 spec = Carbon.File.FSSpec(tmpRef)
997
998 else:
999 spec = Carbon.File.FSSpec(ref)
1000
1001 try:
1002 Carbon.Res.HCreateResFile(*spec.as_tuple())
1003 except MacOS.Error:
1004 pass
1005
1006 # Try to create the resource fork again, this will avoid problems
1007 # when adding an icon to a directory. I have no idea why this helps,
1008 # but without this adding the icon to a directory will fail sometimes.
1009 try:
1010 Carbon.Res.HCreateResFile(*spec.as_tuple())
1011 except MacOS.Error:
1012 pass
1013
1014 refNum = Carbon.Res.FSpOpenResFile(spec, fsRdWrPerm)
1015
1016 Carbon.Res.UseResFile(refNum)
1017
1018 # Check if there already is an icon, remove it if there is.
1019 try:
1020 h = Carbon.Res.Get1Resource('icns', kCustomIconResource)
1021 except MacOS.Error:
1022 pass
1023
1024 else:
1025 h.RemoveResource()
1026 del h
1027
1028 # Add the icon to the resource for of the target
1029 res = Carbon.Res.Resource(icon)
1030 res.AddResource('icns', kCustomIconResource, '')
1031 res.WriteResource()
1032 res.DetachResource()
1033 Carbon.Res.CloseResFile(refNum)
1034
1035 # And now set the kHasCustomIcon property for the target. Annoyingly,
1036 # python doesn't seem to have bindings for the API that is needed for
1037 # this. Cop out and call SetFile
1038 os.system("/Developer/Tools/SetFile -a C %s"%(
1039 shellQuote(filePath),))
1040
1041 if isDirectory:
1042 os.system('/Developer/Tools/SetFile -a V %s'%(
1043 shellQuote(tmpPath),
1044 ))
1045
Ronald Oussoren0e258c52007-07-26 21:39:36 +00001046
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00001047def main():
1048 # First parse options and check if we can perform our work
1049 parseOptions()
1050 checkEnvironment()
1051
1052 os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.3'
1053
1054 if os.path.exists(WORKDIR):
1055 shutil.rmtree(WORKDIR)
1056 os.mkdir(WORKDIR)
1057
1058 # Then build third-party libraries such as sleepycat DB4.
1059 buildLibraries()
1060
1061 # Now build python itself
1062 buildPython()
1063 buildPythonDocs()
1064 fn = os.path.join(WORKDIR, "_root", "Applications",
1065 "MacPython %s"%(getVersion(),), "Update Shell Profile.command")
Ronald Oussoren9b8b6192006-06-27 12:53:52 +00001066 patchFile("scripts/postflight.patch-profile", fn)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00001067 os.chmod(fn, 0755)
1068
1069 folder = os.path.join(WORKDIR, "_root", "Applications", "MacPython %s"%(
1070 getVersion(),))
1071 os.chmod(folder, 0755)
1072 setIcon(folder, "../Icons/Python Folder.icns")
1073
1074 # Create the installer
1075 buildInstaller()
1076
1077 # And copy the readme into the directory containing the installer
1078 patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
1079
1080 # Ditto for the license file.
Ronald Oussorenc5555542006-06-11 20:24:45 +00001081 shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00001082
1083 fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
1084 print >> fp, "# BUILD INFO"
1085 print >> fp, "# Date:", time.ctime()
1086 print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
1087 fp.close()
1088
1089 # Custom icon for the DMG, shown when the DMG is mounted.
1090 shutil.copy("../Icons/Disk Image.icns",
1091 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns"))
1092 os.system("/Developer/Tools/SetFile -a C %s"%(
1093 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns")))
1094
1095
1096 # And copy it to a DMG
1097 buildDMG()
1098
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00001099if __name__ == "__main__":
1100 main()