Build system for cross-compiling OpenCode to run natively on Android devices via Termux.
OpenCode is an AI-powered coding assistant for the terminal. It uses Bun as its JavaScript runtime and compiles to a standalone binary via bun build --compile. Since Bun has no official Android support (marked "not planned"), this project cross-compiles Bun itself from source for Android/aarch64, including the full WebKit/JavaScriptCore engine.
# Download and install
curl -LO https://github.com/guysoft/opencode-termux/releases/latest/download/opencode-aarch64.zip
unzip opencode-aarch64.zip
chmod +x opencode
mv opencode $PREFIX/bin/
# Install required dependency
pkg install ripgrep
# Run
opencodecurl -LO https://github.com/guysoft/opencode-termux/releases/latest/download/opencode-aarch64.pkg.tar.xz
pacman -U opencode-*-aarch64.pkg.tar.xz
opencodecurl -LO https://github.com/guysoft/opencode-termux/releases/latest/download/opencode-aarch64.deb
dpkg -i opencode-*-aarch64.deb
opencodeThe pacman and deb packages automatically install ripgrep as a dependency.
OpenCode needs an AI provider to work. Set one up by configuring your environment:
# Example: Use Anthropic Claude
export ANTHROPIC_API_KEY="sk-..."
# Or use OpenAI
export OPENAI_API_KEY="sk-..."
# Then run
opencodeSee the OpenCode docs for full configuration options.
This repo contains patch files and build scripts only -- not the full source trees of Bun or WebKit (which are 1.1GB and 2.7GB respectively). The CI workflow clones upstream repos and applies patches during build.
opencode-termux/
patches/
bun/android-support.patch # 33 files, Bun Android/aarch64 support
webkit/android-support.patch # 5 files, WebKit/JSC Android fixes
zig/posix-android-sigaction.patch # Zig stdlib sigaction/sigprocmask fix
opentui/android-libc-link.patch # Link NDK libc.so for Android dlopen
scripts/
apply-patches.sh # Clone upstream repos + apply patches
build-icu.sh # Cross-compile ICU 75.1 for Android
build-webkit.sh # Cross-compile WebKit/JSC for Android
build-tinycc.sh # Cross-compile TinyCC (libtcc.a) for Android
build-bun.sh # Cross-compile Bun for Android
build-opentui.sh # Build libopentui.so for Android
build-opencode.sh # Build OpenCode standalone binary
make-packages.sh # Create zip, pacman, and deb packages
build-opencode-android.ts # TypeScript helper (module graph extraction)
cmake/
webkit-android-toolchain.cmake # WebKit CMake cross-compilation toolchain
.github/workflows/
build.yml # GitHub Actions CI workflow
This project got OpenCode (a ~136MB standalone binary built on Bun + WebKit/JSC) running on Android/Termux, which required:
-
Cross-compiling Bun v1.2.13 for Android/aarch64 -- Bun has zero Android support. We patched 33 files across the build system (CMake, Zig), syscall layer, Bionic libc compatibility, JSC/JIT configuration, and linker settings.
-
Cross-compiling WebKit/JavaScriptCore for Android -- No prebuilt WebKit exists for Android. We patched 5 files to replace glibc-specific APIs with POSIX/Android equivalents and fixed JIT signal handling for Android's security model.
-
Fixing Zig's stdlib for Android/Bionic -- Zig's
sigaction()andsigprocmask()pass a 152-byte struct through Bionic's libc which expects 32 bytes, causing silent memory corruption. Patched to use raw syscalls on Android. -
Building libopentui.so for Android -- OpenCode's TUI renderer depends on OpenTUI, which needed a patch to link Android NDK's libc.so stub so
dlopen()can resolve symbols at runtime. -
Standalone binary surgery -- Since
bun build --compilehas no Android cross-compilation target, we build a host standalone binary, extract the serialized module graph, and transplant it onto the Android Bun binary. This required understanding and matching the binary format across Bun versions (36-byte vs 52-byte module struct stride). -
Cross-compiling ICU 75.1 -- Bun depends on ICU for Unicode/i18n support. Cross-compiled from source for Android.
-
Cross-compiling TinyCC -- Bun links against libtcc.a for FFI support. TinyCC's build system assumes a host build, so we cross-compile it separately and inject the library.
Stage 1: ICU 75.1 ~5 min (cross-compile for Android)
Stage 2: WebKit/JSC ~60-90 min (cross-compile, CACHED)
Stage 3: TinyCC ~1 min (cross-compile libtcc.a)
Stage 4: Bun binary ~30-45 min (CMake + Ninja, CACHED)
Stage 5: libopentui.so ~2 min (Zig build for aarch64-linux-android)
Stage 6: OpenCode bundle ~30 sec (bun build --compile, extract module graph)
Stage 7: Packages ~10 sec (zip + pacman + deb)
With warm caches (WebKit + Bun cached), CI runs complete in ~4 minutes.
Bun has zero Android support. Every patch falls into one of these categories:
- Android NDK CMake toolchain (
cmake/toolchains/android-aarch64.cmake) -- new file. Sets up cross-compiler, sysroot, and find-root paths for the entire Bun build. -fPICinstead of-fno-pic-- Android requires position-independent code for all executables and shared libraries (since API 21).- PIE linking -- Android mandates position-independent executables. Changed
-Wl,-no-pieto-pie -fPIE. - Rust linker set to NDK's versioned clang -- The lol-html Rust crate must link against Android's libc, not the host's. Set
CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKERto the NDK'saarch64-linux-android24-clang. - Zig
translate-cgiven NDK sysroot headers -- Zig has no bundled Android libc headers. Added-Dandroid-ndk-sysrootbuild option that passes NDK include paths totranslate-c. -Wno-undefined-var-templateadded -- NDK Clang 19 triggers this warning on some JSC template specializations; with-Werror, this becomes a build failure.- RELRO kept enabled -- Desktop Linux disables RELRO (
-Wl,-z,norelro); Android requires it for security. CARGO_ENCODED_RUSTFLAGSreplaced withRUSTFLAGS-- CMake can't encode the 0x1F separator thatCARGO_ENCODED_RUSTFLAGSrequires. Switched to space-separatedRUSTFLAGS.
android_tls_align.s-- new file. Assembly file that creates a.tbsssection with 64-byte alignment. Without this, the Zig linker emitsPT_TLS p_align=8, causing TLS variables to overlap with Bionic's Thread Control Block slots (TPIDR+0..63), corrupting scudo allocator state and crashing on first allocation. This MUST be assembled (not compiled as C) to avoid NDK's emulated TLS (__emutls).android_tls_align.c-- backup C version with__attribute__((aligned(64))).
close_range()fallback -- Android's seccomp filter blocks theclose_rangesyscall in app processes (including Termux). Replaced with iteration over/proc/self/fd.preadv2/pwritev2return ENOSYS -- These syscalls may be blocked by Android's seccomp. Return ENOSYS so the Zig caller falls back to regularread()/write().epoll_pwait2return ENOSYS -- Same seccomp issue. Falls back toepoll_pwait.lchmodreturn ENOSYS -- Not available in Bionic.
memmem,lstat,fstat,statas@externdeclarations -- Bionic has these symbols but Zig'stranslate-cdoesn't pick them up due to symbol visibility differences. Declared manually.getifaddrs/freeifaddrsextern wrappers -- Hidden by__INTRODUCED_INmacros at API 24 despite being available. Declared manually via@extern.pthread_setcancelstatestubbed -- Bionic has no POSIX thread cancellation.posix_spawnattr_setsigdefault/setsigmaskstubbed -- Require API 28, we target API 24. Bun uses its ownposix_spawn_bun()which handles signals directly.- No separate
-lpthread-- Bionic merges pthread into libc. Removed from link flags. pwrite64not used on Android -- Android/Bionic uses standardpwrite, not the glibc compat symbol.open()flag fix -- Removed third argument (mode) fromopen()call when not creating a file (Bionic is stricter about this).
- Signal-based VM traps disabled (
usePollingTraps=true) -- Android'sdebuggerdcrash handler intercepts SIGSEGV before the app's signal handler, so JSC's signal-based traps crash the process instead of being caught. - Wasm fault signal handler disabled (
useWasmFaultSignalHandler=false) -- Samedebuggerdissue. Uses explicit bounds checking instead. - Options set via
setenv("JSC_*")BEFOREJSC::initialize()--Options::initialize()resets all options to defaults before reading env vars. Setting options via the API beforeinitialize()has no effect.
StandaloneModuleGraph.zigOffsets struct extended -- Addedcompile_exec_argv_ptr,flagsfields andFlagstype to match the format produced by host Bun 1.3.2.find()function bug fix -- Fixed variable name frombase_pathtonameinisBunStandaloneFilePath()call (this was actually an upstream bug).
isAndroidconstant added toenv.zig--isLinux and abi == .android.isMuslexcludes Android -- Android uses Bionic, not musl.bun upgradedisabled on Android -- No Android release channel exists.- Crash handler: Enhanced with aarch64 register dump and
/proc/self/mapsoutput for debugging crashes on Android. - File descriptor limit: Raised on Android (like musl) since Termux has low defaults.
- npm libc detection: Reports as
glibcfor package resolution compatibility.
- Post-build test skip -- CMake tries to run
bun --revisionafter linking; can't run aarch64 binary on x86_64 host. Addedif(NOT ANDROID)guard. features.jsonskip -- Same issue; generation requires running the binary.- CI artifact naming -- Uses
bun-android-aarch64triplet.
bcmpreplaced withmemcmp--bcmpis BSD, not available in Bionic'slibpas.aligned_allocreplaced withposix_memalign--aligned_allocrequires API 28, we target API 24.backtrace()stubbed -- Requires API 33.pthread_getname_npstubbed -- Requires API 26.- JSC
InitializeThreading.cpp-- Force polling traps and disable Wasm signal handler on Android (same rationale as Bun patches).
sigaction()andsigprocmask()bypass Bionic libc -- Bionic'sstruct sigactionis 32 bytes with 8-bytesigset_t, but Zig'slinux.Sigactionis 152 bytes with 128-bytesigset_t. Passing Zig's struct through Bionic'ssigaction()causes silent memory corruption. The patch makes these functions use rawrt_sigaction/rt_sigprocmasksyscalls on Android, which correctly handle the kernel's struct layout.
- Link NDK
libc.sostub -- On Android, the.somust haveNEEDED: libc.soin its ELF headers sodlopen()can resolve symbols likegetauxval. Zig doesn't bundle Android libc, so we directly add the NDK sysroot'slibc.sostub as a link input.
Since bun build --compile has no Android cross-compilation target, we use a manual approach:
- Use host Bun (v1.3.2) to
bun build --compileOpenCode for the host platform - Extract the serialized module graph from the host standalone binary by locating the
\n---- Bun! ----\ntrailer and reading theOffsetsstruct - Patch the module graph in-place (fix
undiciglobal reference) - Before bundling, swap x86_64
libopentui.sowith the ARM64 Android-built version, so it gets embedded in the module graph - Append the module graph to our Android Bun binary
- Write a new 8-byte
total_byte_countfooter
The standalone binary format:
[Android Bun binary (~96 MB)]
[Module graph bytes (~46 MB)]
[total_byte_count as u64 LE (8 bytes)]
The CompiledModuleGraphFile struct layout changed between Bun versions:
- Bun <= 1.3.2: 36-byte stride (4 StringPointers + 3 u8 + 1 padding)
- Bun >= 1.3.11: 52-byte stride (6 StringPointers + 4 u8)
The target Android Bun is v1.2.13, which expects 36-byte stride. If the host Bun produces 52-byte modules, the target reads garbage and OOMs immediately (RSS jumps to 1GB on startup).
We can't use Bun 1.2.13 as host either, because OpenCode's monorepo uses catalog: workspace protocol (added in Bun 1.3.x) -- bun install fails. Bun 1.3.2 is the sweet spot: supports catalog: AND produces compatible 36-byte modules.
- Full TUI rendering (ASCII art logo, prompt, model selector, status bar)
- All backend services (server, provider, file watcher, LSP)
opencode --versionoutputs correct version- AI provider connections (tested with Claude, GitHub Copilot)
| Issue | Severity | Details |
|---|---|---|
| File watcher native module | Low | @parcel/watcher .node binding is compiled for x86_64. Falls back gracefully to polling. Logs: dlopen failed: "...00000001.node" is for EM_X86_64 (62) instead of EM_AARCH64 (183) |
bun upgrade |
Low | Disabled on Android -- no Android release channel exists upstream |
| TinyCC FFI compilation | Low | libtcc.a is linked but TCC's runtime code generation may not produce valid ARM64 code. FFI is not commonly used by OpenCode. |
| SIGPWR signals | None | Many SIGPWR signals appear in strace -- related to Android's power management or Bun's signal handling. Not errors. |
| Workaround | Why |
|---|---|
| Host Bun pinned to 1.3.2 | Module graph struct compatibility between host and target Bun versions (see above) |
close_range() replaced with /proc/self/fd iteration |
Android seccomp blocks the close_range syscall in app processes |
preadv2/pwritev2/epoll_pwait2 return ENOSYS |
Seccomp may block these; callers fall back gracefully |
setenv("JSC_*") before JSC::initialize() |
Options API is reset during initialization; env vars survive the reset |
.tbss section with 64-byte alignment in assembly |
Forces PT_TLS p_align=64 to avoid corrupting Bionic's TCB slots |
Raw rt_sigaction/rt_sigprocmask syscalls |
Zig's struct layout doesn't match Bionic's; bypass libc entirely |
NDK libc.so stub linked into libopentui.so |
Zig doesn't provision Android libc; explicit link needed for dlopen symbol resolution |
Module graph extracted via trailer, not process.execPath |
process.execPath is unreliable in CI; trailer-based extraction is version-agnostic |
These patches could potentially be contributed upstream to reduce the maintenance burden of this project.
The Bun team closed Android support as "not planned". However, some patches are clean improvements regardless of Android:
| Patch | Upstreamable? | Notes |
|---|---|---|
StandaloneModuleGraph.zig find() bug fix (base_path -> name) |
Yes | This is an actual bug in upstream Bun |
CARGO_ENCODED_RUSTFLAGS -> RUSTFLAGS |
Maybe | Simpler, avoids CMake 0x1F encoding issues. May have side effects on other platforms. |
open() mode argument fix in bsd.c |
Yes | Passing a mode to open() without O_CREAT is technically undefined behavior |
Android CMake toolchain + if(ANDROID) guards |
No | Team has explicitly declined Android support |
Syscall fallbacks (close_range, preadv2, etc.) |
No | Only needed on Android |
Bionic libc stubs (pthread_setcancelstate, etc.) |
No | Only needed on Android |
| TLS alignment fix | No | Only needed for Android/aarch64 Bionic |
| JSC signal trap changes | No | Only needed on Android due to debuggerd |
isAndroid environment detection |
No | Only needed on Android |
Recommendation: Submit a small PR with the find() bug fix and the open() mode fix. These are correctness improvements that benefit all platforms. The rest is Android-specific and will be rejected per Bun team policy.
| Patch | Upstreamable? | Notes |
|---|---|---|
bcmp -> memcmp |
Maybe | memcmp is more portable, but oven-sh may not care since they only target macOS/Linux/Windows |
aligned_alloc -> posix_memalign |
No | Only needed for Android API < 28 |
backtrace() stub |
No | Only needed for Android API < 33 |
pthread_getname_np stub |
No | Only needed for Android API < 26 |
| Polling traps + Wasm signal handler | No | Only needed on Android |
Recommendation: The bcmp -> memcmp change is the only candidate, but it's unlikely to be accepted since oven-sh/WebKit is a Bun-specific fork. Not worth the effort.
| Patch | Upstreamable? | Notes |
|---|---|---|
sigaction/sigprocmask Android bypass |
Yes | This is a real bug: Zig's POSIX layer corrupts memory on Android/Bionic due to struct size mismatch |
Recommendation: This should be submitted to upstream Zig (ziglang/zig), not just oven-sh/zig. The struct layout mismatch between Zig's Sigaction (152 bytes) and Bionic's struct sigaction (32 bytes) is a genuine bug that affects any Zig program targeting Android. The fix (using raw syscalls on Android) is clean and correct.
Note: oven-sh/zig is Bun's custom Zig fork (v0.14.0), not upstream Zig. The patch should be adapted for current Zig master as well.
| Patch | Upstreamable? | Notes |
|---|---|---|
| NDK libc.so stub linking for Android | Yes | Clean, conditionally compiled, needed for any Android target |
Recommendation: Submit a PR to anomalyco/opentui. The patch correctly detects Android targets in build.zig and links the NDK's libc.so stub only when targeting aarch64-linux-android. It's a small, self-contained change that enables Android support without affecting other targets.
- Bun Android support as a whole -- The Bun team has explicitly declined this. The CMake toolchain, all
if(ANDROID)guards, syscall fallbacks, Bionic stubs, and TLS alignment fix are permanent patches we'll need to maintain for every Bun version bump. - WebKit Android patches -- oven-sh/WebKit only supports macOS/Linux/Windows. No incentive to accept Android-specific changes.
- Host Bun version pinning -- This is a build-time constraint, not a code patch. It will need to be re-evaluated with each Bun version bump (checking if the
CompiledModuleGraphFilestruct changed). - Standalone binary surgery (module graph extraction + transplant) -- This entire approach is a workaround for the lack of cross-compilation in
bun build --compile. If Bun ever adds cross-compile targets, this becomes unnecessary.
| Component | Version/Commit | Why pinned |
|---|---|---|
| Bun (target) | v1.2.13 (tag bun-v1.2.13) |
Proven working, patches validated |
| Bun (host) | v1.3.2 | Module graph compat (36-byte stride) + catalog: support |
| WebKit/JSC | 017930eb (oven-sh/WebKit) |
Matches Bun v1.2.13's expected WebKit |
| ICU | 75.1 | Matches Bun v1.2.13's expected ICU |
| Android NDK | r28b (28.1.13356709) | Clang 19, stable |
| Android API level | 24 (Android 7.0+) | Minimum for 64-bit Termux |
| Zig (for opentui) | 0.15.2 | Latest stable, Android target support |
| OpenCode | 1.3.13 | Current release |
| TinyCC | b91835d8 (oven-sh/tinycc) |
Matches Bun v1.2.13's expected TinyCC |
- Build host: x86_64 Linux (Ubuntu 22.04+)
- RAM: 16GB minimum (30GB recommended for WebKit link step)
- Disk: 60GB+ free space
- CPU: 8+ cores recommended (4 cores works but slow)
| Tool | Version | Purpose |
|---|---|---|
| Android NDK | r28b (28.1.13356709) | Cross-compiler toolchain |
| CMake | 3.24+ (CI installs 3.28) | Build system |
| Ninja | 1.10+ | Build tool |
| Rust | stable | lol-html crate (with aarch64-linux-android target) |
| Go | 1.20+ | BoringSSL |
| Zig | 0.15.2 | libopentui.so build |
| Bun | 1.3.2 (host, pinned) | OpenCode bundling |
| Python3 | 3.8+ | WebKit code generation |
| Ruby | 2.7+ | WebKit code generation |
| Perl | 5.20+ | WebKit code generation |
- Samsung Galaxy S10e (Android 12, Termux, aarch64) -- full TUI confirmed working
- Meta Quest 2 (Android 12L, adb shell)
- OpenCode by Anomaly
- Bun by Oven
- WebKit/JavaScriptCore (oven-sh fork)
- OpenTUI by Anomaly
- Termux -- terminal emulator for Android
MIT