blob: 98471aa93a9e4c5cf30fcea19b72e6b5f2fedfb5 [file] [log] [blame]
#
# Copyright (C) 2020 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 cmake scripts."""
import os
from pathlib import Path
import pprint
import shlex
import shutil
import subprocess
from typing import Dict, List, Optional
from ndk.hosts import Host, get_default_host
import ndk.paths
import ndk.toolchains
SYSTEM_NAME_MAP = {
Host.Darwin: 'Darwin',
Host.Linux: 'Linux',
Host.Windows64: 'Windows'
}
HOST_TRIPLE_MAP = {
Host.Darwin: 'x86_64-apple-darwin',
Host.Linux: 'x86_64-linux-gnu',
Host.Windows64: 'x86_64-w64-mingw32',
}
class CMakeBuilder:
"""Builder for an cmake project."""
toolchain: ndk.toolchains.Toolchain
def __init__(self,
src_path: Path,
build_dir: Path,
host: Host,
additional_flags: List[str] = None,
additional_env: Optional[Dict[str, str]] = None) -> None:
"""Initializes an autoconf builder.
Args:
src_path: Path to the cmake project.
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).
additional_flags: Additional flags to pass to the compiler.
additional_env: Additional environment to set, used during
configure, build, and install.
"""
self.src_path = src_path
self.build_directory = build_dir
self.host = host
self.additional_flags = additional_flags
self.additional_env = additional_env
self.working_directory = self.build_directory / 'build'
self.install_directory = self.build_directory / 'install'
self.toolchain = ndk.toolchains.ClangToolchain(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',
'-s',
]
if not self.host == Host.Darwin:
flags.append('-fuse-ld=lld')
if self.additional_flags:
flags.extend(self.additional_flags)
return flags
def _run(self, cmd: List[str]) -> None:
"""Runs and logs execution of a subprocess."""
subproc_env = dict(os.environ)
if self.additional_env:
subproc_env.update(self.additional_env)
pp_cmd = shlex.join(cmd)
if subproc_env != os.environ:
pp_env = pprint.pformat(self.additional_env, indent=4)
print('Running: {} with env:\n{}'.format(pp_cmd, pp_env))
else:
print('Running: {}'.format(pp_cmd))
subprocess.check_call(cmd, env=subproc_env, cwd=self.working_directory)
@property
def _cmake(self) -> Path:
return (ndk.paths.ANDROID_DIR / 'prebuilts' / 'cmake' /
(get_default_host().value + '-x86') / 'bin' / 'cmake')
@property
def _ninja(self) -> Path:
return (ndk.paths.ANDROID_DIR / 'prebuilts' / 'ninja' /
(get_default_host().value + '-x86') / 'ninja')
@property
def cmake_defines(self) -> Dict[str, str]:
"""CMake defines."""
flags = self.toolchain.flags + self.flags
cflags = ' '.join(flags)
cxxflags = ' '.join(flags + ['-stdlib=libc++'])
defines: Dict[str, str] = {
'CMAKE_C_COMPILER': str(self.toolchain.cc),
'CMAKE_C_COMPILER_TARGET': HOST_TRIPLE_MAP[self.host],
'CMAKE_CXX_COMPILER': str(self.toolchain.cxx),
'CMAKE_CXX_COMPILER_TARGET': HOST_TRIPLE_MAP[self.host],
'CMAKE_AR': str(self.toolchain.ar),
'CMAKE_RANLIB': str(self.toolchain.ranlib),
'CMAKE_NM': str(self.toolchain.nm),
'CMAKE_STRIP': str(self.toolchain.strip),
'CMAKE_LINKER': str(self.toolchain.ld),
'CMAKE_ASM_FLAGS': cflags,
'CMAKE_C_FLAGS': cflags,
'CMAKE_CXX_FLAGS': cxxflags,
'CMAKE_BUILD_TYPE': 'Release',
'CMAKE_INSTALL_PREFIX': str(self.install_directory),
'CMAKE_MAKE_PROGRAM': str(self._ninja),
'CMAKE_SYSTEM_NAME': SYSTEM_NAME_MAP[self.host],
'CMAKE_SYSTEM_PROCESSOR': 'x86_64',
'CMAKE_FIND_ROOT_PATH_MODE_INCLUDE': 'ONLY',
'CMAKE_FIND_ROOT_PATH_MODE_LIBRARY': 'ONLY',
'CMAKE_FIND_ROOT_PATH_MODE_PACKAGE': 'ONLY',
'CMAKE_FIND_ROOT_PATH_MODE_PROGRAM': 'NEVER',
}
if self.host.is_windows:
defines['CMAKE_RC'] = str(self.toolchain.rescomp)
return defines
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, additional_defines: Dict[str, str]) -> None:
"""Invokes cmake configure."""
cmake_cmd = [str(self._cmake), '-GNinja']
defines = self.cmake_defines
defines.update(additional_defines)
cmake_cmd.extend(f'-D{key}={val}' for key, val in defines.items())
cmake_cmd.append(str(self.src_path))
self._run(cmake_cmd)
def make(self) -> None:
"""Builds the project."""
self._run([str(self._ninja)])
def install(self) -> None:
"""Installs the project."""
self._run([str(self._ninja), 'install'])
def build(self,
additional_defines: Optional[Dict[str, str]] = None) -> None:
"""Configures and builds an cmake 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 additional_defines is None else additional_defines)
self.make()
self.install()