diff --git a/tslang/test/tester/CMakeLists.txt b/tslang/test/tester/CMakeLists.txt index d1a5016c8..0a750c9da 100644 --- a/tslang/test/tester/CMakeLists.txt +++ b/tslang/test/tester/CMakeLists.txt @@ -2416,6 +2416,19 @@ add_test(NAME test-compile-target-crt "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/target-crt" -P "${CMAKE_CURRENT_SOURCE_DIR}/target-crt.cmake") +# Compiling and linking for Android - see android-link.cmake. `codegen` needs no NDK and always +# runs; `link` is skipped (not failed) without the NDK and the Android libraries. +foreach(android_mode codegen link) + add_test(NAME test-compile-android-${android_mode} + COMMAND ${CMAKE_COMMAND} + "-DMODE=${android_mode}" + "-DTSLANG=$" + "-DWORK_DIR=${CMAKE_CURRENT_BINARY_DIR}/android-${android_mode}" + "-DREPO_DIR=${PROJECT_SOURCE_DIR}/.." + -P "${CMAKE_CURRENT_SOURCE_DIR}/android-link.cmake") + set_tests_properties(test-compile-android-${android_mode} PROPERTIES SKIP_REGULAR_EXPRESSION "SKIPPED:") +endforeach() + # One collector per process with the real default library in it - see defaultlib-collector.cmake. # Skipped (not failed) when no default library has been built; the release workflows run them after # building one. The sources live outside tests/: without the default library they do not compile, diff --git a/tslang/test/tester/android-link.cmake b/tslang/test/tester/android-link.cmake new file mode 100644 index 000000000..1e7c448de --- /dev/null +++ b/tslang/test/tester/android-link.cmake @@ -0,0 +1,131 @@ +# Compiling and linking for Android (-mtriple=-linux-android). +# +# MODE=codegen runs everywhere, with no NDK: +# - objects for Android are position independent without -relocation-model=pic +# (Android executables must be PIE): a constant's address is RIP-relative, where +# the static default for x86_64 Linux takes it as an absolute immediate; +# - a triple without an API level, and no NDK, are refused with an error that says so. +# MODE=link links an executable and a shared library for x86_64 Android with the real default +# library, collector and async runtime, through the NDK. Prints SKIPPED (CTest then +# reports it skipped) unless ANDROID_NDK_HOME and the Android builds of those are +# there: scripts/build_gc_release_android.bat, scripts/build_tslang_runtime_release_ +# android.bat, and TypeScriptCompilerDefaultLib's scripts/build_android.bat (found +# next to this repository, or at TSLANG_ANDROID_DEFAULT_LIB_PATH). A shared library +# links with --no-undefined, so a symbol Bionic lacks fails here, not in the app. + +cmake_minimum_required(VERSION 3.17.3) + +foreach(var MODE TSLANG WORK_DIR REPO_DIR) + if(NOT DEFINED ${var}) + message(FATAL_ERROR "${var} is required") + endif() +endforeach() + +file(REMOVE_RECURSE "${WORK_DIR}") +file(MAKE_DIRECTORY "${WORK_DIR}") + +file(WRITE "${WORK_DIR}/app.ts" [=[ +class Acc { + total = 0; + add(n: number) { this.total += n; return this; } +} + +export function sum3(a: number, b: number, c: number): number { + const acc = new Acc().add(a).add(b).add(c); + assert(acc.total >= 0, "negative"); + print(`sum3 = ${acc.total}`); + return acc.total; +} + +function main() { + print(sum3(1, 2, 3)); +} +]=]) + +# run( ) - leaves the output in run_output +function(run what expect_success) + execute_process(COMMAND ${ARGN} + WORKING_DIRECTORY "${WORK_DIR}" + OUTPUT_VARIABLE out + ERROR_VARIABLE err + RESULT_VARIABLE status) + set(run_output "${out}${err}" PARENT_SCOPE) + if(expect_success AND NOT status EQUAL 0) + message(FATAL_ERROR "${what}: exit ${status}\n${out}\n${err}") + endif() + if(NOT expect_success AND status EQUAL 0) + message(FATAL_ERROR "${what}: expected to fail, but succeeded\n${out}\n${err}") + endif() +endfunction() + +if(MODE STREQUAL "codegen") + foreach(triple x86_64-linux-android29 x86_64-unknown-linux-gnu) + run("--emit=asm for ${triple}" TRUE + "${TSLANG}" --emit=asm --opt -mm=none --no-default-lib -mtriple=${triple} app.ts -o ${triple}.s) + file(READ "${WORK_DIR}/${triple}.s" asm) + if(asm MATCHES "\\$frmt_") + set(${triple}_absolute TRUE) + endif() + endforeach() + if(x86_64-linux-android29_absolute) + file(READ "${WORK_DIR}/x86_64-linux-android29.s" asm) + message(FATAL_ERROR "the code for Android takes an absolute address, it is not position independent:\n${asm}") + endif() + if(NOT x86_64-unknown-linux-gnu_absolute) + message(FATAL_ERROR "the check cannot tell: x86_64 Linux code without -relocation-model=pic takes no absolute address either") + endif() + + run("--emit=exe for Android with no API level" FALSE + "${TSLANG}" --emit=exe -mm=none --no-default-lib -mtriple=x86_64-linux-android app.ts -o app) + if(NOT run_output MATCHES "an Android target needs its API level in the triple, e.g. -mtriple=x86_64-linux-android29") + message(FATAL_ERROR "a triple without an API level failed for another reason:\n${run_output}") + endif() + + set(ENV{ANDROID_NDK_HOME} "") + run("--emit=exe for Android with no NDK" FALSE + "${TSLANG}" --emit=exe -mm=none --no-default-lib -mtriple=x86_64-linux-android29 app.ts -o app) + if(NOT run_output MATCHES "linking for Android needs the Android NDK: pass --android-ndk-path or set ANDROID_NDK_HOME") + message(FATAL_ERROR "linking with no NDK failed for another reason:\n${run_output}") + endif() + + message(STATUS "Android code is position independent; a missing API level or NDK is refused") +elseif(MODE STREQUAL "link") + set(abi x86_64) + set(triple x86_64-linux-android29) + set(gc_lib "${REPO_DIR}/3rdParty/gc/android/${abi}/release/lib") + set(runtime_lib "${REPO_DIR}/__build/tslang-runtime/release/android/${abi}") + if(DEFINED ENV{TSLANG_ANDROID_DEFAULT_LIB_PATH}) + set(default_lib "$ENV{TSLANG_ANDROID_DEFAULT_LIB_PATH}") + else() + set(default_lib "${REPO_DIR}/../TypeScriptCompilerDefaultLib/__build/android/${abi}") + endif() + + if("$ENV{ANDROID_NDK_HOME}" STREQUAL "") + message("SKIPPED: ANDROID_NDK_HOME is not set") + return() + endif() + foreach(file "${gc_lib}/libgc.a" "${runtime_lib}/libTypeScriptAsyncRuntime.a" + "${default_lib}/defaultlib/lib/release/gc/libTypeScriptDefaultLib.a") + if(NOT EXISTS "${file}") + message("SKIPPED: ${file} is not built") + return() + endif() + endforeach() + + set(libs "--default-lib-path=${default_lib}" "--gc-lib-path=${gc_lib}" "--tslang-lib-path=${runtime_lib}") + run("--emit=exe for ${triple}" TRUE + "${TSLANG}" --emit=exe --opt -mm=gc -mtriple=${triple} ${libs} app.ts -o app) + run("--emit=dll for ${triple}" TRUE + "${TSLANG}" --emit=dll --opt -mm=gc -mtriple=${triple} ${libs} app.ts -o libapp.so) + + foreach(binary app libapp.so) + file(READ "${WORK_DIR}/${binary}" magic LIMIT 4 HEX) + if(NOT magic STREQUAL "7f454c46") + message(FATAL_ERROR "${binary} is not an ELF file") + endif() + endforeach() + + message(STATUS "an executable and a shared library linked for ${triple}") +else() + message(FATAL_ERROR "MODE must be codegen or link, not '${MODE}'") +endif() diff --git a/tslang/tslang/exe.cpp b/tslang/tslang/exe.cpp index 3c8c78031..e878762e3 100644 --- a/tslang/tslang/exe.cpp +++ b/tslang/tslang/exe.cpp @@ -48,6 +48,7 @@ namespace Dump extern cl::opt llvmlibpath; extern cl::opt tslanglibpath; extern cl::opt emsdksysrootpath; +extern cl::opt androidndkpath; extern cl::opt enableOpt; extern cl::list libs; extern cl::list objs; @@ -132,12 +133,23 @@ bool checkFileExistsAtPath(std::string path, std::string fileName) // Windows x86 takes its libraries from an `x86` subdirectory of each configured lib path, so the // flat path has none of them by design; buildExe checks the subdirectory instead (see // resolveWindowsLibPath). Every other target keeps the flat layout. +static llvm::Triple getLinkTargetTriple() +{ + return llvm::Triple(TargetTriple.empty() ? llvm::sys::getDefaultTargetTriple() : llvm::Triple::normalize(TargetTriple)); +} + static bool isWindowsX86Target() { - llvm::Triple triple(TargetTriple.empty() ? llvm::sys::getDefaultTargetTriple() : llvm::Triple::normalize(TargetTriple)); + auto triple = getLinkTargetTriple(); return triple.getOS() == llvm::Triple::Win32 && triple.getArch() == llvm::Triple::x86; } +// A library's file name as the target names it, not the host: gc.lib on Windows, libgc.a elsewhere. +static std::string getTargetStaticLibName(llvm::StringRef name) +{ + return getLinkTargetTriple().getOS() == llvm::Triple::Win32 ? (name + ".lib").str() : ("lib" + name + ".a").str(); +} + // The directory a lib path's libraries are in: its `x86` subdirectory for Windows x86, else itself. static std::string getTargetLibDir(llvm::StringRef path, bool windowsX86) { @@ -157,12 +169,7 @@ void checkGCLibPath(std::string path) return; } -#ifdef WIN32 - const auto libName = "gc.lib"; -#else - const auto libName = "libgc.a"; -#endif - checkFileExistsAtPath(path, libName); + checkFileExistsAtPath(path, getTargetStaticLibName("gc")); } void checkTslangLibPath(std::string path) @@ -172,12 +179,7 @@ void checkTslangLibPath(std::string path) return; } -#ifdef WIN32 - const auto libName = "TypeScriptAsyncRuntime.lib"; -#else - const auto libName = "libTypeScriptAsyncRuntime.a"; -#endif - checkFileExistsAtPath(path, libName); + checkFileExistsAtPath(path, getTargetStaticLibName("TypeScriptAsyncRuntime")); } std::string getGCLibPath() @@ -312,12 +314,90 @@ std::string getEMSDKSysRootPath() return emsdksysrootpath; } - if (auto emsdksysrootpathEnvValue = llvm::sys::Process::GetEnv("EMSDK_SYSROOT_PATH")) + if (auto emsdksysrootpathEnvValue = llvm::sys::Process::GetEnv("EMSDK_SYSROOT_PATH")) { return emsdksysrootpathEnvValue.value(); - } + } - return ""; + return ""; +} + +std::string getAndroidNDKPath() +{ + if (!androidndkpath.empty()) + { + return androidndkpath; + } + + if (auto androidNDKEnvValue = llvm::sys::Process::GetEnv("ANDROID_NDK_HOME")) + { + return androidNDKEnvValue.value(); + } + + return ""; +} + +// The subdirectory of `dir` with the highest version-like name: one per NDK host (windows-x86_64, +// linux-x86_64, ...) under prebuilt, one per clang version under lib/clang. Empty when there is none. +static std::string findHighestSubdirectory(llvm::StringRef dir) +{ + std::string best; + unsigned bestVersion = 0; + std::error_code ec; + for (llvm::sys::fs::directory_iterator it(dir, ec), end; it != end && !ec; it.increment(ec)) + { + if (!llvm::sys::fs::is_directory(it->path())) + { + continue; + } + + auto name = llvm::sys::path::filename(it->path()); + unsigned version = 0; + name.getAsInteger(10, version); + if (best.empty() || version > bestVersion) + { + best = it->path(); + bestVersion = version; + } + } + + return best; +} + +// What the driver needs from the NDK to link for Android: the sysroot (Bionic's startup objects +// and libraries, per API level, and libc++) and the clang resource directory (compiler-rt's +// builtins and libunwind). The NDK's own clang version, whatever it is, not tslang's. +static bool findAndroidNDK(std::string &sysroot, std::string &resourceDir) +{ + auto ndkPath = getAndroidNDKPath(); + if (ndkPath.empty()) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "linking for Android needs the Android NDK: pass --android-ndk-path or set ANDROID_NDK_HOME\n"; + return false; + } + + llvm::SmallString<256> prebuilt(ndkPath); + llvm::sys::path::append(prebuilt, "toolchains", "llvm", "prebuilt"); + auto host = findHighestSubdirectory(prebuilt); + + llvm::SmallString<256> sysrootPath(host); + llvm::sys::path::append(sysrootPath, "sysroot"); + + llvm::SmallString<256> clangDir(host); + llvm::sys::path::append(clangDir, "lib", "clang"); + auto clangVersionDir = host.empty() ? std::string() : findHighestSubdirectory(clangDir); + + if (host.empty() || !llvm::sys::fs::is_directory(sysrootPath) || clangVersionDir.empty()) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "'" << ndkPath << "' is not an Android NDK: no toolchains/llvm/prebuilt/ with a sysroot and lib/clang/\n"; + return false; + } + + sysroot = sysrootPath.str().str(); + resourceDir = clangVersionDir; + return true; } std::string concatIfNotEmpty(const char *prefix, std::string path) @@ -608,7 +688,42 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio if (wasm) { - isTslangLibNeeded = false; + isTslangLibNeeded = false; + } + + // Android links with the NDK's sysroot and runtime libraries, everything into one + // position-independent binary (executables must be PIE; obj.cpp defaults the objects to PIC). + auto android = TheTriple.isAndroid(); + std::string androidSysrootOpt; + std::string androidResourceDirOpt; + if (android) + { + // The API level picks the sysroot's library directory (usr/lib//); without + // one the driver takes the oldest, which lacks what the default library calls. + if (TheTriple.getEnvironmentVersion().getMajor() == 0) + { + llvm::WithColor::error(llvm::errs(), "tslang") + << "an Android target needs its API level in the triple, e.g. -mtriple=" << TheTriple.getArchName() + << "-linux-android29\n"; + return 1; + } + + std::string sysroot; + std::string resourceDir; + if (!findAndroidNDK(sysroot, resourceDir)) + { + return 1; + } + + androidSysrootOpt = "--sysroot=" + sysroot; + androidResourceDirOpt = "-resource-dir=" + resourceDir; + args.push_back(androidSysrootOpt.c_str()); + args.push_back(androidResourceDirOpt.c_str()); + + if (!RM) + { + RM = llvm::Reloc::PIC_; + } } args.push_back(objFileName.c_str()); @@ -638,7 +753,13 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio if (shared) { args.push_back("-shared"); - if (!win) + if (android) + { + // An app loads the library, so an unresolved symbol would only show when it does + // (UnsatisfiedLinkError); with this it is a link error here instead. + args.push_back("-Wl,--no-undefined"); + } + else if (!win) { // added search path args.push_back("-Wl,-rpath=."); @@ -672,7 +793,11 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio // ...and per memory model: the default lib allocates the way the model it was built for // allocates, so a `gc` build linked into an `-mm=rc` program would drag Boehm in and hand // back objects this program's ownership rules do not describe. See getDefaultLibSubDir. - auto defaultLibSubDir = getDefaultLibSubDir(shared, compileOptions.generateDebugInfo, + // Android: a shared library links the static default library too, so the app ships one + // self-contained .so. Its shared default library leaves GC_* to the process, and an app's + // process (the Java VM) has no collector to give it. + auto sharedDefaultLib = shared && !android; + auto defaultLibSubDir = getDefaultLibSubDir(sharedDefaultLib, compileOptions.generateDebugInfo, memoryModelName(compileOptions.memoryModel), x86DefaultLib ? DEFAULT_LIB_ARCH_X86 : ""); auto defaultLibDir = mergeWithDefaultLibPath(getDefaultLibPath(), defaultLibSubDir); @@ -910,11 +1035,24 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio // TODO: review some options args.push_back("-frtti"); args.push_back("-fexceptions"); - args.push_back("-lstdc++"); - args.push_back("-lm"); - args.push_back("-lpthread"); - args.push_back("-ldl"); - args.push_back("-lrt"); + if (android) + { + // libc++ and libc++abi, statically (the sysroot's libc++.a is a script naming both): the + // binary keeps its C++ runtime to itself and the app ships no libc++_shared.so for it. + // Bionic's libc has pthread and rt in it; there is no libcurl (the default library + // builds http_stub.cpp for Android instead). + args.push_back("-l:libc++.a"); + args.push_back("-lm"); + args.push_back("-ldl"); + } + else + { + args.push_back("-lstdc++"); + args.push_back("-lm"); + args.push_back("-lpthread"); + args.push_back("-ldl"); + args.push_back("-lrt"); + } // The default library's HTTP wrapper (http_linux.cpp) is implemented on // top of libcurl (the Linux counterpart of WinHTTP on Windows, which is @@ -923,7 +1061,7 @@ int buildExe(int argc, char **argv, std::string objFileName, std::string additio // early with the exact install command if the development library is not // present, then still pass -lcurl so the linker error is emitted too if // it really is missing. - if (!compileOptions.noDefaultLib) + if (!compileOptions.noDefaultLib && !android) { static const char *curlSharedLibs[] = { "/usr/lib/x86_64-linux-gnu/libcurl.so", diff --git a/tslang/tslang/obj.cpp b/tslang/tslang/obj.cpp index 560fe8dd1..43351e7be 100644 --- a/tslang/tslang/obj.cpp +++ b/tslang/tslang/obj.cpp @@ -210,6 +210,13 @@ int setupTargetTriple(llvm::Module *llvmModule, std::unique_ptr gcsharedlibpath("gc-shared-lib-path", cl::desc("Shared GC l cl::opt llvmlibpath("llvm-lib-path", cl::desc("LLVM library path (or LLVM_LIB_PATH environmental variable). Not needed any more: programs no longer link an LLVM library. Accepted so older scripts keep working"), cl::value_desc("llvmlibpath"), cl::cat(TypeScriptCompilerBuildCategory)); cl::opt tslanglibpath("tslang-lib-path", cl::desc("TypeScript Compiler Runtime library path. Should point to file 'TypeScriptAsyncRuntime.lib' or TSLANG_LIB_PATH environmental variable"), cl::value_desc("tslanglibpath"), cl::cat(TypeScriptCompilerBuildCategory)); cl::opt emsdksysrootpath("emsdk-sysroot-path", cl::desc("TypeScript Compiler Runtime library path. Should point to dir '<...>/emsdk/upstream/emscripten/cache/sysroot' or EMSDK_SYSROOT_PATH environmental variable. (used when '-mtriple=wasm32-pc-emscripten')"), cl::value_desc("emsdksysrootpath"), cl::cat(TypeScriptCompilerBuildCategory)); +cl::opt androidndkpath("android-ndk-path", cl::desc("Android NDK path, the directory holding 'toolchains/llvm/prebuilt', or ANDROID_NDK_HOME environmental variable. (used when '-mtriple=-linux-android')"), cl::value_desc("androidndkpath"), cl::cat(TypeScriptCompilerBuildCategory)); cl::list libs{"lib", cl::desc("Libraries to link statically. (used in --emit=exe)"), cl::ZeroOrMore, cl::MiscFlags::CommaSeparated, cl::cat(TypeScriptCompilerBuildCategory)}; cl::list objs{"obj", cl::desc("Object files to link statically. (used in --emit=exe and --emit=dll)"), cl::ZeroOrMore, cl::MiscFlags::CommaSeparated, cl::cat(TypeScriptCompilerBuildCategory)};