| #!/usr/bin/env python3 |
| # |
| # Copyright (C) 2016 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. |
| # |
| """Creates a toolchain installation for a given Android target. |
| |
| THIS TOOL IS OBSOLETE. It is no longer necessary to create a separate toolchain for use |
| with build systems that lack explicit NDK support. The compiler installed to |
| <NDK>/toolchains/llvm/prebuilt/<host>/bin can be used directly. See |
| https://developer.android.com/ndk/guides/other_build_systems for more details. |
| """ |
| import argparse |
| import atexit |
| import inspect |
| import json |
| import logging |
| import os |
| import shutil |
| import stat |
| import sys |
| import tempfile |
| import textwrap |
| from pathlib import Path |
| |
| THIS_DIR = os.path.realpath(os.path.dirname(__file__)) |
| NDK_DIR = os.path.realpath(os.path.join(THIS_DIR, "../..")) |
| |
| |
| def logger(): |
| """Return the main logger for this module.""" |
| return logging.getLogger(__name__) |
| |
| |
| def check_ndk_or_die(): |
| """Verify that our NDK installation is somewhat present or die.""" |
| checks = [ |
| "build/core", |
| "prebuilt", |
| "toolchains", |
| ] |
| |
| for check in checks: |
| check_path = os.path.join(NDK_DIR, check) |
| if not os.path.exists(check_path): |
| sys.exit("Missing {}".format(check_path)) |
| |
| |
| def get_triple(arch): |
| """Return the triple for the given architecture.""" |
| return { |
| "arm": "arm-linux-androideabi", |
| "arm64": "aarch64-linux-android", |
| "riscv64": "riscv64-linux-android", |
| "x86": "i686-linux-android", |
| "x86_64": "x86_64-linux-android", |
| }[arch] |
| |
| |
| def arch_to_abi(arch: str) -> str: |
| """Return the ABI name for the given architecture.""" |
| return { |
| "arm": "armeabi-v7a", |
| "arm64": "arm64-v8a", |
| "riscv64": "riscv64", |
| "x86": "x86", |
| "x86_64": "x86_64", |
| }[arch] |
| |
| |
| def get_host_tag_or_die(): |
| """Return the host tag for this platform. Die if not supported.""" |
| if sys.platform.startswith("linux"): |
| return "linux-x86_64" |
| elif sys.platform == "darwin": |
| return "darwin-x86_64" |
| elif sys.platform == "win32" or sys.platform == "cygwin": |
| return "windows-x86_64" |
| sys.exit("Unsupported platform: " + sys.platform) |
| |
| |
| def get_toolchain_path_or_die(host_tag): |
| """Return the toolchain path or die.""" |
| toolchain_path = os.path.join(NDK_DIR, "toolchains/llvm/prebuilt", host_tag) |
| if not os.path.exists(toolchain_path): |
| sys.exit("Could not find toolchain: {}".format(toolchain_path)) |
| return toolchain_path |
| |
| |
| def make_clang_target(triple, api): |
| """Returns the Clang target for the given GNU triple and API combo.""" |
| arch, os_name, env = triple.split("-") |
| if arch == "arm": |
| arch = "armv7a" # Target armv7, not armv5. |
| |
| return "{}-{}-{}{}".format(arch, os_name, env, api) |
| |
| |
| def make_clang_scripts(install_dir, arch, api, windows): |
| """Creates Clang wrapper scripts. |
| |
| The Clang in standalone toolchains historically was designed to be used as |
| a drop-in replacement for GCC for better compatibility with existing |
| projects. Since a large number of projects are not set up for cross |
| compiling (and those that are expect the GCC style), our Clang should |
| already know what target it is building for. |
| |
| Create wrapper scripts that invoke Clang with `-target` and `--sysroot` |
| preset. |
| """ |
| with open(os.path.join(install_dir, "AndroidVersion.txt")) as version_file: |
| first_line = version_file.read().strip().splitlines()[0] |
| major, minor, _build = first_line.split(".") |
| |
| version_number = major + minor |
| |
| exe = "" |
| if windows: |
| exe = ".exe" |
| |
| bin_dir = os.path.join(install_dir, "bin") |
| shutil.move( |
| os.path.join(bin_dir, "clang" + exe), |
| os.path.join(bin_dir, "clang{}".format(version_number) + exe), |
| ) |
| shutil.move( |
| os.path.join(bin_dir, "clang++" + exe), |
| os.path.join(bin_dir, "clang{}++".format(version_number) + exe), |
| ) |
| |
| triple = get_triple(arch) |
| target = make_clang_target(triple, api) |
| flags = "-target {}".format(target) |
| |
| # We only need mstackrealign to fix issues on 32-bit x86 pre-24. After 24, |
| # this consumes an extra register unnecessarily, which can cause issues for |
| # inline asm. |
| # https://github.com/android-ndk/ndk/issues/693 |
| if arch == "i686" and api < 24: |
| flags += " -mstackrealign" |
| |
| cxx_flags = str(flags) |
| |
| clang_path = os.path.join(install_dir, "bin/clang") |
| with open(clang_path, "w") as clang: |
| clang.write( |
| textwrap.dedent( |
| """\ |
| #!/usr/bin/env bash |
| bin_dir=`dirname "$0"` |
| if [ "$1" != "-cc1" ]; then |
| "$bin_dir/clang{version}" {flags} "$@" |
| else |
| # target/triple already spelled out. |
| "$bin_dir/clang{version}" "$@" |
| fi |
| """.format( |
| version=version_number, flags=flags |
| ) |
| ) |
| ) |
| |
| mode = os.stat(clang_path).st_mode |
| os.chmod(clang_path, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) |
| |
| clangpp_path = os.path.join(install_dir, "bin/clang++") |
| with open(clangpp_path, "w") as clangpp: |
| clangpp.write( |
| textwrap.dedent( |
| """\ |
| #!/usr/bin/env bash |
| bin_dir=`dirname "$0"` |
| if [ "$1" != "-cc1" ]; then |
| "$bin_dir/clang{version}++" {flags} "$@" |
| else |
| # target/triple already spelled out. |
| "$bin_dir/clang{version}++" "$@" |
| fi |
| """.format( |
| version=version_number, flags=cxx_flags |
| ) |
| ) |
| ) |
| |
| mode = os.stat(clangpp_path).st_mode |
| os.chmod(clangpp_path, mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) |
| |
| shutil.copy2( |
| os.path.join(install_dir, "bin/clang"), |
| os.path.join(install_dir, "bin", triple + "-clang"), |
| ) |
| shutil.copy2( |
| os.path.join(install_dir, "bin/clang++"), |
| os.path.join(install_dir, "bin", triple + "-clang++"), |
| ) |
| |
| if windows: |
| for pp_suffix in ("", "++"): |
| is_cpp = pp_suffix == "++" |
| exe_name = "clang{}{}.exe".format(version_number, pp_suffix) |
| clangbat_text = textwrap.dedent( |
| """\ |
| @echo off |
| setlocal |
| call :find_bin |
| if "%1" == "-cc1" goto :L |
| |
| set "_BIN_DIR=" && "%_BIN_DIR%{exe}" {flags} %* |
| if ERRORLEVEL 1 exit /b 1 |
| goto :done |
| |
| :L |
| rem target/triple already spelled out. |
| set "_BIN_DIR=" && "%_BIN_DIR%{exe}" %* |
| if ERRORLEVEL 1 exit /b 1 |
| goto :done |
| |
| :find_bin |
| rem Accommodate a quoted arg0, e.g.: "clang" |
| rem https://github.com/android-ndk/ndk/issues/616 |
| set _BIN_DIR=%~dp0 |
| exit /b |
| |
| :done |
| """.format( |
| exe=exe_name, flags=cxx_flags if is_cpp else flags |
| ) |
| ) |
| |
| for triple_prefix in ("", triple + "-"): |
| clangbat_path = os.path.join( |
| install_dir, "bin", "{}clang{}.cmd".format(triple_prefix, pp_suffix) |
| ) |
| with open(clangbat_path, "w") as clangbat: |
| clangbat.write(clangbat_text) |
| |
| |
| def replace_gcc_wrappers(install_path, triple, is_windows): |
| cmd = ".cmd" if is_windows else "" |
| |
| gcc = os.path.join(install_path, "bin", triple + "-gcc" + cmd) |
| clang = os.path.join(install_path, "bin", "clang" + cmd) |
| shutil.copy2(clang, gcc) |
| |
| gpp = os.path.join(install_path, "bin", triple + "-g++" + cmd) |
| clangpp = os.path.join(install_path, "bin", "clang++" + cmd) |
| shutil.copy2(clangpp, gpp) |
| |
| |
| def copytree(src, dst): |
| # A Python invocation running concurrently with make_standalone_toolchain.py |
| # can create a __pycache__ directory inside the src dir. Avoid copying it, |
| # because it can be in an inconsistent state. |
| shutil.copytree( |
| src, dst, ignore=shutil.ignore_patterns("__pycache__"), dirs_exist_ok=True |
| ) |
| |
| |
| def create_toolchain(install_path, arch, api, toolchain_path, host_tag): |
| """Create a standalone toolchain.""" |
| copytree(toolchain_path, install_path) |
| triple = get_triple(arch) |
| make_clang_scripts(install_path, arch, api, host_tag == "windows-x86_64") |
| replace_gcc_wrappers(install_path, triple, host_tag == "windows-x86_64") |
| |
| prebuilt_path = os.path.join(NDK_DIR, "prebuilt", host_tag) |
| copytree(prebuilt_path, install_path) |
| |
| |
| def warn_unnecessary(arch, api, host_tag): |
| """Emits a warning that this script is no longer needed.""" |
| if host_tag == "windows-x86_64": |
| ndk_var = "%NDK%" |
| prompt = "C:\\>" |
| else: |
| ndk_var = "$NDK" |
| prompt = "$ " |
| |
| target = make_clang_target(get_triple(arch), api) |
| standalone_toolchain = os.path.join( |
| ndk_var, "build", "tools", "make_standalone_toolchain.py" |
| ) |
| toolchain_dir = os.path.join( |
| ndk_var, "toolchains", "llvm", "prebuilt", host_tag, "bin" |
| ) |
| old_clang = os.path.join("toolchain", "bin", "clang++") |
| new_clang = os.path.join(toolchain_dir, target + "-clang++") |
| |
| logger().warning( |
| textwrap.dedent( |
| """\ |
| THIS TOOL IS OBSOLETE. The {toolchain_dir} directory contains |
| target-specific scripts that perform the same task. For example, |
| instead of: |
| |
| {prompt}python {standalone_toolchain} \\ |
| --arch {arch} --api {api} --install-dir toolchain |
| {prompt}{old_clang} src.cpp |
| |
| Instead use: |
| |
| {prompt}{new_clang} src.cpp |
| |
| See https://developer.android.com/ndk/guides/other_build_systems for more |
| details. |
| """.format( |
| toolchain_dir=toolchain_dir, |
| prompt=prompt, |
| standalone_toolchain=standalone_toolchain, |
| arch=arch, |
| api=api, |
| old_clang=old_clang, |
| new_clang=new_clang, |
| ) |
| ) |
| ) |
| |
| |
| def get_min_supported_api_level(arch: str) -> int: |
| abis_json = Path(NDK_DIR) / "meta/abis.json" |
| with abis_json.open(encoding="utf-8") as abis_file: |
| data = json.load(abis_file) |
| return int(data[arch_to_abi(arch)]["min_os_version"]) |
| |
| |
| def parse_args(): |
| """Parse command line arguments from sys.argv.""" |
| parser = argparse.ArgumentParser( |
| description=inspect.getdoc(sys.modules[__name__]), |
| # Even when there are invalid arguments, we want to emit the deprecation |
| # warning. We usually wait until after argument parsing to emit that warning so |
| # that we can use the --abi and --api inputs to provide a more complete |
| # replacement example, so to do that in the case of an argument error we need to |
| # catch the error rather than allow it to exit immediately. |
| exit_on_error=False, |
| ) |
| |
| parser.add_argument( |
| "--arch", required=True, choices=("arm", "arm64", "riscv64", "x86", "x86_64") |
| ) |
| parser.add_argument( |
| "--api", type=int, help='Target the given API version (example: "--api 24").' |
| ) |
| parser.add_argument( |
| "--stl", help="Ignored. Retained for compatibility until NDK r19." |
| ) |
| |
| parser.add_argument( |
| "--force", |
| action="store_true", |
| help="Remove existing installation directory if it exists.", |
| ) |
| parser.add_argument( |
| "-v", "--verbose", action="count", help="Increase output verbosity." |
| ) |
| |
| def path_arg(arg): |
| return os.path.realpath(os.path.expanduser(arg)) |
| |
| output_group = parser.add_mutually_exclusive_group() |
| output_group.add_argument( |
| "--package-dir", |
| type=path_arg, |
| default=os.getcwd(), |
| help=( |
| "Build a tarball and install it to the given directory. If " |
| "neither --package-dir nor --install-dir is specified, a " |
| "tarball will be created and installed to the current " |
| "directory." |
| ), |
| ) |
| output_group.add_argument( |
| "--install-dir", |
| type=path_arg, |
| help="Install toolchain to the given directory instead of packaging.", |
| ) |
| |
| return parser.parse_args() |
| |
| |
| def main(): |
| """Program entry point.""" |
| try: |
| args = parse_args() |
| except argparse.ArgumentError as ex: |
| warn_unnecessary("arm64", "21", get_host_tag_or_die()) |
| sys.exit(ex) |
| |
| if args.verbose is None: |
| logging.basicConfig(level=logging.WARNING) |
| elif args.verbose == 1: |
| logging.basicConfig(level=logging.INFO) |
| elif args.verbose >= 2: |
| logging.basicConfig(level=logging.DEBUG) |
| |
| host_tag = get_host_tag_or_die() |
| |
| warn_unnecessary(args.arch, args.api, host_tag) |
| |
| check_ndk_or_die() |
| |
| min_api = get_min_supported_api_level(args.arch) |
| api = args.api |
| if api is None: |
| logger().warning( |
| "Defaulting to target API %d (minimum supported target for %s)", |
| min_api, |
| args.arch, |
| ) |
| api = min_api |
| elif api < min_api: |
| sys.exit( |
| "{} is less than minimum platform for {} ({})".format( |
| api, args.arch, min_api |
| ) |
| ) |
| |
| triple = get_triple(args.arch) |
| toolchain_path = get_toolchain_path_or_die(host_tag) |
| |
| if args.install_dir is not None: |
| install_path = args.install_dir |
| if os.path.exists(install_path): |
| if args.force: |
| logger().info("Cleaning installation directory %s", install_path) |
| shutil.rmtree(install_path) |
| else: |
| sys.exit("Installation directory already exists. Use --force.") |
| else: |
| tempdir = tempfile.mkdtemp() |
| atexit.register(shutil.rmtree, tempdir) |
| install_path = os.path.join(tempdir, triple) |
| |
| create_toolchain(install_path, args.arch, api, toolchain_path, host_tag) |
| |
| if args.install_dir is None: |
| if host_tag == "windows-x86_64": |
| package_format = "zip" |
| else: |
| package_format = "bztar" |
| |
| package_basename = os.path.join(args.package_dir, triple) |
| shutil.make_archive( |
| package_basename, |
| package_format, |
| root_dir=os.path.dirname(install_path), |
| base_dir=os.path.basename(install_path), |
| ) |
| |
| |
| if __name__ == "__main__": |
| main() |