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

769 lines
29 KiB
Bash

#!/usr/bin/env bash
# OpenClaw Android — Post-Bootstrap Setup
# Runs in the terminal after Termux bootstrap extraction.
# Installs: git, glibc, Node.js, OpenClaw
#
# Strategy:
# - Termux .deb packages: dpkg-deb -x + relocate (bypasses dpkg hardcoded paths)
# - Pacman .pkg.tar.xz packages: tar -xJf + relocate (bypasses pacman entirely)
# - Both have files under data/data/com.termux/files/usr/ which we relocate to $PREFIX
#
# Why not apt-get/dpkg/pacman?
# All three have hardcoded /data/data/com.termux/... paths that libtermux-exec
# cannot rewrite (it only intercepts execve, not open/opendir).
set -eo pipefail
# ─── Paths ────────────────────────────────────
: "${PREFIX:?PREFIX not set}"
: "${HOME:?HOME not set}"
: "${TMPDIR:=$(dirname "$PREFIX")/tmp}"
OCA_DIR="$HOME/.openclaw-android"
NODE_DIR="$OCA_DIR/node"
BIN_DIR="$OCA_DIR/bin"
NODE_VERSION="22.22.0"
GLIBC_LDSO="$PREFIX/glibc/lib/ld-linux-aarch64.so.1"
MARKER="$OCA_DIR/.post-setup-done"
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
NC='\033[0m'
# ─── GitHub mirror fallback (for China/restricted networks) ──
REPO_BASE_ORIGIN="https://raw.githubusercontent.com/AidanPark/openclaw-android/main"
REPO_BASE="$REPO_BASE_ORIGIN"
resolve_repo_base() {
if curl -sI --connect-timeout 3 "$REPO_BASE_ORIGIN/oa.sh" >/dev/null 2>&1; then
REPO_BASE="$REPO_BASE_ORIGIN"; return 0
fi
local mirrors=(
"https://ghfast.top/$REPO_BASE_ORIGIN"
"https://ghproxy.net/$REPO_BASE_ORIGIN"
"https://mirror.ghproxy.com/$REPO_BASE_ORIGIN"
)
for m in "${mirrors[@]}"; do
if curl -sI --connect-timeout 3 "$m/oa.sh" >/dev/null 2>&1; then
echo -e " ${YELLOW}[MIRROR]${NC} Using mirror for GitHub downloads"
REPO_BASE="$m"; return 0
fi
done
return 1
}
# Kept in sync with scripts/lib.sh resolve_npm_registry()
NPM_REGISTRY_ORIGIN="https://registry.npmjs.org/"
NPM_REGISTRY_MIRROR="https://registry.npmmirror.com/"
resolve_npm_registry() {
local choice
local cache_file="$OCA_DIR/.npm-registry"
local reachable=0
if curl -sI --connect-timeout 5 "$NPM_REGISTRY_ORIGIN" >/dev/null 2>&1; then
choice="$NPM_REGISTRY_ORIGIN"
reachable=1
elif curl -sI --connect-timeout 5 "$NPM_REGISTRY_MIRROR" >/dev/null 2>&1; then
echo -e " ${YELLOW}[MIRROR]${NC} Using npm mirror: ${NPM_REGISTRY_MIRROR}"
choice="$NPM_REGISTRY_MIRROR"
reachable=1
else
choice="$NPM_REGISTRY_ORIGIN"
fi
mkdir -p "$(dirname "$cache_file")"
printf '%s' "$choice" > "$cache_file.tmp" && mv "$cache_file.tmp" "$cache_file"
export NPM_CONFIG_REGISTRY="$choice"
if [ "$reachable" -eq 1 ]; then
return 0
fi
return 1
}
# SSL cert for curl (bootstrap curl looks at hardcoded com.termux path)
export CURL_CA_BUNDLE="$PREFIX/etc/tls/cert.pem"
export SSL_CERT_FILE="$PREFIX/etc/tls/cert.pem"
export GIT_SSL_CAINFO="$PREFIX/etc/tls/cert.pem"
# Git system config has hardcoded com.termux path — skip it
export GIT_CONFIG_NOSYSTEM=1
# Git exec path (git looks for helpers like git-remote-https here)
export GIT_EXEC_PATH="$PREFIX/libexec/git-core"
# Git template dir (hardcoded /data/data/com.termux path workaround)
export GIT_TEMPLATE_DIR="$PREFIX/share/git-core/templates"
if [ -f "$MARKER" ]; then
echo -e "${GREEN}Post-setup already completed.${NC}"
exit 0
fi
echo ""
echo "══════════════════════════════════════════════"
echo " OpenClaw Android — Installing components"
echo "══════════════════════════════════════════════"
echo ""
mkdir -p "$OCA_DIR" "$OCA_DIR/patches" "$TMPDIR"
TERMUX_DEB_REPO="https://packages-cf.termux.dev/apt/termux-main"
PACMAN_PKG_REPO="https://service.termux-pacman.dev/gpkg/aarch64"
TERMUX_INNER="data/data/com.termux/files/usr"
DEB_DIR="$TMPDIR/debs"
PKG_DIR="$TMPDIR/pkgs"
EXTRACT_DIR="$TMPDIR/pkg-extract"
# ─── Helper: install_deb ──────────────────────
# Downloads a .deb from Termux repo and extracts into $PREFIX
install_deb() {
local filename="$1"
local name
name=$(basename "$filename" | sed 's/_[0-9].*//')
local url="${TERMUX_DEB_REPO}/${filename}"
local deb_file="${DEB_DIR}/$(basename "$filename")"
if [ -f "$deb_file" ]; then
echo " (cached) $name"
else
echo " downloading $name..."
curl -fsSL --max-time 120 -o "$deb_file" "$url"
fi
rm -rf "$EXTRACT_DIR"
mkdir -p "$EXTRACT_DIR"
dpkg-deb -x "$deb_file" "$EXTRACT_DIR" 2>/dev/null
# Relocate: data/data/com.termux/files/usr/* → $PREFIX/
if [ -d "$EXTRACT_DIR/$TERMUX_INNER" ]; then
cp -a "$EXTRACT_DIR/$TERMUX_INNER/"* "$PREFIX/" 2>/dev/null || true
fi
rm -rf "$EXTRACT_DIR"
}
# ─── Helper: install_pacman_pkg ───────────────
# Downloads a .pkg.tar.xz from pacman repo and extracts into target dir
install_pacman_pkg() {
local filename="$1"
local target="$2" # e.g., $PREFIX/glibc
local name
name=${filename%%-[0-9]*}
local url="${PACMAN_PKG_REPO}/${filename}"
local pkg_file="${PKG_DIR}/${filename}"
if [ -f "$pkg_file" ]; then
echo " (cached) $name"
else
echo " downloading $name..."
curl -fsSL --max-time 300 -o "$pkg_file" "$url"
fi
rm -rf "$EXTRACT_DIR"
mkdir -p "$EXTRACT_DIR"
tar -xJf "$pkg_file" -C "$EXTRACT_DIR" 2>/dev/null
# Pacman packages also extract under data/data/com.termux/files/usr/...
local inner="$EXTRACT_DIR/$TERMUX_INNER"
if [ -d "$inner/glibc" ]; then
# glibc packages go under $PREFIX/glibc/
cp -a "$inner/glibc/"* "$target/" 2>/dev/null || true
elif [ -d "$inner" ]; then
cp -a "$inner/"* "$target/" 2>/dev/null || true
fi
rm -rf "$EXTRACT_DIR"
}
# ─── [1/7] Install essential packages ─────────
echo -e "▸ ${YELLOW}[1/7]${NC} Installing essential packages..."
mkdir -p "$DEB_DIR" "$PKG_DIR"
# Download Packages index to resolve .deb filenames
echo " Fetching package index..."
PACKAGES_FILE="$TMPDIR/Packages"
curl -fsSL --max-time 60 \
"${TERMUX_DEB_REPO}/dists/stable/main/binary-aarch64/Packages" \
-o "$PACKAGES_FILE"
# Resolve package filename from Packages index
get_deb_filename() {
local pkg="$1"
awk -v pkg="$pkg" '
/^Package: / { found = ($2 == pkg) }
found && /^Filename:/ { print $2; exit }
' "$PACKAGES_FILE"
}
# Packages to install via dpkg-deb (dependency order, only those missing from bootstrap)
DEB_PACKAGES=(
libexpat # git dep
pcre2 # git dep
git # for npm/openclaw
)
TOTAL=${#DEB_PACKAGES[@]}
COUNT=0
for pkg in "${DEB_PACKAGES[@]}"; do
COUNT=$((COUNT + 1))
filename=$(get_deb_filename "$pkg")
if [ -z "$filename" ]; then
echo -e " ${RED}${NC} Package '$pkg' not found in index"
continue
fi
echo " [$COUNT/$TOTAL] $pkg"
install_deb "$filename"
done
# Make sure newly extracted binaries are executable
chmod +x "$PREFIX/bin/"* 2>/dev/null || true
# Verify git
if [ -f "$PREFIX/bin/git" ]; then
echo -e " ${GREEN}${NC} git $(git --version 2>/dev/null | head -1)"
else
echo -e " ${RED}${NC} git not found after extraction"
exit 1
fi
# ─── [2/7] glibc runtime ─────────────────────
echo -e "▸ ${YELLOW}[2/7]${NC} Installing glibc runtime..."
if [ -x "$GLIBC_LDSO" ]; then
echo -e " ${GREEN}[SKIP]${NC} glibc already installed"
else
mkdir -p "$PREFIX/glibc"
# Download glibc package directly from pacman repo (no pacman needed)
# The gpkg.db tells us: glibc-2.42-0-aarch64.pkg.tar.xz (~9.7MB)
echo " Downloading glibc (~10MB)..."
install_pacman_pkg "glibc-2.42-0-aarch64.pkg.tar.xz" "$PREFIX/glibc"
# gcc-libs-glibc provides libstdc++.so.6 needed by Node.js (~24MB)
echo " Downloading gcc-libs (~24MB)..."
install_pacman_pkg "gcc-libs-glibc-14.2.1-1-aarch64.pkg.tar.xz" "$PREFIX/glibc"
# Verify linker
if [ ! -f "$GLIBC_LDSO" ]; then
echo -e " ${RED}${NC} glibc linker not found at $GLIBC_LDSO"
exit 1
fi
chmod +x "$GLIBC_LDSO"
mkdir -p "$OCA_DIR"
touch "$OCA_DIR/.glibc-arch"
echo -e " ${GREEN}${NC} glibc installed"
fi
# Install supplementary glibc libraries (libcap etc.)
_GLIBC_LIBS_SRC="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/patches/glibc-libs"
if [ -d "$PREFIX/glibc/lib" ] && [ -d "$_GLIBC_LIBS_SRC" ]; then
for _lib in "$_GLIBC_LIBS_SRC"/*.so.*; do
[ -f "$_lib" ] || continue
_fn=$(basename "$_lib")
if [ ! -f "$PREFIX/glibc/lib/$_fn" ]; then
cp "$_lib" "$PREFIX/glibc/lib/$_fn"
_sn=$(echo "$_fn" | sed -E 's/^(lib[^.]+\.so\.[0-9]+)\..*/\1/')
[ "$_sn" != "$_fn" ] && ln -sf "$_fn" "$PREFIX/glibc/lib/$_sn"
echo -e " ${GREEN}${NC} Installed $_sn"
fi
done
fi
# Ensure glibc /etc/hosts exists (localhost resolution)
if [ -d "$PREFIX/glibc/etc" ] && [ ! -f "$PREFIX/glibc/etc/hosts" ]; then
cat > "$PREFIX/glibc/etc/hosts" <<'HOSTS'
127.0.0.1 localhost localhost.localdomain
::1 localhost ip6-localhost ip6-loopback
HOSTS
echo -e " ${GREEN}${NC} Created glibc /etc/hosts"
fi
echo -e " Linker: $GLIBC_LDSO"
# ─── [3/7] Node.js ──────────────────────────
echo -e "▸ ${YELLOW}[3/7]${NC} Installing Node.js v${NODE_VERSION}..."
mkdir -p "$NODE_DIR/bin"
_NODE_CMD=""
if [ -x "$BIN_DIR/node" ]; then _NODE_CMD="$BIN_DIR/node"
elif [ -f "$NODE_DIR/bin/node.real" ] && [ -x "$NODE_DIR/bin/node" ]; then _NODE_CMD="$NODE_DIR/bin/node"
fi
if [ -n "$_NODE_CMD" ] && "$_NODE_CMD" --version &>/dev/null; then
INSTALLED_VER=$("$_NODE_CMD" --version 2>/dev/null || echo "")
echo -e " ${GREEN}[SKIP]${NC} Node.js already installed ($INSTALLED_VER)"
# Repair wrappers in BIN_DIR (safe from npm overwrites)
mkdir -p "$BIN_DIR"
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
chmod +x "$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
chmod +x "$BIN_DIR/npx"
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"
fi
else
NODE_TAR="node-v${NODE_VERSION}-linux-arm64"
echo " Downloading Node.js v${NODE_VERSION} (~25MB)..."
curl -fSL --max-time 300 \
"https://nodejs.org/dist/v${NODE_VERSION}/${NODE_TAR}.tar.xz" \
-o "$TMPDIR/${NODE_TAR}.tar.xz"
echo " Extracting..."
tar -xJf "$TMPDIR/${NODE_TAR}.tar.xz" -C "$NODE_DIR" --strip-components=1
# Move original binary → 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
rm -f "$TMPDIR/${NODE_TAR}.tar.xz"
# Create grun-style node wrapper in BIN_DIR (safe from npm overwrites)
mkdir -p "$BIN_DIR"
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"
# Create npm/npx wrappers in BIN_DIR
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
chmod +x "$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
chmod +x "$BIN_DIR/npx"
fi
# corepack: shebang patch only
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"
fi
# Configure npm
export PATH="$BIN_DIR:$NODE_DIR/bin:$PATH"
"$BIN_DIR/npm" config set script-shell "$PREFIX/bin/sh" 2>/dev/null || true
# Verify
NODE_VER=$("$BIN_DIR/node" --version 2>/dev/null) || {
echo -e " ${RED}${NC} Node.js verification failed"
exit 1
}
echo -e " ${GREEN}${NC} Node.js $NODE_VER (glibc)"
fi
# ─── [4/7] OpenClaw ─────────────────────────
echo -e "▸ ${YELLOW}[4/7]${NC} Installing OpenClaw..."
export PATH="$BIN_DIR:$NODE_DIR/bin:$PATH"
# Auto-detect GitHub mirror for restricted networks
resolve_repo_base
# Auto-detect npm registry (session-scoped via NPM_CONFIG_REGISTRY env var).
# Does NOT write to ~/.npmrc — see CHANGELOG v1.0.24.
resolve_npm_registry || true
# Force git to use HTTPS instead of SSH (no SSH client available).
# Preserve any existing user .gitconfig (name, email, aliases); only set our keys.
touch "$HOME/.gitconfig"
git config --global http.sslCAInfo "$PREFIX/etc/tls/cert.pem"
git config --global --unset-all url."https://github.com/".insteadOf 2>/dev/null || true
git config --global --add url."https://github.com/".insteadOf "ssh://git@github.com/"
git config --global --add url."https://github.com/".insteadOf "git@github.com:"
# Git wrapper: replace $PREFIX/bin/git with a wrapper that:
# 1. Strips --recurse-submodules (triggers open() on hardcoded com.termux path)
# 2. Cleans existing target dirs before clone (npm's withTempDir creates dir first)
# npm caches git path at module load via which.sync('git'), so we must replace the binary.
# $PREFIX/bin/git is a symlink -> ../libexec/git-core/git (the real ELF binary).
REAL_GIT="$PREFIX/libexec/git-core/git"
if [ -f "$REAL_GIT" ] && [ ! -f "$PREFIX/bin/git.wrapper-installed" ]; then
echo " Installing git wrapper (strips --recurse-submodules)..."
rm -f "$PREFIX/bin/git"
# Write shebang with absolute path (no LD_PRELOAD = no /bin/bash rewrite)
echo "#!${PREFIX}/bin/bash" > "$PREFIX/bin/git"
cat >> "$PREFIX/bin/git" << 'ENDWRAP'
filtered=()
is_clone=false
for a in "$@"; do
case "$a" in
--recurse-submodules) ;;
clone) is_clone=true; filtered+=("$a") ;;
*) filtered+=("$a") ;;
esac
done
if $is_clone; then
for a in "${filtered[@]}"; do
case "$a" in
clone|--*|-*|http*|ssh*|git*|[0-9]) ;;
*) [ -d "$a" ] && rm -rf "$a" ;;
esac
done
fi
ENDWRAP
echo "exec \"$REAL_GIT\" \"\${filtered[@]}\"" >> "$PREFIX/bin/git"
chmod +x "$PREFIX/bin/git"
touch "$PREFIX/bin/git.wrapper-installed"
echo -e " ${GREEN}\u2713${NC} git wrapper installed"
else
if [ -f "$PREFIX/bin/git.wrapper-installed" ]; then
echo -e " ${GREEN}[SKIP]${NC} git wrapper already installed"
else
echo -e " ${RED}\u2717${NC} Real git not found at $REAL_GIT"
exit 1
fi
fi
if command -v openclaw &>/dev/null 2>&1; then
OC_VER=$(openclaw --version 2>/dev/null || echo "unknown")
echo -e " ${GREEN}[SKIP]${NC} OpenClaw already installed ($OC_VER)"
else
# Clean npm cache tmp dir (leftover from previous failed installs)
rm -rf "$HOME/.npm/_cacache/tmp" 2>/dev/null || true
npm install -g openclaw@latest --ignore-scripts 2>&1
OC_VER=$(openclaw --version 2>/dev/null || echo "installed")
echo -e " ${GREEN}${NC} OpenClaw $OC_VER"
fi
# Restore optional/channel deps that --ignore-scripts skips.
# Uses npm_config_ignore_scripts=true so sharp's native build doesn't block.
OPENCLAW_DIR="$(npm root -g)/openclaw"
if [ -d "$OPENCLAW_DIR" ]; then
echo " Restoring optional dependencies..."
(cd "$OPENCLAW_DIR" && npm_config_ignore_scripts=true node scripts/postinstall-bundled-plugins.mjs 2>/dev/null) || true
fi
# Install clawdhub (skill manager)
echo " Installing clawdhub..."
if npm install -g clawdhub --no-fund --no-audit; then
echo -e " ${GREEN}${NC} clawdhub installed"
CLAWHUB_DIR="$(npm root -g)/clawdhub"
if [ -d "$CLAWHUB_DIR" ] && ! (cd "$CLAWHUB_DIR" && node -e "require('undici')" 2>/dev/null); then
echo " Installing undici dependency for clawdhub..."
(cd "$CLAWHUB_DIR" && npm install undici --no-fund --no-audit) || true
fi
else
echo -e " ${YELLOW}[WARN]${NC} clawdhub installation failed (non-critical)"
fi
# PyYAML (for .skill packaging)
command -v python &>/dev/null && { python -c "import yaml" 2>/dev/null || pip install pyyaml -q || true; }
# Run openclaw update (builds native modules like sharp)
echo " Running: openclaw update (this may take 5-10 minutes)..."
openclaw update || true
# ─── [5/7] Patches ──────────────────────────
echo -e "▸ ${YELLOW}[5/7]${NC} Applying patches..."
# Copy glibc-compat.js from project (bundled alongside this script)
COMPAT_SRC="$(dirname "$0")/glibc-compat.js"
if [ -f "$COMPAT_SRC" ]; then
cp "$COMPAT_SRC" "$OCA_DIR/patches/glibc-compat.js"
else
# Fallback: download from repo
curl -fsSL "$REPO_BASE/patches/glibc-compat.js" \
-o "$OCA_DIR/patches/glibc-compat.js" 2>/dev/null || true
fi
# systemctl stub
printf '#!%s/bin/bash\nexit 0\n' "$PREFIX" > "$PREFIX/bin/systemctl"
chmod +x "$PREFIX/bin/systemctl"
# sharp WASM fallback (prebuilt native binaries don't load on Android)
if [ -d "$OPENCLAW_DIR/node_modules/sharp" ]; then
if ! node -e "require('$OPENCLAW_DIR/node_modules/sharp')" 2>/dev/null; then
echo " Installing sharp WebAssembly runtime..."
(cd "$OPENCLAW_DIR" && npm install @img/sharp-wasm32 --force --no-audit --no-fund 2>&1 | tail -3) || true
fi
fi
echo -e " ${GREEN}${NC} Patches applied"
# ─── [6/7] Environment ──────────────────────
echo -e "▸ ${YELLOW}[6/7]${NC} Configuring environment..."
cat > "$HOME/.bashrc" << BASHRC
# OpenClaw Android environment
export PREFIX="$PREFIX"
export HOME="$HOME"
export TMPDIR="$TMPDIR"
export PATH="$BIN_DIR:$NODE_DIR/bin:\$PREFIX/bin:\$PATH"
export LD_LIBRARY_PATH="$PREFIX/lib"
export LD_PRELOAD="$PREFIX/lib/libtermux-exec.so"
export TERMUX__PREFIX="$PREFIX"
export TERMUX_PREFIX="$PREFIX"
export LANG=en_US.UTF-8
export TERM=xterm-256color
export OA_GLIBC=1
export CONTAINER=1
export SSL_CERT_FILE="$PREFIX/etc/tls/cert.pem"
export CURL_CA_BUNDLE="$PREFIX/etc/tls/cert.pem"
export GIT_SSL_CAINFO="$PREFIX/etc/tls/cert.pem"
export GIT_CONFIG_NOSYSTEM=1
export GIT_EXEC_PATH="$PREFIX/libexec/git-core"
export GIT_TEMPLATE_DIR="$PREFIX/share/git-core/templates"
export CLAWDHUB_WORKDIR="$HOME/.openclaw/workspace"
export CPATH="$PREFIX/include/glib-2.0:$PREFIX/lib/glib-2.0/include"
# npm registry (auto-detected by OpenClaw Android, safe to override manually)
[ -z "\${NPM_CONFIG_REGISTRY:-}" ] && [ -s "\$HOME/.openclaw-android/.npm-registry" ] && \\
export NPM_CONFIG_REGISTRY="\$(cat "\$HOME/.openclaw-android/.npm-registry")"
BASHRC
echo -e " ${GREEN}${NC} ~/.bashrc configured"
# oa CLI (enables oa --update, oa --backup, etc.)
if curl -fsSL "$REPO_BASE/oa.sh" \
-o "$PREFIX/bin/oa" 2>/dev/null; then
chmod +x "$PREFIX/bin/oa"
echo -e " ${GREEN}${NC} oa CLI installed"
else
echo -e " ${YELLOW}[WARN]${NC} oa CLI installation failed (non-critical)"
fi
# ─── [7/7] Optional Tools ──────────────────
TOOL_CONF="$OCA_DIR/tool-selections.conf"
if [ -f "$TOOL_CONF" ]; then
# shellcheck source=/dev/null
source "$TOOL_CONF"
HAS_TOOLS=false
for var in INSTALL_TMUX INSTALL_TTYD INSTALL_DUFS INSTALL_CODE_SERVER INSTALL_PLAYWRIGHT INSTALL_CLAUDE_CODE INSTALL_GEMINI_CLI INSTALL_CODEX_CLI; do
eval "val=\${$var:-false}"
# shellcheck disable=SC2154
[ "$val" = "true" ] && HAS_TOOLS=true && break
done
if $HAS_TOOLS; then
echo -e "▸ ${YELLOW}[7/7]${NC} Installing optional tools..."
# Helper: install .deb with direct dependencies
install_with_deps() {
local pkg="$1"
local deps
deps=$(awk -v pkg="$pkg" '
/^Package: / { found = ($2 == pkg) }
found && /^Depends:/ {
gsub(/^Depends: /, "")
gsub(/ *\([^)]*\)/, "")
gsub(/, /, "\n")
print; exit
}
' "$PACKAGES_FILE")
while IFS= read -r dep; do
dep=$(echo "$dep" | tr -d ' ')
[ -z "$dep" ] && continue
local dep_file
dep_file=$(get_deb_filename "$dep")
if [ -n "$dep_file" ]; then install_deb "$dep_file" 2>/dev/null || true; fi
done <<< "$deps"
local filename
filename=$(get_deb_filename "$pkg")
[ -n "$filename" ] && install_deb "$filename"
}
# Termux packages
[ "${INSTALL_TMUX:-false}" = "true" ] && {
echo " Installing tmux..."
install_with_deps tmux
echo -e " ${GREEN}${NC} tmux"
}
[ "${INSTALL_TTYD:-false}" = "true" ] && {
echo " Installing ttyd..."
install_with_deps ttyd
echo -e " ${GREEN}${NC} ttyd"
}
[ "${INSTALL_DUFS:-false}" = "true" ] && {
echo " Installing dufs..."
install_with_deps dufs
echo -e " ${GREEN}${NC} dufs"
}
# npm packages
[ "${INSTALL_CODE_SERVER:-false}" = "true" ] && {
echo " Installing code-server (this may take a while)..."
npm install -g code-server 2>&1 || true
echo -e " ${GREEN}${NC} code-server"
}
[ "${INSTALL_PLAYWRIGHT:-false}" = "true" ] && {
echo " Installing Playwright (playwright-core)..."
npm install -g playwright-core 2>&1 || true
# Set Playwright environment variables if Chromium is available
CHROMIUM_BIN=""
for bin in "$PREFIX/bin/chromium-browser" "$PREFIX/bin/chromium"; do
[ -x "$bin" ] && CHROMIUM_BIN="$bin" && break
done
if [ -n "$CHROMIUM_BIN" ]; then
PW_MARKER_START="# >>> Playwright >>>"
PW_MARKER_END="# <<< Playwright <<<"
if ! grep -qF "$PW_MARKER_START" "$HOME/.bashrc"; then
cat >> "$HOME/.bashrc" << PWENV
${PW_MARKER_START}
export PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH="$CHROMIUM_BIN"
export PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD=1
${PW_MARKER_END}
PWENV
fi
echo -e " ${GREEN}${NC} Playwright (env: PLAYWRIGHT_CHROMIUM_EXECUTABLE_PATH=$CHROMIUM_BIN)"
else
echo -e " ${GREEN}${NC} Playwright (install Chromium later via 'oa --install' for full setup)"
fi
}
[ "${INSTALL_CLAUDE_CODE:-false}" = "true" ] && {
echo " Installing Claude Code..."
npm install -g @anthropic-ai/claude-code 2>&1 || true
echo -e " ${GREEN}${NC} Claude Code"
}
[ "${INSTALL_GEMINI_CLI:-false}" = "true" ] && {
echo " Installing Gemini CLI..."
npm install -g @google/gemini-cli 2>&1 || true
echo -e " ${GREEN}${NC} Gemini CLI"
}
[ "${INSTALL_CODEX_CLI:-false}" = "true" ] && {
echo " Installing Codex CLI (Termux)..."
npm install -g @mmmbuto/codex-cli-termux 2>&1 || true
# Create codex CLI wrapper (DioNanos fork launcher fix)
_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 '#!%s/bin/bash\nPKG_BIN="%s"\nexport LD_LIBRARY_PATH="$PKG_BIN:${LD_LIBRARY_PATH:-}"\nexec "$PKG_BIN/codex.bin" "$@"\n' \
"$PREFIX" "$_codex_pkg" > "$_codex_bin"
chmod +x "$_codex_bin"
fi
echo -e " ${GREEN}${NC} Codex CLI (Termux)"
}
# Fix shebangs in npm global CLIs (kept in sync with scripts/lib.sh fix_npm_global_shebangs())
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
else
echo -e "▸ ${YELLOW}[7/7]${NC} No optional tools selected"
fi
else
echo -e "▸ ${YELLOW}[7/7]${NC} No optional tools selected"
fi
# ─── Cleanup ────────────────────────────────
rm -rf "$DEB_DIR" "$PKG_DIR" "$PACKAGES_FILE" "$TMPDIR/gpkg.db" 2>/dev/null || true
# ─── Done ────────────────────────────────────
touch "$MARKER"
echo ""
echo "══════════════════════════════════════════════"
echo -e " ${GREEN}✓ Installation complete!${NC}"
echo "══════════════════════════════════════════════"
echo ""
echo " Loading environment..."
source "$HOME/.bashrc"
echo ""
echo " Starting OpenClaw onboard..."
echo ""
openclaw onboard