blob: 05afe98babf080aa239f1fc382efa6e0d276ce01 [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.
239 """
240 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())
689 fp = open(outPath, 'wb')
690 fp.write(data)
691 fp.close()
692
693def patchScript(inPath, outPath):
694 data = fileContents(inPath)
695 data = data.replace('@PYVER@', getVersion())
696 fp = open(outPath, 'wb')
697 fp.write(data)
698 fp.close()
699 os.chmod(outPath, 0755)
700
701
702
703def packageFromRecipe(targetDir, recipe):
704 curdir = os.getcwd()
705 try:
706 pkgname = recipe['name']
707 srcdir = recipe.get('source')
708 pkgroot = recipe.get('topdir', srcdir)
709 postflight = recipe.get('postflight')
710 readme = textwrap.dedent(recipe['readme'])
711 isRequired = recipe.get('required', True)
712
713 print "- building package %s"%(pkgname,)
714
715 # Substitute some variables
716 textvars = dict(
717 VER=getVersion(),
718 FULLVER=getFullVersion(),
719 )
720 readme = readme % textvars
721
722 if pkgroot is not None:
723 pkgroot = pkgroot % textvars
724 else:
725 pkgroot = '/'
726
727 if srcdir is not None:
728 srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
729 srcdir = srcdir % textvars
730
731 if postflight is not None:
732 postflight = os.path.abspath(postflight)
733
734 packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
735 os.makedirs(packageContents)
736
737 if srcdir is not None:
738 os.chdir(srcdir)
739 runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
740 runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
741 runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
742
743 fn = os.path.join(packageContents, 'PkgInfo')
744 fp = open(fn, 'w')
745 fp.write('pmkrpkg1')
746 fp.close()
747
748 rsrcDir = os.path.join(packageContents, "Resources")
749 os.mkdir(rsrcDir)
750 fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
751 fp.write(readme)
752 fp.close()
753
754 if postflight is not None:
755 patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
756
757 vers = getFullVersion()
758 major, minor = map(int, getVersion().split('.', 2))
759 pl = Plist(
760 CFBundleGetInfoString="MacPython.%s %s"%(pkgname, vers,),
761 CFBundleIdentifier='org.python.MacPython.%s'%(pkgname,),
762 CFBundleName='MacPython.%s'%(pkgname,),
763 CFBundleShortVersionString=vers,
764 IFMajorVersion=major,
765 IFMinorVersion=minor,
766 IFPkgFormatVersion=0.10000000149011612,
767 IFPkgFlagAllowBackRev=False,
768 IFPkgFlagAuthorizationAction="RootAuthorization",
769 IFPkgFlagDefaultLocation=pkgroot,
770 IFPkgFlagFollowLinks=True,
771 IFPkgFlagInstallFat=True,
772 IFPkgFlagIsRequired=isRequired,
773 IFPkgFlagOverwritePermissions=False,
774 IFPkgFlagRelocatable=False,
775 IFPkgFlagRestartAction="NoRestart",
776 IFPkgFlagRootVolumeOnly=True,
777 IFPkgFlagUpdateInstalledLangauges=False,
778 )
779 writePlist(pl, os.path.join(packageContents, 'Info.plist'))
780
781 pl = Plist(
782 IFPkgDescriptionDescription=readme,
783 IFPkgDescriptionTitle=recipe.get('long_name', "MacPython.%s"%(pkgname,)),
784 IFPkgDescriptionVersion=vers,
785 )
786 writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
787
788 finally:
789 os.chdir(curdir)
790
791
792def makeMpkgPlist(path):
793
794 vers = getFullVersion()
795 major, minor = map(int, getVersion().split('.', 2))
796
797 pl = Plist(
798 CFBundleGetInfoString="MacPython %s"%(vers,),
799 CFBundleIdentifier='org.python.MacPython',
800 CFBundleName='MacPython',
801 CFBundleShortVersionString=vers,
802 IFMajorVersion=major,
803 IFMinorVersion=minor,
804 IFPkgFlagComponentDirectory="Contents/Packages",
805 IFPkgFlagPackageList=[
806 dict(
807 IFPkgFlagPackageLocation='%s.pkg'%(item['name']),
808 IFPkgFlagPackageSelection='selected'
809 )
810 for item in PKG_RECIPES
811 ],
812 IFPkgFormatVersion=0.10000000149011612,
813 IFPkgFlagBackgroundScaling="proportional",
814 IFPkgFlagBackgroundAlignment="left",
815 )
816
817 writePlist(pl, path)
818
819
820def buildInstaller():
821
822 # Zap all compiled files
823 for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
824 for fn in filenames:
825 if fn.endswith('.pyc') or fn.endswith('.pyo'):
826 os.unlink(os.path.join(dirpath, fn))
827
828 outdir = os.path.join(WORKDIR, 'installer')
829 if os.path.exists(outdir):
830 shutil.rmtree(outdir)
831 os.mkdir(outdir)
832
833 pkgroot = os.path.join(outdir, 'MacPython.mpkg', 'Contents')
834 pkgcontents = os.path.join(pkgroot, 'Packages')
835 os.makedirs(pkgcontents)
836 for recipe in PKG_RECIPES:
837 packageFromRecipe(pkgcontents, recipe)
838
839 rsrcDir = os.path.join(pkgroot, 'Resources')
840
841 fn = os.path.join(pkgroot, 'PkgInfo')
842 fp = open(fn, 'w')
843 fp.write('pmkrpkg1')
844 fp.close()
845
846 os.mkdir(rsrcDir)
847
848 makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
849 pl = Plist(
850 IFPkgDescriptionTitle="Universal MacPython",
851 IFPkgDescriptionVersion=getVersion(),
852 )
853
854 writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
855 for fn in os.listdir('resources'):
856 if fn == '.svn': continue
857 if fn.endswith('.jpg'):
858 shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
859 else:
860 patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
861
862 shutil.copy("../../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
863
864
865def installSize(clear=False, _saved=[]):
866 if clear:
867 del _saved[:]
868 if not _saved:
869 data = captureCommand("du -ks %s"%(
870 shellQuote(os.path.join(WORKDIR, '_root'))))
871 _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
872 return _saved[0]
873
874
875def buildDMG():
876 """
877 Create DMG containing the rootDir
878 """
879 outdir = os.path.join(WORKDIR, 'diskimage')
880 if os.path.exists(outdir):
881 shutil.rmtree(outdir)
882
883 imagepath = os.path.join(outdir,
884 'python-%s-macosx'%(getFullVersion(),))
885 if INCLUDE_TIMESTAMP:
886 imagepath = imagepath + '%04d-%02d-%02d'%(time.localtime()[:3])
887 imagepath = imagepath + '.dmg'
888
889 os.mkdir(outdir)
890 runCommand("hdiutil create -volname 'Univeral MacPython %s' -srcfolder %s %s"%(
891 getFullVersion(),
892 shellQuote(os.path.join(WORKDIR, 'installer')),
893 shellQuote(imagepath)))
894
895 return imagepath
896
897
898def setIcon(filePath, icnsPath):
899 """
900 Set the custom icon for the specified file or directory.
901
902 For a directory the icon data is written in a file named 'Icon\r' inside
903 the directory. For both files and directories write the icon as an 'icns'
904 resource. Furthermore set kHasCustomIcon in the finder flags for filePath.
905 """
906 ref, isDirectory = Carbon.File.FSPathMakeRef(icnsPath)
907 icon = Carbon.Icn.ReadIconFile(ref)
908 del ref
909
910 #
911 # Open the resource fork of the target, to add the icon later on.
912 # For directories we use the file 'Icon\r' inside the directory.
913 #
914
915 ref, isDirectory = Carbon.File.FSPathMakeRef(filePath)
916
917 if isDirectory:
918 tmpPath = os.path.join(filePath, "Icon\r")
919 if not os.path.exists(tmpPath):
920 fp = open(tmpPath, 'w')
921 fp.close()
922
923 tmpRef, _ = Carbon.File.FSPathMakeRef(tmpPath)
924 spec = Carbon.File.FSSpec(tmpRef)
925
926 else:
927 spec = Carbon.File.FSSpec(ref)
928
929 try:
930 Carbon.Res.HCreateResFile(*spec.as_tuple())
931 except MacOS.Error:
932 pass
933
934 # Try to create the resource fork again, this will avoid problems
935 # when adding an icon to a directory. I have no idea why this helps,
936 # but without this adding the icon to a directory will fail sometimes.
937 try:
938 Carbon.Res.HCreateResFile(*spec.as_tuple())
939 except MacOS.Error:
940 pass
941
942 refNum = Carbon.Res.FSpOpenResFile(spec, fsRdWrPerm)
943
944 Carbon.Res.UseResFile(refNum)
945
946 # Check if there already is an icon, remove it if there is.
947 try:
948 h = Carbon.Res.Get1Resource('icns', kCustomIconResource)
949 except MacOS.Error:
950 pass
951
952 else:
953 h.RemoveResource()
954 del h
955
956 # Add the icon to the resource for of the target
957 res = Carbon.Res.Resource(icon)
958 res.AddResource('icns', kCustomIconResource, '')
959 res.WriteResource()
960 res.DetachResource()
961 Carbon.Res.CloseResFile(refNum)
962
963 # And now set the kHasCustomIcon property for the target. Annoyingly,
964 # python doesn't seem to have bindings for the API that is needed for
965 # this. Cop out and call SetFile
966 os.system("/Developer/Tools/SetFile -a C %s"%(
967 shellQuote(filePath),))
968
969 if isDirectory:
970 os.system('/Developer/Tools/SetFile -a V %s'%(
971 shellQuote(tmpPath),
972 ))
973
974def main():
975 # First parse options and check if we can perform our work
976 parseOptions()
977 checkEnvironment()
978
979 os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.3'
980
981 if os.path.exists(WORKDIR):
982 shutil.rmtree(WORKDIR)
983 os.mkdir(WORKDIR)
984
985 # Then build third-party libraries such as sleepycat DB4.
986 buildLibraries()
987
988 # Now build python itself
989 buildPython()
990 buildPythonDocs()
991 fn = os.path.join(WORKDIR, "_root", "Applications",
992 "MacPython %s"%(getVersion(),), "Update Shell Profile.command")
993 shutil.copy("scripts/postflight.patch-profile", fn)
994 os.chmod(fn, 0755)
995
996 folder = os.path.join(WORKDIR, "_root", "Applications", "MacPython %s"%(
997 getVersion(),))
998 os.chmod(folder, 0755)
999 setIcon(folder, "../Icons/Python Folder.icns")
1000
1001 # Create the installer
1002 buildInstaller()
1003
1004 # And copy the readme into the directory containing the installer
1005 patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
1006
1007 # Ditto for the license file.
1008 shutil.copy('../../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
1009
1010 fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
1011 print >> fp, "# BUILD INFO"
1012 print >> fp, "# Date:", time.ctime()
1013 print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
1014 fp.close()
1015
1016 # Custom icon for the DMG, shown when the DMG is mounted.
1017 shutil.copy("../Icons/Disk Image.icns",
1018 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns"))
1019 os.system("/Developer/Tools/SetFile -a C %s"%(
1020 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns")))
1021
1022
1023 # And copy it to a DMG
1024 buildDMG()
1025
1026
1027if __name__ == "__main__":
1028 main()