| # |
| # Copyright (C) 2019 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. |
| # |
| """APIs for dealing with autoconf scripts.""" |
| import multiprocessing |
| import os |
| from pathlib import Path |
| import pipes |
| import pprint |
| import shutil |
| import subprocess |
| from typing import ContextManager, Dict, List, Optional |
| |
| import ndk.ext.os |
| from ndk.hosts import Host, get_default_host |
| import ndk.paths |
| import ndk.toolchains |
| |
| |
| HOST_TRIPLE_MAP = { |
| Host.Darwin: 'x86_64-apple-darwin', |
| Host.Linux: 'x86_64-linux-gnu', |
| Host.Windows64: 'x86_64-w64-mingw32', |
| } |
| |
| |
| class AutoconfBuilder: |
| """Builder for an autoconf project.""" |
| |
| jobs_arg = f'-j{multiprocessing.cpu_count()}' |
| |
| toolchain: ndk.toolchains.Toolchain |
| |
| def __init__(self, |
| configure_script: Path, |
| build_dir: Path, |
| host: Host, |
| add_toolchain_to_path: bool = False, |
| use_clang: bool = False, |
| no_build_or_host: bool = False, |
| no_strip: bool = False, |
| additional_flags: List[str] = None, |
| additional_env: Optional[Dict[str, str]] = None) -> None: |
| """Initializes an autoconf builder. |
| |
| Args: |
| configure_script: Path to the configure script. |
| build_dir: Directory to use for building. If the directory exists, |
| it will be deleted and recreated to ensure the build is correct. |
| host: Host to be used for the --host argument (the |
| cross-compilation target). |
| add_toolchain_to_path: Adds the toolchain directory to the PATH |
| when invoking configure and make. Needed for some projects that |
| don't allow all tools to be passed via the environment. |
| use_clang: Set to True to use Clang to build this project. |
| no_build_or_host: Don't pass --build or --host to configure. |
| no_strip: Don't pass -s to compiler. |
| additional_flags: Additional flags to pass to the compiler. |
| additional_env: Additional environment to set, used during |
| configure, build, and install. |
| """ |
| self.configure_script = configure_script |
| self.build_directory = build_dir |
| self.host = host |
| self.add_toolchain_to_path = add_toolchain_to_path |
| self.use_clang = use_clang |
| self.no_build_or_host = no_build_or_host |
| self.no_strip = no_strip |
| self.additional_flags = additional_flags |
| self.additional_env = additional_env |
| |
| self.working_directory = self.build_directory / 'build' |
| self.install_directory = self.build_directory / 'install' |
| |
| if use_clang: |
| self.toolchain = ndk.toolchains.ClangToolchain(self.host) |
| else: |
| self.toolchain = ndk.toolchains.GccToolchain(self.host) |
| |
| @property |
| def flags(self) -> List[str]: |
| """Returns default cflags for the target.""" |
| # TODO: Are these the flags we want? These are what we've used |
| # historically. |
| flags = [ |
| '-Os', |
| '-fomit-frame-pointer', |
| |
| # AC_CHECK_HEADERS fails if the compiler emits any warnings. We're |
| # guaranteed to hit -Wunused-command-line-argument since autoconf |
| # does a bad job with cflags/ldflags, so we need to pass all of the |
| # flags all the time, but use -w since we won't be fixing any GDB |
| # warnings anyway and failures caused by this don't actually appear |
| # until much later in the build. |
| '-w', |
| ] |
| if not self.host == Host.Darwin: |
| flags.append('-fuse-ld=lld') |
| if not self.no_strip: |
| flags.append('-s') |
| if self.additional_flags: |
| flags.extend(self.additional_flags) |
| return flags |
| |
| def cd(self) -> ContextManager: |
| """Context manager that moves into the working directory.""" |
| return ndk.ext.os.cd(str(self.working_directory)) |
| |
| def _run(self, cmd: List[str], |
| extra_env: Optional[Dict[str, str]] = None) -> None: |
| """Runs and logs execution of a subprocess.""" |
| env = dict(extra_env) if extra_env is not None else {} |
| if self.add_toolchain_to_path: |
| paths = [str(p) for p in self.toolchain.bin_paths] |
| paths.append(os.environ['PATH']) |
| env['PATH'] = os.pathsep.join(paths) |
| |
| pp_cmd = ' '.join([pipes.quote(arg) for arg in cmd]) |
| subproc_env = dict(os.environ) |
| if env: |
| subproc_env.update(env) |
| if self.additional_env: |
| subproc_env.update(self.additional_env) |
| |
| if subproc_env != os.environ: |
| pp_env = pprint.pformat(env, indent=4) |
| print('Running: {} with env:\n{}'.format(pp_cmd, pp_env)) |
| else: |
| print('Running: {}'.format(pp_cmd)) |
| |
| subprocess.run(cmd, env=subproc_env, check=True) |
| |
| def clean(self) -> None: |
| """Cleans output directory. |
| |
| If necessary, existing output directory will be removed. After |
| removal, the inner directories (working directory, install directory, |
| and toolchain directory) will be created. |
| """ |
| if self.build_directory.exists(): |
| shutil.rmtree(self.build_directory) |
| |
| self.working_directory.mkdir(parents=True) |
| self.install_directory.mkdir(parents=True) |
| |
| def configure(self, args: List[str]) -> None: |
| """Invokes configure in the current directory with the given arguments. |
| |
| Args: |
| args: List of arguments to be passed to configure. Does not need to |
| include --prefix, --build, or --host. Those are set up |
| automatically. |
| """ |
| with self.cd(): |
| build_host_args: List[str] |
| if self.no_build_or_host: |
| build_host_args = [] |
| else: |
| build_triple = HOST_TRIPLE_MAP[get_default_host()] |
| host_triple = HOST_TRIPLE_MAP[self.host] |
| build_host_args = [ |
| f'--build={build_triple}', |
| f'--host={host_triple}', |
| ] |
| |
| configure_args = [ |
| str(self.configure_script), |
| f'--prefix={self.install_directory}', |
| ] + build_host_args + args |
| |
| flags_str = ' '.join(self.toolchain.flags + self.flags) |
| cc = f'{self.toolchain.cc} {flags_str}' |
| cxx = f'{self.toolchain.cxx} -stdlib=libc++ {flags_str}' |
| |
| configure_env: Dict[str, str] = { |
| 'CC': cc, |
| 'CXX': cxx, |
| 'LD': str(self.toolchain.ld), |
| 'AR': str(self.toolchain.ar), |
| 'AS': str(self.toolchain.asm), |
| 'RANLIB': str(self.toolchain.ranlib), |
| 'NM': str(self.toolchain.nm), |
| 'STRIP': str(self.toolchain.strip), |
| 'STRINGS': str(self.toolchain.strings), |
| } |
| if self.host.is_windows: |
| configure_env['WINDRES'] = str(self.toolchain.rescomp) |
| configure_env['RESCOMP'] = str(self.toolchain.rescomp) |
| |
| self._run(configure_args, configure_env) |
| |
| def make(self) -> None: |
| """Builds the project.""" |
| with self.cd(): |
| self._run(['make', self.jobs_arg]) |
| |
| def install(self) -> None: |
| """Installs the project.""" |
| with self.cd(): |
| self._run(['make', self.jobs_arg, 'install']) |
| |
| def build(self, configure_args: Optional[List[str]] = None) -> None: |
| """Configures and builds an autoconf project. |
| |
| Args: |
| configure_args: List of arguments to be passed to configure. Does |
| not need to include --prefix, --build, or --host. Those are set |
| up automatically. |
| """ |
| self.clean() |
| self.configure([] if configure_args is None else configure_args) |
| self.make() |
| self.install() |