fix(android): harden Termux source-build and wasm-tools fallback

This commit is contained in:
Chummy 2026-02-28 09:48:03 +00:00 committed by Chum Yin
parent f0a5bbdb1b
commit bebb881b5b
6 changed files with 322 additions and 24 deletions

View File

@ -4,9 +4,10 @@ rustflags = ["-C", "link-arg=-static"]
[target.aarch64-unknown-linux-musl]
rustflags = ["-C", "link-arg=-static"]
# Android targets (NDK toolchain)
# Android targets (Termux-native defaults).
# CI/NDK cross builds can override these via CARGO_TARGET_*_LINKER.
[target.armv7-linux-androideabi]
linker = "armv7a-linux-androideabi21-clang"
linker = "clang"
[target.aarch64-linux-android]
linker = "aarch64-linux-android21-clang"
linker = "clang"

View File

@ -205,6 +205,8 @@ landlock = { version = "0.4", optional = true }
libc = "0.2"
[features]
# Default enables wasm-tools where platform runtime dependencies are available.
# Unsupported targets (for example Android/Termux) use a stub implementation.
default = ["wasm-tools"]
hardware = ["nusb", "tokio-serial"]
channel-matrix = ["dep:matrix-sdk"]
@ -228,6 +230,7 @@ probe = ["dep:probe-rs"]
# rag-pdf = PDF ingestion for datasheet RAG
rag-pdf = ["dep:pdf-extract"]
# wasm-tools = WASM plugin engine for dynamically-loaded tool packages (WASI stdio protocol)
# Runtime implementation is active on Linux/macOS/Windows; unsupported targets use stubs.
wasm-tools = ["dep:wasmtime", "dep:wasmtime-wasi"]
# whatsapp-web = Native WhatsApp Web client with custom rusqlite storage backend
whatsapp-web = ["dep:wa-rs", "dep:wa-rs-core", "dep:wa-rs-binary", "dep:wa-rs-proto", "dep:wa-rs-ureq-http", "dep:wa-rs-tokio-transport", "dep:serde-big-array", "dep:prost", "dep:qrcode"]

View File

@ -70,22 +70,67 @@ adb shell /data/local/tmp/zeroclaw --version
## Building from Source
To build for Android yourself:
ZeroClaw supports two Android source-build workflows.
### A) Build directly inside Termux (on-device)
Use this when compiling natively on your phone/tablet.
```bash
# Termux prerequisites
pkg update
pkg install -y clang pkg-config
# Add Android Rust targets (aarch64 target is enough for most devices)
rustup target add aarch64-linux-android armv7-linux-androideabi
# Build for your current device arch
cargo build --release --target aarch64-linux-android
```
Notes:
- `.cargo/config.toml` uses `clang` for Android targets by default.
- You do not need NDK-prefixed linkers such as `aarch64-linux-android21-clang` for native Termux builds.
- The `wasm-tools` runtime is currently unavailable on Android builds; WASM tools fall back to a stub implementation.
### B) Cross-compile from Linux/macOS with Android NDK
Use this when building Android binaries from a desktop CI/dev machine.
```bash
# Install Android NDK
# Add targets
rustup target add armv7-linux-androideabi aarch64-linux-android
# Set NDK path
# Configure Android NDK toolchain
export ANDROID_NDK_HOME=/path/to/ndk
export PATH=$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin:$PATH
export NDK_TOOLCHAIN="$ANDROID_NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin"
export PATH="$NDK_TOOLCHAIN:$PATH"
# Override Cargo defaults with NDK wrapper linkers
export CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER="$NDK_TOOLCHAIN/armv7a-linux-androideabi21-clang"
export CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER="$NDK_TOOLCHAIN/aarch64-linux-android21-clang"
# Ensure cc-rs build scripts use the same compilers
export CC_armv7_linux_androideabi="$CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER"
export CC_aarch64_linux_android="$CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER"
# Build
cargo build --release --target armv7-linux-androideabi
cargo build --release --target aarch64-linux-android
```
### Quick environment self-check
Use the built-in checker to validate linker/toolchain setup before long builds:
```bash
# From repo root
scripts/android/termux_source_build_check.sh --target aarch64-linux-android
# Run an actual cargo check after environment validation
scripts/android/termux_source_build_check.sh --target aarch64-linux-android --run-cargo-check
```
## Troubleshooting
### "Permission denied"
@ -95,9 +140,25 @@ chmod +x zeroclaw
```
### "not found" or linker errors
Make sure you downloaded the correct architecture for your device.
For native Termux builds, make sure `clang` exists and remove stale NDK overrides:
```bash
unset CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER
unset CARGO_TARGET_ARMV7_LINUX_ANDROIDEABI_LINKER
unset CC_aarch64_linux_android
unset CC_armv7_linux_androideabi
command -v clang
```
For cross-compilation, ensure `ANDROID_NDK_HOME` and `CARGO_TARGET_*_LINKER` point to valid NDK binaries.
If build scripts (for example `ring`/`aws-lc-sys`) still report `failed to find tool "aarch64-linux-android-clang"`,
also export `CC_aarch64_linux_android` / `CC_armv7_linux_androideabi` to the same NDK clang wrappers.
### "WASM tools are unavailable on Android"
This is expected today. Android builds run the WASM tool loader in stub mode; build on Linux/macOS/Windows if you need runtime `wasm-tools` execution.
### Old Android (4.x)
Use the `armv7-linux-androideabi` build with API level 16+.

View File

@ -67,6 +67,9 @@ in [section 2](#32-protocol-stdin--stdout).
| `wasmtime` CLI | Local testing (`zeroclaw skill test`) |
| Language-specific toolchain | Building `.wasm` from source |
> Note: Android/Termux builds currently run in stub mode for `wasm-tools`.
> Build on Linux/macOS/Windows for full WASM runtime support.
Install `wasmtime` CLI:
```bash

View File

@ -0,0 +1,178 @@
#!/usr/bin/env bash
set -euo pipefail
TARGET="aarch64-linux-android"
RUN_CARGO_CHECK=0
usage() {
cat <<'EOF'
Usage:
scripts/android/termux_source_build_check.sh [--target <triple>] [--run-cargo-check]
Options:
--target <triple> Android Rust target (default: aarch64-linux-android)
Supported: aarch64-linux-android, armv7-linux-androideabi
--run-cargo-check Run cargo check --locked --target <triple> --no-default-features
-h, --help Show this help
Purpose:
Validate Android source-build environment for ZeroClaw, with focus on:
- Termux native builds using plain clang
- NDK cross-build overrides (CARGO_TARGET_*_LINKER and CC_*)
- Common cc-rs linker mismatch failures
EOF
}
log() {
printf '[android-selfcheck] %s\n' "$*"
}
warn() {
printf '[android-selfcheck] warning: %s\n' "$*" >&2
}
die() {
printf '[android-selfcheck] error: %s\n' "$*" >&2
exit 1
}
while [[ $# -gt 0 ]]; do
case "$1" in
--target)
[[ $# -ge 2 ]] || die "--target requires a value"
TARGET="$2"
shift 2
;;
--run-cargo-check)
RUN_CARGO_CHECK=1
shift
;;
-h|--help)
usage
exit 0
;;
*)
die "unknown argument: $1 (use --help)"
;;
esac
done
case "$TARGET" in
aarch64-linux-android|armv7-linux-androideabi) ;;
*)
die "unsupported target '$TARGET' (expected aarch64-linux-android or armv7-linux-androideabi)"
;;
esac
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]:-$0}")" >/dev/null 2>&1 && pwd || pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/../.." >/dev/null 2>&1 && pwd || pwd)"
CONFIG_FILE="$REPO_ROOT/.cargo/config.toml"
cd "$REPO_ROOT"
TARGET_UPPER="$(printf '%s' "$TARGET" | tr '[:lower:]-' '[:upper:]_')"
TARGET_UNDERSCORE="${TARGET//-/_}"
CARGO_LINKER_VAR="CARGO_TARGET_${TARGET_UPPER}_LINKER"
CC_LINKER_VAR="CC_${TARGET_UNDERSCORE}"
is_termux=0
if [[ -n "${TERMUX_VERSION:-}" ]] || [[ "${PREFIX:-}" == *"/com.termux/files/usr"* ]]; then
is_termux=1
fi
extract_linker_from_config() {
[[ -f "$CONFIG_FILE" ]] || return 0
awk -v target="$TARGET" '
$0 ~ "^\\[target\\." target "\\]$" { in_section=1; next }
in_section && $0 ~ "^\\[" { in_section=0 }
in_section && $1 == "linker" {
gsub(/"/, "", $3);
print $3;
exit
}
' "$CONFIG_FILE"
}
command_exists() {
command -v "$1" >/dev/null 2>&1
}
is_executable_tool() {
local tool="$1"
if [[ "$tool" == */* ]]; then
[[ -x "$tool" ]]
else
command_exists "$tool"
fi
}
log "repo: $REPO_ROOT"
log "target: $TARGET"
if [[ "$is_termux" -eq 1 ]]; then
log "environment: Termux/native"
else
log "environment: non-Termux (likely desktop/CI)"
fi
command_exists rustup || die "rustup is not installed"
command_exists cargo || die "cargo is not installed"
if ! rustup target list --installed | grep -Fx "$TARGET" >/dev/null 2>&1; then
die "Rust target '$TARGET' is not installed. Run: rustup target add $TARGET"
fi
config_linker="$(extract_linker_from_config || true)"
cargo_linker_override="${!CARGO_LINKER_VAR:-}"
cc_linker_override="${!CC_LINKER_VAR:-}"
if [[ -n "$config_linker" ]]; then
log "config linker ($TARGET): $config_linker"
else
warn "no linker configured for $TARGET in .cargo/config.toml"
fi
if [[ -n "$cargo_linker_override" ]]; then
log "env override $CARGO_LINKER_VAR=$cargo_linker_override"
fi
if [[ -n "$cc_linker_override" ]]; then
log "env override $CC_LINKER_VAR=$cc_linker_override"
fi
effective_linker="${cargo_linker_override:-${config_linker:-clang}}"
log "effective linker: $effective_linker"
if [[ "$is_termux" -eq 1 ]]; then
command_exists clang || die "clang is required in Termux. Run: pkg install -y clang pkg-config"
if [[ "${config_linker:-}" != "clang" ]]; then
warn "Termux native build should use linker = \"clang\" for $TARGET"
fi
if [[ -n "$cargo_linker_override" && "$cargo_linker_override" != "clang" ]]; then
warn "Termux native build usually should unset $CARGO_LINKER_VAR (currently '$cargo_linker_override')"
fi
if [[ -n "$cc_linker_override" && "$cc_linker_override" != "clang" ]]; then
warn "Termux native build usually should unset $CC_LINKER_VAR (currently '$cc_linker_override')"
fi
else
if [[ -n "$cargo_linker_override" && -z "$cc_linker_override" ]]; then
warn "cross-build may still fail in cc-rs crates; consider setting $CC_LINKER_VAR=$cargo_linker_override"
fi
fi
if ! is_executable_tool "$effective_linker"; then
if [[ "$is_termux" -eq 1 ]]; then
die "effective linker '$effective_linker' is not executable in PATH"
fi
warn "effective linker '$effective_linker' not found (expected for some desktop hosts without NDK toolchain)"
fi
if [[ "$RUN_CARGO_CHECK" -eq 1 ]]; then
log "running cargo check --locked --target $TARGET --no-default-features"
CARGO_TARGET_DIR="${CARGO_TARGET_DIR:-/tmp/zeroclaw-android-selfcheck-target}" \
cargo check --locked --target "$TARGET" --no-default-features
log "cargo check completed successfully"
else
log "skip cargo check (use --run-cargo-check to enable)"
fi
log "self-check completed"

View File

@ -1,8 +1,10 @@
//! WASM plugin tool — executes a `.wasm` binary as a ZeroClaw tool.
//!
//! # Feature gate
//! Only compiled when `--features wasm-tools` is active.
//! Without the feature, [`WasmTool`] stubs return a clear error.
//! Compiled when `--features wasm-tools` is active on supported targets
//! (Linux, macOS, Windows).
//! Unsupported targets (including Android/Termux) always use the stub implementation.
//! Without runtime support, [`WasmTool`] stubs return a clear error.
//!
//! # Protocol (WASI stdio)
//!
@ -32,7 +34,7 @@
//! - Output capped at 1 MiB (enforced by [`MemoryOutputPipe`] capacity).
use super::traits::{Tool, ToolResult};
use anyhow::{bail, Context};
use anyhow::Context;
use async_trait::async_trait;
use serde_json::Value;
use std::path::Path;
@ -45,12 +47,15 @@ const WASM_TIMEOUT_SECS: u64 = 30;
// ─── Feature-gated implementation ─────────────────────────────────────────────
#[cfg(feature = "wasm-tools")]
#[cfg(all(
feature = "wasm-tools",
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
mod inner {
use super::{
async_trait, bail, Context, Path, Tool, ToolResult, Value, MAX_OUTPUT_BYTES,
WASM_TIMEOUT_SECS,
async_trait, Context, Path, Tool, ToolResult, Value, MAX_OUTPUT_BYTES, WASM_TIMEOUT_SECS,
};
use anyhow::bail;
use wasmtime::{Config as WtConfig, Engine, Linker, Module, Store};
use wasmtime_wasi::{
pipe::{MemoryInputPipe, MemoryOutputPipe},
@ -221,10 +226,31 @@ mod inner {
// ─── Feature-absent stub ──────────────────────────────────────────────────────
#[cfg(not(feature = "wasm-tools"))]
#[cfg(any(
not(feature = "wasm-tools"),
not(any(target_os = "linux", target_os = "macos", target_os = "windows"))
))]
mod inner {
use super::*;
pub(super) fn unavailable_message(
feature_enabled: bool,
target_is_android: bool,
) -> &'static str {
if feature_enabled {
if target_is_android {
"WASM tools are currently unavailable on Android/Termux builds. \
Build on Linux/macOS/Windows to enable wasm-tools."
} else {
"WASM tools are currently unavailable on this target. \
Build on Linux/macOS/Windows to enable wasm-tools."
}
} else {
"WASM tools are not enabled in this build. \
Recompile with '--features wasm-tools'."
}
}
/// Stub: returned when the `wasm-tools` feature is not compiled in.
/// Construction succeeds so callers can enumerate plugins; execution returns a clear error.
pub struct WasmTool {
@ -261,14 +287,13 @@ mod inner {
}
async fn execute(&self, _args: Value) -> anyhow::Result<ToolResult> {
let message =
unavailable_message(cfg!(feature = "wasm-tools"), cfg!(target_os = "android"));
Ok(ToolResult {
success: false,
output: String::new(),
error: Some(
"WASM tools are not enabled in this build. \
Recompile with '--features wasm-tools'."
.into(),
),
error: Some(message.into()),
})
}
}
@ -495,7 +520,26 @@ mod tests {
assert!(tools.is_empty());
}
#[cfg(not(feature = "wasm-tools"))]
#[cfg(any(
not(feature = "wasm-tools"),
not(any(target_os = "linux", target_os = "macos", target_os = "windows"))
))]
#[test]
fn stub_unavailable_message_matrix_is_stable() {
let feature_off = inner::unavailable_message(false, false);
assert!(feature_off.contains("Recompile with '--features wasm-tools'"));
let android = inner::unavailable_message(true, true);
assert!(android.contains("Android/Termux"));
let unsupported_target = inner::unavailable_message(true, false);
assert!(unsupported_target.contains("currently unavailable on this target"));
}
#[cfg(any(
not(feature = "wasm-tools"),
not(any(target_os = "linux", target_os = "macos", target_os = "windows"))
))]
#[tokio::test]
async fn stub_reports_feature_disabled() {
let t = WasmTool::load(
@ -507,7 +551,9 @@ mod tests {
.unwrap();
let r = t.execute(serde_json::json!({})).await.unwrap();
assert!(!r.success);
assert!(r.error.unwrap().contains("wasm-tools"));
let expected =
inner::unavailable_message(cfg!(feature = "wasm-tools"), cfg!(target_os = "android"));
assert_eq!(r.error.as_deref(), Some(expected));
}
// ── WasmManifest error paths ──────────────────────────────────────────────
@ -630,7 +676,10 @@ mod tests {
// ── Feature-gated: invalid WASM binary fails at compile time ─────────────
#[cfg(feature = "wasm-tools")]
#[cfg(all(
feature = "wasm-tools",
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
#[test]
#[ignore = "slow: initializes wasmtime Cranelift compiler; run with --include-ignored"]
fn wasm_tool_load_rejects_invalid_binary() {
@ -651,7 +700,10 @@ mod tests {
);
}
#[cfg(feature = "wasm-tools")]
#[cfg(all(
feature = "wasm-tools",
any(target_os = "linux", target_os = "macos", target_os = "windows")
))]
#[test]
#[ignore = "slow: initializes wasmtime Cranelift compiler; run with --include-ignored"]
fn wasm_tool_load_rejects_missing_file() {