blob: 7ffb065897d0f35e7610f93d2c58dcba24d6be2a [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(
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000137 name="SQLite 3.3.14",
138 url="http://www.sqlite.org/sqlite-3.3.14.tar.gz",
139 checksum='e1a4428a5cb17f28164731b72f06130a',
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",
180 url="http://downloads.sleepycat.com/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
337 if not os.path.exists('/Library/Frameworks/Tcl.framework') or \
338 not os.path.exists('/Library/Frameworks/Tk.framework'):
339
340 fatal("Please install a Universal Tcl/Tk framework in /Library from\n\thttp://tcltkaqua.sourceforge.net/")
341
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000342
343
344def parseOptions(args = None):
345 """
346 Parse arguments and update global settings.
347 """
348 global WORKDIR, DEPSRC, SDKPATH, SRCDIR
349
350 if args is None:
351 args = sys.argv[1:]
352
353 try:
354 options, args = getopt.getopt(args, '?hb',
355 [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir='])
356 except getopt.error, msg:
357 print msg
358 sys.exit(1)
359
360 if args:
361 print "Additional arguments"
362 sys.exit(1)
363
364 for k, v in options:
365 if k in ('-h', '-?'):
366 print USAGE
367 sys.exit(0)
368
369 elif k in ('-d', '--build-dir'):
370 WORKDIR=v
371
372 elif k in ('--third-party',):
373 DEPSRC=v
374
375 elif k in ('--sdk-path',):
376 SDKPATH=v
377
378 elif k in ('--src-dir',):
379 SRCDIR=v
380
381 else:
382 raise NotImplementedError, k
383
384 SRCDIR=os.path.abspath(SRCDIR)
385 WORKDIR=os.path.abspath(WORKDIR)
386 SDKPATH=os.path.abspath(SDKPATH)
387 DEPSRC=os.path.abspath(DEPSRC)
388
389 print "Settings:"
390 print " * Source directory:", SRCDIR
391 print " * Build directory: ", WORKDIR
392 print " * SDK location: ", SDKPATH
393 print " * third-party source:", DEPSRC
394 print ""
395
396
397
398
399def extractArchive(builddir, archiveName):
400 """
401 Extract a source archive into 'builddir'. Returns the path of the
402 extracted archive.
403
404 XXX: This function assumes that archives contain a toplevel directory
405 that is has the same name as the basename of the archive. This is
406 save enough for anything we use.
407 """
408 curdir = os.getcwd()
409 try:
410 os.chdir(builddir)
411 if archiveName.endswith('.tar.gz'):
412 retval = os.path.basename(archiveName[:-7])
413 if os.path.exists(retval):
414 shutil.rmtree(retval)
415 fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')
416
417 elif archiveName.endswith('.tar.bz2'):
418 retval = os.path.basename(archiveName[:-8])
419 if os.path.exists(retval):
420 shutil.rmtree(retval)
421 fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')
422
423 elif archiveName.endswith('.tar'):
424 retval = os.path.basename(archiveName[:-4])
425 if os.path.exists(retval):
426 shutil.rmtree(retval)
427 fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')
428
429 elif archiveName.endswith('.zip'):
430 retval = os.path.basename(archiveName[:-4])
431 if os.path.exists(retval):
432 shutil.rmtree(retval)
433 fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')
434
435 data = fp.read()
436 xit = fp.close()
437 if xit is not None:
438 sys.stdout.write(data)
439 raise RuntimeError, "Cannot extract %s"%(archiveName,)
440
441 return os.path.join(builddir, retval)
442
443 finally:
444 os.chdir(curdir)
445
446KNOWNSIZES = {
447 "http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz": 7952742,
448 "http://downloads.sleepycat.com/db-4.4.20.tar.gz": 2030276,
449}
450
451def downloadURL(url, fname):
452 """
453 Download the contents of the url into the file.
454 """
455 try:
456 size = os.path.getsize(fname)
457 except OSError:
458 pass
459 else:
460 if KNOWNSIZES.get(url) == size:
461 print "Using existing file for", url
462 return
463 fpIn = urllib2.urlopen(url)
464 fpOut = open(fname, 'wb')
465 block = fpIn.read(10240)
466 try:
467 while block:
468 fpOut.write(block)
469 block = fpIn.read(10240)
470 fpIn.close()
471 fpOut.close()
472 except:
473 try:
474 os.unlink(fname)
475 except:
476 pass
477
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000478
479def verifyChecksum(path, checksum):
480 summer = md5.md5()
481 fp = open(path, 'rb')
482 block = fp.read(10240)
483 while block:
484 summer.update(block)
485 block = fp.read(10240)
486
487 return summer.hexdigest() == checksum
488
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000489def buildRecipe(recipe, basedir, archList):
490 """
491 Build software using a recipe. This function does the
492 'configure;make;make install' dance for C software, with a possibility
493 to customize this process, basically a poor-mans DarwinPorts.
494 """
495 curdir = os.getcwd()
496
497 name = recipe['name']
498 url = recipe['url']
499 configure = recipe.get('configure', './configure')
500 install = recipe.get('install', 'make && make install DESTDIR=%s'%(
501 shellQuote(basedir)))
502
503 archiveName = os.path.split(url)[-1]
504 sourceArchive = os.path.join(DEPSRC, archiveName)
505
506 if not os.path.exists(DEPSRC):
507 os.mkdir(DEPSRC)
508
509
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000510 if os.path.exists(sourceArchive) and verifyChecksum(sourceArchive, recipe['checksum']):
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000511 print "Using local copy of %s"%(name,)
512
513 else:
514 print "Downloading %s"%(name,)
515 downloadURL(url, sourceArchive)
516 print "Archive for %s stored as %s"%(name, sourceArchive)
Ronald Oussoren0e258c52007-07-26 21:39:36 +0000517 if not verifyChecksum(sourceArchive, recipe['checksum']):
518 fatal("Download for %s failed: bad checksum"%(url,))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000519
520 print "Extracting archive for %s"%(name,)
521 buildDir=os.path.join(WORKDIR, '_bld')
522 if not os.path.exists(buildDir):
523 os.mkdir(buildDir)
524
525 workDir = extractArchive(buildDir, sourceArchive)
526 os.chdir(workDir)
527 if 'buildDir' in recipe:
528 os.chdir(recipe['buildDir'])
529
530
531 for fn in recipe.get('patches', ()):
532 if fn.startswith('http://'):
533 # Download the patch before applying it.
534 path = os.path.join(DEPSRC, os.path.basename(fn))
535 downloadURL(fn, path)
536 fn = path
537
538 fn = os.path.join(curdir, fn)
539 runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
540 shellQuote(fn),))
541
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000542 if configure is not None:
543 configure_args = [
544 "--prefix=/usr/local",
545 "--enable-static",
546 "--disable-shared",
547 #"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
548 ]
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000549
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000550 if 'configure_pre' in recipe:
551 args = list(recipe['configure_pre'])
552 if '--disable-static' in args:
553 configure_args.remove('--enable-static')
554 if '--enable-shared' in args:
555 configure_args.remove('--disable-shared')
556 configure_args.extend(args)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000557
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000558 if recipe.get('useLDFlags', 1):
559 configure_args.extend([
560 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
561 ' -arch '.join(archList),
562 shellQuote(SDKPATH)[1:-1],
563 shellQuote(basedir)[1:-1],),
564 "LDFLAGS=-syslibroot,%s -L%s/usr/local/lib -arch %s"%(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000565 shellQuote(SDKPATH)[1:-1],
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000566 shellQuote(basedir)[1:-1],
567 ' -arch '.join(archList)),
568 ])
569 else:
570 configure_args.extend([
571 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
572 ' -arch '.join(archList),
573 shellQuote(SDKPATH)[1:-1],
574 shellQuote(basedir)[1:-1],),
575 ])
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000576
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000577 if 'configure_post' in recipe:
578 configure_args = configure_args = list(recipe['configure_post'])
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000579
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000580 configure_args.insert(0, configure)
581 configure_args = [ shellQuote(a) for a in configure_args ]
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000582
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000583 print "Running configure for %s"%(name,)
584 runCommand(' '.join(configure_args) + ' 2>&1')
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000585
586 print "Running install for %s"%(name,)
587 runCommand('{ ' + install + ' ;} 2>&1')
588
589 print "Done %s"%(name,)
590 print ""
591
592 os.chdir(curdir)
593
594def buildLibraries():
595 """
596 Build our dependencies into $WORKDIR/libraries/usr/local
597 """
598 print ""
599 print "Building required libraries"
600 print ""
601 universal = os.path.join(WORKDIR, 'libraries')
602 os.mkdir(universal)
603 os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
604 os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))
605
606 for recipe in LIBRARY_RECIPES:
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000607 buildRecipe(recipe, universal, ARCHLIST)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000608
609
610
611def buildPythonDocs():
612 # This stores the documentation as Resources/English.lproj/Docuentation
613 # inside the framwork. pydoc and IDLE will pick it up there.
614 print "Install python documentation"
615 rootDir = os.path.join(WORKDIR, '_root')
616 version = getVersion()
617 docdir = os.path.join(rootDir, 'pydocs')
618
619 name = 'html-%s.tar.bz2'%(getFullVersion(),)
620 sourceArchive = os.path.join(DEPSRC, name)
621 if os.path.exists(sourceArchive):
622 print "Using local copy of %s"%(name,)
623
624 else:
625 print "Downloading %s"%(name,)
626 downloadURL('http://www.python.org/ftp/python/doc/%s/%s'%(
627 getFullVersion(), name), sourceArchive)
628 print "Archive for %s stored as %s"%(name, sourceArchive)
629
630 extractArchive(os.path.dirname(docdir), sourceArchive)
631 os.rename(
632 os.path.join(
633 os.path.dirname(docdir), 'Python-Docs-%s'%(getFullVersion(),)),
634 docdir)
635
636
637def buildPython():
638 print "Building a universal python"
639
640 buildDir = os.path.join(WORKDIR, '_bld', 'python')
641 rootDir = os.path.join(WORKDIR, '_root')
642
643 if os.path.exists(buildDir):
644 shutil.rmtree(buildDir)
645 if os.path.exists(rootDir):
646 shutil.rmtree(rootDir)
647 os.mkdir(buildDir)
648 os.mkdir(rootDir)
649 os.mkdir(os.path.join(rootDir, 'empty-dir'))
650 curdir = os.getcwd()
651 os.chdir(buildDir)
652
653 # Not sure if this is still needed, the original build script
654 # claims that parts of the install assume python.exe exists.
655 os.symlink('python', os.path.join(buildDir, 'python.exe'))
656
657 # Extract the version from the configure file, needed to calculate
658 # several paths.
659 version = getVersion()
660
661 print "Running configure..."
662 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"%(
663 shellQuote(os.path.join(SRCDIR, 'configure')),
664 shellQuote(SDKPATH), shellQuote(WORKDIR)[1:-1],
665 shellQuote(WORKDIR)[1:-1]))
666
667 print "Running make"
668 runCommand("make")
669
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000670 print "Running make frameworkinstall"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000671 runCommand("make frameworkinstall DESTDIR=%s"%(
672 shellQuote(rootDir)))
673
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000674 print "Running make frameworkinstallextras"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000675 runCommand("make frameworkinstallextras DESTDIR=%s"%(
676 shellQuote(rootDir)))
677
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000678 print "Copying required shared libraries"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000679 if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
680 runCommand("mv %s/* %s"%(
681 shellQuote(os.path.join(
682 WORKDIR, 'libraries', 'Library', 'Frameworks',
683 'Python.framework', 'Versions', getVersion(),
684 'lib')),
685 shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
686 'Python.framework', 'Versions', getVersion(),
687 'lib'))))
688
689 print "Fix file modes"
690 frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
Ronald Oussorena1cf44d2006-10-10 07:54:24 +0000691 gid = grp.getgrnam('admin').gr_gid
692
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000693 for dirpath, dirnames, filenames in os.walk(frmDir):
694 for dn in dirnames:
695 os.chmod(os.path.join(dirpath, dn), 0775)
Ronald Oussorena1cf44d2006-10-10 07:54:24 +0000696 os.chown(os.path.join(dirpath, dn), -1, gid)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000697
698 for fn in filenames:
699 if os.path.islink(fn):
700 continue
701
702 # "chmod g+w $fn"
703 p = os.path.join(dirpath, fn)
704 st = os.stat(p)
Ronald Oussorena1cf44d2006-10-10 07:54:24 +0000705 os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
706 os.chown(p, -1, gid)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000707
708 # We added some directories to the search path during the configure
709 # phase. Remove those because those directories won't be there on
710 # the end-users system.
711 path =os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework',
712 'Versions', version, 'lib', 'python%s'%(version,),
713 'config', 'Makefile')
714 fp = open(path, 'r')
715 data = fp.read()
716 fp.close()
717
718 data = data.replace('-L%s/libraries/usr/local/lib'%(WORKDIR,), '')
719 data = data.replace('-I%s/libraries/usr/local/include'%(WORKDIR,), '')
720 fp = open(path, 'w')
721 fp.write(data)
722 fp.close()
723
724 # Add symlinks in /usr/local/bin, using relative links
725 usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
726 to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
727 'Python.framework', 'Versions', version, 'bin')
728 if os.path.exists(usr_local_bin):
729 shutil.rmtree(usr_local_bin)
730 os.makedirs(usr_local_bin)
731 for fn in os.listdir(
732 os.path.join(frmDir, 'Versions', version, 'bin')):
733 os.symlink(os.path.join(to_framework, fn),
734 os.path.join(usr_local_bin, fn))
735
736 os.chdir(curdir)
737
738
739
740def patchFile(inPath, outPath):
741 data = fileContents(inPath)
742 data = data.replace('$FULL_VERSION', getFullVersion())
743 data = data.replace('$VERSION', getVersion())
744 data = data.replace('$MACOSX_DEPLOYMENT_TARGET', '10.3 or later')
745 data = data.replace('$ARCHITECTURES', "i386, ppc")
746 data = data.replace('$INSTALL_SIZE', installSize())
Ronald Oussorenc5555542006-06-11 20:24:45 +0000747
748 # This one is not handy as a template variable
749 data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000750 fp = open(outPath, 'wb')
751 fp.write(data)
752 fp.close()
753
754def patchScript(inPath, outPath):
755 data = fileContents(inPath)
756 data = data.replace('@PYVER@', getVersion())
757 fp = open(outPath, 'wb')
758 fp.write(data)
759 fp.close()
760 os.chmod(outPath, 0755)
761
762
763
764def packageFromRecipe(targetDir, recipe):
765 curdir = os.getcwd()
766 try:
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000767 # The major version (such as 2.5) is included in the package name
768 # because having two version of python installed at the same time is
Ronald Oussorenc5555542006-06-11 20:24:45 +0000769 # common.
770 pkgname = '%s-%s'%(recipe['name'], getVersion())
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000771 srcdir = recipe.get('source')
772 pkgroot = recipe.get('topdir', srcdir)
773 postflight = recipe.get('postflight')
774 readme = textwrap.dedent(recipe['readme'])
775 isRequired = recipe.get('required', True)
776
777 print "- building package %s"%(pkgname,)
778
779 # Substitute some variables
780 textvars = dict(
781 VER=getVersion(),
782 FULLVER=getFullVersion(),
783 )
784 readme = readme % textvars
785
786 if pkgroot is not None:
787 pkgroot = pkgroot % textvars
788 else:
789 pkgroot = '/'
790
791 if srcdir is not None:
792 srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
793 srcdir = srcdir % textvars
794
795 if postflight is not None:
796 postflight = os.path.abspath(postflight)
797
798 packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
799 os.makedirs(packageContents)
800
801 if srcdir is not None:
802 os.chdir(srcdir)
803 runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
804 runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
805 runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
806
807 fn = os.path.join(packageContents, 'PkgInfo')
808 fp = open(fn, 'w')
809 fp.write('pmkrpkg1')
810 fp.close()
811
812 rsrcDir = os.path.join(packageContents, "Resources")
813 os.mkdir(rsrcDir)
814 fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
815 fp.write(readme)
816 fp.close()
817
818 if postflight is not None:
819 patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
820
821 vers = getFullVersion()
822 major, minor = map(int, getVersion().split('.', 2))
823 pl = Plist(
824 CFBundleGetInfoString="MacPython.%s %s"%(pkgname, vers,),
825 CFBundleIdentifier='org.python.MacPython.%s'%(pkgname,),
826 CFBundleName='MacPython.%s'%(pkgname,),
827 CFBundleShortVersionString=vers,
828 IFMajorVersion=major,
829 IFMinorVersion=minor,
830 IFPkgFormatVersion=0.10000000149011612,
831 IFPkgFlagAllowBackRev=False,
832 IFPkgFlagAuthorizationAction="RootAuthorization",
833 IFPkgFlagDefaultLocation=pkgroot,
834 IFPkgFlagFollowLinks=True,
835 IFPkgFlagInstallFat=True,
836 IFPkgFlagIsRequired=isRequired,
837 IFPkgFlagOverwritePermissions=False,
838 IFPkgFlagRelocatable=False,
839 IFPkgFlagRestartAction="NoRestart",
840 IFPkgFlagRootVolumeOnly=True,
841 IFPkgFlagUpdateInstalledLangauges=False,
842 )
843 writePlist(pl, os.path.join(packageContents, 'Info.plist'))
844
845 pl = Plist(
846 IFPkgDescriptionDescription=readme,
847 IFPkgDescriptionTitle=recipe.get('long_name', "MacPython.%s"%(pkgname,)),
848 IFPkgDescriptionVersion=vers,
849 )
850 writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
851
852 finally:
853 os.chdir(curdir)
854
855
856def makeMpkgPlist(path):
857
858 vers = getFullVersion()
859 major, minor = map(int, getVersion().split('.', 2))
860
861 pl = Plist(
862 CFBundleGetInfoString="MacPython %s"%(vers,),
863 CFBundleIdentifier='org.python.MacPython',
864 CFBundleName='MacPython',
865 CFBundleShortVersionString=vers,
866 IFMajorVersion=major,
867 IFMinorVersion=minor,
868 IFPkgFlagComponentDirectory="Contents/Packages",
869 IFPkgFlagPackageList=[
870 dict(
Ronald Oussorenc5555542006-06-11 20:24:45 +0000871 IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000872 IFPkgFlagPackageSelection='selected'
873 )
874 for item in PKG_RECIPES
875 ],
876 IFPkgFormatVersion=0.10000000149011612,
877 IFPkgFlagBackgroundScaling="proportional",
878 IFPkgFlagBackgroundAlignment="left",
Ronald Oussorenc5555542006-06-11 20:24:45 +0000879 IFPkgFlagAuthorizationAction="RootAuthorization",
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000880 )
881
882 writePlist(pl, path)
883
884
885def buildInstaller():
886
887 # Zap all compiled files
888 for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
889 for fn in filenames:
890 if fn.endswith('.pyc') or fn.endswith('.pyo'):
891 os.unlink(os.path.join(dirpath, fn))
892
893 outdir = os.path.join(WORKDIR, 'installer')
894 if os.path.exists(outdir):
895 shutil.rmtree(outdir)
896 os.mkdir(outdir)
897
898 pkgroot = os.path.join(outdir, 'MacPython.mpkg', 'Contents')
899 pkgcontents = os.path.join(pkgroot, 'Packages')
900 os.makedirs(pkgcontents)
901 for recipe in PKG_RECIPES:
902 packageFromRecipe(pkgcontents, recipe)
903
904 rsrcDir = os.path.join(pkgroot, 'Resources')
905
906 fn = os.path.join(pkgroot, 'PkgInfo')
907 fp = open(fn, 'w')
908 fp.write('pmkrpkg1')
909 fp.close()
910
911 os.mkdir(rsrcDir)
912
913 makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
914 pl = Plist(
915 IFPkgDescriptionTitle="Universal MacPython",
916 IFPkgDescriptionVersion=getVersion(),
917 )
918
919 writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
920 for fn in os.listdir('resources'):
921 if fn == '.svn': continue
922 if fn.endswith('.jpg'):
923 shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
924 else:
925 patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
926
Ronald Oussorenc5555542006-06-11 20:24:45 +0000927 shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000928
929
930def installSize(clear=False, _saved=[]):
931 if clear:
932 del _saved[:]
933 if not _saved:
934 data = captureCommand("du -ks %s"%(
935 shellQuote(os.path.join(WORKDIR, '_root'))))
936 _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
937 return _saved[0]
938
939
940def buildDMG():
941 """
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000942 Create DMG containing the rootDir.
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000943 """
944 outdir = os.path.join(WORKDIR, 'diskimage')
945 if os.path.exists(outdir):
946 shutil.rmtree(outdir)
947
948 imagepath = os.path.join(outdir,
949 'python-%s-macosx'%(getFullVersion(),))
950 if INCLUDE_TIMESTAMP:
951 imagepath = imagepath + '%04d-%02d-%02d'%(time.localtime()[:3])
952 imagepath = imagepath + '.dmg'
953
954 os.mkdir(outdir)
Ronald Oussoren72e405b2006-11-07 15:54:38 +0000955 runCommand("hdiutil create -volname 'Universal MacPython %s' -srcfolder %s %s"%(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000956 getFullVersion(),
957 shellQuote(os.path.join(WORKDIR, 'installer')),
958 shellQuote(imagepath)))
959
960 return imagepath
961
962
963def setIcon(filePath, icnsPath):
964 """
965 Set the custom icon for the specified file or directory.
966
967 For a directory the icon data is written in a file named 'Icon\r' inside
968 the directory. For both files and directories write the icon as an 'icns'
969 resource. Furthermore set kHasCustomIcon in the finder flags for filePath.
970 """
971 ref, isDirectory = Carbon.File.FSPathMakeRef(icnsPath)
972 icon = Carbon.Icn.ReadIconFile(ref)
973 del ref
974
975 #
976 # Open the resource fork of the target, to add the icon later on.
977 # For directories we use the file 'Icon\r' inside the directory.
978 #
979
980 ref, isDirectory = Carbon.File.FSPathMakeRef(filePath)
981
982 if isDirectory:
Ronald Oussoren736fc562006-09-17 18:40:15 +0000983 # There is a problem with getting this into the pax(1) archive,
984 # just ignore directory icons for now.
985 return
986
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000987 tmpPath = os.path.join(filePath, "Icon\r")
988 if not os.path.exists(tmpPath):
989 fp = open(tmpPath, 'w')
990 fp.close()
991
992 tmpRef, _ = Carbon.File.FSPathMakeRef(tmpPath)
993 spec = Carbon.File.FSSpec(tmpRef)
994
995 else:
996 spec = Carbon.File.FSSpec(ref)
997
998 try:
999 Carbon.Res.HCreateResFile(*spec.as_tuple())
1000 except MacOS.Error:
1001 pass
1002
1003 # Try to create the resource fork again, this will avoid problems
1004 # when adding an icon to a directory. I have no idea why this helps,
1005 # but without this adding the icon to a directory will fail sometimes.
1006 try:
1007 Carbon.Res.HCreateResFile(*spec.as_tuple())
1008 except MacOS.Error:
1009 pass
1010
1011 refNum = Carbon.Res.FSpOpenResFile(spec, fsRdWrPerm)
1012
1013 Carbon.Res.UseResFile(refNum)
1014
1015 # Check if there already is an icon, remove it if there is.
1016 try:
1017 h = Carbon.Res.Get1Resource('icns', kCustomIconResource)
1018 except MacOS.Error:
1019 pass
1020
1021 else:
1022 h.RemoveResource()
1023 del h
1024
1025 # Add the icon to the resource for of the target
1026 res = Carbon.Res.Resource(icon)
1027 res.AddResource('icns', kCustomIconResource, '')
1028 res.WriteResource()
1029 res.DetachResource()
1030 Carbon.Res.CloseResFile(refNum)
1031
1032 # And now set the kHasCustomIcon property for the target. Annoyingly,
1033 # python doesn't seem to have bindings for the API that is needed for
1034 # this. Cop out and call SetFile
1035 os.system("/Developer/Tools/SetFile -a C %s"%(
1036 shellQuote(filePath),))
1037
1038 if isDirectory:
1039 os.system('/Developer/Tools/SetFile -a V %s'%(
1040 shellQuote(tmpPath),
1041 ))
1042
Ronald Oussoren0e258c52007-07-26 21:39:36 +00001043
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00001044def main():
1045 # First parse options and check if we can perform our work
1046 parseOptions()
1047 checkEnvironment()
1048
1049 os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.3'
1050
1051 if os.path.exists(WORKDIR):
1052 shutil.rmtree(WORKDIR)
1053 os.mkdir(WORKDIR)
1054
1055 # Then build third-party libraries such as sleepycat DB4.
1056 buildLibraries()
1057
1058 # Now build python itself
1059 buildPython()
1060 buildPythonDocs()
1061 fn = os.path.join(WORKDIR, "_root", "Applications",
1062 "MacPython %s"%(getVersion(),), "Update Shell Profile.command")
Ronald Oussoren9b8b6192006-06-27 12:53:52 +00001063 patchFile("scripts/postflight.patch-profile", fn)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00001064 os.chmod(fn, 0755)
1065
1066 folder = os.path.join(WORKDIR, "_root", "Applications", "MacPython %s"%(
1067 getVersion(),))
1068 os.chmod(folder, 0755)
1069 setIcon(folder, "../Icons/Python Folder.icns")
1070
1071 # Create the installer
1072 buildInstaller()
1073
1074 # And copy the readme into the directory containing the installer
1075 patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
1076
1077 # Ditto for the license file.
Ronald Oussorenc5555542006-06-11 20:24:45 +00001078 shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00001079
1080 fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
1081 print >> fp, "# BUILD INFO"
1082 print >> fp, "# Date:", time.ctime()
1083 print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
1084 fp.close()
1085
1086 # Custom icon for the DMG, shown when the DMG is mounted.
1087 shutil.copy("../Icons/Disk Image.icns",
1088 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns"))
1089 os.system("/Developer/Tools/SetFile -a C %s"%(
1090 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns")))
1091
1092
1093 # And copy it to a DMG
1094 buildDMG()
1095
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00001096if __name__ == "__main__":
1097 main()