blob: add051276c10abce16d7a39375a315e3b67268e6 [file] [log] [blame]
Thomas Wouters477c8d52006-05-27 19:21:47 +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"""
Benjamin Peterson623918e2008-12-20 22:50:25 +000012import platform, os, sys, getopt, textwrap, shutil, urllib2, stat, time, pwd
Thomas Wouters89f507f2006-12-13 04:49:30 +000013import grp
Thomas Wouters477c8d52006-05-27 19:21:47 +000014
Thomas Wouters89f507f2006-12-13 04:49:30 +000015INCLUDE_TIMESTAMP = 1
16VERBOSE = 1
Thomas Wouters477c8d52006-05-27 19:21:47 +000017
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 """
Thomas Wouters89f507f2006-12-13 04:49:30 +000036 Return the string value in a form that can safely be inserted into
Thomas Wouters477c8d52006-05-27 19:21:47 +000037 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
Benjamin Peterson623918e2008-12-20 22:50:25 +000057 raise RuntimeError, "Cannot find full version??"
Thomas Wouters477c8d52006-05-27 19:21:47 +000058
Thomas Wouters89f507f2006-12-13 04:49:30 +000059# The directory we'll use to create the build (will be erased and recreated)
60WORKDIR = "/tmp/_py"
Thomas Wouters477c8d52006-05-27 19:21:47 +000061
Thomas Wouters89f507f2006-12-13 04:49:30 +000062# The directory we'll use to store third-party sources. Set this to something
Thomas Wouters477c8d52006-05-27 19:21:47 +000063# else if you don't want to re-fetch required libraries every time.
Thomas Wouters89f507f2006-12-13 04:49:30 +000064DEPSRC = os.path.join(WORKDIR, 'third-party')
65DEPSRC = os.path.expanduser('~/Universal/other-sources')
Thomas Wouters477c8d52006-05-27 19:21:47 +000066
67# Location of the preferred SDK
Thomas Wouters89f507f2006-12-13 04:49:30 +000068SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
69#SDKPATH = "/"
Thomas Wouters477c8d52006-05-27 19:21:47 +000070
Thomas Wouters89f507f2006-12-13 04:49:30 +000071ARCHLIST = ('i386', 'ppc',)
Thomas Wouters0e3f5912006-08-11 14:57:12 +000072
Thomas Wouters73e5a5b2006-06-08 15:35:45 +000073# Source directory (asume we're in Mac/BuildScript)
Thomas Wouters89f507f2006-12-13 04:49:30 +000074SRCDIR = os.path.dirname(
Thomas Wouters477c8d52006-05-27 19:21:47 +000075 os.path.dirname(
76 os.path.dirname(
Thomas Wouters73e5a5b2006-06-08 15:35:45 +000077 os.path.abspath(__file__
78 ))))
Thomas Wouters477c8d52006-05-27 19:21:47 +000079
Thomas Wouters89f507f2006-12-13 04:49:30 +000080USAGE = textwrap.dedent("""\
Thomas Wouters477c8d52006-05-27 19:21:47 +000081 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.
Thomas Wouters89f507f2006-12-13 04:49:30 +000095LIBRARY_RECIPES = [
Thomas Wouters477c8d52006-05-27 19:21:47 +000096 dict(
Thomas Wouters0e3f5912006-08-11 14:57:12 +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(
Thomas Wouters477c8d52006-05-27 19:21:47 +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(
Benjamin Petersonf10a79a2008-10-11 00:49:57 +0000132 name="SQLite 3.6.3",
133 url="http://www.sqlite.org/sqlite-3.6.3.tar.gz",
Thomas Wouters477c8d52006-05-27 19:21:47 +0000134 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 ),
Thomas Wouters477c8d52006-05-27 19:21:47 +0000172]
173
174
175# Instructions for building packages inside the .mpkg.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000176PKG_RECIPES = [
Thomas Wouters477c8d52006-05-27 19:21:47 +0000177 dict(
178 name="PythonFramework",
179 long_name="Python Framework",
180 source="/Library/Frameworks/Python.framework",
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 postflight="scripts/postflight.framework",
187 ),
188 dict(
189 name="PythonApplications",
190 long_name="GUI Applications",
Christian Heimes81ee3ef2008-05-04 22:42:01 +0000191 source="/Applications/Python %(VER)s",
Thomas Wouters477c8d52006-05-27 19:21:47 +0000192 readme="""\
Thomas Wouters89f507f2006-12-13 04:49:30 +0000193 This package installs IDLE (an interactive Python IDE),
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000194 Python Launcher and Build Applet (create application bundles
195 from python scripts).
196
197 It also installs a number of examples and demos.
Thomas Wouters477c8d52006-05-27 19:21:47 +0000198 """,
199 required=False,
200 ),
201 dict(
202 name="PythonUnixTools",
203 long_name="UNIX command-line tools",
204 source="/usr/local/bin",
205 readme="""\
206 This package installs the unix tools in /usr/local/bin for
207 compatibility with older releases of MacPython. This package
208 is not necessary to use MacPython.
209 """,
210 required=False,
211 ),
212 dict(
213 name="PythonDocumentation",
214 long_name="Python Documentation",
215 topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
216 source="/pydocs",
217 readme="""\
218 This package installs the python documentation at a location
219 that is useable for pydoc and IDLE. If you have installed Xcode
220 it will also install a link to the documentation in
221 /Developer/Documentation/Python
222 """,
223 postflight="scripts/postflight.documentation",
224 required=False,
225 ),
226 dict(
227 name="PythonProfileChanges",
228 long_name="Shell profile updater",
229 readme="""\
230 This packages updates your shell profile to make sure that
231 the MacPython tools are found by your shell in preference of
232 the system provided Python tools.
233
234 If you don't install this package you'll have to add
235 "/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
236 to your PATH by hand.
237 """,
238 postflight="scripts/postflight.patch-profile",
239 topdir="/Library/Frameworks/Python.framework",
240 source="/empty-dir",
241 required=False,
242 ),
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000243 dict(
244 name="PythonSystemFixes",
245 long_name="Fix system Python",
246 readme="""\
247 This package updates the system python installation on
248 Mac OS X 10.3 to ensure that you can build new python extensions
Thomas Wouters89f507f2006-12-13 04:49:30 +0000249 using that copy of python after installing this version.
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000250 """,
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000251 postflight="../Tools/fixapplepython23.py",
252 topdir="/Library/Frameworks/Python.framework",
253 source="/empty-dir",
254 required=False,
255 )
Thomas Wouters477c8d52006-05-27 19:21:47 +0000256]
257
Thomas Wouters477c8d52006-05-27 19:21:47 +0000258def fatal(msg):
259 """
260 A fatal error, bail out.
261 """
262 sys.stderr.write('FATAL: ')
263 sys.stderr.write(msg)
264 sys.stderr.write('\n')
265 sys.exit(1)
266
267def fileContents(fn):
268 """
269 Return the contents of the named file
270 """
271 return open(fn, 'rb').read()
272
273def runCommand(commandline):
274 """
275 Run a command and raise RuntimeError if it fails. Output is surpressed
276 unless the command fails.
277 """
278 fd = os.popen(commandline, 'r')
279 data = fd.read()
280 xit = fd.close()
Benjamin Peterson2a691a82008-03-31 01:51:45 +0000281 if xit is not None:
Thomas Wouters477c8d52006-05-27 19:21:47 +0000282 sys.stdout.write(data)
Benjamin Peterson623918e2008-12-20 22:50:25 +0000283 raise RuntimeError, "command failed: %s"%(commandline,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000284
285 if VERBOSE:
286 sys.stdout.write(data); sys.stdout.flush()
287
288def captureCommand(commandline):
289 fd = os.popen(commandline, 'r')
290 data = fd.read()
291 xit = fd.close()
Benjamin Peterson2a691a82008-03-31 01:51:45 +0000292 if xit is not None:
Thomas Wouters477c8d52006-05-27 19:21:47 +0000293 sys.stdout.write(data)
Benjamin Peterson623918e2008-12-20 22:50:25 +0000294 raise RuntimeError, "command failed: %s"%(commandline,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000295
296 return data
297
298def checkEnvironment():
299 """
300 Check that we're running on a supported system.
301 """
302
303 if platform.system() != 'Darwin':
304 fatal("This script should be run on a Mac OS X 10.4 system")
305
306 if platform.release() <= '8.':
307 fatal("This script should be run on a Mac OS X 10.4 system")
308
309 if not os.path.exists(SDKPATH):
310 fatal("Please install the latest version of Xcode and the %s SDK"%(
311 os.path.basename(SDKPATH[:-4])))
312
313
314
Thomas Wouters89f507f2006-12-13 04:49:30 +0000315def parseOptions(args=None):
Thomas Wouters477c8d52006-05-27 19:21:47 +0000316 """
317 Parse arguments and update global settings.
318 """
319 global WORKDIR, DEPSRC, SDKPATH, SRCDIR
320
321 if args is None:
322 args = sys.argv[1:]
323
324 try:
325 options, args = getopt.getopt(args, '?hb',
326 [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir='])
Benjamin Peterson623918e2008-12-20 22:50:25 +0000327 except getopt.error, msg:
328 print msg
Thomas Wouters477c8d52006-05-27 19:21:47 +0000329 sys.exit(1)
330
331 if args:
Benjamin Peterson623918e2008-12-20 22:50:25 +0000332 print "Additional arguments"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000333 sys.exit(1)
334
335 for k, v in options:
336 if k in ('-h', '-?'):
Benjamin Peterson623918e2008-12-20 22:50:25 +0000337 print USAGE
Thomas Wouters477c8d52006-05-27 19:21:47 +0000338 sys.exit(0)
339
340 elif k in ('-d', '--build-dir'):
341 WORKDIR=v
342
343 elif k in ('--third-party',):
344 DEPSRC=v
345
346 elif k in ('--sdk-path',):
347 SDKPATH=v
348
349 elif k in ('--src-dir',):
350 SRCDIR=v
351
352 else:
Benjamin Peterson623918e2008-12-20 22:50:25 +0000353 raise NotImplementedError, k
Thomas Wouters477c8d52006-05-27 19:21:47 +0000354
355 SRCDIR=os.path.abspath(SRCDIR)
356 WORKDIR=os.path.abspath(WORKDIR)
357 SDKPATH=os.path.abspath(SDKPATH)
358 DEPSRC=os.path.abspath(DEPSRC)
359
Benjamin Peterson623918e2008-12-20 22:50:25 +0000360 print "Settings:"
361 print " * Source directory:", SRCDIR
362 print " * Build directory: ", WORKDIR
363 print " * SDK location: ", SDKPATH
364 print " * third-party source:", DEPSRC
365 print ""
Thomas Wouters477c8d52006-05-27 19:21:47 +0000366
367
368
369
370def extractArchive(builddir, archiveName):
371 """
372 Extract a source archive into 'builddir'. Returns the path of the
373 extracted archive.
374
375 XXX: This function assumes that archives contain a toplevel directory
376 that is has the same name as the basename of the archive. This is
377 save enough for anything we use.
378 """
379 curdir = os.getcwd()
380 try:
381 os.chdir(builddir)
382 if archiveName.endswith('.tar.gz'):
383 retval = os.path.basename(archiveName[:-7])
384 if os.path.exists(retval):
385 shutil.rmtree(retval)
386 fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')
387
388 elif archiveName.endswith('.tar.bz2'):
389 retval = os.path.basename(archiveName[:-8])
390 if os.path.exists(retval):
391 shutil.rmtree(retval)
392 fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')
393
394 elif archiveName.endswith('.tar'):
395 retval = os.path.basename(archiveName[:-4])
396 if os.path.exists(retval):
397 shutil.rmtree(retval)
398 fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')
399
400 elif archiveName.endswith('.zip'):
401 retval = os.path.basename(archiveName[:-4])
402 if os.path.exists(retval):
403 shutil.rmtree(retval)
404 fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')
405
406 data = fp.read()
407 xit = fp.close()
408 if xit is not None:
409 sys.stdout.write(data)
Benjamin Peterson623918e2008-12-20 22:50:25 +0000410 raise RuntimeError, "Cannot extract %s"%(archiveName,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000411
412 return os.path.join(builddir, retval)
413
414 finally:
415 os.chdir(curdir)
416
417KNOWNSIZES = {
418 "http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz": 7952742,
419 "http://downloads.sleepycat.com/db-4.4.20.tar.gz": 2030276,
420}
421
422def downloadURL(url, fname):
423 """
424 Download the contents of the url into the file.
425 """
426 try:
427 size = os.path.getsize(fname)
428 except OSError:
429 pass
430 else:
431 if KNOWNSIZES.get(url) == size:
Benjamin Peterson623918e2008-12-20 22:50:25 +0000432 print "Using existing file for", url
Thomas Wouters477c8d52006-05-27 19:21:47 +0000433 return
Benjamin Peterson623918e2008-12-20 22:50:25 +0000434 fpIn = urllib2.urlopen(url)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000435 fpOut = open(fname, 'wb')
436 block = fpIn.read(10240)
437 try:
438 while block:
439 fpOut.write(block)
440 block = fpIn.read(10240)
441 fpIn.close()
442 fpOut.close()
443 except:
444 try:
445 os.unlink(fname)
446 except:
447 pass
448
449def buildRecipe(recipe, basedir, archList):
450 """
451 Build software using a recipe. This function does the
452 'configure;make;make install' dance for C software, with a possibility
453 to customize this process, basically a poor-mans DarwinPorts.
454 """
455 curdir = os.getcwd()
456
457 name = recipe['name']
458 url = recipe['url']
459 configure = recipe.get('configure', './configure')
460 install = recipe.get('install', 'make && make install DESTDIR=%s'%(
461 shellQuote(basedir)))
462
463 archiveName = os.path.split(url)[-1]
464 sourceArchive = os.path.join(DEPSRC, archiveName)
465
466 if not os.path.exists(DEPSRC):
467 os.mkdir(DEPSRC)
468
469
470 if os.path.exists(sourceArchive):
Benjamin Peterson623918e2008-12-20 22:50:25 +0000471 print "Using local copy of %s"%(name,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000472
473 else:
Benjamin Peterson623918e2008-12-20 22:50:25 +0000474 print "Downloading %s"%(name,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000475 downloadURL(url, sourceArchive)
Benjamin Peterson623918e2008-12-20 22:50:25 +0000476 print "Archive for %s stored as %s"%(name, sourceArchive)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000477
Benjamin Peterson623918e2008-12-20 22:50:25 +0000478 print "Extracting archive for %s"%(name,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000479 buildDir=os.path.join(WORKDIR, '_bld')
480 if not os.path.exists(buildDir):
481 os.mkdir(buildDir)
482
483 workDir = extractArchive(buildDir, sourceArchive)
484 os.chdir(workDir)
485 if 'buildDir' in recipe:
486 os.chdir(recipe['buildDir'])
487
488
489 for fn in recipe.get('patches', ()):
490 if fn.startswith('http://'):
491 # Download the patch before applying it.
492 path = os.path.join(DEPSRC, os.path.basename(fn))
493 downloadURL(fn, path)
494 fn = path
495
496 fn = os.path.join(curdir, fn)
497 runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
498 shellQuote(fn),))
499
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000500 if configure is not None:
501 configure_args = [
502 "--prefix=/usr/local",
503 "--enable-static",
504 "--disable-shared",
505 #"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
506 ]
Thomas Wouters477c8d52006-05-27 19:21:47 +0000507
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000508 if 'configure_pre' in recipe:
509 args = list(recipe['configure_pre'])
510 if '--disable-static' in args:
511 configure_args.remove('--enable-static')
512 if '--enable-shared' in args:
513 configure_args.remove('--disable-shared')
514 configure_args.extend(args)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000515
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000516 if recipe.get('useLDFlags', 1):
517 configure_args.extend([
518 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
519 ' -arch '.join(archList),
520 shellQuote(SDKPATH)[1:-1],
521 shellQuote(basedir)[1:-1],),
522 "LDFLAGS=-syslibroot,%s -L%s/usr/local/lib -arch %s"%(
Thomas Wouters477c8d52006-05-27 19:21:47 +0000523 shellQuote(SDKPATH)[1:-1],
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000524 shellQuote(basedir)[1:-1],
525 ' -arch '.join(archList)),
526 ])
527 else:
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 ])
Thomas Wouters477c8d52006-05-27 19:21:47 +0000534
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000535 if 'configure_post' in recipe:
536 configure_args = configure_args = list(recipe['configure_post'])
Thomas Wouters477c8d52006-05-27 19:21:47 +0000537
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000538 configure_args.insert(0, configure)
539 configure_args = [ shellQuote(a) for a in configure_args ]
Thomas Wouters477c8d52006-05-27 19:21:47 +0000540
Benjamin Peterson623918e2008-12-20 22:50:25 +0000541 print "Running configure for %s"%(name,)
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000542 runCommand(' '.join(configure_args) + ' 2>&1')
Thomas Wouters477c8d52006-05-27 19:21:47 +0000543
Benjamin Peterson623918e2008-12-20 22:50:25 +0000544 print "Running install for %s"%(name,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000545 runCommand('{ ' + install + ' ;} 2>&1')
546
Benjamin Peterson623918e2008-12-20 22:50:25 +0000547 print "Done %s"%(name,)
548 print ""
Thomas Wouters477c8d52006-05-27 19:21:47 +0000549
550 os.chdir(curdir)
551
552def buildLibraries():
553 """
554 Build our dependencies into $WORKDIR/libraries/usr/local
555 """
Benjamin Peterson623918e2008-12-20 22:50:25 +0000556 print ""
557 print "Building required libraries"
558 print ""
Thomas Wouters477c8d52006-05-27 19:21:47 +0000559 universal = os.path.join(WORKDIR, 'libraries')
560 os.mkdir(universal)
561 os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
562 os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))
563
564 for recipe in LIBRARY_RECIPES:
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000565 buildRecipe(recipe, universal, ARCHLIST)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000566
567
568
569def buildPythonDocs():
570 # This stores the documentation as Resources/English.lproj/Docuentation
571 # inside the framwork. pydoc and IDLE will pick it up there.
Benjamin Peterson623918e2008-12-20 22:50:25 +0000572 print "Install python documentation"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000573 rootDir = os.path.join(WORKDIR, '_root')
574 version = getVersion()
575 docdir = os.path.join(rootDir, 'pydocs')
576
Benjamin Petersonf10a79a2008-10-11 00:49:57 +0000577 novername = 'python-docs-html.tar.bz2'
Thomas Wouters477c8d52006-05-27 19:21:47 +0000578 name = 'html-%s.tar.bz2'%(getFullVersion(),)
579 sourceArchive = os.path.join(DEPSRC, name)
580 if os.path.exists(sourceArchive):
Benjamin Peterson623918e2008-12-20 22:50:25 +0000581 print "Using local copy of %s"%(name,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000582
583 else:
Benjamin Petersonf10a79a2008-10-11 00:49:57 +0000584 print "Downloading %s"%(novername,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000585 downloadURL('http://www.python.org/ftp/python/doc/%s/%s'%(
Benjamin Petersonf10a79a2008-10-11 00:49:57 +0000586 getFullVersion(), novername), sourceArchive)
Benjamin Peterson623918e2008-12-20 22:50:25 +0000587 print "Archive for %s stored as %s"%(name, sourceArchive)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000588
589 extractArchive(os.path.dirname(docdir), sourceArchive)
Benjamin Petersonf10a79a2008-10-11 00:49:57 +0000590
Thomas Wouters477c8d52006-05-27 19:21:47 +0000591 os.rename(
592 os.path.join(
Benjamin Petersonf10a79a2008-10-11 00:49:57 +0000593 os.path.dirname(docdir), 'python-docs-html'),
Thomas Wouters477c8d52006-05-27 19:21:47 +0000594 docdir)
595
596
597def buildPython():
Benjamin Peterson623918e2008-12-20 22:50:25 +0000598 print "Building a universal python"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000599
600 buildDir = os.path.join(WORKDIR, '_bld', 'python')
601 rootDir = os.path.join(WORKDIR, '_root')
602
603 if os.path.exists(buildDir):
604 shutil.rmtree(buildDir)
605 if os.path.exists(rootDir):
606 shutil.rmtree(rootDir)
607 os.mkdir(buildDir)
608 os.mkdir(rootDir)
609 os.mkdir(os.path.join(rootDir, 'empty-dir'))
610 curdir = os.getcwd()
611 os.chdir(buildDir)
612
613 # Not sure if this is still needed, the original build script
614 # claims that parts of the install assume python.exe exists.
615 os.symlink('python', os.path.join(buildDir, 'python.exe'))
616
617 # Extract the version from the configure file, needed to calculate
618 # several paths.
619 version = getVersion()
620
Benjamin Peterson623918e2008-12-20 22:50:25 +0000621 print "Running configure..."
Thomas Wouters477c8d52006-05-27 19:21:47 +0000622 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"%(
623 shellQuote(os.path.join(SRCDIR, 'configure')),
624 shellQuote(SDKPATH), shellQuote(WORKDIR)[1:-1],
625 shellQuote(WORKDIR)[1:-1]))
626
Benjamin Peterson623918e2008-12-20 22:50:25 +0000627 print "Running make"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000628 runCommand("make")
629
Benjamin Peterson623918e2008-12-20 22:50:25 +0000630 print "Running make frameworkinstall"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000631 runCommand("make frameworkinstall DESTDIR=%s"%(
632 shellQuote(rootDir)))
633
Benjamin Peterson623918e2008-12-20 22:50:25 +0000634 print "Running make frameworkinstallextras"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000635 runCommand("make frameworkinstallextras DESTDIR=%s"%(
636 shellQuote(rootDir)))
637
Benjamin Peterson623918e2008-12-20 22:50:25 +0000638 print "Copying required shared libraries"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000639 if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
640 runCommand("mv %s/* %s"%(
641 shellQuote(os.path.join(
642 WORKDIR, 'libraries', 'Library', 'Frameworks',
643 'Python.framework', 'Versions', getVersion(),
644 'lib')),
645 shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
646 'Python.framework', 'Versions', getVersion(),
647 'lib'))))
648
Benjamin Peterson623918e2008-12-20 22:50:25 +0000649 print "Fix file modes"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000650 frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
Thomas Wouters89f507f2006-12-13 04:49:30 +0000651 gid = grp.getgrnam('admin').gr_gid
652
Thomas Wouters477c8d52006-05-27 19:21:47 +0000653 for dirpath, dirnames, filenames in os.walk(frmDir):
654 for dn in dirnames:
655 os.chmod(os.path.join(dirpath, dn), 0775)
Thomas Wouters89f507f2006-12-13 04:49:30 +0000656 os.chown(os.path.join(dirpath, dn), -1, gid)
657
Thomas Wouters477c8d52006-05-27 19:21:47 +0000658
659 for fn in filenames:
660 if os.path.islink(fn):
661 continue
662
663 # "chmod g+w $fn"
664 p = os.path.join(dirpath, fn)
665 st = os.stat(p)
Thomas Wouters89f507f2006-12-13 04:49:30 +0000666 os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
667 os.chown(p, -1, gid)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000668
669 # We added some directories to the search path during the configure
670 # phase. Remove those because those directories won't be there on
671 # the end-users system.
672 path =os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework',
673 'Versions', version, 'lib', 'python%s'%(version,),
674 'config', 'Makefile')
675 fp = open(path, 'r')
676 data = fp.read()
677 fp.close()
678
679 data = data.replace('-L%s/libraries/usr/local/lib'%(WORKDIR,), '')
680 data = data.replace('-I%s/libraries/usr/local/include'%(WORKDIR,), '')
681 fp = open(path, 'w')
682 fp.write(data)
683 fp.close()
684
685 # Add symlinks in /usr/local/bin, using relative links
686 usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
687 to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
688 'Python.framework', 'Versions', version, 'bin')
689 if os.path.exists(usr_local_bin):
690 shutil.rmtree(usr_local_bin)
691 os.makedirs(usr_local_bin)
692 for fn in os.listdir(
693 os.path.join(frmDir, 'Versions', version, 'bin')):
694 os.symlink(os.path.join(to_framework, fn),
695 os.path.join(usr_local_bin, fn))
696
697 os.chdir(curdir)
698
699
700
701def patchFile(inPath, outPath):
702 data = fileContents(inPath)
703 data = data.replace('$FULL_VERSION', getFullVersion())
704 data = data.replace('$VERSION', getVersion())
705 data = data.replace('$MACOSX_DEPLOYMENT_TARGET', '10.3 or later')
706 data = data.replace('$ARCHITECTURES', "i386, ppc")
707 data = data.replace('$INSTALL_SIZE', installSize())
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000708
709 # This one is not handy as a template variable
710 data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
Thomas Wouters477c8d52006-05-27 19:21:47 +0000711 fp = open(outPath, 'wb')
712 fp.write(data)
713 fp.close()
714
715def patchScript(inPath, outPath):
716 data = fileContents(inPath)
717 data = data.replace('@PYVER@', getVersion())
718 fp = open(outPath, 'wb')
719 fp.write(data)
720 fp.close()
721 os.chmod(outPath, 0755)
722
723
724
725def packageFromRecipe(targetDir, recipe):
726 curdir = os.getcwd()
727 try:
Thomas Wouters89f507f2006-12-13 04:49:30 +0000728 # The major version (such as 2.5) is included in the package name
729 # because having two version of python installed at the same time is
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000730 # common.
731 pkgname = '%s-%s'%(recipe['name'], getVersion())
Thomas Wouters477c8d52006-05-27 19:21:47 +0000732 srcdir = recipe.get('source')
733 pkgroot = recipe.get('topdir', srcdir)
734 postflight = recipe.get('postflight')
735 readme = textwrap.dedent(recipe['readme'])
736 isRequired = recipe.get('required', True)
737
Benjamin Peterson623918e2008-12-20 22:50:25 +0000738 print "- building package %s"%(pkgname,)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000739
740 # Substitute some variables
741 textvars = dict(
742 VER=getVersion(),
743 FULLVER=getFullVersion(),
744 )
745 readme = readme % textvars
746
747 if pkgroot is not None:
748 pkgroot = pkgroot % textvars
749 else:
750 pkgroot = '/'
751
752 if srcdir is not None:
753 srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
754 srcdir = srcdir % textvars
755
756 if postflight is not None:
757 postflight = os.path.abspath(postflight)
758
759 packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
760 os.makedirs(packageContents)
761
762 if srcdir is not None:
763 os.chdir(srcdir)
764 runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
765 runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
766 runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
767
768 fn = os.path.join(packageContents, 'PkgInfo')
769 fp = open(fn, 'w')
770 fp.write('pmkrpkg1')
771 fp.close()
772
773 rsrcDir = os.path.join(packageContents, "Resources")
774 os.mkdir(rsrcDir)
775 fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
776 fp.write(readme)
777 fp.close()
778
779 if postflight is not None:
780 patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
781
782 vers = getFullVersion()
783 major, minor = map(int, getVersion().split('.', 2))
784 pl = Plist(
785 CFBundleGetInfoString="MacPython.%s %s"%(pkgname, vers,),
786 CFBundleIdentifier='org.python.MacPython.%s'%(pkgname,),
787 CFBundleName='MacPython.%s'%(pkgname,),
788 CFBundleShortVersionString=vers,
789 IFMajorVersion=major,
790 IFMinorVersion=minor,
791 IFPkgFormatVersion=0.10000000149011612,
792 IFPkgFlagAllowBackRev=False,
793 IFPkgFlagAuthorizationAction="RootAuthorization",
794 IFPkgFlagDefaultLocation=pkgroot,
795 IFPkgFlagFollowLinks=True,
796 IFPkgFlagInstallFat=True,
797 IFPkgFlagIsRequired=isRequired,
798 IFPkgFlagOverwritePermissions=False,
799 IFPkgFlagRelocatable=False,
800 IFPkgFlagRestartAction="NoRestart",
801 IFPkgFlagRootVolumeOnly=True,
802 IFPkgFlagUpdateInstalledLangauges=False,
803 )
804 writePlist(pl, os.path.join(packageContents, 'Info.plist'))
805
806 pl = Plist(
807 IFPkgDescriptionDescription=readme,
808 IFPkgDescriptionTitle=recipe.get('long_name', "MacPython.%s"%(pkgname,)),
809 IFPkgDescriptionVersion=vers,
810 )
811 writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
812
813 finally:
814 os.chdir(curdir)
815
816
817def makeMpkgPlist(path):
818
819 vers = getFullVersion()
820 major, minor = map(int, getVersion().split('.', 2))
821
822 pl = Plist(
823 CFBundleGetInfoString="MacPython %s"%(vers,),
824 CFBundleIdentifier='org.python.MacPython',
825 CFBundleName='MacPython',
826 CFBundleShortVersionString=vers,
827 IFMajorVersion=major,
828 IFMinorVersion=minor,
829 IFPkgFlagComponentDirectory="Contents/Packages",
830 IFPkgFlagPackageList=[
831 dict(
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000832 IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
Thomas Wouters477c8d52006-05-27 19:21:47 +0000833 IFPkgFlagPackageSelection='selected'
834 )
835 for item in PKG_RECIPES
836 ],
837 IFPkgFormatVersion=0.10000000149011612,
838 IFPkgFlagBackgroundScaling="proportional",
839 IFPkgFlagBackgroundAlignment="left",
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000840 IFPkgFlagAuthorizationAction="RootAuthorization",
Thomas Wouters477c8d52006-05-27 19:21:47 +0000841 )
842
843 writePlist(pl, path)
844
845
846def buildInstaller():
847
848 # Zap all compiled files
849 for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
850 for fn in filenames:
851 if fn.endswith('.pyc') or fn.endswith('.pyo'):
852 os.unlink(os.path.join(dirpath, fn))
853
854 outdir = os.path.join(WORKDIR, 'installer')
855 if os.path.exists(outdir):
856 shutil.rmtree(outdir)
857 os.mkdir(outdir)
858
859 pkgroot = os.path.join(outdir, 'MacPython.mpkg', 'Contents')
860 pkgcontents = os.path.join(pkgroot, 'Packages')
861 os.makedirs(pkgcontents)
862 for recipe in PKG_RECIPES:
863 packageFromRecipe(pkgcontents, recipe)
864
865 rsrcDir = os.path.join(pkgroot, 'Resources')
866
867 fn = os.path.join(pkgroot, 'PkgInfo')
868 fp = open(fn, 'w')
869 fp.write('pmkrpkg1')
870 fp.close()
871
872 os.mkdir(rsrcDir)
873
874 makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
875 pl = Plist(
876 IFPkgDescriptionTitle="Universal MacPython",
877 IFPkgDescriptionVersion=getVersion(),
878 )
879
880 writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
881 for fn in os.listdir('resources'):
882 if fn == '.svn': continue
883 if fn.endswith('.jpg'):
884 shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
885 else:
886 patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
887
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000888 shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
Thomas Wouters477c8d52006-05-27 19:21:47 +0000889
890
891def installSize(clear=False, _saved=[]):
892 if clear:
893 del _saved[:]
894 if not _saved:
895 data = captureCommand("du -ks %s"%(
896 shellQuote(os.path.join(WORKDIR, '_root'))))
897 _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
898 return _saved[0]
899
900
901def buildDMG():
902 """
Thomas Wouters89f507f2006-12-13 04:49:30 +0000903 Create DMG containing the rootDir.
Thomas Wouters477c8d52006-05-27 19:21:47 +0000904 """
905 outdir = os.path.join(WORKDIR, 'diskimage')
906 if os.path.exists(outdir):
907 shutil.rmtree(outdir)
908
909 imagepath = os.path.join(outdir,
910 'python-%s-macosx'%(getFullVersion(),))
911 if INCLUDE_TIMESTAMP:
912 imagepath = imagepath + '%04d-%02d-%02d'%(time.localtime()[:3])
913 imagepath = imagepath + '.dmg'
914
915 os.mkdir(outdir)
Thomas Wouters89f507f2006-12-13 04:49:30 +0000916 runCommand("hdiutil create -volname 'Universal MacPython %s' -srcfolder %s %s"%(
Thomas Wouters477c8d52006-05-27 19:21:47 +0000917 getFullVersion(),
918 shellQuote(os.path.join(WORKDIR, 'installer')),
919 shellQuote(imagepath)))
920
921 return imagepath
922
923
924def setIcon(filePath, icnsPath):
925 """
926 Set the custom icon for the specified file or directory.
927
928 For a directory the icon data is written in a file named 'Icon\r' inside
929 the directory. For both files and directories write the icon as an 'icns'
930 resource. Furthermore set kHasCustomIcon in the finder flags for filePath.
931 """
932 ref, isDirectory = Carbon.File.FSPathMakeRef(icnsPath)
933 icon = Carbon.Icn.ReadIconFile(ref)
934 del ref
935
936 #
937 # Open the resource fork of the target, to add the icon later on.
938 # For directories we use the file 'Icon\r' inside the directory.
939 #
940
941 ref, isDirectory = Carbon.File.FSPathMakeRef(filePath)
942
943 if isDirectory:
Thomas Wouters89f507f2006-12-13 04:49:30 +0000944 # There is a problem with getting this into the pax(1) archive,
945 # just ignore directory icons for now.
946 return
947
Thomas Wouters477c8d52006-05-27 19:21:47 +0000948 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",
Benjamin Petersonf10a79a2008-10-11 00:49:57 +00001022 "Python %s"%(getVersion(),), "Update Shell Profile.command")
Thomas Wouters0e3f5912006-08-11 14:57:12 +00001023 patchFile("scripts/postflight.patch-profile", fn)
Thomas Wouters477c8d52006-05-27 19:21:47 +00001024 os.chmod(fn, 0755)
1025
Benjamin Petersonf10a79a2008-10-11 00:49:57 +00001026 folder = os.path.join(WORKDIR, "_root", "Applications", "Python %s"%(
Thomas Wouters477c8d52006-05-27 19:21:47 +00001027 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.
Thomas Wouters0e3f5912006-08-11 14:57:12 +00001038 shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
Thomas Wouters477c8d52006-05-27 19:21:47 +00001039
1040 fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
Benjamin Peterson623918e2008-12-20 22:50:25 +00001041 print >> fp, "# BUILD INFO"
1042 print >> fp, "# Date:", time.ctime()
1043 print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
Thomas Wouters477c8d52006-05-27 19:21:47 +00001044 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()