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