From 09fbbb7d6101686c69e31c1c313a9884a7c16522 Mon Sep 17 00:00:00 2001 From: Ignacio Etcheverry Date: Sun, 10 Nov 2019 04:58:04 +0100 Subject: [PATCH] Add build scripts for WASM, AOT cross-compilers, BCL and desktop --- README.md | 99 ++++- __init__.py | 0 android.py | 546 +++++++++++++++++++++++++ bcl.py | 151 +++++++ build_mono_android.py | 597 ---------------------------- cmd_utils.py | 80 ++++ desktop.py | 188 +++++++++ llvm.py | 118 ++++++ options.py | 79 ++++ os_utils.py | 137 +++++++ patch_emscripten.py | 37 ++ patch_mono.py | 39 ++ patches/fix-mono-android-tkill.diff | 70 ++++ reference_assemblies.py | 62 +++ runtime.py | 159 ++++++++ wasm.py | 231 +++++++++++ 16 files changed, 1984 insertions(+), 609 deletions(-) create mode 100644 __init__.py create mode 100755 android.py create mode 100755 bcl.py delete mode 100755 build_mono_android.py create mode 100644 cmd_utils.py create mode 100755 desktop.py create mode 100755 llvm.py create mode 100644 options.py create mode 100644 os_utils.py create mode 100755 patch_emscripten.py create mode 100755 patch_mono.py create mode 100644 patches/fix-mono-android-tkill.diff create mode 100755 reference_assemblies.py create mode 100644 runtime.py create mode 100755 wasm.py diff --git a/README.md b/README.md index 05259ba..e1bc4a5 100644 --- a/README.md +++ b/README.md @@ -1,26 +1,101 @@ # Mono build scripts for Godot This repository contains scripts for building the Mono runtime to use with Godot Engine -## Android instructions +## Command-line options -Run `python build_mono_android.py --help` for the full list of command line options. -You may need to tweak some of those if the default values do not fit your needs. +**Requires Python 3.7 or higher** -Example: +These scripts are based on the Mono [sdks](https://github.com/mono/mono/tree/master/sdks) makefiles, with some changes to work well with Godot. Some platforms or targets depend on files from the `sdks` directory in the Mono source repository. This directory may be missing from tarballs. If that's the case, cloning the git repository may be needed. [This table](https://www.mono-project.com/docs/about-mono/versioning/#mono-source-versioning) can be used to determine the branch for a specific version of Mono. + +Run `python SCRIPT.py --help` for the full list of command line options. + +By default, the scripts will install the resulting files to `$HOME/mono-installs`. +A custom output directory can be specified with the `--install-dir` option. + +When cross-compiling to Windows, `--mxe-prefix` must be specified. For example, with the `mingw-w64` package installed on Ubuntu, one can pass `--mxe-prefix=/usr`. + +A path to the Mono source tree must be provided with the `--mono-sources` option or with the `MONO_SOURCE_ROOT` environment variable: ```bash -# These are the default values. You can omit them if they apply to your system +export MONO_SOURCE_ROOT=$HOME/git/mono +``` + +### Notes +- Python 3.7 or higher is required. +- Cross-compiling for macOS via osxcross is not yet supported. +- Building on Windows is not supported. It's possible to use Cygwin or WSL (Windows Subsystem for Linux) but this hasn't been tested. + +## Desktop + +```bash +# Build the runtimes for 32-bit and 64-bit Linux. +./desktop.py linux configure --target=i686 --target=x86_64 +./desktop.py linux make --target=i686 --target=x86_64 + +# Build the runtimes for 32-bit and 64-bit Windows. +./desktop.py windows configure --target=i686 --target=x86_64 --mxe-prefix=/usr +./desktop.py windows make --target=i686 --target=x86_64 --mxe-prefix=/usr + +# Build the runtime for 64-bit macOS. +./desktop.py osx configure --target=x86_64 +./desktop.py osx make --target=x86_64 +``` + +_AOT cross-compilers for desktop platforms cannot be built with this script yet._ + +## Android + +Some patches may need to be applied to the Mono sources before building for Android. This can be done by running `./patch_mono.py`. + +```bash +# These are the default values. This step can be omitted if SDK and NDK root are in this location. export ANDROID_SDK_ROOT=$HOME/Android/Sdk export ANDROID_NDK_ROOT=$ANDROID_SDK_ROOT/ndk-bundle -# The mono sources may be in a different location on your system -export MONO_SOURCE_ROOT=$HOME/git/mono +# Build the runtime for all supported Android ABIs. +./android.py configure --target=all-runtime +./android.py make --target=all-runtime -./build_mono_android.py configure --target=all -./build_mono_android.py make --target=all +# Build the AOT cross-compilers targeting all supported Android ABIs. +./android.py configure --target=all-cross +./android.py make --target=all-cross + +# Build the AOT cross-compilers for Windows targeting all supported Android ABIs. +./android.py configure --target=all-cross-win --mxe-prefix=/usr +./android.py make --target=all-cross-win --mxe-prefix=/usr ``` -The option `--target=all` is a shortcut for `--target=armeabi-v7a --target=x86 --target=arm64-v8a --target=x86_64`. +The option `--target=all-runtime` is a shortcut for `--target=armeabi-v7a --target=x86 --target=arm64-v8a --target=x86_64`. The equivalent applies for `all-cross` and `all-cross-win`. -By default, the script will install the resulting files to `$HOME/mono-installs`. -You can specify a custom output directory with the `--install-dir` option. +## WebAssembly + +Just like with Godot, an active Emscripten SDK is needed for building the Mono WebAssembly runtime. + +Some patches may need to be applied to the Emscripten SDK before building Mono. This can be done by running `./patch_emscripten.py`. + +```bash +# Build the runtime for WebAssembly. +./wasm.py configure --target=runtime +./wasm.py make --target=runtime +``` + +_AOT cross-compilers for WebAssembly cannot be built with this script yet._ + +## Base Class library + +```bash +# Build the Desktop BCL. +./bcl.py make --product=desktop + +# Build the Android BCL. +./bcl.py make --product=android + +# Build the WebAssembly BCL. +./bcl.py make --product=wasm +``` + +## Reference Assemblies + +```bash +./reference_assemblies.py install +``` diff --git a/__init__.py b/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/android.py b/android.py new file mode 100755 index 0000000..2c9e428 --- /dev/null +++ b/android.py @@ -0,0 +1,546 @@ +#!/usr/bin/python + +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'] + CFLAGS += ['-DMONODROID=1'] if opts.with_monodroid else [] + CFLAGS += ['-D__ANDROID_API__=' + api] if android_new_ndk else [] + CXXFLAGS += ['-fstack-protector'] + CXXFLAGS += ['-DMONODROID=1'] if opts.with_monodroid else [] + 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'] if opts.with_monodroid else [] + 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']: + 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-darwin10' % 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, 'llvm-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, 'llvm-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 = ['-C', build_dir] + make_args += ['V=1'] if opts.verbose_make else [] + + 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') + + 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, 'toolchains', '%s-%s' % (product, target)), + 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='18', help=default_help) + parser.add_argument('--android-cmake-version', default='autodetect', help=default_help) + parser.add_argument('--with-monodroid', type=custom_bool, default=True, 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:]) diff --git a/bcl.py b/bcl.py new file mode 100755 index 0000000..d1d8eac --- /dev/null +++ b/bcl.py @@ -0,0 +1,151 @@ +#!/usr/bin/python +#!/usr/bin/python + +import os +import os.path + +from os.path import join as path_join +from options import * +from os_utils import * + + +product_values = ['desktop', 'android', 'wasm'] +profiles_table = { + 'desktop': ['net_4_x'], + 'android': ['monodroid', 'monodroid_tools'], + 'wasm': ['wasm', 'wasm_tools'] +} +test_profiles_table = { + 'desktop': [], + 'android': ['monodroid', 'monodroid_tools'], + 'wasm': ['wasm'] +} + + +def configure_bcl(opts: BclOpts): + stamp_file = path_join(opts.configure_dir, '.stamp-bcl-configure') + + if os.path.isfile(stamp_file): + return + + build_dir = path_join(opts.configure_dir, 'bcl') + mkdir_p(build_dir) + + CONFIGURE_FLAGS = [ + '--disable-boehm', + '--disable-btls-lib', + '--disable-nls', + '--disable-support-build', + '--with-mcs-docs=no' + ] + + configure = path_join(opts.mono_source_root, 'configure') + configure_args = CONFIGURE_FLAGS + + run_command(configure, args=configure_args, cwd=build_dir, name='configure bcl') + + touch(stamp_file) + + +def make_bcl(opts: BclOpts): + stamp_file = path_join(opts.configure_dir, '.stamp-bcl-make') + + if os.path.isfile(stamp_file): + return + + build_dir = path_join(opts.configure_dir, 'bcl') + + make_args = ['-C', build_dir, '-C', 'mono'] + make_args += ['V=1'] if opts.verbose_make else [] + + run_command('make', args=make_args, name='make bcl') + + touch(stamp_file) + + +def build_bcl(opts: BclOpts): + configure_bcl(opts) + make_bcl(opts) + + +def clean_bcl(opts: BclOpts): + configure_stamp_file = path_join(opts.configure_dir, '.stamp-bcl-configure') + make_stamp_file = path_join(opts.configure_dir, '.stamp-bcl-make') + build_dir = path_join(opts.configure_dir, 'bcl') + rm_rf(configure_stamp_file, make_stamp_file, build_dir) + + +def make_product(opts: BclOpts, product: str): + build_bcl(opts) + + build_dir = path_join(opts.configure_dir, 'bcl') + + profiles = profiles_table[product] + test_profiles = test_profiles_table[product] + + install_dir = path_join(opts.install_dir, '%s-bcl' % product) + + mkdir_p(install_dir) + + for profile in profiles: + mkdir_p('%s/%s' % (install_dir, profile)) + + make_args = ['-C', build_dir, '-C', 'runtime', 'all-mcs', 'build_profiles=%s' % ' '.join(profiles)] + make_args += ['V=1'] if opts.verbose_make else [] + run_command('make', args=make_args, name='make profiles') + + if opts.tests and len(test_profiles) > 0: + test_make_args = ['-C', build_dir, '-C', 'runtime', 'test', 'xunit-test', 'test_profiles=%s' % ' '.join(test_profiles)] + test_make_args += ['V=1'] if opts.verbose_make else [] + run_command('make', args=test_make_args, name='make tests') + + # Copy the bcl profiles to the output directory + from distutils.dir_util import copy_tree + for profile in profiles: + copy_tree('%s/mcs/class/lib/%s' % (opts.mono_source_root, profile), '%s/%s' % (install_dir, profile)) + + # Recursively remove hidden files we shoudln't have copied (e.g.: .stamp) + import glob + hidden_file_pattern = '%s/**/.*' % install_dir + for profile in profiles: + [rm_rf(x) for x in glob.iglob(hidden_file_pattern, recursive=True)] + + +def clean_product(opts: BclOpts, product: str): + clean_bcl(opts) + + install_dir = path_join(opts.install_dir, '%s-bcl' % product) + rm_rf(install_dir) + + +def main(raw_args): + import cmd_utils + + actions = { + 'make': make_product, + 'clean': clean_product + } + + parser = cmd_utils.build_arg_parser(description='Builds the Mono BCL') + + default_help = 'default: %(default)s' + + parser.add_argument('action', choices=actions.keys()) + parser.add_argument('--product', choices=product_values, action='append', required=True) + parser.add_argument('--tests', action='store_true', default=False, help=default_help) + + cmd_utils.add_base_arguments(parser, default_help) + + args = parser.parse_args(raw_args) + + opts = bcl_opts_from_args(args) + products = args.product + + for product in products: + action = actions[args.action] + action(opts, product) + + +if __name__ == '__main__': + from sys import argv + main(argv[1:]) diff --git a/build_mono_android.py b/build_mono_android.py deleted file mode 100755 index fd2aa6f..0000000 --- a/build_mono_android.py +++ /dev/null @@ -1,597 +0,0 @@ -#!/usr/bin/python - -from os import environ -from os.path import exists as path_exists, join as path_join, isfile, isdir, abspath -from sys import exit - - -class MonoBuildError(Exception): - '''Generic exception for custom build errors''' - def __init__(self, msg): - super(MonoBuildError, self).__init__(msg) - self.message = msg - - -def run_command(command, args=[], custom_env=None, name='command'): - def cmd_args_to_str(cmd_args): - return ' '.join([arg if not ' ' in arg else '"%s"' % arg for arg in cmd_args]) - - assert isinstance(command, str) and isinstance(args, list) - args = [command] + args - - import subprocess - try: - print('Running command \'%s\': %s' % (name, cmd_args_to_str(args))) - if custom_env is None: - subprocess.check_call(args) - else: - subprocess.check_call(args, env=custom_env) - print('Command \'%s\' completed successfully' % name) - except subprocess.CalledProcessError as e: - raise MonoBuildError('\'%s\' exited with error code: %s' % (name, e.returncode)) - - -# Creates the directory if no other file or directory with the same path exists -def mkdir_p(path): - from os import makedirs - if not path_exists(path): - print('creating directory: ' + path) - makedirs(path) - - -def chdir(path): - from os import chdir as os_chdir - print('entering directory: ' + path) - os_chdir(path) - - -# Remove files and/or directories recursively -def rm_rf(*paths): - from os import remove - from shutil import rmtree - for path in paths: - if isfile(path): - print('removing file: ' + path) - remove(path) - elif isdir(path): - print('removing directory and its contents: ' + path) - rmtree(path) - - -def globs(pathnames, dirpath='.'): - import glob - files = [] - for pathname in pathnames: - files.extend(glob.glob(path_join(dirpath, pathname))) - return files - - -TOOLCHAIN_NAME_FMT = '%s-api%s-clang' - -CONFIGURATION = None -RELEASE = None - -ANDROID_TOOLCHAINS_PREFIX = None - -ANDROID_SDK_ROOT = None -ANDROID_NDK_ROOT = None - -WITH_MONODROID = None -ENABLE_CXX = None -VERBOSE_MAKE = None -STRIP_LIBS = None - -CONFIGURE_DIR = None -INSTALL_DIR = None - -MONO_SOURCE_ROOT = None - -_ANDROID_API_VERSION = None -_ANDROID_CMAKE_VERSION = None - - -class AndroidTargetInfo: - 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 android_autodetect_cmake(): - from distutils.version import LooseVersion - from os import listdir - - sdk_cmake_basedir = path_join(ANDROID_SDK_ROOT, 'cmake') - versions = [] - - for entry in listdir(sdk_cmake_basedir): - if 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 MonoBuildError('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(target): - min_versions = { 'arm64-v8a': '21', 'x86_64': '21' } - if target in min_versions and int(_ANDROID_API_VERSION) < int(min_versions[target]): - print('WARNING: %s is less than minimum platform for %s; using %s' % (_ANDROID_API_VERSION, target, min_versions[target])) - return min_versions[target] - return _ANDROID_API_VERSION - - -def get_android_cmake_version(): - return _ANDROID_CMAKE_VERSION if _ANDROID_CMAKE_VERSION != 'autodetect' else android_autodetect_cmake() - - -def setup_runtime_template(env, product, target, host_triple): - BITNESS = '' - if any(s in host_triple for s in ['i686', 'i386']): - BITNESS = '-m32' - elif 'x86_64' in host_triple: - BITNESS = '-m64' - - CFLAGS = [] - CFLAGS += ['-O2', '-g'] if RELEASE else ['-O0', '-ggdb3', '-fno-omit-frame-pointer'] - CFLAGS += env.get('_%s-%s_CFLAGS' % (product, target), []) - CFLAGS += env.get('%s-%s_CFLAGS' % (product, target), []) - CFLAGS += [BITNESS] if BITNESS else [] - - CXXFLAGS = [] - CXXFLAGS += ['-O2', '-g'] if RELEASE else ['-O0', '-ggdb3', '-fno-omit-frame-pointer'] - CXXFLAGS += env.get('_%s-%s_CXXFLAGS' % (product, target), []) - CXXFLAGS += env.get('%s-%s_CXXFLAGS' % (product, target), []) - CXXFLAGS += [BITNESS] if BITNESS else [] - - CPPFLAGS = [] - CPPFLAGS += ['-O2', '-g'] if RELEASE else ['-O0', '-ggdb3', '-fno-omit-frame-pointer'] - CPPFLAGS += env.get('_%s-%s_CPPFLAGS' % (product, target), []) - CPPFLAGS += env.get('%s-%s_CPPFLAGS' % (product, target), []) - CPPFLAGS += [BITNESS] if BITNESS else [] - - CXXCPPFLAGS = [] - CXXCPPFLAGS += ['-O2', '-g'] if RELEASE else ['-O0', '-ggdb3', '-fno-omit-frame-pointer'] - CXXCPPFLAGS += env.get('_%s-%s_CXXCPPFLAGS' % (product, target), []) - CXXCPPFLAGS += env.get('%s-%s_CXXCPPFLAGS' % (product, target), []) - CXXCPPFLAGS += [BITNESS] if BITNESS else [] - - LDFLAGS = [] - LDFLAGS += env.get('_%s-%s_LDFLAGS' % (product, target), []) - LDFLAGS += env.get('%s-%s_LDFLAGS' % (product, target), []) - - AC_VARS = [] - AC_VARS += env.get('_%s-%s_AC_VARS' % (product, target), []) - AC_VARS += env.get('%s-%s_AC_VARS' % (product, target), []) - - CONFIGURE_ENVIRONMENT = {} - - def append_product_env_var(var_name): - val = env.get('_%s-%s_%s' % (product, target, var_name), '') - if val: - CONFIGURE_ENVIRONMENT[var_name] = val - - append_product_env_var('AR') - append_product_env_var('AS') - append_product_env_var('CC') - append_product_env_var('CPP') - append_product_env_var('CXX') - append_product_env_var('CXXCPP') - append_product_env_var('DLLTOOL') - append_product_env_var('LD') - append_product_env_var('OBJDUMP') - append_product_env_var('RANLIB') - append_product_env_var('CMAKE') - append_product_env_var('STRIP') - - CONFIGURE_ENVIRONMENT['CFLAGS'] = CFLAGS - CONFIGURE_ENVIRONMENT['CXXFLAGS'] = CXXFLAGS - CONFIGURE_ENVIRONMENT['CPPFLAGS'] = CPPFLAGS - CONFIGURE_ENVIRONMENT['CXXCPPFLAGS'] = CXXCPPFLAGS - CONFIGURE_ENVIRONMENT['LDFLAGS'] = LDFLAGS - - CONFIGURE_ENVIRONMENT.update(env.get('_%s-%s_CONFIGURE_ENVIRONMENT' % (product, target), {})) - CONFIGURE_ENVIRONMENT.update(env.get('%s-%s_CONFIGURE_ENVIRONMENT' % (product, target), {})) - - CONFIGURE_FLAGS = [] - CONFIGURE_FLAGS += ['--host=%s' % host_triple] if host_triple else [] - CONFIGURE_FLAGS += ['--cache-file=%s/%s-%s-%s.config.cache' % (CONFIGURE_DIR, product, target, CONFIGURATION)] - CONFIGURE_FLAGS += ['--prefix=%s/%s-%s-%s' % (INSTALL_DIR, product, target, CONFIGURATION)] - CONFIGURE_FLAGS += ['--enable-cxx'] if ENABLE_CXX else [] - # CONFIGURE_FLAGS += env['_cross-runtime_%s-%s_CONFIGURE_FLAGS' % (product, target)] - CONFIGURE_FLAGS += env.get('_%s-%s_CONFIGURE_FLAGS' % (product, target), []) - CONFIGURE_FLAGS += env.get('%s-%s_CONFIGURE_FLAGS' % (product, target), []) - - env['_runtime_%s-%s_AC_VARS' % (product, target)] = AC_VARS - env['_runtime_%s-%s_CONFIGURE_ENVIRONMENT' % (product, target)] = CONFIGURE_ENVIRONMENT - env['_runtime_%s-%s_CONFIGURE_FLAGS' % (product, target)] = CONFIGURE_FLAGS - - -def setup_android_target_template(env, target): - 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'] - } - } - - env.update(extra_target_envs[target]) - - android_new_ndk = True - - with open(path_join(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 = AndroidTargetInfo.archs[target] - abi_name = AndroidTargetInfo.abi_names[target] - host_triple = AndroidTargetInfo.host_triples[target] - api = env['ANDROID_API_VERSION'] - - toolchain_path = path_join(ANDROID_TOOLCHAINS_PREFIX, TOOLCHAIN_NAME_FMT % (target, api)) - - tools_path = path_join(toolchain_path, 'bin') - name_fmt = abi_name + '-%s' - - sdk_cmake_dir = path_join(ANDROID_SDK_ROOT, 'cmake', get_android_cmake_version()) - if not 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 isfile(CPP): - CPP = path_join(tools_path, (name_fmt % 'clang')) - CPP += ' -E' - - CXXCPP = path_join(tools_path, name_fmt % 'cpp') - if not isfile(CXXCPP): - CXXCPP = path_join(tools_path, (name_fmt % 'clang++')) - CXXCPP += ' -E' - - ccache_path = 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'] - CFLAGS += ['-DMONODROID=1'] if WITH_MONODROID else [] - CFLAGS += ['-D__ANDROID_API__=' + api] if android_new_ndk else [] - CXXFLAGS += ['-fstack-protector'] - CXXFLAGS += ['-DMONODROID=1'] if WITH_MONODROID else [] - 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' % (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' % ANDROID_NDK_ROOT, - '--with-btls-android-api=%s' % api, - ] - - CONFIGURE_FLAGS += ['--enable-monodroid'] if WITH_MONODROID else [] - 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' % 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 - - setup_runtime_template(env, 'android', target, host_triple) - - -def make_standalone_toolchain(target, api): - install_dir = path_join(ANDROID_TOOLCHAINS_PREFIX, TOOLCHAIN_NAME_FMT % (target, api)) - if isdir(path_join(install_dir, 'bin')): - return # Looks like it's already there, so no need to re-create it - command = path_join(ANDROID_NDK_ROOT, 'build', 'tools', 'make_standalone_toolchain.py') - args = ['--verbose', '--force', '--api=' + api, '--arch=' + AndroidTargetInfo.archs[target], - '--install-dir=' + install_dir] - run_command(command, args=args, name='make_standalone_toolchain') - - -def strip_libs(product, target, api): - toolchain_path = path_join(ANDROID_TOOLCHAINS_PREFIX, TOOLCHAIN_NAME_FMT % (target, api)) - - tools_path = path_join(toolchain_path, 'bin') - name_fmt = AndroidTargetInfo.abi_names[target] + '-%s' - - STRIP = path_join(tools_path, name_fmt % 'strip') - - install_dir = '%s/%s-%s-%s' % (INSTALL_DIR, product, target, 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(product, target): - env = { 'ANDROID_API_VERSION': get_api_version_or_min(target) } - - make_standalone_toolchain(target, env['ANDROID_API_VERSION']) - - setup_android_target_template(env, target) - - chdir(MONO_SOURCE_ROOT) - autogen_env = environ.copy() - autogen_env['NOCONFIGURE'] = '1' - run_command(path_join(MONO_SOURCE_ROOT, 'autogen.sh'), custom_env=autogen_env, name='autogen') - - build_dir = CONFIGURE_DIR + '/%s-%s-%s' % (product, target, CONFIGURATION) - mkdir_p(build_dir) - chdir(build_dir) - - def str_dict_val(val): - if isinstance(val, list): - return ' '.join(val) # No need for quotes - return val - - ac_vars = env['_runtime_%s-%s_AC_VARS' % (product, target)] - configure_env = env['_runtime_%s-%s_CONFIGURE_ENVIRONMENT' % (product, target)] - configure_env = [('%s=%s' % (key, str_dict_val(value))) for (key, value) in configure_env.items()] - configure_flags = env['_runtime_%s-%s_CONFIGURE_FLAGS' % (product, target)] - - command = path_join(MONO_SOURCE_ROOT, 'configure') - configure_args = ac_vars + configure_env + configure_flags - - run_command(command, args=configure_args, name='configure') - - -def make(product, target): - env = { 'ANDROID_API_VERSION': get_api_version_or_min(target) } - - build_dir = CONFIGURE_DIR + '/%s-%s-%s' % (product, target, CONFIGURATION) - chdir(build_dir) - - make_args = ['V=1'] if VERBOSE_MAKE else [] - - run_command('make', args=make_args, name='make') - run_command('make', args=['install'], name='make install') - - if STRIP_LIBS: - strip_libs(product, target, env['ANDROID_API_VERSION']) - - -def clean(product, target): - rm_rf( - CONFIGURE_DIR + '/toolchains/%s-%s' % (product, target), - CONFIGURE_DIR + '/%s-%s-%s' % (product, target, CONFIGURATION), - CONFIGURE_DIR + '/%s-%s-%s.config.cache' % (product, target, CONFIGURATION), - INSTALL_DIR + '/%s-%s-%s' % (product, target, CONFIGURATION) - ) - - -def set_arguments(args): - global CONFIGURATION, RELEASE, ANDROID_TOOLCHAINS_PREFIX, ANDROID_SDK_ROOT, ANDROID_NDK_ROOT, \ - _ANDROID_API_VERSION, _ANDROID_CMAKE_VERSION, WITH_MONODROID, ENABLE_CXX, \ - VERBOSE_MAKE, STRIP_LIBS, CONFIGURE_DIR, INSTALL_DIR, MONO_SOURCE_ROOT, CONFIGURATION - - # Need to make paths absolute as we change cwd later - - CONFIGURATION = args.configuration - RELEASE = (CONFIGURATION == 'release') - ANDROID_TOOLCHAINS_PREFIX = abspath(args.toolchains_prefix) - ANDROID_SDK_ROOT = abspath(args.android_sdk) - ANDROID_NDK_ROOT = abspath(args.android_ndk) - WITH_MONODROID = args.with_monodroid - ENABLE_CXX = args.enable_cxx - VERBOSE_MAKE = args.verbose_make - STRIP_LIBS = args.strip_libs - CONFIGURE_DIR = abspath(args.configure_dir) - INSTALL_DIR = abspath(args.install_dir) - MONO_SOURCE_ROOT = abspath(args.mono_sources) - - _ANDROID_API_VERSION = args.android_api_version - _ANDROID_CMAKE_VERSION = args.android_cmake_version - - -def main(raw_args): - import argparse - - from collections import OrderedDict - from textwrap import dedent - - target_indiv_values = ['armeabi-v7a', 'arm64-v8a', 'x86', 'x86_64'] - target_values = target_indiv_values + ['all'] - - actions = OrderedDict() - actions['configure'] = configure - actions['make'] = make - actions['clean'] = clean - - parser = argparse.ArgumentParser( - description='Builds the Mono runtime for Android', - formatter_class=argparse.RawDescriptionHelpFormatter, - epilog=dedent('''\ - environment variables: - ANDROID_SDK_ROOT: Overrides default value for --android-sdk - ANDROID_NDK_ROOT: Overrides default value for --android-ndk - MONO_SOURCE_ROOT: Overrides default value for --mono-sources - ANDROID_HOME: Same as ANDROID_SDK_ROOT - ''') - ) - - def custom_bool(val): - if isinstance(val, bool): - return val - if val.lower() in ('yes', 'true', 't', 'y', '1'): - return True - elif val.lower() in ('no', 'false', 'f', 'n', '0'): - return False - else: - raise argparse.ArgumentTypeError('Boolean value expected.') - - home = environ.get('HOME') - android_sdk_default = environ.get('ANDROID_HOME', environ.get('ANDROID_SDK_ROOT', path_join(home, 'Android/Sdk'))) - android_ndk_default = environ.get('ANDROID_NDK_ROOT', path_join(android_sdk_default, 'ndk-bundle')) - mono_sources_default = environ.get('MONO_SOURCE_ROOT', '') - - default_help = dedent('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('--configuration', choices=['release', 'debug'], default='release', help=default_help) - 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='18', help=default_help) - parser.add_argument('--android-cmake-version', default='autodetect', help=default_help) - parser.add_argument('--enable-cxx', action='store_true', default=False, help=default_help) - parser.add_argument('--verbose-make', action='store_true', default=False, help=default_help) - parser.add_argument('--strip-libs', type=custom_bool, default=True, help=default_help) - parser.add_argument('--with-monodroid', type=custom_bool, default=True, help=default_help) - parser.add_argument('--configure-dir', default=path_join(home, 'mono-configs'), help=default_help) - parser.add_argument('--install-dir', default=path_join(home, 'mono-installs'), help=default_help) - - if mono_sources_default: - parser.add_argument('--mono-sources', default=mono_sources_default, help=default_help) - else: - parser.add_argument('--mono-sources', required=True) - - args = parser.parse_args(raw_args) - - action = args.action - targets = args.target - - set_arguments(args) - - if not isdir(MONO_SOURCE_ROOT): - print('Mono sources directory not found: ' + MONO_SOURCE_ROOT) - exit(1) - - android_targets = [] - - if 'all' in targets: - android_targets = target_indiv_values[:] - else: - for target in targets: - if not target in android_targets: - android_targets += [target] - - action_fn = actions[action] - - try: - for target in android_targets: - action_fn('android', target) - except MonoBuildError as e: - exit(e.message) - - -if __name__ == '__main__': - from sys import argv - main(argv[1:]) diff --git a/cmd_utils.py b/cmd_utils.py new file mode 100644 index 0000000..ba97476 --- /dev/null +++ b/cmd_utils.py @@ -0,0 +1,80 @@ + + +def custom_bool(val): + if isinstance(val, bool): + return val + if val.lower() in ('yes', 'true', 't', 'y', '1'): + return True + elif val.lower() in ('no', 'false', 'f', 'n', '0'): + return False + else: + from argparse import ArgumentTypeError + raise ArgumentTypeError('Boolean value expected.') + + +def build_arg_parser(description, env_vars={}): + from argparse import ArgumentParser, RawDescriptionHelpFormatter + from textwrap import dedent + + base_env_vars = { + 'MONO_SOURCE_ROOT': 'Overrides default value for --mono-sources', + } + + env_vars_text = '\n'.join([' %s: %s' % (var, desc) for var, desc in env_vars.items()]) + base_env_vars_text = '\n'.join([' %s: %s' % (var, desc) for var, desc in base_env_vars.items()]) + + epilog=dedent('''\ +environment variables: +%s +%s +''' % (env_vars_text, base_env_vars_text)) + + return ArgumentParser( + description=description, + formatter_class=RawDescriptionHelpFormatter, + epilog=epilog + ) + + +def add_base_arguments(parser, default_help): + import os + from os.path import join as path_join + + home = os.environ.get('HOME') + mono_sources_default = os.environ.get('MONO_SOURCE_ROOT', '') + + parser.add_argument('--verbose-make', action='store_true', default=False, help=default_help) + parser.add_argument('--configure-dir', default=path_join(home, 'mono-configs'), help=default_help) + parser.add_argument('--install-dir', default=path_join(home, 'mono-installs'), help=default_help) + + if mono_sources_default: + parser.add_argument('--mono-sources', default=mono_sources_default, help=default_help) + else: + parser.add_argument('--mono-sources', required=True) + + parser.add_argument('--mxe-prefix', default='/usr', help=default_help) + + +def add_runtime_arguments(parser, default_help): + add_base_arguments(parser, default_help) + + parser.add_argument('--configuration', choices=['release', 'debug'], default='release', help=default_help) + parser.add_argument('--enable-cxx', action='store_true', default=False, help=default_help) + parser.add_argument('--strip-libs', type=custom_bool, default=True, help='Strip the libraries if possible after running make.\n' + default_help) + + +def expand_input_targets(input_targets, target_shortcuts=[]): + targets = [] + + for shortcut in target_shortcuts.keys(): + if shortcut in input_targets: + targets += target_shortcuts[shortcut][:] + + # The shortcuts options ('all-*') have already been handled. Remove them this way as there may be duplicates. + input_targets = [t for t in input_targets if not t in target_shortcuts] + + for target in input_targets: + if not target in targets: + targets += [target] + + return targets diff --git a/desktop.py b/desktop.py new file mode 100755 index 0000000..cdfac69 --- /dev/null +++ b/desktop.py @@ -0,0 +1,188 @@ +#!/usr/bin/python + +import os +import os.path +import sys + +from os.path import join as path_join + +from options import * +from os_utils import * +import runtime + +# TODO: mono cross-compilers + +target_platforms = ['linux', 'windows', 'osx'] + +targets = { + 'linux': ['i686', 'x86_64'], + 'windows': ['i686', 'x86_64'], + 'osx': ['x86_64'] +} + +host_triples = { + 'linux': '%s-linux-gnu', + 'windows': '%s-w64-mingw32', + 'osx': '%s-apple-darwin', +} + + +def is_cross_compiling(target_platform: str) -> bool: + return (sys.platform == 'darwin' and target_platform != 'osx') or \ + (sys.platform in ['linux', 'linux2', 'cygwin'] and target_platform != 'linux') + + +def setup_desktop_template(env: dict, opts: RuntimeOpts, product: str, target_platform: str, target: str): + host_triple = host_triples[target_platform] % target + + CONFIGURE_FLAGS = [ + '--disable-boehm', + '--disable-iconv', + '--disable-mcs-build', + '--disable-nls', + '--enable-dynamic-btls', + '--enable-maintainer-mode', + '--with-sigaltstack=yes', + '--with-tls=pthread', + '--without-ikvm-native' + ] + + if target_platform == 'windows': + mxe_bin = path_join(opts.mxe_prefix, 'bin') + + env['_%s-%s_PATH' % (product, target)] = mxe_bin + + name_fmt = target + '-w64-mingw32-%s' + + env['_%s-%s_AR' % (product, target)] = path_join(mxe_bin, name_fmt % 'ar') + env['_%s-%s_AS' % (product, target)] = path_join(mxe_bin, name_fmt % 'as') + env['_%s-%s_CC' % (product, target)] = path_join(mxe_bin, name_fmt % 'gcc') + env['_%s-%s_CXX' % (product, target)] = path_join(mxe_bin, name_fmt % 'g++') + env['_%s-%s_DLLTOOL' % (product, target)] = path_join(mxe_bin, name_fmt % 'dlltool') + env['_%s-%s_LD' % (product, target)] = path_join(mxe_bin, name_fmt % 'ld') + env['_%s-%s_OBJDUMP' % (product, target)] = path_join(mxe_bin, name_fmt % 'objdump') + env['_%s-%s_RANLIB' % (product, target)] = path_join(mxe_bin, name_fmt % 'ranlib') + env['_%s-%s_STRIP' % (product, target)] = path_join(mxe_bin, name_fmt % 'strip') + + CONFIGURE_FLAGS += [ + '--enable-static-gcc-libs' + ] + else: + env['_%s-%s_CC' % (product, target)] = 'cc' + + env['_%s-%s_CONFIGURE_FLAGS' % (product, target)] = CONFIGURE_FLAGS + + runtime.setup_runtime_template(env, opts, product, target, host_triple) + + +def strip_libs(opts: RuntimeOpts, product: str, target_platform: str, target: str): + if is_cross_compiling(target_platform): + if target_platform == 'windows': + mxe_bin = path_join(opts.mxe_prefix, 'bin') + name_fmt = target + '-w64-mingw32-%s' + strip = path_join(mxe_bin, name_fmt % 'strip') + elif target_platform == 'osx': + assert False # TODO osxcross + else: + strip = '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') + + if target_platform == 'windows': + out_bin_dir = path_join(install_dir, 'bin') + + dll_files = globs(('*.dll',), dirpath=out_bin_dir) + if len(dll_files): + run_command(strip, args=['--strip-unneeded'] + dll_files, name='strip') + + +def configure(opts: RuntimeOpts, product: str, target_platform: str, target: str): + env = {} + + setup_desktop_template(env, opts, product, target_platform, 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: RuntimeOpts, product: str, target_platform: str, target: str): + build_dir = path_join(opts.configure_dir, '%s-%s-%s' % (product, target, opts.configuration)) + + make_args = ['-C', build_dir] + make_args += ['V=1'] if opts.verbose_make else [] + + 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') + + if opts.strip_libs: + strip_libs(opts, product, target_platform, target) + + +def clean(opts: RuntimeOpts, product: str, target_platform: 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 collections import OrderedDict + from typing import Callable + + actions = OrderedDict() + actions['configure'] = configure + actions['make'] = make + actions['clean'] = clean + + parser = cmd_utils.build_arg_parser(description='Builds the Mono runtime for the Desktop') + subparsers = parser.add_subparsers(dest='platform') + + default_help = 'default: %(default)s' + + for target_platform in target_platforms: + target_platform_subparser = subparsers.add_parser(target_platform) + target_platform_subparser.add_argument('action', choices=['configure', 'make', 'clean']) + target_platform_subparser.add_argument('--target', choices=targets[target_platform], action='append', required=True) + + cmd_utils.add_runtime_arguments(parser, default_help) + + args = parser.parse_args(raw_args) + + input_action = args.action + input_target_platform = args.platform + input_targets = args.target + + opts = runtime_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) + + if is_cross_compiling(input_target_platform) and input_target_platform == 'osx': + raise RuntimeError('Cross-compiling for macOS is not currently supported') # TODO: osxcross + + if is_cross_compiling(input_target_platform) and sys.platform == 'darwin': + raise RuntimeError('Cross-compiling from macOS is not supported') + + action = actions[input_action] + + try: + for target in input_targets: + action(opts, 'desktop-%s' % input_target_platform, input_target_platform, target) + except BuildError as e: + sys.exit(e.message) + + +if __name__ == '__main__': + from sys import argv + main(argv[1:]) diff --git a/llvm.py b/llvm.py new file mode 100755 index 0000000..5da7563 --- /dev/null +++ b/llvm.py @@ -0,0 +1,118 @@ +#!/usr/bin/python + +import os +import sys + +from os.path import join as path_join +from options import * +from os_utils import * + + +target_values = ['llvm64', 'llvmwin64'] +mxe_targets = {'llvmwin64': {'arch': 'x86_64', 'mxe': 'mxe-Win64'}} + + +def make(opts: BaseOpts, target: str): + stamp_file = path_join(opts.configure_dir, '.stamp-%s-make' % target) + + if os.path.isfile(stamp_file): + return + + build_dir = path_join(opts.configure_dir, 'llvm-%s' % target) + install_dir = path_join(opts.install_dir, 'llvm-%s' % target) + + mkdir_p(build_dir) + mkdir_p(install_dir) + + CMAKE_ARGS = [] + + if target in mxe_targets: + mxe = mxe_targets[target]['mxe'] + arch = mxe_targets[target]['arch'] + + CMAKE_ARGS += [ + '-DCMAKE_EXE_LINKER_FLAGS="-static"', + '-DCROSS_TOOLCHAIN_FLAGS_NATIVE=-DCMAKE_TOOLCHAIN_FILE=%s/external/llvm/cmake/modules/NATIVE.cmake' % opts.mono_source_root, + '-DCMAKE_TOOLCHAIN_FILE=%s/external/llvm/cmake/modules/%s.cmake' % (opts.mono_source_root, mxe), + '-DLLVM_ENABLE_THREADS=Off', + '-DLLVM_BUILD_EXECUTION_ENGINE=Off' + ] + + if sys.platform == 'darwin': + mingw_zlib_prefix = '%s/opt/mingw-zlib/usr' % opts.mxe_prefix + if not os.path.isfile(mingw_zlib_prefix): + mingw_zlib_prefix = opts.mxe_prefix + + CMAKE_ARGS += [ + '-DZLIB_ROOT=%s/%s-w64-mingw32' % (mingw_zlib_prefix, arch), + '-DZLIB_LIBRARY=%s/%s-w64-mingw32/lib/libz.a' % (mingw_zlib_prefix, arch), + '-DZLIB_INCLUDE_DIR=%s/%s-w64-mingw32/include' % (mingw_zlib_prefix, arch) + ] + + replace_in_new_file( + src_file='%s/sdks/builds/%s.cmake.in' % (opts.mono_source_root, mxe), + search='@MXE_PATH@', replace=opts.mxe_prefix, + dst_file='%s/external/llvm/cmake/modules/%s.cmake' % (opts.mono_source_root, mxe) + ) + + CMAKE_ARGS += [os.environ.get('llvm-%s_CMAKE_ARGS' % target, '')] + + make_args = [ + '-C', '%s/llvm' % opts.mono_source_root, + '-f', 'build.mk', 'install-llvm', + 'LLVM_BUILD=%s' % build_dir, + 'LLVM_PREFIX=%s' % install_dir, + 'LLVM_CMAKE_ARGS=%s' % ' '.join([a for a in CMAKE_ARGS if a]) + ] + + make_args += ['V=1'] if opts.verbose_make else [] + + run_command('make', args=make_args, name='make') + + touch(stamp_file) + + +def clean(opts: BaseOpts, target: str): + build_dir = path_join(opts.configure_dir, 'llvm-%s' % target) + install_dir = path_join(opts.install_dir, 'llvm-%s' % target) + stamp_file = path_join(opts.configure_dir, '.stamp-%s-make' % target) + + rm_rf(stamp_file) + + make_args = [ + '-C', '%s/llvm' % opts.mono_source_root, + '-f', 'build.mk', 'clean-llvm', + 'LLVM_BUILD=%s' % build_dir, + 'LLVM_PREFIX=%s' % install_dir + ] + + make_args += ['V=1'] if opts.verbose_make else [] + + run_command('make', args=make_args, name='make clean') + + +def main(raw_args): + import cmd_utils + + parser = cmd_utils.build_arg_parser(description='Builds LLVM for Mono') + + default_help = 'default: %(default)s' + + parser.add_argument('action', choices=['make', 'clean']) + parser.add_argument('--target', choices=target_values, action='append', required=True) + + cmd_utils.add_base_arguments(parser, default_help) + + args = parser.parse_args(raw_args) + + opts = base_opts_from_args(args) + targets = args.target + + for target in targets: + action = { 'make': make, 'clean': clean }[args.action] + action(opts, target) + + +if __name__ == '__main__': + from sys import argv + main(argv[1:]) diff --git a/options.py b/options.py new file mode 100644 index 0000000..54d4438 --- /dev/null +++ b/options.py @@ -0,0 +1,79 @@ + +from dataclasses import dataclass +from os.path import abspath + + +@dataclass +class BaseOpts: + verbose_make: bool + configure_dir: str + install_dir: str + mono_source_root: str + mxe_prefix: str + + +@dataclass +class RuntimeOpts(BaseOpts): + configuration: str + release: bool + enable_cxx: bool + strip_libs: bool + + +@dataclass +class AndroidOpts(RuntimeOpts): + android_toolchains_prefix: str + android_sdk_root: str + android_ndk_root: str + with_monodroid: bool + android_api_version: str + android_cmake_version: str + toolchain_name_fmt: str = '%s-api%s-clang' + + +@dataclass +class BclOpts(BaseOpts): + tests: bool + + +# Need to make paths absolute as we change cwd + + +def base_opts_from_args(args): + from os.path import abspath + return BaseOpts( + verbose_make = args.verbose_make, + configure_dir = abspath(args.configure_dir), + install_dir = abspath(args.install_dir), + mono_source_root = abspath(args.mono_sources), + mxe_prefix = args.mxe_prefix + ) + + +def runtime_opts_from_args(args): + return RuntimeOpts( + **vars(base_opts_from_args(args)), + configuration = args.configuration, + release = (args.configuration == 'release'), + enable_cxx = args.enable_cxx, + strip_libs = args.strip_libs + ) + + +def android_opts_from_args(args): + return AndroidOpts( + **vars(runtime_opts_from_args(args)), + android_toolchains_prefix = abspath(args.toolchains_prefix), + android_sdk_root = abspath(args.android_sdk), + android_ndk_root = abspath(args.android_ndk), + with_monodroid = args.with_monodroid, + android_api_version = args.android_api_version, + android_cmake_version = args.android_cmake_version + ) + + +def bcl_opts_from_args(args): + return BclOpts( + **vars(base_opts_from_args(args)), + tests = args.tests + ) diff --git a/os_utils.py b/os_utils.py new file mode 100644 index 0000000..2068d92 --- /dev/null +++ b/os_utils.py @@ -0,0 +1,137 @@ + +import os +import os.path + + +class BuildError(Exception): + '''Generic exception for custom build errors''' + def __init__(self, msg): + super(BuildError, self).__init__(msg) + self.message = msg + + +def run_command(command, args=[], cwd=None, env=None, name='command'): + def cmd_args_to_str(cmd_args): + return ' '.join([arg if not ' ' in arg else '"%s"' % arg for arg in cmd_args]) + + assert isinstance(command, str) and isinstance(args, list) + args = [command] + args + + check_call_args = {} + if cwd is not None: + check_call_args['cwd'] = cwd + if env is not None: + check_call_args['env'] = env + + import subprocess + try: + print('Running command \'%s\': %s' % (name, subprocess.list2cmdline(args))) + subprocess.check_call(args, **check_call_args) + print('Command \'%s\' completed successfully' % name) + except subprocess.CalledProcessError as e: + raise BuildError('\'%s\' exited with error code: %s' % (name, e.returncode)) + + +def source(script: str, cwd=None) -> dict: + popen_args = {} + if cwd is not None: + popen_args['cwd'] = cwd + + import subprocess + proc = subprocess.Popen('bash -c \'source %s; env -0\'' % script, stdout=subprocess.PIPE, shell=True, **popen_args) + output = proc.communicate()[0] + return dict(line.split('=', 1) for line in output.decode().split('\x00') if line) + + +# Creates the directory if no other file or directory with the same path exists +def mkdir_p(path): + if not os.path.exists(path): + print('creating directory: ' + path) + os.makedirs(path) + + +# Remove files and/or directories recursively +def rm_rf(*paths): + from shutil import rmtree + for path in paths: + if os.path.isfile(path): + print('removing file: ' + path) + os.remove(path) + elif os.path.isdir(path): + print('removing directory and its contents: ' + path) + rmtree(path) + + +ENV_PATH_SEP = ';' if os.name == 'nt' else ':' + + +def find_executable(name) -> str: + is_windows = os.name == 'nt' + windows_exts = ENV_PATH_SEP.split(os.environ['PATHEXT']) if is_windows else None + path_dirs = ENV_PATH_SEP.split(os.environ['PATH']) + + search_dirs = path_dirs + [os.getcwd()] # cwd is last in the list + + for dir in search_dirs: + path = os.path.join(dir, name) + + if is_windows: + for extension in windows_exts: + path_with_ext = path + extension + + if os.path.isfile(path_with_ext) and os.access(path_with_ext, os.X_OK): + return path_with_ext + else: + if os.path.isfile(path) and os.access(path, os.X_OK): + return path + + return '' + + +def replace_in_new_file(src_file, search, replace, dst_file): + with open(src_file, 'r') as file: + content = file.read() + + content = content.replace(search, replace) + + with open(dst_file, 'w') as file: + file.write(content) + + +def replace_in_file(filepath, search, replace): + replace_in_new_file(src_file=filepath, search=search, replace=replace, dst_file=filepath) + + +def touch(filepath: str): + import pathlib + pathlib.Path(filepath).touch() + + +def get_emsdk_root(): + # Shamelessly copied from Godot's detect.py + em_config_file = os.getenv('EM_CONFIG') or os.path.expanduser('~/.emscripten') + if not os.path.exists(em_config_file): + raise BuildError("Emscripten configuration file '%s' does not exist" % em_config_file) + with open(em_config_file) as f: + em_config = {} + try: + # Emscripten configuration file is a Python file with simple assignments. + exec(f.read(), em_config) + except StandardError as e: + raise BuildError("Emscripten configuration file '%s' is invalid:\n%s" % (em_config_file, e)) + if 'BINARYEN_ROOT' in em_config and os.path.isdir(os.path.join(em_config.get('BINARYEN_ROOT'), 'emscripten')): + # New style, emscripten path as a subfolder of BINARYEN_ROOT + return os.path.join(em_config.get('BINARYEN_ROOT'), 'emscripten') + elif 'EMSCRIPTEN_ROOT' in em_config: + # Old style (but can be there as a result from previous activation, so do last) + return em_config.get('EMSCRIPTEN_ROOT') + else: + raise BuildError("'BINARYEN_ROOT' or 'EMSCRIPTEN_ROOT' missing in Emscripten configuration file '%s'" % em_config_file) + + +def globs(pathnames, dirpath='.'): + import glob + files = [] + for pathname in pathnames: + files.extend(glob.glob(os.path.join(dirpath, pathname))) + return files diff --git a/patch_emscripten.py b/patch_emscripten.py new file mode 100755 index 0000000..bce6720 --- /dev/null +++ b/patch_emscripten.py @@ -0,0 +1,37 @@ +#!/usr/bin/python + + +def main(raw_args): + import os + import cmd_utils + from os_utils import get_emsdk_root + + parser = cmd_utils.build_arg_parser(description='Apply patches to the active Emscripten SDK') + + default_help = 'default: %(default)s' + + mono_sources_default = os.environ.get('MONO_SOURCE_ROOT', '') + + if mono_sources_default: + parser.add_argument('--mono-sources', default=mono_sources_default, help=default_help) + else: + parser.add_argument('--mono-sources', required=True) + + args = parser.parse_args(raw_args) + + mono_source_root = args.mono_sources + emsdk_root = get_emsdk_root() + + patches = [ + '%s/sdks/builds/fix-emscripten-8511.diff' % mono_source_root, + '%s/sdks/builds/emscripten-pr-8457.diff' % mono_source_root + ] + + from subprocess import Popen + for patch in patches: + proc = Popen('bash -c \'patch -N -p1 < %s; exit 0\'' % patch, cwd=emsdk_root, shell=True) + + +if __name__ == '__main__': + from sys import argv + main(argv[1:]) diff --git a/patch_mono.py b/patch_mono.py new file mode 100755 index 0000000..331c986 --- /dev/null +++ b/patch_mono.py @@ -0,0 +1,39 @@ +#!/usr/bin/python + + +def main(raw_args): + import cmd_utils + import os + import os.path + from os_utils import get_emsdk_root + + parser = cmd_utils.build_arg_parser(description='Apply patches to the Mono source tree') + + default_help = 'default: %(default)s' + + mono_sources_default = os.environ.get('MONO_SOURCE_ROOT', '') + + if mono_sources_default: + parser.add_argument('--mono-sources', default=mono_sources_default, help=default_help) + else: + parser.add_argument('--mono-sources', required=True) + + args = parser.parse_args(raw_args) + + this_script_dir = os.path.dirname(os.path.realpath(__file__)) + patches_dir = os.path.join(this_script_dir, 'patches') + + mono_source_root = args.mono_sources + + patches = [ + 'fix-mono-android-tkill.diff' + ] + + from subprocess import Popen + for patch in patches: + proc = Popen('bash -c \'patch -N -p1 < %s; exit 0\'' % os.path.join(patches_dir, patch), cwd=mono_source_root, shell=True) + + +if __name__ == '__main__': + from sys import argv + main(argv[1:]) diff --git a/patches/fix-mono-android-tkill.diff b/patches/fix-mono-android-tkill.diff new file mode 100644 index 0000000..05f8dca --- /dev/null +++ b/patches/fix-mono-android-tkill.diff @@ -0,0 +1,70 @@ +diff --git a/libgc/include/private/gcconfig.h b/libgc/include/private/gcconfig.h +index e2bdf13ac3e..f962200ba4e 100644 +--- a/libgc/include/private/gcconfig.h ++++ b/libgc/include/private/gcconfig.h +@@ -2255,6 +2255,14 @@ + # define GETPAGESIZE() getpagesize() + # endif + ++#if defined(HOST_ANDROID) && !(__ANDROID_API__ >= 23) \ ++ && ((defined(MIPS) && (CPP_WORDSZ == 32)) \ ++ || defined(ARM32) || defined(I386) /* but not x32 */) ++ /* tkill() exists only on arm32/mips(32)/x86. */ ++ /* NDK r11+ deprecates tkill() but keeps it for Mono clients. */ ++# define USE_TKILL_ON_ANDROID ++#endif ++ + # if defined(SUNOS5) || defined(DRSNX) || defined(UTS4) + /* OS has SVR4 generic features. Probably others also qualify. */ + # define SVR4 +diff --git a/libgc/pthread_stop_world.c b/libgc/pthread_stop_world.c +index f93ce26b562..4a49a6d578c 100644 +--- a/libgc/pthread_stop_world.c ++++ b/libgc/pthread_stop_world.c +@@ -336,7 +336,7 @@ void GC_push_all_stacks() + pthread_t GC_stopping_thread; + int GC_stopping_pid; + +-#ifdef HOST_ANDROID ++#ifdef USE_TKILL_ON_ANDROID + static + int android_thread_kill(pid_t tid, int sig) + { +diff --git a/mono/metadata/threads.c b/mono/metadata/threads.c +index ad9b8823f8f..3542b32b540 100644 +--- a/mono/metadata/threads.c ++++ b/mono/metadata/threads.c +@@ -77,8 +77,12 @@ mono_native_thread_join_handle (HANDLE thread_handle, gboolean close_handle); + #include + #endif + +-#if defined(HOST_ANDROID) && !defined(TARGET_ARM64) && !defined(TARGET_AMD64) +-#define USE_TKILL_ON_ANDROID 1 ++#if defined(HOST_ANDROID) && !(__ANDROID_API__ >= 23) \ ++ && ((defined(MIPS) && (CPP_WORDSZ == 32)) \ ++ || defined(ARM32) || defined(I386) /* but not x32 */) ++ /* tkill() exists only on arm32/mips(32)/x86. */ ++ /* NDK r11+ deprecates tkill() but keeps it for Mono clients. */ ++# define USE_TKILL_ON_ANDROID + #endif + + #ifdef HOST_ANDROID +diff --git a/mono/utils/mono-threads-posix.c b/mono/utils/mono-threads-posix.c +index 3e4bf93de5f..79c9f731fe7 100644 +--- a/mono/utils/mono-threads-posix.c ++++ b/mono/utils/mono-threads-posix.c +@@ -31,8 +31,12 @@ + + #include + +-#if defined(HOST_ANDROID) && !defined(TARGET_ARM64) && !defined(TARGET_AMD64) +-#define USE_TKILL_ON_ANDROID 1 ++#if defined(HOST_ANDROID) && !(__ANDROID_API__ >= 23) \ ++ && ((defined(MIPS) && (CPP_WORDSZ == 32)) \ ++ || defined(ARM32) || defined(I386) /* but not x32 */) ++ /* tkill() exists only on arm32/mips(32)/x86. */ ++ /* NDK r11+ deprecates tkill() but keeps it for Mono clients. */ ++# define USE_TKILL_ON_ANDROID + #endif + + #ifdef USE_TKILL_ON_ANDROID diff --git a/reference_assemblies.py b/reference_assemblies.py new file mode 100755 index 0000000..5d05438 --- /dev/null +++ b/reference_assemblies.py @@ -0,0 +1,62 @@ +#!/usr/bin/python + +from os.path import join as path_join +from options import * +from os_utils import * + + +def build(opts: BaseOpts): + build_dir = '%s/mcs/class/reference-assemblies' % opts.mono_source_root + install_dir = path_join(opts.install_dir, 'reference-assemblies') + + mkdir_p(install_dir) + + make_args = ['-C', build_dir, 'build-reference-assemblies'] + make_args += ['V=1'] if opts.verbose_make else [] + run_command('make', args=make_args, name='make build-reference-assemblies') + + +def install(opts: BaseOpts): + build_dir = '%s/mcs/class/reference-assemblies' % opts.mono_source_root + install_dir = path_join(opts.install_dir, 'reference-assemblies') + + mkdir_p(install_dir) + + make_args = ['-C', build_dir, 'install-local', 'DESTDIR=%s' % install_dir, 'prefix=/'] + make_args += ['V=1'] if opts.verbose_make else [] + run_command('make', args=make_args, name='make install-local') + + +def clean(opts: BaseOpts): + install_dir = path_join(opts.install_dir, 'reference-assemblies') + rm_rf(install_dir) + + +def main(raw_args): + import cmd_utils + + actions = { + 'build': build, + 'install': install, + 'clean': clean + } + + parser = cmd_utils.build_arg_parser(description='Copy the reference assemblies') + + default_help = 'default: %(default)s' + + parser.add_argument('action', choices=actions.keys()) + + cmd_utils.add_base_arguments(parser, default_help) + + args = parser.parse_args(raw_args) + + opts = base_opts_from_args(args) + + action = actions[args.action] + action(opts) + + +if __name__ == '__main__': + from sys import argv + main(argv[1:]) diff --git a/runtime.py b/runtime.py new file mode 100644 index 0000000..27443d8 --- /dev/null +++ b/runtime.py @@ -0,0 +1,159 @@ + +import os +from os.path import join as path_join + +from options import RuntimeOpts +from os_utils import * + + +def setup_runtime_template(env: dict, opts: RuntimeOpts, product: str, target: str, host_triple: str): + BITNESS = '' + if any(s in host_triple for s in ['i686', 'i386']): + BITNESS = '-m32' + elif 'x86_64' in host_triple: + BITNESS = '-m64' + + CFLAGS = [] + CFLAGS += ['-O2', '-g'] if opts.release else ['-O0', '-ggdb3', '-fno-omit-frame-pointer'] + CFLAGS += env.get('_%s-%s_CFLAGS' % (product, target), []) + CFLAGS += env.get('%s-%s_CFLAGS' % (product, target), []) + CFLAGS += [BITNESS] if BITNESS else [] + + CXXFLAGS = [] + CXXFLAGS += ['-O2', '-g'] if opts.release else ['-O0', '-ggdb3', '-fno-omit-frame-pointer'] + CXXFLAGS += env.get('_%s-%s_CXXFLAGS' % (product, target), []) + CXXFLAGS += env.get('%s-%s_CXXFLAGS' % (product, target), []) + CXXFLAGS += [BITNESS] if BITNESS else [] + + CPPFLAGS = [] + CPPFLAGS += ['-O2', '-g'] if opts.release else ['-O0', '-ggdb3', '-fno-omit-frame-pointer'] + CPPFLAGS += env.get('_%s-%s_CPPFLAGS' % (product, target), []) + CPPFLAGS += env.get('%s-%s_CPPFLAGS' % (product, target), []) + CPPFLAGS += [BITNESS] if BITNESS else [] + + CXXCPPFLAGS = [] + CXXCPPFLAGS += ['-O2', '-g'] if opts.release else ['-O0', '-ggdb3', '-fno-omit-frame-pointer'] + CXXCPPFLAGS += env.get('_%s-%s_CXXCPPFLAGS' % (product, target), []) + CXXCPPFLAGS += env.get('%s-%s_CXXCPPFLAGS' % (product, target), []) + CXXCPPFLAGS += [BITNESS] if BITNESS else [] + + LDFLAGS = [] + LDFLAGS += env.get('_%s-%s_LDFLAGS' % (product, target), []) + LDFLAGS += env.get('%s-%s_LDFLAGS' % (product, target), []) + + AC_VARS = [] + AC_VARS += env.get('_%s-%s_AC_VARS' % (product, target), []) + AC_VARS += env.get('%s-%s_AC_VARS' % (product, target), []) + + CONFIGURE_ENVIRONMENT = {} + + def set_product_env_var(var_name): + val = env.get('_%s-%s_%s' % (product, target, var_name), '') + if val: + CONFIGURE_ENVIRONMENT[var_name] = val + + set_product_env_var('AR') + set_product_env_var('AS') + set_product_env_var('CC') + set_product_env_var('CPP') + set_product_env_var('CXX') + set_product_env_var('CXXCPP') + set_product_env_var('DLLTOOL') + set_product_env_var('LD') + set_product_env_var('OBJDUMP') + set_product_env_var('RANLIB') + set_product_env_var('CMAKE') + set_product_env_var('STRIP') + + CONFIGURE_ENVIRONMENT['CFLAGS'] = CFLAGS + CONFIGURE_ENVIRONMENT['CXXFLAGS'] = CXXFLAGS + CONFIGURE_ENVIRONMENT['CPPFLAGS'] = CPPFLAGS + CONFIGURE_ENVIRONMENT['CXXCPPFLAGS'] = CXXCPPFLAGS + CONFIGURE_ENVIRONMENT['LDFLAGS'] = LDFLAGS + + CONFIGURE_ENVIRONMENT.update(env.get('_%s-%s_CONFIGURE_ENVIRONMENT' % (product, target), {})) + CONFIGURE_ENVIRONMENT.update(env.get('%s-%s_CONFIGURE_ENVIRONMENT' % (product, target), {})) + + CONFIGURE_FLAGS = [] + CONFIGURE_FLAGS += ['--host=%s' % host_triple] if host_triple else [] + CONFIGURE_FLAGS += ['--cache-file=%s/%s-%s-%s.config.cache' % (opts.configure_dir, product, target, opts.configuration)] + CONFIGURE_FLAGS += ['--prefix=%s/%s-%s-%s' % (opts.install_dir, product, target, opts.configuration)] + CONFIGURE_FLAGS += ['--enable-cxx'] if opts.enable_cxx else [] + CONFIGURE_FLAGS += env.get('_cross-runtime_%s-%s_CONFIGURE_FLAGS' % (product, target), []) + CONFIGURE_FLAGS += env.get('_%s-%s_CONFIGURE_FLAGS' % (product, target), []) + CONFIGURE_FLAGS += env.get('%s-%s_CONFIGURE_FLAGS' % (product, target), []) + + env['_runtime_%s-%s_AC_VARS' % (product, target)] = AC_VARS + env['_runtime_%s-%s_CONFIGURE_ENVIRONMENT' % (product, target)] = CONFIGURE_ENVIRONMENT + env['_runtime_%s-%s_CONFIGURE_FLAGS' % (product, target)] = CONFIGURE_FLAGS + + +def setup_runtime_cross_template(env: dict, opts: RuntimeOpts, product: str, target: str, host_triple: str, + target_triple: str, device_target: str, llvm: str, offsets_dumper_abi: str): + CONFIGURE_FLAGS = [ + '--target=%s' % target_triple, + '--with-cross-offsets=%s.h' % target_triple, + '--with-llvm=%s/%s' % (opts.install_dir, llvm) + ] + + env['_cross-runtime_%s-%s_CONFIGURE_FLAGS' % (product, target)] = CONFIGURE_FLAGS + + # Setup offsets-tool-py + run_command('make', ['-C', '%s/tools/offsets-tool-py' % opts.mono_source_root, 'setup'], name='make offsets-tool-py') + + # Run offsets-tool in its virtual env + + virtualenv_vars = source('%s/tools/offsets-tool-py/offtool/bin/activate' % opts.mono_source_root) + + offsets_tool_env = os.environ.copy() + offsets_tool_env.update(virtualenv_vars) + + build_dir = '%s/%s-%s-%s' % (opts.configure_dir, product, target, opts.configuration) + mkdir_p(build_dir) + + run_command('python3', [ + '%s/tools/offsets-tool-py/offsets-tool.py' % opts.mono_source_root, + '--targetdir=%s/%s-%s-%s' % (opts.configure_dir, product, device_target, opts.configuration), + '--abi=%s' % offsets_dumper_abi, + '--monodir=%s' % opts.mono_source_root, + '--outfile=%s/%s.h' % (build_dir, target_triple) + ] + env['_%s-%s_OFFSETS_DUMPER_ARGS' % (product, target)], + env=offsets_tool_env, name='offsets-tool') + + # Runtime template + setup_runtime_template(env, opts, product, target, host_triple) + + +def run_autogen(opts: RuntimeOpts): + autogen_env = os.environ.copy() + autogen_env['NOCONFIGURE'] = '1' + + if not find_executable('glibtoolize') and 'CUSTOM_GLIBTOOLIZE_PATH' in os.environ: + autogen_env['PATH'] = os.environ['CUSTOM_GLIBTOOLIZE_PATH'] + ':' + autogen_env['PATH'] + + run_command(os.path.join(opts.mono_source_root, 'autogen.sh'), cwd=opts.mono_source_root, env=autogen_env, name='autogen') + + +def run_configure(env: dict, opts: RuntimeOpts, product: str, target: str): + build_dir = path_join(opts.configure_dir, '%s-%s-%s' % (product, target, opts.configuration)) + mkdir_p(build_dir) + + def str_dict_val(val): + if isinstance(val, list): + return ' '.join(val) # Don't need to surround with quotes + return val + + ac_vars = env['_runtime_%s-%s_AC_VARS' % (product, target)] + configure_env_args = env['_runtime_%s-%s_CONFIGURE_ENVIRONMENT' % (product, target)] + configure_env_args = [('%s=%s' % (key, str_dict_val(value))) for (key, value) in configure_env_args.items()] + configure_flags = env['_runtime_%s-%s_CONFIGURE_FLAGS' % (product, target)] + + configure = path_join(opts.mono_source_root, 'configure') + configure_args = ac_vars + configure_env_args + configure_flags + + configure_env = os.environ.copy() + target_extra_path = env.get('_%s-%s_PATH' % (product, target), '') + if target_extra_path: + configure_env['PATH'] += ':' + target_extra_path + + run_command(configure, args=configure_args, cwd=build_dir, env=configure_env, name='configure') diff --git a/wasm.py b/wasm.py new file mode 100755 index 0000000..50c0d8f --- /dev/null +++ b/wasm.py @@ -0,0 +1,231 @@ +#!/usr/bin/python + +import os +import os.path +import runtime +import sys + +from options import * +from os_utils import * +from os.path import join as path_join + + +runtime_targets = ['runtime', 'runtime-threads', 'runtime-dynamic'] +cross_targets = [] # ['cross'] # TODO +cross_mxe_targets = [] # ['cross-win'] # TODO + + +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 setup_wasm_target_template(env: dict, opts: RuntimeOpts, target: str): + extra_target_envs = { + 'runtime-threads': { + 'wasm_runtime-threads_CFLAGS': ['-s', 'USE_PTHREADS=1', '-pthread'], + 'wasm_runtime-threads_CXXFLAGS': ['-s', 'USE_PTHREADS=1', '-pthread'] + }, + 'runtime-dynamic': { + 'wasm_runtime-dynamic_CFLAGS': ['-s', 'WASM_OBJECT_FILES=0'], + 'wasm_runtime-dynamic_CXXFLAGS': ['-s', 'WASM_OBJECT_FILES=0'] + } + } + + if target in extra_target_envs: + env.update(extra_target_envs[target]) + + CFLAGS = ['-fexceptions'] + CFLAGS += ['-Os', '-g'] if opts.release else ['-O0', '-ggdb3', '-fno-omit-frame-pointer'] + CXXFLAGS = CFLAGS + ['-s', 'DISABLE_EXCEPTION_CATCHING=0'] + + CONFIGURE_FLAGS = [ + '--disable-mcs-build', + '--disable-nls', + '--disable-boehm', + '--disable-btls', + '--with-lazy-gc-thread-creation=yes', + '--with-libgc=none', + '--disable-executables', + '--disable-support-build', + '--disable-visibility-hidden', + '--enable-maintainer-mode', + '--enable-minimal=ssa,com,jit,reflection_emit_save,portability,assembly_remapping,attach,verifier,full_messages,appdomains,security,sgen_marksweep_conc,sgen_split_nursery,sgen_gc_bridge,logging,remoting,shared_perfcounters,sgen_debug_helpers,soft_debug,interpreter,assert_messages,cleanup,mdb,gac', + '--host=wasm32', + '--enable-llvm-runtime', + '--enable-icall-export', + '--disable-icall-tables', + '--disable-crash-reporting', + '--with-bitcode=yes' + ] + + CONFIGURE_FLAGS += ['--enable-cxx'] if opts.enable_cxx else [] + + CONFIGURE_FLAGS += [ + '--cache-file=%s/wasm-%s-%s.config.cache' % (opts.configure_dir, target, opts.configuration), + '--prefix=%s/wasm-%s-%s' % (opts.install_dir, target, opts.configuration), + 'CFLAGS=%s %s' % (' '.join(CFLAGS), env.get('wasm_%s_CFLAGS' % target, '')), + 'CXXFLAGS=%s %s' % (' '.join(CXXFLAGS), env.get('$$(wasm_%s_CXXFLAGS' % target, '')) + ] + + CONFIGURE_FLAGS += env.get('wasm_%s_CONFIGURE_FLAGS' % target, []) + + env['_wasm_%s_CONFIGURE_FLAGS' % target] = CONFIGURE_FLAGS + env['_wasm_%s_AC_VARS' % target] = ['ac_cv_func_shm_open_working_with_mmap=no'] + + +def wasm_run_configure(env: dict, opts: RuntimeOpts, product: str, target: str, emsdk_root: str): + build_dir = path_join(opts.configure_dir, '%s-%s-%s' % (product, target, opts.configuration)) + mkdir_p(build_dir) + + def str_dict_val(val): + if isinstance(val, list): + return ' '.join(val) # Don't need to surround with quotes + return val + + ac_vars = env['_%s_%s_AC_VARS' % (product, target)] + configure_flags = env['_%s_%s_CONFIGURE_FLAGS' % (product, target)] + + configure = path_join(opts.mono_source_root, 'configure') + configure_args = ac_vars + configure_flags + + configure_env = os.environ.copy() + + target_extra_path = env.get('_%s-%s_PATH' % (product, target), '') + if target_extra_path: + configure_env['PATH'] += ':' + target_extra_path + + configure_env['PATH'] = emsdk_root + ':' + configure_env['PATH'] + + run_command('emconfigure', args=[configure] + configure_args, cwd=build_dir, env=configure_env, name='configure') + + +def configure(opts: RuntimeOpts, product: str, target: str): + env = {} + + if is_cross(target): + if is_cross_mxe(target): + raise RuntimeError('TODO') + else: + raise RuntimeError('TODO') + else: + setup_wasm_target_template(env, opts, target) + + if not os.path.isfile(path_join(opts.mono_source_root, 'configure')): + runtime.run_autogen(opts) + + wasm_run_configure(env, opts, product, target, get_emsdk_root()) + + +def make(opts: RuntimeOpts, product: str, target: str): + env = {} + + emsdk_root = get_emsdk_root() + + build_dir = path_join(opts.configure_dir, '%s-%s-%s' % (product, target, opts.configuration)) + install_dir = path_join(opts.install_dir, '%s-%s-%s' % (product, target, opts.configuration)) + + make_args = ['-C', build_dir] + make_args += ['V=1'] if opts.verbose_make else [] + + make_env = os.environ.copy() + make_env['PATH'] = emsdk_root + ':' + make_env['PATH'] + + run_command('emmake', args=['make'] + make_args, env=make_env, name='make') + + run_command('make', args=['-C', '%s/mono' % build_dir, 'install'], name='make install mono') + + # Copy support headers + + from shutil import copy + + headers = ['crc32.h', 'deflate.h', 'inffast.h', 'inffixed.h', 'inflate.h', 'inftrees.h', 'trees.h', 'zconf.h', 'zlib.h', 'zutil.h'] + src_support_dir = '%s/support' % opts.mono_source_root + dst_support_dir = '%s/include/support' % install_dir + + mkdir_p(dst_support_dir) + + for header in headers: + copy(path_join(src_support_dir, header), dst_support_dir) + + # Copy wasm src files + + wasm_src_files = [ + 'driver.c', + 'zlib-helper.c', + 'corebindings.c', + 'pinvoke-tables-default.h', + 'pinvoke-tables-default-netcore.h', + 'library_mono.js', + 'binding_support.js', + 'dotnet_support.js' + ] + + dst_wasm_src_dir = path_join(install_dir, 'src') + + mkdir_p(dst_wasm_src_dir) + + for wasm_src_file in wasm_src_files: + copy(path_join(opts.mono_source_root, 'sdks/wasm/src', wasm_src_file), dst_wasm_src_dir) + + +def clean(opts: RuntimeOpts, 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 collections import OrderedDict + from typing import Callable + + target_shortcuts = {'all-runtime': runtime_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 WebAssembly') + + emsdk_root_default = os.environ.get('EMSDK_ROOT', default='') + + default_help = 'default: %(default)s' + + parser.add_argument('action', choices=['configure', 'make', 'clean']) + parser.add_argument('--target', choices=target_values, action='append', required=True) + + cmd_utils.add_runtime_arguments(parser, default_help) + + args = parser.parse_args(raw_args) + + input_action = args.action + input_targets = args.target + + opts = runtime_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, 'wasm', target) + except BuildError as e: + sys.exit(e.message) + + +if __name__ == '__main__': + from sys import argv + main(argv[1:])