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