build-installer.py 54.8 KB
Newer Older
1
#!/usr/bin/env python
2
"""
3
This script is used to build "official" universal installers on Mac OS X.
4
It requires at least Mac OS X 10.5, Xcode 3, and the 10.4u SDK for
5 6
32-bit builds.  64-bit or four-way universal builds require at least
OS X 10.5 and the 10.5 SDK.
7

8 9 10 11
Please ensure that this script keeps working with Python 2.5, to avoid
bootstrap issues (/usr/bin/python is Python 2.5 on OSX 10.5).  Sphinx,
which is used to build the documentation, currently requires at least
Python 2.4.
12

13 14 15 16 17 18 19 20 21 22 23 24 25 26
In addition to what is supplied with OS X 10.5+ and Xcode 3+, the script
requires an installed version of hg and a third-party version of
Tcl/Tk 8.4 (for OS X 10.4 and 10.5 deployment targets) or Tcl/TK 8.5
(for 10.6 or later) installed in /Library/Frameworks.  When installed,
the Python built by this script will attempt to dynamically link first to
Tcl and Tk frameworks in /Library/Frameworks if available otherwise fall
back to the ones in /System/Library/Framework.  For the build, we recommend
installing the most recent ActiveTcl 8.4 or 8.5 version.

32-bit-only installer builds are still possible on OS X 10.4 with Xcode 2.5
and the installation of additional components, such as a newer Python
(2.5 is needed for Python parser updates), hg, and svn (for the documentation
build).

27 28
Usage: see USAGE variable in the script.
"""
29 30 31 32 33 34 35 36 37 38 39 40 41
import platform, os, sys, getopt, textwrap, shutil, stat, time, pwd, grp
try:
    import urllib2 as urllib_request
except ImportError:
    import urllib.request as urllib_request

STAT_0o755 = ( stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
             | stat.S_IRGRP |                stat.S_IXGRP
             | stat.S_IROTH |                stat.S_IXOTH )

STAT_0o775 = ( stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
             | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP
             | stat.S_IROTH |                stat.S_IXOTH )
42

43 44
INCLUDE_TIMESTAMP = 1
VERBOSE = 1
45 46 47 48 49 50 51 52 53 54 55 56

from plistlib import Plist

try:
    from plistlib import writePlist
except ImportError:
    # We're run using python2.3
    def writePlist(plist, path):
        plist.write(path)

def shellQuote(value):
    """
57
    Return the string value in a form that can safely be inserted into
58 59 60 61 62 63 64 65 66 67
    a shell command.
    """
    return "'%s'"%(value.replace("'", "'\"'\"'"))

def grepValue(fn, variable):
    variable = variable + '='
    for ln in open(fn, 'r'):
        if ln.startswith(variable):
            value = ln[len(variable):].strip()
            return value[1:-1]
68 69 70
    raise RuntimeError("Cannot find variable %s" % variable[:-1])

_cache_getVersion = None
71 72

def getVersion():
73 74 75 76 77
    global _cache_getVersion
    if _cache_getVersion is None:
        _cache_getVersion = grepValue(
            os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
    return _cache_getVersion
78

79 80 81
def getVersionTuple():
    return tuple([int(n) for n in getVersion().split('.')])

82 83 84 85 86
def getVersionMajorMinor():
    return tuple([int(n) for n in getVersion().split('.', 2)])

_cache_getFullVersion = None

87
def getFullVersion():
88 89 90
    global _cache_getFullVersion
    if _cache_getFullVersion is not None:
        return _cache_getFullVersion
91 92 93
    fn = os.path.join(SRCDIR, 'Include', 'patchlevel.h')
    for ln in open(fn):
        if 'PY_VERSION' in ln:
94 95 96
            _cache_getFullVersion = ln.split()[-1][1:-1]
            return _cache_getFullVersion
    raise RuntimeError("Cannot find full version??")
97

98 99
# The directory we'll use to create the build (will be erased and recreated)
WORKDIR = "/tmp/_py"
100

101
# The directory we'll use to store third-party sources. Set this to something
102
# else if you don't want to re-fetch required libraries every time.
103 104
DEPSRC = os.path.join(WORKDIR, 'third-party')
DEPSRC = os.path.expanduser('~/Universal/other-sources')
105 106

# Location of the preferred SDK
107 108 109 110

### There are some issues with the SDK selection below here,
### The resulting binary doesn't work on all platforms that
### it should. Always default to the 10.4u SDK until that
111
### issue is resolved.
112 113 114 115 116 117 118 119 120
###
##if int(os.uname()[2].split('.')[0]) == 8:
##    # Explicitly use the 10.4u (universal) SDK when
##    # building on 10.4, the system headers are not
##    # useable for a universal build
##    SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
##else:
##    SDKPATH = "/"

121
SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
122

123 124
universal_opts_map = { '32-bit': ('i386', 'ppc',),
                       '64-bit': ('x86_64', 'ppc64',),
125 126 127 128 129 130 131 132 133
                       'intel':  ('i386', 'x86_64'),
                       '3-way':  ('ppc', 'i386', 'x86_64'),
                       'all':    ('i386', 'ppc', 'x86_64', 'ppc64',) }
default_target_map = {
        '64-bit': '10.5',
        '3-way': '10.5',
        'intel': '10.5',
        'all': '10.5',
}
134 135 136 137 138 139

UNIVERSALOPTS = tuple(universal_opts_map.keys())

UNIVERSALARCHS = '32-bit'

ARCHLIST = universal_opts_map[UNIVERSALARCHS]
140

141
# Source directory (assume we're in Mac/BuildScript)
142
SRCDIR = os.path.dirname(
143 144
        os.path.dirname(
            os.path.dirname(
145 146
                os.path.abspath(__file__
        ))))
147

148 149 150
# $MACOSX_DEPLOYMENT_TARGET -> minimum OS X level
DEPTARGET = '10.3'

151
target_cc_map = {
152 153 154 155 156 157
        '10.3': ('gcc-4.0', 'g++-4.0'),
        '10.4': ('gcc-4.0', 'g++-4.0'),
        '10.5': ('gcc-4.2', 'g++-4.2'),
        '10.6': ('gcc-4.2', 'g++-4.2'),
        '10.7': ('clang', 'clang++'),
        '10.8': ('clang', 'clang++'),
158
        '10.9': ('clang', 'clang++'),
159 160
}

161
CC, CXX = target_cc_map[DEPTARGET]
162 163 164

PYTHON_3 = getVersionTuple() >= (3, 0)

165
USAGE = textwrap.dedent("""\
166 167 168 169 170 171 172 173 174
    Usage: build_python [options]

    Options:
    -? or -h:            Show this message
    -b DIR
    --build-dir=DIR:     Create build here (default: %(WORKDIR)r)
    --third-party=DIR:   Store third-party sources here (default: %(DEPSRC)r)
    --sdk-path=DIR:      Location of the SDK (default: %(SDKPATH)r)
    --src-dir=DIR:       Location of the Python sources (default: %(SRCDIR)r)
175 176
    --dep-target=10.n    OS X deployment target (default: %(DEPTARGET)r)
    --universal-archs=x  universal architectures (options: %(UNIVERSALOPTS)r, default: %(UNIVERSALARCHS)r)
177 178
""")% globals()

179 180 181 182 183 184 185
# Dict of object file names with shared library names to check after building.
# This is to ensure that we ended up dynamically linking with the shared
# library paths and versions we expected.  For example:
#   EXPECTED_SHARED_LIBS['_tkinter.so'] = [
#                       '/Library/Frameworks/Tcl.framework/Versions/8.5/Tcl',
#                       '/Library/Frameworks/Tk.framework/Versions/8.5/Tk']
EXPECTED_SHARED_LIBS = {}
186 187 188

# Instructions for building libraries that are necessary for building a
# batteries included python.
189 190 191
#   [The recipes are defined here for convenience but instantiated later after
#    command line options have been processed.]
def library_recipes():
192 193
    result = []

194 195
    LT_10_5 = bool(DEPTARGET < '10.5')

196
    if (DEPTARGET > '10.5') and (getVersionTuple() >= (3, 4)):
197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217
        result.extend([
          dict(
              name="Tcl 8.5.15",
              url="ftp://ftp.tcl.tk/pub/tcl//tcl8_5/tcl8.5.15-src.tar.gz",
              checksum='f3df162f92c69b254079c4d0af7a690f',
              buildDir="unix",
              configure_pre=[
                    '--enable-shared',
                    '--enable-threads',
                    '--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib'%(getVersion(),),
              ],
              useLDFlags=False,
              install='make TCL_LIBRARY=%(TCL_LIBRARY)s && make install TCL_LIBRARY=%(TCL_LIBRARY)s DESTDIR=%(DESTDIR)s'%{
                  "DESTDIR": shellQuote(os.path.join(WORKDIR, 'libraries')),
                  "TCL_LIBRARY": shellQuote('/Library/Frameworks/Python.framework/Versions/%s/lib/tcl8.5'%(getVersion())),
                  },
              ),
          dict(
              name="Tk 8.5.15",
              url="ftp://ftp.tcl.tk/pub/tcl//tcl8_5/tk8.5.15-src.tar.gz",
              checksum='55b8e33f903210a4e1c8bce0f820657f',
218 219 220
              patches=[
                  "issue19373_tk_8_5_15_source.patch",
                   ],
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
              buildDir="unix",
              configure_pre=[
                    '--enable-aqua',
                    '--enable-shared',
                    '--enable-threads',
                    '--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib'%(getVersion(),),
              ],
              useLDFlags=False,
              install='make TCL_LIBRARY=%(TCL_LIBRARY)s TK_LIBRARY=%(TK_LIBRARY)s && make install TCL_LIBRARY=%(TCL_LIBRARY)s TK_LIBRARY=%(TK_LIBRARY)s DESTDIR=%(DESTDIR)s'%{
                  "DESTDIR": shellQuote(os.path.join(WORKDIR, 'libraries')),
                  "TCL_LIBRARY": shellQuote('/Library/Frameworks/Python.framework/Versions/%s/lib/tcl8.5'%(getVersion())),
                  "TK_LIBRARY": shellQuote('/Library/Frameworks/Python.framework/Versions/%s/lib/tk8.5'%(getVersion())),
                  },
                ),
        ])

237 238 239
    if getVersionTuple() >= (3, 3):
        result.extend([
          dict(
240 241 242
              name="XZ 5.0.5",
              url="http://tukaani.org/xz/xz-5.0.5.tar.gz",
              checksum='19d924e066b6fff0bc9d1981b4e53196',
243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284
              configure_pre=[
                    '--disable-dependency-tracking',
              ]
              ),
        ])

    result.extend([
          dict(
              name="NCurses 5.9",
              url="http://ftp.gnu.org/pub/gnu/ncurses/ncurses-5.9.tar.gz",
              checksum='8cb9c412e5f2d96bc6f459aa8c6282a1',
              configure_pre=[
                  "--enable-widec",
                  "--without-cxx",
                  "--without-cxx-binding",
                  "--without-ada",
                  "--without-curses-h",
                  "--enable-shared",
                  "--with-shared",
                  "--without-debug",
                  "--without-normal",
                  "--without-tests",
                  "--without-manpages",
                  "--datadir=/usr/share",
                  "--sysconfdir=/etc",
                  "--sharedstatedir=/usr/com",
                  "--with-terminfo-dirs=/usr/share/terminfo",
                  "--with-default-terminfo-dir=/usr/share/terminfo",
                  "--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib"%(getVersion(),),
              ],
              patchscripts=[
                  ("ftp://invisible-island.net/ncurses//5.9/ncurses-5.9-20120616-patch.sh.bz2",
                   "f54bf02a349f96a7c4f0d00922f3a0d4"),
                   ],
              useLDFlags=False,
              install='make && make install DESTDIR=%s && cd %s/usr/local/lib && ln -fs ../../../Library/Frameworks/Python.framework/Versions/%s/lib/lib* .'%(
                  shellQuote(os.path.join(WORKDIR, 'libraries')),
                  shellQuote(os.path.join(WORKDIR, 'libraries')),
                  getVersion(),
                  ),
          ),
          dict(
285 286 287
              name="SQLite 3.8.1",
              url="http://www.sqlite.org/2013/sqlite-autoconf-3080100.tar.gz",
              checksum='8b5a0a02dfcb0c7daf90856a5cfd485a',
288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303
              extra_cflags=('-Os '
                            '-DSQLITE_ENABLE_FTS4 '
                            '-DSQLITE_ENABLE_FTS3_PARENTHESIS '
                            '-DSQLITE_ENABLE_RTREE '
                            '-DSQLITE_TCL=0 '
                 '%s' % ('','-DSQLITE_WITHOUT_ZONEMALLOC ')[LT_10_5]),
              configure_pre=[
                  '--enable-threadsafe',
                  '--enable-shared=no',
                  '--enable-static=yes',
                  '--disable-readline',
                  '--disable-dependency-tracking',
              ]
          ),
        ])

304 305 306
    if DEPTARGET < '10.5':
        result.extend([
          dict(
307 308 309
              name="Bzip2 1.0.6",
              url="http://bzip.org/1.0.6/bzip2-1.0.6.tar.gz",
              checksum='00b516f4704d4a7cb50a1d97e6e8e15b',
310
              configure=None,
311 312
              install='make install CC=%s CXX=%s, PREFIX=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
                  CC, CXX,
313 314 315 316
                  shellQuote(os.path.join(WORKDIR, 'libraries')),
                  ' -arch '.join(ARCHLIST),
                  SDKPATH,
              ),
317
          ),
318 319 320 321 322
          dict(
              name="ZLib 1.2.3",
              url="http://www.gzip.org/zlib/zlib-1.2.3.tar.gz",
              checksum='debc62758716a169df9f62e6ab2bc634',
              configure=None,
323 324
              install='make install CC=%s CXX=%s, prefix=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
                  CC, CXX,
325 326 327 328
                  shellQuote(os.path.join(WORKDIR, 'libraries')),
                  ' -arch '.join(ARCHLIST),
                  SDKPATH,
              ),
329
          ),
330 331
          dict(
              # Note that GNU readline is GPL'd software
332 333 334
              name="GNU Readline 6.1.2",
              url="http://ftp.gnu.org/pub/gnu/readline/readline-6.1.tar.gz" ,
              checksum='fc2f7e714fe792db1ce6ddc4c9fb4ef3',
335 336 337 338
              patchlevel='0',
              patches=[
                  # The readline maintainers don't do actual micro releases, but
                  # just ship a set of patches.
339 340 341 342
                  ('http://ftp.gnu.org/pub/gnu/readline/readline-6.1-patches/readline61-001',
                   'c642f2e84d820884b0bf9fd176bc6c3f'),
                  ('http://ftp.gnu.org/pub/gnu/readline/readline-6.1-patches/readline61-002',
                   '1a76781a1ea734e831588285db7ec9b1'),
343 344 345
              ]
          ),
        ])
346

347 348 349 350 351 352 353 354 355 356 357 358 359
    if not PYTHON_3:
        result.extend([
          dict(
              name="Sleepycat DB 4.7.25",
              url="http://download.oracle.com/berkeley-db/db-4.7.25.tar.gz",
              checksum='ec2b87e833779681a0c3a814aa71359e',
              buildDir="build_unix",
              configure="../dist/configure",
              configure_pre=[
                  '--includedir=/usr/local/include/db4',
              ]
          ),
        ])
360 361

    return result
362 363 364


# Instructions for building packages inside the .mpkg.
365 366
def pkg_recipes():
    unselected_for_python3 = ('selected', 'unselected')[PYTHON_3]
367
    unselected_for_lt_python34 = ('selected', 'unselected')[getVersionTuple() < (3, 4)]
368 369 370 371 372 373 374 375 376
    result = [
        dict(
            name="PythonFramework",
            long_name="Python Framework",
            source="/Library/Frameworks/Python.framework",
            readme="""\
                This package installs Python.framework, that is the python
                interpreter and the standard library. This also includes Python
                wrappers for lots of Mac OS X API's.
377
            """,
378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413
            postflight="scripts/postflight.framework",
            selected='selected',
        ),
        dict(
            name="PythonApplications",
            long_name="GUI Applications",
            source="/Applications/Python %(VER)s",
            readme="""\
                This package installs IDLE (an interactive Python IDE),
                Python Launcher and Build Applet (create application bundles
                from python scripts).

                It also installs a number of examples and demos.
                """,
            required=False,
            selected='selected',
        ),
        dict(
            name="PythonUnixTools",
            long_name="UNIX command-line tools",
            source="/usr/local/bin",
            readme="""\
                This package installs the unix tools in /usr/local/bin for
                compatibility with older releases of Python. This package
                is not necessary to use Python.
                """,
            required=False,
            selected='selected',
        ),
        dict(
            name="PythonDocumentation",
            long_name="Python Documentation",
            topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
            source="/pydocs",
            readme="""\
                This package installs the python documentation at a location
414
                that is useable for pydoc and IDLE.
415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435
                """,
            postflight="scripts/postflight.documentation",
            required=False,
            selected='selected',
        ),
        dict(
            name="PythonProfileChanges",
            long_name="Shell profile updater",
            readme="""\
                This packages updates your shell profile to make sure that
                the Python tools are found by your shell in preference of
                the system provided Python tools.

                If you don't install this package you'll have to add
                "/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
                to your PATH by hand.
                """,
            postflight="scripts/postflight.patch-profile",
            topdir="/Library/Frameworks/Python.framework",
            source="/empty-dir",
            required=False,
436
            selected=unselected_for_lt_python34,
437 438 439
        ),
    ]

440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456
    if getVersionTuple() >= (3, 4):
        result.append(
            dict(
                name="PythonInstallPip",
                long_name="Install or upgrade pip",
                readme="""\
                    This package installs (or upgrades from an earlier version)
                    pip, a tool for installing and managing Python packages.
                    """,
                postflight="scripts/postflight.ensurepip",
                topdir="/Library/Frameworks/Python.framework",
                source="/empty-dir",
                required=False,
                selected='selected',
            )
        )

457
    if DEPTARGET < '10.4' and not PYTHON_3:
458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473
        result.append(
            dict(
                name="PythonSystemFixes",
                long_name="Fix system Python",
                readme="""\
                    This package updates the system python installation on
                    Mac OS X 10.3 to ensure that you can build new python extensions
                    using that copy of python after installing this version.
                    """,
                postflight="../Tools/fixapplepython23.py",
                topdir="/Library/Frameworks/Python.framework",
                source="/empty-dir",
                required=False,
                selected=unselected_for_python3,
            )
        )
474

475
    return result
476 477 478 479 480 481 482 483 484 485 486 487 488 489

def fatal(msg):
    """
    A fatal error, bail out.
    """
    sys.stderr.write('FATAL: ')
    sys.stderr.write(msg)
    sys.stderr.write('\n')
    sys.exit(1)

def fileContents(fn):
    """
    Return the contents of the named file
    """
490
    return open(fn, 'r').read()
491 492 493

def runCommand(commandline):
    """
494
    Run a command and raise RuntimeError if it fails. Output is suppressed
495 496 497 498 499
    unless the command fails.
    """
    fd = os.popen(commandline, 'r')
    data = fd.read()
    xit = fd.close()
500
    if xit is not None:
501
        sys.stdout.write(data)
502
        raise RuntimeError("command failed: %s"%(commandline,))
503 504 505 506 507 508 509 510

    if VERBOSE:
        sys.stdout.write(data); sys.stdout.flush()

def captureCommand(commandline):
    fd = os.popen(commandline, 'r')
    data = fd.read()
    xit = fd.close()
511
    if xit is not None:
512
        sys.stdout.write(data)
513
        raise RuntimeError("command failed: %s"%(commandline,))
514 515 516

    return data

517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533
def getTclTkVersion(configfile, versionline):
    """
    search Tcl or Tk configuration file for version line
    """
    try:
        f = open(configfile, "r")
    except:
        fatal("Framework configuration file not found: %s" % configfile)

    for l in f:
        if l.startswith(versionline):
            f.close()
            return l

    fatal("Version variable %s not found in framework configuration file: %s"
            % (versionline, configfile))

534 535 536 537 538
def checkEnvironment():
    """
    Check that we're running on a supported system.
    """

539 540 541
    if sys.version_info[0:2] < (2, 4):
        fatal("This script must be run with Python 2.4 or later")

542
    if platform.system() != 'Darwin':
543
        fatal("This script should be run on a Mac OS X 10.4 (or later) system")
544

545 546
    if int(platform.release().split('.')[0]) < 8:
        fatal("This script should be run on a Mac OS X 10.4 (or later) system")
547 548 549 550 551

    if not os.path.exists(SDKPATH):
        fatal("Please install the latest version of Xcode and the %s SDK"%(
            os.path.basename(SDKPATH[:-4])))

552 553 554
    # Because we only support dynamic load of only one major/minor version of
    # Tcl/Tk, ensure:
    # 1. there are no user-installed frameworks of Tcl/Tk with version
555 556 557 558 559
    #       higher than the Apple-supplied system version in
    #       SDKROOT/System/Library/Frameworks
    # 2. there is a user-installed framework (usually ActiveTcl) in (or linked
    #       in) SDKROOT/Library/Frameworks with the same version as the system
    #       version. This allows users to choose to install a newer patch level.
560

561
    frameworks = {}
562
    for framework in ['Tcl', 'Tk']:
563
        fwpth = 'Library/Frameworks/%s.framework/Versions/Current' % framework
564
        sysfw = os.path.join(SDKPATH, 'System', fwpth)
565
        libfw = os.path.join(SDKPATH, fwpth)
566
        usrfw = os.path.join(os.getenv('HOME'), fwpth)
567 568 569 570 571
        frameworks[framework] = os.readlink(sysfw)
        if not os.path.exists(libfw):
            fatal("Please install a link to a current %s %s as %s so "
                    "the user can override the system framework."
                    % (framework, frameworks[framework], libfw))
572
        if os.readlink(libfw) != os.readlink(sysfw):
573 574 575 576 577
            fatal("Version of %s must match %s" % (libfw, sysfw) )
        if os.path.exists(usrfw):
            fatal("Please rename %s to avoid possible dynamic load issues."
                    % usrfw)

578 579 580 581 582 583 584 585 586 587 588
    if frameworks['Tcl'] != frameworks['Tk']:
        fatal("The Tcl and Tk frameworks are not the same version.")

    # add files to check after build
    EXPECTED_SHARED_LIBS['_tkinter.so'] = [
            "/Library/Frameworks/Tcl.framework/Versions/%s/Tcl"
                % frameworks['Tcl'],
            "/Library/Frameworks/Tk.framework/Versions/%s/Tk"
                % frameworks['Tk'],
            ]

589
    # For 10.6+ builds, we build two versions of _tkinter:
590
    #    - the traditional version (renamed to _tkinter_library.so) linked
591
    #       with /Library/Frameworks/{Tcl,Tk}.framework
592
    #    - the default version linked with our builtin copies of Tcl and Tk
593
    if (DEPTARGET > '10.5') and (getVersionTuple() >= (3, 4)):
594
        EXPECTED_SHARED_LIBS['_tkinter_library.so'] = \
595 596 597 598 599 600 601 602
            EXPECTED_SHARED_LIBS['_tkinter.so']
        EXPECTED_SHARED_LIBS['_tkinter.so'] = [
                "/Library/Frameworks/Python.framework/Versions/%s/lib/libtcl%s.dylib"
                    % (getVersion(), frameworks['Tcl']),
                "/Library/Frameworks/Python.framework/Versions/%s/lib/libtk%s.dylib"
                    % (getVersion(), frameworks['Tk']),
                ]

603 604 605 606 607 608
    # Remove inherited environment variables which might influence build
    environ_var_prefixes = ['CPATH', 'C_INCLUDE_', 'DYLD_', 'LANG', 'LC_',
                            'LD_', 'LIBRARY_', 'PATH', 'PYTHON']
    for ev in list(os.environ):
        for prefix in environ_var_prefixes:
            if ev.startswith(prefix) :
609 610
                print("INFO: deleting environment variable %s=%s" % (
                                                    ev, os.environ[ev]))
611
                del os.environ[ev]
612

613 614 615 616 617 618 619 620 621 622
    base_path = '/bin:/sbin:/usr/bin:/usr/sbin'
    if 'SDK_TOOLS_BIN' in os.environ:
        base_path = os.environ['SDK_TOOLS_BIN'] + ':' + base_path
    # Xcode 2.5 on OS X 10.4 does not include SetFile in its usr/bin;
    # add its fixed location here if it exists
    OLD_DEVELOPER_TOOLS = '/Developer/Tools'
    if os.path.isdir(OLD_DEVELOPER_TOOLS):
        base_path = base_path + ':' + OLD_DEVELOPER_TOOLS
    os.environ['PATH'] = base_path
    print("Setting default PATH: %s"%(os.environ['PATH']))
623

624

625
def parseOptions(args=None):
626 627 628
    """
    Parse arguments and update global settings.
    """
629
    global WORKDIR, DEPSRC, SDKPATH, SRCDIR, DEPTARGET
630
    global UNIVERSALOPTS, UNIVERSALARCHS, ARCHLIST, CC, CXX
631 632 633 634 635 636

    if args is None:
        args = sys.argv[1:]

    try:
        options, args = getopt.getopt(args, '?hb',
637 638
                [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir=',
                  'dep-target=', 'universal-archs=', 'help' ])
639 640
    except getopt.GetoptError:
        print(sys.exc_info()[1])
641 642 643
        sys.exit(1)

    if args:
644
        print("Additional arguments")
645 646
        sys.exit(1)

647
    deptarget = None
648
    for k, v in options:
649
        if k in ('-h', '-?', '--help'):
650
            print(USAGE)
651 652 653 654 655 656 657 658 659 660 661 662 663 664
            sys.exit(0)

        elif k in ('-d', '--build-dir'):
            WORKDIR=v

        elif k in ('--third-party',):
            DEPSRC=v

        elif k in ('--sdk-path',):
            SDKPATH=v

        elif k in ('--src-dir',):
            SRCDIR=v

665 666
        elif k in ('--dep-target', ):
            DEPTARGET=v
667
            deptarget=v
668 669 670 671 672

        elif k in ('--universal-archs', ):
            if v in UNIVERSALOPTS:
                UNIVERSALARCHS = v
                ARCHLIST = universal_opts_map[UNIVERSALARCHS]
673 674 675 676
                if deptarget is None:
                    # Select alternate default deployment
                    # target
                    DEPTARGET = default_target_map.get(v, '10.3')
677
            else:
678
                raise NotImplementedError(v)
679

680
        else:
681
            raise NotImplementedError(k)
682 683 684 685 686 687

    SRCDIR=os.path.abspath(SRCDIR)
    WORKDIR=os.path.abspath(WORKDIR)
    SDKPATH=os.path.abspath(SDKPATH)
    DEPSRC=os.path.abspath(DEPSRC)

688
    CC, CXX=target_cc_map[DEPTARGET]
689

690 691 692 693 694 695 696 697 698 699
    print("Settings:")
    print(" * Source directory:", SRCDIR)
    print(" * Build directory: ", WORKDIR)
    print(" * SDK location:    ", SDKPATH)
    print(" * Third-party source:", DEPSRC)
    print(" * Deployment target:", DEPTARGET)
    print(" * Universal architectures:", ARCHLIST)
    print(" * C compiler:", CC)
    print(" * C++ compiler:", CXX)
    print("")
700 701 702 703 704 705 706 707 708 709 710




def extractArchive(builddir, archiveName):
    """
    Extract a source archive into 'builddir'. Returns the path of the
    extracted archive.

    XXX: This function assumes that archives contain a toplevel directory
    that is has the same name as the basename of the archive. This is
711 712 713 714
    safe enough for almost anything we use.  Unfortunately, it does not
    work for current Tcl and Tk source releases where the basename of
    the archive ends with "-src" but the uncompressed directory does not.
    For now, just special case Tcl and Tk tar.gz downloads.
715 716 717 718 719 720
    """
    curdir = os.getcwd()
    try:
        os.chdir(builddir)
        if archiveName.endswith('.tar.gz'):
            retval = os.path.basename(archiveName[:-7])
721 722 723
            if ((retval.startswith('tcl') or retval.startswith('tk'))
                    and retval.endswith('-src')):
                retval = retval[:-4]
724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749
            if os.path.exists(retval):
                shutil.rmtree(retval)
            fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')

        elif archiveName.endswith('.tar.bz2'):
            retval = os.path.basename(archiveName[:-8])
            if os.path.exists(retval):
                shutil.rmtree(retval)
            fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')

        elif archiveName.endswith('.tar'):
            retval = os.path.basename(archiveName[:-4])
            if os.path.exists(retval):
                shutil.rmtree(retval)
            fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')

        elif archiveName.endswith('.zip'):
            retval = os.path.basename(archiveName[:-4])
            if os.path.exists(retval):
                shutil.rmtree(retval)
            fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')

        data = fp.read()
        xit = fp.close()
        if xit is not None:
            sys.stdout.write(data)
750
            raise RuntimeError("Cannot extract %s"%(archiveName,))
751 752 753 754 755 756 757 758 759 760

        return os.path.join(builddir, retval)

    finally:
        os.chdir(curdir)

def downloadURL(url, fname):
    """
    Download the contents of the url into the file.
    """
761
    fpIn = urllib_request.urlopen(url)
762 763 764 765 766 767 768 769 770 771 772 773 774 775
    fpOut = open(fname, 'wb')
    block = fpIn.read(10240)
    try:
        while block:
            fpOut.write(block)
            block = fpIn.read(10240)
        fpIn.close()
        fpOut.close()
    except:
        try:
            os.unlink(fname)
        except:
            pass

776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793
def verifyThirdPartyFile(url, checksum, fname):
    """
    Download file from url to filename fname if it does not already exist.
    Abort if file contents does not match supplied md5 checksum.
    """
    name = os.path.basename(fname)
    if os.path.exists(fname):
        print("Using local copy of %s"%(name,))
    else:
        print("Did not find local copy of %s"%(name,))
        print("Downloading %s"%(name,))
        downloadURL(url, fname)
        print("Archive for %s stored as %s"%(name, fname))
    if os.system(
            'MD5=$(openssl md5 %s) ; test "${MD5##*= }" = "%s"'
                % (shellQuote(fname), checksum) ):
        fatal('MD5 checksum mismatch for file %s' % fname)

794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813
def buildRecipe(recipe, basedir, archList):
    """
    Build software using a recipe. This function does the
    'configure;make;make install' dance for C software, with a possibility
    to customize this process, basically a poor-mans DarwinPorts.
    """
    curdir = os.getcwd()

    name = recipe['name']
    url = recipe['url']
    configure = recipe.get('configure', './configure')
    install = recipe.get('install', 'make && make install DESTDIR=%s'%(
        shellQuote(basedir)))

    archiveName = os.path.split(url)[-1]
    sourceArchive = os.path.join(DEPSRC, archiveName)

    if not os.path.exists(DEPSRC):
        os.mkdir(DEPSRC)

814 815
    verifyThirdPartyFile(url, recipe['checksum'], sourceArchive)
    print("Extracting archive for %s"%(name,))
816 817 818 819 820 821 822
    buildDir=os.path.join(WORKDIR, '_bld')
    if not os.path.exists(buildDir):
        os.mkdir(buildDir)

    workDir = extractArchive(buildDir, sourceArchive)
    os.chdir(workDir)

823 824 825 826 827 828 829 830
    for patch in recipe.get('patches', ()):
        if isinstance(patch, tuple):
            url, checksum = patch
            fn = os.path.join(DEPSRC, os.path.basename(url))
            verifyThirdPartyFile(url, checksum, fn)
        else:
            # patch is a file in the source directory
            fn = os.path.join(curdir, patch)
831 832 833
        runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
            shellQuote(fn),))

834 835 836 837 838 839 840 841 842 843 844 845 846 847
    for patchscript in recipe.get('patchscripts', ()):
        if isinstance(patchscript, tuple):
            url, checksum = patchscript
            fn = os.path.join(DEPSRC, os.path.basename(url))
            verifyThirdPartyFile(url, checksum, fn)
        else:
            # patch is a file in the source directory
            fn = os.path.join(curdir, patchscript)
        if fn.endswith('.bz2'):
            runCommand('bunzip2 -fk %s' % shellQuote(fn))
            fn = fn[:-4]
        runCommand('sh %s' % shellQuote(fn))
        os.unlink(fn)

848 849 850
    if 'buildDir' in recipe:
        os.chdir(recipe['buildDir'])

851 852 853 854 855 856 857 858 859 860 861 862 863 864 865 866 867 868
    if configure is not None:
        configure_args = [
            "--prefix=/usr/local",
            "--enable-static",
            "--disable-shared",
            #"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
        ]

        if 'configure_pre' in recipe:
            args = list(recipe['configure_pre'])
            if '--disable-static' in args:
                configure_args.remove('--enable-static')
            if '--enable-shared' in args:
                configure_args.remove('--disable-shared')
            configure_args.extend(args)

        if recipe.get('useLDFlags', 1):
            configure_args.extend([
869 870 871 872
                "CFLAGS=%s-mmacosx-version-min=%s -arch %s -isysroot %s "
                            "-I%s/usr/local/include"%(
                        recipe.get('extra_cflags', ''),
                        DEPTARGET,
873 874 875
                        ' -arch '.join(archList),
                        shellQuote(SDKPATH)[1:-1],
                        shellQuote(basedir)[1:-1],),
876 877
                "LDFLAGS=-mmacosx-version-min=%s -syslibroot,%s -L%s/usr/local/lib -arch %s"%(
                    DEPTARGET,
878
                    shellQuote(SDKPATH)[1:-1],
879 880 881 882 883
                    shellQuote(basedir)[1:-1],
                    ' -arch '.join(archList)),
            ])
        else:
            configure_args.extend([
884 885 886 887
                "CFLAGS=%s-mmacosx-version-min=%s -arch %s -isysroot %s "
                            "-I%s/usr/local/include"%(
                        recipe.get('extra_cflags', ''),
                        DEPTARGET,
888 889 890 891
                        ' -arch '.join(archList),
                        shellQuote(SDKPATH)[1:-1],
                        shellQuote(basedir)[1:-1],),
            ])
892

893
        if 'configure_post' in recipe:
894
            configure_args = configure_args + list(recipe['configure_post'])
895

896 897
        configure_args.insert(0, configure)
        configure_args = [ shellQuote(a) for a in configure_args ]
898

899
        print("Running configure for %s"%(name,))
900
        runCommand(' '.join(configure_args) + ' 2>&1')
901

902
    print("Running install for %s"%(name,))
903 904
    runCommand('{ ' + install + ' ;} 2>&1')

905 906
    print("Done %s"%(name,))
    print("")
907 908 909 910 911 912 913

    os.chdir(curdir)

def buildLibraries():
    """
    Build our dependencies into $WORKDIR/libraries/usr/local
    """
914 915 916
    print("")
    print("Building required libraries")
    print("")
917 918 919 920 921
    universal = os.path.join(WORKDIR, 'libraries')
    os.mkdir(universal)
    os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
    os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))

922
    for recipe in library_recipes():
923
        buildRecipe(recipe, universal, ARCHLIST)
924 925 926 927



def buildPythonDocs():
928
    # This stores the documentation as Resources/English.lproj/Documentation
929
    # inside the framwork. pydoc and IDLE will pick it up there.
930
    print("Install python documentation")
931
    rootDir = os.path.join(WORKDIR, '_root')
932
    buildDir = os.path.join('../../Doc')
933
    docdir = os.path.join(rootDir, 'pydocs')
934 935 936
    curDir = os.getcwd()
    os.chdir(buildDir)
    runCommand('make update')
937
    runCommand("make html PYTHON='%s'" % os.path.abspath(sys.executable))
938
    os.chdir(curDir)
939 940
    if not os.path.exists(docdir):
        os.mkdir(docdir)
Ronald Oussoren's avatar
Ronald Oussoren committed
941
    os.rename(os.path.join(buildDir, 'build', 'html'), docdir)
942 943 944


def buildPython():
945
    print("Building a universal python for %s architectures" % UNIVERSALARCHS)
946 947 948 949 950 951 952 953

    buildDir = os.path.join(WORKDIR, '_bld', 'python')
    rootDir = os.path.join(WORKDIR, '_root')

    if os.path.exists(buildDir):
        shutil.rmtree(buildDir)
    if os.path.exists(rootDir):
        shutil.rmtree(rootDir)
954 955 956
    os.makedirs(buildDir)
    os.makedirs(rootDir)
    os.makedirs(os.path.join(rootDir, 'empty-dir'))
957 958 959 960 961 962 963 964 965 966 967
    curdir = os.getcwd()
    os.chdir(buildDir)

    # Not sure if this is still needed, the original build script
    # claims that parts of the install assume python.exe exists.
    os.symlink('python', os.path.join(buildDir, 'python.exe'))

    # Extract the version from the configure file, needed to calculate
    # several paths.
    version = getVersion()

968 969 970 971 972
    # Since the extra libs are not in their installed framework location
    # during the build, augment the library path so that the interpreter
    # will find them during its extension import sanity checks.
    os.environ['DYLD_LIBRARY_PATH'] = os.path.join(WORKDIR,
                                        'libraries', 'usr', 'local', 'lib')
973
    print("Running configure...")
974
    runCommand("%s -C --enable-framework --enable-universalsdk=%s "
975 976
               "--with-universal-archs=%s "
               "%s "
977
               "%s "
978
               "LDFLAGS='-g -L%s/libraries/usr/local/lib' "
979
               "CFLAGS='-g -I%s/libraries/usr/local/include' 2>&1"%(
980 981
        shellQuote(os.path.join(SRCDIR, 'configure')), shellQuote(SDKPATH),
        UNIVERSALARCHS,
982
        (' ', '--with-computed-gotos ')[PYTHON_3],
983
        (' ', '--without-ensurepip ')[getVersionTuple() >= (3, 4)],
984
        shellQuote(WORKDIR)[1:-1],
985 986
        shellQuote(WORKDIR)[1:-1]))

987
    print("Running make")
988 989
    runCommand("make")

990 991 992 993
    # For deployment targets of 10.6 and higher, we build our own version
    # of Tcl and Cocoa Aqua Tk libs because the Apple-supplied Tk 8.5 is
    # out-of-date and has critical bugs.  Save the _tkinter.so that was
    # linked with /Library/Frameworks/{Tck,Tk}.framework and build
994
    # another _tkinter.so linked with our builtin Tcl and Tk libs.
995
    if (DEPTARGET > '10.5') and (getVersionTuple() >= (3, 4)):
996
        runCommand("find build -name '_tkinter.so' "
997 998
                        " -execdir mv '{}' _tkinter_library.so \;")
        print("Running make to build builtin _tkinter")
999 1000 1001 1002
        runCommand("make TCLTK_INCLUDES='-I%s/libraries/usr/local/include' "
                "TCLTK_LIBS='-L%s/libraries/usr/local/lib -ltcl8.5 -ltk8.5'"%(
            shellQuote(WORKDIR)[1:-1],
            shellQuote(WORKDIR)[1:-1]))
1003
        # make a copy which will be moved to lib-tkinter later
1004
        runCommand("find build -name '_tkinter.so' "
1005
                        " -execdir cp -p '{}' _tkinter_builtin.so \;")
1006

1007
    print("Running make install")
Ronald Oussoren's avatar
Ronald Oussoren committed
1008
    runCommand("make install DESTDIR=%s"%(
1009 1010
        shellQuote(rootDir)))

1011
    print("Running make frameworkinstallextras")
1012 1013 1014
    runCommand("make frameworkinstallextras DESTDIR=%s"%(
        shellQuote(rootDir)))

1015
    del os.environ['DYLD_LIBRARY_PATH']
1016
    print("Copying required shared libraries")
1017 1018 1019 1020 1021 1022 1023 1024 1025 1026
    if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
        runCommand("mv %s/* %s"%(
            shellQuote(os.path.join(
                WORKDIR, 'libraries', 'Library', 'Frameworks',
                'Python.framework', 'Versions', getVersion(),
                'lib')),
            shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
                'Python.framework', 'Versions', getVersion(),
                'lib'))))

1027 1028 1029 1030 1031 1032 1033 1034 1035
    path_to_lib = os.path.join(rootDir, 'Library', 'Frameworks',
                                'Python.framework', 'Versions',
                                version, 'lib', 'python%s'%(version,))

    # If we made multiple versions of _tkinter, move them to
    # their own directories under python lib.  This allows
    # users to select which to import by manipulating sys.path
    # directly or with PYTHONPATH.

1036
    if (DEPTARGET > '10.5') and (getVersionTuple() >= (3, 4)):
1037 1038 1039 1040 1041 1042 1043 1044 1045
        TKINTERS = ['builtin', 'library']
        tkinter_moves = [('_tkinter_' + tkn + '.so',
                             os.path.join(path_to_lib, 'lib-tkinter', tkn))
                         for tkn in TKINTERS]
        # Create the destination directories under lib-tkinter.
        # The permissions and uid/gid will be fixed up next.
        for tkm in tkinter_moves:
            os.makedirs(tkm[1])

1046
    print("Fix file modes")
1047
    frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
1048 1049
    gid = grp.getgrnam('admin').gr_gid

1050
    shared_lib_error = False
1051
    moves_list = []
1052 1053
    for dirpath, dirnames, filenames in os.walk(frmDir):
        for dn in dirnames:
1054
            os.chmod(os.path.join(dirpath, dn), STAT_0o775)
1055 1056
            os.chown(os.path.join(dirpath, dn), -1, gid)

1057 1058 1059 1060 1061 1062 1063
        for fn in filenames:
            if os.path.islink(fn):
                continue

            # "chmod g+w $fn"
            p = os.path.join(dirpath, fn)
            st = os.stat(p)
1064 1065
            os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
            os.chown(p, -1, gid)
1066

1067 1068 1069 1070 1071 1072 1073 1074 1075 1076
            if fn in EXPECTED_SHARED_LIBS:
                # check to see that this file was linked with the
                # expected library path and version
                data = captureCommand("otool -L %s" % shellQuote(p))
                for sl in EXPECTED_SHARED_LIBS[fn]:
                    if ("\t%s " % sl) not in data:
                        print("Expected shared lib %s was not linked with %s"
                                % (sl, p))
                        shared_lib_error = True

1077 1078 1079 1080 1081 1082
            # If this is a _tkinter variant, move it to its own directory
            # now that we have fixed its permissions and checked that it
            # was linked properly.  The directory was created earlier.
            # The files are moved after the entire tree has been walked
            # since the shared library checking depends on the files
            # having unique names.
1083
            if (DEPTARGET > '10.5') and (getVersionTuple() >= (3, 4)):
1084 1085 1086 1087 1088
                for tkm in tkinter_moves:
                    if fn == tkm[0]:
                        moves_list.append(
                            (p, os.path.join(tkm[1], '_tkinter.so')))

1089 1090 1091
    if shared_lib_error:
        fatal("Unexpected shared library errors.")

1092 1093 1094 1095
    # Now do the moves.
    for ml in moves_list:
        shutil.move(ml[0], ml[1])

1096 1097 1098 1099
    if PYTHON_3:
        LDVERSION=None
        VERSION=None
        ABIFLAGS=None
1100

1101
        fp = open(os.path.join(buildDir, 'Makefile'), 'r')
1102 1103 1104 1105 1106 1107 1108
        for ln in fp:
            if ln.startswith('VERSION='):
                VERSION=ln.split()[1]
            if ln.startswith('ABIFLAGS='):
                ABIFLAGS=ln.split()[1]
            if ln.startswith('LDVERSION='):
                LDVERSION=ln.split()[1]
1109
        fp.close()
1110

1111 1112 1113 1114 1115
        LDVERSION = LDVERSION.replace('$(VERSION)', VERSION)
        LDVERSION = LDVERSION.replace('$(ABIFLAGS)', ABIFLAGS)
        config_suffix = '-' + LDVERSION
    else:
        config_suffix = ''      # Python 2.x
1116

1117 1118
    # We added some directories to the search path during the configure
    # phase. Remove those because those directories won't be there on
1119 1120 1121
    # the end-users system. Also remove the directories from _sysconfigdata.py
    # (added in 3.3) if it exists.

1122 1123 1124 1125 1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150 1151 1152 1153 1154
    include_path = '-I%s/libraries/usr/local/include' % (WORKDIR,)
    lib_path = '-L%s/libraries/usr/local/lib' % (WORKDIR,)

    # fix Makefile
    path = os.path.join(path_to_lib, 'config' + config_suffix, 'Makefile')
    fp = open(path, 'r')
    data = fp.read()
    fp.close()

    for p in (include_path, lib_path):
        data = data.replace(" " + p, '')
        data = data.replace(p + " ", '')

    fp = open(path, 'w')
    fp.write(data)
    fp.close()

    # fix _sysconfigdata if it exists
    #
    # TODO: make this more robust!  test_sysconfig_module of
    # distutils.tests.test_sysconfig.SysconfigTestCase tests that
    # the output from get_config_var in both sysconfig and
    # distutils.sysconfig is exactly the same for both CFLAGS and
    # LDFLAGS.  The fixing up is now complicated by the pretty
    # printing in _sysconfigdata.py.  Also, we are using the
    # pprint from the Python running the installer build which
    # may not cosmetically format the same as the pprint in the Python
    # being built (and which is used to originally generate
    # _sysconfigdata.py).

    import pprint
    path = os.path.join(path_to_lib, '_sysconfigdata.py')
    if os.path.exists(path):
1155 1156 1157
        fp = open(path, 'r')
        data = fp.read()
        fp.close()
1158 1159 1160 1161 1162 1163 1164 1165 1166
        # create build_time_vars dict
        exec(data)
        vars = {}
        for k, v in build_time_vars.items():
            if type(v) == type(''):
                for p in (include_path, lib_path):
                    v = v.replace(' ' + p, '')
                    v = v.replace(p + ' ', '')
            vars[k] = v
1167

1168
        fp = open(path, 'w')
1169 1170 1171 1172 1173
        # duplicated from sysconfig._generate_posix_vars()
        fp.write('# system configuration generated and used by'
                    ' the sysconfig module\n')
        fp.write('build_time_vars = ')
        pprint.pprint(vars, stream=fp)
1174
        fp.close()
1175 1176 1177 1178 1179 1180 1181 1182 1183 1184 1185 1186 1187 1188 1189

    # Add symlinks in /usr/local/bin, using relative links
    usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
    to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
            'Python.framework', 'Versions', version, 'bin')
    if os.path.exists(usr_local_bin):
        shutil.rmtree(usr_local_bin)
    os.makedirs(usr_local_bin)
    for fn in os.listdir(
                os.path.join(frmDir, 'Versions', version, 'bin')):
        os.symlink(os.path.join(to_framework, fn),
                   os.path.join(usr_local_bin, fn))

    os.chdir(curdir)

1190
    if PYTHON_3:
1191
        # Remove the 'Current' link, that way we don't accidentally mess
1192 1193 1194
        # with an already installed version of python 2
        os.unlink(os.path.join(rootDir, 'Library', 'Frameworks',
                            'Python.framework', 'Versions', 'Current'))
1195 1196 1197 1198 1199

def patchFile(inPath, outPath):
    data = fileContents(inPath)
    data = data.replace('$FULL_VERSION', getFullVersion())
    data = data.replace('$VERSION', getVersion())
1200
    data = data.replace('$MACOSX_DEPLOYMENT_TARGET', ''.join((DEPTARGET, ' or later')))
1201
    data = data.replace('$ARCHITECTURES', ", ".join(universal_opts_map[UNIVERSALARCHS]))
1202
    data = data.replace('$INSTALL_SIZE', installSize())
1203 1204 1205

    # This one is not handy as a template variable
    data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
1206
    fp = open(outPath, 'w')
1207 1208 1209 1210 1211 1212
    fp.write(data)
    fp.close()

def patchScript(inPath, outPath):
    data = fileContents(inPath)
    data = data.replace('@PYVER@', getVersion())
1213
    fp = open(outPath, 'w')
1214 1215
    fp.write(data)
    fp.close()
1216
    os.chmod(outPath, STAT_0o755)
1217 1218 1219 1220 1221 1222



def packageFromRecipe(targetDir, recipe):
    curdir = os.getcwd()
    try:
1223 1224
        # The major version (such as 2.5) is included in the package name
        # because having two version of python installed at the same time is
1225 1226
        # common.
        pkgname = '%s-%s'%(recipe['name'], getVersion())
1227 1228 1229 1230 1231 1232
        srcdir  = recipe.get('source')
        pkgroot = recipe.get('topdir', srcdir)
        postflight = recipe.get('postflight')
        readme = textwrap.dedent(recipe['readme'])
        isRequired = recipe.get('required', True)

1233
        print("- building package %s"%(pkgname,))
1234 1235 1236 1237 1238 1239 1240 1241 1242 1243 1244 1245 1246 1247 1248 1249 1250 1251 1252 1253 1254 1255 1256 1257 1258 1259 1260 1261 1262 1263 1264 1265 1266 1267 1268 1269 1270 1271 1272 1273 1274 1275 1276 1277

        # Substitute some variables
        textvars = dict(
            VER=getVersion(),
            FULLVER=getFullVersion(),
        )
        readme = readme % textvars

        if pkgroot is not None:
            pkgroot = pkgroot % textvars
        else:
            pkgroot = '/'

        if srcdir is not None:
            srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
            srcdir = srcdir % textvars

        if postflight is not None:
            postflight = os.path.abspath(postflight)

        packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
        os.makedirs(packageContents)

        if srcdir is not None:
            os.chdir(srcdir)
            runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
            runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
            runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))

        fn = os.path.join(packageContents, 'PkgInfo')
        fp = open(fn, 'w')
        fp.write('pmkrpkg1')
        fp.close()

        rsrcDir = os.path.join(packageContents, "Resources")
        os.mkdir(rsrcDir)
        fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
        fp.write(readme)
        fp.close()

        if postflight is not None:
            patchScript(postflight, os.path.join(rsrcDir, 'postflight'))

        vers = getFullVersion()
1278
        major, minor = getVersionMajorMinor()
1279
        pl = Plist(
1280 1281 1282
                CFBundleGetInfoString="Python.%s %s"%(pkgname, vers,),
                CFBundleIdentifier='org.python.Python.%s'%(pkgname,),
                CFBundleName='Python.%s'%(pkgname,),
1283 1284 1285 1286 1287 1288 1289 1290 1291 1292 1293 1294 1295 1296 1297 1298 1299 1300 1301 1302
                CFBundleShortVersionString=vers,
                IFMajorVersion=major,
                IFMinorVersion=minor,
                IFPkgFormatVersion=0.10000000149011612,
                IFPkgFlagAllowBackRev=False,
                IFPkgFlagAuthorizationAction="RootAuthorization",
                IFPkgFlagDefaultLocation=pkgroot,
                IFPkgFlagFollowLinks=True,
                IFPkgFlagInstallFat=True,
                IFPkgFlagIsRequired=isRequired,
                IFPkgFlagOverwritePermissions=False,
                IFPkgFlagRelocatable=False,
                IFPkgFlagRestartAction="NoRestart",
                IFPkgFlagRootVolumeOnly=True,
                IFPkgFlagUpdateInstalledLangauges=False,
            )
        writePlist(pl, os.path.join(packageContents, 'Info.plist'))

        pl = Plist(
                    IFPkgDescriptionDescription=readme,
1303
                    IFPkgDescriptionTitle=recipe.get('long_name', "Python.%s"%(pkgname,)),
1304 1305 1306 1307 1308 1309 1310 1311 1312 1313 1314
                    IFPkgDescriptionVersion=vers,
                )
        writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))

    finally:
        os.chdir(curdir)


def makeMpkgPlist(path):

    vers = getFullVersion()
1315
    major, minor = getVersionMajorMinor()
1316 1317

    pl = Plist(
1318 1319 1320
            CFBundleGetInfoString="Python %s"%(vers,),
            CFBundleIdentifier='org.python.Python',
            CFBundleName='Python',
1321 1322 1323 1324 1325 1326
            CFBundleShortVersionString=vers,
            IFMajorVersion=major,
            IFMinorVersion=minor,
            IFPkgFlagComponentDirectory="Contents/Packages",
            IFPkgFlagPackageList=[
                dict(
1327
                    IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
1328
                    IFPkgFlagPackageSelection=item.get('selected', 'selected'),
1329
                )
1330
                for item in pkg_recipes()
1331 1332 1333 1334
            ],
            IFPkgFormatVersion=0.10000000149011612,
            IFPkgFlagBackgroundScaling="proportional",
            IFPkgFlagBackgroundAlignment="left",
1335
            IFPkgFlagAuthorizationAction="RootAuthorization",
1336 1337 1338 1339 1340 1341 1342 1343 1344 1345 1346 1347 1348 1349 1350 1351 1352 1353
        )

    writePlist(pl, path)


def buildInstaller():

    # Zap all compiled files
    for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
        for fn in filenames:
            if fn.endswith('.pyc') or fn.endswith('.pyo'):
                os.unlink(os.path.join(dirpath, fn))

    outdir = os.path.join(WORKDIR, 'installer')
    if os.path.exists(outdir):
        shutil.rmtree(outdir)
    os.mkdir(outdir)

1354
    pkgroot = os.path.join(outdir, 'Python.mpkg', 'Contents')
1355 1356
    pkgcontents = os.path.join(pkgroot, 'Packages')
    os.makedirs(pkgcontents)
1357
    for recipe in pkg_recipes():
1358 1359 1360 1361 1362 1363 1364 1365 1366 1367 1368 1369 1370
        packageFromRecipe(pkgcontents, recipe)

    rsrcDir = os.path.join(pkgroot, 'Resources')

    fn = os.path.join(pkgroot, 'PkgInfo')
    fp = open(fn, 'w')
    fp.write('pmkrpkg1')
    fp.close()

    os.mkdir(rsrcDir)

    makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
    pl = Plist(
1371
                IFPkgDescriptionTitle="Python",
1372 1373 1374 1375 1376 1377 1378 1379 1380 1381 1382
                IFPkgDescriptionVersion=getVersion(),
            )

    writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
    for fn in os.listdir('resources'):
        if fn == '.svn': continue
        if fn.endswith('.jpg'):
            shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
        else:
            patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))

1383
    shutil.copy("../../LICENSE", os.path.join(rsrcDir, 'License.txt'))
1384 1385 1386 1387 1388 1389 1390 1391 1392 1393 1394 1395 1396 1397


def installSize(clear=False, _saved=[]):
    if clear:
        del _saved[:]
    if not _saved:
        data = captureCommand("du -ks %s"%(
                    shellQuote(os.path.join(WORKDIR, '_root'))))
        _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
    return _saved[0]


def buildDMG():
    """
1398
    Create DMG containing the rootDir.
1399 1400 1401 1402 1403 1404
    """
    outdir = os.path.join(WORKDIR, 'diskimage')
    if os.path.exists(outdir):
        shutil.rmtree(outdir)

    imagepath = os.path.join(outdir,
1405
                    'python-%s-macosx%s'%(getFullVersion(),DEPTARGET))
1406
    if INCLUDE_TIMESTAMP:
1407
        imagepath = imagepath + '-%04d-%02d-%02d'%(time.localtime()[:3])
1408 1409 1410
    imagepath = imagepath + '.dmg'

    os.mkdir(outdir)
1411 1412 1413
    volname='Python %s'%(getFullVersion())
    runCommand("hdiutil create -format UDRW -volname %s -srcfolder %s %s"%(
            shellQuote(volname),
1414
            shellQuote(os.path.join(WORKDIR, 'installer')),
1415 1416 1417 1418 1419 1420 1421 1422 1423 1424 1425
            shellQuote(imagepath + ".tmp.dmg" )))


    if not os.path.exists(os.path.join(WORKDIR, "mnt")):
        os.mkdir(os.path.join(WORKDIR, "mnt"))
    runCommand("hdiutil attach %s -mountroot %s"%(
        shellQuote(imagepath + ".tmp.dmg"), shellQuote(os.path.join(WORKDIR, "mnt"))))

    # Custom icon for the DMG, shown when the DMG is mounted.
    shutil.copy("../Icons/Disk Image.icns",
            os.path.join(WORKDIR, "mnt", volname, ".VolumeIcon.icns"))
1426
    runCommand("SetFile -a C %s/"%(
1427 1428 1429 1430 1431 1432 1433 1434 1435 1436
            shellQuote(os.path.join(WORKDIR, "mnt", volname)),))

    runCommand("hdiutil detach %s"%(shellQuote(os.path.join(WORKDIR, "mnt", volname))))

    setIcon(imagepath + ".tmp.dmg", "../Icons/Disk Image.icns")
    runCommand("hdiutil convert %s -format UDZO -o %s"%(
            shellQuote(imagepath + ".tmp.dmg"), shellQuote(imagepath)))
    setIcon(imagepath, "../Icons/Disk Image.icns")

    os.unlink(imagepath + ".tmp.dmg")
1437 1438 1439 1440 1441 1442 1443 1444 1445

    return imagepath


def setIcon(filePath, icnsPath):
    """
    Set the custom icon for the specified file or directory.
    """

1446 1447
    dirPath = os.path.normpath(os.path.dirname(__file__))
    toolPath = os.path.join(dirPath, "seticon.app/Contents/MacOS/seticon")
1448 1449 1450
    if not os.path.exists(toolPath) or os.stat(toolPath).st_mtime < os.stat(dirPath + '/seticon.m').st_mtime:
        # NOTE: The tool is created inside an .app bundle, otherwise it won't work due
        # to connections to the window server.
1451 1452 1453
        appPath = os.path.join(dirPath, "seticon.app/Contents/MacOS")
        if not os.path.exists(appPath):
            os.makedirs(appPath)
1454 1455
        runCommand("cc -o %s %s/seticon.m -framework Cocoa"%(
            shellQuote(toolPath), shellQuote(dirPath)))
1456

1457 1458
    runCommand("%s %s %s"%(shellQuote(os.path.abspath(toolPath)), shellQuote(icnsPath),
        shellQuote(filePath)))
1459 1460 1461 1462 1463 1464

def main():
    # First parse options and check if we can perform our work
    parseOptions()
    checkEnvironment()

1465
    os.environ['MACOSX_DEPLOYMENT_TARGET'] = DEPTARGET
1466
    os.environ['CC'] = CC
1467
    os.environ['CXX'] = CXX
1468 1469 1470 1471 1472

    if os.path.exists(WORKDIR):
        shutil.rmtree(WORKDIR)
    os.mkdir(WORKDIR)

1473 1474
    os.environ['LC_ALL'] = 'C'

1475 1476 1477 1478 1479
    # Then build third-party libraries such as sleepycat DB4.
    buildLibraries()

    # Now build python itself
    buildPython()
1480 1481 1482 1483 1484 1485 1486 1487

    # And then build the documentation
    # Remove the Deployment Target from the shell
    # environment, it's no longer needed and
    # an unexpected build target can cause problems
    # when Sphinx and its dependencies need to
    # be (re-)installed.
    del os.environ['MACOSX_DEPLOYMENT_TARGET']
1488
    buildPythonDocs()
1489 1490 1491


    # Prepare the applications folder
1492
    fn = os.path.join(WORKDIR, "_root", "Applications",
Benjamin Peterson's avatar
Benjamin Peterson committed
1493
                "Python %s"%(getVersion(),), "Update Shell Profile.command")
1494
    patchScript("scripts/postflight.patch-profile",  fn)
1495

Benjamin Peterson's avatar
Benjamin Peterson committed
1496
    folder = os.path.join(WORKDIR, "_root", "Applications", "Python %s"%(
1497
        getVersion(),))
1498
    os.chmod(folder, STAT_0o755)
1499 1500 1501 1502 1503 1504 1505 1506 1507
    setIcon(folder, "../Icons/Python Folder.icns")

    # Create the installer
    buildInstaller()

    # And copy the readme into the directory containing the installer
    patchFile('resources/ReadMe.txt', os.path.join(WORKDIR, 'installer', 'ReadMe.txt'))

    # Ditto for the license file.
1508
    shutil.copy('../../LICENSE', os.path.join(WORKDIR, 'installer', 'License.txt'))
1509 1510

    fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
1511 1512 1513
    fp.write("# BUILD INFO\n")
    fp.write("# Date: %s\n" % time.ctime())
    fp.write("# By: %s\n" % pwd.getpwuid(os.getuid()).pw_gecos)
1514 1515 1516 1517 1518 1519 1520
    fp.close()

    # And copy it to a DMG
    buildDMG()

if __name__ == "__main__":
    main()