Files
2026-07-13 13:16:30 +08:00

353 lines
14 KiB
Bash
Executable File

#!/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)"