blob: f57ca4fc75cb4e1a3e9e05301105a2b2ad5b5dda [file] [log] [blame]
#!/usr/bin/env python
#
# 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.
The output of this tool is a more typical cross-compiling toolchain. It is
indended to be used with existing build systems such as autotools.
"""
import argparse
import atexit
from distutils.dir_util import copy_tree
import inspect
import logging
import os
import shutil
import stat
import sys
import tempfile
import textwrap
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',
'x86': 'i686-linux-android',
'x86_64': 'x86_64-linux-android',
}[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:
major, minor, _build = version_file.read().strip().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("""\
#!/bin/bash
if [ "$1" != "-cc1" ]; then
`dirname $0`/clang{version} {flags} "$@"
else
# target/triple already spelled out.
`dirname $0`/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("""\
#!/bin/bash
if [ "$1" != "-cc1" ]; then
`dirname $0`/clang{version}++ {flags} "$@"
else
# target/triple already spelled out.
`dirname $0`/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 create_toolchain(install_path, arch, api, toolchain_path, host_tag):
"""Create a standalone toolchain."""
copy_tree(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)
copy_tree(prebuilt_path, install_path)
gdbserver_path = os.path.join(
NDK_DIR, 'prebuilt', 'android-' + arch, 'gdbserver')
gdbserver_install = os.path.join(install_path, 'share', 'gdbserver')
shutil.copytree(gdbserver_path, gdbserver_install)
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("""\
make_standalone_toolchain.py is no longer necessary. 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
""".format(
toolchain_dir=toolchain_dir,
prompt=prompt,
standalone_toolchain=standalone_toolchain,
arch=arch,
api=api,
old_clang=old_clang,
new_clang=new_clang)))
def parse_args():
"""Parse command line arguments from sys.argv."""
parser = argparse.ArgumentParser(
description=inspect.getdoc(sys.modules[__name__]))
parser.add_argument(
'--arch', required=True,
choices=('arm', 'arm64', '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."""
args = parse_args()
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()
lp32 = args.arch in ('arm', 'x86')
min_api = 16 if lp32 else 21
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()