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