~malept/ubuntu/lucid/python2.6/dev-dependency-fix

« back to all changes in this revision

Viewing changes to Mac/BuildScript/build-installer.py

  • Committer: Bazaar Package Importer
  • Author(s): Matthias Klose
  • Date: 2009-02-13 12:51:00 UTC
  • Revision ID: james.westby@ubuntu.com-20090213125100-uufgcb9yeqzujpqw
Tags: upstream-2.6.1
ImportĀ upstreamĀ versionĀ 2.6.1

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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.
 
6
 
 
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)
 
9
 
 
10
Usage: see USAGE variable in the script.
 
11
"""
 
12
import platform, os, sys, getopt, textwrap, shutil, urllib2, stat, time, pwd
 
13
import grp
 
14
 
 
15
INCLUDE_TIMESTAMP = 1
 
16
VERBOSE = 1
 
17
 
 
18
from plistlib import Plist
 
19
 
 
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
 
26
 
 
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)
 
33
 
 
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("'", "'\"'\"'"))
 
40
 
 
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]
 
47
 
 
48
def getVersion():
 
49
    return grepValue(os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
 
50
 
 
51
def getFullVersion():
 
52
    fn = os.path.join(SRCDIR, 'Include', 'patchlevel.h')
 
53
    for ln in open(fn):
 
54
        if 'PY_VERSION' in ln:
 
55
            return ln.split()[-1][1:-1]
 
56
 
 
57
    raise RuntimeError, "Cannot find full version??"
 
58
 
 
59
# The directory we'll use to create the build (will be erased and recreated)
 
60
WORKDIR = "/tmp/_py"
 
61
 
 
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')
 
66
 
 
67
# Location of the preferred SDK
 
68
SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
 
69
#SDKPATH = "/"
 
70
 
 
71
ARCHLIST = ('i386', 'ppc',)
 
72
 
 
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
        ))))
 
79
 
 
80
USAGE = textwrap.dedent("""\
 
81
    Usage: build_python [options]
 
82
 
 
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()
 
91
 
 
92
 
 
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,
 
104
        ),
 
105
    ),
 
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,
 
114
        ),
 
115
    ),
 
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',
 
128
        ]
 
129
    ),
 
130
 
 
131
    dict(
 
132
        name="SQLite 3.6.3",
 
133
        url="http://www.sqlite.org/sqlite-3.6.3.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',
 
141
        ]
 
142
    ),
 
143
 
 
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",
 
161
        ],
 
162
        patches=[
 
163
            "ncurses-5.5.patch",
 
164
        ],
 
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(),
 
170
            ),
 
171
    ),
 
172
    dict(
 
173
        name="Sleepycat DB 4.7.25",
 
174
        url="http://download.oracle.com/berkeley-db/db-4.7.25.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',
 
181
        ]
 
182
    ),
 
183
]
 
184
 
 
185
 
 
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",
 
198
    ),
 
199
    dict(
 
200
        name="PythonApplications",
 
201
        long_name="GUI Applications",
 
202
        source="/Applications/Python %(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).
 
207
 
 
208
            It also installs a number of examples and demos.
 
209
            """,
 
210
        required=False,
 
211
    ),
 
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,
 
222
    ),
 
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,
 
236
    ),
 
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.
 
244
 
 
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,
 
253
    ),
 
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,
 
266
    )
 
267
]
 
268
 
 
269
def fatal(msg):
 
270
    """
 
271
    A fatal error, bail out.
 
272
    """
 
273
    sys.stderr.write('FATAL: ')
 
274
    sys.stderr.write(msg)
 
275
    sys.stderr.write('\n')
 
276
    sys.exit(1)
 
277
 
 
278
def fileContents(fn):
 
279
    """
 
280
    Return the contents of the named file
 
281
    """
 
282
    return open(fn, 'rb').read()
 
283
 
 
284
def runCommand(commandline):
 
285
    """
 
286
    Run a command and raise RuntimeError if it fails. Output is surpressed
 
287
    unless the command fails.
 
288
    """
 
289
    fd = os.popen(commandline, 'r')
 
290
    data = fd.read()
 
291
    xit = fd.close()
 
292
    if xit is not None:
 
293
        sys.stdout.write(data)
 
294
        raise RuntimeError, "command failed: %s"%(commandline,)
 
295
 
 
296
    if VERBOSE:
 
297
        sys.stdout.write(data); sys.stdout.flush()
 
298
 
 
299
def captureCommand(commandline):
 
300
    fd = os.popen(commandline, 'r')
 
301
    data = fd.read()
 
302
    xit = fd.close()
 
303
    if xit is not None:
 
304
        sys.stdout.write(data)
 
305
        raise RuntimeError, "command failed: %s"%(commandline,)
 
306
 
 
307
    return data
 
308
 
 
309
def checkEnvironment():
 
310
    """
 
311
    Check that we're running on a supported system.
 
312
    """
 
313
 
 
314
    if platform.system() != 'Darwin':
 
315
        fatal("This script should be run on a Mac OS X 10.4 system")
 
316
 
 
317
    if platform.release() <= '8.':
 
318
        fatal("This script should be run on a Mac OS X 10.4 system")
 
319
 
 
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])))
 
323
 
 
324
 
 
325
 
 
326
def parseOptions(args=None):
 
327
    """
 
328
    Parse arguments and update global settings.
 
329
    """
 
330
    global WORKDIR, DEPSRC, SDKPATH, SRCDIR
 
331
 
 
332
    if args is None:
 
333
        args = sys.argv[1:]
 
334
 
 
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)
 
341
 
 
342
    if args:
 
343
        print "Additional arguments"
 
344
        sys.exit(1)
 
345
 
 
346
    for k, v in options:
 
347
        if k in ('-h', '-?'):
 
348
            print USAGE
 
349
            sys.exit(0)
 
350
 
 
351
        elif k in ('-d', '--build-dir'):
 
352
            WORKDIR=v
 
353
 
 
354
        elif k in ('--third-party',):
 
355
            DEPSRC=v
 
356
 
 
357
        elif k in ('--sdk-path',):
 
358
            SDKPATH=v
 
359
 
 
360
        elif k in ('--src-dir',):
 
361
            SRCDIR=v
 
362
 
 
363
        else:
 
364
            raise NotImplementedError, k
 
365
 
 
366
    SRCDIR=os.path.abspath(SRCDIR)
 
367
    WORKDIR=os.path.abspath(WORKDIR)
 
368
    SDKPATH=os.path.abspath(SDKPATH)
 
369
    DEPSRC=os.path.abspath(DEPSRC)
 
370
 
 
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 ""
 
377
 
 
378
 
 
379
 
 
380
 
 
381
def extractArchive(builddir, archiveName):
 
382
    """
 
383
    Extract a source archive into 'builddir'. Returns the path of the
 
384
    extracted archive.
 
385
 
 
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.
 
389
    """
 
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')
 
398
 
 
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')
 
404
 
 
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')
 
410
 
 
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')
 
416
 
 
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,)
 
422
 
 
423
        return os.path.join(builddir, retval)
 
424
 
 
425
    finally:
 
426
        os.chdir(curdir)
 
427
 
 
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,
 
431
}
 
432
 
 
433
def downloadURL(url, fname):
 
434
    """
 
435
    Download the contents of the url into the file.
 
436
    """
 
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
 
459
 
 
460
def buildRecipe(recipe, basedir, archList):
 
461
    """
 
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.
 
465
    """
 
466
    curdir = os.getcwd()
 
467
 
 
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)))
 
473
 
 
474
    archiveName = os.path.split(url)[-1]
 
475
    sourceArchive = os.path.join(DEPSRC, archiveName)
 
476
 
 
477
    if not os.path.exists(DEPSRC):
 
478
        os.mkdir(DEPSRC)
 
479
 
 
480
 
 
481
    if os.path.exists(sourceArchive):
 
482
        print "Using local copy of %s"%(name,)
 
483
 
 
484
    else:
 
485
        print "Downloading %s"%(name,)
 
486
        downloadURL(url, sourceArchive)
 
487
        print "Archive for %s stored as %s"%(name, sourceArchive)
 
488
 
 
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)
 
493
 
 
494
    workDir = extractArchive(buildDir, sourceArchive)
 
495
    os.chdir(workDir)
 
496
    if 'buildDir' in recipe:
 
497
        os.chdir(recipe['buildDir'])
 
498
 
 
499
 
 
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
 
506
 
 
507
        fn = os.path.join(curdir, fn)
 
508
        runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
 
509
            shellQuote(fn),))
 
510
 
 
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,),),
 
517
        ]
 
518
 
 
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)
 
526
 
 
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)),
 
537
            ])
 
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],),
 
544
            ])
 
545
 
 
546
        if 'configure_post' in recipe:
 
547
            configure_args = configure_args = list(recipe['configure_post'])
 
548
 
 
549
        configure_args.insert(0, configure)
 
550
        configure_args = [ shellQuote(a) for a in configure_args ]
 
551
 
 
552
        print "Running configure for %s"%(name,)
 
553
        runCommand(' '.join(configure_args) + ' 2>&1')
 
554
 
 
555
    print "Running install for %s"%(name,)
 
556
    runCommand('{ ' + install + ' ;} 2>&1')
 
557
 
 
558
    print "Done %s"%(name,)
 
559
    print ""
 
560
 
 
561
    os.chdir(curdir)
 
562
 
 
563
def buildLibraries():
 
564
    """
 
565
    Build our dependencies into $WORKDIR/libraries/usr/local
 
566
    """
 
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'))
 
574
 
 
575
    for recipe in LIBRARY_RECIPES:
 
576
        buildRecipe(recipe, universal, ARCHLIST)
 
577
 
 
578
 
 
579
 
 
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')
 
587
 
 
588
    novername = 'python-docs-html.tar.bz2'
 
589
    name = 'html-%s.tar.bz2'%(getFullVersion(),)
 
590
    sourceArchive = os.path.join(DEPSRC, name)
 
591
    if os.path.exists(sourceArchive):
 
592
        print "Using local copy of %s"%(name,)
 
593
 
 
594
    else:
 
595
        print "Downloading %s"%(novername,)
 
596
        downloadURL('http://www.python.org/ftp/python/doc/%s/%s'%(
 
597
            getFullVersion(), novername), sourceArchive)
 
598
        print "Archive for %s stored as %s"%(name, sourceArchive)
 
599
 
 
600
    extractArchive(os.path.dirname(docdir), sourceArchive)
 
601
 
 
602
    os.rename(
 
603
            os.path.join(
 
604
                os.path.dirname(docdir), 'python-docs-html'),
 
605
            docdir)
 
606
 
 
607
 
 
608
def buildPython():
 
609
    print "Building a universal python"
 
610
 
 
611
    buildDir = os.path.join(WORKDIR, '_bld', 'python')
 
612
    rootDir = os.path.join(WORKDIR, '_root')
 
613
 
 
614
    if os.path.exists(buildDir):
 
615
        shutil.rmtree(buildDir)
 
616
    if os.path.exists(rootDir):
 
617
        shutil.rmtree(rootDir)
 
618
    os.mkdir(buildDir)
 
619
    os.mkdir(rootDir)
 
620
    os.mkdir(os.path.join(rootDir, 'empty-dir'))
 
621
    curdir = os.getcwd()
 
622
    os.chdir(buildDir)
 
623
 
 
624
    # Not sure if this is still needed, the original build script
 
625
    # claims that parts of the install assume python.exe exists.
 
626
    os.symlink('python', os.path.join(buildDir, 'python.exe'))
 
627
 
 
628
    # Extract the version from the configure file, needed to calculate
 
629
    # several paths.
 
630
    version = getVersion()
 
631
 
 
632
    print "Running configure..."
 
633
    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"%(
 
634
        shellQuote(os.path.join(SRCDIR, 'configure')),
 
635
        shellQuote(SDKPATH), shellQuote(WORKDIR)[1:-1],
 
636
        shellQuote(WORKDIR)[1:-1]))
 
637
 
 
638
    print "Running make"
 
639
    runCommand("make")
 
640
 
 
641
    print "Running make frameworkinstall"
 
642
    runCommand("make frameworkinstall DESTDIR=%s"%(
 
643
        shellQuote(rootDir)))
 
644
 
 
645
    print "Running make frameworkinstallextras"
 
646
    runCommand("make frameworkinstallextras DESTDIR=%s"%(
 
647
        shellQuote(rootDir)))
 
648
 
 
649
    print "Copying required shared libraries"
 
650
    if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
 
651
        runCommand("mv %s/* %s"%(
 
652
            shellQuote(os.path.join(
 
653
                WORKDIR, 'libraries', 'Library', 'Frameworks',
 
654
                'Python.framework', 'Versions', getVersion(),
 
655
                'lib')),
 
656
            shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
 
657
                'Python.framework', 'Versions', getVersion(),
 
658
                'lib'))))
 
659
 
 
660
    print "Fix file modes"
 
661
    frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
 
662
    gid = grp.getgrnam('admin').gr_gid
 
663
 
 
664
    for dirpath, dirnames, filenames in os.walk(frmDir):
 
665
        for dn in dirnames:
 
666
            os.chmod(os.path.join(dirpath, dn), 0775)
 
667
            os.chown(os.path.join(dirpath, dn), -1, gid)
 
668
 
 
669
 
 
670
        for fn in filenames:
 
671
            if os.path.islink(fn):
 
672
                continue
 
673
 
 
674
            # "chmod g+w $fn"
 
675
            p = os.path.join(dirpath, fn)
 
676
            st = os.stat(p)
 
677
            os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
 
678
            os.chown(p, -1, gid)
 
679
 
 
680
    # We added some directories to the search path during the configure
 
681
    # phase. Remove those because those directories won't be there on
 
682
    # the end-users system.
 
683
    path =os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework',
 
684
                'Versions', version, 'lib', 'python%s'%(version,),
 
685
                'config', 'Makefile')
 
686
    fp = open(path, 'r')
 
687
    data = fp.read()
 
688
    fp.close()
 
689
 
 
690
    data = data.replace('-L%s/libraries/usr/local/lib'%(WORKDIR,), '')
 
691
    data = data.replace('-I%s/libraries/usr/local/include'%(WORKDIR,), '')
 
692
    fp = open(path, 'w')
 
693
    fp.write(data)
 
694
    fp.close()
 
695
 
 
696
    # Add symlinks in /usr/local/bin, using relative links
 
697
    usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
 
698
    to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
 
699
            'Python.framework', 'Versions', version, 'bin')
 
700
    if os.path.exists(usr_local_bin):
 
701
        shutil.rmtree(usr_local_bin)
 
702
    os.makedirs(usr_local_bin)
 
703
    for fn in os.listdir(
 
704
                os.path.join(frmDir, 'Versions', version, 'bin')):
 
705
        os.symlink(os.path.join(to_framework, fn),
 
706
                   os.path.join(usr_local_bin, fn))
 
707
 
 
708
    os.chdir(curdir)
 
709
 
 
710
 
 
711
 
 
712
def patchFile(inPath, outPath):
 
713
    data = fileContents(inPath)
 
714
    data = data.replace('$FULL_VERSION', getFullVersion())
 
715
    data = data.replace('$VERSION', getVersion())
 
716
    data = data.replace('$MACOSX_DEPLOYMENT_TARGET', '10.3 or later')
 
717
    data = data.replace('$ARCHITECTURES', "i386, ppc")
 
718
    data = data.replace('$INSTALL_SIZE', installSize())
 
719
 
 
720
    # This one is not handy as a template variable
 
721
    data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
 
722
    fp = open(outPath, 'wb')
 
723
    fp.write(data)
 
724
    fp.close()
 
725
 
 
726
def patchScript(inPath, outPath):
 
727
    data = fileContents(inPath)
 
728
    data = data.replace('@PYVER@', getVersion())
 
729
    fp = open(outPath, 'wb')
 
730
    fp.write(data)
 
731
    fp.close()
 
732
    os.chmod(outPath, 0755)
 
733
 
 
734
 
 
735
 
 
736
def packageFromRecipe(targetDir, recipe):
 
737
    curdir = os.getcwd()
 
738
    try:
 
739
        # The major version (such as 2.5) is included in the package name
 
740
        # because having two version of python installed at the same time is
 
741
        # common.
 
742
        pkgname = '%s-%s'%(recipe['name'], getVersion())
 
743
        srcdir  = recipe.get('source')
 
744
        pkgroot = recipe.get('topdir', srcdir)
 
745
        postflight = recipe.get('postflight')
 
746
        readme = textwrap.dedent(recipe['readme'])
 
747
        isRequired = recipe.get('required', True)
 
748
 
 
749
        print "- building package %s"%(pkgname,)
 
750
 
 
751
        # Substitute some variables
 
752
        textvars = dict(
 
753
            VER=getVersion(),
 
754
            FULLVER=getFullVersion(),
 
755
        )
 
756
        readme = readme % textvars
 
757
 
 
758
        if pkgroot is not None:
 
759
            pkgroot = pkgroot % textvars
 
760
        else:
 
761
            pkgroot = '/'
 
762
 
 
763
        if srcdir is not None:
 
764
            srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
 
765
            srcdir = srcdir % textvars
 
766
 
 
767
        if postflight is not None:
 
768
            postflight = os.path.abspath(postflight)
 
769
 
 
770
        packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
 
771
        os.makedirs(packageContents)
 
772
 
 
773
        if srcdir is not None:
 
774
            os.chdir(srcdir)
 
775
            runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
 
776
            runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
 
777
            runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
 
778
 
 
779
        fn = os.path.join(packageContents, 'PkgInfo')
 
780
        fp = open(fn, 'w')
 
781
        fp.write('pmkrpkg1')
 
782
        fp.close()
 
783
 
 
784
        rsrcDir = os.path.join(packageContents, "Resources")
 
785
        os.mkdir(rsrcDir)
 
786
        fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
 
787
        fp.write(readme)
 
788
        fp.close()
 
789
 
 
790
        if postflight is not None:
 
791
            patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
 
792
 
 
793
        vers = getFullVersion()
 
794
        major, minor = map(int, getVersion().split('.', 2))
 
795
        pl = Plist(
 
796
                CFBundleGetInfoString="MacPython.%s %s"%(pkgname, vers,),
 
797
                CFBundleIdentifier='org.python.MacPython.%s'%(pkgname,),
 
798
                CFBundleName='MacPython.%s'%(pkgname,),
 
799
                CFBundleShortVersionString=vers,
 
800
                IFMajorVersion=major,
 
801
                IFMinorVersion=minor,
 
802
                IFPkgFormatVersion=0.10000000149011612,
 
803
                IFPkgFlagAllowBackRev=False,
 
804
                IFPkgFlagAuthorizationAction="RootAuthorization",
 
805
                IFPkgFlagDefaultLocation=pkgroot,
 
806
                IFPkgFlagFollowLinks=True,
 
807
                IFPkgFlagInstallFat=True,
 
808
                IFPkgFlagIsRequired=isRequired,
 
809
                IFPkgFlagOverwritePermissions=False,
 
810
                IFPkgFlagRelocatable=False,
 
811
                IFPkgFlagRestartAction="NoRestart",
 
812
                IFPkgFlagRootVolumeOnly=True,
 
813
                IFPkgFlagUpdateInstalledLangauges=False,
 
814
            )
 
815
        writePlist(pl, os.path.join(packageContents, 'Info.plist'))
 
816
 
 
817
        pl = Plist(
 
818
                    IFPkgDescriptionDescription=readme,
 
819
                    IFPkgDescriptionTitle=recipe.get('long_name', "MacPython.%s"%(pkgname,)),
 
820
                    IFPkgDescriptionVersion=vers,
 
821
                )
 
822
        writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
 
823
 
 
824
    finally:
 
825
        os.chdir(curdir)
 
826
 
 
827
 
 
828
def makeMpkgPlist(path):
 
829
 
 
830
    vers = getFullVersion()
 
831
    major, minor = map(int, getVersion().split('.', 2))
 
832
 
 
833
    pl = Plist(
 
834
            CFBundleGetInfoString="MacPython %s"%(vers,),
 
835
            CFBundleIdentifier='org.python.MacPython',
 
836
            CFBundleName='MacPython',
 
837
            CFBundleShortVersionString=vers,
 
838
            IFMajorVersion=major,
 
839
            IFMinorVersion=minor,
 
840
            IFPkgFlagComponentDirectory="Contents/Packages",
 
841
            IFPkgFlagPackageList=[
 
842
                dict(
 
843
                    IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
 
844
                    IFPkgFlagPackageSelection='selected'
 
845
                )
 
846
                for item in PKG_RECIPES
 
847
            ],
 
848
            IFPkgFormatVersion=0.10000000149011612,
 
849
            IFPkgFlagBackgroundScaling="proportional",
 
850
            IFPkgFlagBackgroundAlignment="left",
 
851
            IFPkgFlagAuthorizationAction="RootAuthorization",
 
852
        )
 
853
 
 
854
    writePlist(pl, path)
 
855
 
 
856
 
 
857
def buildInstaller():
 
858
 
 
859
    # Zap all compiled files
 
860
    for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
 
861
        for fn in filenames:
 
862
            if fn.endswith('.pyc') or fn.endswith('.pyo'):
 
863
                os.unlink(os.path.join(dirpath, fn))
 
864
 
 
865
    outdir = os.path.join(WORKDIR, 'installer')
 
866
    if os.path.exists(outdir):
 
867
        shutil.rmtree(outdir)
 
868
    os.mkdir(outdir)
 
869
 
 
870
    pkgroot = os.path.join(outdir, 'MacPython.mpkg', 'Contents')
 
871
    pkgcontents = os.path.join(pkgroot, 'Packages')
 
872
    os.makedirs(pkgcontents)
 
873
    for recipe in PKG_RECIPES:
 
874
        packageFromRecipe(pkgcontents, recipe)
 
875
 
 
876
    rsrcDir = os.path.join(pkgroot, 'Resources')
 
877
 
 
878
    fn = os.path.join(pkgroot, 'PkgInfo')
 
879
    fp = open(fn, 'w')
 
880
    fp.write('pmkrpkg1')
 
881
    fp.close()
 
882
 
 
883
    os.mkdir(rsrcDir)
 
884
 
 
885
    makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
 
886
    pl = Plist(
 
887
                IFPkgDescriptionTitle="Universal MacPython",
 
888
                IFPkgDescriptionVersion=getVersion(),
 
889
            )
 
890
 
 
891
    writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
 
892
    for fn in os.listdir('resources'):
 
893
        if fn == '.svn': continue
 
894
        if fn.endswith('.jpg'):
 
895
            shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
 
896
        else:
 
897
            patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
 
898
 
 
899
    shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
 
900
 
 
901
 
 
902
def installSize(clear=False, _saved=[]):
 
903
    if clear:
 
904
        del _saved[:]
 
905
    if not _saved:
 
906
        data = captureCommand("du -ks %s"%(
 
907
                    shellQuote(os.path.join(WORKDIR, '_root'))))
 
908
        _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
 
909
    return _saved[0]
 
910
 
 
911
 
 
912
def buildDMG():
 
913
    """
 
914
    Create DMG containing the rootDir.
 
915
    """
 
916
    outdir = os.path.join(WORKDIR, 'diskimage')
 
917
    if os.path.exists(outdir):
 
918
        shutil.rmtree(outdir)
 
919
 
 
920
    imagepath = os.path.join(outdir,
 
921
                    'python-%s-macosx'%(getFullVersion(),))
 
922
    if INCLUDE_TIMESTAMP:
 
923
        imagepath = imagepath + '%04d-%02d-%02d'%(time.localtime()[:3])
 
924
    imagepath = imagepath + '.dmg'
 
925
 
 
926
    os.mkdir(outdir)
 
927
    runCommand("hdiutil create -volname 'Universal MacPython %s' -srcfolder %s %s"%(
 
928
            getFullVersion(),
 
929
            shellQuote(os.path.join(WORKDIR, 'installer')),
 
930
            shellQuote(imagepath)))
 
931
 
 
932
    return imagepath
 
933
 
 
934
 
 
935
def setIcon(filePath, icnsPath):
 
936
    """
 
937
    Set the custom icon for the specified file or directory.
 
938
 
 
939
    For a directory the icon data is written in a file named 'Icon\r' inside
 
940
    the directory. For both files and directories write the icon as an 'icns'
 
941
    resource. Furthermore set kHasCustomIcon in the finder flags for filePath.
 
942
    """
 
943
    ref, isDirectory = Carbon.File.FSPathMakeRef(icnsPath)
 
944
    icon = Carbon.Icn.ReadIconFile(ref)
 
945
    del ref
 
946
 
 
947
    #
 
948
    # Open the resource fork of the target, to add the icon later on.
 
949
    # For directories we use the file 'Icon\r' inside the directory.
 
950
    #
 
951
 
 
952
    ref, isDirectory = Carbon.File.FSPathMakeRef(filePath)
 
953
 
 
954
    if isDirectory:
 
955
        # There is a problem with getting this into the pax(1) archive,
 
956
        # just ignore directory icons for now.
 
957
        return
 
958
 
 
959
        tmpPath = os.path.join(filePath, "Icon\r")
 
960
        if not os.path.exists(tmpPath):
 
961
            fp = open(tmpPath, 'w')
 
962
            fp.close()
 
963
 
 
964
        tmpRef, _ = Carbon.File.FSPathMakeRef(tmpPath)
 
965
        spec = Carbon.File.FSSpec(tmpRef)
 
966
 
 
967
    else:
 
968
        spec = Carbon.File.FSSpec(ref)
 
969
 
 
970
    try:
 
971
        Carbon.Res.HCreateResFile(*spec.as_tuple())
 
972
    except MacOS.Error:
 
973
        pass
 
974
 
 
975
    # Try to create the resource fork again, this will avoid problems
 
976
    # when adding an icon to a directory. I have no idea why this helps,
 
977
    # but without this adding the icon to a directory will fail sometimes.
 
978
    try:
 
979
        Carbon.Res.HCreateResFile(*spec.as_tuple())
 
980
    except MacOS.Error:
 
981
        pass
 
982
 
 
983
    refNum = Carbon.Res.FSpOpenResFile(spec, fsRdWrPerm)
 
984
 
 
985
    Carbon.Res.UseResFile(refNum)
 
986
 
 
987
    # Check if there already is an icon, remove it if there is.
 
988
    try:
 
989
        h = Carbon.Res.Get1Resource('icns', kCustomIconResource)
 
990
    except MacOS.Error:
 
991
        pass
 
992
 
 
993
    else:
 
994
        h.RemoveResource()
 
995
        del h
 
996
 
 
997
    # Add the icon to the resource for of the target
 
998
    res = Carbon.Res.Resource(icon)
 
999
    res.AddResource('icns', kCustomIconResource, '')
 
1000
    res.WriteResource()
 
1001
    res.DetachResource()
 
1002
    Carbon.Res.CloseResFile(refNum)
 
1003
 
 
1004
    # And now set the kHasCustomIcon property for the target. Annoyingly,
 
1005
    # python doesn't seem to have bindings for the API that is needed for
 
1006
    # this. Cop out and call SetFile
 
1007
    os.system("/Developer/Tools/SetFile -a C %s"%(
 
1008
            shellQuote(filePath),))
 
1009
 
 
1010
    if isDirectory:
 
1011
        os.system('/Developer/Tools/SetFile -a V %s'%(
 
1012
            shellQuote(tmpPath),
 
1013
        ))
 
1014
 
 
1015
def main():
 
1016
    # First parse options and check if we can perform our work
 
1017
    parseOptions()
 
1018
    checkEnvironment()
 
1019
 
 
1020
    os.environ['MACOSX_DEPLOYMENT_TARGET'] = '10.3'
 
1021
 
 
1022
    if os.path.exists(WORKDIR):
 
1023
        shutil.rmtree(WORKDIR)
 
1024
    os.mkdir(WORKDIR)
 
1025
 
 
1026
    # Then build third-party libraries such as sleepycat DB4.
 
1027
    buildLibraries()
 
1028
 
 
1029
    # Now build python itself
 
1030
    buildPython()
 
1031
    buildPythonDocs()
 
1032
    fn = os.path.join(WORKDIR, "_root", "Applications",
 
1033
                "Python %s"%(getVersion(),), "Update Shell Profile.command")
 
1034
    patchFile("scripts/postflight.patch-profile",  fn)
 
1035
    os.chmod(fn, 0755)
 
1036
 
 
1037
    folder = os.path.join(WORKDIR, "_root", "Applications", "Python %s"%(
 
1038
        getVersion(),))
 
1039
    os.chmod(folder, 0755)
 
1040
    setIcon(folder, "../Icons/Python Folder.icns")
 
1041
 
 
1042
    # Create the installer
 
1043
    buildInstaller()
 
1044
 
 
1045
    # And copy the readme into the directory containing the installer
 
1046
    patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))
 
1047
 
 
1048
    # Ditto for the license file.
 
1049
    shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
 
1050
 
 
1051
    fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
 
1052
    print >> fp, "# BUILD INFO"
 
1053
    print >> fp, "# Date:", time.ctime()
 
1054
    print >> fp, "# By:", pwd.getpwuid(os.getuid()).pw_gecos
 
1055
    fp.close()
 
1056
 
 
1057
    # Custom icon for the DMG, shown when the DMG is mounted.
 
1058
    shutil.copy("../Icons/Disk Image.icns",
 
1059
            os.path.join(WORKDIR, "installer", ".VolumeIcon.icns"))
 
1060
    os.system("/Developer/Tools/SetFile -a C %s"%(
 
1061
            os.path.join(WORKDIR, "installer", ".VolumeIcon.icns")))
 
1062
 
 
1063
 
 
1064
    # And copy it to a DMG
 
1065
    buildDMG()
 
1066
 
 
1067
 
 
1068
if __name__ == "__main__":
 
1069
    main()