1#!/usr/bin/env python3 2 3import enum 4import glob 5import multiprocessing 6import os 7import subprocess 8import sys 9import tarfile 10 11@enum.unique 12class Host(enum.Enum): 13 """Enumeration of supported hosts.""" 14 Darwin = 'darwin' 15 Linux = 'linux' 16 17 18def get_default_host(): 19 """Returns the Host matching the current machine.""" 20 if sys.platform.startswith('linux'): 21 return Host.Linux 22 elif sys.platform.startswith('darwin'): 23 return Host.Darwin 24 else: 25 raise RuntimeError('Unsupported host: {}'.format(sys.platform)) 26 27 28def build_autoconf_target(host, python_src, build_dir, install_dir, 29 extra_ldflags): 30 print('## Building Python ##') 31 print('## Build Dir : {}'.format(build_dir)) 32 print('## Install Dir : {}'.format(install_dir)) 33 print('## Python Src : {}'.format(python_src)) 34 sys.stdout.flush() 35 36 os.makedirs(build_dir, exist_ok=True) 37 os.makedirs(install_dir, exist_ok=True) 38 39 cflags = ['-Wno-unused-command-line-argument'] 40 ldflags = ['-s'] 41 config_cmd = [ 42 os.path.join(python_src, 'configure'), 43 '--prefix={}'.format(install_dir), 44 '--enable-shared', 45 ] 46 env = dict(os.environ) 47 if host == Host.Darwin: 48 sdkroot = env.get('SDKROOT') 49 if sdkroot: 50 print("Using SDK {}".format(sdkroot)) 51 config_cmd.append('--enable-universalsdk={}'.format(sdkroot)) 52 else: 53 config_cmd.append('--enable-universalsdk') 54 config_cmd.append('--with-universal-archs=universal2') 55 56 MAC_MIN_VERSION = '10.14' 57 cflags.append('-mmacosx-version-min={}'.format(MAC_MIN_VERSION)) 58 cflags.append('-DMACOSX_DEPLOYMENT_TARGET={}'.format(MAC_MIN_VERSION)) 59 cflags.extend(['-arch', 'arm64']) 60 cflags.extend(['-arch', 'x86_64']) 61 env['MACOSX_DEPLOYMENT_TARGET'] = MAC_MIN_VERSION 62 ldflags.append("-Wl,-rpath,'@loader_path/../lib'") 63 64 # Disable functions to support old macOS. See https://bugs.python.org/issue31359 65 # Fails the build if any new API is used. 66 cflags.append('-Werror=unguarded-availability') 67 # We're building with a macOS 11+ SDK, so this should be set, but 68 # configure doesn't find it because of the unguarded-availability error 69 # combined with and older -mmacosx-version-min 70 cflags.append('-DHAVE_DYLD_SHARED_CACHE_CONTAINS_PATH=1') 71 elif host == Host.Linux: 72 # Quoting for -Wl,-rpath,$ORIGIN: 73 # - To link some binaries, make passes -Wl,-rpath,\$ORIGIN to shell. 74 # - To build stdlib extension modules, make invokes: 75 # setup.py LDSHARED='... -Wl,-rpath,\$ORIGIN ...' 76 # - distutils.util.split_quoted then splits LDSHARED into 77 # [... "-Wl,-rpath,$ORIGIN", ...]. 78 ldflags.append("-Wl,-rpath,\\$$ORIGIN/../lib") 79 80 # Omit DT_NEEDED entries for unused dynamic libraries. This is implicit 81 # with Debian's gcc driver but not with CentOS's gcc driver. 82 ldflags.append('-Wl,--as-needed') 83 84 config_cmd.append('CFLAGS={}'.format(' '.join(cflags))) 85 config_cmd.append('LDFLAGS={}'.format(' '.join(cflags + ldflags + [extra_ldflags]))) 86 87 subprocess.check_call(config_cmd, cwd=build_dir, env=env) 88 89 if host == Host.Darwin: 90 # By default, LC_ID_DYLIB for libpython will be set to an absolute path. 91 # Linker will embed this path to all binaries linking this library. 92 # Since configure does not give us a chance to set -install_name, we have 93 # to edit the library afterwards. 94 libpython = 'libpython3.10.dylib' 95 subprocess.check_call(['make', 96 '-j{}'.format(multiprocessing.cpu_count()), 97 libpython], 98 cwd=build_dir) 99 subprocess.check_call(['install_name_tool', '-id', '@rpath/' + libpython, 100 libpython], cwd=build_dir) 101 102 subprocess.check_call(['make', 103 '-j{}'.format(multiprocessing.cpu_count()), 104 'install'], 105 cwd=build_dir) 106 return (build_dir, install_dir) 107 108 109def install_licenses(host, install_dir, extra_notices): 110 (license_path,) = glob.glob(f'{install_dir}/lib/python*/LICENSE.txt') 111 with open(license_path, 'a') as out: 112 for notice in extra_notices: 113 out.write('\n-------------------------------------------------------------------\n\n') 114 with open(notice) as inp: 115 out.write(inp.read()) 116 117 118def package_target(host, install_dir, dest_dir, build_id): 119 package_name = 'python3-{}-{}.tar.bz2'.format(host.value, build_id) 120 package_path = os.path.join(dest_dir, package_name) 121 122 os.makedirs(dest_dir, exist_ok=True) 123 print('## Packaging Python ##') 124 print('## Package : {}'.format(package_path)) 125 print('## Install Dir : {}'.format(install_dir)) 126 sys.stdout.flush() 127 128 # Libs to exclude, from PC/layout/main.py, get_lib_layout(). 129 EXCLUDES = [ 130 "lib/python*/config-*", 131 # EXCLUDE_FROM_LIB 132 "*.pyc", "__pycache__", "*.pickle", 133 # TEST_DIRS_ONLY 134 "test", "tests", 135 # TCLTK_DIRS_ONLY 136 "tkinter", "turtledemo", 137 # IDLE_DIRS_ONLY 138 "idlelib", 139 # VENV_DIRS_ONLY 140 "ensurepip", 141 # TCLTK_FILES_ONLY 142 "turtle.py", 143 # BDIST_WININST_FILES_ONLY 144 "wininst-*", "bdist_wininst.py", 145 ] 146 tar_cmd = ['tar'] 147 for pattern in EXCLUDES: 148 tar_cmd.append('--exclude') 149 tar_cmd.append(pattern) 150 tar_cmd.extend(['-cjf', package_path, '.']) 151 print(subprocess.list2cmdline(tar_cmd)) 152 subprocess.check_call(tar_cmd, cwd=install_dir) 153 154 155def package_logs(out_dir, dest_dir): 156 os.makedirs(dest_dir, exist_ok=True) 157 print('## Packaging Logs ##') 158 sys.stdout.flush() 159 with tarfile.open(os.path.join(dest_dir, "logs.tar.bz2"), "w:bz2") as tar: 160 tar.add(os.path.join(out_dir, 'config.log'), arcname='config.log') 161 162 163def main(argv): 164 python_src = argv[1] 165 out_dir = argv[2] 166 dest_dir = argv[3] 167 build_id = argv[4] 168 extra_ldflags = argv[5] 169 extra_notices = argv[6].split() 170 host = get_default_host() 171 172 build_dir = os.path.join(out_dir, 'build') 173 install_dir = os.path.join(out_dir, 'install') 174 175 try: 176 build_autoconf_target(host, python_src, build_dir, install_dir, 177 extra_ldflags) 178 install_licenses(host, install_dir, extra_notices) 179 package_target(host, install_dir, dest_dir, build_id) 180 except: 181 # Keep logs before exit. 182 package_logs(build_dir, dest_dir) 183 raise 184 185 186if __name__ == '__main__': 187 main(sys.argv) 188