blob: 22951ae3c240ffe48e89822d88f809b80377973f [file] [log] [blame]
#!/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()