464 lines
15 KiB
Python
464 lines
15 KiB
Python
#!/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()
|