From ccdd75f7ad162407b38f3f21a9585453becd319c Mon Sep 17 00:00:00 2001 From: ASDAlexander77 Date: Mon, 5 Oct 2026 14:54:10 +0100 Subject: [PATCH] --emit=exe and --emit=dll link for Android A triple -linux-android now links through the Android NDK (--android-ndk-path or ANDROID_NDK_HOME): the driver gets the NDK's sysroot and clang resource directory, the host tag and clang version found by listing them, and from those takes Bionic's startup objects and libraries for the API level, compiler-rt's builtins and libunwind. - Objects and links default to PIC for Android (obj.cpp, buildExe): executables must be PIE, and LLVM's default is static. - libc++ and libc++abi are linked statically (-l:libc++.a); no -lpthread, -lrt (part of Bionic's libc) or -lcurl (the Android default library has http_stub.cpp). - --emit=dll links the static default library, so the .so an app loads is self-contained: the shared one leaves GC_* to the process, and an app's process has no collector. It links with --no-undefined, so a symbol Bionic lacks is a link error rather than an UnsatisfiedLinkError in the app. - A triple with no API level, a missing NDK and a directory that is not one are refused with an error that says so. - checkGCLibPath/checkTslangLibPath name the library as the target does (libgc.a) rather than the host (gc.lib). test-compile-android-codegen (no NDK needed) checks Android objects are PIC and the two errors; test-compile-android-link links an exe and a .so for x86_64 Android, and is skipped without the NDK and the Android libraries. Co-Authored-By: Claude Opus 5.5 --- tslang/test/tester/CMakeLists.txt | 13 ++ tslang/test/tester/android-link.cmake | 131 ++++++++++++++++++ tslang/tslang/exe.cpp | 188 ++++++++++++++++++++++---- tslang/tslang/obj.cpp | 7 + tslang/tslang/tslang.cpp | 1 + 5 files changed, 315 insertions(+), 25 deletions(-) create mode 100644 tslang/test/tester/android-link.cmake 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)};