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