build-installer.py revision 896c4777c658e6132f5e9d6a34162746d790a4c6
1#!/usr/bin/env python
2"""
3This script is used to build the "official unofficial" universal build on
4Mac OS X. It requires Mac OS X 10.4, Xcode 2.2 and the 10.4u SDK to do its
5work.  64-bit or four-way universal builds require at least OS X 10.5 and
6the 10.5 SDK.
7
8Please ensure that this script keeps working with Python 2.3, to avoid
9bootstrap issues (/usr/bin/python is Python 2.3 on OSX 10.4)
10
11Usage: see USAGE variable in the script.
12"""
13import platform, os, sys, getopt, textwrap, shutil, urllib2, stat, time, pwd
14import grp
15
16INCLUDE_TIMESTAMP = 1
17VERBOSE = 1
18
19from plistlib import Plist
20
21import MacOS
22
23try:
24    from plistlib import writePlist
25except ImportError:
26    # We're run using python2.3
27    def writePlist(plist, path):
28        plist.write(path)
29
30def 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("'", "'\"'\"'"))
36
37def 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
44def getVersion():
45    return grepValue(os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
46
47def 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]
52
53    raise RuntimeError, "Cannot find full version??"
54
55# The directory we'll use to create the build (will be erased and recreated)
56WORKDIR = "/tmp/_py"
57
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.
60DEPSRC = os.path.join(WORKDIR, 'third-party')
61DEPSRC = os.path.expanduser('~/Universal/other-sources')
62
63# Location of the preferred SDK
64
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 = "/"
77
78SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
79
80universal_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',) }
85default_target_map = {
86        '64-bit': '10.5',
87        '3-way': '10.5',
88        'intel': '10.5',
89        'all': '10.5',
90}
91
92UNIVERSALOPTS = tuple(universal_opts_map.keys())
93
94UNIVERSALARCHS = '32-bit'
95
96ARCHLIST = universal_opts_map[UNIVERSALARCHS]
97
98# Source directory (asume we're in Mac/BuildScript)
99SRCDIR = os.path.dirname(
100        os.path.dirname(
101            os.path.dirname(
102                os.path.abspath(__file__
103        ))))
104
105# $MACOSX_DEPLOYMENT_TARGET -> minimum OS X level
106DEPTARGET = '10.3'
107
108target_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',
113}
114
115CC = target_cc_map[DEPTARGET]
116
117USAGE = textwrap.dedent("""\
118    Usage: build_python [options]
119
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()
130
131
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.]
136def library_recipes():
137    result = []
138
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"'%(
147                  CC,
148                  shellQuote(os.path.join(WORKDIR, 'libraries')),
149                  ' -arch '.join(ARCHLIST),
150                  SDKPATH,
151              ),
152          ),
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"'%(
159                  CC,
160                  shellQuote(os.path.join(WORKDIR, 'libraries')),
161                  ' -arch '.join(ARCHLIST),
162                  SDKPATH,
163              ),
164          ),
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',
178              ]
179          ),
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',
190              ]
191          ),
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",
210              ],
211              patches=[
212                  "ncurses-5.5.patch",
213              ],
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(),
219                  ),
220          ),
221        ])
222
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',
232          ]
233      ),
234    ])
235
236    return result
237
238
239# Instructions for building packages inside the .mpkg.
240def 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",
252            selected='selected',
253        ),
254        dict(
255            name="PythonApplications",
256            long_name="GUI Applications",
257            source="/Applications/Python %(VER)s",
258            readme="""\
259                This package installs IDLE (an interactive Python IDE),
260                Python Launcher and Build Applet (create application bundles
261                from python scripts).
262
263                It also installs a number of examples and demos.
264                """,
265            required=False,
266            selected='selected',
267        ),
268        dict(
269            name="PythonUnixTools",
270            long_name="UNIX command-line tools",
271            source="/usr/local/bin",
272            readme="""\
273                This package installs the unix tools in /usr/local/bin for
274                compatibility with older releases of Python. This package
275                is not necessary to use Python.
276                """,
277            required=False,
278            selected='unselected',
279        ),
280        dict(
281            name="PythonDocumentation",
282            long_name="Python Documentation",
283            topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
284            source="/pydocs",
285            readme="""\
286                This package installs the python documentation at a location
287                that is useable for pydoc and IDLE. If you have installed Xcode
288                it will also install a link to the documentation in
289                /Developer/Documentation/Python
290                """,
291            postflight="scripts/postflight.documentation",
292            required=False,
293            selected='selected',
294        ),
295        dict(
296            name="PythonProfileChanges",
297            long_name="Shell profile updater",
298            readme="""\
299                This packages updates your shell profile to make sure that
300                the Python tools are found by your shell in preference of
301                the system provided Python tools.
302
303                If you don't install this package you'll have to add
304                "/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
305                to your PATH by hand.
306                """,
307            postflight="scripts/postflight.patch-profile",
308            topdir="/Library/Frameworks/Python.framework",
309            source="/empty-dir",
310            required=False,
311            selected='selected',
312        ),
313    ]
314
315    if DEPTARGET < '10.4':
316        result.append(
317            dict(
318                name="PythonSystemFixes",
319                long_name="Fix system Python",
320                readme="""\
321                    This package updates the system python installation on
322                    Mac OS X 10.3 to ensure that you can build new python extensions
323                    using that copy of python after installing this version.
324                    """,
325                postflight="../Tools/fixapplepython23.py",
326                topdir="/Library/Frameworks/Python.framework",
327                source="/empty-dir",
328                required=False,
329                selected='selected',
330            )
331        )
332    return result
333
334def fatal(msg):
335    """
336    A fatal error, bail out.
337    """
338    sys.stderr.write('FATAL: ')
339    sys.stderr.write(msg)
340    sys.stderr.write('\n')
341    sys.exit(1)
342
343def fileContents(fn):
344    """
345    Return the contents of the named file
346    """
347    return open(fn, 'rb').read()
348
349def runCommand(commandline):
350    """
351    Run a command and raise RuntimeError if it fails. Output is surpressed
352    unless the command fails.
353    """
354    fd = os.popen(commandline, 'r')
355    data = fd.read()
356    xit = fd.close()
357    if xit is not None:
358        sys.stdout.write(data)
359        raise RuntimeError, "command failed: %s"%(commandline,)
360
361    if VERBOSE:
362        sys.stdout.write(data); sys.stdout.flush()
363
364def captureCommand(commandline):
365    fd = os.popen(commandline, 'r')
366    data = fd.read()
367    xit = fd.close()
368    if xit is not None:
369        sys.stdout.write(data)
370        raise RuntimeError, "command failed: %s"%(commandline,)
371
372    return data
373
374def getTclTkVersion(configfile, versionline):
375    """
376    search Tcl or Tk configuration file for version line
377    """
378    try:
379        f = open(configfile, "r")
380    except:
381        fatal("Framework configuration file not found: %s" % configfile)
382
383    for l in f:
384        if l.startswith(versionline):
385            f.close()
386            return l
387
388    fatal("Version variable %s not found in framework configuration file: %s"
389            % (versionline, configfile))
390
391def checkEnvironment():
392    """
393    Check that we're running on a supported system.
394    """
395
396    if platform.system() != 'Darwin':
397        fatal("This script should be run on a Mac OS X 10.4 (or later) system")
398
399    if int(platform.release().split('.')[0]) < 8:
400        fatal("This script should be run on a Mac OS X 10.4 (or later) system")
401
402    if not os.path.exists(SDKPATH):
403        fatal("Please install the latest version of Xcode and the %s SDK"%(
404            os.path.basename(SDKPATH[:-4])))
405
406    # Because we only support dynamic load of only one major/minor version of
407    # Tcl/Tk, ensure:
408    # 1. there are no user-installed frameworks of Tcl/Tk with version
409    #       higher than the Apple-supplied system version
410    # 2. there is a user-installed framework in /Library/Frameworks with the
411    #       same version as the system version.  This allows users to choose
412    #       to install a newer patch level.
413
414    for framework in ['Tcl', 'Tk']:
415        fw = dict(lower=framework.lower(),
416                    upper=framework.upper(),
417                    cap=framework.capitalize())
418        fwpth = "Library/Frameworks/%(cap)s.framework/%(lower)sConfig.sh" % fw
419        sysfw = os.path.join('/System', fwpth)
420        libfw = os.path.join('/', fwpth)
421        usrfw = os.path.join(os.getenv('HOME'), fwpth)
422        version = "%(upper)s_VERSION" % fw
423        if getTclTkVersion(libfw, version) != getTclTkVersion(sysfw, version):
424            fatal("Version of %s must match %s" % (libfw, sysfw) )
425        if os.path.exists(usrfw):
426            fatal("Please rename %s to avoid possible dynamic load issues."
427                    % usrfw)
428
429    # Remove inherited environment variables which might influence build
430    environ_var_prefixes = ['CPATH', 'C_INCLUDE_', 'DYLD_', 'LANG', 'LC_',
431                            'LD_', 'LIBRARY_', 'PATH', 'PYTHON']
432    for ev in list(os.environ):
433        for prefix in environ_var_prefixes:
434            if ev.startswith(prefix) :
435                print "INFO: deleting environment variable %s=%s" % (
436                                                    ev, os.environ[ev])
437                del os.environ[ev]
438
439    os.environ['PATH'] = '/bin:/sbin:/usr/bin:/usr/sbin'
440    print "Setting default PATH: %s"%(os.environ['PATH'])
441
442
443def parseOptions(args=None):
444    """
445    Parse arguments and update global settings.
446    """
447    global WORKDIR, DEPSRC, SDKPATH, SRCDIR, DEPTARGET
448    global UNIVERSALOPTS, UNIVERSALARCHS, ARCHLIST, CC
449
450    if args is None:
451        args = sys.argv[1:]
452
453    try:
454        options, args = getopt.getopt(args, '?hb',
455                [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir=',
456                  'dep-target=', 'universal-archs=', 'help' ])
457    except getopt.error, msg:
458        print msg
459        sys.exit(1)
460
461    if args:
462        print "Additional arguments"
463        sys.exit(1)
464
465    deptarget = None
466    for k, v in options:
467        if k in ('-h', '-?', '--help'):
468            print USAGE
469            sys.exit(0)
470
471        elif k in ('-d', '--build-dir'):
472            WORKDIR=v
473
474        elif k in ('--third-party',):
475            DEPSRC=v
476
477        elif k in ('--sdk-path',):
478            SDKPATH=v
479
480        elif k in ('--src-dir',):
481            SRCDIR=v
482
483        elif k in ('--dep-target', ):
484            DEPTARGET=v
485            deptarget=v
486
487        elif k in ('--universal-archs', ):
488            if v in UNIVERSALOPTS:
489                UNIVERSALARCHS = v
490                ARCHLIST = universal_opts_map[UNIVERSALARCHS]
491                if deptarget is None:
492                    # Select alternate default deployment
493                    # target
494                    DEPTARGET = default_target_map.get(v, '10.3')
495            else:
496                raise NotImplementedError, v
497
498        else:
499            raise NotImplementedError, k
500
501    SRCDIR=os.path.abspath(SRCDIR)
502    WORKDIR=os.path.abspath(WORKDIR)
503    SDKPATH=os.path.abspath(SDKPATH)
504    DEPSRC=os.path.abspath(DEPSRC)
505
506    CC=target_cc_map[DEPTARGET]
507
508    print "Settings:"
509    print " * Source directory:", SRCDIR
510    print " * Build directory: ", WORKDIR
511    print " * SDK location:    ", SDKPATH
512    print " * Third-party source:", DEPSRC
513    print " * Deployment target:", DEPTARGET
514    print " * Universal architectures:", ARCHLIST
515    print " * C compiler:", CC
516    print ""
517
518
519
520
521def extractArchive(builddir, archiveName):
522    """
523    Extract a source archive into 'builddir'. Returns the path of the
524    extracted archive.
525
526    XXX: This function assumes that archives contain a toplevel directory
527    that is has the same name as the basename of the archive. This is
528    save enough for anything we use.
529    """
530    curdir = os.getcwd()
531    try:
532        os.chdir(builddir)
533        if archiveName.endswith('.tar.gz'):
534            retval = os.path.basename(archiveName[:-7])
535            if os.path.exists(retval):
536                shutil.rmtree(retval)
537            fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')
538
539        elif archiveName.endswith('.tar.bz2'):
540            retval = os.path.basename(archiveName[:-8])
541            if os.path.exists(retval):
542                shutil.rmtree(retval)
543            fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')
544
545        elif archiveName.endswith('.tar'):
546            retval = os.path.basename(archiveName[:-4])
547            if os.path.exists(retval):
548                shutil.rmtree(retval)
549            fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')
550
551        elif archiveName.endswith('.zip'):
552            retval = os.path.basename(archiveName[:-4])
553            if os.path.exists(retval):
554                shutil.rmtree(retval)
555            fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')
556
557        data = fp.read()
558        xit = fp.close()
559        if xit is not None:
560            sys.stdout.write(data)
561            raise RuntimeError, "Cannot extract %s"%(archiveName,)
562
563        return os.path.join(builddir, retval)
564
565    finally:
566        os.chdir(curdir)
567
568KNOWNSIZES = {
569    "http://ftp.gnu.org/pub/gnu/readline/readline-5.1.tar.gz": 7952742,
570    "http://downloads.sleepycat.com/db-4.4.20.tar.gz": 2030276,
571}
572
573def downloadURL(url, fname):
574    """
575    Download the contents of the url into the file.
576    """
577    try:
578        size = os.path.getsize(fname)
579    except OSError:
580        pass
581    else:
582        if KNOWNSIZES.get(url) == size:
583            print "Using existing file for", url
584            return
585    fpIn = urllib2.urlopen(url)
586    fpOut = open(fname, 'wb')
587    block = fpIn.read(10240)
588    try:
589        while block:
590            fpOut.write(block)
591            block = fpIn.read(10240)
592        fpIn.close()
593        fpOut.close()
594    except:
595        try:
596            os.unlink(fname)
597        except:
598            pass
599
600def buildRecipe(recipe, basedir, archList):
601    """
602    Build software using a recipe. This function does the
603    'configure;make;make install' dance for C software, with a possibility
604    to customize this process, basically a poor-mans DarwinPorts.
605    """
606    curdir = os.getcwd()
607
608    name = recipe['name']
609    url = recipe['url']
610    configure = recipe.get('configure', './configure')
611    install = recipe.get('install', 'make && make install DESTDIR=%s'%(
612        shellQuote(basedir)))
613
614    archiveName = os.path.split(url)[-1]
615    sourceArchive = os.path.join(DEPSRC, archiveName)
616
617    if not os.path.exists(DEPSRC):
618        os.mkdir(DEPSRC)
619
620
621    if os.path.exists(sourceArchive):
622        print "Using local copy of %s"%(name,)
623
624    else:
625        print "Did not find local copy of %s"%(name,)
626        print "Downloading %s"%(name,)
627        downloadURL(url, sourceArchive)
628        print "Archive for %s stored as %s"%(name, sourceArchive)
629
630    print "Extracting archive for %s"%(name,)
631    buildDir=os.path.join(WORKDIR, '_bld')
632    if not os.path.exists(buildDir):
633        os.mkdir(buildDir)
634
635    workDir = extractArchive(buildDir, sourceArchive)
636    os.chdir(workDir)
637    if 'buildDir' in recipe:
638        os.chdir(recipe['buildDir'])
639
640
641    for fn in recipe.get('patches', ()):
642        if fn.startswith('http://'):
643            # Download the patch before applying it.
644            path = os.path.join(DEPSRC, os.path.basename(fn))
645            downloadURL(fn, path)
646            fn = path
647
648        fn = os.path.join(curdir, fn)
649        runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
650            shellQuote(fn),))
651
652    if configure is not None:
653        configure_args = [
654            "--prefix=/usr/local",
655            "--enable-static",
656            "--disable-shared",
657            #"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
658        ]
659
660        if 'configure_pre' in recipe:
661            args = list(recipe['configure_pre'])
662            if '--disable-static' in args:
663                configure_args.remove('--enable-static')
664            if '--enable-shared' in args:
665                configure_args.remove('--disable-shared')
666            configure_args.extend(args)
667
668        if recipe.get('useLDFlags', 1):
669            configure_args.extend([
670                "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
671                        ' -arch '.join(archList),
672                        shellQuote(SDKPATH)[1:-1],
673                        shellQuote(basedir)[1:-1],),
674                "LDFLAGS=-syslibroot,%s -L%s/usr/local/lib -arch %s"%(
675                    shellQuote(SDKPATH)[1:-1],
676                    shellQuote(basedir)[1:-1],
677                    ' -arch '.join(archList)),
678            ])
679        else:
680            configure_args.extend([
681                "CFLAGS=-arch %s -isysroot %s -I%s/usr/local/include"%(
682                        ' -arch '.join(archList),
683                        shellQuote(SDKPATH)[1:-1],
684                        shellQuote(basedir)[1:-1],),
685            ])
686
687        if 'configure_post' in recipe:
688            configure_args = configure_args = list(recipe['configure_post'])
689
690        configure_args.insert(0, configure)
691        configure_args = [ shellQuote(a) for a in configure_args ]
692
693        print "Running configure for %s"%(name,)
694        runCommand(' '.join(configure_args) + ' 2>&1')
695
696    print "Running install for %s"%(name,)
697    runCommand('{ ' + install + ' ;} 2>&1')
698
699    print "Done %s"%(name,)
700    print ""
701
702    os.chdir(curdir)
703
704def buildLibraries():
705    """
706    Build our dependencies into $WORKDIR/libraries/usr/local
707    """
708    print ""
709    print "Building required libraries"
710    print ""
711    universal = os.path.join(WORKDIR, 'libraries')
712    os.mkdir(universal)
713    os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
714    os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))
715
716    for recipe in library_recipes():
717        buildRecipe(recipe, universal, ARCHLIST)
718
719
720
721def buildPythonDocs():
722    # This stores the documentation as Resources/English.lproj/Documentation
723    # inside the framwork. pydoc and IDLE will pick it up there.
724    print "Install python documentation"
725    rootDir = os.path.join(WORKDIR, '_root')
726    buildDir = os.path.join('../../Doc')
727    docdir = os.path.join(rootDir, 'pydocs')
728    curDir = os.getcwd()
729    os.chdir(buildDir)
730    runCommand('make update')
731    runCommand("make html PYTHON='%s'" % os.path.abspath(sys.executable))
732    os.chdir(curDir)
733    if not os.path.exists(docdir):
734        os.mkdir(docdir)
735    os.rename(os.path.join(buildDir, 'build', 'html'), docdir)
736
737
738def buildPython():
739    print "Building a universal python for %s architectures" % UNIVERSALARCHS
740
741    buildDir = os.path.join(WORKDIR, '_bld', 'python')
742    rootDir = os.path.join(WORKDIR, '_root')
743
744    if os.path.exists(buildDir):
745        shutil.rmtree(buildDir)
746    if os.path.exists(rootDir):
747        shutil.rmtree(rootDir)
748    os.mkdir(buildDir)
749    os.mkdir(rootDir)
750    os.mkdir(os.path.join(rootDir, 'empty-dir'))
751    curdir = os.getcwd()
752    os.chdir(buildDir)
753
754    # Not sure if this is still needed, the original build script
755    # claims that parts of the install assume python.exe exists.
756    os.symlink('python', os.path.join(buildDir, 'python.exe'))
757
758    # Extract the version from the configure file, needed to calculate
759    # several paths.
760    version = getVersion()
761
762    # Since the extra libs are not in their installed framework location
763    # during the build, augment the library path so that the interpreter
764    # will find them during its extension import sanity checks.
765    os.environ['DYLD_LIBRARY_PATH'] = os.path.join(WORKDIR,
766                                        'libraries', 'usr', 'local', 'lib')
767    print "Running configure..."
768    runCommand("%s -C --enable-framework --enable-universalsdk=%s "
769               "--with-universal-archs=%s "
770               "LDFLAGS='-g -L%s/libraries/usr/local/lib' "
771               "OPT='-g -O3 -I%s/libraries/usr/local/include' 2>&1"%(
772        shellQuote(os.path.join(SRCDIR, 'configure')), shellQuote(SDKPATH),
773        UNIVERSALARCHS,
774        shellQuote(WORKDIR)[1:-1],
775        shellQuote(WORKDIR)[1:-1]))
776
777    print "Running make"
778    runCommand("make")
779
780    print "Running make frameworkinstall"
781    runCommand("make frameworkinstall DESTDIR=%s"%(
782        shellQuote(rootDir)))
783
784    print "Running make frameworkinstallextras"
785    runCommand("make frameworkinstallextras DESTDIR=%s"%(
786        shellQuote(rootDir)))
787
788    del os.environ['DYLD_LIBRARY_PATH']
789    print "Copying required shared libraries"
790    if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
791        runCommand("mv %s/* %s"%(
792            shellQuote(os.path.join(
793                WORKDIR, 'libraries', 'Library', 'Frameworks',
794                'Python.framework', 'Versions', getVersion(),
795                'lib')),
796            shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
797                'Python.framework', 'Versions', getVersion(),
798                'lib'))))
799
800    print "Fix file modes"
801    frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
802    gid = grp.getgrnam('admin').gr_gid
803
804    for dirpath, dirnames, filenames in os.walk(frmDir):
805        for dn in dirnames:
806            os.chmod(os.path.join(dirpath, dn), 0775)
807            os.chown(os.path.join(dirpath, dn), -1, gid)
808
809
810        for fn in filenames:
811            if os.path.islink(fn):
812                continue
813
814            # "chmod g+w $fn"
815            p = os.path.join(dirpath, fn)
816            st = os.stat(p)
817            os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
818            os.chown(p, -1, gid)
819
820    # We added some directories to the search path during the configure
821    # phase. Remove those because those directories won't be there on
822    # the end-users system.
823    path =os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework',
824                'Versions', version, 'lib', 'python%s'%(version,),
825                'config', 'Makefile')
826    fp = open(path, 'r')
827    data = fp.read()
828    fp.close()
829
830    data = data.replace('-L%s/libraries/usr/local/lib'%(WORKDIR,), '')
831    data = data.replace('-I%s/libraries/usr/local/include'%(WORKDIR,), '')
832    fp = open(path, 'w')
833    fp.write(data)
834    fp.close()
835
836    # Add symlinks in /usr/local/bin, using relative links
837    usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
838    to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
839            'Python.framework', 'Versions', version, 'bin')
840    if os.path.exists(usr_local_bin):
841        shutil.rmtree(usr_local_bin)
842    os.makedirs(usr_local_bin)
843    for fn in os.listdir(
844                os.path.join(frmDir, 'Versions', version, 'bin')):
845        os.symlink(os.path.join(to_framework, fn),
846                   os.path.join(usr_local_bin, fn))
847
848    os.chdir(curdir)
849
850
851
852def patchFile(inPath, outPath):
853    data = fileContents(inPath)
854    data = data.replace('$FULL_VERSION', getFullVersion())
855    data = data.replace('$VERSION', getVersion())
856    data = data.replace('$MACOSX_DEPLOYMENT_TARGET', ''.join((DEPTARGET, ' or later')))
857    data = data.replace('$ARCHITECTURES', "i386, ppc")
858    data = data.replace('$INSTALL_SIZE', installSize())
859
860    # This one is not handy as a template variable
861    data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
862    fp = open(outPath, 'wb')
863    fp.write(data)
864    fp.close()
865
866def patchScript(inPath, outPath):
867    data = fileContents(inPath)
868    data = data.replace('@PYVER@', getVersion())
869    fp = open(outPath, 'wb')
870    fp.write(data)
871    fp.close()
872    os.chmod(outPath, 0755)
873
874
875
876def packageFromRecipe(targetDir, recipe):
877    curdir = os.getcwd()
878    try:
879        # The major version (such as 2.5) is included in the package name
880        # because having two version of python installed at the same time is
881        # common.
882        pkgname = '%s-%s'%(recipe['name'], getVersion())
883        srcdir  = recipe.get('source')
884        pkgroot = recipe.get('topdir', srcdir)
885        postflight = recipe.get('postflight')
886        readme = textwrap.dedent(recipe['readme'])
887        isRequired = recipe.get('required', True)
888
889        print "- building package %s"%(pkgname,)
890
891        # Substitute some variables
892        textvars = dict(
893            VER=getVersion(),
894            FULLVER=getFullVersion(),
895        )
896        readme = readme % textvars
897
898        if pkgroot is not None:
899            pkgroot = pkgroot % textvars
900        else:
901            pkgroot = '/'
902
903        if srcdir is not None:
904            srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
905            srcdir = srcdir % textvars
906
907        if postflight is not None:
908            postflight = os.path.abspath(postflight)
909
910        packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
911        os.makedirs(packageContents)
912
913        if srcdir is not None:
914            os.chdir(srcdir)
915            runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
916            runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
917            runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
918
919        fn = os.path.join(packageContents, 'PkgInfo')
920        fp = open(fn, 'w')
921        fp.write('pmkrpkg1')
922        fp.close()
923
924        rsrcDir = os.path.join(packageContents, "Resources")
925        os.mkdir(rsrcDir)
926        fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
927        fp.write(readme)
928        fp.close()
929
930        if postflight is not None:
931            patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
932
933        vers = getFullVersion()
934        major, minor = map(int, getVersion().split('.', 2))
935        pl = Plist(
936                CFBundleGetInfoString="Python.%s %s"%(pkgname, vers,),
937                CFBundleIdentifier='org.python.Python.%s'%(pkgname,),
938                CFBundleName='Python.%s'%(pkgname,),
939                CFBundleShortVersionString=vers,
940                IFMajorVersion=major,
941                IFMinorVersion=minor,
942                IFPkgFormatVersion=0.10000000149011612,
943                IFPkgFlagAllowBackRev=False,
944                IFPkgFlagAuthorizationAction="RootAuthorization",
945                IFPkgFlagDefaultLocation=pkgroot,
946                IFPkgFlagFollowLinks=True,
947                IFPkgFlagInstallFat=True,
948                IFPkgFlagIsRequired=isRequired,
949                IFPkgFlagOverwritePermissions=False,
950                IFPkgFlagRelocatable=False,
951                IFPkgFlagRestartAction="NoRestart",
952                IFPkgFlagRootVolumeOnly=True,
953                IFPkgFlagUpdateInstalledLangauges=False,
954            )
955        writePlist(pl, os.path.join(packageContents, 'Info.plist'))
956
957        pl = Plist(
958                    IFPkgDescriptionDescription=readme,
959                    IFPkgDescriptionTitle=recipe.get('long_name', "Python.%s"%(pkgname,)),
960                    IFPkgDescriptionVersion=vers,
961                )
962        writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
963
964    finally:
965        os.chdir(curdir)
966
967
968def makeMpkgPlist(path):
969
970    vers = getFullVersion()
971    major, minor = map(int, getVersion().split('.', 2))
972
973    pl = Plist(
974            CFBundleGetInfoString="Python %s"%(vers,),
975            CFBundleIdentifier='org.python.Python',
976            CFBundleName='Python',
977            CFBundleShortVersionString=vers,
978            IFMajorVersion=major,
979            IFMinorVersion=minor,
980            IFPkgFlagComponentDirectory="Contents/Packages",
981            IFPkgFlagPackageList=[
982                dict(
983                    IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
984                    IFPkgFlagPackageSelection=item.get('selected', 'selected'),
985                )
986                for item in pkg_recipes()
987            ],
988            IFPkgFormatVersion=0.10000000149011612,
989            IFPkgFlagBackgroundScaling="proportional",
990            IFPkgFlagBackgroundAlignment="left",
991            IFPkgFlagAuthorizationAction="RootAuthorization",
992        )
993
994    writePlist(pl, path)
995
996
997def buildInstaller():
998
999    # Zap all compiled files
1000    for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
1001        for fn in filenames:
1002            if fn.endswith('.pyc') or fn.endswith('.pyo'):
1003                os.unlink(os.path.join(dirpath, fn))
1004
1005    outdir = os.path.join(WORKDIR, 'installer')
1006    if os.path.exists(outdir):
1007        shutil.rmtree(outdir)
1008    os.mkdir(outdir)
1009
1010    pkgroot = os.path.join(outdir, 'Python.mpkg', 'Contents')
1011    pkgcontents = os.path.join(pkgroot, 'Packages')
1012    os.makedirs(pkgcontents)
1013    for recipe in pkg_recipes():
1014        packageFromRecipe(pkgcontents, recipe)
1015
1016    rsrcDir = os.path.join(pkgroot, 'Resources')
1017
1018    fn = os.path.join(pkgroot, 'PkgInfo')
1019    fp = open(fn, 'w')
1020    fp.write('pmkrpkg1')
1021    fp.close()
1022
1023    os.mkdir(rsrcDir)
1024
1025    makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
1026    pl = Plist(
1027                IFPkgDescriptionTitle="Python",
1028                IFPkgDescriptionVersion=getVersion(),
1029            )
1030
1031    writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
1032    for fn in os.listdir('resources'):
1033        if fn == '.svn': continue
1034        if fn.endswith('.jpg'):
1035            shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
1036        else:
1037            patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
1038
1039    shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
1040
1041
1042def installSize(clear=False, _saved=[]):
1043    if clear:
1044        del _saved[:]
1045    if not _saved:
1046        data = captureCommand("du -ks %s"%(
1047                    shellQuote(os.path.join(WORKDIR, '_root'))))
1048        _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
1049    return _saved[0]
1050
1051
1052def buildDMG():
1053    """
1054    Create DMG containing the rootDir.
1055    """
1056    outdir = os.path.join(WORKDIR, 'diskimage')
1057    if os.path.exists(outdir):
1058        shutil.rmtree(outdir)
1059
1060    imagepath = os.path.join(outdir,
1061                    'python-%s-macosx%s'%(getFullVersion(),DEPTARGET))
1062    if INCLUDE_TIMESTAMP:
1063        imagepath = imagepath + '-%04d-%02d-%02d'%(time.localtime()[:3])
1064    imagepath = imagepath + '.dmg'
1065
1066    os.mkdir(outdir)
1067    volname='Python %s'%(getFullVersion())
1068    runCommand("hdiutil create -format UDRW -volname %s -srcfolder %s %s"%(
1069            shellQuote(volname),
1070            shellQuote(os.path.join(WORKDIR, 'installer')),
1071            shellQuote(imagepath + ".tmp.dmg" )))
1072
1073
1074    if not os.path.exists(os.path.join(WORKDIR, "mnt")):
1075        os.mkdir(os.path.join(WORKDIR, "mnt"))
1076    runCommand("hdiutil attach %s -mountroot %s"%(
1077        shellQuote(imagepath + ".tmp.dmg"), shellQuote(os.path.join(WORKDIR, "mnt"))))
1078
1079    # Custom icon for the DMG, shown when the DMG is mounted.
1080    shutil.copy("../Icons/Disk Image.icns",
1081            os.path.join(WORKDIR, "mnt", volname, ".VolumeIcon.icns"))
1082    runCommand("/Developer/Tools/SetFile -a C %s/"%(
1083            shellQuote(os.path.join(WORKDIR, "mnt", volname)),))
1084
1085    runCommand("hdiutil detach %s"%(shellQuote(os.path.join(WORKDIR, "mnt", volname))))
1086
1087    setIcon(imagepath + ".tmp.dmg", "../Icons/Disk Image.icns")
1088    runCommand("hdiutil convert %s -format UDZO -o %s"%(
1089            shellQuote(imagepath + ".tmp.dmg"), shellQuote(imagepath)))
1090    setIcon(imagepath, "../Icons/Disk Image.icns")
1091
1092    os.unlink(imagepath + ".tmp.dmg")
1093
1094    return imagepath
1095
1096
1097def setIcon(filePath, icnsPath):
1098    """
1099    Set the custom icon for the specified file or directory.
1100    """
1101
1102    toolPath = os.path.join(os.path.dirname(__file__), "seticon.app/Contents/MacOS/seticon")
1103    dirPath = os.path.dirname(__file__)
1104    if not os.path.exists(toolPath) or os.stat(toolPath).st_mtime < os.stat(dirPath + '/seticon.m').st_mtime:
1105        # NOTE: The tool is created inside an .app bundle, otherwise it won't work due
1106        # to connections to the window server.
1107        if not os.path.exists('seticon.app/Contents/MacOS'):
1108            os.makedirs('seticon.app/Contents/MacOS')
1109        runCommand("cc -o %s %s/seticon.m -framework Cocoa"%(
1110            shellQuote(toolPath), shellQuote(dirPath)))
1111
1112    runCommand("%s %s %s"%(shellQuote(os.path.abspath(toolPath)), shellQuote(icnsPath),
1113        shellQuote(filePath)))
1114
1115def main():
1116    # First parse options and check if we can perform our work
1117    parseOptions()
1118    checkEnvironment()
1119
1120    os.environ['MACOSX_DEPLOYMENT_TARGET'] = DEPTARGET
1121    os.environ['CC'] = CC
1122
1123    if os.path.exists(WORKDIR):
1124        shutil.rmtree(WORKDIR)
1125    os.mkdir(WORKDIR)
1126
1127    os.environ['LC_ALL'] = 'C'
1128
1129    # Then build third-party libraries such as sleepycat DB4.
1130    buildLibraries()
1131
1132    # Now build python itself
1133    buildPython()
1134
1135    # And then build the documentation
1136    # Remove the Deployment Target from the shell
1137    # environment, it's no longer needed and
1138    # an unexpected build target can cause problems
1139    # when Sphinx and its dependencies need to
1140    # be (re-)installed.
1141    del os.environ['MACOSX_DEPLOYMENT_TARGET']
1142    buildPythonDocs()
1143
1144
1145    # Prepare the applications folder
1146    fn = os.path.join(WORKDIR, "_root", "Applications",
1147                "Python %s"%(getVersion(),), "Update Shell Profile.command")
1148    patchScript("scripts/postflight.patch-profile",  fn)
1149
1150    folder = os.path.join(WORKDIR, "_root", "Applications", "Python %s"%(
1151        getVersion(),))
1152    os.chmod(folder, 0755)
1153    setIcon(folder, "../Icons/Python Folder.icns")
1154
1155    # Create the installer
1156    buildInstaller()
1157
1158    # And copy the readme into the directory containing the installer
1159    patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
1160
1161    # Ditto for the license file.
1162    shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
1163
1164    fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
1165    print >> fp, "# BUILD INFO"
1166    print >> fp, "# Date:", time.ctime()
1167    print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
1168    fp.close()
1169
1170    # And copy it to a DMG
1171    buildDMG()
1172
1173if __name__ == "__main__":
1174    main()
1175