blob: b24f5dcee098bdd3771ec6f5b3c0731b60ee5f73 [file] [log] [blame]
Tim Peters8ff672e2006-05-23 21:55:53 +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..."
Ronald Oussoren73612362006-05-26 12:23:20 +0000594 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"%(
Tim Peters8ff672e2006-05-23 21:55:53 +0000595 shellQuote(os.path.join(SRCDIR, 'configure')),
Tim Peters7e8053f2006-05-26 12:31:20 +0000596 shellQuote(SDKPATH), shellQuote(WORKDIR)[1:-1],
Ronald Oussoren73612362006-05-26 12:23:20 +0000597 shellQuote(WORKDIR)[1:-1]))
Tim Peters8ff672e2006-05-23 21:55:53 +0000598
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'):
Ronald Oussoren73612362006-05-26 12:23:20 +0000842 if fn == '.svn': continue
Tim Peters8ff672e2006-05-23 21:55:53 +0000843 if fn.endswith('.jpg'):
844 shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
845 else:
846 patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
847
848 shutil.copy("../../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
849
850
851def installSize(clear=False, _saved=[]):
852 if clear:
853 del _saved[:]
854 if not _saved:
855 data = captureCommand("du -ks %s"%(
856 shellQuote(os.path.join(WORKDIR, '_root'))))
857 _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
858 return _saved[0]
859
860
861def buildDMG():
862 """
863 Create DMG containing the rootDir
864 """
865 outdir = os.path.join(WORKDIR, 'diskimage')
866 if os.path.exists(outdir):
867 shutil.rmtree(outdir)
868
869 imagepath = os.path.join(outdir,
870 'python-%s-macosx'%(getFullVersion(),))
871 if INCLUDE_TIMESTAMP:
872 imagepath = imagepath + '%04d-%02d-%02d'%(time.localtime()[:3])
873 imagepath = imagepath + '.dmg'
874
875 os.mkdir(outdir)
876 runCommand("hdiutil create -volname 'Univeral MacPython %s' -srcfolder %s %s"%(
877 getFullVersion(),
878 shellQuote(os.path.join(WORKDIR, 'installer')),
879 shellQuote(imagepath)))
880
881 return imagepath
882
883
884def setIcon(filePath, icnsPath):
885 """
886 Set the custom icon for the specified file or directory.
887
888 For a directory the icon data is written in a file named 'Icon\r' inside
889 the directory. For both files and directories write the icon as an 'icns'
890 resource. Furthermore set kHasCustomIcon in the finder flags for filePath.
891 """
892 ref, isDirectory = Carbon.File.FSPathMakeRef(icnsPath)
893 icon = Carbon.Icn.ReadIconFile(ref)
894 del ref
895
896 #
897 # Open the resource fork of the target, to add the icon later on.
898 # For directories we use the file 'Icon\r' inside the directory.
899 #
900
901 ref, isDirectory = Carbon.File.FSPathMakeRef(filePath)
902
903 if isDirectory:
904 tmpPath = os.path.join(filePath, "Icon\r")
905 if not os.path.exists(tmpPath):
906 fp = open(tmpPath, 'w')
907 fp.close()
908
909 tmpRef, _ = Carbon.File.FSPathMakeRef(tmpPath)
910 spec = Carbon.File.FSSpec(tmpRef)
911
912 else:
913 spec = Carbon.File.FSSpec(ref)
914
915 try:
916 Carbon.Res.HCreateResFile(*spec.as_tuple())
917 except MacOS.Error:
918 pass
919
920 # Try to create the resource fork again, this will avoid problems
921 # when adding an icon to a directory. I have no idea why this helps,
922 # but without this adding the icon to a directory will fail sometimes.
923 try:
924 Carbon.Res.HCreateResFile(*spec.as_tuple())
925 except MacOS.Error:
926 pass
927
928 refNum = Carbon.Res.FSpOpenResFile(spec, fsRdWrPerm)
929
930 Carbon.Res.UseResFile(refNum)
931
932 # Check if there already is an icon, remove it if there is.
933 try:
934 h = Carbon.Res.Get1Resource('icns', kCustomIconResource)
935 except MacOS.Error:
936 pass
937
938 else:
939 h.RemoveResource()
940 del h
941
942 # Add the icon to the resource for of the target
943 res = Carbon.Res.Resource(icon)
944 res.AddResource('icns', kCustomIconResource, '')
945 res.WriteResource()
946 res.DetachResource()
947 Carbon.Res.CloseResFile(refNum)
948
949 # And now set the kHasCustomIcon property for the target. Annoyingly,
950 # python doesn't seem to have bindings for the API that is needed for
951 # this. Cop out and call SetFile
952 os.system("/Developer/Tools/SetFile -a C %s"%(
953 shellQuote(filePath),))
954
955 if isDirectory:
956 os.system('/Developer/Tools/SetFile -a V %s'%(
957 shellQuote(tmpPath),
958 ))
959
960def main():
961 # First parse options and check if we can perform our work
962 parseOptions()
963 checkEnvironment()
964
965 os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.3'
966
967 if os.path.exists(WORKDIR):
968 shutil.rmtree(WORKDIR)
969 os.mkdir(WORKDIR)
970
971 # Then build third-party libraries such as sleepycat DB4.
972 buildLibraries()
973
974 # Now build python itself
975 buildPython()
976 buildPythonDocs()
977 fn = os.path.join(WORKDIR, "_root", "Applications",
978 "MacPython %s"%(getVersion(),), "Update Shell Profile.command")
979 shutil.copy("scripts/postflight.patch-profile", fn)
980 os.chmod(fn, 0755)
981
982 folder = os.path.join(WORKDIR, "_root", "Applications", "MacPython %s"%(
983 getVersion(),))
984 os.chmod(folder, 0755)
985 setIcon(folder, "../Icons/Python Folder.icns")
986
987 # Create the installer
988 buildInstaller()
989
990 # And copy the readme into the directory containing the installer
991 patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
992
993 # Ditto for the license file.
994 shutil.copy('../../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
995
996 fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
997 print >> fp, "# BUILD INFO"
998 print >> fp, "# Date:", time.ctime()
999 print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
1000 fp.close()
1001
1002 # Custom icon for the DMG, shown when the DMG is mounted.
1003 shutil.copy("../Icons/Disk Image.icns",
1004 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns"))
1005 os.system("/Developer/Tools/SetFile -a C %s"%(
1006 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns")))
1007
1008
1009 # And copy it to a DMG
1010 buildDMG()
1011
1012
1013if __name__ == "__main__":
1014 main()