diff --git a/.cargo/config.toml b/.cargo/config.toml index 67d105683..272541e47 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -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" diff --git a/Cargo.toml b/Cargo.toml index a669fd4f0..551ab2a5d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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"] diff --git a/docs/android-setup.md b/docs/android-setup.md index 34a3cb448..367446726 100644 --- a/docs/android-setup.md +++ b/docs/android-setup.md @@ -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+. diff --git a/docs/wasm-tools-guide.md b/docs/wasm-tools-guide.md index b865f4cb5..7960d4040 100644 --- a/docs/wasm-tools-guide.md +++ b/docs/wasm-tools-guide.md @@ -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 diff --git a/scripts/android/termux_source_build_check.sh b/scripts/android/termux_source_build_check.sh new file mode 100755 index 000000000..609d69298 --- /dev/null +++ b/scripts/android/termux_source_build_check.sh @@ -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 ] [--run-cargo-check] + +Options: + --target Android Rust target (default: aarch64-linux-android) + Supported: aarch64-linux-android, armv7-linux-androideabi + --run-cargo-check Run cargo check --locked --target --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" diff --git a/src/tools/wasm_tool.rs b/src/tools/wasm_tool.rs index 3a7a18bcc..f03f664f0 100644 --- a/src/tools/wasm_tool.rs +++ b/src/tools/wasm_tool.rs @@ -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 { + 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() {