#!/data/data/com.termux/files/usr/bin/bash # openclaw.sh - Install OpenClaw on Termux with glibc runtime # Usage: bash openclaw.sh set -euo pipefail # ── Colors ──────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' BOLD='\033[1m' NC='\033[0m' # ── Config ──────────────────────────────────────────────────── NODE_VERSION="22.14.0" NODE_DIR="$HOME/glibc-node" GLIBC_DIR="$PREFIX/glibc/lib" GLIBC_LDSO="$GLIBC_DIR/ld-linux-aarch64.so.1" COMPAT_JS="$NODE_DIR/lib/compat.js" BASHRC="$HOME/.bashrc" MARKER_START="# >>> OpenClaw >>>" MARKER_END="# <<< OpenClaw <<<" # ── Helpers ─────────────────────────────────────────────────── step() { echo ""; echo -e "${BOLD}[$1] $2${NC}"; echo "----------------------------------------"; } ok() { echo -e " ${GREEN}[OK]${NC} $1"; } warn() { echo -e " ${YELLOW}[WARN]${NC} $1"; } fail() { echo -e " ${RED}[FAIL]${NC} $1"; exit 1; } # ── Banner ──────────────────────────────────────────────────── echo "" echo -e "${BOLD}======================================${NC}" echo -e "${BOLD} OpenClaw on Termux — glibc install${NC}" echo -e "${BOLD}======================================${NC}" # ── Step 0: Pre-flight ──────────────────────────────────────── step "0/5" "Pre-flight checks" [ -z "${PREFIX:-}" ] && fail "Not running in Termux (\$PREFIX not set)" ok "Termux detected" [ "$(uname -m)" = "aarch64" ] || fail "Requires aarch64 (got: $(uname -m))" ok "Architecture: aarch64" AVAIL=$(df "$PREFIX" 2>/dev/null | awk 'NR==2 {print int($4/1024)}') [ "${AVAIL:-0}" -lt 1000 ] && fail "Insufficient disk space: ${AVAIL}MB (need 1000MB+)" ok "Disk space: ${AVAIL}MB available" # Tạo .bashrc nếu chưa có # Termux fresh install không có file này if [ ! -f "$BASHRC" ]; then touch "$BASHRC" ok "Created ~/.bashrc" else ok "~/.bashrc exists" fi # ── Step 1: Base deps ───────────────────────────────────────── step "1/5" "Installing base dependencies" pkg update -y && pkg upgrade -y # git: npm cần để resolve git dependencies # pacman: cài glibc-runner (không có trong apt repo) # proot: cần bởi glibc-runner # curl: download Node.js tarball pkg install -y git pacman proot curl ok "git, pacman, proot, curl installed" # ── Step 2: glibc-runner ────────────────────────────────────── step "2/5" "Installing glibc-runner via pacman" if [ -x "$GLIBC_LDSO" ]; then ok "glibc-runner already installed — skipping" else # SigLevel workaround: một số device có GPGME bug khi verify signature PACMAN_CONF="$PREFIX/etc/pacman.conf" if [ -f "$PACMAN_CONF" ] && ! grep -q "^SigLevel = Never" "$PACMAN_CONF"; then cp "$PACMAN_CONF" "${PACMAN_CONF}.bak" sed -i 's/^SigLevel\s*=.*/SigLevel = Never/' "$PACMAN_CONF" ok "Applied SigLevel = Never workaround" fi pacman-key --init 2>/dev/null || true pacman-key --populate 2>/dev/null || true # --assume-installed: pacman không biết về các package đã có qua apt pacman -Sy glibc-runner --noconfirm \ --assume-installed bash,patchelf,resolv-conf # Restore pacman.conf if [ -f "${PACMAN_CONF}.bak" ]; then mv "${PACMAN_CONF}.bak" "$PACMAN_CONF" ok "Restored pacman.conf" fi fi [ -x "$GLIBC_LDSO" ] || fail "glibc dynamic linker not found at $GLIBC_LDSO" ok "glibc dynamic linker ready" # Fix libc.so: glibc-runner ship linker script thay vì ELF binary # ld.so sẽ fail "invalid ELF header" nếu load linker script if [ -f "$GLIBC_DIR/libc.so" ]; then MAGIC=$(dd if="$GLIBC_DIR/libc.so" bs=4 count=1 2>/dev/null \ | od -A n -t x1 | tr -d ' \n') if [ "$MAGIC" != "7f454c46" ]; then rm "$GLIBC_DIR/libc.so" ln -s "$GLIBC_DIR/libc.so.6" "$GLIBC_DIR/libc.so" ok "Fixed libc.so (linker script → symlink to libc.so.6)" else ok "libc.so is already a valid ELF" fi fi # ── Step 3: Node.js glibc ───────────────────────────────────── step "3/5" "Installing Node.js $NODE_VERSION (linux-arm64 / glibc)" # Skip nếu wrapper đã hoạt động if [ -x "$NODE_DIR/bin/node" ] && "$NODE_DIR/bin/node" --version &>/dev/null; then ok "Node.js $("$NODE_DIR/bin/node" --version) already installed — skipping download" else NODE_URL="https://nodejs.org/dist/v${NODE_VERSION}/node-v${NODE_VERSION}-linux-arm64.tar.xz" TMP_TAR="$PREFIX/tmp/node-lts.tar.xz" mkdir -p "$NODE_DIR" echo " Downloading Node.js v${NODE_VERSION} (~25MB)..." curl -fL --progress-bar "$NODE_URL" -o "$TMP_TAR" tar -xJf "$TMP_TAR" -C "$NODE_DIR" --strip-components=1 rm -f "$TMP_TAR" ok "Node.js extracted to $NODE_DIR" # Rename binary → node.real [ -f "$NODE_DIR/bin/node.real" ] || mv "$NODE_DIR/bin/node" "$NODE_DIR/bin/node.real" ok "Renamed node → node.real" fi # Tạo compat.js — Android runtime fixes: # # Fix 1: process.execPath → wrapper script # node.real (ELF) spawn child node → child gọi ld.so trực tiếp với # --disable-warning=ExperimentalWarning → ld.so crash "unrecognized option" # Override execPath về wrapper bash để child dùng đúng wrapper # # Fix 2: os.networkInterfaces() → EACCES (error 13) # Android chặn đọc network interfaces → crash openclaw dashboard # Catch exception, trả về loopback interface tối giản # # Fix 3: os.cpus() → trả về [] # Android SELinux chặn /proc/stat → os.cpus() trả về [] # Tools dùng os.cpus().length cho parallelism sẽ crash vì length = 0 # Trả về 1 CPU fake nếu array rỗng mkdir -p "$NODE_DIR/lib" cat > "$COMPAT_JS" << 'COMPAT' 'use strict'; const fs = require('fs'); const os = require('os'); // Fix 1: process.execPath → wrapper script const WRAPPER = require('path').join( process.env.HOME, 'glibc-node', 'bin', 'node' ); try { if (fs.existsSync(WRAPPER)) { Object.defineProperty(process, 'execPath', { value: WRAPPER, writable: true, configurable: true }); } } catch {} // Fix 2: os.networkInterfaces() → EACCES (error 13) trên Android const _origNet = os.networkInterfaces; os.networkInterfaces = function networkInterfaces() { try { return _origNet.call(os); } catch { return { lo: [{ address: '127.0.0.1', netmask: '255.0.0.0', family: 'IPv4', mac: '00:00:00:00:00:00', internal: true, cidr: '127.0.0.1/8', }] }; } }; // Fix 3: os.cpus() → [] trên Android (SELinux chặn /proc/stat) const _origCpus = os.cpus; os.cpus = function cpus() { const r = _origCpus.call(os); if (r.length > 0) return r; return [{ model: 'unknown', speed: 0, times: { user: 0, nice: 0, sys: 0, idle: 0, irq: 0 } }]; }; COMPAT ok "compat.js created" # Tạo wrapper script: # - unset LD_PRELOAD: tránh Bionic libtermux-exec.so load vào glibc process # (ABI mismatch giữa Bionic và glibc → crash) # - --require compat.js: load Android fixes trước mọi code khác # - Move leading --flags vào NODE_OPTIONS: ld.so misparsed chúng là # option của chính nó (--disable-warning=X → "unrecognized option") cat > "$NODE_DIR/bin/node" << WRAPPER #!/data/data/com.termux/files/usr/bin/bash unset LD_PRELOAD LDSO="$GLIBC_LDSO" GLIBC="$GLIBC_DIR" REAL="\$(dirname "\$0")/node.real" COMPAT="$COMPAT_JS" # Load compat.js để fix process.execPath, networkInterfaces, cpus if [ -f "\$COMPAT" ]; then case "\${NODE_OPTIONS:-}" in *"\$COMPAT"*) ;; *) export NODE_OPTIONS="\${NODE_OPTIONS:+\$NODE_OPTIONS }--require \$COMPAT" ;; esac fi # Move leading --flags vào NODE_OPTIONS # ld.so không hiểu --disable-warning=X, --experimental-xxx, v.v. _COUNT=0 for _a in "\$@"; do case "\$_a" in --*) _COUNT=\$((_COUNT+1)) ;; *) break ;; esac done if [ \$_COUNT -gt 0 ] && [ \$_COUNT -lt \$# ]; then while [ \$# -gt 0 ]; do case "\$1" in --*) NODE_OPTIONS="\${NODE_OPTIONS:+\$NODE_OPTIONS }\$1"; shift ;; *) break ;; esac done export NODE_OPTIONS fi exec "\$LDSO" --library-path "\$GLIBC" "\$REAL" "\$@" WRAPPER chmod +x "$NODE_DIR/bin/node" ok "Wrapper script created" # Verify export PATH="$NODE_DIR/bin:$PATH" NODE_VER=$(node --version 2>/dev/null) || fail "Node.js wrapper failed — check glibc setup" PLATFORM=$(node -e "console.log(process.platform)" 2>/dev/null) [ "$PLATFORM" = "linux" ] || warn "process.platform=$PLATFORM (expected: linux)" ok "Node.js $NODE_VER — platform: $PLATFORM" ok "npm $(npm --version)" # ── Step 4: Install OpenClaw ────────────────────────────────── step "4/5" "Installing OpenClaw" export TMPDIR="$PREFIX/tmp" export CONTAINER=1 npm config set script-shell "$PREFIX/bin/sh" 2>/dev/null || true # ar symlink (Termux có llvm-ar, không có ar) if [ ! -e "$PREFIX/bin/ar" ] && [ -x "$PREFIX/bin/llvm-ar" ]; then ln -s "$PREFIX/bin/llvm-ar" "$PREFIX/bin/ar" ok "Created ar → llvm-ar symlink" fi # --ignore-scripts: bỏ qua postinstall của node-llama-cpp # Nó cố compile llama.cpp từ source (~30 phút, fail trên Android) # Prebuilt @node-llama-cpp/linux-arm64 binary vẫn được install bình thường echo " Running npm install -g openclaw@latest --ignore-scripts..." npm install -g openclaw@latest --ignore-scripts ok "OpenClaw installed" # Patch hardcoded paths trong JS files # OpenClaw hardcode /tmp, /bin/sh, /bin/bash, /usr/bin/env # Các path này không tồn tại trên Android — phải map về $PREFIX echo " Patching hardcoded paths..." OC_DIR="$(npm root -g)/openclaw" [ -d "$OC_DIR" ] || fail "OpenClaw directory not found at $OC_DIR" do_patch() { local old="$1"; local new="$2" grep -rl "$old" "$OC_DIR" \ --include='*.js' --include='*.mjs' --include='*.cjs' \ 2>/dev/null | while read -r f; do sed -i "s|$old|$new|g" "$f" done } do_patch '"/tmp"' "\"$PREFIX/tmp\"" do_patch "'/tmp'" "'$PREFIX/tmp'" do_patch '"/tmp/' "\"$PREFIX/tmp/" do_patch "'/tmp/" "'$PREFIX/tmp/" do_patch '"/bin/sh"' "\"$PREFIX/bin/sh\"" do_patch "'/bin/sh'" "'$PREFIX/bin/sh'" do_patch '"/bin/bash"' "\"$PREFIX/bin/bash\"" do_patch "'/bin/bash'" "'$PREFIX/bin/bash'" do_patch '"/usr/bin/env"' "\"$PREFIX/bin/env\"" do_patch "'/usr/bin/env'" "'$PREFIX/bin/env'" ok "Path patching complete" OC_VER=$(openclaw --version 2>/dev/null) || fail "openclaw --version failed" ok "OpenClaw $OC_VER" # ── Step 5: Environment ─────────────────────────────────────── step "5/5" "Configuring environment (~/.bashrc)" # Xóa block cũ nếu có (idempotent) if grep -qF "$MARKER_START" "$BASHRC" 2>/dev/null; then sed -i "/${MARKER_START//\//\\/}/,/${MARKER_END//\//\\/}/d" "$BASHRC" ok "Removed old environment block" fi cat >> "$BASHRC" << EOF ${MARKER_START} export PATH="$NODE_DIR/bin:\$PATH" export TMPDIR="\$PREFIX/tmp" export TMP="\$TMPDIR" export TEMP="\$TMPDIR" export CONTAINER=1 ${MARKER_END} EOF ok "Environment block added to ~/.bashrc" # Apply cho session hiện tại export PATH="$NODE_DIR/bin:$PATH" export TMPDIR="$PREFIX/tmp" export TMP="$TMPDIR" export TEMP="$TMPDIR" export CONTAINER=1 ok "Environment applied to current session" # ── Done ────────────────────────────────────────────────────── echo "" echo -e "${BOLD}======================================${NC}" echo -e "${GREEN}${BOLD} Installation Complete!${NC}" echo -e "${BOLD}======================================${NC}" echo "" echo -e " OpenClaw $(openclaw --version)" echo -e " Node.js $(node --version) (glibc linux-arm64)" echo "" echo " Run setup:" echo "" echo " openclaw onboard" echo ""