• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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