diff --git a/.buildkite/Dockerfile b/.buildkite/Dockerfile index 3882ab1fa5af..4ed4ad85ec37 100644 --- a/.buildkite/Dockerfile +++ b/.buildkite/Dockerfile @@ -124,8 +124,41 @@ ARG BUILDKITE_AGENT_TAGS RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ && export PATH=$HOME/.cargo/bin:$PATH \ && rustup install nightly \ - && rustup default nightly - + && rustup default nightly \ + && rustup target add aarch64-linux-android x86_64-linux-android + +# Android NDK — sysroot/libc++/compiler-rt for --abi=android cross-compile. +ARG ANDROID_NDK_VERSION="r27c" +RUN curl -fsSL "https://dl.google.com/android/repository/android-ndk-${ANDROID_NDK_VERSION}-linux.zip" -o /tmp/ndk.zip \ + && unzip -q /tmp/ndk.zip -d /opt \ + && mv /opt/android-ndk-${ANDROID_NDK_VERSION} /opt/android-ndk \ + && rm /tmp/ndk.zip \ + # Trim ~1.1GB we don't use (NDK clang/lld, lldb, non-android runtimes) — + # we only need sysroot + android compiler-rt. Dramatically shrinks the + # docker layer / AMI size. + && rm -rf /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/bin \ + /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/python3 \ + /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/lib/liblldb.so \ + /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/lib/*-gnu \ + /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/lib/*-musl* \ + /opt/android-ndk/simpleperf /opt/android-ndk/shader-tools /opt/android-ndk/sources \ + # Symlink NDK compiler-rt builtins + libunwind into host clang's resource + # dir — clang's driver hardcodes /lib//libclang_rt.* + # with no -L fallback. Done at image-build time (root) since the build + # user can't write to /usr. + && RES=$(clang -print-resource-dir) \ + && NDK_RT=/opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/$(ls /opt/android-ndk/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/)/lib/linux \ + && mkdir -p $RES/lib/linux \ + && for A in aarch64 x86_64; do \ + ln -sf $NDK_RT/libclang_rt.builtins-${A}-android.a $RES/lib/linux/; \ + mkdir -p $RES/lib/linux/${A}; \ + ln -sf $NDK_RT/${A}/libunwind.a $RES/lib/linux/${A}/; \ + DIR=$RES/lib/${A}-unknown-linux-android28; \ + mkdir -p $DIR; \ + ln -sf $NDK_RT/libclang_rt.builtins-${A}-android.a $DIR/libclang_rt.builtins.a; \ + ln -sf $NDK_RT/${A}/libunwind.a $DIR/libunwind.a; \ + done +ENV ANDROID_NDK_ROOT=/opt/android-ndk RUN ARCH=$(if [ "$TARGETARCH" = "arm64" ]; then echo "arm64"; else echo "amd64"; fi) && \ echo "Downloading buildkite" && \ @@ -190,7 +223,8 @@ COPY . /workspace/bun RUN curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y \ && export PATH=$HOME/.cargo/bin:$PATH \ && rustup install nightly \ - && rustup default nightly + && rustup default nightly \ + && rustup target add aarch64-linux-android x86_64-linux-android ENV PATH=/root/.cargo/bin:$PATH diff --git a/.buildkite/ci.mjs b/.buildkite/ci.mjs index 6a9b1e0838ff..f9f0072e64c3 100755 --- a/.buildkite/ci.mjs +++ b/.buildkite/ci.mjs @@ -33,7 +33,7 @@ import { /** * @typedef {"linux" | "darwin" | "windows"} Os * @typedef {"aarch64" | "x64"} Arch - * @typedef {"musl"} Abi + * @typedef {"musl" | "android"} Abi * @typedef {"debian" | "ubuntu" | "alpine" | "amazonlinux"} Distro * @typedef {"latest" | "previous" | "oldest" | "eol"} Tier * @typedef {"release" | "assert" | "debug" | "asan"} Profile @@ -129,6 +129,10 @@ const buildPlatforms = [ { os: "linux", arch: "aarch64", abi: "musl", distro: "alpine", release: "3.23" }, { os: "linux", arch: "x64", abi: "musl", distro: "alpine", release: "3.23" }, { os: "linux", arch: "x64", abi: "musl", baseline: true, distro: "alpine", release: "3.23" }, + // Android: cross-compiled from glibc amazonlinux via NDK sysroot. Host arch + // matches target arch so only --abi/--target/--sysroot are cross. + { os: "linux", arch: "aarch64", abi: "android", distro: "amazonlinux", release: "2023", features: ["docker"] }, + { os: "linux", arch: "x64", abi: "android", distro: "amazonlinux", release: "2023", features: ["docker"] }, { os: "windows", arch: "x64", release: "2019" }, { os: "windows", arch: "x64", baseline: true, release: "2019" }, { os: "windows", arch: "aarch64", release: "11" }, @@ -207,7 +211,9 @@ function getImageKey(platform) { key += `-with-${features.join("-")}`; } - if (abi) { + // Android cross-compiles from the same glibc image as gnu (just needs NDK, + // which bootstrap.sh installs on all Linux build images) — no separate image. + if (abi && abi !== "android") { key += `-${abi}`; } @@ -488,6 +494,10 @@ function getBuildArgs(target, options, mode) { if (os === "linux") args.push(`--abi=${abi ?? "gnu"}`); } else if (abi === "musl") { args.push("--abi=musl"); + } else if (abi === "android") { + // Android cross-compiles C++ from a glibc host: arch/abi must be explicit + // (host detection would report the build box's gnu/x64, not the target). + args.push(`--os=${os}`, `--arch=${arch}`, "--abi=android"); } if (baseline) args.push("--baseline=on"); if (profile === "asan") args.push("--asan=on"); @@ -598,6 +608,9 @@ function getTargetTriplet(platform) { if (abi === "musl") { triplet += "-musl"; } + if (abi === "android") { + triplet += "-android"; + } if (baseline) { triplet += "-baseline"; } diff --git a/build.zig b/build.zig index 48ee67c00d01..86d968c54cc6 100644 --- a/build.zig +++ b/build.zig @@ -53,6 +53,7 @@ const BunBuildOptions = struct { no_llvm: bool, lto: bool, override_no_export_cpp_apis: bool, + android_ndk_sysroot: ?[]const u8 = null, cached_options_module: ?*Module = null, windows_shim: ?WindowsShim = null, @@ -204,6 +205,14 @@ pub fn build(b: *Build) !void { const no_llvm = b.option(bool, "no_llvm", "Experiment with Zig self hosted backends. No stability guaranteed") orelse false; const lto = b.option(bool, "lto", "Emit LLVM bitcode for full LTO instead of a native object") orelse false; const override_no_export_cpp_apis = b.option(bool, "override-no-export-cpp-apis", "Override the default export_cpp_apis logic to disable exports") orelse false; + // Zig does not bundle bionic headers, so translate-c needs the NDK + // sysroot include paths explicitly. The obj's linkLibC() gets bionic + // via `zig build --libc ` (b.libc_file), which the build script + // also passes when targeting Android. + const android_ndk_sysroot = b.option([]const u8, "android_ndk_sysroot", "Android NDK sysroot for translate-c headers"); + if (abi.isAndroid() and android_ndk_sysroot == null) { + std.debug.panic("-Dandroid_ndk_sysroot is required when targeting Android (zig does not bundle bionic headers)", .{}); + } var build_options = BunBuildOptions{ .target = target, @@ -267,6 +276,7 @@ pub fn build(b: *Build) !void { .enable_tinycc = b.option(bool, "enable_tinycc", "Enable TinyCC for FFI JIT compilation") orelse true, .use_mimalloc = b.option(bool, "use_mimalloc", "Use mimalloc as default allocator") orelse false, .llvm_codegen_threads = b.option(u32, "llvm_codegen_threads", "Number of threads to use for LLVM codegen") orelse 1, + .android_ndk_sysroot = android_ndk_sysroot, }; // zig build obj @@ -443,7 +453,7 @@ pub fn build(b: *Build) !void { }) |t| { const resolved = t.resolveTarget(b); step.dependOn( - &b.addInstallFile(getTranslateC(b, resolved, .Debug), b.fmt("translated-c-headers/{s}.zig", .{ + &b.addInstallFile(getTranslateC(b, resolved, .Debug, null), b.fmt("translated-c-headers/{s}.zig", .{ resolved.result.zigTriple(b.allocator) catch @panic("OOM"), })).step, ); @@ -608,6 +618,7 @@ const TargetDescription = struct { os: OperatingSystem, arch: Arch, musl: bool = false, + android: bool = false, fn resolveTarget(desc: TargetDescription, b: *Build) std.Build.ResolvedTarget { return b.resolveTargetQuery(.{ @@ -615,7 +626,8 @@ const TargetDescription = struct { .cpu_arch = desc.arch, .cpu_model = getCpuModel(desc.os, desc.arch) orelse .determined_by_arch_os, .os_version_min = getOSVersionMin(desc.os), - .glibc_version = if (desc.musl) null else getOSGlibCVersion(desc.os), + .glibc_version = if (desc.musl or desc.android) null else getOSGlibCVersion(desc.os), + .abi = if (desc.android) .android else null, }); } }; @@ -659,7 +671,7 @@ fn addMultiCheck( } } -fn getTranslateC(b: *Build, initial_target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode) LazyPath { +fn getTranslateC(b: *Build, initial_target: std.Build.ResolvedTarget, optimize: std.builtin.OptimizeMode, android_ndk_sysroot: ?[]const u8) LazyPath { const target = b.resolveTargetQuery(q: { var query = initial_target.query; if (query.os_tag == .windows) @@ -684,6 +696,18 @@ fn getTranslateC(b: *Build, initial_target: std.Build.ResolvedTarget, optimize: translate_c.addIncludePath(b.path("vendor/zstd/lib")); + if (target.result.abi.isAndroid()) { + const sysroot = android_ndk_sysroot orelse + std.debug.panic("translate-c for Android requires -Dandroid_ndk_sysroot", .{}); + const arch_triple = switch (target.result.cpu.arch) { + .aarch64 => "aarch64-linux-android", + .x86_64 => "x86_64-linux-android", + else => |a| std.debug.panic("unsupported Android arch: {s}", .{@tagName(a)}), + }; + translate_c.addSystemIncludePath(.{ .cwd_relative = b.fmt("{s}/usr/include", .{sysroot}) }); + translate_c.addSystemIncludePath(.{ .cwd_relative = b.fmt("{s}/usr/include/{s}", .{ sysroot, arch_triple }) }); + } + if (target.result.os.tag == .windows) { // translate-c is unable to translate the unsuffixed windows functions // like `SetCurrentDirectory` since they are defined with an odd macro @@ -888,7 +912,7 @@ fn addInternalImports(b: *Build, mod: *Module, opts: *BunBuildOptions) void { mod.addImport("build_options", opts.buildOptionsModule(b)); - const translate_c = getTranslateC(b, opts.target, opts.optimize); + const translate_c = getTranslateC(b, opts.target, opts.optimize, opts.android_ndk_sysroot); mod.addImport("translated-c-headers", b.createModule(.{ .root_source_file = translate_c })); const zlib_internal_path = switch (os) { diff --git a/packages/bun-release/src/platform.ts b/packages/bun-release/src/platform.ts index 253de0968cf0..9fe98fc55de9 100644 --- a/packages/bun-release/src/platform.ts +++ b/packages/bun-release/src/platform.ts @@ -10,12 +10,12 @@ export const avx2 = arch === "x64" && ((os === "linux" && isLinuxAVX2()) || (os === "darwin" && isDarwinAVX2()) || (os === "win32" && isWindowsAVX2())); -export const abi = os === "linux" && isLinuxMusl() ? "musl" : undefined; +export const abi = os === "android" ? "android" : os === "linux" && isLinuxMusl() ? "musl" : undefined; export type Platform = { os: string; arch: string; - abi?: "musl"; + abi?: "musl" | "android"; avx2?: boolean; bin: string; exe: string; @@ -82,6 +82,24 @@ export const platforms: Platform[] = [ bin: "bun-linux-x64-musl-baseline", exe: "bin/bun", }, + { + // Node's process.platform is "android" on Android (Termux etc.), not "linux". + // The release asset is still named bun-linux-* for consistency with the + // build triplet, but npm's os field must be "android" for optionalDependency + // resolution to pick it up on-device. + os: "android", + arch: "arm64", + abi: "android", + bin: "bun-linux-aarch64-android", + exe: "bin/bun", + }, + { + os: "android", + arch: "x64", + abi: "android", + bin: "bun-linux-x64-android", + exe: "bin/bun", + }, { os: "win32", arch: "x64", diff --git a/packages/bun-usockets/src/crypto/root_certs_linux.cpp b/packages/bun-usockets/src/crypto/root_certs_linux.cpp index fe767801f550..7a3801a093af 100644 --- a/packages/bun-usockets/src/crypto/root_certs_linux.cpp +++ b/packages/bun-usockets/src/crypto/root_certs_linux.cpp @@ -24,11 +24,28 @@ static void load_certs_from_directory(const char* dir_path, STACK_OF(X509)* cert continue; } - // Check if file has .crt, .pem, or .cer extension + // Accept .crt/.pem/.cer. On Android also accept OpenSSL c_rehash-style names + // (^[0-9a-f]{8}\.[0-9]+$) since /system/etc/security/cacerts/ uses ONLY that + // format. On Debian /etc/ssl/certs/ has both *.pem and .0 symlinks to + // the same files, so accepting both there would just double-load. const char* ext = strrchr(entry->d_name, '.'); - if (!ext || (strcmp(ext, ".crt") != 0 && strcmp(ext, ".pem") != 0 && strcmp(ext, ".cer") != 0)) { - continue; + if (!ext) continue; + bool ok = strcmp(ext, ".crt") == 0 || strcmp(ext, ".pem") == 0 || strcmp(ext, ".cer") == 0; +#ifdef __ANDROID__ + if (!ok) { + size_t prefix = (size_t)(ext - entry->d_name); + if (prefix == 8) { + ok = true; + for (size_t i = 0; i < 8; i++) { + char c = entry->d_name[i]; + if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f'))) { ok = false; break; } + } + for (const char* p = ext + 1; ok && *p; p++) if (*p < '0' || *p > '9') ok = false; + if (ext[1] == '\0') ok = false; + } } +#endif + if (!ok) continue; // Build full path char filepath[PATH_MAX]; @@ -109,7 +126,16 @@ extern "C" void us_load_system_certificates_linux(STACK_OF(X509) **system_certs) // Otherwise, load certificates from standard Linux/Unix paths // These are the common locations for system certificates - +#ifdef __ANDROID__ + // Android: no bundle files. System CAs are individual hashed PEM files. + static const char* bundle_paths[] = { NULL }; + static const char* dir_paths[] = { + "/apex/com.android.conscrypt/cacerts", // API 30+ (mainline updatable) + "/system/etc/security/cacerts", // base system store + "/data/misc/user/0/cacerts-added", // user-installed + NULL + }; +#else // Common certificate bundle locations (single file with multiple certs) // These paths are based on common Linux distributions and OpenSSL defaults static const char* bundle_paths[] = { @@ -123,7 +149,7 @@ extern "C" void us_load_system_certificates_linux(STACK_OF(X509) **system_certs) "/usr/local/share/ca-certificates/ca-certificates.crt", // Custom CA installs NULL }; - + // Common certificate directory locations (multiple files) // Note: OpenSSL expects hashed symlinks in directories (c_rehash format) static const char* dir_paths[] = { @@ -131,12 +157,13 @@ extern "C" void us_load_system_certificates_linux(STACK_OF(X509) **system_certs) "/etc/pki/tls/certs", // RHEL/Fedora "/usr/share/ca-certificates", // Debian/Ubuntu (original certs, not hashed) "/usr/local/share/certs", // FreeBSD - "/etc/openssl/certs", // NetBSD + "/etc/openssl/certs", // NetBSD "/var/ssl/certs", // AIX "/usr/local/etc/openssl/certs", // Homebrew OpenSSL on macOS "/System/Library/OpenSSL/certs", // macOS system OpenSSL (older versions) NULL }; +#endif // Try loading from bundle files first for (const char** path = bundle_paths; *path != NULL; path++) { diff --git a/packages/bun-usockets/src/eventing/epoll_kqueue.c b/packages/bun-usockets/src/eventing/epoll_kqueue.c index 380fedd2a3bb..044c61dae3fd 100644 --- a/packages/bun-usockets/src/eventing/epoll_kqueue.c +++ b/packages/bun-usockets/src/eventing/epoll_kqueue.c @@ -138,7 +138,7 @@ static int bun_epoll_pwait2(int epfd, struct epoll_event *events, int maxevents, ret = sys_epoll_pwait2(epfd, events, maxevents, timeout, &mask); } while (ret == -EINTR); - if (LIKELY(ret != -ENOSYS && ret != -EPERM && ret != -EOPNOTSUPP)) { + if (LIKELY(ret != -ENOSYS && ret != -EPERM && ret != -EOPNOTSUPP && ret != -EACCES)) { return ret; } @@ -564,7 +564,7 @@ size_t us_internal_accept_poll_event(struct us_poll_t *p) { struct us_timer_t *us_create_timer(struct us_loop_t *loop, int fallthrough, unsigned int ext_size) { struct us_poll_t *p = us_create_poll(loop, fallthrough, sizeof(struct us_internal_callback_t) + ext_size); memset(p, 0, sizeof(struct us_internal_callback_t) + ext_size); - int timerfd = timerfd_create(CLOCK_REALTIME, TFD_NONBLOCK | TFD_CLOEXEC); + int timerfd = timerfd_create(CLOCK_MONOTONIC, TFD_NONBLOCK | TFD_CLOEXEC); if (timerfd == -1) { return NULL; } @@ -677,7 +677,14 @@ struct us_internal_async *us_internal_create_async(struct us_loop_t *loop, int f struct us_poll_t *p = us_create_poll(loop, fallthrough, sizeof(struct us_internal_callback_t) + ext_size); memset(p, 0, sizeof(struct us_internal_callback_t) + ext_size); - us_poll_init(p, eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC), POLL_TYPE_CALLBACK); + int efd = eventfd(0, EFD_NONBLOCK | EFD_CLOEXEC); + if (efd == -1) { + // eventfd only fails on EMFILE/ENFILE — the loop is unusable without + // wakeup_async, and the sole caller doesn't NULL-check. Crash loudly + // rather than NULL-deref or store -1 as a poll fd. + BUN_PANIC("eventfd() failed during loop init (out of file descriptors?)"); + } + us_poll_init(p, efd, POLL_TYPE_CALLBACK); struct us_internal_callback_t *cb = (struct us_internal_callback_t *) p; cb->loop = loop; diff --git a/packages/bun-usockets/src/internal/internal.h b/packages/bun-usockets/src/internal/internal.h index dddff9ec5bf6..028fdb6ebc37 100644 --- a/packages/bun-usockets/src/internal/internal.h +++ b/packages/bun-usockets/src/internal/internal.h @@ -62,6 +62,9 @@ void us_internal_loop_update_pending_ready_polls(struct us_loop_t *loop, #define UNLIKELY(cond) __builtin_expect((_Bool)(cond), 0) #endif +extern void __attribute((__noreturn__)) Bun__panic(const char *message, size_t length); +#define BUN_PANIC(message) Bun__panic(message, sizeof(message) - 1) + #ifdef _WIN32 #define IS_EINTR(rc) (rc == SOCKET_ERROR && WSAGetLastError() == WSAEINTR) #define LIBUS_ERR WSAGetLastError() diff --git a/packages/bun-usockets/src/loop.c b/packages/bun-usockets/src/loop.c index 11cabd0238a6..a51f1bc73980 100644 --- a/packages/bun-usockets/src/loop.c +++ b/packages/bun-usockets/src/loop.c @@ -35,8 +35,6 @@ #if ASSERT_ENABLED extern const size_t Bun__lock__size; -extern void __attribute((__noreturn__)) Bun__panic(const char* message, size_t length); -#define BUN_PANIC(message) Bun__panic(message, sizeof(message) - 1) #endif extern void Bun__internal_ensureDateHeaderTimerIsEnabled(struct us_loop_t *loop); diff --git a/scripts/bootstrap.sh b/scripts/bootstrap.sh index 222819338d4a..94c6b9eee535 100755 --- a/scripts/bootstrap.sh +++ b/scripts/bootstrap.sh @@ -1,5 +1,5 @@ #!/bin/sh -# Version: 30 +# Version: 31 # A script that installs the dependencies needed to build and test Bun. # This should work on macOS and Linux with a POSIX shell. @@ -1091,6 +1091,7 @@ install_build_essentials() { install_osxcross install_gcc install_rust + install_android_ndk install_ccache install_docker } @@ -1249,6 +1250,66 @@ install_rust() { execute_as_user "$rustup" target add x86_64-apple-darwin ;; esac + + case "$os" in + linux) + rustup="$rust_home/bin/rustup" + if ! [ -x "$rustup" ]; then + error "rustup not found at $rustup after install" + fi + execute_as_user "$rustup" target add aarch64-linux-android + execute_as_user "$rustup" target add x86_64-linux-android + ;; + esac +} + +android_ndk_version() { + print "r27c" +} + +install_android_ndk() { + case "$os" in + linux) ;; + *) return ;; + esac + + ndk_version="$(android_ndk_version)" + ndk_home="/opt/android-ndk" + if [ -d "$ndk_home" ]; then + return + fi + + ndk_zip=$(download_file "https://dl.google.com/android/repository/android-ndk-${ndk_version}-linux.zip") + unzip="$(require unzip)" + execute_sudo "$unzip" -q "$ndk_zip" -d /opt + execute_sudo mv "/opt/android-ndk-${ndk_version}" "$ndk_home" + # Trim ~1.1GB unused (NDK clang/lld, lldb, non-android runtimes). + ndk_prebuilt="$ndk_home/toolchains/llvm/prebuilt/linux-x86_64" + execute_sudo rm -rf "$ndk_prebuilt/bin" "$ndk_prebuilt/python3" "$ndk_prebuilt/lib/liblldb.so" \ + "$ndk_home/simpleperf" "$ndk_home/shader-tools" "$ndk_home/sources" + append_to_profile "export ANDROID_NDK_ROOT=$ndk_home" + + # Symlink NDK compiler-rt builtins + libunwind into host clang's resource + # dir. clang's driver hardcodes /lib//libclang_rt.* + # with no -L fallback, so the file must exist there for any android link. + # Done here (as root) so the build user doesn't need write access to /usr. + clang="$(which clang-$(llvm_version) || which clang)" + if [ -x "$clang" ]; then + res_dir="$("$clang" -print-resource-dir)" + ndk_clang_ver="$(ls "$ndk_home/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/" | head -1)" + ndk_rt="$ndk_home/toolchains/llvm/prebuilt/linux-x86_64/lib/clang/$ndk_clang_ver/lib/linux" + execute_sudo mkdir -p "$res_dir/lib/linux" + for ndk_arch in aarch64 x86_64; do + # Old-style flat layout (apt.llvm.org clang) AND new-style per-triple. + execute_sudo ln -sf "$ndk_rt/libclang_rt.builtins-${ndk_arch}-android.a" "$res_dir/lib/linux/" + execute_sudo mkdir -p "$res_dir/lib/linux/${ndk_arch}" + execute_sudo ln -sf "$ndk_rt/${ndk_arch}/libunwind.a" "$res_dir/lib/linux/${ndk_arch}/" + triple_dir="$res_dir/lib/${ndk_arch}-unknown-linux-android28" + execute_sudo mkdir -p "$triple_dir" + execute_sudo ln -sf "$ndk_rt/libclang_rt.builtins-${ndk_arch}-android.a" "$triple_dir/libclang_rt.builtins.a" + execute_sudo ln -sf "$ndk_rt/${ndk_arch}/libunwind.a" "$triple_dir/libunwind.a" + done + fi } install_docker() { diff --git a/scripts/build.ts b/scripts/build.ts index d0fe6beaf123..5ff5ef938543 100644 --- a/scripts/build.ts +++ b/scripts/build.ts @@ -319,6 +319,7 @@ function parseArgs(argv: string[]): CliArgs { "webkitVersion", "pgoGenerate", "pgoUse", + "androidNdk", ]); for (let i = 0; i < argv.length; i++) { diff --git a/scripts/build/bun.ts b/scripts/build/bun.ts index cc4c56245536..311f7e47ecd9 100644 --- a/scripts/build/bun.ts +++ b/scripts/build/bun.ts @@ -56,19 +56,26 @@ function systemLibs(cfg: Config): string[] { const libs: string[] = []; if (cfg.linux) { - libs.push("-lc", "-lpthread", "-ldl"); - // libatomic: static by default (CI distros ship it), dynamic on Arch-like. - // The static path needs to be the actual file path for lld to find it; - // dynamic uses -l syntax. We emit what CMake does: bare libatomic.a gets - // found in lib search paths, -latomic.so doesn't exist so we use -latomic. - if (cfg.staticLibatomic) { - libs.push("-l:libatomic.a"); + if (cfg.abi === "android") { + // bionic: pthread/dl/rt are folded into libc; no separate libatomic + // (compiler-rt builtins). -llog for __android_log_*. + libs.push("-lc", "-lm", "-llog"); } else { - libs.push("-latomic"); + libs.push("-lc", "-lpthread", "-ldl"); + // libatomic: static by default (CI distros ship it), dynamic on Arch-like. + // The static path needs to be the actual file path for lld to find it; + // dynamic uses -l syntax. We emit what CMake does: bare libatomic.a gets + // found in lib search paths, -latomic.so doesn't exist so we use -latomic. + if (cfg.staticLibatomic) { + libs.push("-l:libatomic.a"); + } else { + libs.push("-latomic"); + } } // Linux local WebKit: link system ICU (prebuilt bundles its own). // Assumes system ICU is in default lib paths — true on most distros. - if (cfg.webkit === "local") { + // Android: no system ICU; the local WebKit build must bundle it. + if (cfg.webkit === "local" && cfg.abi !== "android") { libs.push("-licudata", "-licui18n", "-licuuc"); } } @@ -623,6 +630,12 @@ function emitLinkOnly(n: Ninja, cfg: Config): BunOutput { * mismatch, etc.). */ function emitSmokeTest(n: Ninja, cfg: Config, exe: string, exeName: string): void { + // Cross-compiled binaries can't run on the build host. Skip the smoke + // test entirely — `ninja check` becomes a no-op alias for the exe. + if (cfg.crossTarget !== undefined) { + n.phony("check", [exe]); + return; + } const stamp = resolve(cfg.buildDir, `${exeName}.smoke-test-passed`); // Linux+ASAN: wrap in `setarch -R` to disable ASLR. Fall back diff --git a/scripts/build/ci.ts b/scripts/build/ci.ts index 5b7d7de2f985..d75cc2bd6c30 100644 --- a/scripts/build/ci.ts +++ b/scripts/build/ci.ts @@ -8,7 +8,7 @@ */ import { spawn as nodeSpawn, spawnSync } from "node:child_process"; -import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync } from "node:fs"; +import { cpSync, existsSync, mkdirSync, readdirSync, rmSync, statSync, writeFileSync } from "node:fs"; import { basename, relative, resolve } from "node:path"; import { fileURLToPath } from "node:url"; // @ts-ignore — utils.mjs has JSDoc types but no .d.ts @@ -335,6 +335,7 @@ function upload(paths: string[], cwd: string): void { function computeBunTriplet(cfg: Config): string { let t = `bun-${cfg.os}-${cfg.arch}`; if (cfg.abi === "musl") t += "-musl"; + if (cfg.abi === "android") t += "-android"; if (cfg.baseline) t += "-baseline"; return t; } @@ -363,12 +364,18 @@ export function packageAndUpload(cfg: Config, output: BunOutput): void { // Env vars match cmake's (BuildBun.cmake ~1462). // No setarch wrapper — cmake doesn't use one for features.mjs either // (only for the --revision smoke test). - console.log("Generating features.json..."); - run([exe, resolve(cfg.cwd, "scripts", "features.mjs")], buildDir, { - BUN_GARBAGE_COLLECTOR_LEVEL: "1", - BUN_DEBUG_QUIET_LOGS: "1", - BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1", - }); + // Cross-compiled binaries can't run on the build host — write a stub. + if (cfg.crossTarget !== undefined) { + console.log("Skipping features.json (cross-compiled binary cannot run on host)"); + writeFileSync(resolve(buildDir, "features.json"), JSON.stringify({ crossTarget: cfg.crossTarget })); + } else { + console.log("Generating features.json..."); + run([exe, resolve(cfg.cwd, "scripts", "features.mjs")], buildDir, { + BUN_GARBAGE_COLLECTOR_LEVEL: "1", + BUN_DEBUG_QUIET_LOGS: "1", + BUN_FEATURE_FLAG_INTERNAL_FOR_TESTING: "1", + }); + } const zipPaths: string[] = []; diff --git a/scripts/build/codegen.ts b/scripts/build/codegen.ts index dfde4bd41301..5514ad6e449c 100644 --- a/scripts/build/codegen.ts +++ b/scripts/build/codegen.ts @@ -93,6 +93,18 @@ function readPackageDeps(pkgDir: string): string[] { // Ninja rule registration // ─────────────────────────────────────────────────────────────────────────── +/** + * Node-style platform/arch strings for the TARGET (not the host running + * codegen). Passed as TARGET_PLATFORM/TARGET_ARCH so scripts that inline + * `process.platform` into bundled JS use the target's value. + */ +function codegenTarget(cfg: Config): { platform: string; arch: string } { + const platform = + cfg.abi === "android" ? "android" : cfg.os === "darwin" ? "darwin" : cfg.os === "windows" ? "win32" : "linux"; + const arch = cfg.x64 ? "x64" : "arm64"; + return { platform, arch }; +} + /** * Register ninja rules shared by all codegen steps. Call once before * emitCodegen(). @@ -104,16 +116,25 @@ export function registerCodegenRules(n: Ninja, cfg: Config): void { const q = (p: string) => quote(p, hostWin); const bun = q(cfg.bun); const esbuild = q(cfg.esbuild); + const { platform, arch } = codegenTarget(cfg); // Generic codegen: `cd && [env VARS] bun `. // Both `bun run script.ts` and `bun script.ts` go through this — the // caller puts the `run` subcommand in $args when needed. // + // TARGET_PLATFORM/ARCH: scripts that inline process.platform into the + // bundled JS modules (replacements.ts, bundle-modules.ts, + // create-hash-table.ts) read these so a cross-compiled binary doesn't + // ship with the build host's platform baked in. + // // restat = 1 because most scripts use writeIfNotChanged(). Scripts that // don't (generate-jssink, ci_info) always write → restat is a no-op for // them, no harm. + const env = hostWin + ? `set TARGET_PLATFORM=${platform}&& set TARGET_ARCH=${arch}&& ` + : `TARGET_PLATFORM=${platform} TARGET_ARCH=${arch} `; n.rule("codegen", { - command: hostWin ? `cmd /c "cd /d $cwd && ${bun} $args"` : `cd $cwd && ${bun} $args`, + command: hostWin ? `cmd /c "cd /d $cwd && ${env}${bun} $args"` : `cd $cwd && ${env}${bun} $args`, description: "gen $desc", restat: true, }); @@ -890,12 +911,8 @@ function emitObjectLuts({ n, cfg, o, dirStamp }: Ctx): void { [resolve(cfg.cwd, "src/bun.js/bindings/webcore/JSEvent.cpp"), resolve(cfg.codegenDir, "JSEvent.lut.h")], ]; - // create-hash-table.ts reads TARGET_PLATFORM env with process.platform - // fallback. We don't set it — cmake never did either. The preprocessing - // is OS-based (#if OS(WINDOWS) etc.) not arch-based, and bun only - // cross-compiles across arch on the same OS, so host platform == target - // OS. If cross-OS builds are ever added, thread the platform through - // argv here rather than shell env (which isn't portable to cmd.exe). + // create-hash-table.ts reads TARGET_PLATFORM env (set in registerCodegenRules) + // with process.platform fallback. for (const [src, out] of pairs) { n.build({ outputs: [out], diff --git a/scripts/build/config.ts b/scripts/build/config.ts index 66c192702e9d..1a2d489739af 100644 --- a/scripts/build/config.ts +++ b/scripts/build/config.ts @@ -7,19 +7,19 @@ */ import { execSync } from "node:child_process"; -import { existsSync, readFileSync, realpathSync } from "node:fs"; +import { existsSync, mkdirSync, readdirSync, readFileSync, realpathSync, symlinkSync } from "node:fs"; import { homedir, arch as hostArch, platform as hostPlatform } from "node:os"; import { isAbsolute, join, relative, resolve, sep } from "node:path"; import { NODEJS_ABI_VERSION, NODEJS_VERSION } from "./deps/nodejs-headers.ts"; import { WEBKIT_VERSION } from "./deps/webkit.ts"; -import { BuildError, assert } from "./error.ts"; +import { assert, BuildError } from "./error.ts"; import { clangTargetArch } from "./tools.ts"; import { cyan, dim, green } from "./tty.ts"; import { ZIG_COMMIT } from "./zig.ts"; export type OS = "linux" | "darwin" | "windows"; export type Arch = "x64" | "aarch64"; -export type Abi = "gnu" | "musl"; +export type Abi = "gnu" | "musl" | "android"; export type BuildType = "Debug" | "Release" | "RelWithDebInfo" | "MinSizeRel"; export type BuildMode = "full" | "cpp-only" | "zig-only" | "link-only"; export type WebKitMode = "prebuilt" | "local"; @@ -34,7 +34,8 @@ export type WebKitMode = "prebuilt" | "local"; * vs sh), quoting, and tool executable suffixes. * * For all other modes (full, cpp-only, link-only), host == target - * since we don't cross-compile C++. + * unless cfg.crossTarget is set (currently: Android), in which case + * the C++ side is cross-compiled via clang's --target/--sysroot. */ export interface Host { os: OS; @@ -200,6 +201,20 @@ export interface Config { /** SDK path from `xcrun --show-sdk-path`. Passed to deps as -DCMAKE_OSX_SYSROOT. */ osxSysroot: string | undefined; + // ─── Cross-compilation (set when host != target for C++) ─── + // Generic so future targets (e.g. cross-compiling to macOS from Linux) + // go through the same plumbing. Currently populated only for Android. + /** clang `--target=` triple, e.g. "aarch64-unknown-linux-android28". undefined = native. */ + crossTarget: string | undefined; + /** clang `--sysroot=` path. For Android: `/toolchains/llvm/prebuilt//sysroot`. */ + sysroot: string | undefined; + /** Android NDK root. undefined when abi != "android". */ + androidNdk: string | undefined; + /** Android API level (the N in `__ANDROID_API__=N`). undefined when abi != "android". */ + androidApiLevel: number | undefined; + /** NDK compiler-rt/libunwind dir: `/toolchains/llvm/prebuilt//lib/clang//lib/linux`. */ + androidNdkRuntimeDir: string | undefined; + // ─── Versioning ─── /** Bun's own version (from package.json). */ version: string; @@ -247,6 +262,10 @@ export interface PartialConfig { webkit?: WebKitMode; buildDir?: string; cacheDir?: string; + /** Override NDK location (default: $ANDROID_NDK_ROOT etc). Only used when abi=android. */ + androidNdk?: string; + /** Override Android API level (default: ANDROID_API_LEVEL_DEFAULT). Only used when abi=android. */ + androidApiLevel?: number; // Version pins (defaults in versions.ts). nodejsVersion?: string; nodejsAbiVersion?: string; @@ -344,11 +363,116 @@ export function detectHost(): Host { /** * Detect linux ABI (gnu vs musl) by checking for /etc/alpine-release. + * Android is never auto-detected — it's always a cross-compile target, + * so it must be requested explicitly via --abi=android. */ export function detectLinuxAbi(): Abi { return existsSync("/etc/alpine-release") ? "musl" : "gnu"; } +/** + * Minimum Android API level we target. 28 = Android 9 (2018), the oldest + * release with the bionic syscall wrappers we rely on without raw-syscall + * fallbacks. Covers ~96% of active devices as of 2026. + */ +export const ANDROID_API_LEVEL_DEFAULT = 28; + +/** + * Locate the Android NDK. Checks the conventional env vars in priority + * order, then a couple of well-known install paths. Returns undefined if + * none found — caller decides whether to error. + */ +export function detectAndroidNdk(): string | undefined { + for (const v of ["ANDROID_NDK_ROOT", "ANDROID_NDK_HOME", "ANDROID_NDK"]) { + const p = process.env[v]; + if (p && existsSync(join(p, "toolchains"))) return p; + } + for (const p of ["/opt/android-ndk", "/usr/local/android-ndk"]) { + if (existsSync(join(p, "toolchains"))) return p; + } + // Android Studio's sdkmanager puts NDKs under $ANDROID_HOME/ndk/. + // We don't pick one automatically — too easy to get a stale version. + return undefined; +} + +/** + * NDK toolchain prebuilt directory for the current build host. The NDK + * ships one prebuilt per host OS (always x86_64; arm64 macOS runs it + * under Rosetta). + */ +function ndkHostTag(host: Host): string { + switch (host.os) { + case "linux": + return "linux-x86_64"; + case "darwin": + return "darwin-x86_64"; + case "windows": + return "windows-x86_64"; + } +} + +/** + * Make the host clang able to link Android binaries by symlinking the + * NDK's compiler-rt builtins + libunwind into clang's resource dir. + * + * clang's driver emits a FULL PATH to `/lib// + * libclang_rt.builtins.a` — there's no `-L`-style search, so the file + * must exist at exactly that path. Our host clang has no Android-target + * compiler-rt; the NDK does. This is the standard "bring your own clang" + * setup for NDK cross-builds (Chromium does the same). + * + * Idempotent. Warns with a sudo hint if the resource dir isn't writable + * (CI build images create the symlinks as root in bootstrap.sh/Dockerfile). + */ +function linkNdkRuntimesIntoClang(cc: string, ndk: string, host: Host, triple: string): void { + const resourceDir = execSync(`"${cc}" -print-resource-dir`, { encoding: "utf8" }).trim(); + const targetDir = join(resourceDir, "lib", triple); + // NDK r23+ layout: /lib/clang//lib/linux// for + // libunwind.a + new-style libclang_rt.builtins.a + const ndkPrebuilt = join(ndk, "toolchains", "llvm", "prebuilt", ndkHostTag(host)); + const ndkClangLib = join(ndkPrebuilt, "lib", "clang"); + // NDK ships exactly one clang version per release. + const ndkClangVer = readdirSync(ndkClangLib)[0]; + if (ndkClangVer === undefined) { + throw new BuildError(`NDK clang resource dir not found under ${ndkClangLib}`); + } + const arch = triple.startsWith("x86_64") ? "x86_64" : "aarch64"; + const ndkRtLinux = join(ndkClangLib, ndkClangVer, "lib", "linux"); + // Populate BOTH layouts: apt.llvm.org clang uses old-style flat + // (lib/linux/libclang_rt.builtins--android.a) while tarball builds use + // per-triple (lib//libclang_rt.builtins.a). NDK r27 keeps builtins in + // the flat dir but libunwind in the per-arch subdir. + const flatDir = join(resourceDir, "lib", "linux"); + const links = { + [join(targetDir, "libclang_rt.builtins.a")]: join(ndkRtLinux, `libclang_rt.builtins-${arch}-android.a`), + [join(targetDir, "libunwind.a")]: join(ndkRtLinux, arch, "libunwind.a"), + [join(flatDir, `libclang_rt.builtins-${arch}-android.a`)]: join( + ndkRtLinux, + `libclang_rt.builtins-${arch}-android.a`, + ), + [join(flatDir, arch, "libunwind.a")]: join(ndkRtLinux, arch, "libunwind.a"), + }; + if (Object.keys(links).every(dst => existsSync(dst))) return; + try { + mkdirSync(targetDir, { recursive: true }); + mkdirSync(join(flatDir, arch), { recursive: true }); + for (const [dst, src] of Object.entries(links)) { + if (!existsSync(dst)) symlinkSync(src, dst); + } + } catch (cause) { + // Don't throw — zig-only mode doesn't need these, and on CI bootstrap.sh + // creates them as root during image build. The actual link step will fail + // loudly later if they're genuinely missing where needed. + const lnCmds = Object.entries(links) + .map(([dst, src]) => `sudo ln -sf "${src}" "${dst}"`) + .join(" && "); + console.warn( + `warning: could not link NDK compiler-rt into ${resourceDir} (${(cause as NodeJS.ErrnoException).code}). ` + + `If the final link fails on libclang_rt.builtins.a, run: sudo mkdir -p "${targetDir}" "${join(flatDir, arch)}" && ${lnCmds}`, + ); + } +} + /** * Resolve a PartialConfig into a full Config. * @@ -401,7 +525,9 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con // ASAN: default on for debug builds on arm64 macOS or linux const asanDefault = debug && ((darwin && arm64) || linux); - const asan = partial.asan ?? asanDefault; + // Android: force off. NDK ASAN deployment needs wrap.sh + runtime .so + // shipping alongside the binary; UBSan likewise. Not worth the matrix. + const asan = abi === "android" ? false : (partial.asan ?? asanDefault); // Zig ASAN follows ASAN unless explicitly overridden const zigAsan = partial.zigAsan ?? asan; @@ -416,8 +542,9 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con // LTO: default on only for CI release linux non-asan non-assertions const ltoDefault = release && linux && ci && !assertions && !asan; let lto = partial.lto ?? ltoDefault; - // ASAN and LTO don't mix — ASAN wins (silently, no warn — config is explicit) - if (asan && lto) { + // ASAN and LTO don't mix — ASAN wins (silently, no warn — config is explicit). + // Android: no LTO prebuilt WebKit exists; force off so the right tarball is fetched. + if ((asan && lto) || abi === "android") { lto = false; } @@ -443,8 +570,9 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con // failure is loud ("cannot find -l:libatomic.a") and the fix is obvious. const staticLibatomic = partial.staticLibatomic ?? true; - // TinyCC: off on Windows ARM64 (not supported), on elsewhere - const tinycc = partial.tinycc ?? !(windows && arm64); + // TinyCC: off on Windows ARM64 (not supported) and Android (no upstream + // bionic support; FFI cc() falls back to dlopen-only), on elsewhere. + const tinycc = partial.tinycc ?? !((windows && arm64) || abi === "android"); const valgrind = partial.valgrind ?? false; const fuzzilli = partial.fuzzilli ?? false; @@ -479,7 +607,43 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con assert(!baseline || x64, "baseline=true requires arch=x64 (baseline disables AVX which is x64-only)"); assert(!valgrind || linux, "valgrind=true requires os=linux"); assert(!(asan && valgrind), "Cannot enable both asan and valgrind simultaneously"); - assert(os !== "linux" || abi !== undefined, "Linux builds require an abi (gnu or musl)"); + assert(os !== "linux" || abi !== undefined, "Linux builds require an abi (gnu, musl, or android)"); + + // ─── Cross-compilation (Android) ─── + // We keep using the host's clang (same version everywhere) and pass + // --target/--sysroot. The NDK is needed only for its bionic sysroot, + // libc++, and compiler-rt — not for its bundled clang. + let crossTarget: string | undefined; + let sysroot: string | undefined; + let androidNdk: string | undefined; + let androidApiLevel: number | undefined; + let androidNdkRuntimeDir: string | undefined; + if (abi === "android") { + androidNdk = partial.androidNdk ?? detectAndroidNdk(); + if (androidNdk === undefined) { + throw new BuildError("--abi=android requires the Android NDK", { + hint: "Set ANDROID_NDK_ROOT or pass --android-ndk=. Download: https://developer.android.com/ndk/downloads", + }); + } + androidApiLevel = partial.androidApiLevel ?? ANDROID_API_LEVEL_DEFAULT; + const ndkPrebuilt = join(androidNdk, "toolchains", "llvm", "prebuilt", ndkHostTag(host)); + sysroot = join(ndkPrebuilt, "sysroot"); + if (!existsSync(sysroot)) { + throw new BuildError(`Android NDK sysroot not found at ${sysroot}`, { + hint: `Is ANDROID_NDK_ROOT (${androidNdk}) a valid NDK? Expected r26 or newer.`, + }); + } + // NDK ships exactly one clang version per release. + const ndkClangLib = join(ndkPrebuilt, "lib", "clang"); + const ndkClangVer = readdirSync(ndkClangLib)[0]; + if (ndkClangVer === undefined) { + throw new BuildError(`NDK clang resource dir not found under ${ndkClangLib}`); + } + androidNdkRuntimeDir = join(ndkClangLib, ndkClangVer, "lib", "linux"); + const llvmArch = arch === "x64" ? "x86_64" : "aarch64"; + crossTarget = `${llvmArch}-unknown-linux-android${androidApiLevel}`; + linkNdkRuntimesIntoClang(toolchain.cc, androidNdk, host, crossTarget); + } // ─── Versioning ─── const pkgJsonPath = resolve(cwd, "package.json"); @@ -573,6 +737,11 @@ export function resolveConfig(partial: PartialConfig, toolchain: Toolchain): Con nasm: toolchain.nasm, osxDeploymentTarget, osxSysroot, + crossTarget, + sysroot, + androidNdk, + androidApiLevel, + androidNdkRuntimeDir, version, revision, nodejsVersion, diff --git a/scripts/build/deps/cares.ts b/scripts/build/deps/cares.ts index dd92c37e37b7..8c97b2d16f25 100644 --- a/scripts/build/deps/cares.ts +++ b/scripts/build/deps/cares.ts @@ -176,10 +176,22 @@ const POSIX = def1([ // prettier-ignore const LINUX = def1([ "HAVE_MALLOC_H", "HAVE_SYS_EPOLL_H", "HAVE_SYS_RANDOM_H", - "HAVE_EPOLL", "HAVE_GETRANDOM", "HAVE_GETSERVBYPORT_R", "HAVE_GETSERVBYNAME_R", + "HAVE_EPOLL", "HAVE_GETRANDOM", "HAVE_MSG_NOSIGNAL", "HAVE_PIPE2", ]); +// glibc + musl have these; bionic only has the non-_r variants. +// prettier-ignore +const LINUX_NETDB_R = def1([ + "HAVE_GETSERVBYPORT_R", "HAVE_GETSERVBYNAME_R", +]); + +// Android: enables ares_sysconfig's net.dns1..4 fallback when JNI is unavailable. +// prettier-ignore +const ANDROID = def1([ + "HAVE___SYSTEM_PROPERTY_GET", +]); + // prettier-ignore const DARWIN = def1([ "HAVE_SYS_EVENT_H", "HAVE_SYS_SOCKIO_H", @@ -258,8 +270,12 @@ function configH(cfg: Config): string { if (cfg.windows) { platform = WINDOWS; types = WINDOWS_SOCKET_TYPES; + } else if (cfg.darwin) { + platform = `${POSIX}\n${DARWIN}`; + types = POSIX_SOCKET_TYPES; } else { - platform = `${POSIX}\n${cfg.darwin ? DARWIN : LINUX}`; + const abiExtra = cfg.abi === "android" ? ANDROID : LINUX_NETDB_R; + platform = `${POSIX}\n${LINUX}\n${abiExtra}`; types = POSIX_SOCKET_TYPES; } return `/* Generated by scripts/build/deps/cares.ts for ${cfg.os}-${cfg.arch} */ diff --git a/scripts/build/deps/libarchive.ts b/scripts/build/deps/libarchive.ts index c355bd660087..12fab21bcd7f 100644 --- a/scripts/build/deps/libarchive.ts +++ b/scripts/build/deps/libarchive.ts @@ -120,7 +120,9 @@ export const libarchive: Dependency = { sources: [...SOURCES, ...(cfg.windows ? SOURCES_WIN : [])].map(s => `libarchive/${s}.c`), // zlib's build dir holds the generated zlib.h (subst'd from .in) that // the gzip filter includes. Absolute path → emitDirect quotes it. - includes: ["libarchive", depBuildDir(cfg, "zlib")], + // android: archive.h does `#include ` under __ANDROID__; + // that header lives under contrib/android/include. + includes: ["libarchive", depBuildDir(cfg, "zlib"), ...(cfg.abi === "android" ? ["contrib/android/include"] : [])], pic: true, defines: { HAVE_CONFIG_H: 1, @@ -139,9 +141,9 @@ export const libarchive: Dependency = { headers: { "config.h": configH(cfg) }, }), - provides: () => ({ + provides: cfg => ({ libs: [], - includes: ["libarchive"], + includes: cfg.abi === "android" ? ["libarchive", "contrib/android/include"] : ["libarchive"], }), }; diff --git a/scripts/build/deps/lolhtml.ts b/scripts/build/deps/lolhtml.ts index 901f12f4612e..6bb3bf477f49 100644 --- a/scripts/build/deps/lolhtml.ts +++ b/scripts/build/deps/lolhtml.ts @@ -45,6 +45,12 @@ export const lolhtml: Dependency = { spec.rustTarget = "aarch64-pc-windows-msvc"; } + // Android: always a cross-compile. Static lib only, so cargo needs ar + // (any llvm-ar works) but no linker. + if (cfg.abi === "android") { + spec.rustTarget = cfg.arm64 ? "aarch64-linux-android" : "x86_64-linux-android"; + } + return spec; }, diff --git a/scripts/build/deps/webkit.ts b/scripts/build/deps/webkit.ts index dd082ef31466..215cb87baaff 100644 --- a/scripts/build/deps/webkit.ts +++ b/scripts/build/deps/webkit.ts @@ -57,6 +57,7 @@ import { type Dependency, type NestedCmakeBuild, type Source, depBuildDir, depSo function prebuiltSuffix(cfg: Config): string { let s = ""; if (cfg.linux && cfg.abi === "musl") s += "-musl"; + if (cfg.linux && cfg.abi === "android") s += "-android"; // Baseline WebKit artifacts (-march=nehalem, /arch:SSE2 ICU) exist for // Linux amd64 (glibc + musl) and Windows amd64. No baseline variant for // arm64 or macOS. Suffix order matches the release asset names: @@ -231,10 +232,47 @@ export const webkit: Dependency = { "-Wno-backend-plugin", ); } + // Android local mode: NOT using CMAKE_SYSTEM_NAME=Android because that + // module force-selects the NDK's bundled clang, overriding our + // CMAKE_{C,CXX}_COMPILER. Instead, treat it as a generic Linux + // cross-compile (CMAKE_SYSTEM_NAME=Linux + CMAKE_CROSSCOMPILING) and + // pass --target/--sysroot in CFLAGS. WebKit's source detects Android + // via __ANDROID__ (set by clang --target=*-android*); we set the cmake + // ANDROID variable manually so `if (ANDROID)` blocks trigger too. + if (cfg.abi === "android") { + const icuRoot = process.env.BUN_ANDROID_ICU_ROOT ?? "/tmp/icu-android"; + optFlags.push(`--target=${cfg.crossTarget!}`, `--sysroot=${cfg.sysroot!}`, `-isystem`, join(icuRoot, "include")); + } const optFlagStr = optFlags.join(" "); + let cxxOptFlagStr = optFlagStr; + if (cfg.abi === "android") { + const inc = join(cfg.sysroot!, "usr", "include"); + const triple = `${cfg.x64 ? "x86_64" : "aarch64"}-linux-android`; + cxxOptFlagStr += ` -nostdlibinc -isystem ${join(inc, "c++", "v1")} -isystem ${join(inc, triple)} -isystem ${inc}`; + } const args: Record = { CMAKE_C_FLAGS: optFlagStr, - CMAKE_CXX_FLAGS: optFlagStr, + CMAKE_CXX_FLAGS: cxxOptFlagStr, + ...(cfg.abi === "android" + ? { + CMAKE_SYSTEM_NAME: "Linux", + CMAKE_SYSTEM_PROCESSOR: cfg.arm64 ? "aarch64" : "x86_64", + CMAKE_SYSROOT: cfg.sysroot!, + ANDROID: "ON", + ENABLE_API_TESTS: "OFF", + // No system ICU on Android. Point at a static cross-built ICU + // (recipe in oven-sh/WebKit:Dockerfile.android). FindICU also probes + // CMAKE_FIND_ROOT_PATH so we whitelist the prefix. ICU_INCLUDE_DIR + // explicit: the NDK sysroot ships annotated headers that mark + // most ICU functions __INTRODUCED_IN(31), so FindICU picking + // those up makes everything unavailable at API 28. + ICU_ROOT: process.env.BUN_ANDROID_ICU_ROOT ?? "/tmp/icu-android", + ICU_INCLUDE_DIR: join(process.env.BUN_ANDROID_ICU_ROOT ?? "/tmp/icu-android", "include"), + CMAKE_FIND_ROOT_PATH_MODE_PACKAGE: "BOTH", + CMAKE_FIND_ROOT_PATH_MODE_LIBRARY: "BOTH", + CMAKE_FIND_ROOT_PATH_MODE_INCLUDE: "BOTH", + } + : {}), PORT: "JSCOnly", ENABLE_STATIC_JSC: "ON", USE_THIN_ARCHIVES: "OFF", @@ -329,8 +367,17 @@ export const webkit: Dependency = { // Windows ICU libs are NOT listed here — they're preBuild.outputs, // which source.ts appends to the resolved libs automatically. Listing // them here would make dep_build also claim to produce them (dup error). - // Posix uses system ICU (linked via -licu* in bun.ts). + // Posix uses system ICU (linked via -licu* in bun.ts). Android has no + // system ICU — link the static cross-built libs from BUN_ANDROID_ICU_ROOT. const libs = [...coreLibs(cfg), bmallocLib(cfg)]; + if (cfg.abi === "android") { + const icuRoot = process.env.BUN_ANDROID_ICU_ROOT ?? "/tmp/icu-android"; + libs.push( + resolve(icuRoot, "lib", "libicui18n.a"), + resolve(icuRoot, "lib", "libicuuc.a"), + resolve(icuRoot, "lib", "libicudata.a"), + ); + } const includes = [ // ABSOLUTE — resolved here because they're in the build dir, not src. @@ -344,6 +391,11 @@ export const webkit: Dependency = { ]; // Windows: ICU headers from preBuild output. if (cfg.windows) includes.push(resolve(icuDir(cfg), "include")); + // Android: ICU headers from BUN_ANDROID_ICU_ROOT (the NDK sysroot's + // unicode/ headers are __INTRODUCED_IN(31)-gated and unusable at API 28). + if (cfg.abi === "android") { + includes.push(resolve(process.env.BUN_ANDROID_ICU_ROOT ?? "/tmp/icu-android", "include")); + } return { libs, includes }; }, diff --git a/scripts/build/flags.ts b/scripts/build/flags.ts index c82583229499..89c235c124a4 100644 --- a/scripts/build/flags.ts +++ b/scripts/build/flags.ts @@ -43,9 +43,14 @@ export const cpuTargetFlags: Flag[] = [ }, { flag: ["-march=armv8-a+crc", "-mtune=ampere1"], - when: c => c.linux && c.arm64, + when: c => c.linux && c.arm64 && c.abi !== "android", desc: "ARM64 Linux: ARMv8-A base + CRC, tuned for Ampere (Graviton-like)", }, + { + flag: ["-march=armv8-a+crc", "-mtune=cortex-a78"], + when: c => c.linux && c.arm64 && c.abi === "android", + desc: "ARM64 Android: ARMv8-A base + CRC, tuned for Cortex-A78 (common big core)", + }, { flag: ["/clang:-march=armv8-a+crc", "/clang:-mtune=ampere1"], when: c => c.windows && c.arm64, @@ -70,6 +75,42 @@ export const cpuTargetFlags: Flag[] = [ // ═══════════════════════════════════════════════════════════════════════════ export const globalFlags: Flag[] = [ + // ─── Cross-compilation target/sysroot ─── + // Generic — currently only Android sets these. Kept first so the + // target triple is in effect before any arch-dependent flags. + { + flag: c => `--target=${c.crossTarget!}`, + when: c => c.crossTarget !== undefined, + desc: "Cross-compile target triple (clang is inherently a cross-compiler)", + }, + { + flag: c => `--sysroot=${c.sysroot!}`, + when: c => c.sysroot !== undefined, + desc: "Cross-compile sysroot (target libc headers + libs)", + }, + { + // -nostdlibinc drops ALL builtin system include dirs (keeping only the + // compiler resource dir), then we add back exactly the NDK paths. + // -nostdinc++ alone is insufficient on distro-packaged clang (apt.llvm.org + // on Ubuntu): the GCC-install detection still leaks /usr/include/c++/N, + // and NDK libc++'s #include_next finds host libstdc++ → ldiv_t/ + // cmath breakage. Order matters: libc++ first so its #include_next falls + // through to bionic. + flag: c => { + const inc = join(c.sysroot!, "usr", "include"); + const triple = `${c.x64 ? "x86_64" : "aarch64"}-linux-android`; + return ["-nostdlibinc", "-isystem", join(inc, "c++", "v1"), "-isystem", join(inc, triple), "-isystem", inc]; + }, + when: c => c.abi === "android", + lang: "cxx", + desc: "Android: drop all builtin includes, use only NDK libc++ + bionic", + }, + { + flag: ["-DANDROID", "-D_FILE_OFFSET_BITS=64"], + when: c => c.abi === "android", + desc: "Android: platform define + 64-bit off_t (bionic defaults to 32-bit on LP32)", + }, + // ─── CPU target ─── ...cpuTargetFlags, { @@ -442,7 +483,7 @@ export const bunOnlyFlags: Flag[] = [ "-fsanitize=returns-nonnull-attribute", "-fsanitize=unreachable", ], - when: c => c.unix && ((c.debug && c.abi !== "musl") || (c.release && c.asan)), + when: c => c.unix && ((c.debug && c.abi !== "musl" && c.abi !== "android") || (c.release && c.asan)), desc: "Undefined-behavior sanitizers", }, { @@ -460,9 +501,14 @@ export const bunOnlyFlags: Flag[] = [ }, { flag: ["-fno-pic", "-fno-pie"], - when: c => c.unix, + when: c => c.unix && c.abi !== "android", desc: "No position-independent code (we're a final executable)", }, + { + flag: "-fPIC", + when: c => c.abi === "android", + desc: "Android requires PIE since API 21; bionic's loader rejects non-PIE", + }, // ─── Warnings-as-errors (unix) ─── { @@ -620,7 +666,7 @@ export const linkerFlags: Flag[] = [ }, { flag: "-fsanitize=null", - when: c => c.unix && c.debug && c.abi !== "musl", + when: c => c.unix && c.debug && c.abi !== "musl" && c.abi !== "android", desc: "Link UBSan runtime", }, { @@ -735,12 +781,12 @@ export const linkerFlags: Flag[] = [ "-Wl,--wrap=powf", "-Wl,--wrap=quick_exit", ], - when: c => c.linux && c.abi !== "musl", + when: c => c.linux && c.abi === "gnu", desc: "Wrap glibc 2.18+ symbols (portable down to glibc 2.17)", }, { flag: ["-static-libstdc++", "-static-libgcc"], - when: c => c.linux && c.abi !== "musl", + when: c => c.linux && c.abi === "gnu", desc: "Static C++ runtime (don't depend on host libstdc++)", }, { @@ -748,6 +794,21 @@ export const linkerFlags: Flag[] = [ when: c => c.linux && c.abi === "musl", desc: "Dynamic C++ runtime on musl (static unavailable)", }, + { + flag: c => [ + `--target=${c.crossTarget!}`, + `--sysroot=${c.sysroot!}`, + "--rtlib=compiler-rt", + "--unwindlib=libunwind", + "-stdlib=libc++", + "-static-libstdc++", + // -l:libunwind.a (driver-emitted) searches -L paths; point at the NDK's + // own per-arch runtime dir so it resolves regardless of resource-dir layout. + `-L${join(c.androidNdkRuntimeDir!, c.arm64 ? "aarch64" : "x86_64")}`, + ], + when: c => c.linux && c.abi === "android", + desc: "Android link: target/sysroot + compiler-rt/libunwind + static libc++", + }, { // Paired with compile-side -fno-unwind-tables above. // Only in LTO builds — otherwise .eh_frame is needed for backtraces. @@ -767,9 +828,14 @@ export const linkerFlags: Flag[] = [ }, { flag: ["-fno-pic", "-Wl,-no-pie"], - when: c => c.linux, + when: c => c.linux && c.abi !== "android", desc: "No PIE (we don't need ASLR; simpler codegen)", }, + { + flag: ["-fPIC", "-pie"], + when: c => c.abi === "android", + desc: "Android: bionic loader requires PIE", + }, { flag: [ "-Wl,--as-needed", @@ -875,7 +941,7 @@ export const stripFlags: Flag[] = [ // PT_GNU_EH_FRAME phdr entry also survives as an orphan. See the // --no-eh-frame-hdr rationale in linkFlags above. flag: ["-R", ".eh_frame", "-R", ".eh_frame_hdr", "-R", ".gcc_except_table"], - when: c => c.linux && c.abi !== "musl", + when: c => c.linux && c.abi === "gnu", desc: "Remove unwind sections (GNU strip required — llvm-strip leaves [LOAD #2 [R]])", }, ]; @@ -950,7 +1016,7 @@ export const fileOverrides: FileOverride[] = [ // -fwhole-program-vtables requires -flto; disabling one requires // disabling the other or clang errors. extraFlags: ["-fno-lto", "-fno-whole-program-vtables"], - when: c => c.linux && c.lto && c.abi !== "musl", + when: c => c.linux && c.lto && c.abi === "gnu", desc: "Disable LTO: LLD 21 emits glibc versioned symbols (exp@GLIBC_2.17) into .lto_discard which fails to parse '@'", }, { diff --git a/scripts/build/profiles.ts b/scripts/build/profiles.ts index d12ae156adcf..32cfae523b93 100644 --- a/scripts/build/profiles.ts +++ b/scripts/build/profiles.ts @@ -47,6 +47,26 @@ export const profiles = { asan: false, }, + /** + * Android aarch64 cross-compile. Requires ANDROID_NDK_ROOT. + * Sanitizers are forced off in resolveConfig() regardless of profile. + */ + android: { + buildType: "Debug", + os: "linux", + arch: "aarch64", + abi: "android", + webkit: "prebuilt", + }, + + "android-release": { + buildType: "Release", + os: "linux", + arch: "aarch64", + abi: "android", + webkit: "prebuilt", + }, + /** Release build for local testing. No LTO (that's CI-only). */ release: { buildType: "Release", diff --git a/scripts/build/source.ts b/scripts/build/source.ts index 36b56bb971df..fe40cb87d8da 100644 --- a/scripts/build/source.ts +++ b/scripts/build/source.ts @@ -20,7 +20,7 @@ */ import { existsSync, mkdirSync, readFileSync, rmSync } from "node:fs"; -import { isAbsolute, join, resolve } from "node:path"; +import { dirname, isAbsolute, join, resolve } from "node:path"; import { ar, cc, cxx, nasm } from "./compile.ts"; import type { BuildType, Config } from "./config.ts"; import { assert } from "./error.ts"; @@ -581,6 +581,20 @@ export function registerDepRules(n: Ninja, cfg: Config): void { restat: true, pool: "dep", }); + // Cross-compile variant: ensure the rust std for the target triple is + // installed before building. CI images install rustup as a different + // user/HOME than the build runs under, so the target may be missing + // even though `rustup target add` ran at image-build time. Idempotent; + // chained at the ninja-shell level (no nested quoting). + const rustup = q(join(dirname(cfg.cargo), `rustup${cfg.host.exeSuffix}`)); + n.rule("dep_cargo_cross", { + command: + `${stream} $env ${rustup} target add $rust_target && ` + + `${stream} --cwd=$manifestdir $env ${q(cfg.cargo)} build $args`, + description: "cargo $name ($rust_target)", + restat: true, + pool: "dep", + }); } // preBuild: runs an arbitrary command before cmake configure. Used for @@ -1348,10 +1362,28 @@ function emitCargo(n: Ninja, cfg: Config, name: string, spec: CargoBuild, input: env[envKey] = cfg.msvcLinker; } + // Cross-compile (Android): cargo's default `cc` linker can't handle the + // foreign ELF objects. Use our clang as the linker driver and pass + // --target/--sysroot through, same as the C/C++ deps do via globalFlags. + // The cdylib output also wants -lunwind, which lives in the NDK's + // bundled clang resource dir (not the sysroot), so we add that -L too. + if (cfg.crossTarget !== undefined && spec.rustTarget !== undefined) { + const envKey = `CARGO_TARGET_${spec.rustTarget.toUpperCase().replace(/-/g, "_")}_LINKER`; + env[envKey] = cfg.cc; + const linkArgs = [`-Clink-arg=--target=${cfg.crossTarget}`]; + if (cfg.sysroot !== undefined) linkArgs.push(`-Clink-arg=--sysroot=${cfg.sysroot}`); + if (cfg.androidNdkRuntimeDir !== undefined) { + const llvmArch = cfg.arm64 ? "aarch64" : "x86_64"; + linkArgs.push(`-Clink-arg=-L${join(cfg.androidNdkRuntimeDir, llvmArch)}`); + } + env.CARGO_ENCODED_RUSTFLAGS = [...(spec.rustflags ?? []), ...linkArgs].join("\x1f"); + } + // ─── Emit build node ─── + const cross = cfg.crossTarget !== undefined && spec.rustTarget !== undefined; n.build({ outputs: [lib], - rule: "dep_cargo", + rule: cross ? "dep_cargo_cross" : "dep_cargo", inputs: [], // Rebuild if source changed or cargo binary changed. Cargo's own // dependency tracking handles file-level granularity below manifestDir. @@ -1360,6 +1392,7 @@ function emitCargo(n: Ninja, cfg: Config, name: string, spec: CargoBuild, input: name, manifestdir: manifestDir, args: quoteArgs(args, hostWin), + ...(cross ? { rust_target: spec.rustTarget! } : {}), // stream.ts's --env=K=V format. Values platform-quoted since ninja // passes the command line through the host's argv parser; stream.ts // receives them as proper argv entries. diff --git a/scripts/build/zig.ts b/scripts/build/zig.ts index c40ad30d95d5..e9250507087a 100644 --- a/scripts/build/zig.ts +++ b/scripts/build/zig.ts @@ -23,6 +23,7 @@ import type { Config } from "./config.ts"; import { downloadWithRetry, extractZip, tryPrefetchExtracted } from "./download.ts"; import { assert } from "./error.ts"; import { fetchCliPath } from "./fetch-cli.ts"; +import { writeIfChanged } from "./fs.ts"; import type { Ninja } from "./ninja.ts"; import { quote, quoteArgs } from "./shell.ts"; import { streamPath } from "./stream.ts"; @@ -93,9 +94,39 @@ export function zigTarget(cfg: Config): string { if (cfg.windows) return `${arch}-windows-msvc`; // linux: abi is always set (resolveConfig asserts) assert(cfg.abi !== undefined, "linux build missing abi"); + if (cfg.abi === "android") { + assert(cfg.androidApiLevel !== undefined, "android build missing api level"); + return `${arch}-linux-android.${cfg.androidApiLevel}`; + } return `${arch}-linux-${cfg.abi}`; } +/** + * Zig doesn't bundle bionic headers, so Android needs an explicit libc + * file (`--libc`) pointing at the NDK sysroot for Compile steps, and the + * sysroot path passed separately for translate-c. Writes the libc file at + * configure time (idempotent via writeIfChanged). + */ +function androidLibcArgs(cfg: Config): string[] { + if (cfg.abi !== "android") return []; + assert(cfg.sysroot !== undefined && cfg.androidApiLevel !== undefined, "android build missing sysroot"); + const archTriple = cfg.x64 ? "x86_64-linux-android" : "aarch64-linux-android"; + const libcFile = resolve(cfg.buildDir, "android-libc.txt"); + writeIfChanged( + libcFile, + [ + `include_dir=${cfg.sysroot}/usr/include`, + `sys_include_dir=${cfg.sysroot}/usr/include/${archTriple}`, + `crt_dir=${cfg.sysroot}/usr/lib/${archTriple}/${cfg.androidApiLevel}`, + `msvc_lib_dir=`, + `kernel32_lib_dir=`, + `gcc_dir=`, + ``, + ].join("\n"), + ); + return ["--libc", libcFile, `-Dandroid_ndk_sysroot=${cfg.sysroot}`]; +} + /** * Zig optimize level. * @@ -434,6 +465,7 @@ function zigBuildArgs(cfg: Config): string[] { `-Dtarget=${zigTarget(cfg)}`, `-Doptimize=${zigOptimize(cfg)}`, `-Dcpu=${zigCpu(cfg)}`, + ...androidLibcArgs(cfg), // Feature flags `-Denable_logs=${bool(cfg.logs)}`, diff --git a/src/Global.zig b/src/Global.zig index 1c6c1bf22a3f..718503170417 100644 --- a/src/Global.zig +++ b/src/Global.zig @@ -37,14 +37,19 @@ else if (Environment.is_canary) else std.fmt.comptimePrint(version_string ++ "+{s}", .{Environment.git_sha_short}); -pub const os_name = Environment.os.nameString(); +// Node-style platform string. Distinct from Environment.os.nameString() on +// Android: the kernel-level OS enum stays .linux (so syscall switches keep +// working), but user-facing strings — npm user-agent, process.platform — +// must be "android" so native-addon postinstalls don't fetch glibc binaries. +pub const os_name = if (Environment.isAndroid) "android" else Environment.os.nameString(); +pub const os_display = if (Environment.isAndroid) "Android" else Environment.os.displayString(); // Bun v1.0.0 (Linux x64 baseline) // Bun v1.0.0-debug (Linux x64) // Bun v1.0.0-canary.0+44e09bb7f (Linux x64) pub const unhandled_error_bun_version_string = "Bun v" ++ (if (Environment.is_canary) package_json_version_with_revision else package_json_version) ++ - " (" ++ Environment.os.displayString() ++ " " ++ arch_name ++ + " (" ++ os_display ++ " " ++ arch_name ++ (if (Environment.baseline) " baseline)" else ")"); pub const arch_name = if (Environment.isX64) diff --git a/src/analytics.zig b/src/analytics.zig index f62f8f44083d..caebfa0f873d 100644 --- a/src/analytics.zig +++ b/src/analytics.zig @@ -344,6 +344,10 @@ pub const GenerateHeader = struct { // Confusingly, the "release" tends to contain the kernel version much more frequently than the "version" field. const release = bun.sliceTo(&linux_os_name.release, 0); + if (comptime Environment.isAndroid) { + return analytics.Platform{ .os = analytics.OperatingSystem.android, .version = release, .arch = platform_arch }; + } + // Linux DESKTOP-P4LCIEM 5.10.16.3-microsoft-standard-WSL2 #1 SMP Fri Apr 2 22:23:49 UTC 2021 x86_64 x86_64 x86_64 GNU/Linux if (std.mem.indexOf(u8, release, "microsoft") != null) { return analytics.Platform{ .os = analytics.OperatingSystem.wsl, .version = release, .arch = platform_arch }; diff --git a/src/analytics/schema.zig b/src/analytics/schema.zig index 33d3fcadbbbb..85eba20e2760 100644 --- a/src/analytics/schema.zig +++ b/src/analytics/schema.zig @@ -335,6 +335,9 @@ pub const analytics = struct { /// wsl wsl, + /// android + android, + _, pub fn jsonStringify(self: @This(), writer: anytype) !void { diff --git a/src/bun.js/RuntimeTranspilerStore.zig b/src/bun.js/RuntimeTranspilerStore.zig index dff054ca309e..137dd27f0b5c 100644 --- a/src/bun.js/RuntimeTranspilerStore.zig +++ b/src/bun.js/RuntimeTranspilerStore.zig @@ -26,7 +26,7 @@ pub fn dumpSourceStringFailiable(vm: *VirtualMachine, specifier: string, written const dir = BunDebugHolder.dir orelse dir: { const base_name = switch (Environment.os) { - else => "/tmp/bun-debug-src/", + else => if (comptime Environment.isAndroid) "/data/local/tmp/bun-debug-src/" else "/tmp/bun-debug-src/", .windows => brk: { const temp = bun.fs.FileSystem.RealFS.platformTempDir(); var win_temp_buffer: bun.PathBuffer = undefined; diff --git a/src/bun.js/bindings/BunProcess.cpp b/src/bun.js/bindings/BunProcess.cpp index 1485fc7f5d0a..20ac58c15030 100644 --- a/src/bun.js/bindings/BunProcess.cpp +++ b/src/bun.js/bindings/BunProcess.cpp @@ -182,6 +182,8 @@ static JSValue constructPlatform(VM& vm, JSObject* processObject) { #if defined(__APPLE__) return JSC::jsString(vm, makeAtomString("darwin"_s)); +#elif defined(__ANDROID__) + return JSC::jsString(vm, makeAtomString("android"_s)); #elif defined(__linux__) return JSC::jsString(vm, makeAtomString("linux"_s)); #elif OS(WINDOWS) diff --git a/src/bun.js/bindings/NodeVM.cpp b/src/bun.js/bindings/NodeVM.cpp index 5b06b52f669e..2e5528b7be77 100644 --- a/src/bun.js/bindings/NodeVM.cpp +++ b/src/bun.js/bindings/NodeVM.cpp @@ -347,7 +347,12 @@ static JSPromise* importModuleInner(JSGlobalObject* globalObject, JSString* modu JSObject* thenResult = promise->then(globalObject, transformer, globalObject->promiseEmptyOnRejectedFunction()); RETURN_IF_EXCEPTION(scope, nullptr); - RELEASE_AND_RETURN(scope, uncheckedDowncast(thenResult)); + // JSPromise::then() may return a non-JSPromise when Promise[Symbol.species] + // is overridden in the vm context — don't uncheckedDowncast (release-mode + // static_cast) on a user-influenced shape. + if (auto* thenPromise = dynamicDowncast(thenResult)) + RELEASE_AND_RETURN(scope, thenPromise); + RELEASE_AND_RETURN(scope, JSPromise::resolvedPromise(globalObject, thenResult)); } // Helper function to create an anonymous function expression with parameters diff --git a/src/bun.js/bindings/TextEncodingRegistry.cpp b/src/bun.js/bindings/TextEncodingRegistry.cpp index 356537245b06..1e2a865cd92d 100644 --- a/src/bun.js/bindings/TextEncodingRegistry.cpp +++ b/src/bun.js/bindings/TextEncodingRegistry.cpp @@ -29,7 +29,12 @@ // config.h removed - not needed in Bun #include "TextEncodingRegistry.h" -// #include "Logging.h" - not available in Bun +// #include "Logging.h" - not available in Bun. On platforms where +// RELEASE_LOG is enabled (Android), RELEASE_LOG_ERROR references the +// PAL TextEncoding log channel which Bun doesn't compile, so route it +// to plain LOG_ERROR (the same expansion non-Android platforms get). +#undef RELEASE_LOG_ERROR +#define RELEASE_LOG_ERROR(channel, ...) LOG_ERROR(__VA_ARGS__) #include "TextCodecCJK.h" // TextCodecICU removed - ICU data not available // Native UTF-8, UTF-16, Latin1 support - removed includes diff --git a/src/bun.js/bindings/bun-spawn.cpp b/src/bun.js/bindings/bun-spawn.cpp index 6d9191417afb..be7157efba44 100644 --- a/src/bun.js/bindings/bun-spawn.cpp +++ b/src/bun.js/bindings/bun-spawn.cpp @@ -141,7 +141,9 @@ extern "C" ssize_t posix_spawn_bun( sigfillset(&blockall); sigprocmask(SIG_SETMASK, &blockall, &oldmask); +#if !OS(ANDROID) pthread_setcancelstate(PTHREAD_CANCEL_DISABLE, &cs); +#endif #if OS(LINUX) // On Linux, use vfork() for performance. The parent is suspended until @@ -380,7 +382,11 @@ extern "C" ssize_t posix_spawn_bun( #endif sigprocmask(SIG_SETMASK, &oldmask, 0); +#if !OS(ANDROID) pthread_setcancelstate(cs, 0); +#else + (void)cs; +#endif return res; } diff --git a/src/bun.js/modules/NodeModuleModule.cpp b/src/bun.js/modules/NodeModuleModule.cpp index edae8e0353cb..8415a05e2bc4 100644 --- a/src/bun.js/modules/NodeModuleModule.cpp +++ b/src/bun.js/modules/NodeModuleModule.cpp @@ -785,11 +785,6 @@ JSC_DEFINE_HOST_FUNCTION(jsFunctionLoad, (JSGlobalObject * globalObject, JSC::Ca return JSC::JSValue::encode(JSC::jsUndefined()); } -static JSC::EncodedJSValue resolverFunctionCallback(JSC::JSGlobalObject* globalObject, JSC::CallFrame* callFrame) -{ - return JSC::JSValue::encode(JSC::jsUndefined()); -} - extern "C" void Bun__VirtualMachine__setOverrideModuleRunMainPromise(void* bunVM, JSPromise* promise); JSC_DEFINE_HOST_FUNCTION(jsFunctionRunMain, (JSGlobalObject * globalObject, JSC::CallFrame* callFrame)) { diff --git a/src/bun.js/node/node_fs.zig b/src/bun.js/node/node_fs.zig index e672f01b2bb9..cbd1604f58cf 100644 --- a/src/bun.js/node/node_fs.zig +++ b/src/bun.js/node/node_fs.zig @@ -3859,6 +3859,11 @@ pub const NodeFS = struct { if (comptime Environment.isWindows) { return Maybe(Return.Lchmod).todo(); } + if (comptime Environment.isAndroid) { + // bionic has no lchmod(); symlink modes are meaningless on Linux + // anyway. Match glibc's stub behaviour. + return .{ .err = .{ .errno = @intFromEnum(bun.sys.E.OPNOTSUPP), .syscall = .lchmod, .path = args.path.slice() } }; + } const path = args.path.sliceZ(&this.sync_error_buf); return Maybe(Return.Lchmod).errnoSysP(c.lchmod(path, @truncate(args.mode)), .lchmod, path) orelse diff --git a/src/bun.js/node/node_os.zig b/src/bun.js/node/node_os.zig index 0e55f406dd55..87ef6e9b650d 100644 --- a/src/bun.js/node/node_os.zig +++ b/src/bun.js/node/node_os.zig @@ -64,7 +64,22 @@ fn cpusImplLinux(globalThis: *jsc.JSGlobalObject) !jsc.JSValue { // Read /proc/stat to get number of CPUs and times { - const file = try std.fs.cwd().openFile("/proc/stat", .{}); + const file = std.fs.cwd().openFile("/proc/stat", .{}) catch { + // hidepid mounts (common on Android) deny /proc/stat. lazyCpus in os.ts + // pre-creates hostCpuCount lazy proxies, so return that many stub + // entries (zeroed times / unknown model / speed 0) — matches Node. + const count: u32 = @intCast(@max(1, bun_sysconf__SC_NPROCESSORS_ONLN())); + const stubs = try jsc.JSValue.createEmptyArray(globalThis, count); + var i: u32 = 0; + while (i < count) : (i += 1) { + const cpu = jsc.JSValue.createEmptyObject(globalThis, 3); + cpu.put(globalThis, jsc.ZigString.static("times"), (CPUTimes{}).toValue(globalThis)); + cpu.put(globalThis, jsc.ZigString.static("model"), jsc.ZigString.static("unknown").withEncoding().toJS(globalThis)); + cpu.put(globalThis, jsc.ZigString.static("speed"), jsc.JSValue.jsNumber(0)); + try stubs.putIndex(globalThis, i, cpu); + } + return stubs; + }; defer file.close(); const read = try bun.sys.File.from(file).readToEndWithArrayList(&file_buf, .probably_small).unwrap(); @@ -368,6 +383,11 @@ pub fn homedir(global: *jsc.JSGlobalObject) !bun.String { } if (result == null) { + // bionic has no passwd entries for app uids; with HOME also unset + // (zygote/run-as), return a usable default rather than throwing. + if (comptime Environment.isAndroid) { + return bun.String.static("/data/local/tmp"); + } // in uv__getpwuid_r, null result throws UV_ENOENT. return global.throwValue(try bun.sys.Error.fromCode( .NOENT, @@ -463,10 +483,18 @@ fn networkInterfacesPosix(globalThis: *jsc.JSGlobalObject) bun.JSError!jsc.JSVal var interface_start: ?*c.ifaddrs = null; const rc = c.getifaddrs(&interface_start); if (rc != 0) { + const errno = std.posix.errno(rc); + // Android API 30+: SELinux denies the netlink socket getifaddrs uses. + // Node returns {} rather than throwing. + if (comptime Environment.isAndroid) { + if (errno == .ACCES or errno == .PERM) { + return jsc.JSValue.createEmptyObject(globalThis, 0); + } + } const err = jsc.SystemError{ .message = bun.String.static("A system error occurred: getifaddrs returned an error"), .code = bun.String.static("ERR_SYSTEM_ERROR"), - .errno = @intFromEnum(std.posix.errno(rc)), + .errno = @intFromEnum(errno), .syscall = bun.String.static("getifaddrs"), }; diff --git a/src/cli/run_command.zig b/src/cli/run_command.zig index 11017ea7e06e..62ed841aa52c 100644 --- a/src/cli/run_command.zig +++ b/src/cli/run_command.zig @@ -38,6 +38,7 @@ pub const RunCommand = struct { "/usr/bin/sh", // don't think this is a real one "/usr/bin/zsh", "/usr/local/bin/zsh", + "/system/bin/sh", // Android }; inline for (hardcoded_popular_ones) |shell| { if (Try.shell(shell)) { @@ -607,7 +608,7 @@ pub const RunCommand = struct { .windows => @compileError("Do not use RunCommand.bun_node_dir on Windows"), .mac => "/private/tmp", - else => "/tmp", + else => if (Environment.isAndroid) "/data/local/tmp" else "/tmp", } ++ if (!Environment.isDebug) "/bun-node" ++ if (Environment.git_sha_short.len > 0) "-" ++ Environment.git_sha_short else "" else diff --git a/src/cli/upgrade_command.zig b/src/cli/upgrade_command.zig index 036d9d9e7543..0cd5013974e5 100644 --- a/src/cli/upgrade_command.zig +++ b/src/cli/upgrade_command.zig @@ -44,7 +44,7 @@ pub const Version = struct { pub const arch_label = if (Environment.isAarch64) "aarch64" else "x64"; pub const triplet = platform_label ++ "-" ++ arch_label; - const suffix_abi = if (Environment.isMusl) "-musl" else ""; + const suffix_abi = if (Environment.isMusl) "-musl" else if (Environment.isAndroid) "-android" else ""; const suffix_cpu = if (Environment.baseline) "-baseline" else ""; const suffix = suffix_abi ++ suffix_cpu; pub const folder_name = "bun-" ++ triplet ++ suffix; diff --git a/src/codegen/bundle-modules.ts b/src/codegen/bundle-modules.ts index c282da4560bb..48aa65f97c1a 100644 --- a/src/codegen/bundle-modules.ts +++ b/src/codegen/bundle-modules.ts @@ -555,8 +555,8 @@ for (const file of evalFiles) { format: "esm", env: "disable", define: { - "process.platform": JSON.stringify(process.platform), - "process.arch": JSON.stringify(process.arch), + "process.platform": JSON.stringify(process.env.TARGET_PLATFORM ?? process.platform), + "process.arch": JSON.stringify(process.env.TARGET_ARCH ?? process.arch), }, }); writeIfNotChanged(path.join(CODEGEN_DIR, "eval", path.basename(file)), await output.text()); diff --git a/src/compile_target.zig b/src/compile_target.zig index a1400d015104..e5e0bfcee65b 100644 --- a/src/compile_target.zig +++ b/src/compile_target.zig @@ -14,7 +14,7 @@ version: bun.Semver.Version = .{ .minor = @truncate(Environment.version.minor), .patch = @truncate(Environment.version.patch), }, -libc: Libc = if (!Environment.isMusl) .default else .musl, +libc: Libc = if (Environment.isMusl) .musl else if (Environment.isAndroid) .android else .default, const Libc = enum { /// The default libc for the target @@ -22,19 +22,20 @@ const Libc = enum { default, /// musl libc musl, + /// bionic (Android) + android, /// npm package name, `@oven-sh/bun-{os}-{arch}` pub fn npmName(this: Libc) []const u8 { return switch (this) { .default => "", .musl => "-musl", + .android => "-android", }; } pub fn format(self: @This(), writer: *std.Io.Writer) std.Io.Writer.Error!void { - if (self == .musl) { - try writer.writeAll("-musl"); - } + try writer.writeAll(self.npmName()); } }; @@ -364,12 +365,16 @@ pub fn tryFrom(input_: []const u8) ParseError!CompileTarget { this.libc = .musl; found_libc = true; continue; + } else if (strings.eqlComptime(token, "android")) { + this.libc = .android; + found_libc = true; + continue; } else { return error.UnsupportedTarget; } } - if (!found_libc and this.libc == .musl and this.os != .linux) { + if (!found_libc and this.libc != .default and this.os != .linux) { // "bun-windows-x64" should not implicitly be "bun-windows-x64-musl" this.libc = .default; } @@ -386,7 +391,7 @@ pub fn tryFrom(input_: []const u8) ParseError!CompileTarget { this.baseline = false; } - if (this.libc == .musl and this.os != .linux) { + if (this.libc != .default and this.os != .linux) { return error.InvalidTarget; } @@ -411,6 +416,7 @@ pub fn from(input_: []const u8) CompileTarget { !strings.eqlComptime(token, "modern") and !strings.eqlComptime(token, "baseline") and !strings.eqlComptime(token, "musl") and + !strings.eqlComptime(token, "android") and !(strings.hasPrefixComptime(token, "v1.") or strings.hasPrefixComptime(token, "v0."))) { unsupported_token = token; @@ -436,6 +442,8 @@ pub fn from(input_: []const u8) CompileTarget { const input = bun.strings.trim(input_, " \t\r"); if (strings.containsComptime(input, "musl") and !strings.containsComptime(input, "linux")) { Output.errGeneric("invalid target, musl libc only exists on linux", .{}); + } else if (strings.containsComptime(input, "android") and !strings.containsComptime(input, "linux")) { + Output.errGeneric("invalid target, android only exists with linux (use bun-linux-arm64-android)", .{}); } else if (strings.containsComptime(input, "wasm")) { Output.errGeneric("invalid target, WebAssembly is not supported. Sorry!", .{}); } else if (strings.containsComptime(input, "v")) { @@ -462,19 +470,22 @@ pub fn defineValues(this: *const CompileTarget) []const []const u8 { // Use inline else to avoid extra allocations. switch (this.os) { inline else => |os| switch (this.arch) { - inline .arm64, .x64 => |arch| return struct { - pub const values = &.{ - "\"" ++ os.nameString() ++ "\"", - - switch (arch) { - .x64 => "\"x64\"", - .arm64 => "\"arm64\"", - .wasm => @compileError("TODO"), - }, - - "\"" ++ Global.package_json_version ++ "\"", - }; - }.values, + inline .arm64, .x64 => |arch| switch (this.libc) { + inline else => |libc| return struct { + pub const values = &.{ + // process.platform: Node reports "android" on Android, not "linux". + if (libc == .android) "\"android\"" else "\"" ++ os.nameString() ++ "\"", + + switch (arch) { + .x64 => "\"x64\"", + .arm64 => "\"arm64\"", + .wasm => @compileError("TODO"), + }, + + "\"" ++ Global.package_json_version ++ "\"", + }; + }.values, + }, else => @panic("TODO"), }, } diff --git a/src/crash_handler.zig b/src/crash_handler.zig index 58a6a191bdcf..f8c84d08b09f 100644 --- a/src/crash_handler.zig +++ b/src/crash_handler.zig @@ -351,7 +351,7 @@ pub fn crashHandler( const desired_begin_addr = begin_addr orelse @returnAddress(); std.debug.captureStackTrace(desired_begin_addr, &trace_buf); - if (comptime bun.Environment.isLinux and !bun.Environment.isMusl) { + if (comptime bun.Environment.isGlibc) { var addr_buf_libc: [20]usize = undefined; var trace_buf_libc: std.builtin.StackTrace = .{ .index = 0, @@ -964,7 +964,7 @@ pub fn printMetadata(writer: anytype) !void { { const platform = bun.analytics.GenerateHeader.GeneratePlatform.forOS(); const cpu_features = CPUFeatures.get(); - if (bun.Environment.isLinux and !bun.Environment.isMusl) { + if (bun.Environment.isGlibc) { const version = gnu_get_libc_version() orelse ""; const kernel_version = bun.analytics.GenerateHeader.GeneratePlatform.kernelVersion(); if (platform.os == .wsl) { @@ -975,6 +975,9 @@ pub fn printMetadata(writer: anytype) !void { } else if (bun.Environment.isLinux and bun.Environment.isMusl) { const kernel_version = bun.analytics.GenerateHeader.GeneratePlatform.kernelVersion(); try writer.print("Linux Kernel v{d}.{d}.{d} | musl\n", .{ kernel_version.major, kernel_version.minor, kernel_version.patch }); + } else if (bun.Environment.isAndroid) { + const kernel_version = bun.analytics.GenerateHeader.GeneratePlatform.kernelVersion(); + try writer.print("Android Kernel v{d}.{d}.{d} | bionic\n", .{ kernel_version.major, kernel_version.minor, kernel_version.patch }); } else if (bun.Environment.isMac) { try writer.print("macOS v{s}\n", .{platform.version}); } else if (bun.Environment.isWindows) { diff --git a/src/deps/c_ares.zig b/src/deps/c_ares.zig index cf95893ca452..a4ddd1062e4d 100644 --- a/src/deps/c_ares.zig +++ b/src/deps/c_ares.zig @@ -580,9 +580,6 @@ pub const Channel = opaque { libraryInit(); - if (Error.get(ares_init(&channel))) |err| { - return err; - } const SockStateWrap = struct { pub fn onSockState(ctx: ?*anyopaque, socket: ares_socket_t, readable: c_int, writable: c_int) callconv(.c) void { const container = bun.cast(*Container, ctx.?); @@ -592,7 +589,12 @@ pub const Channel = opaque { var opts = bun.zero(Options); - opts.flags = ARES_FLAG_NOCHECKRESP; + opts.flags = ARES_FLAG_NOCHECKRESP | + // Android: c-ares can't auto-discover servers (no /etc/resolv.conf, + // no JNI). Without this flag it silently uses 127.0.0.1 and every + // query times out ~20s; with it, init returns ENOSERVERS so the + // caller can seed via setServers() or fall back to .system. + (if (bun.Environment.isAndroid) ARES_FLAG_NO_DFLT_SVR else 0); opts.sock_state_cb = &SockStateWrap.onSockState; opts.sock_state_cb_data = @as(*anyopaque, @ptrCast(this)); opts.timeout = options.timeout orelse -1; @@ -1796,8 +1798,12 @@ pub const Error = enum(i32) { } if (comptime bun.Environment.isLinux) { + if (eai == .SOCKTYPE) return Error.ECONNREFUSED; + } + if (comptime bun.Environment.isGlibc) { + // glibc-only async getaddrinfo_a / IDN extensions; absent on + // musl and bionic. switch (eai) { - .SOCKTYPE => return Error.ECONNREFUSED, .IDN_ENCODE => return Error.EBADSTR, .ALLDONE => return Error.ENOTFOUND, .INPROGRESS => return Error.ETIMEOUT, @@ -1901,6 +1907,7 @@ pub const ARES_FLAG_STAYOPEN = @as(c_int, 1) << @as(c_int, 4); pub const ARES_FLAG_NOSEARCH = @as(c_int, 1) << @as(c_int, 5); pub const ARES_FLAG_NOALIASES = @as(c_int, 1) << @as(c_int, 6); pub const ARES_FLAG_NOCHECKRESP = @as(c_int, 1) << @as(c_int, 7); +pub const ARES_FLAG_NO_DFLT_SVR = @as(c_int, 1) << @as(c_int, 9); pub const ARES_FLAG_EDNS = @as(c_int, 1) << @as(c_int, 8); pub const ARES_OPT_FLAGS = @as(c_int, 1) << @as(c_int, 0); pub const ARES_OPT_TIMEOUT = @as(c_int, 1) << @as(c_int, 1); diff --git a/src/dns.zig b/src/dns.zig index a9c43a0e5c10..39efb1030ce4 100644 --- a/src/dns.zig +++ b/src/dns.zig @@ -288,7 +288,10 @@ pub const GetAddrInfo = struct { pub const default: GetAddrInfo.Backend = switch (bun.Environment.os) { .mac, .windows => .system, - else => .c_ares, + // Android: c-ares can't discover nameservers (no /etc/resolv.conf, + // no JNI for ares_library_init_android). bionic getaddrinfo proxies + // through netd which knows the real resolvers. + else => if (bun.Environment.isAndroid) .system else .c_ares, }; pub const FromJSError = JSError || error{ diff --git a/src/env.zig b/src/env.zig index 4d4fa2bbdaf0..c97c1f76d478 100644 --- a/src/env.zig +++ b/src/env.zig @@ -21,6 +21,8 @@ pub const isAarch64 = builtin.target.cpu.arch.isAARCH64(); pub const isX86 = builtin.target.cpu.arch.isX86(); pub const isX64 = builtin.target.cpu.arch == .x86_64; pub const isMusl = builtin.target.abi.isMusl(); +pub const isAndroid = builtin.target.abi.isAndroid(); +pub const isGlibc = isLinux and builtin.target.abi.isGnu(); pub const allow_assert = isDebug or isTest or std.builtin.OptimizeMode.ReleaseSafe == builtin.mode; pub const ci_assert = isDebug or isTest or enable_asan or (std.builtin.OptimizeMode.ReleaseSafe == builtin.mode and is_canary); pub const show_crash_trace = isDebug or isTest or enable_asan; diff --git a/src/fs.zig b/src/fs.zig index b11806186d94..3721d02905f3 100644 --- a/src/fs.zig +++ b/src/fs.zig @@ -579,7 +579,7 @@ pub const FileSystem = struct { ) catch |err| bun.handleOom(err); }, .mac => "/private/tmp", - else => "/tmp", + else => if (comptime Environment.isAndroid) "/data/local/tmp" else "/tmp", }; } diff --git a/src/install/PackageManager.zig b/src/install/PackageManager.zig index 31a94b30d071..4e7aaa8d30b4 100644 --- a/src/install/PackageManager.zig +++ b/src/install/PackageManager.zig @@ -495,14 +495,13 @@ var ensureTempNodeGypScriptOnce = bun.once(struct { \\) \\ , - else => - \\#!/bin/sh - \\if [ "x$npm_config_node_gyp" = "x" ]; then - \\ bun x --silent node-gyp $@ - \\else - \\ "$npm_config_node_gyp" $@ - \\fi - \\ + else => (if (Environment.isAndroid) "#!/system/bin/sh\n" else "#!/bin/sh\n") ++ + \\if [ "x$npm_config_node_gyp" = "x" ]; then + \\ bun x --silent node-gyp $@ + \\else + \\ "$npm_config_node_gyp" $@ + \\fi + \\ , }; diff --git a/src/install/npm.zig b/src/install/npm.zig index 64493fe600b6..d0b047f16e19 100644 --- a/src/install/npm.zig +++ b/src/install/npm.zig @@ -643,7 +643,7 @@ pub const OperatingSystem = enum(u16) { pub const all_value: u16 = aix | darwin | freebsd | linux | openbsd | sunos | win32 | android; pub const current: OperatingSystem = switch (Environment.os) { - .linux => @enumFromInt(linux), + .linux => @enumFromInt(if (Environment.isAndroid) android else linux), .mac => @enumFromInt(darwin), .windows => @enumFromInt(win32), .wasm => @compileError("Unsupported operating system: " ++ @tagName(Environment.os)), diff --git a/src/js/internal/html.ts b/src/js/internal/html.ts index 1af882828ace..d47469ded933 100644 --- a/src/js/internal/html.ts +++ b/src/js/internal/html.ts @@ -384,6 +384,10 @@ yourself with Bun.serve(). Bun.spawn(["open", url]).exited.catch(() => {}); } else if (process.platform === "win32") { Bun.spawn(["start", url]).exited.catch(() => {}); + } else if (process.platform === "android") { + Bun.spawn(["/system/bin/am", "start", "-a", "android.intent.action.VIEW", "-d", url]).exited.catch( + () => {}, + ); } else { Bun.spawn(["xdg-open", url]).exited.catch(() => {}); } diff --git a/src/js/node/net.ts b/src/js/node/net.ts index e4a4952905a8..ad1e87b7d0d5 100644 --- a/src/js/node/net.ts +++ b/src/js/node/net.ts @@ -2250,7 +2250,7 @@ Server.prototype.listen = function listen(port, hostname, onListen) { port = 0; } - const isLinux = process.platform === "linux"; + const isLinux = process.platform === "linux" || process.platform === "android"; if (!Number.isSafeInteger(port) || port < 0) { if (path) { diff --git a/src/js/node/os.ts b/src/js/node/os.ts index 64b1a1c0ac9b..6aa9a9e1c098 100644 --- a/src/js/node/os.ts +++ b/src/js/node/os.ts @@ -12,7 +12,8 @@ var tmpdir = function () { } return path; } - var path = env["TMPDIR"] || env["TMP"] || env["TEMP"] || "/tmp"; + var path = + env["TMPDIR"] || env["TMP"] || env["TEMP"] || (process.platform === "android" ? "/data/local/tmp" : "/tmp"); const length = path.length; if (length > 1 && path[length - 1] === "/") path = path.slice(0, -1); return path; @@ -120,7 +121,7 @@ function bound(binding) { ? "Windows_NT" : process.platform === "darwin" ? "Darwin" - : process.platform === "linux" + : process.platform === "linux" || process.platform === "android" ? "Linux" : $bundleError("TODO: type"); }, @@ -128,8 +129,12 @@ function bound(binding) { userInfo: binding.userInfo, version: binding.version, machine: function () { - return process.arch === "arm64" // - ? "arm64" + // TODO: linux arm64 should also return "aarch64" (Node/uname compat) — + // separate PR to avoid behavior change in the Android port. + return process.arch === "arm64" + ? process.platform === "android" + ? "aarch64" + : "arm64" : process.arch === "x64" ? "x86_64" : $bundleError("TODO: machine"); diff --git a/src/napi/napi.zig b/src/napi/napi.zig index 5792849138ad..fc922bbc3152 100644 --- a/src/napi/napi.zig +++ b/src/napi/napi.zig @@ -1998,16 +1998,14 @@ const V8API = if (!bun.Environment.isWindows) struct { pub extern fn @"?FromJustIsNothing@api_internal@v8@@YAXXZ"() *anyopaque; }; -/// V8 API functions whose mangled name differs between Linux and macOS -const posix_platform_specific_v8_apis = switch (bun.Environment.os) { - .mac => struct { - pub extern fn _ZN2v85Array3NewENS_5LocalINS_7ContextEEEmNSt3__18functionIFNS_10MaybeLocalINS_5ValueEEEvEEE() *anyopaque; - }, - .linux => struct { - pub extern fn _ZN2v85Array3NewENS_5LocalINS_7ContextEEEmSt8functionIFNS_10MaybeLocalINS_5ValueEEEvEE() *anyopaque; - }, - .windows => struct {}, - else => unreachable, +/// V8 API functions whose mangled name differs by C++ stdlib namespace: +/// libstdc++ = std::, Apple libc++ = std::__1::, NDK libc++ = std::__ndk1::. +const posix_platform_specific_v8_apis = if (bun.Environment.os == .windows) struct {} else if (bun.Environment.isAndroid) struct { + pub extern fn _ZN2v85Array3NewENS_5LocalINS_7ContextEEEmNSt6__ndk18functionIFNS_10MaybeLocalINS_5ValueEEEvEEE() *anyopaque; +} else if (bun.Environment.isMac) struct { + pub extern fn _ZN2v85Array3NewENS_5LocalINS_7ContextEEEmNSt3__18functionIFNS_10MaybeLocalINS_5ValueEEEvEEE() *anyopaque; +} else struct { + pub extern fn _ZN2v85Array3NewENS_5LocalINS_7ContextEEEmSt8functionIFNS_10MaybeLocalINS_5ValueEEEvEE() *anyopaque; }; // To update this list, use find + multi-cursor in your editor. diff --git a/src/open.zig b/src/open.zig index d2878e2e75ab..a0ab71b06e26 100644 --- a/src/open.zig +++ b/src/open.zig @@ -12,11 +12,13 @@ fn fallback(url: string) void { pub fn openURL(url: stringZ) void { if (comptime Environment.isWasi) return fallback(url); - var args_buf = [_]stringZ{ opener, url }; + var am_args = [_]stringZ{ "/system/bin/am", "start", "-a", "android.intent.action.VIEW", "-d", url }; + var two_args = [_]stringZ{ opener, url }; + const args_buf: []const stringZ = if (comptime Environment.isAndroid) &am_args else &two_args; maybe_fallback: { switch (bun.spawnSync(&.{ - .argv = &args_buf, + .argv = args_buf, .envp = null, diff --git a/src/shell/interpreter.zig b/src/shell/interpreter.zig index 1016f1603158..b90a618861d2 100644 --- a/src/shell/interpreter.zig +++ b/src/shell/interpreter.zig @@ -680,7 +680,7 @@ pub const Interpreter = struct { const static_str = if (comptime bun.Environment.isWindows) EnvStr.initSlice("USERPROFILE") else EnvStr.initSlice("HOME"); break :brk self.shell_env.get(static_str) orelse self.export_env.get(static_str); }; - return env_var orelse EnvStr.initSlice(""); + return env_var orelse EnvStr.initSlice(if (comptime bun.Environment.isAndroid) "/data/local/tmp" else ""); } pub fn writeFailingErrorFmt( diff --git a/src/shell/subproc.zig b/src/shell/subproc.zig index 0065e61eb81a..57d802c71504 100644 --- a/src/shell/subproc.zig +++ b/src/shell/subproc.zig @@ -698,7 +698,12 @@ pub const ShellSubprocess = struct { .inherit, }, .lazy = false, - .PATH = event_loop.env().get("PATH") orelse "", + .PATH = if (event_loop.env().get("PATH")) |p| + if (p.len > 0 or !bun.Environment.isPosix) p else bun.sliceTo(BUN_DEFAULT_PATH_FOR_SPAWN, 0) + else if (bun.Environment.isPosix) + bun.sliceTo(BUN_DEFAULT_PATH_FOR_SPAWN, 0) + else + "", .detached = false, .cmd_parent = cmd_parent, // .ipc_mode = IPCMode.none, @@ -1444,6 +1449,8 @@ pub inline fn assertStdioResult(result: StdioResult) void { } } +extern "C" const BUN_DEFAULT_PATH_FOR_SPAWN: [*:0]const u8; + const std = @import("std"); const util = @import("./util.zig"); const Allocator = std.mem.Allocator; diff --git a/src/sys.zig b/src/sys.zig index 41cb922a469b..15bb6cb2a543 100644 --- a/src/sys.zig +++ b/src/sys.zig @@ -2022,7 +2022,7 @@ pub fn pread(fd: bun.FD, buf: []u8, offset: i64) Maybe(usize) { } } -const pwrite_sym = if (builtin.os.tag == .linux and builtin.link_libc and !bun.Environment.isMusl) +const pwrite_sym = if (builtin.os.tag == .linux and builtin.link_libc and bun.Environment.isGlibc) libc.pwrite64 else syscall.pwrite; @@ -3156,7 +3156,7 @@ pub fn memfd_create(name: [:0]const u8, flags_: MemfdFlags) Maybe(bun.FD) { continue; } }, - .NOSYS => memfd_enosys.store(true, .monotonic), + .NOSYS, .PERM, .ACCES => memfd_enosys.store(true, .monotonic), else => {}, } @@ -3939,7 +3939,7 @@ pub fn readNonblocking(fd: bun.FD, buf: []u8) Maybe(usize) { if (Maybe(usize).errnoSysFd(rc, .read, fd)) |err| { switch (err.getErrno()) { - .OPNOTSUPP, .NOSYS => { + .OPNOTSUPP, .NOSYS, .PERM, .ACCES => { bun.linux.RWFFlagSupport.disable(); switch (bun.isReadable(fd)) { .hup, .ready => return read(fd, buf), @@ -3992,7 +3992,7 @@ pub fn writeNonblocking(fd: bun.FD, buf: []const u8) Maybe(usize) { if (Maybe(usize).errnoSysFd(rc, .write, fd)) |err| { switch (err.getErrno()) { - .OPNOTSUPP, .NOSYS => { + .OPNOTSUPP, .NOSYS, .PERM, .ACCES => { bun.linux.RWFFlagSupport.disable(); switch (bun.isWritable(fd)) { .hup, .ready => return write(fd, buf),