Skip to content

build(android): pin NDK r30 together with the prebuilt WebKit - #46623

Draft
robobun wants to merge 1 commit into
mainfrom
robobun/a106d4cb/android-ndk-r30
Draft

robobun wants to merge 1 commit into
mainfrom
robobun/a106d4cb/android-ndk-r30

Conversation

@robobun

@robobun robobun commented Oct 23, 2026

Copy link
Copy Markdown
Collaborator

Fixes #46595

Do not merge as is. WEBKIT_VERSION must move in the same commit, to a prebuilt built with NDK r30. That change is on the oven-sh/WebKit branch robobun/a106d4cb/android-ndk-r30 (two commits on 5e3554face, the commit this repo pins). This session cannot open a pull request there. A maintainer needs to open it. oven-sh/WebKit main has moved past 5e3554face (#889), so the build that carries it is also an engine upgrade: fold this diff into the next "Upgrade WebKit" PR, or move WEBKIT_VERSION here once that build exists.

Problem

  • Bun's Android build pins NDK r27c (scripts/build/ci-images/spec.ts, pins.androidNdk). The Android prebuilt of JavaScriptCore is built with the same r27c in oven-sh/WebKit's Dockerfile.android.
  • r27c's libc++ is an unreleased LLVM snapshot. Its std::expected stores has_val after the union. Every libc++ from LLVM 18.1 on, and every NDK from r28 on, stores it in the value type's tail padding when there is any. Bun decodes std::expected<ParseInnerResult, String> from the prebuilt inline on every module parse (Parser.h, via BunAnalyzeTranspiledModule.cpp:257).
  • At Bun 1.4.4's pin (d6375c72) ParseInnerResult ends in a uint16_t, so has_val sits at 0xE8 under r27c and 0xE2 under r30. A Bun built with r30 against that prebuilt crashes. Termux builds with r30 and carries a patched expected.h plus a sed on scripts/build/flags.ts.

Fix

  • pins.androidNdk moves to r30 with the sha256 of android-ndk-r30-linux.zip. Google publishes only the SHA-1 (5107f898313790e449e87eee2183d9a20602dee9). The CI image bake checks the sha256.
  • oven-sh/WebKit Dockerfile.android moves to the same r30. WTF/Atomics.h drops the std::atomic_ref fallback that Upgrade to upstream WebKit 33ce3bf11a71 WebKit#860 added for r27c. Its author named a newer NDK as the other way. The file is upstream's again.
  • The invariant is written where both halves move: next to WEBKIT_VERSION in deps/webkit.ts, next to pins.androidNdk, and in the upgrade checklist. The two NDK hints in config.ts name the pinned release and its download.
  • Verified: bun run ci:images renames only linux-aarch64-debian (the Android build host), so the PR build bakes it. test/internal/source-lints/{ci-images,ci-image-pins,webkit-prebuilt-url}.test.ts pass. tsc on scripts/build passes.

Background

  • The NDK gives the Android build the bionic sysroot, libc++ headers and static libc++, compiler-rt builtins and libunwind. The compiler is Bun's pinned host clang, not the NDK's. androidNdk() in spec.ts and linkNdkRuntimesIntoClang() in config.ts symlink the NDK's runtimes into the host clang's resource dir. Dockerfile.android does the same symlinks and test-links an Android executable, so the fork's image build proves r30 keeps that layout.
  • At the current pin (5e3554face) ParseInnerResult gained a trailing unsigned, so this one type has no tail padding and the same layout under both libc++ versions. The class remains: 64 std::expected declarations in the prebuilt headers, and the next upstream edit to ParseInnerResult reopens it. The pin bump removes the class. Both Android lanes are build-only, so CI cannot observe a mismatch.
  • Considered a configure-time check that the builder's NDK matches the pin: it guards one of three copies, reads a version file per Android configure, and misfires with --webkit=local. Considered out-of-line Parser instantiations in the fork: they protect one of 64 declarations.

Downsides

  • A downstream build of Bun needs an NDK from r28 on. r27 and older now mismatch the prebuilt the same way r30 did before. Nothing checks this at configure time.
  • Moving WEBKIT_VERSION pulls every fork change between 5e3554face and that build into the same change. Binary size and symbol deltas get measured once the build exists.
Notes

Why the bump is one-time. LLVM history of libcxx/include/__expected/expected.h: commit 134c915955 (2023-10-30, llvm #68733) moved has_val out of the union's tail padding. Commit 4f4690530e (2024-01-22, llvm #69673) added __fits_in_tail_padding and moved it back. NDK r27's clang-r522817 is an LLVM merge dated 2023-12-29, between those two commits. llvmorg-18.1.0 has __fits_in_tail_padding. NDK r28 (clang-r530567c), r29 (clang-r563880c), r30 (clang-r574158c) and Bun's pinned host clang 23.1.1 all share the final layout.

The offsets. ParseInnerResult at d6375c72: two pointers, FunctionStack (16), two VariableEnvironment (96 each), CodeFeatures (uint16_t at 224), 6 bytes of tail padding, size 232. has_val at 232 (0xE8) in the r27c layout, at 226 (0xE2) in the tail padding with LLVM 18.1+. At 5e3554face the struct adds unsigned usedPrivateNameCount at 228: size 232, no tail padding, has_val at 232 in both.

The sha256. This container has no route to dl.google.com. The value is the one termux/termux-packages scripts/setup-android-sdk.sh uses for android-ndk-r30-linux.zip. Both the oven-sh/WebKit image build (sha256sum -c) and the CI image bake (download with sha256) fail if it is wrong.

What renames. bun run ci:images before and after: linux-aarch64-debian-a8bf3197baf56377 becomes linux-aarch64-debian-a26465ce83461917. linux-x64-debian keeps its name.

Old pin copies. git grep for r27c and the old sha256 finds no other copy in this repo.

Self-review: 14 concerns raised, 12 addressed. Two left as notes: a compile-time static_assert on the prebuilt's _LIBCPP_VERSION (a define emitted by Dockerfile.android into cmakeconfig.h) would catch a future mismatch at build time, and pins.freebsd (14.4) differs from the fork's Dockerfile.freebsd (14.3). Both are separate changes.

Layout check once the build exists. llvm-dwarfdump --debug-info on the Android debug lane's libJavaScriptCore.a and on Bun's object for BunAnalyzeTranspiledModule.cpp: compare the __has_val_ member offset of std::expected<JSC::ParseInnerResult, WTF::String>.

NDK r27c ships a libc++ from an unreleased LLVM snapshot. Its std::expected
stores has_val after the union. Every libc++ from LLVM 18.1 on, and every
NDK from r28 on, stores it in the union's tail padding when the value type
has any. Bun decodes std::expected<ParseInnerResult, String> from the
prebuilt JavaScriptCore inline on every module parse, so Bun's Android
objects and the prebuilt must be built with NDKs that agree on libc++'s
layout. At Bun 1.4.4's pin the two layouts differed (has_val at 0xE8 against
0xE2), which is what a Bun built with NDK r30 against the r27c prebuilt hit.

The pin moves together with NDK_VERSION in oven-sh/WebKit's
Dockerfile.android and with WEBKIT_VERSION. The note next to WEBKIT_VERSION,
the upgrade checklist and the NDK hints in config.ts now say so.
@robobun

robobun commented Oct 23, 2026 •

Copy link
Copy Markdown
Collaborator Author
Updated 3:38 PM PT - Oct 23rd, 2026

✅ @robobun, your commit f12efcca49882630627600ae6bdb70b095d2d009 passed in Build #131453! 🎉


🧪   To try this PR locally:

bunx bun-pr 46623

That installs a local version of the PR into your bun-46623 executable, so you can run:

bun-46623 --bun

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

build(android): upgrade Android NDK from r27c to r30+ for prebuilt WebKit

2 participants