Merged revisions 79119 via svnmerge from
[python/dscho.git] / Mac / BuildScript / build-installer.py
blob8f58dbbdac986a1bf806c3e603639c2a96a6f7de
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]
43 raise RuntimeError, "Cannot find variable %s" % variable[:-1]
45 def getVersion():
46 return grepValue(os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
48 def getVersionTuple():
49 return tuple([int(n) for n in getVersion().split('.')])
51 def 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 raise RuntimeError, "Cannot find full version??"
58 # The directory we'll use to create the build (will be erased and recreated)
59 WORKDIR = "/tmp/_py"
61 # The directory we'll use to store third-party sources. Set this to something
62 # else if you don't want to re-fetch required libraries every time.
63 DEPSRC = os.path.join(WORKDIR, 'third-party')
64 DEPSRC = os.path.expanduser('~/Universal/other-sources')
66 # Location of the preferred SDK
68 ### There are some issues with the SDK selection below here,
69 ### The resulting binary doesn't work on all platforms that
70 ### it should. Always default to the 10.4u SDK until that
71 ### isue is resolved.
72 ###
73 ##if int(os.uname()[2].split('.')[0]) == 8:
74 ## # Explicitly use the 10.4u (universal) SDK when
75 ## # building on 10.4, the system headers are not
76 ## # useable for a universal build
77 ## SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
78 ##else:
79 ## SDKPATH = "/"
81 SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
83 universal_opts_map = { '32-bit': ('i386', 'ppc',),
84 '64-bit': ('x86_64', 'ppc64',),
85 'intel': ('i386', 'x86_64'),
86 '3-way': ('ppc', 'i386', 'x86_64'),
87 'all': ('i386', 'ppc', 'x86_64', 'ppc64',) }
88 default_target_map = {
89 '64-bit': '10.5',
90 '3-way': '10.5',
91 'intel': '10.5',
92 'all': '10.5',
95 UNIVERSALOPTS = tuple(universal_opts_map.keys())
97 UNIVERSALARCHS = '32-bit'
99 ARCHLIST = universal_opts_map[UNIVERSALARCHS]
101 # Source directory (asume we're in Mac/BuildScript)
102 SRCDIR = os.path.dirname(
103 os.path.dirname(
104 os.path.dirname(
105 os.path.abspath(__file__
106 ))))
108 # $MACOSX_DEPLOYMENT_TARGET -> minimum OS X level
109 DEPTARGET = '10.3'
111 target_cc_map = {
112 '10.3': 'gcc-4.0',
113 '10.4': 'gcc-4.0',
114 '10.5': 'gcc-4.0',
115 '10.6': 'gcc-4.2',
118 CC = target_cc_map[DEPTARGET]
120 PYTHON_3 = getVersionTuple() >= (3, 0)
122 USAGE = textwrap.dedent("""\
123 Usage: build_python [options]
125 Options:
126 -? or -h: Show this message
127 -b DIR
128 --build-dir=DIR: Create build here (default: %(WORKDIR)r)
129 --third-party=DIR: Store third-party sources here (default: %(DEPSRC)r)
130 --sdk-path=DIR: Location of the SDK (default: %(SDKPATH)r)
131 --src-dir=DIR: Location of the Python sources (default: %(SRCDIR)r)
132 --dep-target=10.n OS X deployment target (default: %(DEPTARGET)r)
133 --universal-archs=x universal architectures (options: %(UNIVERSALOPTS)r, default: %(UNIVERSALARCHS)r)
134 """)% globals()
137 # Instructions for building libraries that are necessary for building a
138 # batteries included python.
139 # [The recipes are defined here for convenience but instantiated later after
140 # command line options have been processed.]
141 def library_recipes():
142 result = []
144 if DEPTARGET < '10.5':
145 result.extend([
146 dict(
147 name="Bzip2 1.0.5",
148 url="http://www.bzip.org/1.0.5/bzip2-1.0.5.tar.gz",
149 checksum='3c15a0c8d1d3ee1c46a1634d00617b1a',
150 configure=None,
151 install='make install CC=%s PREFIX=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
153 shellQuote(os.path.join(WORKDIR, 'libraries')),
154 ' -arch '.join(ARCHLIST),
155 SDKPATH,
158 dict(
159 name="ZLib 1.2.3",
160 url="http://www.gzip.org/zlib/zlib-1.2.3.tar.gz",
161 checksum='debc62758716a169df9f62e6ab2bc634',
162 configure=None,
163 install='make install CC=%s prefix=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
165 shellQuote(os.path.join(WORKDIR, 'libraries')),
166 ' -arch '.join(ARCHLIST),
167 SDKPATH,
170 dict(
171 # Note that GNU readline is GPL'd software
172 name="GNU Readline 5.1.4",
173 url="http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz" ,
174 checksum='7ee5a692db88b30ca48927a13fd60e46',
175 patchlevel='0',
176 patches=[
177 # The readline maintainers don't do actual micro releases, but
178 # just ship a set of patches.
179 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-001',
180 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-002',
181 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-003',
182 'http://ftp.gnu.org/pub/gnu/readline/readline-5.1-patches/readline51-004',
185 dict(
186 name="SQLite 3.6.11",
187 url="http://www.sqlite.org/sqlite-3.6.11.tar.gz",
188 checksum='7ebb099696ab76cc6ff65dd496d17858',
189 configure_pre=[
190 '--enable-threadsafe',
191 '--enable-tempstore',
192 '--enable-shared=no',
193 '--enable-static=yes',
194 '--disable-tcl',
197 dict(
198 name="NCurses 5.5",
199 url="http://ftp.gnu.org/pub/gnu/ncurses/ncurses-5.5.tar.gz",
200 checksum='e73c1ac10b4bfc46db43b2ddfd6244ef',
201 configure_pre=[
202 "--without-cxx",
203 "--without-ada",
204 "--without-progs",
205 "--without-curses-h",
206 "--enable-shared",
207 "--with-shared",
208 "--datadir=/usr/share",
209 "--sysconfdir=/etc",
210 "--sharedstatedir=/usr/com",
211 "--with-terminfo-dirs=/usr/share/terminfo",
212 "--with-default-terminfo-dir=/usr/share/terminfo",
213 "--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib"%(getVersion(),),
214 "--enable-termcap",
216 patches=[
217 "ncurses-5.5.patch",
219 useLDFlags=False,
220 install='make && make install DESTDIR=%s && cd %s/usr/local/lib && ln -fs ../../../Library/Frameworks/Python.framework/Versions/%s/lib/lib* .'%(
221 shellQuote(os.path.join(WORKDIR, 'libraries')),
222 shellQuote(os.path.join(WORKDIR, 'libraries')),
223 getVersion(),
228 result.extend([
229 dict(
230 name="Sleepycat DB 4.7.25",
231 url="http://download.oracle.com/berkeley-db/db-4.7.25.tar.gz",
232 checksum='ec2b87e833779681a0c3a814aa71359e',
233 buildDir="build_unix",
234 configure="../dist/configure",
235 configure_pre=[
236 '--includedir=/usr/local/include/db4',
241 return result
244 # Instructions for building packages inside the .mpkg.
245 def pkg_recipes():
246 unselected_for_python3 = ('selected', 'unselected')[PYTHON_3]
247 result = [
248 dict(
249 name="PythonFramework",
250 long_name="Python Framework",
251 source="/Library/Frameworks/Python.framework",
252 readme="""\
253 This package installs Python.framework, that is the python
254 interpreter and the standard library. This also includes Python
255 wrappers for lots of Mac OS X API's.
256 """,
257 postflight="scripts/postflight.framework",
258 selected='selected',
260 dict(
261 name="PythonApplications",
262 long_name="GUI Applications",
263 source="/Applications/Python %(VER)s",
264 readme="""\
265 This package installs IDLE (an interactive Python IDE),
266 Python Launcher and Build Applet (create application bundles
267 from python scripts).
269 It also installs a number of examples and demos.
270 """,
271 required=False,
272 selected='selected',
274 dict(
275 name="PythonUnixTools",
276 long_name="UNIX command-line tools",
277 source="/usr/local/bin",
278 readme="""\
279 This package installs the unix tools in /usr/local/bin for
280 compatibility with older releases of Python. This package
281 is not necessary to use Python.
282 """,
283 required=False,
284 selected='selected',
286 dict(
287 name="PythonDocumentation",
288 long_name="Python Documentation",
289 topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
290 source="/pydocs",
291 readme="""\
292 This package installs the python documentation at a location
293 that is useable for pydoc and IDLE. If you have installed Xcode
294 it will also install a link to the documentation in
295 /Developer/Documentation/Python
296 """,
297 postflight="scripts/postflight.documentation",
298 required=False,
299 selected='selected',
301 dict(
302 name="PythonProfileChanges",
303 long_name="Shell profile updater",
304 readme="""\
305 This packages updates your shell profile to make sure that
306 the Python tools are found by your shell in preference of
307 the system provided Python tools.
309 If you don't install this package you'll have to add
310 "/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
311 to your PATH by hand.
312 """,
313 postflight="scripts/postflight.patch-profile",
314 topdir="/Library/Frameworks/Python.framework",
315 source="/empty-dir",
316 required=False,
317 selected=unselected_for_python3,
321 if DEPTARGET < '10.4':
322 result.append(
323 dict(
324 name="PythonSystemFixes",
325 long_name="Fix system Python",
326 readme="""\
327 This package updates the system python installation on
328 Mac OS X 10.3 to ensure that you can build new python extensions
329 using that copy of python after installing this version.
330 """,
331 postflight="../Tools/fixapplepython23.py",
332 topdir="/Library/Frameworks/Python.framework",
333 source="/empty-dir",
334 required=False,
335 selected=unselected_for_python3,
338 return result
340 def fatal(msg):
342 A fatal error, bail out.
344 sys.stderr.write('FATAL: ')
345 sys.stderr.write(msg)
346 sys.stderr.write('\n')
347 sys.exit(1)
349 def fileContents(fn):
351 Return the contents of the named file
353 return open(fn, 'rb').read()
355 def runCommand(commandline):
357 Run a command and raise RuntimeError if it fails. Output is surpressed
358 unless the command fails.
360 fd = os.popen(commandline, 'r')
361 data = fd.read()
362 xit = fd.close()
363 if xit is not None:
364 sys.stdout.write(data)
365 raise RuntimeError, "command failed: %s"%(commandline,)
367 if VERBOSE:
368 sys.stdout.write(data); sys.stdout.flush()
370 def captureCommand(commandline):
371 fd = os.popen(commandline, 'r')
372 data = fd.read()
373 xit = fd.close()
374 if xit is not None:
375 sys.stdout.write(data)
376 raise RuntimeError, "command failed: %s"%(commandline,)
378 return data
380 def checkEnvironment():
382 Check that we're running on a supported system.
385 if platform.system() != 'Darwin':
386 fatal("This script should be run on a Mac OS X 10.4 (or later) system")
388 if int(platform.release().split('.')[0]) < 8:
389 fatal("This script should be run on a Mac OS X 10.4 (or later) system")
391 if not os.path.exists(SDKPATH):
392 fatal("Please install the latest version of Xcode and the %s SDK"%(
393 os.path.basename(SDKPATH[:-4])))
397 def parseOptions(args=None):
399 Parse arguments and update global settings.
401 global WORKDIR, DEPSRC, SDKPATH, SRCDIR, DEPTARGET
402 global UNIVERSALOPTS, UNIVERSALARCHS, ARCHLIST, CC
404 if args is None:
405 args = sys.argv[1:]
407 try:
408 options, args = getopt.getopt(args, '?hb',
409 [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir=',
410 'dep-target=', 'universal-archs=', 'help' ])
411 except getopt.error, msg:
412 print msg
413 sys.exit(1)
415 if args:
416 print "Additional arguments"
417 sys.exit(1)
419 deptarget = None
420 for k, v in options:
421 if k in ('-h', '-?', '--help'):
422 print USAGE
423 sys.exit(0)
425 elif k in ('-d', '--build-dir'):
426 WORKDIR=v
428 elif k in ('--third-party',):
429 DEPSRC=v
431 elif k in ('--sdk-path',):
432 SDKPATH=v
434 elif k in ('--src-dir',):
435 SRCDIR=v
437 elif k in ('--dep-target', ):
438 DEPTARGET=v
439 deptarget=v
441 elif k in ('--universal-archs', ):
442 if v in UNIVERSALOPTS:
443 UNIVERSALARCHS = v
444 ARCHLIST = universal_opts_map[UNIVERSALARCHS]
445 if deptarget is None:
446 # Select alternate default deployment
447 # target
448 DEPTARGET = default_target_map.get(v, '10.3')
449 else:
450 raise NotImplementedError, v
452 else:
453 raise NotImplementedError, k
455 SRCDIR=os.path.abspath(SRCDIR)
456 WORKDIR=os.path.abspath(WORKDIR)
457 SDKPATH=os.path.abspath(SDKPATH)
458 DEPSRC=os.path.abspath(DEPSRC)
460 CC=target_cc_map[DEPTARGET]
462 print "Settings:"
463 print " * Source directory:", SRCDIR
464 print " * Build directory: ", WORKDIR
465 print " * SDK location: ", SDKPATH
466 print " * Third-party source:", DEPSRC
467 print " * Deployment target:", DEPTARGET
468 print " * Universal architectures:", ARCHLIST
469 print " * C compiler:", CC
470 print ""
475 def extractArchive(builddir, archiveName):
477 Extract a source archive into 'builddir'. Returns the path of the
478 extracted archive.
480 XXX: This function assumes that archives contain a toplevel directory
481 that is has the same name as the basename of the archive. This is
482 save enough for anything we use.
484 curdir = os.getcwd()
485 try:
486 os.chdir(builddir)
487 if archiveName.endswith('.tar.gz'):
488 retval = os.path.basename(archiveName[:-7])
489 if os.path.exists(retval):
490 shutil.rmtree(retval)
491 fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')
493 elif archiveName.endswith('.tar.bz2'):
494 retval = os.path.basename(archiveName[:-8])
495 if os.path.exists(retval):
496 shutil.rmtree(retval)
497 fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')
499 elif archiveName.endswith('.tar'):
500 retval = os.path.basename(archiveName[:-4])
501 if os.path.exists(retval):
502 shutil.rmtree(retval)
503 fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')
505 elif archiveName.endswith('.zip'):
506 retval = os.path.basename(archiveName[:-4])
507 if os.path.exists(retval):
508 shutil.rmtree(retval)
509 fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')
511 data = fp.read()
512 xit = fp.close()
513 if xit is not None:
514 sys.stdout.write(data)
515 raise RuntimeError, "Cannot extract %s"%(archiveName,)
517 return os.path.join(builddir, retval)
519 finally:
520 os.chdir(curdir)
522 KNOWNSIZES = {
523 "http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz": 7952742,
524 "http://downloads.sleepycat.com/db-4.4.20.tar.gz": 2030276,
527 def downloadURL(url, fname):
529 Download the contents of the url into the file.
531 try:
532 size = os.path.getsize(fname)
533 except OSError:
534 pass
535 else:
536 if KNOWNSIZES.get(url) == size:
537 print "Using existing file for", url
538 return
539 fpIn = urllib2.urlopen(url)
540 fpOut = open(fname, 'wb')
541 block = fpIn.read(10240)
542 try:
543 while block:
544 fpOut.write(block)
545 block = fpIn.read(10240)
546 fpIn.close()
547 fpOut.close()
548 except:
549 try:
550 os.unlink(fname)
551 except:
552 pass
554 def buildRecipe(recipe, basedir, archList):
556 Build software using a recipe. This function does the
557 'configure;make;make install' dance for C software, with a possibility
558 to customize this process, basically a poor-mans DarwinPorts.
560 curdir = os.getcwd()
562 name = recipe['name']
563 url = recipe['url']
564 configure = recipe.get('configure', './configure')
565 install = recipe.get('install', 'make && make install DESTDIR=%s'%(
566 shellQuote(basedir)))
568 archiveName = os.path.split(url)[-1]
569 sourceArchive = os.path.join(DEPSRC, archiveName)
571 if not os.path.exists(DEPSRC):
572 os.mkdir(DEPSRC)
575 if os.path.exists(sourceArchive):
576 print "Using local copy of %s"%(name,)
578 else:
579 print "Did not find local copy of %s"%(name,)
580 print "Downloading %s"%(name,)
581 downloadURL(url, sourceArchive)
582 print "Archive for %s stored as %s"%(name, sourceArchive)
584 print "Extracting archive for %s"%(name,)
585 buildDir=os.path.join(WORKDIR, '_bld')
586 if not os.path.exists(buildDir):
587 os.mkdir(buildDir)
589 workDir = extractArchive(buildDir, sourceArchive)
590 os.chdir(workDir)
591 if 'buildDir' in recipe:
592 os.chdir(recipe['buildDir'])
595 for fn in recipe.get('patches', ()):
596 if fn.startswith('http://'):
597 # Download the patch before applying it.
598 path = os.path.join(DEPSRC, os.path.basename(fn))
599 downloadURL(fn, path)
600 fn = path
602 fn = os.path.join(curdir, fn)
603 runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
604 shellQuote(fn),))
606 if configure is not None:
607 configure_args = [
608 "--prefix=/usr/local",
609 "--enable-static",
610 "--disable-shared",
611 #"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
614 if 'configure_pre' in recipe:
615 args = list(recipe['configure_pre'])
616 if '--disable-static' in args:
617 configure_args.remove('--enable-static')
618 if '--enable-shared' in args:
619 configure_args.remove('--disable-shared')
620 configure_args.extend(args)
622 if recipe.get('useLDFlags', 1):
623 configure_args.extend([
624 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
625 ' -arch '.join(archList),
626 shellQuote(SDKPATH)[1:-1],
627 shellQuote(basedir)[1:-1],),
628 "LDFLAGS=-syslibroot,%s -L%s/usr/local/lib -arch %s"%(
629 shellQuote(SDKPATH)[1:-1],
630 shellQuote(basedir)[1:-1],
631 ' -arch '.join(archList)),
633 else:
634 configure_args.extend([
635 "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
636 ' -arch '.join(archList),
637 shellQuote(SDKPATH)[1:-1],
638 shellQuote(basedir)[1:-1],),
641 if 'configure_post' in recipe:
642 configure_args = configure_args = list(recipe['configure_post'])
644 configure_args.insert(0, configure)
645 configure_args = [ shellQuote(a) for a in configure_args ]
647 print "Running configure for %s"%(name,)
648 runCommand(' '.join(configure_args) + ' 2>&1')
650 print "Running install for %s"%(name,)
651 runCommand('{ ' + install + ' ;} 2>&1')
653 print "Done %s"%(name,)
654 print ""
656 os.chdir(curdir)
658 def buildLibraries():
660 Build our dependencies into $WORKDIR/libraries/usr/local
662 print ""
663 print "Building required libraries"
664 print ""
665 universal = os.path.join(WORKDIR, 'libraries')
666 os.mkdir(universal)
667 os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
668 os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))
670 for recipe in library_recipes():
671 buildRecipe(recipe, universal, ARCHLIST)
675 def buildPythonDocs():
676 # This stores the documentation as Resources/English.lproj/Documentation
677 # inside the framwork. pydoc and IDLE will pick it up there.
678 print "Install python documentation"
679 rootDir = os.path.join(WORKDIR, '_root')
680 buildDir = os.path.join('../../Doc')
681 docdir = os.path.join(rootDir, 'pydocs')
682 curDir = os.getcwd()
683 os.chdir(buildDir)
684 runCommand('make update')
685 runCommand('make html')
686 os.chdir(curDir)
687 if not os.path.exists(docdir):
688 os.mkdir(docdir)
689 os.rename(os.path.join(buildDir, 'build', 'html'), docdir)
692 def buildPython():
693 print "Building a universal python for %s architectures" % UNIVERSALARCHS
695 buildDir = os.path.join(WORKDIR, '_bld', 'python')
696 rootDir = os.path.join(WORKDIR, '_root')
698 if os.path.exists(buildDir):
699 shutil.rmtree(buildDir)
700 if os.path.exists(rootDir):
701 shutil.rmtree(rootDir)
702 os.mkdir(buildDir)
703 os.mkdir(rootDir)
704 os.mkdir(os.path.join(rootDir, 'empty-dir'))
705 curdir = os.getcwd()
706 os.chdir(buildDir)
708 # Not sure if this is still needed, the original build script
709 # claims that parts of the install assume python.exe exists.
710 os.symlink('python', os.path.join(buildDir, 'python.exe'))
712 # Extract the version from the configure file, needed to calculate
713 # several paths.
714 version = getVersion()
716 # Since the extra libs are not in their installed framework location
717 # during the build, augment the library path so that the interpreter
718 # will find them during its extension import sanity checks.
719 os.environ['DYLD_LIBRARY_PATH'] = os.path.join(WORKDIR,
720 'libraries', 'usr', 'local', 'lib')
721 print "Running configure..."
722 runCommand("%s -C --enable-framework --enable-universalsdk=%s "
723 "--with-universal-archs=%s "
724 "%s "
725 "LDFLAGS='-g -L%s/libraries/usr/local/lib' "
726 "OPT='-g -O3 -I%s/libraries/usr/local/include' 2>&1"%(
727 shellQuote(os.path.join(SRCDIR, 'configure')), shellQuote(SDKPATH),
728 UNIVERSALARCHS,
729 (' ', '--with-computed-gotos ')[PYTHON_3],
730 shellQuote(WORKDIR)[1:-1],
731 shellQuote(WORKDIR)[1:-1]))
733 print "Running make"
734 runCommand("make")
736 print "Running make install"
737 runCommand("make install DESTDIR=%s"%(
738 shellQuote(rootDir)))
740 print "Running make frameworkinstallextras"
741 runCommand("make frameworkinstallextras DESTDIR=%s"%(
742 shellQuote(rootDir)))
744 del os.environ['DYLD_LIBRARY_PATH']
745 print "Copying required shared libraries"
746 if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
747 runCommand("mv %s/* %s"%(
748 shellQuote(os.path.join(
749 WORKDIR, 'libraries', 'Library', 'Frameworks',
750 'Python.framework', 'Versions', getVersion(),
751 'lib')),
752 shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
753 'Python.framework', 'Versions', getVersion(),
754 'lib'))))
756 print "Fix file modes"
757 frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
758 gid = grp.getgrnam('admin').gr_gid
760 for dirpath, dirnames, filenames in os.walk(frmDir):
761 for dn in dirnames:
762 os.chmod(os.path.join(dirpath, dn), 0775)
763 os.chown(os.path.join(dirpath, dn), -1, gid)
766 for fn in filenames:
767 if os.path.islink(fn):
768 continue
770 # "chmod g+w $fn"
771 p = os.path.join(dirpath, fn)
772 st = os.stat(p)
773 os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
774 os.chown(p, -1, gid)
776 # We added some directories to the search path during the configure
777 # phase. Remove those because those directories won't be there on
778 # the end-users system.
779 path =os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework',
780 'Versions', version, 'lib', 'python%s'%(version,),
781 'config', 'Makefile')
782 fp = open(path, 'r')
783 data = fp.read()
784 fp.close()
786 data = data.replace('-L%s/libraries/usr/local/lib'%(WORKDIR,), '')
787 data = data.replace('-I%s/libraries/usr/local/include'%(WORKDIR,), '')
788 fp = open(path, 'w')
789 fp.write(data)
790 fp.close()
792 # Add symlinks in /usr/local/bin, using relative links
793 usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
794 to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
795 'Python.framework', 'Versions', version, 'bin')
796 if os.path.exists(usr_local_bin):
797 shutil.rmtree(usr_local_bin)
798 os.makedirs(usr_local_bin)
799 for fn in os.listdir(
800 os.path.join(frmDir, 'Versions', version, 'bin')):
801 os.symlink(os.path.join(to_framework, fn),
802 os.path.join(usr_local_bin, fn))
804 os.chdir(curdir)
806 if PYTHON_3:
807 # Remove the 'Current' link, that way we don't accidently mess
808 # with an already installed version of python 2
809 os.unlink(os.path.join(rootDir, 'Library', 'Frameworks',
810 'Python.framework', 'Versions', 'Current'))
812 def patchFile(inPath, outPath):
813 data = fileContents(inPath)
814 data = data.replace('$FULL_VERSION', getFullVersion())
815 data = data.replace('$VERSION', getVersion())
816 data = data.replace('$MACOSX_DEPLOYMENT_TARGET', ''.join((DEPTARGET, ' or later')))
817 data = data.replace('$ARCHITECTURES', "i386, ppc")
818 data = data.replace('$INSTALL_SIZE', installSize())
820 # This one is not handy as a template variable
821 data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
822 fp = open(outPath, 'wb')
823 fp.write(data)
824 fp.close()
826 def patchScript(inPath, outPath):
827 data = fileContents(inPath)
828 data = data.replace('@PYVER@', getVersion())
829 fp = open(outPath, 'wb')
830 fp.write(data)
831 fp.close()
832 os.chmod(outPath, 0755)
836 def packageFromRecipe(targetDir, recipe):
837 curdir = os.getcwd()
838 try:
839 # The major version (such as 2.5) is included in the package name
840 # because having two version of python installed at the same time is
841 # common.
842 pkgname = '%s-%s'%(recipe['name'], getVersion())
843 srcdir = recipe.get('source')
844 pkgroot = recipe.get('topdir', srcdir)
845 postflight = recipe.get('postflight')
846 readme = textwrap.dedent(recipe['readme'])
847 isRequired = recipe.get('required', True)
849 print "- building package %s"%(pkgname,)
851 # Substitute some variables
852 textvars = dict(
853 VER=getVersion(),
854 FULLVER=getFullVersion(),
856 readme = readme % textvars
858 if pkgroot is not None:
859 pkgroot = pkgroot % textvars
860 else:
861 pkgroot = '/'
863 if srcdir is not None:
864 srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
865 srcdir = srcdir % textvars
867 if postflight is not None:
868 postflight = os.path.abspath(postflight)
870 packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
871 os.makedirs(packageContents)
873 if srcdir is not None:
874 os.chdir(srcdir)
875 runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
876 runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
877 runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
879 fn = os.path.join(packageContents, 'PkgInfo')
880 fp = open(fn, 'w')
881 fp.write('pmkrpkg1')
882 fp.close()
884 rsrcDir = os.path.join(packageContents, "Resources")
885 os.mkdir(rsrcDir)
886 fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
887 fp.write(readme)
888 fp.close()
890 if postflight is not None:
891 patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
893 vers = getFullVersion()
894 major, minor = map(int, getVersion().split('.', 2))
895 pl = Plist(
896 CFBundleGetInfoString="Python.%s %s"%(pkgname, vers,),
897 CFBundleIdentifier='org.python.Python.%s'%(pkgname,),
898 CFBundleName='Python.%s'%(pkgname,),
899 CFBundleShortVersionString=vers,
900 IFMajorVersion=major,
901 IFMinorVersion=minor,
902 IFPkgFormatVersion=0.10000000149011612,
903 IFPkgFlagAllowBackRev=False,
904 IFPkgFlagAuthorizationAction="RootAuthorization",
905 IFPkgFlagDefaultLocation=pkgroot,
906 IFPkgFlagFollowLinks=True,
907 IFPkgFlagInstallFat=True,
908 IFPkgFlagIsRequired=isRequired,
909 IFPkgFlagOverwritePermissions=False,
910 IFPkgFlagRelocatable=False,
911 IFPkgFlagRestartAction="NoRestart",
912 IFPkgFlagRootVolumeOnly=True,
913 IFPkgFlagUpdateInstalledLangauges=False,
915 writePlist(pl, os.path.join(packageContents, 'Info.plist'))
917 pl = Plist(
918 IFPkgDescriptionDescription=readme,
919 IFPkgDescriptionTitle=recipe.get('long_name', "Python.%s"%(pkgname,)),
920 IFPkgDescriptionVersion=vers,
922 writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
924 finally:
925 os.chdir(curdir)
928 def makeMpkgPlist(path):
930 vers = getFullVersion()
931 major, minor = map(int, getVersion().split('.', 2))
933 pl = Plist(
934 CFBundleGetInfoString="Python %s"%(vers,),
935 CFBundleIdentifier='org.python.Python',
936 CFBundleName='Python',
937 CFBundleShortVersionString=vers,
938 IFMajorVersion=major,
939 IFMinorVersion=minor,
940 IFPkgFlagComponentDirectory="Contents/Packages",
941 IFPkgFlagPackageList=[
942 dict(
943 IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
944 IFPkgFlagPackageSelection=item.get('selected', 'selected'),
946 for item in pkg_recipes()
948 IFPkgFormatVersion=0.10000000149011612,
949 IFPkgFlagBackgroundScaling="proportional",
950 IFPkgFlagBackgroundAlignment="left",
951 IFPkgFlagAuthorizationAction="RootAuthorization",
954 writePlist(pl, path)
957 def buildInstaller():
959 # Zap all compiled files
960 for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
961 for fn in filenames:
962 if fn.endswith('.pyc') or fn.endswith('.pyo'):
963 os.unlink(os.path.join(dirpath, fn))
965 outdir = os.path.join(WORKDIR, 'installer')
966 if os.path.exists(outdir):
967 shutil.rmtree(outdir)
968 os.mkdir(outdir)
970 pkgroot = os.path.join(outdir, 'Python.mpkg', 'Contents')
971 pkgcontents = os.path.join(pkgroot, 'Packages')
972 os.makedirs(pkgcontents)
973 for recipe in pkg_recipes():
974 packageFromRecipe(pkgcontents, recipe)
976 rsrcDir = os.path.join(pkgroot, 'Resources')
978 fn = os.path.join(pkgroot, 'PkgInfo')
979 fp = open(fn, 'w')
980 fp.write('pmkrpkg1')
981 fp.close()
983 os.mkdir(rsrcDir)
985 makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
986 pl = Plist(
987 IFPkgDescriptionTitle="Python",
988 IFPkgDescriptionVersion=getVersion(),
991 writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
992 for fn in os.listdir('resources'):
993 if fn == '.svn': continue
994 if fn.endswith('.jpg'):
995 shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
996 else:
997 patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
999 shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
1002 def installSize(clear=False, _saved=[]):
1003 if clear:
1004 del _saved[:]
1005 if not _saved:
1006 data = captureCommand("du -ks %s"%(
1007 shellQuote(os.path.join(WORKDIR, '_root'))))
1008 _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
1009 return _saved[0]
1012 def buildDMG():
1014 Create DMG containing the rootDir.
1016 outdir = os.path.join(WORKDIR, 'diskimage')
1017 if os.path.exists(outdir):
1018 shutil.rmtree(outdir)
1020 imagepath = os.path.join(outdir,
1021 'python-%s-macosx%s'%(getFullVersion(),DEPTARGET))
1022 if INCLUDE_TIMESTAMP:
1023 imagepath = imagepath + '-%04d-%02d-%02d'%(time.localtime()[:3])
1024 imagepath = imagepath + '.dmg'
1026 os.mkdir(outdir)
1027 volname='Python %s'%(getFullVersion())
1028 runCommand("hdiutil create -format UDRW -volname %s -srcfolder %s %s"%(
1029 shellQuote(volname),
1030 shellQuote(os.path.join(WORKDIR, 'installer')),
1031 shellQuote(imagepath + ".tmp.dmg" )))
1034 if not os.path.exists(os.path.join(WORKDIR, "mnt")):
1035 os.mkdir(os.path.join(WORKDIR, "mnt"))
1036 runCommand("hdiutil attach %s -mountroot %s"%(
1037 shellQuote(imagepath + ".tmp.dmg"), shellQuote(os.path.join(WORKDIR, "mnt"))))
1039 # Custom icon for the DMG, shown when the DMG is mounted.
1040 shutil.copy("../Icons/Disk Image.icns",
1041 os.path.join(WORKDIR, "mnt", volname, ".VolumeIcon.icns"))
1042 runCommand("/Developer/Tools/SetFile -a C %s/"%(
1043 shellQuote(os.path.join(WORKDIR, "mnt", volname)),))
1045 runCommand("hdiutil detach %s"%(shellQuote(os.path.join(WORKDIR, "mnt", volname))))
1047 setIcon(imagepath + ".tmp.dmg", "../Icons/Disk Image.icns")
1048 runCommand("hdiutil convert %s -format UDZO -o %s"%(
1049 shellQuote(imagepath + ".tmp.dmg"), shellQuote(imagepath)))
1050 setIcon(imagepath, "../Icons/Disk Image.icns")
1052 os.unlink(imagepath + ".tmp.dmg")
1054 return imagepath
1057 def setIcon(filePath, icnsPath):
1059 Set the custom icon for the specified file or directory.
1062 toolPath = os.path.join(os.path.dirname(__file__), "seticon.app/Contents/MacOS/seticon")
1063 dirPath = os.path.dirname(__file__)
1064 if not os.path.exists(toolPath) or os.stat(toolPath).st_mtime < os.stat(dirPath + '/seticon.m').st_mtime:
1065 # NOTE: The tool is created inside an .app bundle, otherwise it won't work due
1066 # to connections to the window server.
1067 if not os.path.exists('seticon.app/Contents/MacOS'):
1068 os.makedirs('seticon.app/Contents/MacOS')
1069 runCommand("cc -o %s %s/seticon.m -framework Cocoa"%(
1070 shellQuote(toolPath), shellQuote(dirPath)))
1072 runCommand("%s %s %s"%(shellQuote(os.path.abspath(toolPath)), shellQuote(icnsPath),
1073 shellQuote(filePath)))
1075 def main():
1076 # First parse options and check if we can perform our work
1077 parseOptions()
1078 checkEnvironment()
1080 os.environ['MACOSX_DEPLOYMENT_TARGET'] = DEPTARGET
1081 os.environ['CC'] = CC
1083 if os.path.exists(WORKDIR):
1084 shutil.rmtree(WORKDIR)
1085 os.mkdir(WORKDIR)
1087 # Then build third-party libraries such as sleepycat DB4.
1088 buildLibraries()
1090 # Now build python itself
1091 buildPython()
1093 # And then build the documentation
1094 # Remove the Deployment Target from the shell
1095 # environment, it's no longer needed and
1096 # an unexpected build target can cause problems
1097 # when Sphinx and its dependencies need to
1098 # be (re-)installed.
1099 del os.environ['MACOSX_DEPLOYMENT_TARGET']
1100 buildPythonDocs()
1103 # Prepare the applications folder
1104 fn = os.path.join(WORKDIR, "_root", "Applications",
1105 "Python %s"%(getVersion(),), "Update Shell Profile.command")
1106 patchScript("scripts/postflight.patch-profile", fn)
1108 folder = os.path.join(WORKDIR, "_root", "Applications", "Python %s"%(
1109 getVersion(),))
1110 os.chmod(folder, 0755)
1111 setIcon(folder, "../Icons/Python Folder.icns")
1113 # Create the installer
1114 buildInstaller()
1116 # And copy the readme into the directory containing the installer
1117 patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
1119 # Ditto for the license file.
1120 shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
1122 fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
1123 print >> fp, "# BUILD INFO"
1124 print >> fp, "# Date:", time.ctime()
1125 print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
1126 fp.close()
1128 # And copy it to a DMG
1129 buildDMG()
1131 if __name__ == "__main__":
1132 main()