Added WatchedFileHandler (based on SF patch #1598415)
[python.git] / Mac / BuildScript / build-installer.py
blob8bdebe6806d0635a9f4109adfb25741276c641c6
1 #!/usr/bin/python2.3
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.
7 Please ensure that this script keeps working with Python 2.3, to avoid
8 bootstrap issues (/usr/bin/python is Python 2.3 on OSX 10.4)
10 Usage: see USAGE variable in the script.
11 """
12 import platform, os, sys, getopt, textwrap, shutil, urllib2, stat, time, pwd
13 import grp
15 INCLUDE_TIMESTAMP = 1
16 VERBOSE = 1
18 from plistlib import Plist
20 import MacOS
21 import Carbon.File
22 import Carbon.Icn
23 import Carbon.Res
24 from Carbon.Files import kCustomIconResource, fsRdWrPerm, kHasCustomIcon
25 from Carbon.Files import kFSCatInfoFinderInfo
27 try:
28 from plistlib import writePlist
29 except ImportError:
30 # We're run using python2.3
31 def writePlist(plist, path):
32 plist.write(path)
34 def shellQuote(value):
35 """
36 Return the string value in a form that can safely be inserted into
37 a shell command.
38 """
39 return "'%s'"%(value.replace("'", "'\"'\"'"))
41 def 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]
48 def getVersion():
49 return grepValue(os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
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]
57 raise RuntimeError, "Cannot find full version??"
59 # The directory we'll use to create the build (will be erased and recreated)
60 WORKDIR = "/tmp/_py"
62 # The directory we'll use to store third-party sources. Set this to something
63 # else if you don't want to re-fetch required libraries every time.
64 DEPSRC = os.path.join(WORKDIR, 'third-party')
65 DEPSRC = os.path.expanduser('~/Universal/other-sources')
67 # Location of the preferred SDK
68 SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
69 #SDKPATH = "/"
71 ARCHLIST = ('i386', 'ppc',)
73 # Source directory (asume we're in Mac/BuildScript)
74 SRCDIR = os.path.dirname(
75 os.path.dirname(
76 os.path.dirname(
77 os.path.abspath(__file__
78 ))))
80 USAGE = textwrap.dedent("""\
81 Usage: build_python [options]
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()
93 # Instructions for building libraries that are necessary for building a
94 # batteries included python.
95 LIBRARY_RECIPES = [
96 dict(
97 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,
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,
116 dict(
117 # 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',
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',
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",
162 patches=[
163 "ncurses-5.5.patch",
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(),
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',
186 # Instructions for building packages inside the .mpkg.
187 PKG_RECIPES = [
188 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",
199 dict(
200 name="PythonApplications",
201 long_name="GUI Applications",
202 source="/Applications/MacPython %(VER)s",
203 readme="""\
204 This package installs IDLE (an interactive Python IDE),
205 Python Launcher and Build Applet (create application bundles
206 from python scripts).
208 It also installs a number of examples and demos.
209 """,
210 required=False,
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,
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,
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.
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,
254 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
260 using that copy of python after installing this version.
261 """,
262 postflight="../Tools/fixapplepython23.py",
263 topdir="/Library/Frameworks/Python.framework",
264 source="/empty-dir",
265 required=False,
269 def fatal(msg):
271 A fatal error, bail out.
273 sys.stderr.write('FATAL: ')
274 sys.stderr.write(msg)
275 sys.stderr.write('\n')
276 sys.exit(1)
278 def fileContents(fn):
280 Return the contents of the named file
282 return open(fn, 'rb').read()
284 def runCommand(commandline):
286 Run a command and raise RuntimeError if it fails. Output is surpressed
287 unless the command fails.
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,)
296 if VERBOSE:
297 sys.stdout.write(data); sys.stdout.flush()
299 def 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,)
307 return data
309 def checkEnvironment():
311 Check that we're running on a supported system.
314 if platform.system() != 'Darwin':
315 fatal("This script should be run on a Mac OS X 10.4 system")
317 if platform.release() <= '8.':
318 fatal("This script should be run on a Mac OS X 10.4 system")
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])))
326 def parseOptions(args=None):
328 Parse arguments and update global settings.
330 global WORKDIR, DEPSRC, SDKPATH, SRCDIR
332 if args is None:
333 args = sys.argv[1:]
335 try:
336 options, args = getopt.getopt(args, '?hb',
337 [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir='])
338 except getopt.error, msg:
339 print msg
340 sys.exit(1)
342 if args:
343 print "Additional arguments"
344 sys.exit(1)
346 for k, v in options:
347 if k in ('-h', '-?'):
348 print USAGE
349 sys.exit(0)
351 elif k in ('-d', '--build-dir'):
352 WORKDIR=v
354 elif k in ('--third-party',):
355 DEPSRC=v
357 elif k in ('--sdk-path',):
358 SDKPATH=v
360 elif k in ('--src-dir',):
361 SRCDIR=v
363 else:
364 raise NotImplementedError, k
366 SRCDIR=os.path.abspath(SRCDIR)
367 WORKDIR=os.path.abspath(WORKDIR)
368 SDKPATH=os.path.abspath(SDKPATH)
369 DEPSRC=os.path.abspath(DEPSRC)
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 ""
381 def extractArchive(builddir, archiveName):
383 Extract a source archive into 'builddir'. Returns the path of the
384 extracted archive.
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.
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')
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')
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')
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')
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,)
423 return os.path.join(builddir, retval)
425 finally:
426 os.chdir(curdir)
428 KNOWNSIZES = {
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,
433 def downloadURL(url, fname):
435 Download the contents of the url into the file.
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
460 def buildRecipe(recipe, basedir, archList):
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.
466 curdir = os.getcwd()
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)))
474 archiveName = os.path.split(url)[-1]
475 sourceArchive = os.path.join(DEPSRC, archiveName)
477 if not os.path.exists(DEPSRC):
478 os.mkdir(DEPSRC)
481 if os.path.exists(sourceArchive):
482 print "Using local copy of %s"%(name,)
484 else:
485 print "Downloading %s"%(name,)
486 downloadURL(url, sourceArchive)
487 print "Archive for %s stored as %s"%(name, sourceArchive)
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)
494 workDir = extractArchive(buildDir, sourceArchive)
495 os.chdir(workDir)
496 if 'buildDir' in recipe:
497 os.chdir(recipe['buildDir'])
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
507 fn = os.path.join(curdir, fn)
508 runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
509 shellQuote(fn),))
511 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,),),
519 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)
527 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"%(
534 shellQuote(SDKPATH)[1:-1],
535 shellQuote(basedir)[1:-1],
536 ' -arch '.join(archList)),
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],),
546 if 'configure_post' in recipe:
547 configure_args = configure_args = list(recipe['configure_post'])
549 configure_args.insert(0, configure)
550 configure_args = [ shellQuote(a) for a in configure_args ]
552 print "Running configure for %s"%(name,)
553 runCommand(' '.join(configure_args) + ' 2>&1')
555 print "Running install for %s"%(name,)
556 runCommand('{ ' + install + ' ;} 2>&1')
558 print "Done %s"%(name,)
559 print ""
561 os.chdir(curdir)
563 def buildLibraries():
565 Build our dependencies into $WORKDIR/libraries/usr/local
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'))
575 for recipe in LIBRARY_RECIPES:
576 buildRecipe(recipe, universal, ARCHLIST)
580 def 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')
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,)
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)
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)
606 def buildPython():
607 print "Building a universal python"
609 buildDir = os.path.join(WORKDIR, '_bld', 'python')
610 rootDir = os.path.join(WORKDIR, '_root')
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)
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'))
626 # Extract the version from the configure file, needed to calculate
627 # several paths.
628 version = getVersion()
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]))
636 print "Running make"
637 runCommand("make")
639 print "Running make frameworkinstall"
640 runCommand("make frameworkinstall DESTDIR=%s"%(
641 shellQuote(rootDir)))
643 print "Running make frameworkinstallextras"
644 runCommand("make frameworkinstallextras DESTDIR=%s"%(
645 shellQuote(rootDir)))
647 print "Copying required shared libraries"
648 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'))))
658 print "Fix file modes"
659 frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
660 gid = grp.getgrnam('admin').gr_gid
662 for dirpath, dirnames, filenames in os.walk(frmDir):
663 for dn in dirnames:
664 os.chmod(os.path.join(dirpath, dn), 0775)
665 os.chown(os.path.join(dirpath, dn), -1, gid)
668 for fn in filenames:
669 if os.path.islink(fn):
670 continue
672 # "chmod g+w $fn"
673 p = os.path.join(dirpath, fn)
674 st = os.stat(p)
675 os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
676 os.chown(p, -1, gid)
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()
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()
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))
706 os.chdir(curdir)
710 def 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())
718 # This one is not handy as a template variable
719 data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
720 fp = open(outPath, 'wb')
721 fp.write(data)
722 fp.close()
724 def 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)
734 def packageFromRecipe(targetDir, recipe):
735 curdir = os.getcwd()
736 try:
737 # 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
739 # common.
740 pkgname = '%s-%s'%(recipe['name'], getVersion())
741 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)
747 print "- building package %s"%(pkgname,)
749 # Substitute some variables
750 textvars = dict(
751 VER=getVersion(),
752 FULLVER=getFullVersion(),
754 readme = readme % textvars
756 if pkgroot is not None:
757 pkgroot = pkgroot % textvars
758 else:
759 pkgroot = '/'
761 if srcdir is not None:
762 srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
763 srcdir = srcdir % textvars
765 if postflight is not None:
766 postflight = os.path.abspath(postflight)
768 packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
769 os.makedirs(packageContents)
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')),))
777 fn = os.path.join(packageContents, 'PkgInfo')
778 fp = open(fn, 'w')
779 fp.write('pmkrpkg1')
780 fp.close()
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()
788 if postflight is not None:
789 patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
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,
813 writePlist(pl, os.path.join(packageContents, 'Info.plist'))
815 pl = Plist(
816 IFPkgDescriptionDescription=readme,
817 IFPkgDescriptionTitle=recipe.get('long_name', "MacPython.%s"%(pkgname,)),
818 IFPkgDescriptionVersion=vers,
820 writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
822 finally:
823 os.chdir(curdir)
826 def makeMpkgPlist(path):
828 vers = getFullVersion()
829 major, minor = map(int, getVersion().split('.', 2))
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(
841 IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
842 IFPkgFlagPackageSelection='selected'
844 for item in PKG_RECIPES
846 IFPkgFormatVersion=0.10000000149011612,
847 IFPkgFlagBackgroundScaling="proportional",
848 IFPkgFlagBackgroundAlignment="left",
849 IFPkgFlagAuthorizationAction="RootAuthorization",
852 writePlist(pl, path)
855 def buildInstaller():
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))
863 outdir = os.path.join(WORKDIR, 'installer')
864 if os.path.exists(outdir):
865 shutil.rmtree(outdir)
866 os.mkdir(outdir)
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)
874 rsrcDir = os.path.join(pkgroot, 'Resources')
876 fn = os.path.join(pkgroot, 'PkgInfo')
877 fp = open(fn, 'w')
878 fp.write('pmkrpkg1')
879 fp.close()
881 os.mkdir(rsrcDir)
883 makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
884 pl = Plist(
885 IFPkgDescriptionTitle="Universal MacPython",
886 IFPkgDescriptionVersion=getVersion(),
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))
897 shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
900 def 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]
910 def buildDMG():
912 Create DMG containing the rootDir.
914 outdir = os.path.join(WORKDIR, 'diskimage')
915 if os.path.exists(outdir):
916 shutil.rmtree(outdir)
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'
924 os.mkdir(outdir)
925 runCommand("hdiutil create -volname 'Universal MacPython %s' -srcfolder %s %s"%(
926 getFullVersion(),
927 shellQuote(os.path.join(WORKDIR, 'installer')),
928 shellQuote(imagepath)))
930 return imagepath
933 def setIcon(filePath, icnsPath):
935 Set the custom icon for the specified file or directory.
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.
941 ref, isDirectory = Carbon.File.FSPathMakeRef(icnsPath)
942 icon = Carbon.Icn.ReadIconFile(ref)
943 del ref
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.
950 ref, isDirectory = Carbon.File.FSPathMakeRef(filePath)
952 if isDirectory:
953 # There is a problem with getting this into the pax(1) archive,
954 # just ignore directory icons for now.
955 return
957 tmpPath = os.path.join(filePath, "Icon\r")
958 if not os.path.exists(tmpPath):
959 fp = open(tmpPath, 'w')
960 fp.close()
962 tmpRef, _ = Carbon.File.FSPathMakeRef(tmpPath)
963 spec = Carbon.File.FSSpec(tmpRef)
965 else:
966 spec = Carbon.File.FSSpec(ref)
968 try:
969 Carbon.Res.HCreateResFile(*spec.as_tuple())
970 except MacOS.Error:
971 pass
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
981 refNum = Carbon.Res.FSpOpenResFile(spec, fsRdWrPerm)
983 Carbon.Res.UseResFile(refNum)
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
991 else:
992 h.RemoveResource()
993 del h
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)
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),))
1008 if isDirectory:
1009 os.system('/Developer/Tools/SetFile -a V %s'%(
1010 shellQuote(tmpPath),
1013 def main():
1014 # First parse options and check if we can perform our work
1015 parseOptions()
1016 checkEnvironment()
1018 os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.3'
1020 if os.path.exists(WORKDIR):
1021 shutil.rmtree(WORKDIR)
1022 os.mkdir(WORKDIR)
1024 # Then build third-party libraries such as sleepycat DB4.
1025 buildLibraries()
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")
1032 patchFile("scripts/postflight.patch-profile", fn)
1033 os.chmod(fn, 0755)
1035 folder = os.path.join(WORKDIR, "_root", "Applications", "MacPython %s"%(
1036 getVersion(),))
1037 os.chmod(folder, 0755)
1038 setIcon(folder, "../Icons/Python Folder.icns")
1040 # Create the installer
1041 buildInstaller()
1043 # And copy the readme into the directory containing the installer
1044 patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
1046 # Ditto for the license file.
1047 shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
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()
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")))
1062 # And copy it to a DMG
1063 buildDMG()
1066 if __name__ == "__main__":
1067 main()