PageRenderTime 1049ms CodeModel.GetById 252ms app.highlight 462ms RepoModel.GetById 198ms app.codeStats 1ms

/Mac/BuildScript/build-installer.py

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