PocketClaw originally ran Node.js inside a proot Ubuntu container (Hack #8). Proot intercepts every syscall via ptrace, translating paths and UIDs to simulate a root filesystem. This costs ~30 MB of RSS overhead, adds latency to every filesystem operation, and causes subtle bugs: some syscalls are not perfectly translated, leading to intermittent ENOSYS errors and broken file locking.
The fix is to compile Node.js to run natively on Termux, eliminating proot entirely. The challenge: Termux on this phone runs Android 6.0 (API 23), but NDK r26c's minimum target for modern C++ features is API 24. The solution is to compile against API 24 headers and provide the 11 missing symbols at runtime via a tiny LD_PRELOAD shim. This gives us full API 24 functionality on an API 23 phone.
NDK r26c was chosen because it is the last NDK release with full ARM32 (armv7a) support. Later NDKs begin deprecating 32-bit ARM targets.
The cross-compilation targets armv7a-linux-androideabi24 using NDK r26c's clang toolchain. The critical flag is --with-intl=small-icu — using --without-intl saves ~4 MB binary size but breaks Unicode property escapes (\p{L}, \p{N}) which OpenClaw uses extensively for multilingual text processing.
# Configure with NDK r26c toolchain
export NDK=$HOME/Android/Sdk/ndk/26.1.10909125
export TOOLCHAIN=$NDK/toolchains/llvm/prebuilt/linux-x86_64
export CC=$TOOLCHAIN/bin/armv7a-linux-androideabi24-clang
export CXX=$TOOLCHAIN/bin/armv7a-linux-androideabi24-clang++
export AR=$TOOLCHAIN/bin/llvm-ar
export RANLIB=$TOOLCHAIN/bin/llvm-ranlib
cd node-v22.12.0
./configure \
--dest-cpu=arm \
--dest-os=android \
--cross-compiling \
--with-intl=small-icu \
--partly-static \
--without-inspector \
--without-corepack \
--without-npmTwo build system quirks must be fixed before compiling:
# Fix 1: ICU host tools compile with -m64, but V8 host tools use -m32.
# The mixed-mode build fails. Force all host tools to -m64:
find out -name "*.host.mk" -exec sed -i 's/-m32/-m64/g' {} +
# Fix 2: Use "make node" NOT "make". The default target includes cctest,
# which calls aligned_alloc (API 28+) and fails to link against API 24.
make -j$(nproc) node
# Strip the binary (113 MB -> 58 MB)
$TOOLCHAIN/bin/llvm-strip out/Release/nodeThe LD_PRELOAD shim provides 11 symbols that exist in API 24 headers but are missing from the API 23 phone's libc:
// libapi23compat.c — 8.4 KB compiled
// Compiled with: $CC -shared -o libapi23compat.so libapi23compat.c
#include <netinet/in.h>
#include <pthread.h>
// IPv6 constants (exist as externs in API 24, missing in API 23)
const struct in6_addr in6addr_any = IN6ADDR_ANY_INIT;
const struct in6_addr in6addr_loopback = IN6ADDR_LOOPBACK_INIT;
// TLS emulation (used by NDK r26c's compiler-rt)
void *__emutls_get_address(void *p) { return p; }
// Network interface enumeration (Node.js os.networkInterfaces())
int getifaddrs(void **ifap) { *ifap = NULL; return 0; }
void freeifaddrs(void *ifa) {}
// Group/passwd lookups (Node.js os.userInfo())
int getgrnam_r(const char *n, void *g, char *b, size_t l, void **r)
{ *r = NULL; return 0; }
int getgrgid_r(int gid, void *g, char *b, size_t l, void **r)
{ *r = NULL; return 0; }
// 64-bit file offset functions
long long fseeko64(void *f, long long o, int w) { return fseek(f, o, w); }
long long ftello64(void *f) { return ftell(f); }
// pthread barriers (used by V8 internally)
int pthread_barrier_init(void *b, void *a, unsigned c) { return 0; }
int pthread_barrier_wait(void *b) { return 0; }
int pthread_barrier_destroy(void *b) { return 0; }One more fix: Termux's libc++_shared.so is too old for C++17 filesystem support that Node.js 22 uses internally. Replace it with the NDK r26c version:
# On the phone (backup first):
cp $PREFIX/lib/libc++_shared.so $PREFIX/lib/libc++_shared.so.termux.bak
# Copy NDK version (from build machine):
adb push $TOOLCHAIN/sysroot/usr/lib/arm-linux-androideabi/libc++_shared.so \
/data/data/com.termux/files/usr/lib/libc++_shared.soDeploy and run:
# Push to phone
adb push out/Release/node /data/data/com.termux/files/usr/bin/node22-icu
adb push libapi23compat.so /data/data/com.termux/files/usr/lib/libapi23compat.so
chmod 755 $PREFIX/bin/node22-icu
# Run with LD_PRELOAD
LD_PRELOAD=$PREFIX/lib/libapi23compat.so $PREFIX/bin/node22-icu --version
# v22.12.0# Confirm the binary is native ARM32 (not x86, not aarch64):
file $PREFIX/bin/node22-icu
# ELF 32-bit LSB executable, ARM, EABI5 version 1 (SYSV), dynamically linked
# Confirm ICU works (Unicode property escapes):
LD_PRELOAD=$PREFIX/lib/libapi23compat.so $PREFIX/bin/node22-icu \
-e "console.log('cafe'.match(/\p{L}+/gu))"
# [ 'cafe' ]
# Confirm binary size:
ls -lh $PREFIX/bin/node22-icu
# 58M (stripped)
# Confirm shim size:
ls -lh $PREFIX/lib/libapi23compat.so
# 8.4K--without-intl breaks \p{L} and \p{N} Unicode property escapes at runtime with a SyntaxError. OpenClaw uses these throughout its multilingual text pipeline. Always use --with-intl=small-icu-m64 while V8 host tools compile with -m32. The mixed-mode build fails unless you sed all .host.mk files to force -m64make (the default target) includes cctest, which uses aligned_alloc (API 28+). This fails to link against API 24. Always use make node to build only the node binarytr -d '\r' before deploying bash scripts to Termuxsed -i can corrupt binary files with null bytes. Do all binary manipulation on the build machine, not on the phonelibc++_shared.so replacement affects ALL Termux packages. If a Termux package breaks after this change, restore the backup| Metric | proot (Before) | Native (After) |
|---|---|---|
| RSS overhead | ~30 MB (proot process) | 0 MB |
| Binary size | 113 MB (unstripped) | 58 MB (stripped) |
| Shim size | N/A | 8.4 KB |
| Stack layers | 6 (Termux > bash > proot > Ubuntu > bash > node) | 4 (Termux > bash > node22-icu > gateway) |
| Startup time | ~45s | ~25s |
| Syscall bugs | Intermittent ENOSYS | None |