From 21d28d61f05c979c205a4489e78159b8d40f0981 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 11 Jul 2026 20:53:36 -0700 Subject: [PATCH 01/10] Use Bun's JavaScriptCore build on Android --- .../Source/AppRuntime_JavaScriptCore.cpp | 10 ++ Core/Node-API/CMakeLists.txt | 105 ++++++++++++++++-- .../Include/Engine/JavaScriptCore/napi/env.h | 27 +++++ .../Source/js_native_api_javascriptcore.cc | 4 + .../Source/js_native_api_javascriptcore.h | 73 +++++++++++- Core/Node-API/Source/jsc_android.cpp | 55 +++++++++ Core/Node-API/Source/jsc_android.exports | 8 ++ Core/Node-API/package-jsc.json | 5 - Tests/UnitTests/Android/app/build.gradle | 7 +- 9 files changed, 274 insertions(+), 20 deletions(-) create mode 100644 Core/Node-API/Source/jsc_android.cpp create mode 100644 Core/Node-API/Source/jsc_android.exports delete mode 100644 Core/Node-API/package-jsc.json diff --git a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp index f2483f41..dfe20f20 100644 --- a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp +++ b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp @@ -1,10 +1,17 @@ #include "AppRuntime.h" #include +#if __ANDROID__ +extern "C" void JSCAndroidInitialize(); +#endif + namespace Babylon { void AppRuntime::RunEnvironmentTier(const char*) { +#if __ANDROID__ + JSCAndroidInitialize(); +#endif auto globalContext = JSGlobalContextCreateInGroup(nullptr, nullptr); #if __APPLE__ @@ -18,6 +25,9 @@ namespace Babylon Run(env); +#if __ANDROID__ + Napi::ContextLock contextLock{env}; +#endif JSGlobalContextRelease(globalContext); // Detach must come after JSGlobalContextRelease since it triggers finalizers which require env. diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 5f495695..c15d9fd4 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -68,20 +68,109 @@ if(NAPI_BUILD_ABI) "Source/js_native_api_javascriptcore.h") if(ANDROID) - set(V8_PACKAGE_NAME "jsc-android") - set(JSC_ANDROID_DIR "${CMAKE_CURRENT_BINARY_DIR}/${V8_PACKAGE_NAME}") - napi_install_android_package(jsc "dist/org/webkit/android-jsc" ${JSC_ANDROID_DIR}) + # Bun publishes the JavaScriptCore build used by each release as pinned Android static + # archives. Relink the selected archives into the libjsc.so boundary expected by napi, + # instead of installing an npm package only to unpack its embedded AAR. + set(JSR_JSC_BUN_WEBKIT_REVISION "5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b") + set(JSR_JSC_BUN_WEBKIT_RELEASE "autobuild-${JSR_JSC_BUN_WEBKIT_REVISION}") + set(JSR_JSC_ANDROID_ROOT "" CACHE PATH + "Path to an extracted Bun Android WebKit 'bun-webkit' directory; empty downloads the pinned release") - # Add `JavaScriptCore` prefix to the include path - file(RENAME "${JSC_ANDROID_DIR}/include" "${JSC_ANDROID_DIR}/JavaScriptCore") - file(MAKE_DIRECTORY "${JSC_ANDROID_DIR}/include") - file(RENAME "${JSC_ANDROID_DIR}/JavaScriptCore" "${JSC_ANDROID_DIR}/include/JavaScriptCore") + string(REGEX REPLACE "^android-" "" JSR_JSC_ANDROID_API_LEVEL "${ANDROID_PLATFORM}") + if(JSR_JSC_ANDROID_API_LEVEL LESS 28) + message(FATAL_ERROR + "Bun's pinned Android WebKit archives target API 28; set Android minSdkVersion to 28 or newer") + endif() + + if(ANDROID_ABI STREQUAL "arm64-v8a") + set(JSR_JSC_BUN_WEBKIT_ASSET "bun-webkit-linux-arm64-android.tar.gz") + set(JSR_JSC_BUN_WEBKIT_SHA256 "63c18a18c89dc3e1c0a93107f95290c27fbe310eb90e30dc197a7936804d034b") + elseif(ANDROID_ABI STREQUAL "x86_64") + set(JSR_JSC_BUN_WEBKIT_ASSET "bun-webkit-linux-amd64-android.tar.gz") + set(JSR_JSC_BUN_WEBKIT_SHA256 "c198efb68c17ae65ff66968f7d04bdab4845a5abaefb941b15e0e2b36aaab774") + else() + message(FATAL_ERROR + "Bun Android JavaScriptCore supports only arm64-v8a and x86_64; got '${ANDROID_ABI}'") + endif() + + if(JSR_JSC_ANDROID_ROOT) + set(JSC_ANDROID_DIR "${JSR_JSC_ANDROID_ROOT}") + else() + FetchContent_Declare(bun_webkit_android + URL + "https://github.com/oven-sh/WebKit/releases/download/${JSR_JSC_BUN_WEBKIT_RELEASE}/${JSR_JSC_BUN_WEBKIT_ASSET}" + URL_HASH "SHA256=${JSR_JSC_BUN_WEBKIT_SHA256}") + FetchContent_GetProperties(bun_webkit_android) + if(NOT bun_webkit_android_POPULATED) + message(STATUS + "Downloading Bun WebKit ${JSR_JSC_BUN_WEBKIT_REVISION} for ${ANDROID_ABI}") + FetchContent_Populate(bun_webkit_android) + endif() + # FetchContent strips the archive's single top-level bun-webkit directory. + set(JSC_ANDROID_DIR "${bun_webkit_android_SOURCE_DIR}") + endif() + + set(JSC_ANDROID_STATIC_LIBRARIES + "${JSC_ANDROID_DIR}/lib/libJavaScriptCore.a" + "${JSC_ANDROID_DIR}/lib/libWTF.a" + "${JSC_ANDROID_DIR}/lib/libbmalloc.a" + "${JSC_ANDROID_DIR}/lib/libicui18n.a" + "${JSC_ANDROID_DIR}/lib/libicuuc.a" + "${JSC_ANDROID_DIR}/lib/libicudata.a") + + foreach(JSC_ANDROID_FILE + "${JSC_ANDROID_DIR}/include/JavaScriptCore/JavaScript.h" + ${JSC_ANDROID_STATIC_LIBRARIES}) + if(NOT EXISTS "${JSC_ANDROID_FILE}") + message(FATAL_ERROR "Bun Android WebKit archive is missing '${JSC_ANDROID_FILE}'") + endif() + endforeach() set(INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} PUBLIC "${JSC_ANDROID_DIR}/include") + add_library(jsc_android SHARED "Source/jsc_android.cpp") + set_target_properties(jsc_android PROPERTIES + OUTPUT_NAME jsc + CXX_STANDARD 23 + CXX_STANDARD_REQUIRED ON + LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_android.exports") + target_compile_definitions(jsc_android PRIVATE + JSR_JSC_BUN_WEBKIT_REVISION="${JSR_JSC_BUN_WEBKIT_REVISION}" + BUILDING_JSCONLY__ + BUILDING_JavaScriptCore + BUILDING_WEBKIT=1 + BUILDING_WITH_CMAKE=1 + HAVE_CONFIG_H=1 + PAS_BMALLOC=1 + STATICALLY_LINKED_WITH_WTF + STATICALLY_LINKED_WITH_bmalloc + U_STATIC_IMPLEMENTATION=1) + target_include_directories(jsc_android PUBLIC "${JSC_ANDROID_DIR}/include") + target_link_options(jsc_android PRIVATE + "-Wl,--no-undefined" + "-Wl,-Bsymbolic" + "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_android.exports" + "-Wl,-z,max-page-size=16384" + "-Wl,-z,common-page-size=16384") + target_link_libraries(jsc_android PRIVATE + "-Wl,--whole-archive" + "${JSC_ANDROID_DIR}/lib/libJavaScriptCore.a" + "-Wl,--no-whole-archive" + "-Wl,--start-group" + "${JSC_ANDROID_DIR}/lib/libWTF.a" + "${JSC_ANDROID_DIR}/lib/libbmalloc.a" + "${JSC_ANDROID_DIR}/lib/libicui18n.a" + "${JSC_ANDROID_DIR}/lib/libicuuc.a" + "${JSC_ANDROID_DIR}/lib/libicudata.a" + "-Wl,--end-group" + log + dl + m) + set_property(TARGET jsc_android PROPERTY FOLDER Dependencies) + set(LINK_LIBRARIES ${LINK_LIBRARIES} - PUBLIC "${JSC_ANDROID_DIR}/jni/${ANDROID_ABI}/libjsc.so") + PUBLIC jsc_android) elseif(APPLE) find_library(JAVASCRIPTCORE_LIBRARY JavaScriptCore) set(LINK_LIBRARIES ${LINK_LIBRARIES} diff --git a/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h b/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h index 64184b36..9d94d78f 100644 --- a/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h +++ b/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h @@ -3,6 +3,11 @@ #include #include +#if __ANDROID__ +extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context); +extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock); +#endif + namespace Napi { Napi::Env Attach(JSGlobalContextRef); @@ -12,4 +17,26 @@ namespace Napi Napi::Value Eval(Napi::Env env, const char* source, const char* sourceUrl); JSGlobalContextRef GetContext(Napi::Env); + +#if __ANDROID__ + class ContextLock final + { + public: + explicit ContextLock(Napi::Env env) + : m_opaqueLock{JSCAndroidAcquireContextLock(GetContext(env))} + { + } + + ~ContextLock() + { + JSCAndroidReleaseContextLock(m_opaqueLock); + } + + ContextLock(const ContextLock&) = delete; + ContextLock& operator=(const ContextLock&) = delete; + + private: + void* m_opaqueLock{}; + }; +#endif } diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.cc b/Core/Node-API/Source/js_native_api_javascriptcore.cc index 5c8583bc..9e209a3d 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.cc +++ b/Core/Node-API/Source/js_native_api_javascriptcore.cc @@ -740,6 +740,9 @@ struct napi_ref__ { CHECK_NAPI(ReferenceInfo::GetObjectId(env, _value, &_objectId)); if (_objectId == 0) { CHECK_NAPI(ReferenceInfo::Initialize(env, _value, [value = _value](ReferenceInfo* info) { + if (info->Env()->shutting_down) { + return; + } auto entry{info->Env()->active_ref_values.find(value)}; // NOTE: The finalizer callback is actually on a "sentinel" JS object that is linked to the // actual JS object we are trying to track. This means it is possible for the tracked object @@ -1907,6 +1910,7 @@ napi_status napi_get_value_string_utf16(napi_env env, napi_status napi_coerce_to_bool(napi_env env, napi_value value, napi_value* result) { + CHECK_ENV(env); CHECK_ARG(env, result); *result = ToNapi(JSValueMakeBoolean(env->context, JSValueToBoolean(env->context, ToJSValue(value)))); diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index 8d8dbd02..57251269 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -9,12 +9,55 @@ #include #include +#if __ANDROID__ +extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context); +extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock); +extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context); +extern "C" void JSCAndroidUnlockContext(JSGlobalContextRef context); + +class JSCAndroidContextLock final { + public: + explicit JSCAndroidContextLock(JSGlobalContextRef context) + : lock{JSCAndroidAcquireContextLock(context)} {} + + ~JSCAndroidContextLock() { + JSCAndroidReleaseContextLock(lock); + } + + JSCAndroidContextLock(const JSCAndroidContextLock&) = delete; + JSCAndroidContextLock& operator=(const JSCAndroidContextLock&) = delete; + + private: + void* lock{}; +}; + +class JSCAndroidAPILock final { + public: + explicit JSCAndroidAPILock(JSGlobalContextRef context) + : context{context}, locked{JSCAndroidLockContext(context)} {} + + ~JSCAndroidAPILock() { + if (locked) { + JSCAndroidUnlockContext(context); + } + } + + JSCAndroidAPILock(const JSCAndroidAPILock&) = delete; + JSCAndroidAPILock& operator=(const JSCAndroidAPILock&) = delete; + + private: + JSGlobalContextRef context{}; + bool locked{}; +}; +#endif + struct napi_env__ { JSGlobalContextRef context{}; JSValueRef last_exception{}; napi_extended_error_info last_error{nullptr, nullptr, 0, napi_ok}; std::unordered_map active_ref_values{}; std::list strong_refs{}; + bool shutting_down{false}; JSValueRef constructor_info_symbol{}; JSValueRef function_info_symbol{}; @@ -32,6 +75,9 @@ struct napi_env__ { const std::thread::id thread_id{std::this_thread::get_id()}; napi_env__(JSGlobalContextRef context) : context{context} { +#if __ANDROID__ + JSCAndroidContextLock contextLock{context}; +#endif napi_envs[context] = this; JSGlobalContextRetain(context); init_symbol(constructor_info_symbol, "BabylonNative_ConstructorInfo"); @@ -41,6 +87,10 @@ struct napi_env__ { } ~napi_env__() { +#if __ANDROID__ + JSCAndroidContextLock contextLock{context}; +#endif + shutting_down = true; deinit_refs(); deinit_symbol(wrapper_info_symbol); deinit_symbol(reference_info_symbol); @@ -74,13 +124,24 @@ struct napi_env__ { } \ } while (0) -#define CHECK_ENV(env) \ - do { \ - if ((env) == nullptr) { \ - return napi_invalid_arg; \ - } \ - assert(env->thread_id == std::this_thread::get_id()); \ +#if __ANDROID__ +#define CHECK_ENV(env) \ + do { \ + if ((env) == nullptr) { \ + return napi_invalid_arg; \ + } \ + } while (0); \ + JSCAndroidAPILock jscAndroidAPILock{(env)->context}; \ + assert((env)->thread_id == std::this_thread::get_id()) +#else +#define CHECK_ENV(env) \ + do { \ + if ((env) == nullptr) { \ + return napi_invalid_arg; \ + } \ + assert((env)->thread_id == std::this_thread::get_id()); \ } while (0) +#endif #define CHECK_ARG(env, arg) \ RETURN_STATUS_IF_FALSE((env), ((arg) != nullptr), napi_invalid_arg) diff --git a/Core/Node-API/Source/jsc_android.cpp b/Core/Node-API/Source/jsc_android.cpp new file mode 100644 index 00000000..389dff90 --- /dev/null +++ b/Core/Node-API/Source/jsc_android.cpp @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#ifndef JSR_JSC_BUN_WEBKIT_REVISION +#error JSR_JSC_BUN_WEBKIT_REVISION must identify the linked Bun WebKit build +#endif + +#include +#include +#include +#include + +extern "C" void JSCAndroidInitialize() +{ + // Match the standalone JavaScriptCore C API initialization path. In particular, do not call + // WTF::initializeMainThread(): AppRuntime runs JavaScript on a worker thread, while Android may + // load this library and initialize other WTF thread-local state on the instrumentation thread. + JSC::initialize(); +} + +extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context) +{ + // Bun's WebKit fork removes the per-call JSLockHolder instances from the C API and expects its + // embedding event loop to hold the VM lock. This holder is used only for lifecycle operations + // that must also retain the VM, such as environment teardown and the final context release. + auto* vm = toJS(JSContextGetGroup(context)); + return new JSC::JSLockHolder(*vm); +} + +extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock) +{ + delete static_cast(opaqueLock); +} + +extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context) +{ + auto* vm = toJS(JSContextGetGroup(context)); + if (vm->currentThreadIsHoldingAPILock()) + { + return false; + } + + vm->apiLock().lock(); + return true; +} + +extern "C" void JSCAndroidUnlockContext(JSGlobalContextRef context) +{ + toJS(JSContextGetGroup(context))->apiLock().unlock(); +} + +extern "C" const char* JSCAndroidGetWebKitRevision() +{ + return JSR_JSC_BUN_WEBKIT_REVISION; +} diff --git a/Core/Node-API/Source/jsc_android.exports b/Core/Node-API/Source/jsc_android.exports new file mode 100644 index 00000000..47b182c0 --- /dev/null +++ b/Core/Node-API/Source/jsc_android.exports @@ -0,0 +1,8 @@ +{ + global: + JS*; + JSCAndroid*; + kJS*; + local: + *; +}; diff --git a/Core/Node-API/package-jsc.json b/Core/Node-API/package-jsc.json deleted file mode 100644 index d9f70a7a..00000000 --- a/Core/Node-API/package-jsc.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "dependencies": { - "jsc-android": "250231.0.0" - } -} diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index c6137034..b4cc7768 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -7,11 +7,16 @@ if (project.hasProperty("jsEngine")) { jsEngine = project.property("jsEngine") } +def appMinSdk = jsEngine == "JavaScriptCore" ? 28 : 21 + def cmakeArguments = [ "-DANDROID_STL=c++_shared", "-DNAPI_JAVASCRIPT_ENGINE=${jsEngine}", "-DJSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR=ON" ] +if (project.hasProperty("jscAndroidRoot")) { + cmakeArguments.add("-DJSR_JSC_ANDROID_ROOT=${project.property('jscAndroidRoot')}") +} // Hermes for Android needs HOST hermesc/shermes (they emit bytecode at build // time and can't run as NDK-cross-compiled binaries). The JsRuntimeHost CMake // bootstraps these host compilers automatically when cross-compiling, so no @@ -32,7 +37,7 @@ android { defaultConfig { applicationId "com.jsruntimehost.unittests" - minSdk 21 + minSdk appMinSdk targetSdk 33 versionCode 1 versionName "1.0" From 86bd72e4c64b90744e1b497184f0c432d4d871ea Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sat, 11 Jul 2026 23:13:39 -0700 Subject: [PATCH 02/10] Use Bun's LTO JavaScriptCore build on Linux --- .github/tsan_suppressions.txt | 12 +- .github/workflows/build-linux.yml | 54 ++- .github/workflows/ci.yml | 16 +- .../Source/AppRuntime_JavaScriptCore.cpp | 21 +- Core/Node-API/CMakeLists.txt | 344 +++++++++++++----- .../Include/Engine/JavaScriptCore/napi/env.h | 12 +- .../Source/js_native_api_javascriptcore.h | 71 ++-- .../Source/{jsc_android.cpp => jsc_bun.cpp} | 24 +- .../{jsc_android.exports => jsc_bun.exports} | 2 +- Tests/UnitTests/Android/app/build.gradle | 6 +- 10 files changed, 394 insertions(+), 168 deletions(-) rename Core/Node-API/Source/{jsc_android.cpp => jsc_bun.cpp} (58%) rename Core/Node-API/Source/{jsc_android.exports => jsc_bun.exports} (75%) diff --git a/.github/tsan_suppressions.txt b/.github/tsan_suppressions.txt index eebd4661..b17ed653 100644 --- a/.github/tsan_suppressions.txt +++ b/.github/tsan_suppressions.txt @@ -2,16 +2,16 @@ # # These suppress data races internal to third-party libraries that we cannot fix. # TSan on macOS (JavaScriptCore via Xcode) passes clean — these suppressions are -# only needed for the Ubuntu JSC build (libjavascriptcoregtk). +# only needed for the Ubuntu Bun JSC build. -# JavaScriptCore internal races in libjavascriptcoregtk. +# Bun's Linux JavaScriptCore is statically linked into the final executable, so +# a called_from_lib suppression cannot identify it. Match the interceptor frames +# previously observed on JSC's JIT worker threads instead. # Races manifest in TSan interceptors (free/malloc/memcpy/close) called from -# JSC's JIT worker threads. function-name suppressions are needed because the -# top frame is a libc interceptor attributed to UnitTests, not libjavascriptcoregtk. -called_from_lib:libjavascriptcoregtk +# JSC's JIT worker threads. race:free race:close race:memcpy # JSC signal handler that doesn't save/restore errno -signal:libjavascriptcoregtk +signal:WTF::jscSignalHandler diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 3357bf59..078155d1 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -6,11 +6,11 @@ on: cc: required: false type: string - default: gcc + default: clang-21 cxx: required: false type: string - default: g++ + default: clang++-21 js-engine: required: false type: string @@ -26,8 +26,10 @@ on: jobs: build: - runs-on: ubuntu-latest - timeout-minutes: 15 + # Bun's JSC requires GLIBCXX_3.4.30. Building on 22.04 preserves that practical + # runtime floor instead of accidentally importing newer glibc symbols. + runs-on: ubuntu-22.04 + timeout-minutes: 30 env: CC: ${{ inputs.cc }} CXX: ${{ inputs.cxx }} @@ -37,7 +39,22 @@ jobs: - name: Install packages run: | sudo apt-get update - sudo apt-get install -y libjavascriptcoregtk-4.1-dev libcurl4-openssl-dev ninja-build clang + sudo apt-get install -y --no-install-recommends ca-certificates gnupg libcurl4-openssl-dev libstdc++-12-dev ninja-build wget + + - name: Install LLVM 21 for Bun JavaScriptCore LTO + if: inputs.cc == 'clang-21' || inputs.cxx == 'clang++-21' + run: | + wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/apt.llvm.org.gpg + echo "deb [signed-by=/usr/share/keyrings/apt.llvm.org.gpg] https://apt.llvm.org/jammy/ llvm-toolchain-jammy-21 main" | sudo tee /etc/apt/sources.list.d/llvm21.list + sudo apt-get update + sudo apt-get install -y --no-install-recommends clang-21 lld-21 + + - name: Install Clang sanitizer runtimes + if: (inputs.enable-sanitizers || inputs.enable-thread-sanitizer) && (inputs.cc == 'clang-21' || inputs.cxx == 'clang++-21') + run: sudo apt-get install -y --no-install-recommends libclang-rt-21-dev + + - name: Reclaim package indexes + run: sudo rm -rf /var/lib/apt/lists/* - name: Configure CMake run: | @@ -52,6 +69,32 @@ jobs: - name: Build run: ninja -C Build/ubuntu + - name: Audit statically linked Bun JavaScriptCore runtime + if: inputs.js-engine == '' || inputs.js-engine == 'JavaScriptCore' + run: | + jsc_archive=Build/ubuntu/Core/Node-API/libjsruntimehost-jsc.a + executable=Build/ubuntu/Tests/UnitTests/UnitTests + test -f "$jsc_archive" + test -x "$executable" + readelf --file-header "$executable" 2>/dev/null | grep 'DYN (Position-Independent Executable file)' + readelf --dynamic "$executable" 2>/dev/null | grep -E 'FLAGS_1.*PIE' + readelf --syms "$executable" 2>/dev/null | grep JSCBunInitialize + + ldd_output="$(ldd -r "$executable" 2>&1)" + printf '%s\n' "$ldd_output" + if grep -q 'undefined symbol' <<<"$ldd_output"; then + exit 1 + fi + if grep -q 'javascriptcore' <<<"${ldd_output,,}"; then + exit 1 + fi + + max_glibc="$(readelf --version-info "$executable" 2>/dev/null | grep -o 'GLIBC_[0-9.]*' | sort -Vu | tail -n 1)" + max_glibcxx="$(readelf --version-info "$executable" 2>/dev/null | grep -o 'GLIBCXX_[0-9.]*' | sort -Vu | tail -n 1)" + echo "Maximum required symbol versions: $max_glibc, $max_glibcxx" + dpkg --compare-versions "${max_glibc#GLIBC_}" le 2.35 + dpkg --compare-versions "${max_glibcxx#GLIBCXX_}" le 3.4.30 + - name: Run Tests working-directory: Build/ubuntu/Tests/UnitTests run: ./UnitTests @@ -64,4 +107,3 @@ jobs: # on the mutator without cross-thread signals. macOS JSC uses Mach # thread_suspend() and is unaffected. JSC_useConcurrentGC: ${{ inputs.enable-thread-sanitizer && '0' || '' }} - diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fa226b76..5061e9fa 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -128,30 +128,22 @@ jobs: simulator: 'iPhone 17' # Linux - Ubuntu_gcc: + Ubuntu_JSC_LTO: uses: ./.github/workflows/build-linux.yml - Ubuntu_clang: - uses: ./.github/workflows/build-linux.yml - with: - cc: clang - cxx: clang++ - - Ubuntu_QuickJS: + Ubuntu_QuickJS_gcc: uses: ./.github/workflows/build-linux.yml with: + cc: gcc + cxx: g++ js-engine: QuickJS Ubuntu_Sanitizers_clang: uses: ./.github/workflows/build-linux.yml with: - cc: clang - cxx: clang++ enable-sanitizers: true Ubuntu_ThreadSanitizer_clang: uses: ./.github/workflows/build-linux.yml with: - cc: clang - cxx: clang++ enable-thread-sanitizer: true diff --git a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp index dfe20f20..23c3d317 100644 --- a/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp +++ b/Core/AppRuntime/Source/AppRuntime_JavaScriptCore.cpp @@ -1,16 +1,16 @@ #include "AppRuntime.h" #include -#if __ANDROID__ -extern "C" void JSCAndroidInitialize(); +#if defined(JSR_USE_BUN_JSC) +extern "C" void JSCBunInitialize(); #endif namespace Babylon { void AppRuntime::RunEnvironmentTier(const char*) { -#if __ANDROID__ - JSCAndroidInitialize(); +#if defined(JSR_USE_BUN_JSC) + JSCBunInitialize(); #endif auto globalContext = JSGlobalContextCreateInGroup(nullptr, nullptr); @@ -25,10 +25,17 @@ namespace Babylon Run(env); -#if __ANDROID__ - Napi::ContextLock contextLock{env}; +#if defined(JSR_USE_BUN_JSC) + { + // Scope this holder to the host's context reference. Keeping it alive across Detach + // delays VM destruction until after napi_env has been deleted, but JSC's last-chance + // finalizers are allowed to call Node-API with that environment. + Napi::ContextLock contextLock{env}; +#endif + JSGlobalContextRelease(globalContext); +#if defined(JSR_USE_BUN_JSC) + } #endif - JSGlobalContextRelease(globalContext); // Detach must come after JSGlobalContextRelease since it triggers finalizers which require env. Napi::Detach(env); diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index c15d9fd4..4e1bd160 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -7,7 +7,6 @@ elseif(ANDROID) set(NAPI_JAVASCRIPT_ENGINE "V8" CACHE STRING "JavaScript engine for Node-API") elseif(UNIX) set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") - set(JAVASCRIPTCORE_LIBRARY "/usr/lib/x86_64-linux-gnu/libjavascriptcoregtk-4.1.so" CACHE STRING "Path to the JavaScriptCore shared library") else() message(FATAL_ERROR "Unable to select Node-API JavaScript engine for platform") endif() @@ -67,76 +66,209 @@ if(NAPI_BUILD_ABI) "Source/js_native_api_javascriptcore.cc" "Source/js_native_api_javascriptcore.h") - if(ANDROID) - # Bun publishes the JavaScriptCore build used by each release as pinned Android static - # archives. Relink the selected archives into the libjsc.so boundary expected by napi, - # instead of installing an npm package only to unpack its embedded AAR. - set(JSR_JSC_BUN_WEBKIT_REVISION "5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b") - set(JSR_JSC_BUN_WEBKIT_RELEASE "autobuild-${JSR_JSC_BUN_WEBKIT_REVISION}") - set(JSR_JSC_ANDROID_ROOT "" CACHE PATH - "Path to an extracted Bun Android WebKit 'bun-webkit' directory; empty downloads the pinned release") - - string(REGEX REPLACE "^android-" "" JSR_JSC_ANDROID_API_LEVEL "${ANDROID_PLATFORM}") - if(JSR_JSC_ANDROID_API_LEVEL LESS 28) - message(FATAL_ERROR - "Bun's pinned Android WebKit archives target API 28; set Android minSdkVersion to 28 or newer") - endif() + if(ANDROID OR CMAKE_SYSTEM_NAME STREQUAL "Linux") + # Bun publishes the JavaScriptCore build used by each release as pinned static + # archives. Download only headers and link inputs, then use Android's libjsc.so + # boundary or Linux's final LTO executable link instead of maintaining a separate + # JSC build/package pipeline. + set(JSR_BUN_JSC_WEBKIT_REVISION "5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b") + set(JSR_BUN_JSC_WEBKIT_RELEASE "autobuild-${JSR_BUN_JSC_WEBKIT_REVISION}") + set(JSR_BUN_JSC_ROOT "" CACHE PATH + "Path to an extracted Bun WebKit 'bun-webkit' directory; empty downloads the pinned release") + + if(ANDROID) + set(JSR_BUN_JSC_ANDROID_NDK_VERSION "${CMAKE_ANDROID_NDK_VERSION}") + if(NOT JSR_BUN_JSC_ANDROID_NDK_VERSION) + set(JSR_BUN_JSC_ANDROID_NDK_ROOT "${CMAKE_ANDROID_NDK}") + if(NOT JSR_BUN_JSC_ANDROID_NDK_ROOT) + set(JSR_BUN_JSC_ANDROID_NDK_ROOT "${ANDROID_NDK}") + endif() + if(EXISTS "${JSR_BUN_JSC_ANDROID_NDK_ROOT}/source.properties") + file(STRINGS "${JSR_BUN_JSC_ANDROID_NDK_ROOT}/source.properties" + JSR_BUN_JSC_ANDROID_NDK_REVISION_LINE + REGEX "^Pkg\\.Revision = ") + string(REGEX REPLACE "^Pkg\\.Revision = ([0-9]+\\.[0-9]+).*$" "\\1" + JSR_BUN_JSC_ANDROID_NDK_VERSION + "${JSR_BUN_JSC_ANDROID_NDK_REVISION_LINE}") + endif() + endif() + if(NOT JSR_BUN_JSC_ANDROID_NDK_VERSION OR + JSR_BUN_JSC_ANDROID_NDK_VERSION VERSION_LESS "26.1") + message(FATAL_ERROR + "Bun's current WebKit headers require Android NDK r26b or newer for C++23 library support; found r${JSR_BUN_JSC_ANDROID_NDK_VERSION}") + endif() - if(ANDROID_ABI STREQUAL "arm64-v8a") - set(JSR_JSC_BUN_WEBKIT_ASSET "bun-webkit-linux-arm64-android.tar.gz") - set(JSR_JSC_BUN_WEBKIT_SHA256 "63c18a18c89dc3e1c0a93107f95290c27fbe310eb90e30dc197a7936804d034b") - elseif(ANDROID_ABI STREQUAL "x86_64") - set(JSR_JSC_BUN_WEBKIT_ASSET "bun-webkit-linux-amd64-android.tar.gz") - set(JSR_JSC_BUN_WEBKIT_SHA256 "c198efb68c17ae65ff66968f7d04bdab4845a5abaefb941b15e0e2b36aaab774") + string(REGEX REPLACE "^android-" "" JSR_BUN_JSC_ANDROID_API_LEVEL "${ANDROID_PLATFORM}") + if(JSR_BUN_JSC_ANDROID_API_LEVEL LESS 28) + message(FATAL_ERROR + "Bun's pinned Android WebKit archives target API 28; set Android minSdkVersion to 28 or newer") + endif() + + # Bun does not publish Android LTO archives. These are its optimized native + # arm64 and x86_64 builds; the full-LTO variants are Linux-only. + if(ANDROID_ABI STREQUAL "arm64-v8a") + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-android.tar.gz") + set(JSR_BUN_JSC_SHA256 "63c18a18c89dc3e1c0a93107f95290c27fbe310eb90e30dc197a7936804d034b") + elseif(ANDROID_ABI STREQUAL "x86_64") + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-android.tar.gz") + set(JSR_BUN_JSC_SHA256 "c198efb68c17ae65ff66968f7d04bdab4845a5abaefb941b15e0e2b36aaab774") + else() + message(FATAL_ERROR + "Bun Android JavaScriptCore supports only arm64-v8a and x86_64; got '${ANDROID_ABI}'") + endif() + set(JSR_BUN_JSC_OUTPUT_NAME jsc) else() - message(FATAL_ERROR - "Bun Android JavaScriptCore supports only arm64-v8a and x86_64; got '${ANDROID_ABI}'") + include(CheckCXXSourceCompiles) + check_cxx_source_compiles([=[ + #include + #ifndef __GLIBC__ + #error Bun's selected Linux JavaScriptCore archive requires glibc + #endif + int main() { return 0; } + ]=] JSR_BUN_JSC_USES_GLIBC) + if(NOT JSR_BUN_JSC_USES_GLIBC) + message(FATAL_ERROR + "Bun JavaScriptCore currently supports glibc Linux in JsRuntimeHost; musl requires Bun's separate musl archive") + endif() + + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR + NOT CMAKE_CXX_COMPILER_VERSION MATCHES "^21\\.") + message(FATAL_ERROR + "Bun's Linux JavaScriptCore LTO archives require Clang 21") + endif() + + set(JSR_BUN_JSC_SAVED_CXX_STANDARD "${CMAKE_CXX_STANDARD}") + set(CMAKE_CXX_STANDARD 23) + check_cxx_source_compiles([=[ + #include + int main() { std::expected value{1}; return *value - 1; } + ]=] JSR_BUN_JSC_HAS_STD_EXPECTED) + set(CMAKE_CXX_STANDARD "${JSR_BUN_JSC_SAVED_CXX_STANDARD}") + if(NOT JSR_BUN_JSC_HAS_STD_EXPECTED) + message(FATAL_ERROR + "Bun's current WebKit headers require a C++23 standard library with (install libstdc++-12-dev on Ubuntu 22.04)") + endif() + + find_program(JSR_BUN_JSC_LLD NAMES ld.lld-21 ld.lld REQUIRED) + execute_process( + COMMAND "${JSR_BUN_JSC_LLD}" --version + OUTPUT_VARIABLE JSR_BUN_JSC_LLD_VERSION + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT JSR_BUN_JSC_LLD_VERSION MATCHES "LLD 21\\.") + message(FATAL_ERROR + "Bun's Linux JavaScriptCore LTO archives require LLD 21; found '${JSR_BUN_JSC_LLD_VERSION}'") + endif() + + string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" JSR_BUN_JSC_PROCESSOR) + if(JSR_BUN_JSC_PROCESSOR MATCHES "^(x86_64|amd64)$") + # Use Bun's Nehalem-baseline full-LTO variant rather than imposing Haswell on + # every JsRuntimeHost Linux consumer. + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-baseline-lto.tar.gz") + set(JSR_BUN_JSC_SHA256 "2756c59963525ee01f8878c07d5bceff98f6e926ccbe79dff52dfe44f4a0b5c3") + elseif(JSR_BUN_JSC_PROCESSOR MATCHES "^(aarch64|arm64)$") + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-lto.tar.gz") + set(JSR_BUN_JSC_SHA256 "5739c4e6b32258f30db22aeeddefe9dfa3b05d6edb5ea92985b8fbefa85a2cfe") + else() + message(FATAL_ERROR + "Bun Linux JavaScriptCore supports only x86_64 and arm64; got '${CMAKE_SYSTEM_PROCESSOR}'") + endif() + set(JSR_BUN_JSC_OUTPUT_NAME jsruntimehost-jsc) endif() - if(JSR_JSC_ANDROID_ROOT) - set(JSC_ANDROID_DIR "${JSR_JSC_ANDROID_ROOT}") + if(JSR_BUN_JSC_ROOT) + set(BUN_JSC_DIR "${JSR_BUN_JSC_ROOT}") else() - FetchContent_Declare(bun_webkit_android - URL - "https://github.com/oven-sh/WebKit/releases/download/${JSR_JSC_BUN_WEBKIT_RELEASE}/${JSR_JSC_BUN_WEBKIT_ASSET}" - URL_HASH "SHA256=${JSR_JSC_BUN_WEBKIT_SHA256}") - FetchContent_GetProperties(bun_webkit_android) - if(NOT bun_webkit_android_POPULATED) + string(REGEX REPLACE "\\.tar\\.gz$" "" JSR_BUN_JSC_ASSET_STEM "${JSR_BUN_JSC_ASSET}") + set(JSR_BUN_JSC_FETCH_DIR + "${CMAKE_CURRENT_BINARY_DIR}/${JSR_BUN_JSC_WEBKIT_REVISION}/${JSR_BUN_JSC_ASSET_STEM}") + set(BUN_JSC_DIR "${JSR_BUN_JSC_FETCH_DIR}/bun-webkit") + set(JSR_BUN_JSC_ARCHIVE "${JSR_BUN_JSC_FETCH_DIR}/${JSR_BUN_JSC_ASSET}") + set(JSR_BUN_JSC_COMPLETE_STAMP + "${BUN_JSC_DIR}/.jsruntimehost-${JSR_BUN_JSC_SHA256}.complete") + + if(NOT EXISTS "${JSR_BUN_JSC_COMPLETE_STAMP}") + file(REMOVE_RECURSE "${BUN_JSC_DIR}") + file(MAKE_DIRECTORY "${JSR_BUN_JSC_FETCH_DIR}") message(STATUS - "Downloading Bun WebKit ${JSR_JSC_BUN_WEBKIT_REVISION} for ${ANDROID_ABI}") - FetchContent_Populate(bun_webkit_android) + "Downloading Bun WebKit ${JSR_BUN_JSC_WEBKIT_REVISION}: ${JSR_BUN_JSC_ASSET}") + file(DOWNLOAD + "https://github.com/oven-sh/WebKit/releases/download/${JSR_BUN_JSC_WEBKIT_RELEASE}/${JSR_BUN_JSC_ASSET}" + "${JSR_BUN_JSC_ARCHIVE}" + EXPECTED_HASH "SHA256=${JSR_BUN_JSC_SHA256}" + TLS_VERIFY ON + SHOW_PROGRESS + STATUS JSR_BUN_JSC_DOWNLOAD_STATUS) + list(GET JSR_BUN_JSC_DOWNLOAD_STATUS 0 JSR_BUN_JSC_DOWNLOAD_CODE) + if(NOT JSR_BUN_JSC_DOWNLOAD_CODE EQUAL 0) + list(GET JSR_BUN_JSC_DOWNLOAD_STATUS 1 JSR_BUN_JSC_DOWNLOAD_MESSAGE) + message(FATAL_ERROR "Unable to download Bun WebKit: ${JSR_BUN_JSC_DOWNLOAD_MESSAGE}") + endif() + file(ARCHIVE_EXTRACT + INPUT "${JSR_BUN_JSC_ARCHIVE}" + DESTINATION "${JSR_BUN_JSC_FETCH_DIR}" + PATTERNS + "bun-webkit/include" + "bun-webkit/lib/libJavaScriptCore.a" + "bun-webkit/lib/libWTF.a" + "bun-webkit/lib/libbmalloc.a" + "bun-webkit/lib/libicui18n.a" + "bun-webkit/lib/libicuuc.a" + "bun-webkit/lib/libicudata.a" + "bun-webkit/package.json") + + foreach(JSR_BUN_JSC_EXTRACTED_FILE + "${BUN_JSC_DIR}/include/JavaScriptCore/JavaScript.h" + "${BUN_JSC_DIR}/lib/libJavaScriptCore.a" + "${BUN_JSC_DIR}/lib/libWTF.a" + "${BUN_JSC_DIR}/lib/libbmalloc.a" + "${BUN_JSC_DIR}/lib/libicui18n.a" + "${BUN_JSC_DIR}/lib/libicuuc.a" + "${BUN_JSC_DIR}/lib/libicudata.a") + if(NOT EXISTS "${JSR_BUN_JSC_EXTRACTED_FILE}") + message(FATAL_ERROR + "Bun WebKit extraction is missing '${JSR_BUN_JSC_EXTRACTED_FILE}'") + endif() + endforeach() + + file(WRITE "${JSR_BUN_JSC_COMPLETE_STAMP}" + "${JSR_BUN_JSC_WEBKIT_REVISION}\n${JSR_BUN_JSC_SHA256}\n") + file(REMOVE "${JSR_BUN_JSC_ARCHIVE}") endif() - # FetchContent strips the archive's single top-level bun-webkit directory. - set(JSC_ANDROID_DIR "${bun_webkit_android_SOURCE_DIR}") endif() - set(JSC_ANDROID_STATIC_LIBRARIES - "${JSC_ANDROID_DIR}/lib/libJavaScriptCore.a" - "${JSC_ANDROID_DIR}/lib/libWTF.a" - "${JSC_ANDROID_DIR}/lib/libbmalloc.a" - "${JSC_ANDROID_DIR}/lib/libicui18n.a" - "${JSC_ANDROID_DIR}/lib/libicuuc.a" - "${JSC_ANDROID_DIR}/lib/libicudata.a") - - foreach(JSC_ANDROID_FILE - "${JSC_ANDROID_DIR}/include/JavaScriptCore/JavaScript.h" - ${JSC_ANDROID_STATIC_LIBRARIES}) - if(NOT EXISTS "${JSC_ANDROID_FILE}") - message(FATAL_ERROR "Bun Android WebKit archive is missing '${JSC_ANDROID_FILE}'") + set(BUN_JSC_STATIC_LIBRARIES + "${BUN_JSC_DIR}/lib/libJavaScriptCore.a" + "${BUN_JSC_DIR}/lib/libWTF.a" + "${BUN_JSC_DIR}/lib/libbmalloc.a" + "${BUN_JSC_DIR}/lib/libicui18n.a" + "${BUN_JSC_DIR}/lib/libicuuc.a" + "${BUN_JSC_DIR}/lib/libicudata.a") + + foreach(BUN_JSC_FILE + "${BUN_JSC_DIR}/include/JavaScriptCore/JavaScript.h" + ${BUN_JSC_STATIC_LIBRARIES}) + if(NOT EXISTS "${BUN_JSC_FILE}") + message(FATAL_ERROR "Bun WebKit archive is missing '${BUN_JSC_FILE}'") endif() endforeach() set(INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} - PUBLIC "${JSC_ANDROID_DIR}/include") + PUBLIC "${BUN_JSC_DIR}/include") - add_library(jsc_android SHARED "Source/jsc_android.cpp") - set_target_properties(jsc_android PROPERTIES - OUTPUT_NAME jsc + if(ANDROID) + add_library(bun_jsc SHARED "Source/jsc_bun.cpp") + else() + # Bun's Linux LTO archives contain local-exec TLS relocations and are intended to + # be linked into the initial executable. They cannot be repackaged into a shared + # object, so keep this wrapper static and propagate the archives to the final link. + add_library(bun_jsc STATIC "Source/jsc_bun.cpp") + endif() + set_target_properties(bun_jsc PROPERTIES + OUTPUT_NAME "${JSR_BUN_JSC_OUTPUT_NAME}" CXX_STANDARD 23 - CXX_STANDARD_REQUIRED ON - LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_android.exports") - target_compile_definitions(jsc_android PRIVATE - JSR_JSC_BUN_WEBKIT_REVISION="${JSR_JSC_BUN_WEBKIT_REVISION}" + CXX_STANDARD_REQUIRED ON) + target_compile_definitions(bun_jsc PRIVATE + JSR_BUN_JSC_WEBKIT_REVISION="${JSR_BUN_JSC_WEBKIT_REVISION}" BUILDING_JSCONLY__ BUILDING_JavaScriptCore BUILDING_WEBKIT=1 @@ -146,40 +278,73 @@ if(NAPI_BUILD_ABI) STATICALLY_LINKED_WITH_WTF STATICALLY_LINKED_WITH_bmalloc U_STATIC_IMPLEMENTATION=1) - target_include_directories(jsc_android PUBLIC "${JSC_ANDROID_DIR}/include") - target_link_options(jsc_android PRIVATE - "-Wl,--no-undefined" - "-Wl,-Bsymbolic" - "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_android.exports" - "-Wl,-z,max-page-size=16384" - "-Wl,-z,common-page-size=16384") - target_link_libraries(jsc_android PRIVATE - "-Wl,--whole-archive" - "${JSC_ANDROID_DIR}/lib/libJavaScriptCore.a" - "-Wl,--no-whole-archive" - "-Wl,--start-group" - "${JSC_ANDROID_DIR}/lib/libWTF.a" - "${JSC_ANDROID_DIR}/lib/libbmalloc.a" - "${JSC_ANDROID_DIR}/lib/libicui18n.a" - "${JSC_ANDROID_DIR}/lib/libicuuc.a" - "${JSC_ANDROID_DIR}/lib/libicudata.a" - "-Wl,--end-group" - log - dl - m) - set_property(TARGET jsc_android PROPERTY FOLDER Dependencies) + target_include_directories(bun_jsc PUBLIC "${BUN_JSC_DIR}/include") + + if(ANDROID) + set_target_properties(bun_jsc PROPERTIES + LINK_DEPENDS "${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_bun.exports") + target_link_options(bun_jsc PRIVATE + "-Wl,--no-undefined" + "-Wl,-Bsymbolic" + "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/Source/jsc_bun.exports" + "-Wl,-z,max-page-size=16384" + "-Wl,-z,common-page-size=16384") + target_link_libraries(bun_jsc PRIVATE + "-Wl,--whole-archive" + "${BUN_JSC_DIR}/lib/libJavaScriptCore.a" + "-Wl,--no-whole-archive" + "-Wl,--start-group" + "${BUN_JSC_DIR}/lib/libWTF.a" + "${BUN_JSC_DIR}/lib/libbmalloc.a" + "${BUN_JSC_DIR}/lib/libicui18n.a" + "${BUN_JSC_DIR}/lib/libicuuc.a" + "${BUN_JSC_DIR}/lib/libicudata.a" + "-Wl,--end-group" + log + ${CMAKE_DL_LIBS} + m) + else() + set_property(TARGET bun_jsc PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON) + target_compile_options(bun_jsc PRIVATE + -O2 + -flto=full + -fwhole-program-vtables + -fforce-emit-vtables + -fno-sanitize=all) + target_link_options(bun_jsc INTERFACE + -flto=full + -fwhole-program-vtables + -fforce-emit-vtables + -O2 + -pie + "--ld-path=${JSR_BUN_JSC_LLD}" + "-Wl,--gc-sections") + find_package(Threads REQUIRED) + target_link_libraries(bun_jsc PUBLIC + "-Wl,--whole-archive" + "${BUN_JSC_DIR}/lib/libJavaScriptCore.a" + "-Wl,--no-whole-archive" + "-Wl,--start-group" + "${BUN_JSC_DIR}/lib/libWTF.a" + "${BUN_JSC_DIR}/lib/libbmalloc.a" + "${BUN_JSC_DIR}/lib/libicui18n.a" + "${BUN_JSC_DIR}/lib/libicuuc.a" + "${BUN_JSC_DIR}/lib/libicudata.a" + "-Wl,--end-group" + Threads::Threads + ${CMAKE_DL_LIBS} + m + atomic) + endif() + set_property(TARGET bun_jsc PROPERTY FOLDER Dependencies) + set(JSR_USE_BUN_JSC TRUE) set(LINK_LIBRARIES ${LINK_LIBRARIES} - PUBLIC jsc_android) + PUBLIC bun_jsc) elseif(APPLE) find_library(JAVASCRIPTCORE_LIBRARY JavaScriptCore) set(LINK_LIBRARIES ${LINK_LIBRARIES} PUBLIC ${JAVASCRIPTCORE_LIBRARY}) - elseif(UNIX) - set(LINK_LIBRARIES ${LINK_LIBRARIES} - PUBLIC ${JAVASCRIPTCORE_LIBRARY}) - set(INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} - PUBLIC "/usr/include/webkitgtk-4.1/") else() message(FATAL_ERROR "Unsupported JavaScript engine: ${NAPI_JAVASCRIPT_ENGINE}") endif() @@ -355,6 +520,17 @@ add_library(napi ${SOURCES}) target_include_directories(napi ${INCLUDE_DIRECTORIES}) target_link_libraries(napi ${LINK_LIBRARIES}) +if(JSR_USE_BUN_JSC) + target_compile_definitions(napi PUBLIC JSR_USE_BUN_JSC=1) + if(CMAKE_SYSTEM_NAME STREQUAL "Linux") + get_target_property(JSR_BUN_JSC_NAPI_TYPE napi TYPE) + if(NOT JSR_BUN_JSC_NAPI_TYPE STREQUAL "STATIC_LIBRARY") + message(FATAL_ERROR + "Bun's Linux JavaScriptCore LTO archives must reach the final executable; build JsRuntimeHost static (BUILD_SHARED_LIBS=OFF)") + endif() + endif() +endif() + if(NAPI_JAVASCRIPT_ENGINE STREQUAL "Hermes") # Apply Hermes-specific warning suppressions ONLY to env_hermes.cc so # they don't relax the rules for the rest of the napi sources. diff --git a/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h b/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h index 9d94d78f..9c1584f6 100644 --- a/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h +++ b/Core/Node-API/Include/Engine/JavaScriptCore/napi/env.h @@ -3,9 +3,9 @@ #include #include -#if __ANDROID__ -extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context); -extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock); +#if defined(JSR_USE_BUN_JSC) +extern "C" void* JSCBunAcquireContextLock(JSGlobalContextRef context); +extern "C" void JSCBunReleaseContextLock(void* opaqueLock); #endif namespace Napi @@ -18,18 +18,18 @@ namespace Napi JSGlobalContextRef GetContext(Napi::Env); -#if __ANDROID__ +#if defined(JSR_USE_BUN_JSC) class ContextLock final { public: explicit ContextLock(Napi::Env env) - : m_opaqueLock{JSCAndroidAcquireContextLock(GetContext(env))} + : m_opaqueLock{JSCBunAcquireContextLock(GetContext(env))} { } ~ContextLock() { - JSCAndroidReleaseContextLock(m_opaqueLock); + JSCBunReleaseContextLock(m_opaqueLock); } ContextLock(const ContextLock&) = delete; diff --git a/Core/Node-API/Source/js_native_api_javascriptcore.h b/Core/Node-API/Source/js_native_api_javascriptcore.h index 57251269..eb46735d 100644 --- a/Core/Node-API/Source/js_native_api_javascriptcore.h +++ b/Core/Node-API/Source/js_native_api_javascriptcore.h @@ -9,41 +9,41 @@ #include #include -#if __ANDROID__ -extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context); -extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock); -extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context); -extern "C" void JSCAndroidUnlockContext(JSGlobalContextRef context); +#if defined(JSR_USE_BUN_JSC) +extern "C" void* JSCBunAcquireContextLock(JSGlobalContextRef context); +extern "C" void JSCBunReleaseContextLock(void* opaqueLock); +extern "C" bool JSCBunLockContext(JSGlobalContextRef context); +extern "C" void JSCBunUnlockContext(JSGlobalContextRef context); -class JSCAndroidContextLock final { +class JSCBunContextLock final { public: - explicit JSCAndroidContextLock(JSGlobalContextRef context) - : lock{JSCAndroidAcquireContextLock(context)} {} + explicit JSCBunContextLock(JSGlobalContextRef context) + : lock{JSCBunAcquireContextLock(context)} {} - ~JSCAndroidContextLock() { - JSCAndroidReleaseContextLock(lock); + ~JSCBunContextLock() { + JSCBunReleaseContextLock(lock); } - JSCAndroidContextLock(const JSCAndroidContextLock&) = delete; - JSCAndroidContextLock& operator=(const JSCAndroidContextLock&) = delete; + JSCBunContextLock(const JSCBunContextLock&) = delete; + JSCBunContextLock& operator=(const JSCBunContextLock&) = delete; private: void* lock{}; }; -class JSCAndroidAPILock final { +class JSCBunAPILock final { public: - explicit JSCAndroidAPILock(JSGlobalContextRef context) - : context{context}, locked{JSCAndroidLockContext(context)} {} + explicit JSCBunAPILock(JSGlobalContextRef context, bool acquire = true) + : context{context}, locked{acquire && JSCBunLockContext(context)} {} - ~JSCAndroidAPILock() { + ~JSCBunAPILock() { if (locked) { - JSCAndroidUnlockContext(context); + JSCBunUnlockContext(context); } } - JSCAndroidAPILock(const JSCAndroidAPILock&) = delete; - JSCAndroidAPILock& operator=(const JSCAndroidAPILock&) = delete; + JSCBunAPILock(const JSCBunAPILock&) = delete; + JSCBunAPILock& operator=(const JSCBunAPILock&) = delete; private: JSGlobalContextRef context{}; @@ -75,8 +75,8 @@ struct napi_env__ { const std::thread::id thread_id{std::this_thread::get_id()}; napi_env__(JSGlobalContextRef context) : context{context} { -#if __ANDROID__ - JSCAndroidContextLock contextLock{context}; +#if defined(JSR_USE_BUN_JSC) + JSCBunContextLock contextLock{context}; #endif napi_envs[context] = this; JSGlobalContextRetain(context); @@ -87,16 +87,22 @@ struct napi_env__ { } ~napi_env__() { -#if __ANDROID__ - JSCAndroidContextLock contextLock{context}; +#if defined(JSR_USE_BUN_JSC) + { + // Releasing this holder may destroy the VM and run last-chance finalizers. Keep both this + // environment and its context lookup registered until those finalizers have completed. + JSCBunContextLock contextLock{context}; +#endif + shutting_down = true; + deinit_refs(); + deinit_symbol(wrapper_info_symbol); + deinit_symbol(reference_info_symbol); + deinit_symbol(function_info_symbol); + deinit_symbol(constructor_info_symbol); + JSGlobalContextRelease(context); +#if defined(JSR_USE_BUN_JSC) + } #endif - shutting_down = true; - deinit_refs(); - deinit_symbol(wrapper_info_symbol); - deinit_symbol(reference_info_symbol); - deinit_symbol(function_info_symbol); - deinit_symbol(constructor_info_symbol); - JSGlobalContextRelease(context); napi_envs.erase(context); } @@ -124,14 +130,15 @@ struct napi_env__ { } \ } while (0) -#if __ANDROID__ +#if defined(JSR_USE_BUN_JSC) #define CHECK_ENV(env) \ do { \ if ((env) == nullptr) { \ return napi_invalid_arg; \ } \ } while (0); \ - JSCAndroidAPILock jscAndroidAPILock{(env)->context}; \ + /* Last-chance finalizers hold JSC's API lock. Avoid context APIs after shutdown. */ \ + JSCBunAPILock jscBunAPILock{(env)->context, !(env)->shutting_down}; \ assert((env)->thread_id == std::this_thread::get_id()) #else #define CHECK_ENV(env) \ diff --git a/Core/Node-API/Source/jsc_android.cpp b/Core/Node-API/Source/jsc_bun.cpp similarity index 58% rename from Core/Node-API/Source/jsc_android.cpp rename to Core/Node-API/Source/jsc_bun.cpp index 389dff90..b0c366d3 100644 --- a/Core/Node-API/Source/jsc_android.cpp +++ b/Core/Node-API/Source/jsc_bun.cpp @@ -1,8 +1,8 @@ // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. -#ifndef JSR_JSC_BUN_WEBKIT_REVISION -#error JSR_JSC_BUN_WEBKIT_REVISION must identify the linked Bun WebKit build +#ifndef JSR_BUN_JSC_WEBKIT_REVISION +#error JSR_BUN_JSC_WEBKIT_REVISION must identify the linked Bun WebKit build #endif #include @@ -10,15 +10,15 @@ #include #include -extern "C" void JSCAndroidInitialize() +extern "C" void JSCBunInitialize() { - // Match the standalone JavaScriptCore C API initialization path. In particular, do not call - // WTF::initializeMainThread(): AppRuntime runs JavaScript on a worker thread, while Android may - // load this library and initialize other WTF thread-local state on the instrumentation thread. + // Match the standalone JavaScriptCore C API initialization path. Do not separately call + // WTF::initializeMainThread(): the host may load the library and initialize other WTF + // thread-local state on a different thread from the AppRuntime JavaScript worker. JSC::initialize(); } -extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context) +extern "C" void* JSCBunAcquireContextLock(JSGlobalContextRef context) { // Bun's WebKit fork removes the per-call JSLockHolder instances from the C API and expects its // embedding event loop to hold the VM lock. This holder is used only for lifecycle operations @@ -27,12 +27,12 @@ extern "C" void* JSCAndroidAcquireContextLock(JSGlobalContextRef context) return new JSC::JSLockHolder(*vm); } -extern "C" void JSCAndroidReleaseContextLock(void* opaqueLock) +extern "C" void JSCBunReleaseContextLock(void* opaqueLock) { delete static_cast(opaqueLock); } -extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context) +extern "C" bool JSCBunLockContext(JSGlobalContextRef context) { auto* vm = toJS(JSContextGetGroup(context)); if (vm->currentThreadIsHoldingAPILock()) @@ -44,12 +44,12 @@ extern "C" bool JSCAndroidLockContext(JSGlobalContextRef context) return true; } -extern "C" void JSCAndroidUnlockContext(JSGlobalContextRef context) +extern "C" void JSCBunUnlockContext(JSGlobalContextRef context) { toJS(JSContextGetGroup(context))->apiLock().unlock(); } -extern "C" const char* JSCAndroidGetWebKitRevision() +extern "C" const char* JSCBunGetWebKitRevision() { - return JSR_JSC_BUN_WEBKIT_REVISION; + return JSR_BUN_JSC_WEBKIT_REVISION; } diff --git a/Core/Node-API/Source/jsc_android.exports b/Core/Node-API/Source/jsc_bun.exports similarity index 75% rename from Core/Node-API/Source/jsc_android.exports rename to Core/Node-API/Source/jsc_bun.exports index 47b182c0..15968d28 100644 --- a/Core/Node-API/Source/jsc_android.exports +++ b/Core/Node-API/Source/jsc_bun.exports @@ -1,7 +1,7 @@ { global: JS*; - JSCAndroid*; + JSCBun*; kJS*; local: *; diff --git a/Tests/UnitTests/Android/app/build.gradle b/Tests/UnitTests/Android/app/build.gradle index b4cc7768..3cd00d00 100644 --- a/Tests/UnitTests/Android/app/build.gradle +++ b/Tests/UnitTests/Android/app/build.gradle @@ -15,7 +15,7 @@ def cmakeArguments = [ "-DJSRUNTIMEHOST_CORE_APPRUNTIME_V8_INSPECTOR=ON" ] if (project.hasProperty("jscAndroidRoot")) { - cmakeArguments.add("-DJSR_JSC_ANDROID_ROOT=${project.property('jscAndroidRoot')}") + cmakeArguments.add("-DJSR_BUN_JSC_ROOT=${project.property('jscAndroidRoot')}") } // Hermes for Android needs HOST hermesc/shermes (they emit bytecode at build // time and can't run as NDK-cross-compiled binaries). The JsRuntimeHost CMake @@ -30,7 +30,9 @@ if (project.hasProperty("importHostCompilers")) { android { namespace 'com.jsruntimehost.unittests' compileSdk 33 - ndkVersion = "23.1.7779620" + // Bun's current WebKit headers use the C++23 standard library. Keep the existing V8 default, + // but select the same modern NDK used in CI when the JavaScriptCore engine is requested. + ndkVersion = jsEngine == "JavaScriptCore" ? "28.2.13676358" : "23.1.7779620" if (project.hasProperty("ndkVersion")) { ndkVersion = project.property("ndkVersion") } From c39d93554751d58ea57e772315c62c175c66f145 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 12 Jul 2026 14:57:34 -0700 Subject: [PATCH 03/10] Address JSC review feedback --- .github/tsan_suppressions.txt | 2 +- Core/Node-API/CMakeLists.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/tsan_suppressions.txt b/.github/tsan_suppressions.txt index b17ed653..82fdb8ab 100644 --- a/.github/tsan_suppressions.txt +++ b/.github/tsan_suppressions.txt @@ -7,7 +7,7 @@ # Bun's Linux JavaScriptCore is statically linked into the final executable, so # a called_from_lib suppression cannot identify it. Match the interceptor frames # previously observed on JSC's JIT worker threads instead. -# Races manifest in TSan interceptors (free/malloc/memcpy/close) called from +# Races manifest in TSan interceptors (free/memcpy/close) called from # JSC's JIT worker threads. race:free race:close diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 4e1bd160..e0629f0f 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -5,7 +5,7 @@ elseif(APPLE) set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") elseif(ANDROID) set(NAPI_JAVASCRIPT_ENGINE "V8" CACHE STRING "JavaScript engine for Node-API") -elseif(UNIX) +elseif(CMAKE_SYSTEM_NAME STREQUAL "Linux") set(NAPI_JAVASCRIPT_ENGINE "JavaScriptCore" CACHE STRING "JavaScript engine for Node-API") else() message(FATAL_ERROR "Unable to select Node-API JavaScript engine for platform") From b42a8b5d21324775bc16d842e6d2ac485cfcc0db Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 13 Sep 2026 13:52:50 -0500 Subject: [PATCH 04/10] Bun JSC: move to the latest stable autobuild (1bd03678, 2026-09-13); the linux-amd64 asset is now plain -lto (baseline-lto is no longer published) --- Core/Node-API/CMakeLists.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index e0629f0f..4e7ee1ca 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -71,7 +71,7 @@ if(NAPI_BUILD_ABI) # archives. Download only headers and link inputs, then use Android's libjsc.so # boundary or Linux's final LTO executable link instead of maintaining a separate # JSC build/package pipeline. - set(JSR_BUN_JSC_WEBKIT_REVISION "5488984d20e0dbfe4be2c3ba8fb18eb81a5e0e8b") + set(JSR_BUN_JSC_WEBKIT_REVISION "1bd03678132783e3bb98a48205f4e8439253cf90") set(JSR_BUN_JSC_WEBKIT_RELEASE "autobuild-${JSR_BUN_JSC_WEBKIT_REVISION}") set(JSR_BUN_JSC_ROOT "" CACHE PATH "Path to an extracted Bun WebKit 'bun-webkit' directory; empty downloads the pinned release") @@ -163,7 +163,7 @@ if(NAPI_BUILD_ABI) if(JSR_BUN_JSC_PROCESSOR MATCHES "^(x86_64|amd64)$") # Use Bun's Nehalem-baseline full-LTO variant rather than imposing Haswell on # every JsRuntimeHost Linux consumer. - set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-baseline-lto.tar.gz") + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-lto.tar.gz") set(JSR_BUN_JSC_SHA256 "2756c59963525ee01f8878c07d5bceff98f6e926ccbe79dff52dfe44f4a0b5c3") elseif(JSR_BUN_JSC_PROCESSOR MATCHES "^(aarch64|arm64)$") set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-lto.tar.gz") From 3852b20f997be61887dd8f6dcd1cdde5e9c0678a Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 13 Sep 2026 14:03:14 -0500 Subject: [PATCH 05/10] Bun JSC: SHA256 pins for autobuild-1bd03678 (arm64/amd64 android, amd64/arm64 lto) --- Core/Node-API/CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 4e7ee1ca..bbbd985d 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -108,10 +108,10 @@ if(NAPI_BUILD_ABI) # arm64 and x86_64 builds; the full-LTO variants are Linux-only. if(ANDROID_ABI STREQUAL "arm64-v8a") set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-android.tar.gz") - set(JSR_BUN_JSC_SHA256 "63c18a18c89dc3e1c0a93107f95290c27fbe310eb90e30dc197a7936804d034b") + set(JSR_BUN_JSC_SHA256 "2e082e6fdad7441c167a5079764a4c989bde73b114115125be10bc821b62d5a9") elseif(ANDROID_ABI STREQUAL "x86_64") set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-android.tar.gz") - set(JSR_BUN_JSC_SHA256 "c198efb68c17ae65ff66968f7d04bdab4845a5abaefb941b15e0e2b36aaab774") + set(JSR_BUN_JSC_SHA256 "b1bb4bc6c489af5409466a4d31d89550c197baae800c9a2ab42d79158201135f") else() message(FATAL_ERROR "Bun Android JavaScriptCore supports only arm64-v8a and x86_64; got '${ANDROID_ABI}'") @@ -164,10 +164,10 @@ if(NAPI_BUILD_ABI) # Use Bun's Nehalem-baseline full-LTO variant rather than imposing Haswell on # every JsRuntimeHost Linux consumer. set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-lto.tar.gz") - set(JSR_BUN_JSC_SHA256 "2756c59963525ee01f8878c07d5bceff98f6e926ccbe79dff52dfe44f4a0b5c3") + set(JSR_BUN_JSC_SHA256 "a2d781473f54e674d4aff44528a0c6177d48d091f85a4d50726cb841eba9e490") elseif(JSR_BUN_JSC_PROCESSOR MATCHES "^(aarch64|arm64)$") set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-lto.tar.gz") - set(JSR_BUN_JSC_SHA256 "5739c4e6b32258f30db22aeeddefe9dfa3b05d6edb5ea92985b8fbefa85a2cfe") + set(JSR_BUN_JSC_SHA256 "3f54821a771f2507586e05357f31d8e70f07181dc87f5e444d858a0bf0018471") else() message(FATAL_ERROR "Bun Linux JavaScriptCore supports only x86_64 and arm64; got '${CMAKE_SYSTEM_PROCESSOR}'") From 1e1d8c186713baaa62d104655cf10bf433c29d70 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Sun, 13 Sep 2026 15:14:32 -0500 Subject: [PATCH 06/10] Bun JSC: drop -fwhole-program-vtables from the LTO shim to match the archive's split-lto-unit state The Sept autobuild's libJavaScriptCore.a is LTO bitcode built with split-lto-unit OFF (LLVM 21, summary version 12). Our shim compiled with -fwhole-program-vtables, which forces split-lto-unit ON, so lld-21 rejected the mixed link: 'inconsistent LTO Unit splitting'. The prebuilt archive can't be recompiled, so the shim matches it with plain -flto and no whole-program devirtualization (a no-op on a one-TU shim). --- Core/Node-API/CMakeLists.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index bbbd985d..4fc2c8fd 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -305,16 +305,16 @@ if(NAPI_BUILD_ABI) m) else() set_property(TARGET bun_jsc PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON) + # No -fwhole-program-vtables here: it forces -fsplit-lto-unit (LTO-unit splitting on), + # but Bun's prebuilt LTO archive is built with splitting OFF, and lld refuses to link + # LTO modules whose split-lto-unit state disagrees ("inconsistent LTO Unit splitting"). + # Since the archive is immovable, the shim must match it: plain -flto, no WPD. target_compile_options(bun_jsc PRIVATE -O2 -flto=full - -fwhole-program-vtables - -fforce-emit-vtables -fno-sanitize=all) target_link_options(bun_jsc INTERFACE -flto=full - -fwhole-program-vtables - -fforce-emit-vtables -O2 -pie "--ld-path=${JSR_BUN_JSC_LLD}" From a7ae6172ac0a489e629c04392588d017ee694698 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 14 Sep 2026 04:13:30 -0500 Subject: [PATCH 07/10] Bun JSC: make cross-module LTO opt-in (JSR_BUN_JSC_LTO) and match Bun's ThinLTO with an LLVM 21 toolchain Bun publishes two Linux flavours of its JavaScriptCore: plain native archives, and bitcode archives that are ThinLTO (module summaries, EnableSplitLTOUnit=0) produced by LLVM 21. The bitcode flavour lets the final link optimize across JSC and this runtime, but it couples the whole build to one LLVM major: the same one must compile the bitcode we contribute, index the archives, and run the LTO link. Two things broke that coupling. clang emits -flto-unit (LTO-unit splitting ON) for every -flto=full translation unit regardless of -fno-split-lto-unit, so our shim could never agree with Bun's split=0 modules ("inconsistent LTO Unit splitting"); the shim now uses -flto=thin, matching Bun's mode. And binutils ar (with a stale LLVMgold plugin) cannot read LLVM 21 bitcode, leaving our bitcode archive without a symbol index ("LLVM gold plugin has failed to create LTO module ... Reader: LLVM 13.0.1"); the LTO configuration now requires llvm-ar/llvm-ranlib from LLVM 21 and the Linux workflow passes them for the Ubuntu_JSC_LTO job. Default is the plain archives with no LTO flags or toolchain-version requirements beyond Clang, so a stock toolchain (and the sanitizer jobs) just work; the LTO build is opt-in with -D JSR_BUN_JSC_LTO=ON. Android keeps Bun's native archives: Bun now publishes Android ThinLTO archives too, but the NDK is not LLVM 21. --- .github/workflows/build-linux.yml | 13 +++- .github/workflows/ci.yml | 2 + Core/Node-API/CMakeLists.txt | 103 ++++++++++++++++++++++-------- 3 files changed, 91 insertions(+), 27 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 078155d1..2d9ca86e 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -23,6 +23,13 @@ on: required: false type: boolean default: false + bun-jsc-lto: + # Link Bun's ThinLTO JavaScriptCore archives. Needs the LLVM 21 toolchain end-to-end + # (clang-21, lld-21, llvm-ar/ranlib-21) so bitcode archives are indexed and LTO-linked by + # the same LLVM that produced Bun's bitcode (21.1.x). + required: false + type: boolean + default: false jobs: build: @@ -47,7 +54,7 @@ jobs: wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/apt.llvm.org.gpg echo "deb [signed-by=/usr/share/keyrings/apt.llvm.org.gpg] https://apt.llvm.org/jammy/ llvm-toolchain-jammy-21 main" | sudo tee /etc/apt/sources.list.d/llvm21.list sudo apt-get update - sudo apt-get install -y --no-install-recommends clang-21 lld-21 + sudo apt-get install -y --no-install-recommends clang-21 lld-21 ${{ inputs.bun-jsc-lto && 'llvm-21' || '' }} - name: Install Clang sanitizer runtimes if: (inputs.enable-sanitizers || inputs.enable-thread-sanitizer) && (inputs.cc == 'clang-21' || inputs.cxx == 'clang++-21') @@ -64,7 +71,9 @@ jobs: -D ENABLE_SANITIZERS=${{ inputs.enable-sanitizers && 'ON' || 'OFF' }} \ -D ENABLE_THREAD_SANITIZER=${{ inputs.enable-thread-sanitizer && 'ON' || 'OFF' }} \ -D CMAKE_C_COMPILER=${{ inputs.cc }} \ - -D CMAKE_CXX_COMPILER=${{ inputs.cxx }} + -D CMAKE_CXX_COMPILER=${{ inputs.cxx }} \ + -D JSR_BUN_JSC_LTO=${{ inputs.bun-jsc-lto && 'ON' || 'OFF' }} \ + ${{ inputs.bun-jsc-lto && '-D CMAKE_AR=/usr/bin/llvm-ar-21 -D CMAKE_RANLIB=/usr/bin/llvm-ranlib-21 -D CMAKE_NM=/usr/bin/llvm-nm-21' || '' }} - name: Build run: ninja -C Build/ubuntu diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5061e9fa..bc4f0f1a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -130,6 +130,8 @@ jobs: # Linux Ubuntu_JSC_LTO: uses: ./.github/workflows/build-linux.yml + with: + bun-jsc-lto: true Ubuntu_QuickJS_gcc: uses: ./.github/workflows/build-linux.yml diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 4fc2c8fd..5b71e4d3 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -104,7 +104,9 @@ if(NAPI_BUILD_ABI) "Bun's pinned Android WebKit archives target API 28; set Android minSdkVersion to 28 or newer") endif() - # Bun does not publish Android LTO archives. These are its optimized native + # Bun now also publishes Android ThinLTO archives (bun-webkit-linux-*-android-lto), but + # the NDK toolchain is not LLVM 21, so cross-module LTO with them is not possible here; + # these are its optimized native # arm64 and x86_64 builds; the full-LTO variants are Linux-only. if(ANDROID_ABI STREQUAL "arm64-v8a") set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-android.tar.gz") @@ -131,10 +133,22 @@ if(NAPI_BUILD_ABI) "Bun JavaScriptCore currently supports glibc Linux in JsRuntimeHost; musl requires Bun's separate musl archive") endif() - if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang" OR - NOT CMAKE_CXX_COMPILER_VERSION MATCHES "^21\\.") + # Bun publishes two flavours of its Linux JavaScriptCore: plain native archives, and + # ThinLTO bitcode archives (module summaries, EnableSplitLTOUnit=0) produced by LLVM 21. + # The bitcode flavour lets the final link optimize across JSC and this runtime, but it + # couples the whole build to one LLVM: the same major must compile the bitcode we add + # to the LTO unit, index the archives (llvm-ar, not binutils ar), and run the LTO link + # (lld). Default to the plain archives so a stock toolchain works; opt in to the + # cross-module LTO build with -D JSR_BUN_JSC_LTO=ON on an LLVM 21 toolchain. + option(JSR_BUN_JSC_LTO + "Link Bun's ThinLTO JavaScriptCore archives (requires clang, lld and llvm-ar/ranlib from LLVM 21)" OFF) + if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "Clang") message(FATAL_ERROR - "Bun's Linux JavaScriptCore LTO archives require Clang 21") + "Bun's Linux JavaScriptCore archives are built against Clang/libstdc++; build with Clang") + endif() + if(JSR_BUN_JSC_LTO AND NOT CMAKE_CXX_COMPILER_VERSION MATCHES "^21\\.") + message(FATAL_ERROR + "JSR_BUN_JSC_LTO needs Clang 21 (Bun's bitcode is LLVM 21); found ${CMAKE_CXX_COMPILER_VERSION}") endif() set(JSR_BUN_JSC_SAVED_CXX_STANDARD "${CMAKE_CXX_STANDARD}") @@ -154,20 +168,46 @@ if(NAPI_BUILD_ABI) COMMAND "${JSR_BUN_JSC_LLD}" --version OUTPUT_VARIABLE JSR_BUN_JSC_LLD_VERSION OUTPUT_STRIP_TRAILING_WHITESPACE) - if(NOT JSR_BUN_JSC_LLD_VERSION MATCHES "LLD 21\\.") - message(FATAL_ERROR - "Bun's Linux JavaScriptCore LTO archives require LLD 21; found '${JSR_BUN_JSC_LLD_VERSION}'") + if(JSR_BUN_JSC_LTO) + if(NOT JSR_BUN_JSC_LLD_VERSION MATCHES "LLD 21\\.") + message(FATAL_ERROR + "JSR_BUN_JSC_LTO needs LLD 21 to LTO-link Bun's LLVM 21 bitcode; found '${JSR_BUN_JSC_LLD_VERSION}'") + endif() + # Static archives of bitcode need an LLVM-aware archiver for their symbol index; + # binutils ar (with or without a mismatched LLVMgold plugin) cannot read LLVM 21 + # bitcode and leaves the index empty, which the LTO link then fails on. + execute_process( + COMMAND "${CMAKE_AR}" --version + OUTPUT_VARIABLE JSR_BUN_JSC_AR_VERSION + ERROR_QUIET + OUTPUT_STRIP_TRAILING_WHITESPACE) + if(NOT JSR_BUN_JSC_AR_VERSION MATCHES "LLVM version 21\\.") + message(FATAL_ERROR + "JSR_BUN_JSC_LTO needs llvm-ar/llvm-ranlib from LLVM 21 as the archiver: configure with " + "-D CMAKE_AR=/usr/bin/llvm-ar-21 -D CMAKE_RANLIB=/usr/bin/llvm-ranlib-21 -D CMAKE_NM=/usr/bin/llvm-nm-21 " + "(CMAKE_AR is '${CMAKE_AR}': '${JSR_BUN_JSC_AR_VERSION}')") + endif() endif() string(TOLOWER "${CMAKE_SYSTEM_PROCESSOR}" JSR_BUN_JSC_PROCESSOR) if(JSR_BUN_JSC_PROCESSOR MATCHES "^(x86_64|amd64)$") # Use Bun's Nehalem-baseline full-LTO variant rather than imposing Haswell on # every JsRuntimeHost Linux consumer. - set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-lto.tar.gz") - set(JSR_BUN_JSC_SHA256 "a2d781473f54e674d4aff44528a0c6177d48d091f85a4d50726cb841eba9e490") + if(JSR_BUN_JSC_LTO) + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64-lto.tar.gz") + set(JSR_BUN_JSC_SHA256 "a2d781473f54e674d4aff44528a0c6177d48d091f85a4d50726cb841eba9e490") + else() + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-amd64.tar.gz") + set(JSR_BUN_JSC_SHA256 "dd3d3a304a1227fc8ffea071a2f2894ccb53fcc497a4d500e46b6d21554458c2") + endif() elseif(JSR_BUN_JSC_PROCESSOR MATCHES "^(aarch64|arm64)$") - set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-lto.tar.gz") - set(JSR_BUN_JSC_SHA256 "3f54821a771f2507586e05357f31d8e70f07181dc87f5e444d858a0bf0018471") + if(JSR_BUN_JSC_LTO) + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64-lto.tar.gz") + set(JSR_BUN_JSC_SHA256 "3f54821a771f2507586e05357f31d8e70f07181dc87f5e444d858a0bf0018471") + else() + set(JSR_BUN_JSC_ASSET "bun-webkit-linux-arm64.tar.gz") + set(JSR_BUN_JSC_SHA256 "3a5bb3a25ffd8bf8a45e961580cb158c17bcf03408c6c54ccfd5b12b98c6a5f9") + endif() else() message(FATAL_ERROR "Bun Linux JavaScriptCore supports only x86_64 and arm64; got '${CMAKE_SYSTEM_PROCESSOR}'") @@ -305,20 +345,33 @@ if(NAPI_BUILD_ABI) m) else() set_property(TARGET bun_jsc PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON) - # No -fwhole-program-vtables here: it forces -fsplit-lto-unit (LTO-unit splitting on), - # but Bun's prebuilt LTO archive is built with splitting OFF, and lld refuses to link - # LTO modules whose split-lto-unit state disagrees ("inconsistent LTO Unit splitting"). - # Since the archive is immovable, the shim must match it: plain -flto, no WPD. - target_compile_options(bun_jsc PRIVATE - -O2 - -flto=full - -fno-sanitize=all) - target_link_options(bun_jsc INTERFACE - -flto=full - -O2 - -pie - "--ld-path=${JSR_BUN_JSC_LLD}" - "-Wl,--gc-sections") + if(JSR_BUN_JSC_LTO) + # Bun's bitcode archives are ThinLTO (module summaries, EnableSplitLTOUnit=0), so + # the bitcode this runtime contributes must be ThinLTO too: clang emits -flto-unit + # (splitting ON) for every -flto=full translation unit regardless of + # -fno-split-lto-unit, and lld rejects a link whose modules disagree + # ("inconsistent LTO Unit splitting"). No -fwhole-program-vtables for the same reason. + set(JSR_BUN_JSC_LTO_FLAG -flto=thin) + target_compile_options(bun_jsc PRIVATE + -O2 + ${JSR_BUN_JSC_LTO_FLAG} + -fno-sanitize=all) + target_link_options(bun_jsc INTERFACE + ${JSR_BUN_JSC_LTO_FLAG} + -O2 + -pie + "--ld-path=${JSR_BUN_JSC_LLD}" + "-Wl,--gc-sections") + else() + target_compile_options(bun_jsc PRIVATE + -O2 + -fno-sanitize=all) + target_link_options(bun_jsc INTERFACE + -O2 + -pie + "--ld-path=${JSR_BUN_JSC_LLD}" + "-Wl,--gc-sections") + endif() find_package(Threads REQUIRED) target_link_libraries(bun_jsc PUBLIC "-Wl,--whole-archive" From f5fe5b11698ee9e2c48ac309a05034b9d7df328d Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 14 Sep 2026 04:36:35 -0500 Subject: [PATCH 08/10] Bun JSC (Linux): link Bun's mimalloc fork, which the current WebKit archives allocate through The September autobuild's libJavaScriptCore/libWTF call mi_malloc, mi_free, mi_malloc_aligned and Bun's own mi_theap_get_default/mi_theap_collect (thread-heap entry points that exist only in oven-sh/mimalloc), and the archives no longer bundle an allocator, so every Linux link failed with those symbols undefined. Build the fork as a static library with MI_OVERRIDE=OFF -- JavaScriptCore calls the mi_* API explicitly and the process allocator is left alone -- and put it in the link group with WTF/bmalloc/ICU. Android's archives are unaffected (the shared shim links with --no-undefined and did). --- Core/Node-API/CMakeLists.txt | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 5b71e4d3..12772fd0 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -344,6 +344,26 @@ if(NAPI_BUILD_ABI) ${CMAKE_DL_LIBS} m) else() + # Bun's Linux JavaScriptCore builds allocate through Bun's fork of mimalloc (the + # mi_theap_* thread-heap entry points exist only there) and the archives do not + # bundle it, so the host has to provide it. Build the fork's static library with the + # standard allocator untouched (MI_OVERRIDE=OFF): JavaScriptCore calls the mi_* API + # explicitly, and nothing else in the process should change allocator. + include(FetchContent) + FetchContent_Declare(bun_mimalloc + GIT_REPOSITORY https://github.com/oven-sh/mimalloc.git + GIT_TAG 58460467413a27ca426951e9e06c5a222be9553c + EXCLUDE_FROM_ALL) + set(MI_OVERRIDE OFF CACHE BOOL "" FORCE) + set(MI_BUILD_SHARED OFF CACHE BOOL "" FORCE) + set(MI_BUILD_STATIC ON CACHE BOOL "" FORCE) + set(MI_BUILD_OBJECT OFF CACHE BOOL "" FORCE) + set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE) + set(MI_INSTALL_TOPLEVEL OFF CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(bun_mimalloc) + set_property(TARGET mimalloc-static PROPERTY POSITION_INDEPENDENT_CODE ON) + set_property(TARGET mimalloc-static PROPERTY FOLDER Dependencies) + set_property(TARGET bun_jsc PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON) if(JSR_BUN_JSC_LTO) # Bun's bitcode archives are ThinLTO (module summaries, EnableSplitLTOUnit=0), so @@ -383,6 +403,7 @@ if(NAPI_BUILD_ABI) "${BUN_JSC_DIR}/lib/libicui18n.a" "${BUN_JSC_DIR}/lib/libicuuc.a" "${BUN_JSC_DIR}/lib/libicudata.a" + mimalloc-static "-Wl,--end-group" Threads::Threads ${CMAKE_DL_LIBS} From 4b9ef41ba91508a054cafcd58fa3e33e67c61bd7 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 14 Sep 2026 04:49:59 -0500 Subject: [PATCH 09/10] Bun JSC: link Bun's mimalloc fork on Android too The Android archives from the same autobuild reference the same mi_* entry points (mi_malloc, mi_free, mi_theap_get_default, mi_collect, mi_manage_os_memory_ex, ...), so the libjsc.so link failed just like Linux. Declare the mimalloc fork once, before the platform branch, and link mimalloc-static into both shims. --- Core/Node-API/CMakeLists.txt | 59 ++++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 19 deletions(-) diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 12772fd0..4ae830ff 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -295,6 +295,45 @@ if(NAPI_BUILD_ABI) set(INCLUDE_DIRECTORIES ${INCLUDE_DIRECTORIES} PUBLIC "${BUN_JSC_DIR}/include") + # Bun's JavaScriptCore builds (Linux and Android) allocate through Bun's fork of mimalloc (the + + # mi_theap_* thread-heap entry points exist only there) and the archives do not + + # bundle it, so the host has to provide it. Build the fork's static library with the + + # standard allocator untouched (MI_OVERRIDE=OFF): JavaScriptCore calls the mi_* API + + # explicitly, and nothing else in the process should change allocator. + + include(FetchContent) + + FetchContent_Declare(bun_mimalloc + + GIT_REPOSITORY https://github.com/oven-sh/mimalloc.git + + GIT_TAG 58460467413a27ca426951e9e06c5a222be9553c + + EXCLUDE_FROM_ALL) + + set(MI_OVERRIDE OFF CACHE BOOL "" FORCE) + + set(MI_BUILD_SHARED OFF CACHE BOOL "" FORCE) + + set(MI_BUILD_STATIC ON CACHE BOOL "" FORCE) + + set(MI_BUILD_OBJECT OFF CACHE BOOL "" FORCE) + + set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE) + + set(MI_INSTALL_TOPLEVEL OFF CACHE BOOL "" FORCE) + + FetchContent_MakeAvailable(bun_mimalloc) + + set_property(TARGET mimalloc-static PROPERTY POSITION_INDEPENDENT_CODE ON) + + set_property(TARGET mimalloc-static PROPERTY FOLDER Dependencies) + + if(ANDROID) add_library(bun_jsc SHARED "Source/jsc_bun.cpp") else() @@ -339,30 +378,12 @@ if(NAPI_BUILD_ABI) "${BUN_JSC_DIR}/lib/libicui18n.a" "${BUN_JSC_DIR}/lib/libicuuc.a" "${BUN_JSC_DIR}/lib/libicudata.a" + mimalloc-static "-Wl,--end-group" log ${CMAKE_DL_LIBS} m) else() - # Bun's Linux JavaScriptCore builds allocate through Bun's fork of mimalloc (the - # mi_theap_* thread-heap entry points exist only there) and the archives do not - # bundle it, so the host has to provide it. Build the fork's static library with the - # standard allocator untouched (MI_OVERRIDE=OFF): JavaScriptCore calls the mi_* API - # explicitly, and nothing else in the process should change allocator. - include(FetchContent) - FetchContent_Declare(bun_mimalloc - GIT_REPOSITORY https://github.com/oven-sh/mimalloc.git - GIT_TAG 58460467413a27ca426951e9e06c5a222be9553c - EXCLUDE_FROM_ALL) - set(MI_OVERRIDE OFF CACHE BOOL "" FORCE) - set(MI_BUILD_SHARED OFF CACHE BOOL "" FORCE) - set(MI_BUILD_STATIC ON CACHE BOOL "" FORCE) - set(MI_BUILD_OBJECT OFF CACHE BOOL "" FORCE) - set(MI_BUILD_TESTS OFF CACHE BOOL "" FORCE) - set(MI_INSTALL_TOPLEVEL OFF CACHE BOOL "" FORCE) - FetchContent_MakeAvailable(bun_mimalloc) - set_property(TARGET mimalloc-static PROPERTY POSITION_INDEPENDENT_CODE ON) - set_property(TARGET mimalloc-static PROPERTY FOLDER Dependencies) set_property(TARGET bun_jsc PROPERTY INTERFACE_POSITION_INDEPENDENT_CODE ON) if(JSR_BUN_JSC_LTO) From af7e60ebcc176ad11d274abbc718465c9170d8d2 Mon Sep 17 00:00:00 2001 From: Matt Hargett Date: Mon, 14 Sep 2026 05:21:19 -0500 Subject: [PATCH 10/10] CI: symbolize ThreadSanitizer reports and build mimalloc with MI_TSAN The TSan job's reports came back as bare addresses (llvm-21 only ships llvm-symbolizer-21, which TSan does not find on its own), so none of the 153 warnings on the current Bun archives could be attributed. Point TSAN_OPTIONS at the versioned symbolizer and install llvm-21 for that job too. mimalloc is already instrumented through the global -fsanitize=thread, but its lock-free paths read a few fields racily by design; MI_TSAN routes those through atomics so only real races remain. --- .github/workflows/build-linux.yml | 6 ++++-- Core/Node-API/CMakeLists.txt | 5 +++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/.github/workflows/build-linux.yml b/.github/workflows/build-linux.yml index 2d9ca86e..961503bb 100644 --- a/.github/workflows/build-linux.yml +++ b/.github/workflows/build-linux.yml @@ -54,7 +54,7 @@ jobs: wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo gpg --dearmor -o /usr/share/keyrings/apt.llvm.org.gpg echo "deb [signed-by=/usr/share/keyrings/apt.llvm.org.gpg] https://apt.llvm.org/jammy/ llvm-toolchain-jammy-21 main" | sudo tee /etc/apt/sources.list.d/llvm21.list sudo apt-get update - sudo apt-get install -y --no-install-recommends clang-21 lld-21 ${{ inputs.bun-jsc-lto && 'llvm-21' || '' }} + sudo apt-get install -y --no-install-recommends clang-21 lld-21 ${{ (inputs.bun-jsc-lto || inputs.enable-thread-sanitizer) && 'llvm-21' || '' }} - name: Install Clang sanitizer runtimes if: (inputs.enable-sanitizers || inputs.enable-thread-sanitizer) && (inputs.cc == 'clang-21' || inputs.cxx == 'clang++-21') @@ -108,7 +108,9 @@ jobs: working-directory: Build/ubuntu/Tests/UnitTests run: ./UnitTests env: - TSAN_OPTIONS: ${{ inputs.enable-thread-sanitizer && format('suppressions={0}/.github/tsan_suppressions.txt', github.workspace) || '' }} + # llvm-21 ships the symbolizer as llvm-symbolizer-21 only; without the explicit path TSan + # prints bare addresses for every frame, which makes a report undiagnosable. + TSAN_OPTIONS: ${{ inputs.enable-thread-sanitizer && format('suppressions={0}/.github/tsan_suppressions.txt:external_symbolizer_path=/usr/bin/llvm-symbolizer-21', github.workspace) || '' }} # JSC's concurrent GC on Linux uses SIGUSR1 + sem_wait to suspend mutator # threads at safepoints. TSan's signal interception delays SIGUSR1 delivery # indefinitely, deadlocking the Collector Thread's sem_wait. Disabling the diff --git a/Core/Node-API/CMakeLists.txt b/Core/Node-API/CMakeLists.txt index 4ae830ff..02ecf7c9 100644 --- a/Core/Node-API/CMakeLists.txt +++ b/Core/Node-API/CMakeLists.txt @@ -327,6 +327,11 @@ if(NAPI_BUILD_ABI) set(MI_INSTALL_TOPLEVEL OFF CACHE BOOL "" FORCE) + # ThreadSanitizer instruments mimalloc through the global compile options, but its + # lock-free paths read a few fields racily on purpose; MI_TSAN switches those to + # atomics so an instrumented build only reports real races. + set(MI_TSAN ${ENABLE_THREAD_SANITIZER} CACHE BOOL "" FORCE) + FetchContent_MakeAvailable(bun_mimalloc) set_property(TARGET mimalloc-static PROPERTY POSITION_INDEPENDENT_CODE ON)