#!/usr/bin/env bash # install-nodejs.sh - Install Node.js linux-arm64 with grun wrapper (L2 conditional) # Extracted from install-glibc-env.sh — Node.js only, assumes glibc already installed. # Called by orchestrator when config.env PLATFORM_NEEDS_NODEJS=true. # # What it does: # 1. Download Node.js linux-arm64 LTS # 2. Create grun-style wrapper scripts (ld.so direct execution) # 3. Configure npm # 4. Verify everything works # # patchelf is NOT used — Android seccomp causes SIGSEGV on patchelf'd binaries. # All glibc binaries are executed via: exec ld.so binary "$@" set -euo pipefail RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' NC='\033[0m' OPENCLAW_DIR="$HOME/.openclaw-android" NODE_DIR="$OPENCLAW_DIR/node" BIN_DIR="$OPENCLAW_DIR/bin" GLIBC_LDSO="$PREFIX/glibc/lib/ld-linux-aarch64.so.1" # Node.js LTS version to install NODE_VERSION="22.22.0" NODE_TARBALL="node-v${NODE_VERSION}-linux-arm64.tar.xz" NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TARBALL}" echo "=== Installing Node.js (glibc) ===" echo "" # ── Pre-checks ─────────────────────────────── if [ -z "${PREFIX:-}" ]; then echo -e "${RED}[FAIL]${NC} Not running in Termux (\$PREFIX not set)" exit 1 fi if [ ! -x "$GLIBC_LDSO" ]; then echo -e "${RED}[FAIL]${NC} glibc dynamic linker not found — run install-glibc.sh first" exit 1 fi # Check if already installed (check BIN_DIR wrapper first, fall back to NODE_DIR) _NODE_CMD="" if [ -x "$BIN_DIR/node" ]; then _NODE_CMD="$BIN_DIR/node" elif [ -x "$NODE_DIR/bin/node" ]; then _NODE_CMD="$NODE_DIR/bin/node" fi if [ -n "$_NODE_CMD" ]; then if "$_NODE_CMD" --version &>/dev/null; then INSTALLED_VER=$("$_NODE_CMD" --version 2>/dev/null | sed 's/^v//') if [ "$INSTALLED_VER" = "$NODE_VERSION" ]; then echo -e "${GREEN}[SKIP]${NC} Node.js already installed (v${INSTALLED_VER})" # Repair wrappers — ensure they exist in BIN_DIR (npm-safe location) mkdir -p "$BIN_DIR" _any_fixed=false # Ensure node wrapper exists in BIN_DIR (may be missing for pre-v1.0.16 installs) if [ ! -x "$BIN_DIR/node" ]; then # Ensure node.real exists if [ -f "$NODE_DIR/bin/node" ] && [ ! -L "$NODE_DIR/bin/node" ] && file "$NODE_DIR/bin/node" 2>/dev/null | grep -q ELF; then mv "$NODE_DIR/bin/node" "$NODE_DIR/bin/node.real" fi if [ -f "$NODE_DIR/bin/node.real" ]; then cat > "$BIN_DIR/node" << NODEWRAP #!${PREFIX}/bin/bash [ -n "\$LD_PRELOAD" ] && export _OA_ORIG_LD_PRELOAD="\$LD_PRELOAD" unset LD_PRELOAD export _OA_WRAPPER_PATH="$BIN_DIR/node" _OA_COMPAT="\$HOME/.openclaw-android/patches/glibc-compat.js" if [ -f "\$_OA_COMPAT" ]; then case "\${NODE_OPTIONS:-}" in *"\$_OA_COMPAT"*) ;; *) export NODE_OPTIONS="\${NODE_OPTIONS:+\$NODE_OPTIONS }-r \$_OA_COMPAT" ;; esac fi _LEADING_OPTS="" _COUNT=0 for _arg in "\$@"; do case "\$_arg" in --*) _COUNT=\$((_COUNT + 1)) ;; *) break ;; esac done if [ \$_COUNT -gt 0 ] && [ \$_COUNT -lt \$# ]; then while [ \$# -gt 0 ]; do case "\$1" in --*) _LEADING_OPTS="\${_LEADING_OPTS:+\$_LEADING_OPTS }\$1"; shift ;; *) break ;; esac done export NODE_OPTIONS="\${NODE_OPTIONS:+\$NODE_OPTIONS }\$_LEADING_OPTS" fi exec "$GLIBC_LDSO" --library-path "$PREFIX/glibc/lib" "$NODE_DIR/bin/node.real" "\$@" NODEWRAP chmod +x "$BIN_DIR/node" _any_fixed=true fi fi if [ -f "$NODE_DIR/lib/node_modules/npm/bin/npm-cli.js" ]; then cat > "$BIN_DIR/npm" << 'NPMWRAP' #!__PREFIX__/bin/bash "__BIN_DIR__/node" "__NODE_DIR__/lib/node_modules/npm/bin/npm-cli.js" "$@" _npm_exit=$? case "$*" in *-g*openclaw*|*--global*openclaw*|*openclaw*-g*|*openclaw*--global*) _oc_bin="__PREFIX__/bin/openclaw" _oc_mjs="__PREFIX__/lib/node_modules/openclaw/openclaw.mjs" if [ -f "$_oc_mjs" ]; then [ -L "$_oc_bin" ] && rm -f "$_oc_bin" printf '#!__PREFIX__/bin/bash\nexec "__BIN_DIR__/node" "%s" "$@"\n' "$_oc_mjs" > "$_oc_bin" chmod +x "$_oc_bin" fi ;; esac # Re-patch codex CLI wrapper after global install/update (DioNanos fork launcher fix) case "$*" in *codex-cli-termux*) _codex_bin="__PREFIX__/bin/codex" _codex_pkg="__PREFIX__/lib/node_modules/@mmmbuto/codex-cli-termux/bin" if [ -f "$_codex_pkg/codex.bin" ]; then [ -L "$_codex_bin" ] && rm -f "$_codex_bin" printf '#!__PREFIX__/bin/bash\nPKG_BIN="%s"\nexport LD_LIBRARY_PATH="$PKG_BIN:${LD_LIBRARY_PATH:-}"\nexec "$PKG_BIN/codex.bin" "$@"\n' "$_codex_pkg" > "$_codex_bin" chmod +x "$_codex_bin" fi ;; esac # Fix shebangs in npm global CLI entry points after global install case "$*" in *-g*|*--global*) for _js in __PREFIX__/lib/node_modules/*/bin/*.js \ __PREFIX__/lib/node_modules/@*/*/bin/*.js; do [ -f "$_js" ] || continue head -1 "$_js" | grep -q '^#!/usr/bin/env node$' || continue sed -i "1s|#!/usr/bin/env node|#!__BIN_DIR__/node|" "$_js" done ;; esac exit $_npm_exit NPMWRAP sed -i "s|__PREFIX__|$PREFIX|g; s|__BIN_DIR__|$BIN_DIR|g; s|__NODE_DIR__|$NODE_DIR|g" "$BIN_DIR/npm" chmod +x "$BIN_DIR/npm" _any_fixed=true fi if [ -f "$NODE_DIR/lib/node_modules/npm/bin/npx-cli.js" ]; then cat > "$BIN_DIR/npx" << 'NPXWRAP' #!__PREFIX__/bin/bash exec "__BIN_DIR__/node" "__NODE_DIR__/lib/node_modules/npm/bin/npx-cli.js" "$@" NPXWRAP sed -i "s|__PREFIX__|$PREFIX|g; s|__BIN_DIR__|$BIN_DIR|g; s|__NODE_DIR__|$NODE_DIR|g" "$BIN_DIR/npx" chmod +x "$BIN_DIR/npx" _any_fixed=true fi if [ -f "$NODE_DIR/bin/corepack" ] && head -1 "$NODE_DIR/bin/corepack" 2>/dev/null | grep -q '#!/usr/bin/env node'; then sed -i "1s|#!/usr/bin/env node|#!$BIN_DIR/node|" "$NODE_DIR/bin/corepack" _any_fixed=true fi if [ "$_any_fixed" = true ]; then echo -e "${YELLOW}[FIX]${NC} wrappers repaired in $BIN_DIR" fi exit 0 fi LOWEST=$(printf '%s\n%s\n' "$INSTALLED_VER" "$NODE_VERSION" | sort -V | head -1) if [ "$LOWEST" = "$INSTALLED_VER" ] && [ "$INSTALLED_VER" != "$NODE_VERSION" ]; then echo -e "${YELLOW}[INFO]${NC} Node.js v${INSTALLED_VER} -> v${NODE_VERSION} (upgrading)" else echo -e "${GREEN}[SKIP]${NC} Node.js v${INSTALLED_VER} is newer than target v${NODE_VERSION}" exit 0 fi else echo -e "${YELLOW}[INFO]${NC} Node.js exists but broken — reinstalling" fi fi # ── Step 1: Download Node.js linux-arm64 ────── echo "Downloading Node.js v${NODE_VERSION} (linux-arm64)..." echo " (File size ~25MB — may take a few minutes depending on network speed)" mkdir -p "$NODE_DIR" TMP_DIR=$(mktemp -d "$PREFIX/tmp/node-install.XXXXXX") || { echo -e "${RED}[FAIL]${NC} Failed to create temp directory" exit 1 } trap 'rm -rf "$TMP_DIR"' EXIT if ! curl -fL --max-time 300 "$NODE_URL" -o "$TMP_DIR/$NODE_TARBALL"; then echo -e "${RED}[FAIL]${NC} Failed to download Node.js v${NODE_VERSION}" exit 1 fi echo -e "${GREEN}[OK]${NC} Downloaded $NODE_TARBALL" # Extract echo "Extracting Node.js... (this may take a moment)" if ! tar -xJf "$TMP_DIR/$NODE_TARBALL" -C "$NODE_DIR" --strip-components=1; then echo -e "${RED}[FAIL]${NC} Failed to extract Node.js" exit 1 fi echo -e "${GREEN}[OK]${NC} Extracted to $NODE_DIR" # ── Step 2: Create wrapper scripts ──────────── # # Wrappers are placed in BIN_DIR (~/.openclaw-android/bin/), separate from # NODE_DIR/bin/ which npm manages. This prevents npm from overwriting our # wrappers when users run 'npm install -g npm' or similar commands. echo "" echo "Creating wrapper scripts (grun-style, no patchelf)..." mkdir -p "$BIN_DIR" # Move original node binary to node.real if [ -f "$NODE_DIR/bin/node" ] && [ ! -L "$NODE_DIR/bin/node" ]; then mv "$NODE_DIR/bin/node" "$NODE_DIR/bin/node.real" fi # Create node wrapper script in BIN_DIR # This uses grun-style execution: ld.so directly loads the binary # LD_PRELOAD must be unset to prevent Bionic libtermux-exec.so from # being loaded into the glibc process (causes version mismatch crash) # glibc-compat.js is auto-loaded to fix Android kernel quirks (os.cpus() returns 0, # os.networkInterfaces() throws EACCES) that affect native module builds and runtime. cat > "$BIN_DIR/node" << WRAPPER #!${PREFIX}/bin/bash [ -n "\$LD_PRELOAD" ] && export _OA_ORIG_LD_PRELOAD="\$LD_PRELOAD" unset LD_PRELOAD export _OA_WRAPPER_PATH="$BIN_DIR/node" _OA_COMPAT="\$HOME/.openclaw-android/patches/glibc-compat.js" if [ -f "\$_OA_COMPAT" ]; then case "\${NODE_OPTIONS:-}" in *"\$_OA_COMPAT"*) ;; *) export NODE_OPTIONS="\${NODE_OPTIONS:+\$NODE_OPTIONS }-r \$_OA_COMPAT" ;; esac fi _LEADING_OPTS="" _COUNT=0 for _arg in "\$@"; do case "\$_arg" in --*) _COUNT=\$((_COUNT + 1)) ;; *) break ;; esac done if [ \$_COUNT -gt 0 ] && [ \$_COUNT -lt \$# ]; then while [ \$# -gt 0 ]; do case "\$1" in --*) _LEADING_OPTS="\${_LEADING_OPTS:+\$_LEADING_OPTS }\$1"; shift ;; *) break ;; esac done export NODE_OPTIONS="\${NODE_OPTIONS:+\$NODE_OPTIONS }\$_LEADING_OPTS" fi exec "$GLIBC_LDSO" --library-path "$PREFIX/glibc/lib" "$NODE_DIR/bin/node.real" "\$@" WRAPPER chmod +x "$BIN_DIR/node" echo -e "${GREEN}[OK]${NC} node wrapper created ($BIN_DIR/node)" # ── Step 2.5: Create npm/npx wrapper scripts in BIN_DIR ── # # npm/npx wrappers go in BIN_DIR (not NODE_DIR/bin/) so npm can't overwrite them. echo "Creating npm/npx wrapper scripts..." if [ -f "$NODE_DIR/lib/node_modules/npm/bin/npm-cli.js" ]; then cat > "$BIN_DIR/npm" << 'NPMWRAP' #!__PREFIX__/bin/bash "__BIN_DIR__/node" "__NODE_DIR__/lib/node_modules/npm/bin/npm-cli.js" "$@" _npm_exit=$? # Re-patch openclaw CLI wrapper after global install/update case "$*" in *-g*openclaw*|*--global*openclaw*|*openclaw*-g*|*openclaw*--global*) _oc_bin="__PREFIX__/bin/openclaw" _oc_mjs="__PREFIX__/lib/node_modules/openclaw/openclaw.mjs" if [ -f "$_oc_mjs" ]; then [ -L "$_oc_bin" ] && rm -f "$_oc_bin" printf '#!__PREFIX__/bin/bash\nexec "__BIN_DIR__/node" "%s" "$@"\n' "$_oc_mjs" > "$_oc_bin" chmod +x "$_oc_bin" fi ;; esac # Re-patch codex CLI wrapper after global install/update (DioNanos fork launcher fix) case "$*" in *codex-cli-termux*) _codex_bin="__PREFIX__/bin/codex" _codex_pkg="__PREFIX__/lib/node_modules/@mmmbuto/codex-cli-termux/bin" if [ -f "$_codex_pkg/codex.bin" ]; then [ -L "$_codex_bin" ] && rm -f "$_codex_bin" printf '#!__PREFIX__/bin/bash\nPKG_BIN="%s"\nexport LD_LIBRARY_PATH="$PKG_BIN:${LD_LIBRARY_PATH:-}"\nexec "$PKG_BIN/codex.bin" "$@"\n' "$_codex_pkg" > "$_codex_bin" chmod +x "$_codex_bin" fi ;; esac # Fix shebangs in npm global CLI entry points after global install case "$*" in *-g*|*--global*) for _js in __PREFIX__/lib/node_modules/*/bin/*.js \ __PREFIX__/lib/node_modules/@*/*/bin/*.js; do [ -f "$_js" ] || continue head -1 "$_js" | grep -q '^#!/usr/bin/env node$' || continue sed -i "1s|#!/usr/bin/env node|#!__BIN_DIR__/node|" "$_js" done ;; esac exit $_npm_exit NPMWRAP sed -i "s|__PREFIX__|$PREFIX|g; s|__BIN_DIR__|$BIN_DIR|g; s|__NODE_DIR__|$NODE_DIR|g" "$BIN_DIR/npm" chmod +x "$BIN_DIR/npm" echo -e "${GREEN}[OK]${NC} npm wrapper created ($BIN_DIR/npm)" fi if [ -f "$NODE_DIR/lib/node_modules/npm/bin/npx-cli.js" ]; then cat > "$BIN_DIR/npx" << 'NPXWRAP' #!__PREFIX__/bin/bash exec "__BIN_DIR__/node" "__NODE_DIR__/lib/node_modules/npm/bin/npx-cli.js" "$@" NPXWRAP sed -i "s|__PREFIX__|$PREFIX|g; s|__BIN_DIR__|$BIN_DIR|g; s|__NODE_DIR__|$NODE_DIR|g" "$BIN_DIR/npx" chmod +x "$BIN_DIR/npx" echo -e "${GREEN}[OK]${NC} npx wrapper created ($BIN_DIR/npx)" fi # corepack uses a different structure — shebang patch is sufficient if [ -f "$NODE_DIR/bin/corepack" ] && head -1 "$NODE_DIR/bin/corepack" 2>/dev/null | grep -q '#!/usr/bin/env node'; then sed -i "1s|#!/usr/bin/env node|#!$BIN_DIR/node|" "$NODE_DIR/bin/corepack" echo -e "${GREEN}[OK]${NC} corepack shebang patched" fi # ── Step 3: Configure npm ───────────────────── echo "" echo "Configuring npm..." # Set script-shell to ensure npm lifecycle scripts use the correct shell # On Android 9+, /bin/sh exists. On 7-8 it doesn't. # Using $PREFIX/bin/sh is always safe. export PATH="$BIN_DIR:$NODE_DIR/bin:$PATH" "$BIN_DIR/npm" config set script-shell "$PREFIX/bin/sh" 2>/dev/null || true echo -e "${GREEN}[OK]${NC} npm script-shell set to $PREFIX/bin/sh" # ── Step 4: Verify ──────────────────────────── echo "" echo "Verifying glibc Node.js..." NODE_VER=$("$BIN_DIR/node" --version 2>/dev/null) || { echo -e "${RED}[FAIL]${NC} Node.js verification failed — wrapper script may be broken" exit 1 } echo -e "${GREEN}[OK]${NC} Node.js $NODE_VER (glibc, grun wrapper)" NPM_VER=$("$BIN_DIR/npm" --version 2>/dev/null) || { echo -e "${YELLOW}[WARN]${NC} npm verification failed" } if [ -n "${NPM_VER:-}" ]; then echo -e "${GREEN}[OK]${NC} npm $NPM_VER" fi # Quick platform check PLATFORM=$("$BIN_DIR/node" -e "console.log(process.platform)" 2>/dev/null) || true if [ "$PLATFORM" = "linux" ]; then echo -e "${GREEN}[OK]${NC} platform: linux (correct)" else echo -e "${YELLOW}[WARN]${NC} platform: ${PLATFORM:-unknown} (expected: linux)" fi echo "" echo -e "${GREEN}Node.js installed successfully.${NC}" echo " Node.js: $NODE_VER ($BIN_DIR/node)"