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