Fix for issue6957: ensure that the OSX installer
[python.git] / Mac / BuildScript / build-installer.py
blob740e45ebb61e1f569c89707592beb77b3bbf62c6
1 #!/usr/bin/python
2 """
3 This script is used to build the "official unofficial" universal build on
4 Mac OS X. It requires Mac OS X 10.4, Xcode 2.2 and the 10.4u SDK to do its
5 work. 64-bit or four-way universal builds require at least OS X 10.5 and
6 the 10.5 SDK.
8 Please ensure that this script keeps working with Python 2.3, to avoid
9 bootstrap issues (/usr/bin/python is Python 2.3 on OSX 10.4)
11 Usage: see USAGE variable in the script.
12 """
13 import platform, os, sys, getopt, textwrap, shutil, urllib2, stat, time, pwd
14 import grp
16 INCLUDE_TIMESTAMP = 1
17 VERBOSE = 1
19 from plistlib import Plist
21 import MacOS
23 try:
24 from plistlib import writePlist
25 except ImportError:
26 # We're run using python2.3
27 def writePlist(plist, path):
28 plist.write(path)
30 def shellQuote(value):
31 """
32 Return the string value in a form that can safely be inserted into
33 a shell command.
34 """
35 return "'%s'"%(value.replace("'", "'\"'\"'"))
37 def grepValue(fn, variable):
38 variable = variable + '='
39 for ln in open(fn, 'r'):
40 if ln.startswith(variable):
41 value = ln[len(variable):].strip()
42 return value[1:-1]
44 def getVersion():
45 return grepValue(os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
47 def getFullVersion():
48 fn = os.path.join(SRCDIR, 'Include', 'patchlevel.h')
49 for ln in open(fn):
50 if 'PY_VERSION' in ln:
51 return ln.split()[-1][1:-1]
53 raise RuntimeError, "Cannot find full version??"
55 # The directory we'll use to create the build (will be erased and recreated)
56 WORKDIR = "/tmp/_py"
58 # The directory we'll use to store third-party sources. Set this to something
59 # else if you don't want to re-fetch required libraries every time.
60 DEPSRC = os.path.join(WORKDIR, 'third-party')
61 DEPSRC = os.path.expanduser('~/Universal/other-sources')
63 # Location of the preferred SDK
65 ### There are some issues with the SDK selection below here,
66 ### The resulting binary doesn't work on all platforms that
67 ### it should. Always default to the 10.4u SDK until that
68 ### isue is resolved.
69 ###
70 ##if int(os.uname()[2].split('.')[0]) == 8:
71 ## # Explicitly use the 10.4u (universal) SDK when
72 ## # building on 10.4, the system headers are not
73 ## # useable for a universal build
74 ## SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
75 ##else:
76 ## SDKPATH = "/"
78 SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
80 universal_opts_map = { '32-bit': ('i386', 'ppc',),
81 '64-bit': ('x86_64', 'ppc64',),
82 'intel': ('i386', 'x86_64'),
83 '3-way': ('ppc', 'i386', 'x86_64'),
84 'all': ('i386', 'ppc', 'x86_64', 'ppc64',) }
85 default_target_map = {
86 '64-bit': '10.5',
87 '3-way': '10.5',
88 'intel': '10.5',
89 'all': '10.5',
92 UNIVERSALOPTS = tuple(universal_opts_map.keys())
94 UNIVERSALARCHS = '32-bit'
96 ARCHLIST = universal_opts_map[UNIVERSALARCHS]
98 # Source directory (asume we're in Mac/BuildScript)
99 SRCDIR = os.path.dirname(
100 os.path.dirname(
101 os.path.dirname(
102 os.path.abspath(__file__
103 ))))
105 # $MACOSX_DEPLOYMENT_TARGET -> minimum OS X level
106 DEPTARGET = '10.3'
108 target_cc_map = {
109 '10.3': 'gcc-4.0',
110 '10.4': 'gcc-4.0',
111 '10.5': 'gcc-4.0',
112 '10.6': 'gcc-4.2',
115 CC = target_cc_map[DEPTARGET]
117 USAGE = textwrap.dedent("""\
118 Usage: build_python [options]
120 Options:
121 -? or -h: Show this message
122 -b DIR
123 --build-dir=DIR: Create build here (default: %(WORKDIR)r)
124 --third-party=DIR: Store third-party sources here (default: %(DEPSRC)r)
125 --sdk-path=DIR: Location of the SDK (default: %(SDKPATH)r)
126 --src-dir=DIR: Location of the Python sources (default: %(SRCDIR)r)
127 --dep-target=10.n OS X deployment target (default: %(DEPTARGET)r)
128 --universal-archs=x universal architectures (options: %(UNIVERSALOPTS)r, default: %(UNIVERSALARCHS)r)
129 """)% globals()
132 # Instructions for building libraries that are necessary for building a
133 # batteries included python.
134 # [The recipes are defined here for convenience but instantiated later after
135 # command line options have been processed.]
136 def library_recipes():
137 result = []
139 if DEPTARGET < '10.5':
140 result.extend([
141 dict(
142 name="Bzip2 1.0.5",
143 url="http://www.bzip.org/1.0.5/bzip2-1.0.5.tar.gz",
144 checksum='3c15a0c8d1d3ee1c46a1634d00617b1a',
145 configure=None,
146 install='make install CC=%s PREFIX=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
148 shellQuote(os.path.join(WORKDIR, 'libraries')),
149 ' -arch '.join(ARCHLIST),
150 SDKPATH,
153 dict(
154 name="ZLib 1.2.3",
155 url="http://www.gzip.org/zlib/zlib-1.2.3.tar.gz",
156 checksum='debc62758716a169df9f62e6ab2bc634',
157 configure=None,
158 install='make install CC=%s prefix=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
160 shellQuote(os.path.join(WORKDIR, 'libraries')),
161 ' -arch '.join(ARCHLIST),
162 SDKPATH,
165 dict(
166 # Note that GNU readline is GPL'd software
167 name="GNU Readline 5.1.4",
168 url="http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz" ,
169 checksum='7ee5a692db88b30ca48927a13fd60e46',
170 patchlevel='0',
171 patches=[
172 # The readline maintainers don't do actual micro releases, but
173 # just ship a set of patches.
174 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-001',
175 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-002',
176 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-003',
177 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-004',
180 dict(
181 name="SQLite 3.6.11",
182 url="http://www.sqlite.org/sqlite-3.6.11.tar.gz",
183 checksum='7ebb099696ab76cc6ff65dd496d17858',
184 configure_pre=[
185 '--enable-threadsafe',
186 '--enable-tempstore',
187 '--enable-shared=no',
188 '--enable-static=yes',
189 '--disable-tcl',
192 dict(
193 name="NCurses 5.5",
194 url="http://ftp.gnu.org/pub/gnu/ncurses/ncurses-5.5.tar.gz",
195 checksum='e73c1ac10b4bfc46db43b2ddfd6244ef',
196 configure_pre=[
197 "--without-cxx",
198 "--without-ada",
199 "--without-progs",
200 "--without-curses-h",
201 "--enable-shared",
202 "--with-shared",
203 "--datadir=/usr/share",
204 "--sysconfdir=/etc",
205 "--sharedstatedir=/usr/com",
206 "--with-terminfo-dirs=/usr/share/terminfo",
207 "--with-default-terminfo-dir=/usr/share/terminfo",
208 "--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib"%(getVersion(),),
209 "--enable-termcap",
211 patches=[
212 "ncurses-5.5.patch",
214 useLDFlags=False,
215 install='make && make install DESTDIR=%s && cd %s/usr/local/lib && ln -fs ../../../Library/Frameworks/Python.framework/Versions/%s/lib/lib* .'%(
216 shellQuote(os.path.join(WORKDIR, 'libraries')),
217 shellQuote(os.path.join(WORKDIR, 'libraries')),
218 getVersion(),
223 result.extend([
224 dict(
225 name="Sleepycat DB 4.7.25",
226 url="http://download.oracle.com/berkeley-db/db-4.7.25.tar.gz",
227 checksum='ec2b87e833779681a0c3a814aa71359e',
228 buildDir="build_unix",
229 configure="../dist/configure",
230 configure_pre=[
231 '--includedir=/usr/local/include/db4',
236 return result
239 # Instructions for building packages inside the .mpkg.
240 def pkg_recipes():
241 result = [
242 dict(
243 name="PythonFramework",
244 long_name="Python Framework",
245 source="/Library/Frameworks/Python.framework",
246 readme="""\
247 This package installs Python.framework, that is the python
248 interpreter and the standard library. This also includes Python
249 wrappers for lots of Mac OS X API's.
250 """,
251 postflight="scripts/postflight.framework",
253 dict(
254 name="PythonApplications",
255 long_name="GUI Applications",
256 source="/Applications/Python %(VER)s",
257 readme="""\
258 This package installs IDLE (an interactive Python IDE),
259 Python Launcher and Build Applet (create application bundles
260 from python scripts).
262 It also installs a number of examples and demos.
263 """,
264 required=False,
266 dict(
267 name="PythonUnixTools",
268 long_name="UNIX command-line tools",
269 source="/usr/local/bin",
270 readme="""\
271 This package installs the unix tools in /usr/local/bin for
272 compatibility with older releases of Python. This package
273 is not necessary to use Python.
274 """,
275 required=False,
277 dict(
278 name="PythonDocumentation",
279 long_name="Python Documentation",
280 topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
281 source="/pydocs",
282 readme="""\
283 This package installs the python documentation at a location
284 that is useable for pydoc and IDLE. If you have installed Xcode
285 it will also install a link to the documentation in
286 /Developer/Documentation/Python
287 """,
288 postflight="scripts/postflight.documentation",
289 required=False,
291 dict(
292 name="PythonProfileChanges",
293 long_name="Shell profile updater",
294 readme="""\
295 This packages updates your shell profile to make sure that
296 the Python tools are found by your shell in preference of
297 the system provided Python tools.
299 If you don't install this package you'll have to add
300 "/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
301 to your PATH by hand.
302 """,
303 postflight="scripts/postflight.patch-profile",
304 topdir="/Library/Frameworks/Python.framework",
305 source="/empty-dir",
306 required=False,
310 if DEPTARGET < '10.4':
311 result.append(
312 dict(
313 name="PythonSystemFixes",
314 long_name="Fix system Python",
315 readme="""\
316 This package updates the system python installation on
317 Mac OS X 10.3 to ensure that you can build new python extensions
318 using that copy of python after installing this version.
319 """,
320 postflight="../Tools/fixapplepython23.py",
321 topdir="/Library/Frameworks/Python.framework",
322 source="/empty-dir",
323 required=False,
326 return result
328 def fatal(msg):
330 A fatal error, bail out.
332 sys.stderr.write('FATAL: ')
333 sys.stderr.write(msg)
334 sys.stderr.write('\n')
335 sys.exit(1)
337 def fileContents(fn):
339 Return the contents of the named file
341 return open(fn, 'rb').read()
343 def runCommand(commandline):
345 Run a command and raise RuntimeError if it fails. Output is surpressed
346 unless the command fails.
348 fd = os.popen(commandline, 'r')
349 data = fd.read()
350 xit = fd.close()
351 if xit is not None:
352 sys.stdout.write(data)
353 raise RuntimeError, "command failed: %s"%(commandline,)
355 if VERBOSE:
356 sys.stdout.write(data); sys.stdout.flush()
358 def captureCommand(commandline):
359 fd = os.popen(commandline, 'r')
360 data = fd.read()
361 xit = fd.close()
362 if xit is not None:
363 sys.stdout.write(data)
364 raise RuntimeError, "command failed: %s"%(commandline,)
366 return data
368 def checkEnvironment():
370 Check that we're running on a supported system.
373 if platform.system() != 'Darwin':
374 fatal("This script should be run on a Mac OS X 10.4 (or later) system")
376 if int(platform.release().split('.')[0]) < 8:
377 fatal("This script should be run on a Mac OS X 10.4 (or later) system")
379 if not os.path.exists(SDKPATH):
380 fatal("Please install the latest version of Xcode and the %s SDK"%(
381 os.path.basename(SDKPATH[:-4])))
385 def parseOptions(args=None):
387 Parse arguments and update global settings.
389 global WORKDIR, DEPSRC, SDKPATH, SRCDIR, DEPTARGET
390 global UNIVERSALOPTS, UNIVERSALARCHS, ARCHLIST, CC
392 if args is None:
393 args = sys.argv[1:]
395 try:
396 options, args = getopt.getopt(args, '?hb',
397 [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir=',
398 'dep-target=', 'universal-archs=', 'help' ])
399 except getopt.error, msg:
400 print msg
401 sys.exit(1)
403 if args:
404 print "Additional arguments"
405 sys.exit(1)
407 deptarget = None
408 for k, v in options:
409 if k in ('-h', '-?', '--help'):
410 print USAGE
411 sys.exit(0)
413 elif k in ('-d', '--build-dir'):
414 WORKDIR=v
416 elif k in ('--third-party',):
417 DEPSRC=v
419 elif k in ('--sdk-path',):
420 SDKPATH=v
422 elif k in ('--src-dir',):
423 SRCDIR=v
425 elif k in ('--dep-target', ):
426 DEPTARGET=v
427 deptarget=v
429 elif k in ('--universal-archs', ):
430 if v in UNIVERSALOPTS:
431 UNIVERSALARCHS = v
432 ARCHLIST = universal_opts_map[UNIVERSALARCHS]
433 if deptarget is None:
434 # Select alternate default deployment
435 # target
436 DEPTARGET = default_target_map.get(v, '10.3')
437 else:
438 raise NotImplementedError, v
440 else:
441 raise NotImplementedError, k
443 SRCDIR=os.path.abspath(SRCDIR)
444 WORKDIR=os.path.abspath(WORKDIR)
445 SDKPATH=os.path.abspath(SDKPATH)
446 DEPSRC=os.path.abspath(DEPSRC)
448 CC=target_cc_map[DEPTARGET]
450 print "Settings:"
451 print " * Source directory:", SRCDIR
452 print " * Build directory: ", WORKDIR
453 print " * SDK location: ", SDKPATH
454 print " * Third-party source:", DEPSRC
455 print " * Deployment target:", DEPTARGET
456 print " * Universal architectures:", ARCHLIST
457 print " * C compiler:", CC
458 print ""
463 def extractArchive(builddir, archiveName):
465 Extract a source archive into 'builddir'. Returns the path of the
466 extracted archive.
468 XXX: This function assumes that archives contain a toplevel directory
469 that is has the same name as the basename of the archive. This is
470 save enough for anything we use.
472 curdir = os.getcwd()
473 try:
474 os.chdir(builddir)
475 if archiveName.endswith('.tar.gz'):
476 retval = os.path.basename(archiveName[:-7])
477 if os.path.exists(retval):
478 shutil.rmtree(retval)
479 fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')
481 elif archiveName.endswith('.tar.bz2'):
482 retval = os.path.basename(archiveName[:-8])
483 if os.path.exists(retval):
484 shutil.rmtree(retval)
485 fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')
487 elif archiveName.endswith('.tar'):
488 retval = os.path.basename(archiveName[:-4])
489 if os.path.exists(retval):
490 shutil.rmtree(retval)
491 fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')
493 elif archiveName.endswith('.zip'):
494 retval = os.path.basename(archiveName[:-4])
495 if os.path.exists(retval):
496 shutil.rmtree(retval)
497 fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')
499 data = fp.read()
500 xit = fp.close()
501 if xit is not None:
502 sys.stdout.write(data)
503 raise RuntimeError, "Cannot extract %s"%(archiveName,)
505 return os.path.join(builddir, retval)
507 finally:
508 os.chdir(curdir)
510 KNOWNSIZES = {
511 "http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz": 7952742,
512 "http://downloads.sleepycat.com/db-4.4.20.tar.gz": 2030276,
515 def downloadURL(url, fname):
517 Download the contents of the url into the file.
519 try:
520 size = os.path.getsize(fname)
521 except OSError:
522 pass
523 else:
524 if KNOWNSIZES.get(url) == size:
525 print "Using existing file for", url
526 return
527 fpIn = urllib2.urlopen(url)
528 fpOut = open(fname, 'wb')
529 block = fpIn.read(10240)
530 try:
531 while block:
532 fpOut.write(block)
533 block = fpIn.read(10240)
534 fpIn.close()
535 fpOut.close()
536 except:
537 try:
538 os.unlink(fname)
539 except:
540 pass
542 def buildRecipe(recipe, basedir, archList):
544 Build software using a recipe. This function does the
545 'configure;make;make install' dance for C software, with a possibility
546 to customize this process, basically a poor-mans DarwinPorts.
548 curdir = os.getcwd()
550 name = recipe['name']
551 url = recipe['url']
552 configure = recipe.get('configure', './configure')
553 install = recipe.get('install', 'make && make install DESTDIR=%s'%(
554 shellQuote(basedir)))
556 archiveName = os.path.split(url)[-1]
557 sourceArchive = os.path.join(DEPSRC, archiveName)
559 if not os.path.exists(DEPSRC):
560 os.mkdir(DEPSRC)
563 if os.path.exists(sourceArchive):
564 print "Using local copy of %s"%(name,)
566 else:
567 print "Did not find local copy of %s"%(name,)
568 print "Downloading %s"%(name,)
569 downloadURL(url, sourceArchive)
570 print "Archive for %s stored as %s"%(name, sourceArchive)
572 print "Extracting archive for %s"%(name,)
573 buildDir=os.path.join(WORKDIR, '_bld')
574 if not os.path.exists(buildDir):
575 os.mkdir(buildDir)
577 workDir = extractArchive(buildDir, sourceArchive)
578 os.chdir(workDir)
579 if 'buildDir' in recipe:
580 os.chdir(recipe['buildDir'])
583 for fn in recipe.get('patches', ()):
584 if fn.startswith('http://'):
585 # Download the patch before applying it.
586 path = os.path.join(DEPSRC, os.path.basename(fn))
587 downloadURL(fn, path)
588 fn = path
590 fn = os.path.join(curdir, fn)
591 runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
592 shellQuote(fn),))
594 if configure is not None:
595 configure_args = [
596 "--prefix=/usr/local",
597 "--enable-static",
598 "--disable-shared",
599 #"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
602 if 'configure_pre' in recipe:
603 args = list(recipe['configure_pre'])
604 if '--disable-static' in args:
605 configure_args.remove('--enable-static')
606 if '--enable-shared' in args:
607 configure_args.remove('--disable-shared')
608 configure_args.extend(args)
610 if recipe.get('useLDFlags', 1):
611 configure_args.extend([
612 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
613 ' -arch '.join(archList),
614 shellQuote(SDKPATH)[1:-1],
615 shellQuote(basedir)[1:-1],),
616 "LDFLAGS=-syslibroot,%s -L%s/usr/local/lib -arch %s"%(
617 shellQuote(SDKPATH)[1:-1],
618 shellQuote(basedir)[1:-1],
619 ' -arch '.join(archList)),
621 else:
622 configure_args.extend([
623 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
624 ' -arch '.join(archList),
625 shellQuote(SDKPATH)[1:-1],
626 shellQuote(basedir)[1:-1],),
629 if 'configure_post' in recipe:
630 configure_args = configure_args = list(recipe['configure_post'])
632 configure_args.insert(0, configure)
633 configure_args = [ shellQuote(a) for a in configure_args ]
635 print "Running configure for %s"%(name,)
636 runCommand(' '.join(configure_args) + ' 2>&1')
638 print "Running install for %s"%(name,)
639 runCommand('{ ' + install + ' ;} 2>&1')
641 print "Done %s"%(name,)
642 print ""
644 os.chdir(curdir)
646 def buildLibraries():
648 Build our dependencies into $WORKDIR/libraries/usr/local
650 print ""
651 print "Building required libraries"
652 print ""
653 universal = os.path.join(WORKDIR, 'libraries')
654 os.mkdir(universal)
655 os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
656 os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))
658 for recipe in library_recipes():
659 buildRecipe(recipe, universal, ARCHLIST)
663 def buildPythonDocs():
664 # This stores the documentation as Resources/English.lproj/Documentation
665 # inside the framwork. pydoc and IDLE will pick it up there.
666 print "Install python documentation"
667 rootDir = os.path.join(WORKDIR, '_root')
668 buildDir = os.path.join('../../Doc')
669 docdir = os.path.join(rootDir, 'pydocs')
670 curDir = os.getcwd()
671 os.chdir(buildDir)
672 runCommand('make update')
673 runCommand('make html')
674 os.chdir(curDir)
675 if not os.path.exists(docdir):
676 os.mkdir(docdir)
677 os.rename(os.path.join(buildDir, 'build', 'html'), docdir)
680 def buildPython():
681 print "Building a universal python for %s architectures" % UNIVERSALARCHS
683 buildDir = os.path.join(WORKDIR, '_bld', 'python')
684 rootDir = os.path.join(WORKDIR, '_root')
686 if os.path.exists(buildDir):
687 shutil.rmtree(buildDir)
688 if os.path.exists(rootDir):
689 shutil.rmtree(rootDir)
690 os.mkdir(buildDir)
691 os.mkdir(rootDir)
692 os.mkdir(os.path.join(rootDir, 'empty-dir'))
693 curdir = os.getcwd()
694 os.chdir(buildDir)
696 # Not sure if this is still needed, the original build script
697 # claims that parts of the install assume python.exe exists.
698 os.symlink('python', os.path.join(buildDir, 'python.exe'))
700 # Extract the version from the configure file, needed to calculate
701 # several paths.
702 version = getVersion()
704 # Since the extra libs are not in their installed framework location
705 # during the build, augment the library path so that the interpreter
706 # will find them during its extension import sanity checks.
707 os.environ['DYLD_LIBRARY_PATH'] = os.path.join(WORKDIR,
708 'libraries', 'usr', 'local', 'lib')
709 print "Running configure..."
710 runCommand("%s -C --enable-framework --enable-universalsdk=%s "
711 "--with-universal-archs=%s "
712 "LDFLAGS='-g -L%s/libraries/usr/local/lib' "
713 "OPT='-g -O3 -I%s/libraries/usr/local/include' 2>&1"%(
714 shellQuote(os.path.join(SRCDIR, 'configure')), shellQuote(SDKPATH),
715 UNIVERSALARCHS,
716 shellQuote(WORKDIR)[1:-1],
717 shellQuote(WORKDIR)[1:-1]))
719 print "Running make"
720 runCommand("make")
722 print "Running make frameworkinstall"
723 runCommand("make frameworkinstall DESTDIR=%s"%(
724 shellQuote(rootDir)))
726 print "Running make frameworkinstallextras"
727 runCommand("make frameworkinstallextras DESTDIR=%s"%(
728 shellQuote(rootDir)))
730 del os.environ['DYLD_LIBRARY_PATH']
731 print "Copying required shared libraries"
732 if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
733 runCommand("mv %s/* %s"%(
734 shellQuote(os.path.join(
735 WORKDIR, 'libraries', 'Library', 'Frameworks',
736 'Python.framework', 'Versions', getVersion(),
737 'lib')),
738 shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
739 'Python.framework', 'Versions', getVersion(),
740 'lib'))))
742 print "Fix file modes"
743 frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
744 gid = grp.getgrnam('admin').gr_gid
746 for dirpath, dirnames, filenames in os.walk(frmDir):
747 for dn in dirnames:
748 os.chmod(os.path.join(dirpath, dn), 0775)
749 os.chown(os.path.join(dirpath, dn), -1, gid)
752 for fn in filenames:
753 if os.path.islink(fn):
754 continue
756 # "chmod g+w $fn"
757 p = os.path.join(dirpath, fn)
758 st = os.stat(p)
759 os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
760 os.chown(p, -1, gid)
762 # We added some directories to the search path during the configure
763 # phase. Remove those because those directories won't be there on
764 # the end-users system.
765 path =os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework',
766 'Versions', version, 'lib', 'python%s'%(version,),
767 'config', 'Makefile')
768 fp = open(path, 'r')
769 data = fp.read()
770 fp.close()
772 data = data.replace('-L%s/libraries/usr/local/lib'%(WORKDIR,), '')
773 data = data.replace('-I%s/libraries/usr/local/include'%(WORKDIR,), '')
774 fp = open(path, 'w')
775 fp.write(data)
776 fp.close()
778 # Add symlinks in /usr/local/bin, using relative links
779 usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
780 to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
781 'Python.framework', 'Versions', version, 'bin')
782 if os.path.exists(usr_local_bin):
783 shutil.rmtree(usr_local_bin)
784 os.makedirs(usr_local_bin)
785 for fn in os.listdir(
786 os.path.join(frmDir, 'Versions', version, 'bin')):
787 os.symlink(os.path.join(to_framework, fn),
788 os.path.join(usr_local_bin, fn))
790 os.chdir(curdir)
794 def patchFile(inPath, outPath):
795 data = fileContents(inPath)
796 data = data.replace('$FULL_VERSION', getFullVersion())
797 data = data.replace('$VERSION', getVersion())
798 data = data.replace('$MACOSX_DEPLOYMENT_TARGET', ''.join((DEPTARGET, ' or later')))
799 data = data.replace('$ARCHITECTURES', "i386, ppc")
800 data = data.replace('$INSTALL_SIZE', installSize())
802 # This one is not handy as a template variable
803 data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
804 fp = open(outPath, 'wb')
805 fp.write(data)
806 fp.close()
808 def patchScript(inPath, outPath):
809 data = fileContents(inPath)
810 data = data.replace('@PYVER@', getVersion())
811 fp = open(outPath, 'wb')
812 fp.write(data)
813 fp.close()
814 os.chmod(outPath, 0755)
818 def packageFromRecipe(targetDir, recipe):
819 curdir = os.getcwd()
820 try:
821 # The major version (such as 2.5) is included in the package name
822 # because having two version of python installed at the same time is
823 # common.
824 pkgname = '%s-%s'%(recipe['name'], getVersion())
825 srcdir = recipe.get('source')
826 pkgroot = recipe.get('topdir', srcdir)
827 postflight = recipe.get('postflight')
828 readme = textwrap.dedent(recipe['readme'])
829 isRequired = recipe.get('required', True)
831 print "- building package %s"%(pkgname,)
833 # Substitute some variables
834 textvars = dict(
835 VER=getVersion(),
836 FULLVER=getFullVersion(),
838 readme = readme % textvars
840 if pkgroot is not None:
841 pkgroot = pkgroot % textvars
842 else:
843 pkgroot = '/'
845 if srcdir is not None:
846 srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
847 srcdir = srcdir % textvars
849 if postflight is not None:
850 postflight = os.path.abspath(postflight)
852 packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
853 os.makedirs(packageContents)
855 if srcdir is not None:
856 os.chdir(srcdir)
857 runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
858 runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
859 runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
861 fn = os.path.join(packageContents, 'PkgInfo')
862 fp = open(fn, 'w')
863 fp.write('pmkrpkg1')
864 fp.close()
866 rsrcDir = os.path.join(packageContents, "Resources")
867 os.mkdir(rsrcDir)
868 fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
869 fp.write(readme)
870 fp.close()
872 if postflight is not None:
873 patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
875 vers = getFullVersion()
876 major, minor = map(int, getVersion().split('.', 2))
877 pl = Plist(
878 CFBundleGetInfoString="Python.%s %s"%(pkgname, vers,),
879 CFBundleIdentifier='org.python.Python.%s'%(pkgname,),
880 CFBundleName='Python.%s'%(pkgname,),
881 CFBundleShortVersionString=vers,
882 IFMajorVersion=major,
883 IFMinorVersion=minor,
884 IFPkgFormatVersion=0.10000000149011612,
885 IFPkgFlagAllowBackRev=False,
886 IFPkgFlagAuthorizationAction="RootAuthorization",
887 IFPkgFlagDefaultLocation=pkgroot,
888 IFPkgFlagFollowLinks=True,
889 IFPkgFlagInstallFat=True,
890 IFPkgFlagIsRequired=isRequired,
891 IFPkgFlagOverwritePermissions=False,
892 IFPkgFlagRelocatable=False,
893 IFPkgFlagRestartAction="NoRestart",
894 IFPkgFlagRootVolumeOnly=True,
895 IFPkgFlagUpdateInstalledLangauges=False,
897 writePlist(pl, os.path.join(packageContents, 'Info.plist'))
899 pl = Plist(
900 IFPkgDescriptionDescription=readme,
901 IFPkgDescriptionTitle=recipe.get('long_name', "Python.%s"%(pkgname,)),
902 IFPkgDescriptionVersion=vers,
904 writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
906 finally:
907 os.chdir(curdir)
910 def makeMpkgPlist(path):
912 vers = getFullVersion()
913 major, minor = map(int, getVersion().split('.', 2))
915 pl = Plist(
916 CFBundleGetInfoString="Python %s"%(vers,),
917 CFBundleIdentifier='org.python.Python',
918 CFBundleName='Python',
919 CFBundleShortVersionString=vers,
920 IFMajorVersion=major,
921 IFMinorVersion=minor,
922 IFPkgFlagComponentDirectory="Contents/Packages",
923 IFPkgFlagPackageList=[
924 dict(
925 IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
926 IFPkgFlagPackageSelection='selected'
928 for item in pkg_recipes()
930 IFPkgFormatVersion=0.10000000149011612,
931 IFPkgFlagBackgroundScaling="proportional",
932 IFPkgFlagBackgroundAlignment="left",
933 IFPkgFlagAuthorizationAction="RootAuthorization",
936 writePlist(pl, path)
939 def buildInstaller():
941 # Zap all compiled files
942 for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
943 for fn in filenames:
944 if fn.endswith('.pyc') or fn.endswith('.pyo'):
945 os.unlink(os.path.join(dirpath, fn))
947 outdir = os.path.join(WORKDIR, 'installer')
948 if os.path.exists(outdir):
949 shutil.rmtree(outdir)
950 os.mkdir(outdir)
952 pkgroot = os.path.join(outdir, 'Python.mpkg', 'Contents')
953 pkgcontents = os.path.join(pkgroot, 'Packages')
954 os.makedirs(pkgcontents)
955 for recipe in pkg_recipes():
956 packageFromRecipe(pkgcontents, recipe)
958 rsrcDir = os.path.join(pkgroot, 'Resources')
960 fn = os.path.join(pkgroot, 'PkgInfo')
961 fp = open(fn, 'w')
962 fp.write('pmkrpkg1')
963 fp.close()
965 os.mkdir(rsrcDir)
967 makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
968 pl = Plist(
969 IFPkgDescriptionTitle="Python",
970 IFPkgDescriptionVersion=getVersion(),
973 writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
974 for fn in os.listdir('resources'):
975 if fn == '.svn': continue
976 if fn.endswith('.jpg'):
977 shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
978 else:
979 patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
981 shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
984 def installSize(clear=False, _saved=[]):
985 if clear:
986 del _saved[:]
987 if not _saved:
988 data = captureCommand("du -ks %s"%(
989 shellQuote(os.path.join(WORKDIR, '_root'))))
990 _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
991 return _saved[0]
994 def buildDMG():
996 Create DMG containing the rootDir.
998 outdir = os.path.join(WORKDIR, 'diskimage')
999 if os.path.exists(outdir):
1000 shutil.rmtree(outdir)
1002 imagepath = os.path.join(outdir,
1003 'python-%s-macosx%s'%(getFullVersion(),DEPTARGET))
1004 if INCLUDE_TIMESTAMP:
1005 imagepath = imagepath + '-%04d-%02d-%02d'%(time.localtime()[:3])
1006 imagepath = imagepath + '.dmg'
1008 os.mkdir(outdir)
1009 volname='Python %s'%(getFullVersion())
1010 runCommand("hdiutil create -format UDRW -volname %s -srcfolder %s %s"%(
1011 shellQuote(volname),
1012 shellQuote(os.path.join(WORKDIR, 'installer')),
1013 shellQuote(imagepath + ".tmp.dmg" )))
1016 if not os.path.exists(os.path.join(WORKDIR, "mnt")):
1017 os.mkdir(os.path.join(WORKDIR, "mnt"))
1018 runCommand("hdiutil attach %s -mountroot %s"%(
1019 shellQuote(imagepath + ".tmp.dmg"), shellQuote(os.path.join(WORKDIR, "mnt"))))
1021 # Custom icon for the DMG, shown when the DMG is mounted.
1022 shutil.copy("../Icons/Disk Image.icns",
1023 os.path.join(WORKDIR, "mnt", volname, ".VolumeIcon.icns"))
1024 runCommand("/Developer/Tools/SetFile -a C %s/"%(
1025 shellQuote(os.path.join(WORKDIR, "mnt", volname)),))
1027 runCommand("hdiutil detach %s"%(shellQuote(os.path.join(WORKDIR, "mnt", volname))))
1029 setIcon(imagepath + ".tmp.dmg", "../Icons/Disk Image.icns")
1030 runCommand("hdiutil convert %s -format UDZO -o %s"%(
1031 shellQuote(imagepath + ".tmp.dmg"), shellQuote(imagepath)))
1032 setIcon(imagepath, "../Icons/Disk Image.icns")
1034 os.unlink(imagepath + ".tmp.dmg")
1036 return imagepath
1039 def setIcon(filePath, icnsPath):
1041 Set the custom icon for the specified file or directory.
1044 toolPath = os.path.join(os.path.dirname(__file__), "seticon.app/Contents/MacOS/seticon")
1045 dirPath = os.path.dirname(__file__)
1046 if not os.path.exists(toolPath) or os.stat(toolPath).st_mtime < os.stat(dirPath + '/seticon.m').st_mtime:
1047 # NOTE: The tool is created inside an .app bundle, otherwise it won't work due
1048 # to connections to the window server.
1049 if not os.path.exists('seticon.app/Contents/MacOS'):
1050 os.makedirs('seticon.app/Contents/MacOS')
1051 runCommand("cc -o %s %s/seticon.m -framework Cocoa"%(
1052 shellQuote(toolPath), shellQuote(dirPath)))
1054 runCommand("%s %s %s"%(shellQuote(os.path.abspath(toolPath)), shellQuote(icnsPath),
1055 shellQuote(filePath)))
1057 def main():
1058 # First parse options and check if we can perform our work
1059 parseOptions()
1060 checkEnvironment()
1062 os.environ['MACOSX_DEPLOYMENT_TARGET'] = DEPTARGET
1063 os.environ['CC'] = CC
1065 if os.path.exists(WORKDIR):
1066 shutil.rmtree(WORKDIR)
1067 os.mkdir(WORKDIR)
1069 # Then build third-party libraries such as sleepycat DB4.
1070 buildLibraries()
1072 # Now build python itself
1073 buildPython()
1075 # And then build the documentation
1076 # Remove the Deployment Target from the shell
1077 # environment, it's no longer needed and
1078 # an unexpected build target can cause problems
1079 # when Sphinx and its dependencies need to
1080 # be (re-)installed.
1081 del os.environ['MACOSX_DEPLOYMENT_TARGET']
1082 buildPythonDocs()
1085 # Prepare the applications folder
1086 fn = os.path.join(WORKDIR, "_root", "Applications",
1087 "Python %s"%(getVersion(),), "Update Shell Profile.command")
1088 patchScript("scripts/postflight.patch-profile", fn)
1090 folder = os.path.join(WORKDIR, "_root", "Applications", "Python %s"%(
1091 getVersion(),))
1092 os.chmod(folder, 0755)
1093 setIcon(folder, "../Icons/Python Folder.icns")
1095 # Create the installer
1096 buildInstaller()
1098 # And copy the readme into the directory containing the installer
1099 patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
1101 # Ditto for the license file.
1102 shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
1104 fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
1105 print >> fp, "# BUILD INFO"
1106 print >> fp, "# Date:", time.ctime()
1107 print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
1108 fp.close()
1110 # And copy it to a DMG
1111 buildDMG()
1113 if __name__ == "__main__":
1114 main()