#!/usr/bin/env python3 import os import os.path import sys from os.path import join as path_join from options import * from os_utils import * import runtime runtime_targets = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'] cross_targets = ['cross-arm', 'cross-arm64', 'cross-x86', 'cross-x86_64'] cross_mxe_targets = ['cross-arm-win', 'cross-arm64-win', 'cross-x86-win', 'cross-x86_64-win'] def is_cross(target) -> bool: return target in cross_targets or is_cross_mxe(target) def is_cross_mxe(target) -> bool: return target in cross_mxe_targets def android_autodetect_cmake(opts: AndroidOpts) -> str: from distutils.version import LooseVersion from os import listdir sdk_cmake_basedir = path_join(opts.android_sdk_root, 'cmake') versions = [] for entry in listdir(sdk_cmake_basedir): if os.path.isdir(path_join(sdk_cmake_basedir, entry)): try: version = LooseVersion(entry) versions += [version] except ValueError: continue # Not a version folder if len(versions) == 0: raise BuildError('Cannot auto-detect Android CMake version') lattest_version = str(sorted(versions)[-1]) print('Auto-detected Android CMake version: ' + lattest_version) return lattest_version def get_api_version_or_min(opts: AndroidOpts, target: str) -> str: min_versions = { 'arm64-v8a': '21', 'x86_64': '21' } if target in min_versions and int(opts.android_api_version) < int(min_versions[target]): print('WARNING: %s is less than minimum platform for %s; using %s' % (opts.android_api_version, target, min_versions[target])) return min_versions[target] return opts.android_api_version def get_android_cmake_version(opts: AndroidOpts) -> str: return opts.android_cmake_version if opts.android_cmake_version != 'autodetect' else android_autodetect_cmake(opts) class AndroidTargetTable: archs = { 'armeabi-v7a': 'arm', 'arm64-v8a': 'arm64', 'x86': 'x86', 'x86_64': 'x86_64' } abi_names = { 'armeabi-v7a': 'arm-linux-androideabi', 'arm64-v8a': 'aarch64-linux-android', 'x86': 'i686-linux-android', 'x86_64': 'x86_64-linux-android' } host_triples = { 'armeabi-v7a': 'armv5-linux-androideabi', 'arm64-v8a': 'aarch64-linux-android', 'x86': 'i686-linux-android', 'x86_64': 'x86_64-linux-android' } def setup_android_target_template(env: dict, opts: AndroidOpts, target: str): extra_target_envs = { 'armeabi-v7a': { 'android-armeabi-v7a_CFLAGS': ['-D__POSIX_VISIBLE=201002', '-DSK_RELEASE', '-DNDEBUG', '-UDEBUG', '-fpic', '-march=armv7-a', '-mtune=cortex-a8', '-mfpu=vfp', '-mfloat-abi=softfp'], 'android-armeabi-v7a_CXXFLAGS': ['-D__POSIX_VISIBLE=201002', '-DSK_RELEASE', '-DNDEBUG', '-UDEBUG', '-fpic', '-march=armv7-a', '-mtune=cortex-a8', '-mfpu=vfp', '-mfloat-abi=softfp'], 'android-armeabi-v7a_LDFLAGS': ['-Wl,--fix-cortex-a8'] }, 'arm64-v8a': { 'android-arm64-v8a_CFLAGS': ['-D__POSIX_VISIBLE=201002', '-DSK_RELEASE', '-DNDEBUG', '-UDEBUG', '-fpic', '-DL_cuserid=9', '-DANDROID64'], 'android-arm64-v8a_CXXFLAGS': ['-D__POSIX_VISIBLE=201002', '-DSK_RELEASE', '-DNDEBUG', '-UDEBUG', '-fpic', '-DL_cuserid=9', '-DANDROID64'] }, 'x86': {}, 'x86_64': { 'android-x86_64_CFLAGS': ['-DL_cuserid=9'], 'android-x86_64_CXXFLAGS': ['-DL_cuserid=9'] } } if target in extra_target_envs: env.update(extra_target_envs[target]) android_new_ndk = True with open(path_join(opts.android_ndk_root, 'source.properties')) as file: for line in file: line = line.strip() if line.startswith('Pkg.Revision ') or line.startswith('Pkg.Revision='): pkg_revision = line.split('=')[1].strip() mayor = int(pkg_revision.split('.')[0]) android_new_ndk = mayor >= 18 break arch = AndroidTargetTable.archs[target] abi_name = AndroidTargetTable.abi_names[target] host_triple = AndroidTargetTable.host_triples[target] api = env['ANDROID_API_VERSION'] toolchain_path = path_join(opts.android_toolchains_prefix, opts.toolchain_name_fmt % (target, api)) tools_path = path_join(toolchain_path, 'bin') name_fmt = abi_name + '-%s' sdk_cmake_dir = path_join(opts.android_sdk_root, 'cmake', get_android_cmake_version(opts)) if not os.path.isdir(sdk_cmake_dir): print('Android CMake directory \'%s\' not found' % sdk_cmake_dir) AR = path_join(tools_path, name_fmt % 'ar') AS = path_join(tools_path, name_fmt % 'as') CC = path_join(tools_path, name_fmt % 'clang') CXX = path_join(tools_path, name_fmt % 'clang++') DLLTOOL = '' LD = path_join(tools_path, name_fmt % 'ld') OBJDUMP = path_join(tools_path, name_fmt % 'objdump') RANLIB = path_join(tools_path, name_fmt % 'ranlib') CMAKE = path_join(sdk_cmake_dir, 'bin', 'cmake') STRIP = path_join(tools_path, name_fmt % 'strip') CPP = path_join(tools_path, name_fmt % 'cpp') if not os.path.isfile(CPP): CPP = path_join(tools_path, (name_fmt % 'clang')) CPP += ' -E' CXXCPP = path_join(tools_path, name_fmt % 'cpp') if not os.path.isfile(CXXCPP): CXXCPP = path_join(tools_path, (name_fmt % 'clang++')) CXXCPP += ' -E' ccache_path = os.environ.get('CCACHE', '') if ccache_path: CC = '%s %s' % (ccache_path, CC) CXX = '%s %s' % (ccache_path, CXX) CPP = '%s %s' % (ccache_path, CPP) CXXCPP = '%s %s' % (ccache_path, CXXCPP) AC_VARS = [ 'mono_cv_uscore=yes', 'ac_cv_func_sched_getaffinity=no', 'ac_cv_func_sched_setaffinity=no', 'ac_cv_func_shm_open_working_with_mmap=no' ] CFLAGS, CXXFLAGS, CPPFLAGS, CXXCPPFLAGS, LDFLAGS = [], [], [], [], [] # On Android we use 'getApplicationInfo().nativeLibraryDir' as the libdir where Mono will look for shared objects. # This path looks something like this: '/data/app-lib/{package_name-{num}}'. However, Mono does some relocation # and the actual path it will look at will be '/data/app-lib/{package_name}-{num}/../lib', which doesn't exist. # Cannot use '/data/data/{package_name}/lib' either, as '/data/data/{package_name}/lib/../lib' may result in # permission denied. Therefore we just override 'MONO_RELOC_LIBDIR' here to avoid the relocation. CPPFLAGS += ['-DMONO_RELOC_LIBDIR=\\\".\\\"'] CFLAGS += [ '-fstack-protector', '-DMONODROID=1' ] CFLAGS += ['-D__ANDROID_API__=' + api] if android_new_ndk else [] CXXFLAGS += [ '-fstack-protector', '-DMONODROID=1' ] CXXFLAGS += ['-D__ANDROID_API__=' + api] if android_new_ndk else [] CPPFLAGS += ['-I%s/sysroot/usr/include' % toolchain_path] CXXCPPFLAGS += ['-I%s/sysroot/usr/include' % toolchain_path] path_link = '%s/platforms/android-%s/arch-%s/usr/lib' % (opts.android_ndk_root, api, arch) LDFLAGS += [ '-z', 'now', '-z', 'relro', '-z', 'noexecstack', '-ldl', '-lm', '-llog', '-lc', '-lgcc', '-Wl,-rpath-link=%s,-dynamic-linker=/system/bin/linker' % path_link, '-L' + path_link ] # Fixes this error: DllImport unable to load library 'dlopen failed: empty/missing DT_HASH in "libmono-native.so" (built with --hash-style=gnu?)'. LDFLAGS += ['-Wl,--hash-style=both'] CONFIGURE_FLAGS = [ '--disable-boehm', '--disable-executables', '--disable-iconv', '--disable-mcs-build', '--disable-nls', '--enable-dynamic-btls', '--enable-maintainer-mode', '--enable-minimal=ssa,portability,attach,verifier,full_messages,sgen_remset' ',sgen_marksweep_par,sgen_marksweep_fixed,sgen_marksweep_fixed_par' ',sgen_copying,logging,security,shared_handles,interpreter', '--with-btls-android-ndk=%s' % opts.android_ndk_root, '--with-btls-android-api=%s' % api, ] CONFIGURE_FLAGS += ['--enable-monodroid'] CONFIGURE_FLAGS += ['--with-btls-android-ndk-asm-workaround'] if android_new_ndk else [] CONFIGURE_FLAGS += [ '--with-btls-android-cmake-toolchain=%s/build/cmake/android.toolchain.cmake' % opts.android_ndk_root, '--with-sigaltstack=yes', '--with-tls=pthread', '--without-ikvm-native', '--disable-cooperative-suspend', '--disable-hybrid-suspend', '--disable-crash-reporting' ] env['_android-%s_AR' % target] = AR env['_android-%s_AS' % target] = AS env['_android-%s_CC' % target] = CC env['_android-%s_CXX' % target] = CXX env['_android-%s_CPP' % target] = CPP env['_android-%s_CXXCPP' % target] = CXXCPP env['_android-%s_DLLTOOL' % target] = DLLTOOL env['_android-%s_LD' % target] = LD env['_android-%s_OBJDUMP' % target] = OBJDUMP env['_android-%s_RANLIB' % target] = RANLIB env['_android-%s_CMAKE' % target] = CMAKE env['_android-%s_STRIP' % target] = STRIP env['_android-%s_AC_VARS' % target] = AC_VARS env['_android-%s_CFLAGS' % target] = CFLAGS env['_android-%s_CXXFLAGS' % target] = CXXFLAGS env['_android-%s_CPPFLAGS' % target] = CPPFLAGS env['_android-%s_CXXCPPFLAGS' % target] = CXXCPPFLAGS env['_android-%s_LDFLAGS' % target] = LDFLAGS env['_android-%s_CONFIGURE_FLAGS' % target] = CONFIGURE_FLAGS # Runtime template runtime.setup_runtime_template(env, opts, 'android', target, host_triple) class AndroidCrossTable: target_archs = { 'cross-arm': 'armv7', 'cross-arm64': 'aarch64-v8a', 'cross-x86': 'i686', 'cross-x86_64': 'x86_64' } device_targets = { 'cross-arm': 'armeabi-v7a', 'cross-arm64': 'arm64-v8a', 'cross-x86': 'x86', 'cross-x86_64': 'x86_64' } offsets_dumper_abis = { 'cross-arm': 'armv7-none-linux-androideabi', 'cross-arm64': 'aarch64-v8a-linux-android', 'cross-x86': 'i686-none-linux-android', 'cross-x86_64': 'x86_64-none-linux-android' } def get_android_libclang_path(opts): if sys.platform == 'darwin': return '%s/toolchains/llvm/prebuilt/darwin-x86_64/lib64/libclang.dylib' % opts.android_ndk_root elif sys.platform in ['linux', 'linux2']: loc = '%s/toolchains/llvm/prebuilt/linux-x86_64/lib64/libclang.so.9svn' % opts.android_ndk_root if os.path.isfile(loc): return loc return '%s/toolchains/llvm/prebuilt/linux-x86_64/lib64/libclang.so.8svn' % opts.android_ndk_root assert False def setup_android_cross_template(env: dict, opts: AndroidOpts, target: str, host_arch: str): def get_host_triple(): if sys.platform == 'darwin': return '%s-apple-darwin11' % host_arch elif sys.platform in ['linux', 'linux2']: return '%s-linux-gnu' % host_arch assert False target_arch = AndroidCrossTable.target_archs[target] device_target = AndroidCrossTable.device_targets[target] offsets_dumper_abi = AndroidCrossTable.offsets_dumper_abis[target] host_triple = get_host_triple() target_triple = '%s-linux-android' % target_arch android_libclang = get_android_libclang_path(opts) env['_android-%s_OFFSETS_DUMPER_ARGS' % target] = [ '--libclang=%s' % android_libclang, '--sysroot=%s/sysroot' % opts.android_ndk_root ] env['_android-%s_AR' % target] = 'ar' env['_android-%s_AS' % target] = 'as' env['_android-%s_CC' % target] = 'cc' env['_android-%s_CXX' % target] = 'c++' env['_android-%s_CXXCPP' % target] = 'cpp' env['_android-%s_LD' % target] = 'ld' env['_android-%s_RANLIB' % target] = 'ranlib' env['_android-%s_STRIP' % target] = 'strip' is_darwin = sys.platform == 'darwin' CFLAGS = [] CFLAGS += ['-DDEBUG_CROSS'] if not opts.release else [] CFLAGS += ['-mmacosx-version-min=10.9'] if is_darwin else [] env['_android-%s_CFLAGS' % target] = CFLAGS CXXFLAGS = [] CXXFLAGS += ['-DDEBUG_CROSS'] if not opts.release else [] CXXFLAGS += ['-mmacosx-version-min=10.9', '-stdlib=libc++'] if is_darwin else [] env['_android-%s_CXXFLAGS' % target] = CXXFLAGS env['_android-%s_CONFIGURE_FLAGS' % target] = [ '--disable-boehm', '--disable-mcs-build', '--disable-nls', '--enable-maintainer-mode', '--with-tls=pthread' ] # Runtime cross template runtime.setup_runtime_cross_template(env, opts, 'android', target, host_triple, target_triple, device_target, 'llvm64', offsets_dumper_abi) def setup_android_cross_mxe_template(env: dict, opts: AndroidOpts, target: str, host_arch: str): table_target = cross_targets[cross_mxe_targets.index(target)] # Re-use the non-mxe table target_arch = AndroidCrossTable.target_archs[table_target] device_target = AndroidCrossTable.device_targets[table_target] offsets_dumper_abi = AndroidCrossTable.offsets_dumper_abis[table_target] host_triple = '%s-w64-mingw32' % host_arch target_triple = '%s-linux-android' % target_arch android_libclang = get_android_libclang_path(opts) env['_android-%s_OFFSETS_DUMPER_ARGS' % target] = [ '--libclang=%s' % android_libclang, '--sysroot=%s/sysroot' % opts.android_ndk_root ] mxe_bin = path_join(opts.mxe_prefix, 'bin') env['_android-%s_PATH' % target] = mxe_bin name_fmt = host_arch + '-w64-mingw32-%s' env['_android-%s_AR' % target] = path_join(mxe_bin, name_fmt % 'ar') env['_android-%s_AS' % target] = path_join(mxe_bin, name_fmt % 'as') env['_android-%s_CC' % target] = path_join(mxe_bin, name_fmt % 'gcc') env['_android-%s_CXX' % target] = path_join(mxe_bin, name_fmt % 'g++') env['_android-%s_DLLTOOL' % target] = path_join(mxe_bin, name_fmt % 'dlltool') env['_android-%s_LD' % target] = path_join(mxe_bin, name_fmt % 'ld') env['_android-%s_OBJDUMP' % target] = path_join(mxe_bin, name_fmt % 'objdump') env['_android-%s_RANLIB' % target] = path_join(mxe_bin, name_fmt % 'ranlib') env['_android-%s_STRIP' % target] = path_join(mxe_bin, name_fmt % 'strip') mingw_zlib_prefix = '%s/opt/mingw-zlib/usr' % opts.mxe_prefix if not os.path.isdir(mingw_zlib_prefix): mingw_zlib_prefix = opts.mxe_prefix CFLAGS = [] CFLAGS += ['-DDEBUG_CROSS'] if not opts.release else [] CFLAGS += ['-I%s/%s-w64-mingw32/include' % (mingw_zlib_prefix, host_arch)] env['_android-%s_CFLAGS' % target] = CFLAGS CXXFLAGS = [] CXXFLAGS += ['-DDEBUG_CROSS'] if not opts.release else [] CXXFLAGS += ['-I%s/%s-w64-mingw32/include' % (mingw_zlib_prefix, host_arch)] env['_android-%s_CXXFLAGS' % target] = CXXFLAGS env['_android-%s_LDFLAGS' % target] = [] CONFIGURE_FLAGS = [ '--disable-boehm', '--disable-mcs-build', '--disable-nls', '--enable-static-gcc-libs', '--enable-maintainer-mode', '--with-tls=pthread' ] CONFIGURE_FLAGS += ['--with-static-zlib=%s/%s-w64-mingw32/lib/libz.a' % (mingw_zlib_prefix, host_arch)] env['_android-%s_CONFIGURE_FLAGS' % target] = CONFIGURE_FLAGS # Runtime cross template runtime.setup_runtime_cross_template(env, opts, 'android', target, host_triple, target_triple, device_target, 'llvmwin64', offsets_dumper_abi) def make_standalone_toolchain(opts: AndroidOpts, target: str, api: str): install_dir = path_join(opts.android_toolchains_prefix, opts.toolchain_name_fmt % (target, api)) if os.path.isdir(path_join(install_dir, 'bin')): return # Looks like it's already there, so no need to re-create it command = path_join(opts.android_ndk_root, 'build', 'tools', 'make_standalone_toolchain.py') args = ['--verbose', '--force', '--api=' + api, '--arch=' + AndroidTargetTable.archs[target], '--install-dir=' + install_dir] run_command(command, args=args, name='make_standalone_toolchain') def strip_libs(opts: AndroidOpts, product: str, target: str, api: str): toolchain_path = path_join(opts.android_toolchains_prefix, opts.toolchain_name_fmt % (target, api)) tools_path = path_join(toolchain_path, 'bin') name_fmt = AndroidTargetTable.abi_names[target] + '-%s' strip = path_join(tools_path, name_fmt % 'strip') install_dir = path_join(opts.install_dir, '%s-%s-%s' % (product, target, opts.configuration)) out_libs_dir = path_join(install_dir, 'lib') lib_files = globs(('*.a', '*.so'), dirpath=out_libs_dir) if len(lib_files): run_command(strip, args=['--strip-unneeded'] + lib_files, name='strip') def configure(opts: AndroidOpts, product: str, target: str): env = { 'ANDROID_API_VERSION': get_api_version_or_min(opts, target) } if is_cross(target): import llvm if is_cross_mxe(target): llvm.make(opts, 'llvmwin64') setup_android_cross_mxe_template(env, opts, target, host_arch='x86_64') else: llvm.make(opts, 'llvm64') setup_android_cross_template(env, opts, target, host_arch='x86_64') else: make_standalone_toolchain(opts, target, env['ANDROID_API_VERSION']) setup_android_target_template(env, opts, target) if not os.path.isfile(path_join(opts.mono_source_root, 'configure')): runtime.run_autogen(opts) runtime.run_configure(env, opts, product, target) def make(opts: AndroidOpts, product: str, target: str): env = { 'ANDROID_API_VERSION': get_api_version_or_min(opts, target) } build_dir = path_join(opts.configure_dir, '%s-%s-%s' % (product, target, opts.configuration)) make_args = make_default_args(opts) make_args += ['-C', build_dir] run_command('make', args=make_args, name='make') run_command('make', args=['-C', '%s/mono' % build_dir, 'install'], name='make install mono') run_command('make', args=['-C', '%s/support' % build_dir, 'install'], name='make install support') run_command('make', args=['-C', '%s/data' % build_dir, 'install'], name='make install data') if opts.strip_libs and not is_cross(target): strip_libs(opts, product, target, env['ANDROID_API_VERSION']) def clean(opts: AndroidOpts, product: str, target: str): rm_rf( path_join(opts.configure_dir, '%s-%s-%s' % (product, target, opts.configuration)), path_join(opts.configure_dir, '%s-%s-%s.config.cache' % (product, target, opts.configuration)), path_join(opts.install_dir, '%s-%s-%s' % (product, target, opts.configuration)) ) def main(raw_args): import cmd_utils from cmd_utils import custom_bool from collections import OrderedDict from typing import Callable target_shortcuts = { 'all-runtime': runtime_targets, 'all-cross': cross_targets, 'all-cross-win': cross_mxe_targets } target_values = runtime_targets + cross_targets + cross_mxe_targets + list(target_shortcuts) actions = OrderedDict() actions['configure'] = configure actions['make'] = make actions['clean'] = clean parser = cmd_utils.build_arg_parser( description='Builds the Mono runtime for Android', env_vars={ 'ANDROID_SDK_ROOT': 'Overrides default value for --android-sdk', 'ANDROID_NDK_ROOT': 'Overrides default value for --android-ndk', 'ANDROID_HOME': 'Same as ANDROID_SDK_ROOT' } ) home = os.environ.get('HOME') android_sdk_default = os.environ.get('ANDROID_HOME', os.environ.get('ANDROID_SDK_ROOT', path_join(home, 'Android/Sdk'))) android_ndk_default = os.environ.get('ANDROID_NDK_ROOT', path_join(android_sdk_default, 'ndk-bundle')) default_help = 'default: %(default)s' parser.add_argument('action', choices=['configure', 'make', 'clean']) parser.add_argument('--target', choices=target_values, action='append', required=True) parser.add_argument('--toolchains-prefix', default=path_join(home, 'android-toolchains'), help=default_help) parser.add_argument('--android-sdk', default=android_sdk_default, help=default_help) parser.add_argument('--android-ndk', default=android_ndk_default, help=default_help) parser.add_argument('--android-api-version', default='21', help=default_help) parser.add_argument('--android-cmake-version', default='autodetect', help=default_help) cmd_utils.add_runtime_arguments(parser, default_help) args = parser.parse_args(raw_args) input_action = args.action input_targets = args.target opts = android_opts_from_args(args) if not os.path.isdir(opts.mono_source_root): print('Mono sources directory not found: ' + opts.mono_source_root) sys.exit(1) targets = cmd_utils.expand_input_targets(input_targets, target_shortcuts) action = actions[input_action] try: for target in targets: action(opts, 'android', target) except BuildError as e: sys.exit(e.message) if __name__ == '__main__': from sys import argv main(argv[1:])