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