Home | History | Annotate | Download | only in BuildScript
      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 32-bit builds.  64-bit or four-way universal builds require at least
      6 OS X 10.5 and the 10.5 SDK.
      7 
      8 Please ensure that this script keeps working with Python 2.5, to avoid
      9 bootstrap issues (/usr/bin/python is Python 2.5 on OSX 10.5).  Sphinx,
     10 which is used to build the documentation, currently requires at least
     11 Python 2.4.  However, as of Python 3.4.1, Doc builds require an external
     12 sphinx-build and the current versions of Sphinx now require at least
     13 Python 2.6.
     14 
     15 In addition to what is supplied with OS X 10.5+ and Xcode 3+, the script
     16 requires an installed version of hg and a third-party version of
     17 Tcl/Tk 8.4 (for OS X 10.4 and 10.5 deployment targets) or Tcl/TK 8.5
     18 (for 10.6 or later) installed in /Library/Frameworks.  When installed,
     19 the Python built by this script will attempt to dynamically link first to
     20 Tcl and Tk frameworks in /Library/Frameworks if available otherwise fall
     21 back to the ones in /System/Library/Framework.  For the build, we recommend
     22 installing the most recent ActiveTcl 8.4 or 8.5 version.
     23 
     24 32-bit-only installer builds are still possible on OS X 10.4 with Xcode 2.5
     25 and the installation of additional components, such as a newer Python
     26 (2.5 is needed for Python parser updates), hg, and for the documentation
     27 build either svn (pre-3.4.1) or sphinx-build (3.4.1 and later).
     28 
     29 Usage: see USAGE variable in the script.
     30 """
     31 import platform, os, sys, getopt, textwrap, shutil, stat, time, pwd, grp
     32 try:
     33     import urllib2 as urllib_request
     34 except ImportError:
     35     import urllib.request as urllib_request
     36 
     37 STAT_0o755 = ( stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
     38              | stat.S_IRGRP |                stat.S_IXGRP
     39              | stat.S_IROTH |                stat.S_IXOTH )
     40 
     41 STAT_0o775 = ( stat.S_IRUSR | stat.S_IWUSR | stat.S_IXUSR
     42              | stat.S_IRGRP | stat.S_IWGRP | stat.S_IXGRP
     43              | stat.S_IROTH |                stat.S_IXOTH )
     44 
     45 INCLUDE_TIMESTAMP = 1
     46 VERBOSE = 1
     47 
     48 from plistlib import Plist
     49 
     50 try:
     51     from plistlib import writePlist
     52 except ImportError:
     53     # We're run using python2.3
     54     def writePlist(plist, path):
     55         plist.write(path)
     56 
     57 def shellQuote(value):
     58     """
     59     Return the string value in a form that can safely be inserted into
     60     a shell command.
     61     """
     62     return "'%s'"%(value.replace("'", "'\"'\"'"))
     63 
     64 def grepValue(fn, variable):
     65     """
     66     Return the unquoted value of a variable from a file..
     67     QUOTED_VALUE='quotes'    -> str('quotes')
     68     UNQUOTED_VALUE=noquotes  -> str('noquotes')
     69     """
     70     variable = variable + '='
     71     for ln in open(fn, 'r'):
     72         if ln.startswith(variable):
     73             value = ln[len(variable):].strip()
     74             return value.strip("\"'")
     75     raise RuntimeError("Cannot find variable %s" % variable[:-1])
     76 
     77 _cache_getVersion = None
     78 
     79 def getVersion():
     80     global _cache_getVersion
     81     if _cache_getVersion is None:
     82         _cache_getVersion = grepValue(
     83             os.path.join(SRCDIR, 'configure'), 'PACKAGE_VERSION')
     84     return _cache_getVersion
     85 
     86 def getVersionMajorMinor():
     87     return tuple([int(n) for n in getVersion().split('.', 2)])
     88 
     89 _cache_getFullVersion = None
     90 
     91 def getFullVersion():
     92     global _cache_getFullVersion
     93     if _cache_getFullVersion is not None:
     94         return _cache_getFullVersion
     95     fn = os.path.join(SRCDIR, 'Include', 'patchlevel.h')
     96     for ln in open(fn):
     97         if 'PY_VERSION' in ln:
     98             _cache_getFullVersion = ln.split()[-1][1:-1]
     99             return _cache_getFullVersion
    100     raise RuntimeError("Cannot find full version??")
    101 
    102 FW_PREFIX = ["Library", "Frameworks", "Python.framework"]
    103 FW_VERSION_PREFIX = "--undefined--" # initialized in parseOptions
    104 
    105 # The directory we'll use to create the build (will be erased and recreated)
    106 WORKDIR = "/tmp/_py"
    107 
    108 # The directory we'll use to store third-party sources. Set this to something
    109 # else if you don't want to re-fetch required libraries every time.
    110 DEPSRC = os.path.join(WORKDIR, 'third-party')
    111 DEPSRC = os.path.expanduser('~/Universal/other-sources')
    112 
    113 # Location of the preferred SDK
    114 
    115 ### There are some issues with the SDK selection below here,
    116 ### The resulting binary doesn't work on all platforms that
    117 ### it should. Always default to the 10.4u SDK until that
    118 ### issue is resolved.
    119 ###
    120 ##if int(os.uname()[2].split('.')[0]) == 8:
    121 ##    # Explicitly use the 10.4u (universal) SDK when
    122 ##    # building on 10.4, the system headers are not
    123 ##    # useable for a universal build
    124 ##    SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
    125 ##else:
    126 ##    SDKPATH = "/"
    127 
    128 SDKPATH = "/Developer/SDKs/MacOSX10.4u.sdk"
    129 
    130 universal_opts_map = { '32-bit': ('i386', 'ppc',),
    131                        '64-bit': ('x86_64', 'ppc64',),
    132                        'intel':  ('i386', 'x86_64'),
    133                        '3-way':  ('ppc', 'i386', 'x86_64'),
    134                        'all':    ('i386', 'ppc', 'x86_64', 'ppc64',) }
    135 default_target_map = {
    136         '64-bit': '10.5',
    137         '3-way': '10.5',
    138         'intel': '10.5',
    139         'all': '10.5',
    140 }
    141 
    142 UNIVERSALOPTS = tuple(universal_opts_map.keys())
    143 
    144 UNIVERSALARCHS = '32-bit'
    145 
    146 ARCHLIST = universal_opts_map[UNIVERSALARCHS]
    147 
    148 # Source directory (assume we're in Mac/BuildScript)
    149 SRCDIR = os.path.dirname(
    150         os.path.dirname(
    151             os.path.dirname(
    152                 os.path.abspath(__file__
    153         ))))
    154 
    155 # $MACOSX_DEPLOYMENT_TARGET -> minimum OS X level
    156 DEPTARGET = '10.3'
    157 
    158 def getDeptargetTuple():
    159     return tuple([int(n) for n in DEPTARGET.split('.')[0:2]])
    160 
    161 def getTargetCompilers():
    162     target_cc_map = {
    163         '10.3': ('gcc-4.0', 'g++-4.0'),
    164         '10.4': ('gcc-4.0', 'g++-4.0'),
    165         '10.5': ('gcc-4.2', 'g++-4.2'),
    166         '10.6': ('gcc-4.2', 'g++-4.2'),
    167     }
    168     return target_cc_map.get(DEPTARGET, ('clang', 'clang++') )
    169 
    170 CC, CXX = getTargetCompilers()
    171 
    172 PYTHON_3 = getVersionMajorMinor() >= (3, 0)
    173 
    174 USAGE = textwrap.dedent("""\
    175     Usage: build_python [options]
    176 
    177     Options:
    178     -? or -h:            Show this message
    179     -b DIR
    180     --build-dir=DIR:     Create build here (default: %(WORKDIR)r)
    181     --third-party=DIR:   Store third-party sources here (default: %(DEPSRC)r)
    182     --sdk-path=DIR:      Location of the SDK (default: %(SDKPATH)r)
    183     --src-dir=DIR:       Location of the Python sources (default: %(SRCDIR)r)
    184     --dep-target=10.n    OS X deployment target (default: %(DEPTARGET)r)
    185     --universal-archs=x  universal architectures (options: %(UNIVERSALOPTS)r, default: %(UNIVERSALARCHS)r)
    186 """)% globals()
    187 
    188 # Dict of object file names with shared library names to check after building.
    189 # This is to ensure that we ended up dynamically linking with the shared
    190 # library paths and versions we expected.  For example:
    191 #   EXPECTED_SHARED_LIBS['_tkinter.so'] = [
    192 #                       '/Library/Frameworks/Tcl.framework/Versions/8.5/Tcl',
    193 #                       '/Library/Frameworks/Tk.framework/Versions/8.5/Tk']
    194 EXPECTED_SHARED_LIBS = {}
    195 
    196 # List of names of third party software built with this installer.
    197 # The names will be inserted into the rtf version of the License.
    198 THIRD_PARTY_LIBS = []
    199 
    200 # Instructions for building libraries that are necessary for building a
    201 # batteries included python.
    202 #   [The recipes are defined here for convenience but instantiated later after
    203 #    command line options have been processed.]
    204 def library_recipes():
    205     result = []
    206 
    207     LT_10_5 = bool(getDeptargetTuple() < (10, 5))
    208 
    209     if not (10, 5) < getDeptargetTuple() < (10, 10):
    210         # The OpenSSL libs shipped with OS X 10.5 and earlier are
    211         # hopelessly out-of-date and do not include Apple's tie-in to
    212         # the root certificates in the user and system keychains via TEA
    213         # that was introduced in OS X 10.6.  Note that this applies to
    214         # programs built and linked with a 10.5 SDK even when run on
    215         # newer versions of OS X.
    216         #
    217         # Dealing with CAs is messy.  For now, just supply a
    218         # local libssl and libcrypto for the older installer variants
    219         # (e.g. the python.org 10.5+ 32-bit-only installer) that use the
    220         # same default ssl certfile location as the system libs do:
    221         #   /System/Library/OpenSSL/cert.pem
    222         # Then at least TLS connections can be negotiated with sites that
    223         # use sha-256 certs like python.org, assuming the proper CA certs
    224         # have been supplied.  The default CA cert management issues for
    225         # 10.5 and earlier builds are the same as before, other than it is
    226         # now more obvious with cert checking enabled by default in the
    227         # standard library.
    228         #
    229         # For builds with 10.6 through 10.9 SDKs,
    230         # continue to use the deprecated but
    231         # less out-of-date Apple 0.9.8 libs for now.  While they are less
    232         # secure than using an up-to-date 1.0.1 version, doing so
    233         # avoids the big problems of forcing users to have to manage
    234         # default CAs themselves, thanks to the Apple libs using private TEA
    235         # APIs for cert validation from keychains if validation using the
    236         # standard OpenSSL locations (/System/Library/OpenSSL, normally empty)
    237         # fails.
    238         #
    239         # Since Apple removed the header files for the deprecated system
    240         # OpenSSL as of the Xcode 7 release (for OS X 10.10+), we do not
    241         # have much choice but to build our own copy here, too.
    242 
    243         result.extend([
    244           dict(
    245               name="OpenSSL 1.0.2j",
    246               url="https://www.openssl.org/source/openssl-1.0.2j.tar.gz",
    247               checksum='96322138f0b69e61b7212bc53d5e912b',
    248               patches=[
    249                   "openssl_sdk_makedepend.patch",
    250                    ],
    251               buildrecipe=build_universal_openssl,
    252               configure=None,
    253               install=None,
    254           ),
    255         ])
    256 
    257 #   Disable for now
    258     if False:   # if getDeptargetTuple() > (10, 5):
    259         result.extend([
    260           dict(
    261               name="Tcl 8.5.15",
    262               url="ftp://ftp.tcl.tk/pub/tcl//tcl8_5/tcl8.5.15-src.tar.gz",
    263               checksum='f3df162f92c69b254079c4d0af7a690f',
    264               buildDir="unix",
    265               configure_pre=[
    266                     '--enable-shared',
    267                     '--enable-threads',
    268                     '--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib'%(getVersion(),),
    269               ],
    270               useLDFlags=False,
    271               install='make TCL_LIBRARY=%(TCL_LIBRARY)s && make install TCL_LIBRARY=%(TCL_LIBRARY)s DESTDIR=%(DESTDIR)s'%{
    272                   "DESTDIR": shellQuote(os.path.join(WORKDIR, 'libraries')),
    273                   "TCL_LIBRARY": shellQuote('/Library/Frameworks/Python.framework/Versions/%s/lib/tcl8.5'%(getVersion())),
    274                   },
    275               ),
    276           dict(
    277               name="Tk 8.5.15",
    278               url="ftp://ftp.tcl.tk/pub/tcl//tcl8_5/tk8.5.15-src.tar.gz",
    279               checksum='55b8e33f903210a4e1c8bce0f820657f',
    280               patches=[
    281                   "issue19373_tk_8_5_15_source.patch",
    282                    ],
    283               buildDir="unix",
    284               configure_pre=[
    285                     '--enable-aqua',
    286                     '--enable-shared',
    287                     '--enable-threads',
    288                     '--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib'%(getVersion(),),
    289               ],
    290               useLDFlags=False,
    291               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'%{
    292                   "DESTDIR": shellQuote(os.path.join(WORKDIR, 'libraries')),
    293                   "TCL_LIBRARY": shellQuote('/Library/Frameworks/Python.framework/Versions/%s/lib/tcl8.5'%(getVersion())),
    294                   "TK_LIBRARY": shellQuote('/Library/Frameworks/Python.framework/Versions/%s/lib/tk8.5'%(getVersion())),
    295                   },
    296                 ),
    297         ])
    298 
    299     if PYTHON_3:
    300         result.extend([
    301           dict(
    302               name="XZ 5.0.5",
    303               url="http://tukaani.org/xz/xz-5.0.5.tar.gz",
    304               checksum='19d924e066b6fff0bc9d1981b4e53196',
    305               configure_pre=[
    306                     '--disable-dependency-tracking',
    307               ]
    308               ),
    309         ])
    310 
    311     result.extend([
    312           dict(
    313               name="NCurses 5.9",
    314               url="http://ftp.gnu.org/pub/gnu/ncurses/ncurses-5.9.tar.gz",
    315               checksum='8cb9c412e5f2d96bc6f459aa8c6282a1',
    316               configure_pre=[
    317                   "--enable-widec",
    318                   "--without-cxx",
    319                   "--without-cxx-binding",
    320                   "--without-ada",
    321                   "--without-curses-h",
    322                   "--enable-shared",
    323                   "--with-shared",
    324                   "--without-debug",
    325                   "--without-normal",
    326                   "--without-tests",
    327                   "--without-manpages",
    328                   "--datadir=/usr/share",
    329                   "--sysconfdir=/etc",
    330                   "--sharedstatedir=/usr/com",
    331                   "--with-terminfo-dirs=/usr/share/terminfo",
    332                   "--with-default-terminfo-dir=/usr/share/terminfo",
    333                   "--libdir=/Library/Frameworks/Python.framework/Versions/%s/lib"%(getVersion(),),
    334               ],
    335               patchscripts=[
    336                   ("ftp://invisible-island.net/ncurses//5.9/ncurses-5.9-20120616-patch.sh.bz2",
    337                    "f54bf02a349f96a7c4f0d00922f3a0d4"),
    338                    ],
    339               useLDFlags=False,
    340               install='make && make install DESTDIR=%s && cd %s/usr/local/lib && ln -fs ../../../Library/Frameworks/Python.framework/Versions/%s/lib/lib* .'%(
    341                   shellQuote(os.path.join(WORKDIR, 'libraries')),
    342                   shellQuote(os.path.join(WORKDIR, 'libraries')),
    343                   getVersion(),
    344                   ),
    345           ),
    346           dict(
    347               name="SQLite 3.8.3.1",
    348               url="http://www.sqlite.org/2014/sqlite-autoconf-3080301.tar.gz",
    349               checksum='509ff98d8dc9729b618b7e96612079c6',
    350               extra_cflags=('-Os '
    351                             '-DSQLITE_ENABLE_FTS4 '
    352                             '-DSQLITE_ENABLE_FTS3_PARENTHESIS '
    353                             '-DSQLITE_ENABLE_RTREE '
    354                             '-DSQLITE_TCL=0 '
    355                  '%s' % ('','-DSQLITE_WITHOUT_ZONEMALLOC ')[LT_10_5]),
    356               configure_pre=[
    357                   '--enable-threadsafe',
    358                   '--enable-shared=no',
    359                   '--enable-static=yes',
    360                   '--disable-readline',
    361                   '--disable-dependency-tracking',
    362               ]
    363           ),
    364         ])
    365 
    366     if getDeptargetTuple() < (10, 5):
    367         result.extend([
    368           dict(
    369               name="Bzip2 1.0.6",
    370               url="http://bzip.org/1.0.6/bzip2-1.0.6.tar.gz",
    371               checksum='00b516f4704d4a7cb50a1d97e6e8e15b',
    372               configure=None,
    373               install='make install CC=%s CXX=%s, PREFIX=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
    374                   CC, CXX,
    375                   shellQuote(os.path.join(WORKDIR, 'libraries')),
    376                   ' -arch '.join(ARCHLIST),
    377                   SDKPATH,
    378               ),
    379           ),
    380           dict(
    381               name="ZLib 1.2.3",
    382               url="http://www.gzip.org/zlib/zlib-1.2.3.tar.gz",
    383               checksum='debc62758716a169df9f62e6ab2bc634',
    384               configure=None,
    385               install='make install CC=%s CXX=%s, prefix=%s/usr/local/ CFLAGS="-arch %s -isysroot %s"'%(
    386                   CC, CXX,
    387                   shellQuote(os.path.join(WORKDIR, 'libraries')),
    388                   ' -arch '.join(ARCHLIST),
    389                   SDKPATH,
    390               ),
    391           ),
    392           dict(
    393               # Note that GNU readline is GPL'd software
    394               name="GNU Readline 6.1.2",
    395               url="http://ftp.gnu.org/pub/gnu/readline/readline-6.1.tar.gz" ,
    396               checksum='fc2f7e714fe792db1ce6ddc4c9fb4ef3',
    397               patchlevel='0',
    398               patches=[
    399                   # The readline maintainers don't do actual micro releases, but
    400                   # just ship a set of patches.
    401                   ('http://ftp.gnu.org/pub/gnu/readline/readline-6.1-patches/readline61-001',
    402                    'c642f2e84d820884b0bf9fd176bc6c3f'),
    403                   ('http://ftp.gnu.org/pub/gnu/readline/readline-6.1-patches/readline61-002',
    404                    '1a76781a1ea734e831588285db7ec9b1'),
    405               ]
    406           ),
    407         ])
    408 
    409     if not PYTHON_3:
    410         result.extend([
    411           dict(
    412               name="Sleepycat DB 4.7.25",
    413               url="http://download.oracle.com/berkeley-db/db-4.7.25.tar.gz",
    414               checksum='ec2b87e833779681a0c3a814aa71359e',
    415               buildDir="build_unix",
    416               configure="../dist/configure",
    417               configure_pre=[
    418                   '--includedir=/usr/local/include/db4',
    419               ]
    420           ),
    421         ])
    422 
    423     return result
    424 
    425 
    426 # Instructions for building packages inside the .mpkg.
    427 def pkg_recipes():
    428     unselected_for_python3 = ('selected', 'unselected')[PYTHON_3]
    429     result = [
    430         dict(
    431             name="PythonFramework",
    432             long_name="Python Framework",
    433             source="/Library/Frameworks/Python.framework",
    434             readme="""\
    435                 This package installs Python.framework, that is the python
    436                 interpreter and the standard library. This also includes Python
    437                 wrappers for lots of Mac OS X API's.
    438             """,
    439             postflight="scripts/postflight.framework",
    440             selected='selected',
    441         ),
    442         dict(
    443             name="PythonApplications",
    444             long_name="GUI Applications",
    445             source="/Applications/Python %(VER)s",
    446             readme="""\
    447                 This package installs IDLE (an interactive Python IDE),
    448                 Python Launcher and Build Applet (create application bundles
    449                 from python scripts).
    450 
    451                 It also installs a number of examples and demos.
    452                 """,
    453             required=False,
    454             selected='selected',
    455         ),
    456         dict(
    457             name="PythonUnixTools",
    458             long_name="UNIX command-line tools",
    459             source="/usr/local/bin",
    460             readme="""\
    461                 This package installs the unix tools in /usr/local/bin for
    462                 compatibility with older releases of Python. This package
    463                 is not necessary to use Python.
    464                 """,
    465             required=False,
    466             selected='selected',
    467         ),
    468         dict(
    469             name="PythonDocumentation",
    470             long_name="Python Documentation",
    471             topdir="/Library/Frameworks/Python.framework/Versions/%(VER)s/Resources/English.lproj/Documentation",
    472             source="/pydocs",
    473             readme="""\
    474                 This package installs the python documentation at a location
    475                 that is useable for pydoc and IDLE.
    476                 """,
    477             postflight="scripts/postflight.documentation",
    478             required=False,
    479             selected='selected',
    480         ),
    481         dict(
    482             name="PythonProfileChanges",
    483             long_name="Shell profile updater",
    484             readme="""\
    485                 This packages updates your shell profile to make sure that
    486                 the Python tools are found by your shell in preference of
    487                 the system provided Python tools.
    488 
    489                 If you don't install this package you'll have to add
    490                 "/Library/Frameworks/Python.framework/Versions/%(VER)s/bin"
    491                 to your PATH by hand.
    492                 """,
    493             postflight="scripts/postflight.patch-profile",
    494             topdir="/Library/Frameworks/Python.framework",
    495             source="/empty-dir",
    496             required=False,
    497             selected='selected',
    498         ),
    499         dict(
    500             name="PythonInstallPip",
    501             long_name="Install or upgrade pip",
    502             readme="""\
    503                 This package installs (or upgrades from an earlier version)
    504                 pip, a tool for installing and managing Python packages.
    505                 """,
    506             postflight="scripts/postflight.ensurepip",
    507             topdir="/Library/Frameworks/Python.framework",
    508             source="/empty-dir",
    509             required=False,
    510             selected='selected',
    511         ),
    512     ]
    513 
    514     if getDeptargetTuple() < (10, 4) and not PYTHON_3:
    515         result.append(
    516             dict(
    517                 name="PythonSystemFixes",
    518                 long_name="Fix system Python",
    519                 readme="""\
    520                     This package updates the system python installation on
    521                     Mac OS X 10.3 to ensure that you can build new python extensions
    522                     using that copy of python after installing this version.
    523                     """,
    524                 postflight="../Tools/fixapplepython23.py",
    525                 topdir="/Library/Frameworks/Python.framework",
    526                 source="/empty-dir",
    527                 required=False,
    528                 selected=unselected_for_python3,
    529             )
    530         )
    531 
    532     return result
    533 
    534 def fatal(msg):
    535     """
    536     A fatal error, bail out.
    537     """
    538     sys.stderr.write('FATAL: ')
    539     sys.stderr.write(msg)
    540     sys.stderr.write('\n')
    541     sys.exit(1)
    542 
    543 def fileContents(fn):
    544     """
    545     Return the contents of the named file
    546     """
    547     return open(fn, 'r').read()
    548 
    549 def runCommand(commandline):
    550     """
    551     Run a command and raise RuntimeError if it fails. Output is suppressed
    552     unless the command fails.
    553     """
    554     fd = os.popen(commandline, 'r')
    555     data = fd.read()
    556     xit = fd.close()
    557     if xit is not None:
    558         sys.stdout.write(data)
    559         raise RuntimeError("command failed: %s"%(commandline,))
    560 
    561     if VERBOSE:
    562         sys.stdout.write(data); sys.stdout.flush()
    563 
    564 def captureCommand(commandline):
    565     fd = os.popen(commandline, 'r')
    566     data = fd.read()
    567     xit = fd.close()
    568     if xit is not None:
    569         sys.stdout.write(data)
    570         raise RuntimeError("command failed: %s"%(commandline,))
    571 
    572     return data
    573 
    574 def getTclTkVersion(configfile, versionline):
    575     """
    576     search Tcl or Tk configuration file for version line
    577     """
    578     try:
    579         f = open(configfile, "r")
    580     except:
    581         fatal("Framework configuration file not found: %s" % configfile)
    582 
    583     for l in f:
    584         if l.startswith(versionline):
    585             f.close()
    586             return l
    587 
    588     fatal("Version variable %s not found in framework configuration file: %s"
    589             % (versionline, configfile))
    590 
    591 def checkEnvironment():
    592     """
    593     Check that we're running on a supported system.
    594     """
    595 
    596     if sys.version_info[0:2] < (2, 4):
    597         fatal("This script must be run with Python 2.4 or later")
    598 
    599     if platform.system() != 'Darwin':
    600         fatal("This script should be run on a Mac OS X 10.4 (or later) system")
    601 
    602     if int(platform.release().split('.')[0]) < 8:
    603         fatal("This script should be run on a Mac OS X 10.4 (or later) system")
    604 
    605     if not os.path.exists(SDKPATH):
    606         fatal("Please install the latest version of Xcode and the %s SDK"%(
    607             os.path.basename(SDKPATH[:-4])))
    608 
    609     # Because we only support dynamic load of only one major/minor version of
    610     # Tcl/Tk, ensure:
    611     # 1. there are no user-installed frameworks of Tcl/Tk with version
    612     #       higher than the Apple-supplied system version in
    613     #       SDKROOT/System/Library/Frameworks
    614     # 2. there is a user-installed framework (usually ActiveTcl) in (or linked
    615     #       in) SDKROOT/Library/Frameworks with the same version as the system
    616     #       version. This allows users to choose to install a newer patch level.
    617 
    618     frameworks = {}
    619     for framework in ['Tcl', 'Tk']:
    620         fwpth = 'Library/Frameworks/%s.framework/Versions/Current' % framework
    621         sysfw = os.path.join(SDKPATH, 'System', fwpth)
    622         libfw = os.path.join(SDKPATH, fwpth)
    623         usrfw = os.path.join(os.getenv('HOME'), fwpth)
    624         frameworks[framework] = os.readlink(sysfw)
    625         if not os.path.exists(libfw):
    626             fatal("Please install a link to a current %s %s as %s so "
    627                     "the user can override the system framework."
    628                     % (framework, frameworks[framework], libfw))
    629         if os.readlink(libfw) != os.readlink(sysfw):
    630             fatal("Version of %s must match %s" % (libfw, sysfw) )
    631         if os.path.exists(usrfw):
    632             fatal("Please rename %s to avoid possible dynamic load issues."
    633                     % usrfw)
    634 
    635     if frameworks['Tcl'] != frameworks['Tk']:
    636         fatal("The Tcl and Tk frameworks are not the same version.")
    637 
    638     # add files to check after build
    639     EXPECTED_SHARED_LIBS['_tkinter.so'] = [
    640             "/Library/Frameworks/Tcl.framework/Versions/%s/Tcl"
    641                 % frameworks['Tcl'],
    642             "/Library/Frameworks/Tk.framework/Versions/%s/Tk"
    643                 % frameworks['Tk'],
    644             ]
    645 
    646     # Remove inherited environment variables which might influence build
    647     environ_var_prefixes = ['CPATH', 'C_INCLUDE_', 'DYLD_', 'LANG', 'LC_',
    648                             'LD_', 'LIBRARY_', 'PATH', 'PYTHON']
    649     for ev in list(os.environ):
    650         for prefix in environ_var_prefixes:
    651             if ev.startswith(prefix) :
    652                 print("INFO: deleting environment variable %s=%s" % (
    653                                                     ev, os.environ[ev]))
    654                 del os.environ[ev]
    655 
    656     base_path = '/bin:/sbin:/usr/bin:/usr/sbin'
    657     if 'SDK_TOOLS_BIN' in os.environ:
    658         base_path = os.environ['SDK_TOOLS_BIN'] + ':' + base_path
    659     # Xcode 2.5 on OS X 10.4 does not include SetFile in its usr/bin;
    660     # add its fixed location here if it exists
    661     OLD_DEVELOPER_TOOLS = '/Developer/Tools'
    662     if os.path.isdir(OLD_DEVELOPER_TOOLS):
    663         base_path = base_path + ':' + OLD_DEVELOPER_TOOLS
    664     os.environ['PATH'] = base_path
    665     print("Setting default PATH: %s"%(os.environ['PATH']))
    666     # Ensure ws have access to hg and to sphinx-build.
    667     # You may have to create links in /usr/bin for them.
    668     runCommand('hg --version')
    669     runCommand('sphinx-build --version')
    670 
    671 def parseOptions(args=None):
    672     """
    673     Parse arguments and update global settings.
    674     """
    675     global WORKDIR, DEPSRC, SDKPATH, SRCDIR, DEPTARGET
    676     global UNIVERSALOPTS, UNIVERSALARCHS, ARCHLIST, CC, CXX
    677     global FW_VERSION_PREFIX
    678 
    679     if args is None:
    680         args = sys.argv[1:]
    681 
    682     try:
    683         options, args = getopt.getopt(args, '?hb',
    684                 [ 'build-dir=', 'third-party=', 'sdk-path=' , 'src-dir=',
    685                   'dep-target=', 'universal-archs=', 'help' ])
    686     except getopt.GetoptError:
    687         print(sys.exc_info()[1])
    688         sys.exit(1)
    689 
    690     if args:
    691         print("Additional arguments")
    692         sys.exit(1)
    693 
    694     deptarget = None
    695     for k, v in options:
    696         if k in ('-h', '-?', '--help'):
    697             print(USAGE)
    698             sys.exit(0)
    699 
    700         elif k in ('-d', '--build-dir'):
    701             WORKDIR=v
    702 
    703         elif k in ('--third-party',):
    704             DEPSRC=v
    705 
    706         elif k in ('--sdk-path',):
    707             SDKPATH=v
    708 
    709         elif k in ('--src-dir',):
    710             SRCDIR=v
    711 
    712         elif k in ('--dep-target', ):
    713             DEPTARGET=v
    714             deptarget=v
    715 
    716         elif k in ('--universal-archs', ):
    717             if v in UNIVERSALOPTS:
    718                 UNIVERSALARCHS = v
    719                 ARCHLIST = universal_opts_map[UNIVERSALARCHS]
    720                 if deptarget is None:
    721                     # Select alternate default deployment
    722                     # target
    723                     DEPTARGET = default_target_map.get(v, '10.3')
    724             else:
    725                 raise NotImplementedError(v)
    726 
    727         else:
    728             raise NotImplementedError(k)
    729 
    730     SRCDIR=os.path.abspath(SRCDIR)
    731     WORKDIR=os.path.abspath(WORKDIR)
    732     SDKPATH=os.path.abspath(SDKPATH)
    733     DEPSRC=os.path.abspath(DEPSRC)
    734 
    735     CC, CXX = getTargetCompilers()
    736 
    737     FW_VERSION_PREFIX = FW_PREFIX[:] + ["Versions", getVersion()]
    738 
    739     print("-- Settings:")
    740     print("   * Source directory:    %s" % SRCDIR)
    741     print("   * Build directory:     %s" % WORKDIR)
    742     print("   * SDK location:        %s" % SDKPATH)
    743     print("   * Third-party source:  %s" % DEPSRC)
    744     print("   * Deployment target:   %s" % DEPTARGET)
    745     print("   * Universal archs:     %s" % str(ARCHLIST))
    746     print("   * C compiler:          %s" % CC)
    747     print("   * C++ compiler:        %s" % CXX)
    748     print("")
    749     print(" -- Building a Python %s framework at patch level %s"
    750                 % (getVersion(), getFullVersion()))
    751     print("")
    752 
    753 def extractArchive(builddir, archiveName):
    754     """
    755     Extract a source archive into 'builddir'. Returns the path of the
    756     extracted archive.
    757 
    758     XXX: This function assumes that archives contain a toplevel directory
    759     that is has the same name as the basename of the archive. This is
    760     safe enough for almost anything we use.  Unfortunately, it does not
    761     work for current Tcl and Tk source releases where the basename of
    762     the archive ends with "-src" but the uncompressed directory does not.
    763     For now, just special case Tcl and Tk tar.gz downloads.
    764     """
    765     curdir = os.getcwd()
    766     try:
    767         os.chdir(builddir)
    768         if archiveName.endswith('.tar.gz'):
    769             retval = os.path.basename(archiveName[:-7])
    770             if ((retval.startswith('tcl') or retval.startswith('tk'))
    771                     and retval.endswith('-src')):
    772                 retval = retval[:-4]
    773             if os.path.exists(retval):
    774                 shutil.rmtree(retval)
    775             fp = os.popen("tar zxf %s 2>&1"%(shellQuote(archiveName),), 'r')
    776 
    777         elif archiveName.endswith('.tar.bz2'):
    778             retval = os.path.basename(archiveName[:-8])
    779             if os.path.exists(retval):
    780                 shutil.rmtree(retval)
    781             fp = os.popen("tar jxf %s 2>&1"%(shellQuote(archiveName),), 'r')
    782 
    783         elif archiveName.endswith('.tar'):
    784             retval = os.path.basename(archiveName[:-4])
    785             if os.path.exists(retval):
    786                 shutil.rmtree(retval)
    787             fp = os.popen("tar xf %s 2>&1"%(shellQuote(archiveName),), 'r')
    788 
    789         elif archiveName.endswith('.zip'):
    790             retval = os.path.basename(archiveName[:-4])
    791             if os.path.exists(retval):
    792                 shutil.rmtree(retval)
    793             fp = os.popen("unzip %s 2>&1"%(shellQuote(archiveName),), 'r')
    794 
    795         data = fp.read()
    796         xit = fp.close()
    797         if xit is not None:
    798             sys.stdout.write(data)
    799             raise RuntimeError("Cannot extract %s"%(archiveName,))
    800 
    801         return os.path.join(builddir, retval)
    802 
    803     finally:
    804         os.chdir(curdir)
    805 
    806 def downloadURL(url, fname):
    807     """
    808     Download the contents of the url into the file.
    809     """
    810     fpIn = urllib_request.urlopen(url)
    811     fpOut = open(fname, 'wb')
    812     block = fpIn.read(10240)
    813     try:
    814         while block:
    815             fpOut.write(block)
    816             block = fpIn.read(10240)
    817         fpIn.close()
    818         fpOut.close()
    819     except:
    820         try:
    821             os.unlink(fname)
    822         except:
    823             pass
    824 
    825 def verifyThirdPartyFile(url, checksum, fname):
    826     """
    827     Download file from url to filename fname if it does not already exist.
    828     Abort if file contents does not match supplied md5 checksum.
    829     """
    830     name = os.path.basename(fname)
    831     if os.path.exists(fname):
    832         print("Using local copy of %s"%(name,))
    833     else:
    834         print("Did not find local copy of %s"%(name,))
    835         print("Downloading %s"%(name,))
    836         downloadURL(url, fname)
    837         print("Archive for %s stored as %s"%(name, fname))
    838     if os.system(
    839             'MD5=$(openssl md5 %s) ; test "${MD5##*= }" = "%s"'
    840                 % (shellQuote(fname), checksum) ):
    841         fatal('MD5 checksum mismatch for file %s' % fname)
    842 
    843 def build_universal_openssl(basedir, archList):
    844     """
    845     Special case build recipe for universal build of openssl.
    846 
    847     The upstream OpenSSL build system does not directly support
    848     OS X universal builds.  We need to build each architecture
    849     separately then lipo them together into fat libraries.
    850     """
    851 
    852     # OpenSSL fails to build with Xcode 2.5 (on OS X 10.4).
    853     # If we are building on a 10.4.x or earlier system,
    854     # unilaterally disable assembly code building to avoid the problem.
    855     no_asm = int(platform.release().split(".")[0]) < 9
    856 
    857     def build_openssl_arch(archbase, arch):
    858         "Build one architecture of openssl"
    859         arch_opts = {
    860             "i386": ["darwin-i386-cc"],
    861             "x86_64": ["darwin64-x86_64-cc", "enable-ec_nistp_64_gcc_128"],
    862             "ppc": ["darwin-ppc-cc"],
    863             "ppc64": ["darwin64-ppc-cc"],
    864         }
    865         configure_opts = [
    866             "no-krb5",
    867             "no-idea",
    868             "no-mdc2",
    869             "no-rc5",
    870             "no-zlib",
    871             "enable-tlsext",
    872             "no-ssl2",
    873             "no-ssl3",
    874             "no-ssl3-method",
    875             # "enable-unit-test",
    876             "shared",
    877             "--install_prefix=%s"%shellQuote(archbase),
    878             "--prefix=%s"%os.path.join("/", *FW_VERSION_PREFIX),
    879             "--openssldir=/System/Library/OpenSSL",
    880         ]
    881         if no_asm:
    882             configure_opts.append("no-asm")
    883         runCommand(" ".join(["perl", "Configure"]
    884                         + arch_opts[arch] + configure_opts))
    885         runCommand("make depend OSX_SDK=%s" % SDKPATH)
    886         runCommand("make all OSX_SDK=%s" % SDKPATH)
    887         runCommand("make install_sw OSX_SDK=%s" % SDKPATH)
    888         # runCommand("make test")
    889         return
    890 
    891     srcdir = os.getcwd()
    892     universalbase = os.path.join(srcdir, "..",
    893                         os.path.basename(srcdir) + "-universal")
    894     os.mkdir(universalbase)
    895     archbasefws = []
    896     for arch in archList:
    897         # fresh copy of the source tree
    898         archsrc = os.path.join(universalbase, arch, "src")
    899         shutil.copytree(srcdir, archsrc, symlinks=True)
    900         # install base for this arch
    901         archbase = os.path.join(universalbase, arch, "root")
    902         os.mkdir(archbase)
    903         # Python framework base within install_prefix:
    904         # the build will install into this framework..
    905         # This is to ensure that the resulting shared libs have
    906         # the desired real install paths built into them.
    907         archbasefw = os.path.join(archbase, *FW_VERSION_PREFIX)
    908 
    909         # build one architecture
    910         os.chdir(archsrc)
    911         build_openssl_arch(archbase, arch)
    912         os.chdir(srcdir)
    913         archbasefws.append(archbasefw)
    914 
    915     # copy arch-independent files from last build into the basedir framework
    916     basefw = os.path.join(basedir, *FW_VERSION_PREFIX)
    917     shutil.copytree(
    918             os.path.join(archbasefw, "include", "openssl"),
    919             os.path.join(basefw, "include", "openssl")
    920             )
    921 
    922     shlib_version_number = grepValue(os.path.join(archsrc, "Makefile"),
    923             "SHLIB_VERSION_NUMBER")
    924     #   e.g. -> "1.0.0"
    925     libcrypto = "libcrypto.dylib"
    926     libcrypto_versioned = libcrypto.replace(".", "."+shlib_version_number+".")
    927     #   e.g. -> "libcrypto.1.0.0.dylib"
    928     libssl = "libssl.dylib"
    929     libssl_versioned = libssl.replace(".", "."+shlib_version_number+".")
    930     #   e.g. -> "libssl.1.0.0.dylib"
    931 
    932     try:
    933         os.mkdir(os.path.join(basefw, "lib"))
    934     except OSError:
    935         pass
    936 
    937     # merge the individual arch-dependent shared libs into a fat shared lib
    938     archbasefws.insert(0, basefw)
    939     for (lib_unversioned, lib_versioned) in [
    940                 (libcrypto, libcrypto_versioned),
    941                 (libssl, libssl_versioned)
    942             ]:
    943         runCommand("lipo -create -output " +
    944                     " ".join(shellQuote(
    945                             os.path.join(fw, "lib", lib_versioned))
    946                                     for fw in archbasefws))
    947         # and create an unversioned symlink of it
    948         os.symlink(lib_versioned, os.path.join(basefw, "lib", lib_unversioned))
    949 
    950     # Create links in the temp include and lib dirs that will be injected
    951     # into the Python build so that setup.py can find them while building
    952     # and the versioned links so that the setup.py post-build import test
    953     # does not fail.
    954     relative_path = os.path.join("..", "..", "..", *FW_VERSION_PREFIX)
    955     for fn in [
    956             ["include", "openssl"],
    957             ["lib", libcrypto],
    958             ["lib", libssl],
    959             ["lib", libcrypto_versioned],
    960             ["lib", libssl_versioned],
    961         ]:
    962         os.symlink(
    963             os.path.join(relative_path, *fn),
    964             os.path.join(basedir, "usr", "local", *fn)
    965         )
    966 
    967     return
    968 
    969 def buildRecipe(recipe, basedir, archList):
    970     """
    971     Build software using a recipe. This function does the
    972     'configure;make;make install' dance for C software, with a possibility
    973     to customize this process, basically a poor-mans DarwinPorts.
    974     """
    975     curdir = os.getcwd()
    976 
    977     name = recipe['name']
    978     THIRD_PARTY_LIBS.append(name)
    979     url = recipe['url']
    980     configure = recipe.get('configure', './configure')
    981     buildrecipe = recipe.get('buildrecipe', None)
    982     install = recipe.get('install', 'make && make install DESTDIR=%s'%(
    983         shellQuote(basedir)))
    984 
    985     archiveName = os.path.split(url)[-1]
    986     sourceArchive = os.path.join(DEPSRC, archiveName)
    987 
    988     if not os.path.exists(DEPSRC):
    989         os.mkdir(DEPSRC)
    990 
    991     verifyThirdPartyFile(url, recipe['checksum'], sourceArchive)
    992     print("Extracting archive for %s"%(name,))
    993     buildDir=os.path.join(WORKDIR, '_bld')
    994     if not os.path.exists(buildDir):
    995         os.mkdir(buildDir)
    996 
    997     workDir = extractArchive(buildDir, sourceArchive)
    998     os.chdir(workDir)
    999 
   1000     for patch in recipe.get('patches', ()):
   1001         if isinstance(patch, tuple):
   1002             url, checksum = patch
   1003             fn = os.path.join(DEPSRC, os.path.basename(url))
   1004             verifyThirdPartyFile(url, checksum, fn)
   1005         else:
   1006             # patch is a file in the source directory
   1007             fn = os.path.join(curdir, patch)
   1008         runCommand('patch -p%s < %s'%(recipe.get('patchlevel', 1),
   1009             shellQuote(fn),))
   1010 
   1011     for patchscript in recipe.get('patchscripts', ()):
   1012         if isinstance(patchscript, tuple):
   1013             url, checksum = patchscript
   1014             fn = os.path.join(DEPSRC, os.path.basename(url))
   1015             verifyThirdPartyFile(url, checksum, fn)
   1016         else:
   1017             # patch is a file in the source directory
   1018             fn = os.path.join(curdir, patchscript)
   1019         if fn.endswith('.bz2'):
   1020             runCommand('bunzip2 -fk %s' % shellQuote(fn))
   1021             fn = fn[:-4]
   1022         runCommand('sh %s' % shellQuote(fn))
   1023         os.unlink(fn)
   1024 
   1025     if 'buildDir' in recipe:
   1026         os.chdir(recipe['buildDir'])
   1027 
   1028     if configure is not None:
   1029         configure_args = [
   1030             "--prefix=/usr/local",
   1031             "--enable-static",
   1032             "--disable-shared",
   1033             #"CPP=gcc -arch %s -E"%(' -arch '.join(archList,),),
   1034         ]
   1035 
   1036         if 'configure_pre' in recipe:
   1037             args = list(recipe['configure_pre'])
   1038             if '--disable-static' in args:
   1039                 configure_args.remove('--enable-static')
   1040             if '--enable-shared' in args:
   1041                 configure_args.remove('--disable-shared')
   1042             configure_args.extend(args)
   1043 
   1044         if recipe.get('useLDFlags', 1):
   1045             configure_args.extend([
   1046                 "CFLAGS=%s-mmacosx-version-min=%s -arch %s -isysroot %s "
   1047                             "-I%s/usr/local/include"%(
   1048                         recipe.get('extra_cflags', ''),
   1049                         DEPTARGET,
   1050                         ' -arch '.join(archList),
   1051                         shellQuote(SDKPATH)[1:-1],
   1052                         shellQuote(basedir)[1:-1],),
   1053                 "LDFLAGS=-mmacosx-version-min=%s -isysroot %s -L%s/usr/local/lib -arch %s"%(
   1054                     DEPTARGET,
   1055                     shellQuote(SDKPATH)[1:-1],
   1056                     shellQuote(basedir)[1:-1],
   1057                     ' -arch '.join(archList)),
   1058             ])
   1059         else:
   1060             configure_args.extend([
   1061                 "CFLAGS=%s-mmacosx-version-min=%s -arch %s -isysroot %s "
   1062                             "-I%s/usr/local/include"%(
   1063                         recipe.get('extra_cflags', ''),
   1064                         DEPTARGET,
   1065                         ' -arch '.join(archList),
   1066                         shellQuote(SDKPATH)[1:-1],
   1067                         shellQuote(basedir)[1:-1],),
   1068             ])
   1069 
   1070         if 'configure_post' in recipe:
   1071             configure_args = configure_args + list(recipe['configure_post'])
   1072 
   1073         configure_args.insert(0, configure)
   1074         configure_args = [ shellQuote(a) for a in configure_args ]
   1075 
   1076         print("Running configure for %s"%(name,))
   1077         runCommand(' '.join(configure_args) + ' 2>&1')
   1078 
   1079     if buildrecipe is not None:
   1080         # call special-case build recipe, e.g. for openssl
   1081         buildrecipe(basedir, archList)
   1082 
   1083     if install is not None:
   1084         print("Running install for %s"%(name,))
   1085         runCommand('{ ' + install + ' ;} 2>&1')
   1086 
   1087     print("Done %s"%(name,))
   1088     print("")
   1089 
   1090     os.chdir(curdir)
   1091 
   1092 def buildLibraries():
   1093     """
   1094     Build our dependencies into $WORKDIR/libraries/usr/local
   1095     """
   1096     print("")
   1097     print("Building required libraries")
   1098     print("")
   1099     universal = os.path.join(WORKDIR, 'libraries')
   1100     os.mkdir(universal)
   1101     os.makedirs(os.path.join(universal, 'usr', 'local', 'lib'))
   1102     os.makedirs(os.path.join(universal, 'usr', 'local', 'include'))
   1103 
   1104     for recipe in library_recipes():
   1105         buildRecipe(recipe, universal, ARCHLIST)
   1106 
   1107 
   1108 
   1109 def buildPythonDocs():
   1110     # This stores the documentation as Resources/English.lproj/Documentation
   1111     # inside the framwork. pydoc and IDLE will pick it up there.
   1112     print("Install python documentation")
   1113     rootDir = os.path.join(WORKDIR, '_root')
   1114     buildDir = os.path.join('../../Doc')
   1115     docdir = os.path.join(rootDir, 'pydocs')
   1116     curDir = os.getcwd()
   1117     os.chdir(buildDir)
   1118     # The Doc build changed for 3.4 (technically, for 3.4.1) and for 2.7.9
   1119     runCommand('make clean')
   1120     # Assume sphinx-build is on our PATH, checked in checkEnvironment
   1121     runCommand('make html')
   1122     os.chdir(curDir)
   1123     if not os.path.exists(docdir):
   1124         os.mkdir(docdir)
   1125     os.rename(os.path.join(buildDir, 'build', 'html'), docdir)
   1126 
   1127 
   1128 def buildPython():
   1129     print("Building a universal python for %s architectures" % UNIVERSALARCHS)
   1130 
   1131     buildDir = os.path.join(WORKDIR, '_bld', 'python')
   1132     rootDir = os.path.join(WORKDIR, '_root')
   1133 
   1134     if os.path.exists(buildDir):
   1135         shutil.rmtree(buildDir)
   1136     if os.path.exists(rootDir):
   1137         shutil.rmtree(rootDir)
   1138     os.makedirs(buildDir)
   1139     os.makedirs(rootDir)
   1140     os.makedirs(os.path.join(rootDir, 'empty-dir'))
   1141     curdir = os.getcwd()
   1142     os.chdir(buildDir)
   1143 
   1144     # Not sure if this is still needed, the original build script
   1145     # claims that parts of the install assume python.exe exists.
   1146     os.symlink('python', os.path.join(buildDir, 'python.exe'))
   1147 
   1148     # Extract the version from the configure file, needed to calculate
   1149     # several paths.
   1150     version = getVersion()
   1151 
   1152     # Since the extra libs are not in their installed framework location
   1153     # during the build, augment the library path so that the interpreter
   1154     # will find them during its extension import sanity checks.
   1155     os.environ['DYLD_LIBRARY_PATH'] = os.path.join(WORKDIR,
   1156                                         'libraries', 'usr', 'local', 'lib')
   1157     print("Running configure...")
   1158     runCommand("%s -C --enable-framework --enable-universalsdk=%s "
   1159                "--with-universal-archs=%s "
   1160                "%s "
   1161                "%s "
   1162                "LDFLAGS='-g -L%s/libraries/usr/local/lib' "
   1163                "CFLAGS='-g -I%s/libraries/usr/local/include' 2>&1"%(
   1164         shellQuote(os.path.join(SRCDIR, 'configure')), shellQuote(SDKPATH),
   1165         UNIVERSALARCHS,
   1166         (' ', '--with-computed-gotos ')[PYTHON_3],
   1167         (' ', '--without-ensurepip ')[PYTHON_3],
   1168         shellQuote(WORKDIR)[1:-1],
   1169         shellQuote(WORKDIR)[1:-1]))
   1170 
   1171     print("Running make touch")
   1172     runCommand("make touch")
   1173 
   1174     print("Running make")
   1175     runCommand("make")
   1176 
   1177     print("Running make install")
   1178     runCommand("make install DESTDIR=%s"%(
   1179         shellQuote(rootDir)))
   1180 
   1181     print("Running make frameworkinstallextras")
   1182     runCommand("make frameworkinstallextras DESTDIR=%s"%(
   1183         shellQuote(rootDir)))
   1184 
   1185     del os.environ['DYLD_LIBRARY_PATH']
   1186     print("Copying required shared libraries")
   1187     if os.path.exists(os.path.join(WORKDIR, 'libraries', 'Library')):
   1188         runCommand("mv %s/* %s"%(
   1189             shellQuote(os.path.join(
   1190                 WORKDIR, 'libraries', 'Library', 'Frameworks',
   1191                 'Python.framework', 'Versions', getVersion(),
   1192                 'lib')),
   1193             shellQuote(os.path.join(WORKDIR, '_root', 'Library', 'Frameworks',
   1194                 'Python.framework', 'Versions', getVersion(),
   1195                 'lib'))))
   1196 
   1197     path_to_lib = os.path.join(rootDir, 'Library', 'Frameworks',
   1198                                 'Python.framework', 'Versions',
   1199                                 version, 'lib', 'python%s'%(version,))
   1200 
   1201     print("Fix file modes")
   1202     frmDir = os.path.join(rootDir, 'Library', 'Frameworks', 'Python.framework')
   1203     gid = grp.getgrnam('admin').gr_gid
   1204 
   1205     shared_lib_error = False
   1206     for dirpath, dirnames, filenames in os.walk(frmDir):
   1207         for dn in dirnames:
   1208             os.chmod(os.path.join(dirpath, dn), STAT_0o775)
   1209             os.chown(os.path.join(dirpath, dn), -1, gid)
   1210 
   1211         for fn in filenames:
   1212             if os.path.islink(fn):
   1213                 continue
   1214 
   1215             # "chmod g+w $fn"
   1216             p = os.path.join(dirpath, fn)
   1217             st = os.stat(p)
   1218             os.chmod(p, stat.S_IMODE(st.st_mode) | stat.S_IWGRP)
   1219             os.chown(p, -1, gid)
   1220 
   1221             if fn in EXPECTED_SHARED_LIBS:
   1222                 # check to see that this file was linked with the
   1223                 # expected library path and version
   1224                 data = captureCommand("otool -L %s" % shellQuote(p))
   1225                 for sl in EXPECTED_SHARED_LIBS[fn]:
   1226                     if ("\t%s " % sl) not in data:
   1227                         print("Expected shared lib %s was not linked with %s"
   1228                                 % (sl, p))
   1229                         shared_lib_error = True
   1230 
   1231     if shared_lib_error:
   1232         fatal("Unexpected shared library errors.")
   1233 
   1234     if PYTHON_3:
   1235         LDVERSION=None
   1236         VERSION=None
   1237         ABIFLAGS=None
   1238 
   1239         fp = open(os.path.join(buildDir, 'Makefile'), 'r')
   1240         for ln in fp:
   1241             if ln.startswith('VERSION='):
   1242                 VERSION=ln.split()[1]
   1243             if ln.startswith('ABIFLAGS='):
   1244                 ABIFLAGS=ln.split()[1]
   1245             if ln.startswith('LDVERSION='):
   1246                 LDVERSION=ln.split()[1]
   1247         fp.close()
   1248 
   1249         LDVERSION = LDVERSION.replace('$(VERSION)', VERSION)
   1250         LDVERSION = LDVERSION.replace('$(ABIFLAGS)', ABIFLAGS)
   1251         config_suffix = '-' + LDVERSION
   1252     else:
   1253         config_suffix = ''      # Python 2.x
   1254 
   1255     # We added some directories to the search path during the configure
   1256     # phase. Remove those because those directories won't be there on
   1257     # the end-users system. Also remove the directories from _sysconfigdata.py
   1258     # (added in 3.3) if it exists.
   1259 
   1260     include_path = '-I%s/libraries/usr/local/include' % (WORKDIR,)
   1261     lib_path = '-L%s/libraries/usr/local/lib' % (WORKDIR,)
   1262 
   1263     # fix Makefile
   1264     path = os.path.join(path_to_lib, 'config' + config_suffix, 'Makefile')
   1265     fp = open(path, 'r')
   1266     data = fp.read()
   1267     fp.close()
   1268 
   1269     for p in (include_path, lib_path):
   1270         data = data.replace(" " + p, '')
   1271         data = data.replace(p + " ", '')
   1272 
   1273     fp = open(path, 'w')
   1274     fp.write(data)
   1275     fp.close()
   1276 
   1277     # fix _sysconfigdata if it exists
   1278     #
   1279     # TODO: make this more robust!  test_sysconfig_module of
   1280     # distutils.tests.test_sysconfig.SysconfigTestCase tests that
   1281     # the output from get_config_var in both sysconfig and
   1282     # distutils.sysconfig is exactly the same for both CFLAGS and
   1283     # LDFLAGS.  The fixing up is now complicated by the pretty
   1284     # printing in _sysconfigdata.py.  Also, we are using the
   1285     # pprint from the Python running the installer build which
   1286     # may not cosmetically format the same as the pprint in the Python
   1287     # being built (and which is used to originally generate
   1288     # _sysconfigdata.py).
   1289 
   1290     import pprint
   1291     path = os.path.join(path_to_lib, '_sysconfigdata.py')
   1292     if os.path.exists(path):
   1293         fp = open(path, 'r')
   1294         data = fp.read()
   1295         fp.close()
   1296         # create build_time_vars dict
   1297         exec(data)
   1298         vars = {}
   1299         for k, v in build_time_vars.items():
   1300             if type(v) == type(''):
   1301                 for p in (include_path, lib_path):
   1302                     v = v.replace(' ' + p, '')
   1303                     v = v.replace(p + ' ', '')
   1304             vars[k] = v
   1305 
   1306         fp = open(path, 'w')
   1307         # duplicated from sysconfig._generate_posix_vars()
   1308         fp.write('# system configuration generated and used by'
   1309                     ' the sysconfig module\n')
   1310         fp.write('build_time_vars = ')
   1311         pprint.pprint(vars, stream=fp)
   1312         fp.close()
   1313 
   1314     # Add symlinks in /usr/local/bin, using relative links
   1315     usr_local_bin = os.path.join(rootDir, 'usr', 'local', 'bin')
   1316     to_framework = os.path.join('..', '..', '..', 'Library', 'Frameworks',
   1317             'Python.framework', 'Versions', version, 'bin')
   1318     if os.path.exists(usr_local_bin):
   1319         shutil.rmtree(usr_local_bin)
   1320     os.makedirs(usr_local_bin)
   1321     for fn in os.listdir(
   1322                 os.path.join(frmDir, 'Versions', version, 'bin')):
   1323         os.symlink(os.path.join(to_framework, fn),
   1324                    os.path.join(usr_local_bin, fn))
   1325 
   1326     os.chdir(curdir)
   1327 
   1328     if PYTHON_3:
   1329         # Remove the 'Current' link, that way we don't accidentally mess
   1330         # with an already installed version of python 2
   1331         os.unlink(os.path.join(rootDir, 'Library', 'Frameworks',
   1332                             'Python.framework', 'Versions', 'Current'))
   1333 
   1334 def patchFile(inPath, outPath):
   1335     data = fileContents(inPath)
   1336     data = data.replace('$FULL_VERSION', getFullVersion())
   1337     data = data.replace('$VERSION', getVersion())
   1338     data = data.replace('$MACOSX_DEPLOYMENT_TARGET', ''.join((DEPTARGET, ' or later')))
   1339     data = data.replace('$ARCHITECTURES', ", ".join(universal_opts_map[UNIVERSALARCHS]))
   1340     data = data.replace('$INSTALL_SIZE', installSize())
   1341     data = data.replace('$THIRD_PARTY_LIBS', "\\\n".join(THIRD_PARTY_LIBS))
   1342 
   1343     # This one is not handy as a template variable
   1344     data = data.replace('$PYTHONFRAMEWORKINSTALLDIR', '/Library/Frameworks/Python.framework')
   1345     fp = open(outPath, 'w')
   1346     fp.write(data)
   1347     fp.close()
   1348 
   1349 def patchScript(inPath, outPath):
   1350     major, minor = getVersionMajorMinor()
   1351     data = fileContents(inPath)
   1352     data = data.replace('@PYMAJOR@', str(major))
   1353     data = data.replace('@PYVER@', getVersion())
   1354     fp = open(outPath, 'w')
   1355     fp.write(data)
   1356     fp.close()
   1357     os.chmod(outPath, STAT_0o755)
   1358 
   1359 
   1360 
   1361 def packageFromRecipe(targetDir, recipe):
   1362     curdir = os.getcwd()
   1363     try:
   1364         # The major version (such as 2.5) is included in the package name
   1365         # because having two version of python installed at the same time is
   1366         # common.
   1367         pkgname = '%s-%s'%(recipe['name'], getVersion())
   1368         srcdir  = recipe.get('source')
   1369         pkgroot = recipe.get('topdir', srcdir)
   1370         postflight = recipe.get('postflight')
   1371         readme = textwrap.dedent(recipe['readme'])
   1372         isRequired = recipe.get('required', True)
   1373 
   1374         print("- building package %s"%(pkgname,))
   1375 
   1376         # Substitute some variables
   1377         textvars = dict(
   1378             VER=getVersion(),
   1379             FULLVER=getFullVersion(),
   1380         )
   1381         readme = readme % textvars
   1382 
   1383         if pkgroot is not None:
   1384             pkgroot = pkgroot % textvars
   1385         else:
   1386             pkgroot = '/'
   1387 
   1388         if srcdir is not None:
   1389             srcdir = os.path.join(WORKDIR, '_root', srcdir[1:])
   1390             srcdir = srcdir % textvars
   1391 
   1392         if postflight is not None:
   1393             postflight = os.path.abspath(postflight)
   1394 
   1395         packageContents = os.path.join(targetDir, pkgname + '.pkg', 'Contents')
   1396         os.makedirs(packageContents)
   1397 
   1398         if srcdir is not None:
   1399             os.chdir(srcdir)
   1400             runCommand("pax -wf %s . 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
   1401             runCommand("gzip -9 %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.pax')),))
   1402             runCommand("mkbom . %s 2>&1"%(shellQuote(os.path.join(packageContents, 'Archive.bom')),))
   1403 
   1404         fn = os.path.join(packageContents, 'PkgInfo')
   1405         fp = open(fn, 'w')
   1406         fp.write('pmkrpkg1')
   1407         fp.close()
   1408 
   1409         rsrcDir = os.path.join(packageContents, "Resources")
   1410         os.mkdir(rsrcDir)
   1411         fp = open(os.path.join(rsrcDir, 'ReadMe.txt'), 'w')
   1412         fp.write(readme)
   1413         fp.close()
   1414 
   1415         if postflight is not None:
   1416             patchScript(postflight, os.path.join(rsrcDir, 'postflight'))
   1417 
   1418         vers = getFullVersion()
   1419         major, minor = getVersionMajorMinor()
   1420         pl = Plist(
   1421                 CFBundleGetInfoString="Python.%s %s"%(pkgname, vers,),
   1422                 CFBundleIdentifier='org.python.Python.%s'%(pkgname,),
   1423                 CFBundleName='Python.%s'%(pkgname,),
   1424                 CFBundleShortVersionString=vers,
   1425                 IFMajorVersion=major,
   1426                 IFMinorVersion=minor,
   1427                 IFPkgFormatVersion=0.10000000149011612,
   1428                 IFPkgFlagAllowBackRev=False,
   1429                 IFPkgFlagAuthorizationAction="RootAuthorization",
   1430                 IFPkgFlagDefaultLocation=pkgroot,
   1431                 IFPkgFlagFollowLinks=True,
   1432                 IFPkgFlagInstallFat=True,
   1433                 IFPkgFlagIsRequired=isRequired,
   1434                 IFPkgFlagOverwritePermissions=False,
   1435                 IFPkgFlagRelocatable=False,
   1436                 IFPkgFlagRestartAction="NoRestart",
   1437                 IFPkgFlagRootVolumeOnly=True,
   1438                 IFPkgFlagUpdateInstalledLangauges=False,
   1439             )
   1440         writePlist(pl, os.path.join(packageContents, 'Info.plist'))
   1441 
   1442         pl = Plist(
   1443                     IFPkgDescriptionDescription=readme,
   1444                     IFPkgDescriptionTitle=recipe.get('long_name', "Python.%s"%(pkgname,)),
   1445                     IFPkgDescriptionVersion=vers,
   1446                 )
   1447         writePlist(pl, os.path.join(packageContents, 'Resources', 'Description.plist'))
   1448 
   1449     finally:
   1450         os.chdir(curdir)
   1451 
   1452 
   1453 def makeMpkgPlist(path):
   1454 
   1455     vers = getFullVersion()
   1456     major, minor = getVersionMajorMinor()
   1457 
   1458     pl = Plist(
   1459             CFBundleGetInfoString="Python %s"%(vers,),
   1460             CFBundleIdentifier='org.python.Python',
   1461             CFBundleName='Python',
   1462             CFBundleShortVersionString=vers,
   1463             IFMajorVersion=major,
   1464             IFMinorVersion=minor,
   1465             IFPkgFlagComponentDirectory="Contents/Packages",
   1466             IFPkgFlagPackageList=[
   1467                 dict(
   1468                     IFPkgFlagPackageLocation='%s-%s.pkg'%(item['name'], getVersion()),
   1469                     IFPkgFlagPackageSelection=item.get('selected', 'selected'),
   1470                 )
   1471                 for item in pkg_recipes()
   1472             ],
   1473             IFPkgFormatVersion=0.10000000149011612,
   1474             IFPkgFlagBackgroundScaling="proportional",
   1475             IFPkgFlagBackgroundAlignment="left",
   1476             IFPkgFlagAuthorizationAction="RootAuthorization",
   1477         )
   1478 
   1479     writePlist(pl, path)
   1480 
   1481 
   1482 def buildInstaller():
   1483 
   1484     # Zap all compiled files
   1485     for dirpath, _, filenames in os.walk(os.path.join(WORKDIR, '_root')):
   1486         for fn in filenames:
   1487             if fn.endswith('.pyc') or fn.endswith('.pyo'):
   1488                 os.unlink(os.path.join(dirpath, fn))
   1489 
   1490     outdir = os.path.join(WORKDIR, 'installer')
   1491     if os.path.exists(outdir):
   1492         shutil.rmtree(outdir)
   1493     os.mkdir(outdir)
   1494 
   1495     pkgroot = os.path.join(outdir, 'Python.mpkg', 'Contents')
   1496     pkgcontents = os.path.join(pkgroot, 'Packages')
   1497     os.makedirs(pkgcontents)
   1498     for recipe in pkg_recipes():
   1499         packageFromRecipe(pkgcontents, recipe)
   1500 
   1501     rsrcDir = os.path.join(pkgroot, 'Resources')
   1502 
   1503     fn = os.path.join(pkgroot, 'PkgInfo')
   1504     fp = open(fn, 'w')
   1505     fp.write('pmkrpkg1')
   1506     fp.close()
   1507 
   1508     os.mkdir(rsrcDir)
   1509 
   1510     makeMpkgPlist(os.path.join(pkgroot, 'Info.plist'))
   1511     pl = Plist(
   1512                 IFPkgDescriptionTitle="Python",
   1513                 IFPkgDescriptionVersion=getVersion(),
   1514             )
   1515 
   1516     writePlist(pl, os.path.join(pkgroot, 'Resources', 'Description.plist'))
   1517     for fn in os.listdir('resources'):
   1518         if fn == '.svn': continue
   1519         if fn.endswith('.jpg'):
   1520             shutil.copy(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
   1521         else:
   1522             patchFile(os.path.join('resources', fn), os.path.join(rsrcDir, fn))
   1523 
   1524 
   1525 def installSize(clear=False, _saved=[]):
   1526     if clear:
   1527         del _saved[:]
   1528     if not _saved:
   1529         data = captureCommand("du -ks %s"%(
   1530                     shellQuote(os.path.join(WORKDIR, '_root'))))
   1531         _saved.append("%d"%((0.5 + (int(data.split()[0]) / 1024.0)),))
   1532     return _saved[0]
   1533 
   1534 
   1535 def buildDMG():
   1536     """
   1537     Create DMG containing the rootDir.
   1538     """
   1539     outdir = os.path.join(WORKDIR, 'diskimage')
   1540     if os.path.exists(outdir):
   1541         shutil.rmtree(outdir)
   1542 
   1543     imagepath = os.path.join(outdir,
   1544                     'python-%s-macosx%s'%(getFullVersion(),DEPTARGET))
   1545     if INCLUDE_TIMESTAMP:
   1546         imagepath = imagepath + '-%04d-%02d-%02d'%(time.localtime()[:3])
   1547     imagepath = imagepath + '.dmg'
   1548 
   1549     os.mkdir(outdir)
   1550     volname='Python %s'%(getFullVersion())
   1551     runCommand("hdiutil create -format UDRW -volname %s -srcfolder %s %s"%(
   1552             shellQuote(volname),
   1553             shellQuote(os.path.join(WORKDIR, 'installer')),
   1554             shellQuote(imagepath + ".tmp.dmg" )))
   1555 
   1556 
   1557     if not os.path.exists(os.path.join(WORKDIR, "mnt")):
   1558         os.mkdir(os.path.join(WORKDIR, "mnt"))
   1559     runCommand("hdiutil attach %s -mountroot %s"%(
   1560         shellQuote(imagepath + ".tmp.dmg"), shellQuote(os.path.join(WORKDIR, "mnt"))))
   1561 
   1562     # Custom icon for the DMG, shown when the DMG is mounted.
   1563     shutil.copy("../Icons/Disk Image.icns",
   1564             os.path.join(WORKDIR, "mnt", volname, ".VolumeIcon.icns"))
   1565     runCommand("SetFile -a C %s/"%(
   1566             shellQuote(os.path.join(WORKDIR, "mnt", volname)),))
   1567 
   1568     runCommand("hdiutil detach %s"%(shellQuote(os.path.join(WORKDIR, "mnt", volname))))
   1569 
   1570     setIcon(imagepath + ".tmp.dmg", "../Icons/Disk Image.icns")
   1571     runCommand("hdiutil convert %s -format UDZO -o %s"%(
   1572             shellQuote(imagepath + ".tmp.dmg"), shellQuote(imagepath)))
   1573     setIcon(imagepath, "../Icons/Disk Image.icns")
   1574 
   1575     os.unlink(imagepath + ".tmp.dmg")
   1576 
   1577     return imagepath
   1578 
   1579 
   1580 def setIcon(filePath, icnsPath):
   1581     """
   1582     Set the custom icon for the specified file or directory.
   1583     """
   1584 
   1585     dirPath = os.path.normpath(os.path.dirname(__file__))
   1586     toolPath = os.path.join(dirPath, "seticon.app/Contents/MacOS/seticon")
   1587     if not os.path.exists(toolPath) or os.stat(toolPath).st_mtime < os.stat(dirPath + '/seticon.m').st_mtime:
   1588         # NOTE: The tool is created inside an .app bundle, otherwise it won't work due
   1589         # to connections to the window server.
   1590         appPath = os.path.join(dirPath, "seticon.app/Contents/MacOS")
   1591         if not os.path.exists(appPath):
   1592             os.makedirs(appPath)
   1593         runCommand("cc -o %s %s/seticon.m -framework Cocoa"%(
   1594             shellQuote(toolPath), shellQuote(dirPath)))
   1595 
   1596     runCommand("%s %s %s"%(shellQuote(os.path.abspath(toolPath)), shellQuote(icnsPath),
   1597         shellQuote(filePath)))
   1598 
   1599 def main():
   1600     # First parse options and check if we can perform our work
   1601     parseOptions()
   1602     checkEnvironment()
   1603 
   1604     os.environ['MACOSX_DEPLOYMENT_TARGET'] = DEPTARGET
   1605     os.environ['CC'] = CC
   1606     os.environ['CXX'] = CXX
   1607 
   1608     if os.path.exists(WORKDIR):
   1609         shutil.rmtree(WORKDIR)
   1610     os.mkdir(WORKDIR)
   1611 
   1612     os.environ['LC_ALL'] = 'C'
   1613 
   1614     # Then build third-party libraries such as sleepycat DB4.
   1615     buildLibraries()
   1616 
   1617     # Now build python itself
   1618     buildPython()
   1619 
   1620     # And then build the documentation
   1621     # Remove the Deployment Target from the shell
   1622     # environment, it's no longer needed and
   1623     # an unexpected build target can cause problems
   1624     # when Sphinx and its dependencies need to
   1625     # be (re-)installed.
   1626     del os.environ['MACOSX_DEPLOYMENT_TARGET']
   1627     buildPythonDocs()
   1628 
   1629 
   1630     # Prepare the applications folder
   1631     folder = os.path.join(WORKDIR, "_root", "Applications", "Python %s"%(
   1632         getVersion(),))
   1633     fn = os.path.join(folder, "License.rtf")
   1634     patchFile("resources/License.rtf",  fn)
   1635     fn = os.path.join(folder, "ReadMe.rtf")
   1636     patchFile("resources/ReadMe.rtf",  fn)
   1637     fn = os.path.join(folder, "Update Shell Profile.command")
   1638     patchScript("scripts/postflight.patch-profile",  fn)
   1639     os.chmod(folder, STAT_0o755)
   1640     setIcon(folder, "../Icons/Python Folder.icns")
   1641 
   1642     # Create the installer
   1643     buildInstaller()
   1644 
   1645     # And copy the readme into the directory containing the installer
   1646     patchFile('resources/ReadMe.rtf',
   1647                 os.path.join(WORKDIR, 'installer', 'ReadMe.rtf'))
   1648 
   1649     # Ditto for the license file.
   1650     patchFile('resources/License.rtf',
   1651                 os.path.join(WORKDIR, 'installer', 'License.rtf'))
   1652 
   1653     fp = open(os.path.join(WORKDIR, 'installer', 'Build.txt'), 'w')
   1654     fp.write("# BUILD INFO\n")
   1655     fp.write("# Date: %s\n" % time.ctime())
   1656     fp.write("# By: %s\n" % pwd.getpwuid(os.getuid()).pw_gecos)
   1657     fp.close()
   1658 
   1659     # And copy it to a DMG
   1660     buildDMG()
   1661 
   1662 if __name__ == "__main__":
   1663     main()
   1664