1#!/usr/bin/env python3 2 3import enum 4import multiprocessing 5import os 6import subprocess 7import sys 8import tarfile 9 10@enum.unique 11class Host(enum.Enum): 12 """Enumeration of supported hosts.""" 13 Darwin = 'darwin' 14 Linux = 'linux' 15 16 17def get_default_host(): 18 """Returns the Host matching the current machine.""" 19 if sys.platform.startswith('linux'): 20 return Host.Linux 21 elif sys.platform.startswith('darwin'): 22 return Host.Darwin 23 else: 24 raise RuntimeError('Unsupported host: {}'.format(sys.platform)) 25 26 27def build_autoconf_target(host, python_src, build_dir, install_dir): 28 print('## Building Python ##') 29 print('## Build Dir : {}'.format(build_dir)) 30 print('## Install Dir : {}'.format(install_dir)) 31 print('## Python Src : {}'.format(python_src)) 32 sys.stdout.flush() 33 34 os.makedirs(build_dir, exist_ok=True) 35 os.makedirs(install_dir, exist_ok=True) 36 37 cflags = ['-Wno-unused-command-line-argument'] 38 ldflags = ['-s'] 39 config_cmd = [ 40 os.path.join(python_src, 'configure'), 41 '--prefix={}'.format(install_dir), 42 '--enable-shared', 43 ] 44 env = dict(os.environ) 45 if host == Host.Darwin: 46 sdkroot = env.get('SDKROOT') 47 if sdkroot: 48 print("Using SDK {}".format(sdkroot)) 49 config_cmd.append('--enable-universalsdk={}'.format(sdkroot)) 50 else: 51 config_cmd.append('--enable-universalsdk') 52 config_cmd.append('--with-universal-archs=universal2') 53 54 MAC_MIN_VERSION = '10.9' 55 cflags.append('-mmacosx-version-min={}'.format(MAC_MIN_VERSION)) 56 cflags.append('-DMACOSX_DEPLOYMENT_TARGET={}'.format(MAC_MIN_VERSION)) 57 cflags.extend(['-arch', 'arm64']) 58 cflags.extend(['-arch', 'x86_64']) 59 env['MACOSX_DEPLOYMENT_TARGET'] = MAC_MIN_VERSION 60 ldflags.append("-Wl,-rpath,'@loader_path/../lib'") 61 62 # Disable functions to support old macOS. See https://bugs.python.org/issue31359 63 # Fails the build if any new API is used. 64 cflags.append('-Werror=unguarded-availability') 65 # Disables unavailable functions. 66 disable_funcs = [ 67 # New in 10.13 68 'utimensat', 'futimens', 69 # New in 10.12 70 'getentropy', 'clock_getres', 'clock_gettime', 'clock_settime', 71 # New in 10.10 72 'fstatat', 'faccessat', 'fchmodat', 'fchownat', 'linkat', 'fdopendir', 73 'mkdirat', 'renameat', 'unlinkat', 'readlinkat', 'symlinkat', 'openat', 74 ] 75 config_cmd.extend('ac_cv_func_{}=no'.format(f) for f in disable_funcs) 76 elif host == Host.Linux: 77 ldflags.append("-Wl,-rpath,'$$ORIGIN/../lib'") 78 79 config_cmd.append('CFLAGS={}'.format(' '.join(cflags))) 80 config_cmd.append('LDFLAGS={}'.format(' '.join(cflags + ldflags))) 81 82 subprocess.check_call(config_cmd, cwd=build_dir, env=env) 83 84 if host == Host.Darwin: 85 # By default, LC_ID_DYLIB for libpython will be set to an absolute path. 86 # Linker will embed this path to all binaries linking this library. 87 # Since configure does not give us a chance to set -install_name, we have 88 # to edit the library afterwards. 89 libpython = 'libpython3.9.dylib' 90 subprocess.check_call(['make', 91 '-j{}'.format(multiprocessing.cpu_count()), 92 libpython], 93 cwd=build_dir) 94 subprocess.check_call(['install_name_tool', '-id', '@rpath/' + libpython, 95 libpython], cwd=build_dir) 96 97 subprocess.check_call(['make', 98 '-j{}'.format(multiprocessing.cpu_count()), 99 'install'], 100 cwd=build_dir) 101 return (build_dir, install_dir) 102 103 104def package_target(host, install_dir, dest_dir, build_id): 105 package_name = 'python3-{}-{}.tar.bz2'.format(host.value, build_id) 106 package_path = os.path.join(dest_dir, package_name) 107 108 os.makedirs(dest_dir, exist_ok=True) 109 print('## Packaging Python ##') 110 print('## Package : {}'.format(package_path)) 111 print('## Install Dir : {}'.format(install_dir)) 112 sys.stdout.flush() 113 114 # Libs to exclude, from PC/layout/main.py, get_lib_layout(). 115 EXCLUDES = [ 116 "lib/python*/config-*", 117 # EXCLUDE_FROM_LIB 118 "*.pyc", "__pycache__", "*.pickle", 119 # TEST_DIRS_ONLY 120 "test", "tests", 121 # TCLTK_DIRS_ONLY 122 "tkinter", "turtledemo", 123 # IDLE_DIRS_ONLY 124 "idlelib", 125 # VENV_DIRS_ONLY 126 "venv", "ensurepip", 127 # TCLTK_FILES_ONLY 128 "turtle.py", 129 # BDIST_WININST_FILES_ONLY 130 "wininst-*", "bdist_wininst.py", 131 ] 132 tar_cmd = ['tar'] 133 for pattern in EXCLUDES: 134 tar_cmd.append('--exclude') 135 tar_cmd.append(pattern) 136 tar_cmd.extend(['-cjf', package_path, '.']) 137 print(subprocess.list2cmdline(tar_cmd)) 138 subprocess.check_call(tar_cmd, cwd=install_dir) 139 140 141def package_logs(out_dir, dest_dir): 142 os.makedirs(dest_dir, exist_ok=True) 143 print('## Packaging Logs ##') 144 sys.stdout.flush() 145 with tarfile.open(os.path.join(dest_dir, "logs.tar.bz2"), "w:bz2") as tar: 146 tar.add(os.path.join(out_dir, 'config.log'), arcname='config.log') 147 148 149def main(argv): 150 python_src = argv[1] 151 out_dir = argv[2] 152 dest_dir = argv[3] 153 build_id = argv[4] 154 host = get_default_host() 155 156 build_dir = os.path.join(out_dir, 'build') 157 install_dir = os.path.join(out_dir, 'install') 158 159 try: 160 build_autoconf_target(host, python_src, build_dir, install_dir) 161 package_target(host, install_dir, dest_dir, build_id) 162 except: 163 # Keep logs before exit. 164 package_logs(build_dir, dest_dir) 165 raise 166 167 168if __name__ == '__main__': 169 main(sys.argv) 170