| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2017 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| # |
| """Builds binutils.""" |
| import argparse |
| import logging |
| import multiprocessing |
| import os |
| from pathlib import Path |
| import shutil |
| import site |
| import subprocess |
| |
| |
| THIS_DIR = os.path.realpath(os.path.dirname(__file__)) |
| site.addsitedir(os.path.join(THIS_DIR, '../../ndk')) |
| |
| # pylint: disable=import-error,wrong-import-position |
| import ndk.abis |
| from ndk.hosts import Host |
| import ndk.paths |
| import ndk.timer |
| # pylint: enable=import-error,wrong-import-position |
| |
| |
| def logger(): |
| """Returns the module level logger.""" |
| return logging.getLogger(__name__) |
| |
| |
| def makedirs(path): |
| """os.makedirs with logging.""" |
| logger().info('makedirs %s', path) |
| os.makedirs(path) |
| |
| |
| def rmtree(path): |
| """shutil.rmtree with logging.""" |
| logger().info('rmtree %s', path) |
| shutil.rmtree(path) |
| |
| |
| def chdir(path): |
| """os.chdir with logging.""" |
| logger().info('chdir %s', path) |
| os.chdir(path) |
| |
| |
| def install_file(src, dst): |
| """shutil.copy2 with logging.""" |
| logger().info('copy %s %s', src, dst) |
| shutil.copy2(src, dst) |
| |
| |
| def check_call(cmd, *args, **kwargs): |
| """subprocess.check_call with logging.""" |
| logger().info('check_call %s', subprocess.list2cmdline(cmd)) |
| subprocess.check_call(cmd, *args, **kwargs) |
| |
| |
| def check_output(cmd, *args, **kwargs): |
| """subprocess.check_call with logging.""" |
| logger().info('check_output %s', subprocess.list2cmdline(cmd)) |
| return subprocess.check_output(cmd, *args, **kwargs) |
| |
| |
| def get_osx_deployment_target(): |
| """Determines which macOS deployment target should be used.""" |
| major, minor, _ = check_output(['sw_vers', '-productVersion'], |
| encoding='utf-8').split('.') |
| assert major == '10' |
| if minor == '8': |
| return '10.8' |
| if minor == '10': |
| return '10.9' |
| raise RuntimeError(f'Unconfigured macOS version: {major}.{minor}') |
| |
| |
| def configure(arch, host: Host, install_dir, src_dir): |
| """Configures binutils.""" |
| configure_host = { |
| Host.Darwin: 'x86_64-apple-darwin', |
| Host.Linux: 'x86_64-linux-gnu', |
| Host.Windows64: 'x86_64-w64-mingw32', |
| }[host] |
| |
| sysroot = ndk.paths.sysroot_path(ndk.abis.arch_to_toolchain(arch)) |
| configure_args = [ |
| os.path.join(src_dir, 'configure'), |
| '--target={}'.format(ndk.abis.arch_to_triple(arch)), |
| '--host={}'.format(configure_host), |
| '--enable-initfini-array', |
| '--enable-plugins', |
| '--enable-threads', |
| '--disable-nls', |
| '--with-sysroot={}'.format(sysroot), |
| '--prefix={}'.format(install_dir), |
| ] |
| |
| if arch == 'arm64': |
| configure_args.append('--enable-fix-cortex-a53-835769') |
| configure_args.append('--enable-gold') |
| else: |
| # Gold for aarch64 currently emits broken debug info. |
| # https://issuetracker.google.com/70838247 |
| configure_args.append('--enable-gold=default') |
| |
| env = {} |
| |
| flags = ['-O2', '-m64'] |
| if host == Host.Darwin: |
| toolchain = ndk.paths.android_path( |
| 'prebuilts/gcc/darwin-x86/host/i686-apple-darwin-4.2.1') |
| toolchain_prefix = 'i686-apple-darwin10' |
| deployment_target = get_osx_deployment_target() |
| # These are supposed to be synonymous, but it seems that neither is |
| # quite working as expected. Using just the flag locally causes a |
| # warning to be emitted stating that the compile and link are using |
| # different versions (with the compile version being whatever the |
| # machine happens to be), and using just the environment variable |
| # appears to not work when configuring gold. It appears that the |
| # environment is not being properly preserved? |
| # |
| # Using both should cause no harm. We'll still be getting the warning |
| # in the broken part of gold's configure, but it will allow configure |
| # to get the right answers and it should behave correctly at build |
| # time. |
| # |
| # If at any point we do end up with a successful build where neither of |
| # these arguments worked correctly, it seems it will default to either |
| # the version the toolchain was built against (10.4) or the version of |
| # the build machine (currently 10.10 at the latest). Since 10.10 is the |
| # lowest version of macOS that we support, that's still fine. |
| env['MACOSX_DEPLOYMENT_TARGET'] = deployment_target |
| flags.append(f'-mmacosx-version-min={deployment_target}') |
| elif host == Host.Linux: |
| toolchain = ndk.paths.android_path( |
| 'prebuilts/gcc/linux-x86/host/x86_64-linux-glibc2.15-4.8') |
| toolchain_prefix = 'x86_64-linux' |
| elif host.is_windows: |
| toolchain = ndk.paths.android_path( |
| 'prebuilts/gcc/linux-x86/host/x86_64-w64-mingw32-4.8') |
| toolchain_prefix = 'x86_64-w64-mingw32' |
| else: |
| raise NotImplementedError |
| |
| cc = os.path.join(toolchain, 'bin', '{}-gcc'.format(toolchain_prefix)) |
| cxx = os.path.join(toolchain, 'bin', '{}-g++'.format(toolchain_prefix)) |
| |
| # Our darwin prebuilts are gcc *only*. No binutils. |
| if host == Host.Darwin: |
| ar = 'ar' |
| strip = 'strip' |
| else: |
| ar = os.path.join(toolchain, 'bin', '{}-ar'.format(toolchain_prefix)) |
| strip = os.path.join( |
| toolchain, 'bin', '{}-strip'.format(toolchain_prefix)) |
| |
| env['AR'] = ar |
| env['CC'] = cc |
| env['CXX'] = cxx |
| env['STRIP'] = strip |
| env['CFLAGS'] = ' '.join(flags) |
| env['CXXFLAGS'] = ' '.join(flags) |
| env['LDFLAGS'] = ' '.join(flags) |
| |
| env_args = ['env'] + ['='.join([k, v]) for k, v in env.items()] |
| check_call(env_args + configure_args) |
| |
| |
| def build(jobs): |
| """Builds binutils.""" |
| check_call(['make', '-j', str(jobs)]) |
| |
| |
| def install_winpthreads(is_windows32, install_dir): |
| """Installs the winpthreads runtime to the Windows bin directory.""" |
| lib_name = 'libwinpthread-1.dll' |
| mingw_dir = ndk.paths.android_path( |
| 'prebuilts/gcc/linux-x86/host/x86_64-w64-mingw32-4.8', |
| 'x86_64-w64-mingw32') |
| # Yes, this indeed may be found in bin/ because the executables are the |
| # 64-bit version by default. |
| pthread_dir = 'lib32' if is_windows32 else 'bin' |
| lib_path = os.path.join(mingw_dir, pthread_dir, lib_name) |
| |
| lib_install = os.path.join(install_dir, 'bin', lib_name) |
| install_file(lib_path, lib_install) |
| |
| |
| def install(jobs, arch, host, install_dir): |
| """Installs binutils.""" |
| check_call(['make', 'install-strip', '-j', str(jobs)]) |
| |
| if host.is_windows: |
| arch_install_dir = os.path.join( |
| install_dir, ndk.abis.arch_to_triple(arch)) |
| install_winpthreads(host == 'win', install_dir) |
| install_winpthreads(host == 'win', arch_install_dir) |
| |
| |
| def dist(dist_dir, base_dir, package_name): |
| """Packages binutils for distribution.""" |
| has_pbzip2 = shutil.which('pbzip2') is not None |
| if has_pbzip2: |
| compress_arg = '--use-compress-prog=pbzip2' |
| else: |
| compress_arg = '-j' |
| |
| package_path = os.path.join(dist_dir, package_name + '.tar.bz2') |
| cmd = [ |
| 'tar', compress_arg, '-cf', package_path, '-C', base_dir, package_name, |
| ] |
| subprocess.check_call(cmd) |
| |
| |
| def copy_logs_to_dist_dir(build_dir: Path, base_log_dir: Path) -> None: |
| """Preserves any relevant log files from the build directory.""" |
| log_file = 'config.log' |
| log_dir = base_log_dir / 'autoconf' |
| for root, _, files in os.walk(build_dir): |
| root_path = Path(root) |
| if log_file not in files: |
| continue |
| rel_path = Path(root).relative_to(build_dir) |
| dest_dir = log_dir / rel_path |
| dest_dir.mkdir(parents=True, exist_ok=True) |
| shutil.copyfile(str(root_path / log_file), str(dest_dir / log_file)) |
| |
| |
| def parse_args(): |
| """Parse command line arguments.""" |
| parser = argparse.ArgumentParser() |
| |
| def host_from_arg(arg: str) -> Host: |
| return { |
| 'darwin': Host.Darwin, |
| 'linux': Host.Linux, |
| 'win64': Host.Windows64, |
| }[arg] |
| |
| parser.add_argument( |
| '--arch', choices=ndk.abis.ALL_ARCHITECTURES, required=True) |
| parser.add_argument( |
| '--host', choices=Host, type=host_from_arg, required=True) |
| |
| parser.add_argument( |
| '--clean', action='store_true', |
| help='Clean the out directory before building.') |
| parser.add_argument( |
| '-j', '--jobs', type=int, default=multiprocessing.cpu_count(), |
| help='Number of jobs to use when building.') |
| |
| return parser.parse_args() |
| |
| |
| def main(): |
| """Program entry point.""" |
| args = parse_args() |
| logging.basicConfig(level=logging.INFO) |
| |
| total_timer = ndk.timer.Timer() |
| total_timer.start() |
| |
| out_dir = ndk.paths.get_out_dir() |
| dist_dir = ndk.paths.get_dist_dir(out_dir) |
| artifact_host = { |
| Host.Darwin: 'darwin', |
| Host.Linux: 'linux', |
| Host.Windows64: 'win64', |
| }[args.host] |
| base_build_dir = os.path.join(out_dir, 'binutils', artifact_host, |
| args.arch) |
| build_dir = os.path.join(base_build_dir, 'build') |
| package_name = 'binutils-{}-{}'.format(args.arch, artifact_host) |
| install_dir = os.path.join(base_build_dir, 'install', package_name) |
| binutils_path = os.path.join(THIS_DIR, 'binutils-2.27') |
| |
| did_clean = False |
| clean_timer = ndk.timer.Timer() |
| if args.clean and os.path.exists(build_dir): |
| did_clean = True |
| with clean_timer: |
| rmtree(build_dir) |
| |
| if not os.path.exists(build_dir): |
| makedirs(build_dir) |
| |
| orig_dir = os.getcwd() |
| chdir(build_dir) |
| try: |
| configure_timer = ndk.timer.Timer() |
| with configure_timer: |
| configure(args.arch, args.host, install_dir, binutils_path) |
| |
| build_timer = ndk.timer.Timer() |
| with build_timer: |
| build(args.jobs) |
| |
| install_timer = ndk.timer.Timer() |
| with install_timer: |
| install(args.jobs, args.arch, args.host, install_dir) |
| finally: |
| copy_logs_to_dist_dir(Path(build_dir), Path(dist_dir) / 'logs') |
| chdir(orig_dir) |
| |
| package_timer = ndk.timer.Timer() |
| with package_timer: |
| dist(dist_dir, os.path.dirname(install_dir), package_name) |
| |
| total_timer.finish() |
| |
| if did_clean: |
| print('Clean: {}'.format(clean_timer.duration)) |
| print('Configure: {}'.format(configure_timer.duration)) |
| print('Build: {}'.format(build_timer.duration)) |
| print('Install: {}'.format(install_timer.duration)) |
| print('Package: {}'.format(package_timer.duration)) |
| print('Total: {}'.format(total_timer.duration)) |
| |
| |
| if __name__ == '__main__': |
| main() |