fix(android): harden Termux source-build and wasm-tools fallback
This commit is contained in:
parent
f0a5bbdb1b
commit
bebb881b5b
@ -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"
|
||||
|
||||
@ -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"]
|
||||
|
||||
@ -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+.
|
||||
|
||||
@ -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
|
||||
|
||||
178
scripts/android/termux_source_build_check.sh
Executable file
178
scripts/android/termux_source_build_check.sh
Executable 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"
|
||||
@ -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() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user