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