blob: 8051256ed5a3f5337fb4094f66b5b49a53ae80d7 [file] [log] [blame]
Ronald Oussoren648a4fe2009-03-30 17:15:29 +00001#!/usr/bin/python
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +00002"""
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
Ronald Oussoren158ad592006-11-07 16:00:34 +000015INCLUDE_TIMESTAMP = 1
16VERBOSE = 1
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000017
18from plistlib import Plist
19
20import MacOS
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000021
22try:
23 from plistlib import writePlist
24except ImportError:
25 # We're run using python2.3
26 def writePlist(plist, path):
27 plist.write(path)
28
29def shellQuote(value):
30 """
Ronald Oussorenaa560962006-11-07 15:53:38 +000031 Return the string value in a form that can safely be inserted into
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000032 a shell command.
33 """
34 return "'%s'"%(value.replace("'", "'\"'\"'"))
35
36def grepValue(fn, variable):
37 variable = variable + '='
38 for ln in open(fn, 'r'):
39 if ln.startswith(variable):
40 value = ln[len(variable):].strip()
41 return value[1:-1]
42
43def getVersion():
44 return grepValue(os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
45
46def getFullVersion():
47 fn = os.path.join(SRCDIR, 'Include', 'patchlevel.h')
48 for ln in open(fn):
49 if 'PY_VERSION' in ln:
50 return ln.split()[-1][1:-1]
51
52 raise RuntimeError, "Cannot find full version??"
53
Ronald Oussorenaa560962006-11-07 15:53:38 +000054# The directory we'll use to create the build (will be erased and recreated)
Ronald Oussoren158ad592006-11-07 16:00:34 +000055WORKDIR = "/tmp/_py"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000056
Ronald Oussorenaa560962006-11-07 15:53:38 +000057# The directory we'll use to store third-party sources. Set this to something
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000058# else if you don't want to re-fetch required libraries every time.
Ronald Oussoren158ad592006-11-07 16:00:34 +000059DEPSRC = os.path.join(WORKDIR, 'third-party')
60DEPSRC = os.path.expanduser('~/Universal/other-sources')
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000061
62# Location of the preferred SDK
Ronald Oussoren158ad592006-11-07 16:00:34 +000063SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
64#SDKPATH = "/"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000065
Ronald Oussoren158ad592006-11-07 16:00:34 +000066ARCHLIST = ('i386', 'ppc',)
Ronald Oussoren9b8b6192006-06-27 12:53:52 +000067
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000068# Source directory (asume we're in Mac/BuildScript)
Ronald Oussoren158ad592006-11-07 16:00:34 +000069SRCDIR = os.path.dirname(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000070 os.path.dirname(
71 os.path.dirname(
72 os.path.abspath(__file__
73 ))))
74
Ronald Oussoren158ad592006-11-07 16:00:34 +000075USAGE = textwrap.dedent("""\
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000076 Usage: build_python [options]
77
78 Options:
79 -? or -h: Show this message
80 -b DIR
81 --build-dir=DIR: Create build here (default: %(WORKDIR)r)
82 --third-party=DIR: Store third-party sources here (default: %(DEPSRC)r)
83 --sdk-path=DIR: Location of the SDK (default: %(SDKPATH)r)
84 --src-dir=DIR: Location of the Python sources (default: %(SRCDIR)r)
85""")% globals()
86
87
88# Instructions for building libraries that are necessary for building a
89# batteries included python.
Ronald Oussoren158ad592006-11-07 16:00:34 +000090LIBRARY_RECIPES = [
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +000091 dict(
Ronald Oussoren648a4fe2009-03-30 17:15:29 +000092 name="Bzip2 1.0.4",
93 url="http://www.bzip.org/1.0.4/bzip2-1.0.4.tar.gz",
94 checksum='fc310b254f6ba5fbb5da018f04533688',
Ronald Oussoren9b8b6192006-06-27 12:53:52 +000095 configure=None,
96 install='make install PREFIX=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
97 shellQuote(os.path.join(WORKDIR, 'libraries')),
98 ' -arch '.join(ARCHLIST),
99 SDKPATH,
100 ),
101 ),
102 dict(
103 name="ZLib 1.2.3",
104 url="http://www.gzip.org/zlib/zlib-1.2.3.tar.gz",
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000105 checksum='debc62758716a169df9f62e6ab2bc634',
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000106 configure=None,
107 install='make install prefix=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
108 shellQuote(os.path.join(WORKDIR, 'libraries')),
109 ' -arch '.join(ARCHLIST),
110 SDKPATH,
111 ),
112 ),
113 dict(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000114 # Note that GNU readline is GPL'd software
115 name="GNU Readline 5.1.4",
116 url="http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz" ,
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000117 checksum='7ee5a692db88b30ca48927a13fd60e46',
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000118 patchlevel='0',
119 patches=[
120 # The readline maintainers don't do actual micro releases, but
121 # just ship a set of patches.
122 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-001',
123 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-002',
124 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-003',
125 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-004',
126 ]
127 ),
128
129 dict(
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000130 name="SQLite 3.6.11",
131 url="http://www.sqlite.org/sqlite-3.6.11.tar.gz",
132 checksum='7ebb099696ab76cc6ff65dd496d17858',
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000133 configure_pre=[
134 '--enable-threadsafe',
135 '--enable-tempstore',
136 '--enable-shared=no',
137 '--enable-static=yes',
138 '--disable-tcl',
139 ]
140 ),
141
142 dict(
143 name="NCurses 5.5",
144 url="http://ftp.gnu.org/pub/gnu/ncurses/ncurses-5.5.tar.gz",
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000145 checksum='e73c1ac10b4bfc46db43b2ddfd6244ef',
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000146 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(
Benjamin Petersonc3104762008-10-03 11:52:06 +0000172 name="Sleepycat DB 4.7.25",
173 url="http://download.oracle.com/berkeley-db/db-4.7.25.tar.gz",
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000174 checksum='ec2b87e833779681a0c3a814aa71359e',
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000175 buildDir="build_unix",
176 configure="../dist/configure",
177 configure_pre=[
178 '--includedir=/usr/local/include/db4',
179 ]
180 ),
181]
182
183
184# Instructions for building packages inside the .mpkg.
Ronald Oussoren158ad592006-11-07 16:00:34 +0000185PKG_RECIPES = [
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000186 dict(
187 name="PythonFramework",
188 long_name="Python Framework",
189 source="/Library/Frameworks/Python.framework",
190 readme="""\
191 This package installs Python.framework, that is the python
192 interpreter and the standard library. This also includes Python
193 wrappers for lots of Mac OS X API's.
194 """,
195 postflight="scripts/postflight.framework",
196 ),
197 dict(
198 name="PythonApplications",
199 long_name="GUI Applications",
Ronald Oussoren580c7fe2008-05-02 19:45:11 +0000200 source="/Applications/Python %(VER)s",
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000201 readme="""\
Ronald Oussorenaa560962006-11-07 15:53:38 +0000202 This package installs IDLE (an interactive Python IDE),
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000203 Python Launcher and Build Applet (create application bundles
204 from python scripts).
205
206 It also installs a number of examples and demos.
207 """,
208 required=False,
209 ),
210 dict(
211 name="PythonUnixTools",
212 long_name="UNIX command-line tools",
213 source="/usr/local/bin",
214 readme="""\
215 This package installs the unix tools in /usr/local/bin for
216 compatibility with older releases of MacPython. This package
217 is not necessary to use MacPython.
218 """,
219 required=False,
220 ),
221 dict(
222 name="PythonDocumentation",
223 long_name="Python Documentation",
224 topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
225 source="/pydocs",
226 readme="""\
227 This package installs the python documentation at a location
228 that is useable for pydoc and IDLE. If you have installed Xcode
229 it will also install a link to the documentation in
230 /Developer/Documentation/Python
231 """,
232 postflight="scripts/postflight.documentation",
233 required=False,
234 ),
235 dict(
236 name="PythonProfileChanges",
237 long_name="Shell profile updater",
238 readme="""\
239 This packages updates your shell profile to make sure that
240 the MacPython tools are found by your shell in preference of
241 the system provided Python tools.
242
243 If you don't install this package you'll have to add
244 "/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
245 to your PATH by hand.
246 """,
247 postflight="scripts/postflight.patch-profile",
248 topdir="/Library/Frameworks/Python.framework",
249 source="/empty-dir",
250 required=False,
251 ),
252 dict(
253 name="PythonSystemFixes",
254 long_name="Fix system Python",
255 readme="""\
Tim Petersae6a5a72006-06-07 20:40:06 +0000256 This package updates the system python installation on
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000257 Mac OS X 10.3 to ensure that you can build new python extensions
Ronald Oussorenaa560962006-11-07 15:53:38 +0000258 using that copy of python after installing this version.
Ronald Oussorenc5555542006-06-11 20:24:45 +0000259 """,
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000260 postflight="../Tools/fixapplepython23.py",
261 topdir="/Library/Frameworks/Python.framework",
262 source="/empty-dir",
263 required=False,
264 )
265]
266
267def fatal(msg):
268 """
269 A fatal error, bail out.
270 """
271 sys.stderr.write('FATAL: ')
272 sys.stderr.write(msg)
273 sys.stderr.write('\n')
274 sys.exit(1)
275
276def fileContents(fn):
277 """
278 Return the contents of the named file
279 """
280 return open(fn, 'rb').read()
281
282def runCommand(commandline):
283 """
284 Run a command and raise RuntimeError if it fails. Output is surpressed
285 unless the command fails.
286 """
287 fd = os.popen(commandline, 'r')
288 data = fd.read()
289 xit = fd.close()
Benjamin Peterson5b63acd2008-03-29 15:24:25 +0000290 if xit is not None:
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000291 sys.stdout.write(data)
292 raise RuntimeError, "command failed: %s"%(commandline,)
293
294 if VERBOSE:
295 sys.stdout.write(data); sys.stdout.flush()
296
297def captureCommand(commandline):
298 fd = os.popen(commandline, 'r')
299 data = fd.read()
300 xit = fd.close()
Benjamin Peterson5b63acd2008-03-29 15:24:25 +0000301 if xit is not None:
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000302 sys.stdout.write(data)
303 raise RuntimeError, "command failed: %s"%(commandline,)
304
305 return data
306
307def checkEnvironment():
308 """
309 Check that we're running on a supported system.
310 """
311
312 if platform.system() != 'Darwin':
313 fatal("This script should be run on a Mac OS X 10.4 system")
314
315 if platform.release() <= '8.':
316 fatal("This script should be run on a Mac OS X 10.4 system")
317
318 if not os.path.exists(SDKPATH):
319 fatal("Please install the latest version of Xcode and the %s SDK"%(
320 os.path.basename(SDKPATH[:-4])))
321
322
323
Ronald Oussoren158ad592006-11-07 16:00:34 +0000324def parseOptions(args=None):
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000325 """
326 Parse arguments and update global settings.
327 """
328 global WORKDIR, DEPSRC, SDKPATH, SRCDIR
329
330 if args is None:
331 args = sys.argv[1:]
332
333 try:
334 options, args = getopt.getopt(args, '?hb',
335 [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir='])
336 except getopt.error, msg:
337 print msg
338 sys.exit(1)
339
340 if args:
341 print "Additional arguments"
342 sys.exit(1)
343
344 for k, v in options:
345 if k in ('-h', '-?'):
346 print USAGE
347 sys.exit(0)
348
349 elif k in ('-d', '--build-dir'):
350 WORKDIR=v
351
352 elif k in ('--third-party',):
353 DEPSRC=v
354
355 elif k in ('--sdk-path',):
356 SDKPATH=v
357
358 elif k in ('--src-dir',):
359 SRCDIR=v
360
361 else:
362 raise NotImplementedError, k
363
364 SRCDIR=os.path.abspath(SRCDIR)
365 WORKDIR=os.path.abspath(WORKDIR)
366 SDKPATH=os.path.abspath(SDKPATH)
367 DEPSRC=os.path.abspath(DEPSRC)
368
369 print "Settings:"
370 print " * Source directory:", SRCDIR
371 print " * Build directory: ", WORKDIR
372 print " * SDK location: ", SDKPATH
373 print " * third-party source:", DEPSRC
374 print ""
375
376
377
378
379def extractArchive(builddir, archiveName):
380 """
381 Extract a source archive into 'builddir'. Returns the path of the
382 extracted archive.
383
384 XXX: This function assumes that archives contain a toplevel directory
385 that is has the same name as the basename of the archive. This is
386 save enough for anything we use.
387 """
388 curdir = os.getcwd()
389 try:
390 os.chdir(builddir)
391 if archiveName.endswith('.tar.gz'):
392 retval = os.path.basename(archiveName[:-7])
393 if os.path.exists(retval):
394 shutil.rmtree(retval)
395 fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')
396
397 elif archiveName.endswith('.tar.bz2'):
398 retval = os.path.basename(archiveName[:-8])
399 if os.path.exists(retval):
400 shutil.rmtree(retval)
401 fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')
402
403 elif archiveName.endswith('.tar'):
404 retval = os.path.basename(archiveName[:-4])
405 if os.path.exists(retval):
406 shutil.rmtree(retval)
407 fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')
408
409 elif archiveName.endswith('.zip'):
410 retval = os.path.basename(archiveName[:-4])
411 if os.path.exists(retval):
412 shutil.rmtree(retval)
413 fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')
414
415 data = fp.read()
416 xit = fp.close()
417 if xit is not None:
418 sys.stdout.write(data)
419 raise RuntimeError, "Cannot extract %s"%(archiveName,)
420
421 return os.path.join(builddir, retval)
422
423 finally:
424 os.chdir(curdir)
425
426KNOWNSIZES = {
427 "http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz": 7952742,
428 "http://downloads.sleepycat.com/db-4.4.20.tar.gz": 2030276,
429}
430
431def downloadURL(url, fname):
432 """
433 Download the contents of the url into the file.
434 """
435 try:
436 size = os.path.getsize(fname)
437 except OSError:
438 pass
439 else:
440 if KNOWNSIZES.get(url) == size:
441 print "Using existing file for", url
442 return
443 fpIn = urllib2.urlopen(url)
444 fpOut = open(fname, 'wb')
445 block = fpIn.read(10240)
446 try:
447 while block:
448 fpOut.write(block)
449 block = fpIn.read(10240)
450 fpIn.close()
451 fpOut.close()
452 except:
453 try:
454 os.unlink(fname)
455 except:
456 pass
457
458def buildRecipe(recipe, basedir, archList):
459 """
460 Build software using a recipe. This function does the
461 'configure;make;make install' dance for C software, with a possibility
462 to customize this process, basically a poor-mans DarwinPorts.
463 """
464 curdir = os.getcwd()
465
466 name = recipe['name']
467 url = recipe['url']
468 configure = recipe.get('configure', './configure')
469 install = recipe.get('install', 'make && make install DESTDIR=%s'%(
470 shellQuote(basedir)))
471
472 archiveName = os.path.split(url)[-1]
473 sourceArchive = os.path.join(DEPSRC, archiveName)
474
475 if not os.path.exists(DEPSRC):
476 os.mkdir(DEPSRC)
477
478
479 if os.path.exists(sourceArchive):
480 print "Using local copy of %s"%(name,)
481
482 else:
483 print "Downloading %s"%(name,)
484 downloadURL(url, sourceArchive)
485 print "Archive for %s stored as %s"%(name, sourceArchive)
486
487 print "Extracting archive for %s"%(name,)
488 buildDir=os.path.join(WORKDIR, '_bld')
489 if not os.path.exists(buildDir):
490 os.mkdir(buildDir)
491
492 workDir = extractArchive(buildDir, sourceArchive)
493 os.chdir(workDir)
494 if 'buildDir' in recipe:
495 os.chdir(recipe['buildDir'])
496
497
498 for fn in recipe.get('patches', ()):
499 if fn.startswith('http://'):
500 # Download the patch before applying it.
501 path = os.path.join(DEPSRC, os.path.basename(fn))
502 downloadURL(fn, path)
503 fn = path
504
505 fn = os.path.join(curdir, fn)
506 runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
507 shellQuote(fn),))
508
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000509 if configure is not None:
510 configure_args = [
511 "--prefix=/usr/local",
512 "--enable-static",
513 "--disable-shared",
514 #"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
515 ]
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000516
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000517 if 'configure_pre' in recipe:
518 args = list(recipe['configure_pre'])
519 if '--disable-static' in args:
520 configure_args.remove('--enable-static')
521 if '--enable-shared' in args:
522 configure_args.remove('--disable-shared')
523 configure_args.extend(args)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000524
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000525 if recipe.get('useLDFlags', 1):
526 configure_args.extend([
527 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
528 ' -arch '.join(archList),
529 shellQuote(SDKPATH)[1:-1],
530 shellQuote(basedir)[1:-1],),
531 "LDFLAGS=-syslibroot,%s -L%s/usr/local/lib -arch %s"%(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000532 shellQuote(SDKPATH)[1:-1],
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000533 shellQuote(basedir)[1:-1],
534 ' -arch '.join(archList)),
535 ])
536 else:
537 configure_args.extend([
538 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
539 ' -arch '.join(archList),
540 shellQuote(SDKPATH)[1:-1],
541 shellQuote(basedir)[1:-1],),
542 ])
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000543
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000544 if 'configure_post' in recipe:
545 configure_args = configure_args = list(recipe['configure_post'])
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000546
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000547 configure_args.insert(0, configure)
548 configure_args = [ shellQuote(a) for a in configure_args ]
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000549
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000550 print "Running configure for %s"%(name,)
551 runCommand(' '.join(configure_args) + ' 2>&1')
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000552
553 print "Running install for %s"%(name,)
554 runCommand('{ ' + install + ' ;} 2>&1')
555
556 print "Done %s"%(name,)
557 print ""
558
559 os.chdir(curdir)
560
561def buildLibraries():
562 """
563 Build our dependencies into $WORKDIR/libraries/usr/local
564 """
565 print ""
566 print "Building required libraries"
567 print ""
568 universal = os.path.join(WORKDIR, 'libraries')
569 os.mkdir(universal)
570 os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
571 os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))
572
573 for recipe in LIBRARY_RECIPES:
Ronald Oussoren9b8b6192006-06-27 12:53:52 +0000574 buildRecipe(recipe, universal, ARCHLIST)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000575
576
577
578def buildPythonDocs():
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000579 # This stores the documentation as Resources/English.lproj/Documentation
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000580 # inside the framwork. pydoc and IDLE will pick it up there.
581 print "Install python documentation"
582 rootDir = os.path.join(WORKDIR, '_root')
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000583 buildDir = os.path.join('../../Doc')
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000584 docdir = os.path.join(rootDir, 'pydocs')
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000585 curDir = os.getcwd()
586 os.chdir(buildDir)
587 runCommand('make update')
588 runCommand('make html')
589 os.chdir(curDir)
590 if not os.path.exists(docdir):
591 os.mkdir(docdir)
592 os.rename(os.path.join(buildDir, 'build', 'html'),
593 os.path.join(docdir, 'python-docs-html'))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000594
595
596def buildPython():
597 print "Building a universal python"
598
599 buildDir = os.path.join(WORKDIR, '_bld', 'python')
600 rootDir = os.path.join(WORKDIR, '_root')
601
602 if os.path.exists(buildDir):
603 shutil.rmtree(buildDir)
604 if os.path.exists(rootDir):
605 shutil.rmtree(rootDir)
606 os.mkdir(buildDir)
607 os.mkdir(rootDir)
608 os.mkdir(os.path.join(rootDir, 'empty-dir'))
609 curdir = os.getcwd()
610 os.chdir(buildDir)
611
612 # Not sure if this is still needed, the original build script
613 # claims that parts of the install assume python.exe exists.
614 os.symlink('python', os.path.join(buildDir, 'python.exe'))
615
616 # Extract the version from the configure file, needed to calculate
617 # several paths.
618 version = getVersion()
619
620 print "Running configure..."
621 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"%(
622 shellQuote(os.path.join(SRCDIR, 'configure')),
623 shellQuote(SDKPATH), shellQuote(WORKDIR)[1:-1],
624 shellQuote(WORKDIR)[1:-1]))
625
626 print "Running make"
627 runCommand("make")
628
Ronald Oussorenaa560962006-11-07 15:53:38 +0000629 print "Running make frameworkinstall"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000630 runCommand("make frameworkinstall DESTDIR=%s"%(
631 shellQuote(rootDir)))
632
Ronald Oussorenaa560962006-11-07 15:53:38 +0000633 print "Running make frameworkinstallextras"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000634 runCommand("make frameworkinstallextras DESTDIR=%s"%(
635 shellQuote(rootDir)))
636
Ronald Oussorenaa560962006-11-07 15:53:38 +0000637 print "Copying required shared libraries"
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000638 if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
639 runCommand("mv %s/* %s"%(
640 shellQuote(os.path.join(
641 WORKDIR, 'libraries', 'Library', 'Frameworks',
642 'Python.framework', 'Versions', getVersion(),
643 'lib')),
644 shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
645 'Python.framework', 'Versions', getVersion(),
646 'lib'))))
647
648 print "Fix file modes"
649 frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
Ronald Oussoren74d3eef2006-10-10 07:55:06 +0000650 gid = grp.getgrnam('admin').gr_gid
651
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000652 for dirpath, dirnames, filenames in os.walk(frmDir):
653 for dn in dirnames:
654 os.chmod(os.path.join(dirpath, dn), 0775)
Ronald Oussoren74d3eef2006-10-10 07:55:06 +0000655 os.chown(os.path.join(dirpath, dn), -1, gid)
Tim Petersef3f32f2006-10-18 05:09:12 +0000656
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000657
658 for fn in filenames:
659 if os.path.islink(fn):
660 continue
661
662 # "chmod g+w $fn"
663 p = os.path.join(dirpath, fn)
664 st = os.stat(p)
Ronald Oussoren74d3eef2006-10-10 07:55:06 +0000665 os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
666 os.chown(p, -1, gid)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000667
668 # We added some directories to the search path during the configure
669 # phase. Remove those because those directories won't be there on
670 # the end-users system.
671 path =os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework',
672 'Versions', version, 'lib', 'python%s'%(version,),
673 'config', 'Makefile')
674 fp = open(path, 'r')
675 data = fp.read()
676 fp.close()
677
678 data = data.replace('-L%s/libraries/usr/local/lib'%(WORKDIR,), '')
679 data = data.replace('-I%s/libraries/usr/local/include'%(WORKDIR,), '')
680 fp = open(path, 'w')
681 fp.write(data)
682 fp.close()
683
684 # Add symlinks in /usr/local/bin, using relative links
685 usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
686 to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
687 'Python.framework', 'Versions', version, 'bin')
688 if os.path.exists(usr_local_bin):
689 shutil.rmtree(usr_local_bin)
690 os.makedirs(usr_local_bin)
691 for fn in os.listdir(
692 os.path.join(frmDir, 'Versions', version, 'bin')):
693 os.symlink(os.path.join(to_framework, fn),
694 os.path.join(usr_local_bin, fn))
695
696 os.chdir(curdir)
697
698
699
700def patchFile(inPath, outPath):
701 data = fileContents(inPath)
702 data = data.replace('$FULL_VERSION', getFullVersion())
703 data = data.replace('$VERSION', getVersion())
704 data = data.replace('$MACOSX_DEPLOYMENT_TARGET', '10.3 or later')
705 data = data.replace('$ARCHITECTURES', "i386, ppc")
706 data = data.replace('$INSTALL_SIZE', installSize())
Ronald Oussorenc5555542006-06-11 20:24:45 +0000707
708 # This one is not handy as a template variable
709 data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000710 fp = open(outPath, 'wb')
711 fp.write(data)
712 fp.close()
713
714def patchScript(inPath, outPath):
715 data = fileContents(inPath)
716 data = data.replace('@PYVER@', getVersion())
717 fp = open(outPath, 'wb')
718 fp.write(data)
719 fp.close()
720 os.chmod(outPath, 0755)
721
722
723
724def packageFromRecipe(targetDir, recipe):
725 curdir = os.getcwd()
726 try:
Ronald Oussorenaa560962006-11-07 15:53:38 +0000727 # The major version (such as 2.5) is included in the package name
728 # because having two version of python installed at the same time is
Ronald Oussorenc5555542006-06-11 20:24:45 +0000729 # common.
730 pkgname = '%s-%s'%(recipe['name'], getVersion())
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000731 srcdir = recipe.get('source')
732 pkgroot = recipe.get('topdir', srcdir)
733 postflight = recipe.get('postflight')
734 readme = textwrap.dedent(recipe['readme'])
735 isRequired = recipe.get('required', True)
736
737 print "- building package %s"%(pkgname,)
738
739 # Substitute some variables
740 textvars = dict(
741 VER=getVersion(),
742 FULLVER=getFullVersion(),
743 )
744 readme = readme % textvars
745
746 if pkgroot is not None:
747 pkgroot = pkgroot % textvars
748 else:
749 pkgroot = '/'
750
751 if srcdir is not None:
752 srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
753 srcdir = srcdir % textvars
754
755 if postflight is not None:
756 postflight = os.path.abspath(postflight)
757
758 packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
759 os.makedirs(packageContents)
760
761 if srcdir is not None:
762 os.chdir(srcdir)
763 runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
764 runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
765 runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
766
767 fn = os.path.join(packageContents, 'PkgInfo')
768 fp = open(fn, 'w')
769 fp.write('pmkrpkg1')
770 fp.close()
771
772 rsrcDir = os.path.join(packageContents, "Resources")
773 os.mkdir(rsrcDir)
774 fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
775 fp.write(readme)
776 fp.close()
777
778 if postflight is not None:
779 patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
780
781 vers = getFullVersion()
782 major, minor = map(int, getVersion().split('.', 2))
783 pl = Plist(
784 CFBundleGetInfoString="MacPython.%s %s"%(pkgname, vers,),
785 CFBundleIdentifier='org.python.MacPython.%s'%(pkgname,),
786 CFBundleName='MacPython.%s'%(pkgname,),
787 CFBundleShortVersionString=vers,
788 IFMajorVersion=major,
789 IFMinorVersion=minor,
790 IFPkgFormatVersion=0.10000000149011612,
791 IFPkgFlagAllowBackRev=False,
792 IFPkgFlagAuthorizationAction="RootAuthorization",
793 IFPkgFlagDefaultLocation=pkgroot,
794 IFPkgFlagFollowLinks=True,
795 IFPkgFlagInstallFat=True,
796 IFPkgFlagIsRequired=isRequired,
797 IFPkgFlagOverwritePermissions=False,
798 IFPkgFlagRelocatable=False,
799 IFPkgFlagRestartAction="NoRestart",
800 IFPkgFlagRootVolumeOnly=True,
801 IFPkgFlagUpdateInstalledLangauges=False,
802 )
803 writePlist(pl, os.path.join(packageContents, 'Info.plist'))
804
805 pl = Plist(
806 IFPkgDescriptionDescription=readme,
807 IFPkgDescriptionTitle=recipe.get('long_name', "MacPython.%s"%(pkgname,)),
808 IFPkgDescriptionVersion=vers,
809 )
810 writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
811
812 finally:
813 os.chdir(curdir)
814
815
816def makeMpkgPlist(path):
817
818 vers = getFullVersion()
819 major, minor = map(int, getVersion().split('.', 2))
820
821 pl = Plist(
822 CFBundleGetInfoString="MacPython %s"%(vers,),
823 CFBundleIdentifier='org.python.MacPython',
824 CFBundleName='MacPython',
825 CFBundleShortVersionString=vers,
826 IFMajorVersion=major,
827 IFMinorVersion=minor,
828 IFPkgFlagComponentDirectory="Contents/Packages",
829 IFPkgFlagPackageList=[
830 dict(
Ronald Oussorenc5555542006-06-11 20:24:45 +0000831 IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000832 IFPkgFlagPackageSelection='selected'
833 )
834 for item in PKG_RECIPES
835 ],
836 IFPkgFormatVersion=0.10000000149011612,
837 IFPkgFlagBackgroundScaling="proportional",
838 IFPkgFlagBackgroundAlignment="left",
Ronald Oussorenc5555542006-06-11 20:24:45 +0000839 IFPkgFlagAuthorizationAction="RootAuthorization",
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000840 )
841
842 writePlist(pl, path)
843
844
845def buildInstaller():
846
847 # Zap all compiled files
848 for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
849 for fn in filenames:
850 if fn.endswith('.pyc') or fn.endswith('.pyo'):
851 os.unlink(os.path.join(dirpath, fn))
852
853 outdir = os.path.join(WORKDIR, 'installer')
854 if os.path.exists(outdir):
855 shutil.rmtree(outdir)
856 os.mkdir(outdir)
857
858 pkgroot = os.path.join(outdir, 'MacPython.mpkg', 'Contents')
859 pkgcontents = os.path.join(pkgroot, 'Packages')
860 os.makedirs(pkgcontents)
861 for recipe in PKG_RECIPES:
862 packageFromRecipe(pkgcontents, recipe)
863
864 rsrcDir = os.path.join(pkgroot, 'Resources')
865
866 fn = os.path.join(pkgroot, 'PkgInfo')
867 fp = open(fn, 'w')
868 fp.write('pmkrpkg1')
869 fp.close()
870
871 os.mkdir(rsrcDir)
872
873 makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
874 pl = Plist(
875 IFPkgDescriptionTitle="Universal MacPython",
876 IFPkgDescriptionVersion=getVersion(),
877 )
878
879 writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
880 for fn in os.listdir('resources'):
881 if fn == '.svn': continue
882 if fn.endswith('.jpg'):
883 shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
884 else:
885 patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
886
Ronald Oussorenc5555542006-06-11 20:24:45 +0000887 shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000888
889
890def installSize(clear=False, _saved=[]):
891 if clear:
892 del _saved[:]
893 if not _saved:
894 data = captureCommand("du -ks %s"%(
895 shellQuote(os.path.join(WORKDIR, '_root'))))
896 _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
897 return _saved[0]
898
899
900def buildDMG():
901 """
Ronald Oussorenaa560962006-11-07 15:53:38 +0000902 Create DMG containing the rootDir.
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000903 """
904 outdir = os.path.join(WORKDIR, 'diskimage')
905 if os.path.exists(outdir):
906 shutil.rmtree(outdir)
907
908 imagepath = os.path.join(outdir,
909 'python-%s-macosx'%(getFullVersion(),))
910 if INCLUDE_TIMESTAMP:
911 imagepath = imagepath + '%04d-%02d-%02d'%(time.localtime()[:3])
912 imagepath = imagepath + '.dmg'
913
914 os.mkdir(outdir)
Ronald Oussorenaa560962006-11-07 15:53:38 +0000915 runCommand("hdiutil create -volname 'Universal MacPython %s' -srcfolder %s %s"%(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000916 getFullVersion(),
917 shellQuote(os.path.join(WORKDIR, 'installer')),
918 shellQuote(imagepath)))
919
920 return imagepath
921
922
923def setIcon(filePath, icnsPath):
924 """
925 Set the custom icon for the specified file or directory.
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000926 """
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000927
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000928 toolPath = os.path.join(os.path.dirname(__file__), "seticon.app/Contents/MacOS/seticon")
929 dirPath = os.path.dirname(__file__)
930 if not os.path.exists(toolPath) or os.stat(toolPath).st_mtime < os.stat(dirPath + '/seticon.m').st_mtime:
931 # NOTE: The tool is created inside an .app bundle, otherwise it won't work due
932 # to connections to the window server.
933 if not os.path.exists('seticon.app/Contents/MacOS'):
934 os.makedirs('seticon.app/Contents/MacOS')
935 runCommand("cc -o %s %s/seticon.m -framework Cocoa"%(
936 shellQuote(toolPath), shellQuote(dirPath)))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000937
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000938 runCommand("%s %s %s"%(shellQuote(os.path.abspath(toolPath)), shellQuote(icnsPath),
939 shellQuote(filePath)))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000940
941def main():
942 # First parse options and check if we can perform our work
943 parseOptions()
944 checkEnvironment()
945
946 os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.3'
947
948 if os.path.exists(WORKDIR):
949 shutil.rmtree(WORKDIR)
950 os.mkdir(WORKDIR)
951
952 # Then build third-party libraries such as sleepycat DB4.
953 buildLibraries()
954
955 # Now build python itself
956 buildPython()
957 buildPythonDocs()
958 fn = os.path.join(WORKDIR, "_root", "Applications",
Benjamin Petersonc3104762008-10-03 11:52:06 +0000959 "Python %s"%(getVersion(),), "Update Shell Profile.command")
Ronald Oussoren799868e2009-03-04 21:07:19 +0000960 patchScript("scripts/postflight.patch-profile", fn)
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000961
Benjamin Petersonc3104762008-10-03 11:52:06 +0000962 folder = os.path.join(WORKDIR, "_root", "Applications", "Python %s"%(
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000963 getVersion(),))
964 os.chmod(folder, 0755)
965 setIcon(folder, "../Icons/Python Folder.icns")
966
967 # Create the installer
968 buildInstaller()
969
970 # And copy the readme into the directory containing the installer
971 patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
972
973 # Ditto for the license file.
Ronald Oussorenc5555542006-06-11 20:24:45 +0000974 shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000975
976 fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
977 print >> fp, "# BUILD INFO"
978 print >> fp, "# Date:", time.ctime()
979 print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
980 fp.close()
981
982 # Custom icon for the DMG, shown when the DMG is mounted.
Ronald Oussoren648a4fe2009-03-30 17:15:29 +0000983 # XXX: Code is diabled because it doesn't actually work :-(
984# shutil.copy("../Icons/Disk Image.icns",
985# os.path.join(WORKDIR, "installer", ".VolumeIcon.icns"))
986# os.system("/Developer/Tools/SetFile -a C %s"%(
987# os.path.join(WORKDIR, "installer", ".VolumeIcon.icns")))
988 setIcon(os.path.join(WORKDIR, "installer"), "../Icons/Disk Image.icns")
Ronald Oussoren0e5b70d2006-06-07 18:58:42 +0000989
990
991 # And copy it to a DMG
992 buildDMG()
993
994
995if __name__ == "__main__":
996 main()