blob: 034828b62d750c99e1ebaa95a6c9e816bfc73ab5 [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"""
12import 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
57 raise RuntimeError, "Cannot find full version??"
58
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(
132 name="SQLite 3.3.5",
133 url="http://www.sqlite.org/sqlite-3.3.5.tar.gz",
134 checksum='93f742986e8bc2dfa34792e16df017a6feccf3a2',
135 configure_pre=[
136 '--enable-threadsafe',
137 '--enable-tempstore',
138 '--enable-shared=no',
139 '--enable-static=yes',
140 '--disable-tcl',
141 ]
142 ),
143
144 dict(
145 name="NCurses 5.5",
146 url="http://ftp.gnu.org/pub/gnu/ncurses/ncurses-5.5.tar.gz",
147 configure_pre=[
148 "--without-cxx",
149 "--without-ada",
150 "--without-progs",
151 "--without-curses-h",
152 "--enable-shared",
153 "--with-shared",
154 "--datadir=/usr/share",
155 "--sysconfdir=/etc",
156 "--sharedstatedir=/usr/com",
157 "--with-terminfo-dirs=/usr/share/terminfo",
158 "--with-default-terminfo-dir=/usr/share/terminfo",
159 "--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib"%(getVersion(),),
160 "--enable-termcap",
161 ],
162 patches=[
163 "ncurses-5.5.patch",
164 ],
165 useLDFlags=False,
166 install='make && make install DESTDIR=%s && cd %s/usr/local/lib && ln -fs ../../../Library/Frameworks/Python.framework/Versions/%s/lib/lib* .'%(
167 shellQuote(os.path.join(WORKDIR, 'libraries')),
168 shellQuote(os.path.join(WORKDIR, 'libraries')),
169 getVersion(),
170 ),
171 ),
172 dict(
173 name="Sleepycat DB 4.4",
174 url="http://downloads.sleepycat.com/db-4.4.20.tar.gz",
175 #name="Sleepycat DB 4.3.29",
176 #url="http://downloads.sleepycat.com/db-4.3.29.tar.gz",
177 buildDir="build_unix",
178 configure="../dist/configure",
179 configure_pre=[
180 '--includedir=/usr/local/include/db4',
181 ]
182 ),
183]
184
185
186# Instructions for building packages inside the .mpkg.
Thomas Wouters89f507f2006-12-13 04:49:30 +0000187PKG_RECIPES = [
Thomas Wouters477c8d52006-05-27 19:21:47 +0000188 dict(
189 name="PythonFramework",
190 long_name="Python Framework",
191 source="/Library/Frameworks/Python.framework",
192 readme="""\
193 This package installs Python.framework, that is the python
194 interpreter and the standard library. This also includes Python
195 wrappers for lots of Mac OS X API's.
196 """,
197 postflight="scripts/postflight.framework",
198 ),
199 dict(
200 name="PythonApplications",
201 long_name="GUI Applications",
202 source="/Applications/MacPython %(VER)s",
203 readme="""\
Thomas Wouters89f507f2006-12-13 04:49:30 +0000204 This package installs IDLE (an interactive Python IDE),
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000205 Python Launcher and Build Applet (create application bundles
206 from python scripts).
207
208 It also installs a number of examples and demos.
Thomas Wouters477c8d52006-05-27 19:21:47 +0000209 """,
210 required=False,
211 ),
212 dict(
213 name="PythonUnixTools",
214 long_name="UNIX command-line tools",
215 source="/usr/local/bin",
216 readme="""\
217 This package installs the unix tools in /usr/local/bin for
218 compatibility with older releases of MacPython. This package
219 is not necessary to use MacPython.
220 """,
221 required=False,
222 ),
223 dict(
224 name="PythonDocumentation",
225 long_name="Python Documentation",
226 topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
227 source="/pydocs",
228 readme="""\
229 This package installs the python documentation at a location
230 that is useable for pydoc and IDLE. If you have installed Xcode
231 it will also install a link to the documentation in
232 /Developer/Documentation/Python
233 """,
234 postflight="scripts/postflight.documentation",
235 required=False,
236 ),
237 dict(
238 name="PythonProfileChanges",
239 long_name="Shell profile updater",
240 readme="""\
241 This packages updates your shell profile to make sure that
242 the MacPython tools are found by your shell in preference of
243 the system provided Python tools.
244
245 If you don't install this package you'll have to add
246 "/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
247 to your PATH by hand.
248 """,
249 postflight="scripts/postflight.patch-profile",
250 topdir="/Library/Frameworks/Python.framework",
251 source="/empty-dir",
252 required=False,
253 ),
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000254 dict(
255 name="PythonSystemFixes",
256 long_name="Fix system Python",
257 readme="""\
258 This package updates the system python installation on
259 Mac OS X 10.3 to ensure that you can build new python extensions
Thomas Wouters89f507f2006-12-13 04:49:30 +0000260 using that copy of python after installing this version.
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000261 """,
Thomas Wouters73e5a5b2006-06-08 15:35:45 +0000262 postflight="../Tools/fixapplepython23.py",
263 topdir="/Library/Frameworks/Python.framework",
264 source="/empty-dir",
265 required=False,
266 )
Thomas Wouters477c8d52006-05-27 19:21:47 +0000267]
268
Thomas Wouters477c8d52006-05-27 19:21:47 +0000269def 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
Thomas Wouters89f507f2006-12-13 04:49:30 +0000326def parseOptions(args=None):
Thomas Wouters477c8d52006-05-27 19:21:47 +0000327 """
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='])
Guido van Rossumb940e112007-01-10 16:19:56 +0000338 except getopt.error as msg:
Thomas Wouters477c8d52006-05-27 19:21:47 +0000339 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
Thomas Wouters0e3f5912006-08-11 14:57:12 +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 ]
Thomas Wouters477c8d52006-05-27 19:21:47 +0000518
Thomas Wouters0e3f5912006-08-11 14:57:12 +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)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000526
Thomas Wouters0e3f5912006-08-11 14:57:12 +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"%(
Thomas Wouters477c8d52006-05-27 19:21:47 +0000534 shellQuote(SDKPATH)[1:-1],
Thomas Wouters0e3f5912006-08-11 14:57:12 +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 ])
Thomas Wouters477c8d52006-05-27 19:21:47 +0000545
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000546 if 'configure_post' in recipe:
547 configure_args = configure_args = list(recipe['configure_post'])
Thomas Wouters477c8d52006-05-27 19:21:47 +0000548
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000549 configure_args.insert(0, configure)
550 configure_args = [ shellQuote(a) for a in configure_args ]
Thomas Wouters477c8d52006-05-27 19:21:47 +0000551
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000552 print "Running configure for %s"%(name,)
553 runCommand(' '.join(configure_args) + ' 2>&1')
Thomas Wouters477c8d52006-05-27 19:21:47 +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:
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000576 buildRecipe(recipe, universal, ARCHLIST)
Thomas Wouters477c8d52006-05-27 19:21:47 +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
Thomas Wouters89f507f2006-12-13 04:49:30 +0000639 print "Running make frameworkinstall"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000640 runCommand("make frameworkinstall DESTDIR=%s"%(
641 shellQuote(rootDir)))
642
Thomas Wouters89f507f2006-12-13 04:49:30 +0000643 print "Running make frameworkinstallextras"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000644 runCommand("make frameworkinstallextras DESTDIR=%s"%(
645 shellQuote(rootDir)))
646
Thomas Wouters89f507f2006-12-13 04:49:30 +0000647 print "Copying required shared libraries"
Thomas Wouters477c8d52006-05-27 19:21:47 +0000648 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')
Thomas Wouters89f507f2006-12-13 04:49:30 +0000660 gid = grp.getgrnam('admin').gr_gid
661
Thomas Wouters477c8d52006-05-27 19:21:47 +0000662 for dirpath, dirnames, filenames in os.walk(frmDir):
663 for dn in dirnames:
664 os.chmod(os.path.join(dirpath, dn), 0775)
Thomas Wouters89f507f2006-12-13 04:49:30 +0000665 os.chown(os.path.join(dirpath, dn), -1, gid)
666
Thomas Wouters477c8d52006-05-27 19:21:47 +0000667
668 for fn in filenames:
669 if os.path.islink(fn):
670 continue
671
672 # "chmod g+w $fn"
673 p = os.path.join(dirpath, fn)
674 st = os.stat(p)
Thomas Wouters89f507f2006-12-13 04:49:30 +0000675 os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
676 os.chown(p, -1, gid)
Thomas Wouters477c8d52006-05-27 19:21:47 +0000677
678 # We added some directories to the search path during the configure
679 # phase. Remove those because those directories won't be there on
680 # the end-users system.
681 path =os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework',
682 'Versions', version, 'lib', 'python%s'%(version,),
683 'config', 'Makefile')
684 fp = open(path, 'r')
685 data = fp.read()
686 fp.close()
687
688 data = data.replace('-L%s/libraries/usr/local/lib'%(WORKDIR,), '')
689 data = data.replace('-I%s/libraries/usr/local/include'%(WORKDIR,), '')
690 fp = open(path, 'w')
691 fp.write(data)
692 fp.close()
693
694 # Add symlinks in /usr/local/bin, using relative links
695 usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
696 to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
697 'Python.framework', 'Versions', version, 'bin')
698 if os.path.exists(usr_local_bin):
699 shutil.rmtree(usr_local_bin)
700 os.makedirs(usr_local_bin)
701 for fn in os.listdir(
702 os.path.join(frmDir, 'Versions', version, 'bin')):
703 os.symlink(os.path.join(to_framework, fn),
704 os.path.join(usr_local_bin, fn))
705
706 os.chdir(curdir)
707
708
709
710def patchFile(inPath, outPath):
711 data = fileContents(inPath)
712 data = data.replace('$FULL_VERSION', getFullVersion())
713 data = data.replace('$VERSION', getVersion())
714 data = data.replace('$MACOSX_DEPLOYMENT_TARGET', '10.3 or later')
715 data = data.replace('$ARCHITECTURES', "i386, ppc")
716 data = data.replace('$INSTALL_SIZE', installSize())
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000717
718 # This one is not handy as a template variable
719 data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
Thomas Wouters477c8d52006-05-27 19:21:47 +0000720 fp = open(outPath, 'wb')
721 fp.write(data)
722 fp.close()
723
724def patchScript(inPath, outPath):
725 data = fileContents(inPath)
726 data = data.replace('@PYVER@', getVersion())
727 fp = open(outPath, 'wb')
728 fp.write(data)
729 fp.close()
730 os.chmod(outPath, 0755)
731
732
733
734def packageFromRecipe(targetDir, recipe):
735 curdir = os.getcwd()
736 try:
Thomas Wouters89f507f2006-12-13 04:49:30 +0000737 # The major version (such as 2.5) is included in the package name
738 # because having two version of python installed at the same time is
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000739 # common.
740 pkgname = '%s-%s'%(recipe['name'], getVersion())
Thomas Wouters477c8d52006-05-27 19:21:47 +0000741 srcdir = recipe.get('source')
742 pkgroot = recipe.get('topdir', srcdir)
743 postflight = recipe.get('postflight')
744 readme = textwrap.dedent(recipe['readme'])
745 isRequired = recipe.get('required', True)
746
747 print "- building package %s"%(pkgname,)
748
749 # Substitute some variables
750 textvars = dict(
751 VER=getVersion(),
752 FULLVER=getFullVersion(),
753 )
754 readme = readme % textvars
755
756 if pkgroot is not None:
757 pkgroot = pkgroot % textvars
758 else:
759 pkgroot = '/'
760
761 if srcdir is not None:
762 srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
763 srcdir = srcdir % textvars
764
765 if postflight is not None:
766 postflight = os.path.abspath(postflight)
767
768 packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
769 os.makedirs(packageContents)
770
771 if srcdir is not None:
772 os.chdir(srcdir)
773 runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
774 runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
775 runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
776
777 fn = os.path.join(packageContents, 'PkgInfo')
778 fp = open(fn, 'w')
779 fp.write('pmkrpkg1')
780 fp.close()
781
782 rsrcDir = os.path.join(packageContents, "Resources")
783 os.mkdir(rsrcDir)
784 fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
785 fp.write(readme)
786 fp.close()
787
788 if postflight is not None:
789 patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
790
791 vers = getFullVersion()
792 major, minor = map(int, getVersion().split('.', 2))
793 pl = Plist(
794 CFBundleGetInfoString="MacPython.%s %s"%(pkgname, vers,),
795 CFBundleIdentifier='org.python.MacPython.%s'%(pkgname,),
796 CFBundleName='MacPython.%s'%(pkgname,),
797 CFBundleShortVersionString=vers,
798 IFMajorVersion=major,
799 IFMinorVersion=minor,
800 IFPkgFormatVersion=0.10000000149011612,
801 IFPkgFlagAllowBackRev=False,
802 IFPkgFlagAuthorizationAction="RootAuthorization",
803 IFPkgFlagDefaultLocation=pkgroot,
804 IFPkgFlagFollowLinks=True,
805 IFPkgFlagInstallFat=True,
806 IFPkgFlagIsRequired=isRequired,
807 IFPkgFlagOverwritePermissions=False,
808 IFPkgFlagRelocatable=False,
809 IFPkgFlagRestartAction="NoRestart",
810 IFPkgFlagRootVolumeOnly=True,
811 IFPkgFlagUpdateInstalledLangauges=False,
812 )
813 writePlist(pl, os.path.join(packageContents, 'Info.plist'))
814
815 pl = Plist(
816 IFPkgDescriptionDescription=readme,
817 IFPkgDescriptionTitle=recipe.get('long_name', "MacPython.%s"%(pkgname,)),
818 IFPkgDescriptionVersion=vers,
819 )
820 writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
821
822 finally:
823 os.chdir(curdir)
824
825
826def makeMpkgPlist(path):
827
828 vers = getFullVersion()
829 major, minor = map(int, getVersion().split('.', 2))
830
831 pl = Plist(
832 CFBundleGetInfoString="MacPython %s"%(vers,),
833 CFBundleIdentifier='org.python.MacPython',
834 CFBundleName='MacPython',
835 CFBundleShortVersionString=vers,
836 IFMajorVersion=major,
837 IFMinorVersion=minor,
838 IFPkgFlagComponentDirectory="Contents/Packages",
839 IFPkgFlagPackageList=[
840 dict(
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000841 IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
Thomas Wouters477c8d52006-05-27 19:21:47 +0000842 IFPkgFlagPackageSelection='selected'
843 )
844 for item in PKG_RECIPES
845 ],
846 IFPkgFormatVersion=0.10000000149011612,
847 IFPkgFlagBackgroundScaling="proportional",
848 IFPkgFlagBackgroundAlignment="left",
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000849 IFPkgFlagAuthorizationAction="RootAuthorization",
Thomas Wouters477c8d52006-05-27 19:21:47 +0000850 )
851
852 writePlist(pl, path)
853
854
855def buildInstaller():
856
857 # Zap all compiled files
858 for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
859 for fn in filenames:
860 if fn.endswith('.pyc') or fn.endswith('.pyo'):
861 os.unlink(os.path.join(dirpath, fn))
862
863 outdir = os.path.join(WORKDIR, 'installer')
864 if os.path.exists(outdir):
865 shutil.rmtree(outdir)
866 os.mkdir(outdir)
867
868 pkgroot = os.path.join(outdir, 'MacPython.mpkg', 'Contents')
869 pkgcontents = os.path.join(pkgroot, 'Packages')
870 os.makedirs(pkgcontents)
871 for recipe in PKG_RECIPES:
872 packageFromRecipe(pkgcontents, recipe)
873
874 rsrcDir = os.path.join(pkgroot, 'Resources')
875
876 fn = os.path.join(pkgroot, 'PkgInfo')
877 fp = open(fn, 'w')
878 fp.write('pmkrpkg1')
879 fp.close()
880
881 os.mkdir(rsrcDir)
882
883 makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
884 pl = Plist(
885 IFPkgDescriptionTitle="Universal MacPython",
886 IFPkgDescriptionVersion=getVersion(),
887 )
888
889 writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
890 for fn in os.listdir('resources'):
891 if fn == '.svn': continue
892 if fn.endswith('.jpg'):
893 shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
894 else:
895 patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
896
Thomas Wouters0e3f5912006-08-11 14:57:12 +0000897 shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
Thomas Wouters477c8d52006-05-27 19:21:47 +0000898
899
900def installSize(clear=False, _saved=[]):
901 if clear:
902 del _saved[:]
903 if not _saved:
904 data = captureCommand("du -ks %s"%(
905 shellQuote(os.path.join(WORKDIR, '_root'))))
906 _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
907 return _saved[0]
908
909
910def buildDMG():
911 """
Thomas Wouters89f507f2006-12-13 04:49:30 +0000912 Create DMG containing the rootDir.
Thomas Wouters477c8d52006-05-27 19:21:47 +0000913 """
914 outdir = os.path.join(WORKDIR, 'diskimage')
915 if os.path.exists(outdir):
916 shutil.rmtree(outdir)
917
918 imagepath = os.path.join(outdir,
919 'python-%s-macosx'%(getFullVersion(),))
920 if INCLUDE_TIMESTAMP:
921 imagepath = imagepath + '%04d-%02d-%02d'%(time.localtime()[:3])
922 imagepath = imagepath + '.dmg'
923
924 os.mkdir(outdir)
Thomas Wouters89f507f2006-12-13 04:49:30 +0000925 runCommand("hdiutil create -volname 'Universal MacPython %s' -srcfolder %s %s"%(
Thomas Wouters477c8d52006-05-27 19:21:47 +0000926 getFullVersion(),
927 shellQuote(os.path.join(WORKDIR, 'installer')),
928 shellQuote(imagepath)))
929
930 return imagepath
931
932
933def setIcon(filePath, icnsPath):
934 """
935 Set the custom icon for the specified file or directory.
936
937 For a directory the icon data is written in a file named 'Icon\r' inside
938 the directory. For both files and directories write the icon as an 'icns'
939 resource. Furthermore set kHasCustomIcon in the finder flags for filePath.
940 """
941 ref, isDirectory = Carbon.File.FSPathMakeRef(icnsPath)
942 icon = Carbon.Icn.ReadIconFile(ref)
943 del ref
944
945 #
946 # Open the resource fork of the target, to add the icon later on.
947 # For directories we use the file 'Icon\r' inside the directory.
948 #
949
950 ref, isDirectory = Carbon.File.FSPathMakeRef(filePath)
951
952 if isDirectory:
Thomas Wouters89f507f2006-12-13 04:49:30 +0000953 # There is a problem with getting this into the pax(1) archive,
954 # just ignore directory icons for now.
955 return
956
Thomas Wouters477c8d52006-05-27 19:21:47 +0000957 tmpPath = os.path.join(filePath, "Icon\r")
958 if not os.path.exists(tmpPath):
959 fp = open(tmpPath, 'w')
960 fp.close()
961
962 tmpRef, _ = Carbon.File.FSPathMakeRef(tmpPath)
963 spec = Carbon.File.FSSpec(tmpRef)
964
965 else:
966 spec = Carbon.File.FSSpec(ref)
967
968 try:
969 Carbon.Res.HCreateResFile(*spec.as_tuple())
970 except MacOS.Error:
971 pass
972
973 # Try to create the resource fork again, this will avoid problems
974 # when adding an icon to a directory. I have no idea why this helps,
975 # but without this adding the icon to a directory will fail sometimes.
976 try:
977 Carbon.Res.HCreateResFile(*spec.as_tuple())
978 except MacOS.Error:
979 pass
980
981 refNum = Carbon.Res.FSpOpenResFile(spec, fsRdWrPerm)
982
983 Carbon.Res.UseResFile(refNum)
984
985 # Check if there already is an icon, remove it if there is.
986 try:
987 h = Carbon.Res.Get1Resource('icns', kCustomIconResource)
988 except MacOS.Error:
989 pass
990
991 else:
992 h.RemoveResource()
993 del h
994
995 # Add the icon to the resource for of the target
996 res = Carbon.Res.Resource(icon)
997 res.AddResource('icns', kCustomIconResource, '')
998 res.WriteResource()
999 res.DetachResource()
1000 Carbon.Res.CloseResFile(refNum)
1001
1002 # And now set the kHasCustomIcon property for the target. Annoyingly,
1003 # python doesn't seem to have bindings for the API that is needed for
1004 # this. Cop out and call SetFile
1005 os.system("/Developer/Tools/SetFile -a C %s"%(
1006 shellQuote(filePath),))
1007
1008 if isDirectory:
1009 os.system('/Developer/Tools/SetFile -a V %s'%(
1010 shellQuote(tmpPath),
1011 ))
1012
1013def main():
1014 # First parse options and check if we can perform our work
1015 parseOptions()
1016 checkEnvironment()
1017
1018 os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.3'
1019
1020 if os.path.exists(WORKDIR):
1021 shutil.rmtree(WORKDIR)
1022 os.mkdir(WORKDIR)
1023
1024 # Then build third-party libraries such as sleepycat DB4.
1025 buildLibraries()
1026
1027 # Now build python itself
1028 buildPython()
1029 buildPythonDocs()
1030 fn = os.path.join(WORKDIR, "_root", "Applications",
1031 "MacPython %s"%(getVersion(),), "Update Shell Profile.command")
Thomas Wouters0e3f5912006-08-11 14:57:12 +00001032 patchFile("scripts/postflight.patch-profile", fn)
Thomas Wouters477c8d52006-05-27 19:21:47 +00001033 os.chmod(fn, 0755)
1034
1035 folder = os.path.join(WORKDIR, "_root", "Applications", "MacPython %s"%(
1036 getVersion(),))
1037 os.chmod(folder, 0755)
1038 setIcon(folder, "../Icons/Python Folder.icns")
1039
1040 # Create the installer
1041 buildInstaller()
1042
1043 # And copy the readme into the directory containing the installer
1044 patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
1045
1046 # Ditto for the license file.
Thomas Wouters0e3f5912006-08-11 14:57:12 +00001047 shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
Thomas Wouters477c8d52006-05-27 19:21:47 +00001048
1049 fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
1050 print >> fp, "# BUILD INFO"
1051 print >> fp, "# Date:", time.ctime()
1052 print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
1053 fp.close()
1054
1055 # Custom icon for the DMG, shown when the DMG is mounted.
1056 shutil.copy("../Icons/Disk Image.icns",
1057 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns"))
1058 os.system("/Developer/Tools/SetFile -a C %s"%(
1059 os.path.join(WORKDIR, "installer", ".VolumeIcon.icns")))
1060
1061
1062 # And copy it to a DMG
1063 buildDMG()
1064
1065
1066if __name__ == "__main__":
1067 main()