fix(termux): Android binary support + argv fix

- Add Android/arm64 build target to .goreleaser.yaml
  Produces picoclaw_Android_arm64.tar.gz (PIE/ET_DYN binary built
  with GOOS=android, CGO_ENABLED=0, -ldflags '-s -w')

- Fix Bionic linker argv[0] duplication in cmd/picoclaw/main.go
  Android's dynamic linker inserts the full binary path as an extra
  argv[1], shifting real arguments. Detect and strip the duplicate
  when runtime.GOOS == "android".

- Simplify picoclaw-manager.sh to download-only
  Downloads the pre-compiled Android binary from GitHub Releases
  instead of building from source. No golang/git deps needed.
  Fix missing closing brace in uninstall_picoclaw().

- Update docs/TERMUX_INSTALL.md for Android binary download
- Add -ldflags '-s -w' to all release builds (26MB -> 19MB)
This commit is contained in:
mrbeandev 2026-02-17 16:36:19 +05:30
parent 0ede534b45
commit 3d274ce16d
5 changed files with 164 additions and 61 deletions

View file

@ -11,6 +11,8 @@ builds:
- id: picoclaw - id: picoclaw
env: env:
- CGO_ENABLED=0 - CGO_ENABLED=0
ldflags:
- -s -w
goos: goos:
- linux - linux
- windows - windows
@ -28,6 +30,19 @@ builds:
- goos: windows - goos: windows
goarch: arm goarch: arm
# Android/Termux requires PIE executables (ET_DYN) built with GOOS=android.
# Standard Linux binaries (ET_EXEC) are rejected by Android's kernel.
- id: picoclaw-android
env:
- CGO_ENABLED=0
ldflags:
- -s -w
goos:
- android
goarch:
- arm64
main: ./cmd/picoclaw
dockers_v2: dockers_v2:
- id: picoclaw - id: picoclaw
dockerfile: Dockerfile.goreleaser dockerfile: Dockerfile.goreleaser
@ -45,7 +60,10 @@ dockers_v2:
- linux/riscv64 - linux/riscv64
archives: archives:
- formats: [tar.gz] - ids:
- picoclaw
- picoclaw-android
formats: [tar.gz]
# this name template makes the OS and Arch compatible with the results of `uname`. # this name template makes the OS and Arch compatible with the results of `uname`.
name_template: >- name_template: >-
{{ .ProjectName }}_ {{ .ProjectName }}_

View file

@ -9,8 +9,10 @@ BLUE='\033[0;34m'
YELLOW='\033[1;33m' YELLOW='\033[1;33m'
NC='\033[0m' # No Color NC='\033[0m' # No Color
GITHUB_REPO="sipeed/picoclaw"
CONFIG_FILE="$HOME/.picoclaw/config.json" CONFIG_FILE="$HOME/.picoclaw/config.json"
REPO_DIR="$HOME/.picoclaw-repo" DATA_DIR="$HOME/.picoclaw"
LOG_FILE="$DATA_DIR/picoclaw.log"
BIN_PATH="$PREFIX/bin/picoclaw" BIN_PATH="$PREFIX/bin/picoclaw"
show_header() { show_header() {
@ -29,9 +31,15 @@ EOF
echo -e "==========================================${NC}" echo -e "==========================================${NC}"
} }
is_android() {
[[ "$(uname -o 2>/dev/null)" == "Android" ]] || [ -n "$ANDROID_ROOT" ] || [ -d "/data/data/com.termux" ]
}
check_dependencies() { check_dependencies() {
echo -e "${YELLOW}[*] Checking dependencies...${NC}" echo -e "${YELLOW}[*] Checking dependencies...${NC}"
deps=("golang" "git" "make" "jq" "tmux")
deps=("curl" "jq" "tmux" "tar")
to_install=() to_install=()
for dep in "${deps[@]}"; do for dep in "${deps[@]}"; do
if ! command -v "$dep" &> /dev/null; then if ! command -v "$dep" &> /dev/null; then
@ -47,34 +55,100 @@ check_dependencies() {
fi fi
} }
detect_arch() {
local arch
arch=$(uname -m)
case "$arch" in
aarch64|arm64) echo "arm64" ;;
armv7l|armv6l) echo "armv6" ;;
x86_64|amd64) echo "x86_64" ;;
*)
echo -e "${RED}[!] Unsupported architecture: $arch${NC}" >&2
return 1
;;
esac
}
get_latest_version() {
curl -s "https://api.github.com/repos/${GITHUB_REPO}/releases/latest" | jq -r '.tag_name'
}
# Download prebuilt binary from GitHub Releases
install_from_release() {
local version="$1"
local arch="$2"
# Android/Termux uses a dedicated Android binary (GOOS=android, PIE).
# Standard Linux uses the regular Linux binary.
local os_name="Linux"
if is_android; then
os_name="Android"
fi
local filename="picoclaw_${os_name}_${arch}.tar.gz"
local url="https://github.com/${GITHUB_REPO}/releases/download/${version}/${filename}"
local tmpdir
tmpdir=$(mktemp -d -p "${TMPDIR:-/tmp}")
echo -e "${YELLOW}[*] Downloading ${filename}...${NC}"
if ! curl -fSL --progress-bar "$url" -o "${tmpdir}/${filename}"; then
echo -e "${RED}[!] Download failed. URL: ${url}${NC}"
rm -rf "$tmpdir"
return 1
fi
echo -e "${YELLOW}[*] Extracting binary...${NC}"
tar -xzf "${tmpdir}/${filename}" -C "$tmpdir"
if [ ! -f "${tmpdir}/picoclaw" ]; then
echo -e "${RED}[!] Binary not found in archive.${NC}"
rm -rf "$tmpdir"
return 1
fi
mkdir -p "$(dirname "$BIN_PATH")"
cp "${tmpdir}/picoclaw" "$BIN_PATH"
chmod +x "$BIN_PATH"
rm -rf "$tmpdir"
return 0
}
install_picoclaw() { install_picoclaw() {
check_dependencies check_dependencies
echo -e "${YELLOW}[*] Cloning repository...${NC}" echo -e "${YELLOW}[*] Detecting architecture...${NC}"
if [ -d "$REPO_DIR" ]; then local arch
cd "$REPO_DIR" && git pull arch=$(detect_arch)
else if [ $? -ne 0 ] || [ -z "$arch" ]; then
git clone https://github.com/sipeed/picoclaw.git "$REPO_DIR" echo -e "${RED}[!] Could not detect architecture. Aborting.${NC}"
cd "$REPO_DIR" read -p "Press Enter to return to menu..."
return
fi
echo -e "${GREEN}[✓] Architecture: $arch${NC}"
echo -e "${YELLOW}[*] Fetching latest release...${NC}"
local version
version=$(get_latest_version)
if [ -z "$version" ] || [ "$version" = "null" ]; then
echo -e "${RED}[!] Could not fetch latest version. Check your internet connection.${NC}"
read -p "Press Enter to return to menu..."
return
fi
echo -e "${GREEN}[✓] Latest version: $version${NC}"
install_from_release "$version" "$arch"
if [ $? -ne 0 ]; then
echo -e "${RED}[!] Installation failed.${NC}"
read -p "Press Enter to return to menu..."
return
fi fi
echo -e "${YELLOW}[*] Building PicoClaw...${NC}"
# Adjust Go version
GO_VERSION=$(go version | awk '{print $3}' | sed 's/go//' | cut -d. -f1,2,3)
sed -i "s/go 1.25.7/go $GO_VERSION/" go.mod
export CGO_ENABLED=0
make deps
make build
echo -e "${YELLOW}[*] Installing to bin...${NC}"
cp build/picoclaw-linux-arm64 "$BIN_PATH"
chmod +x "$BIN_PATH"
if ! command -v picoclaw &> /dev/null; then if ! command -v picoclaw &> /dev/null; then
echo -e "${RED}[!] Installation failed. Bin not in PATH?${NC}" echo -e "${RED}[!] Installation failed. Is $PREFIX/bin in your PATH?${NC}"
else else
echo -e "${GREEN}[✓] PicoClaw installed successfully!${NC}" echo -e "${GREEN}[✓] PicoClaw ${version} installed successfully!${NC}"
echo -e "${YELLOW}[*] Running initial setup...${NC}"
picoclaw onboard picoclaw onboard
fi fi
read -p "Press Enter to return to menu..." read -p "Press Enter to return to menu..."
@ -121,7 +195,8 @@ manage_service() {
if tmux has-session -t picoclaw 2>/dev/null; then if tmux has-session -t picoclaw 2>/dev/null; then
echo -e "${YELLOW}[!] Session 'picoclaw' already exists.${NC}" echo -e "${YELLOW}[!] Session 'picoclaw' already exists.${NC}"
else else
tmux new-session -d -s picoclaw "picoclaw gateway | tee $REPO_DIR/picoclaw.log" mkdir -p "$DATA_DIR"
tmux new-session -d -s picoclaw "picoclaw gateway 2>&1 | tee $LOG_FILE"
echo -e "${GREEN}[✓] Gateway started in tmux session 'picoclaw'.${NC}" echo -e "${GREEN}[✓] Gateway started in tmux session 'picoclaw'.${NC}"
fi fi
;; ;;
@ -134,11 +209,16 @@ manage_service() {
pkill -9 picoclaw pkill -9 picoclaw
tmux kill-session -t picoclaw 2>/dev/null tmux kill-session -t picoclaw 2>/dev/null
sleep 1 sleep 1
tmux new-session -d -s picoclaw "picoclaw gateway | tee $REPO_DIR/picoclaw.log" mkdir -p "$DATA_DIR"
tmux new-session -d -s picoclaw "picoclaw gateway 2>&1 | tee $LOG_FILE"
echo -e "${GREEN}[✓] Gateway restarted.${NC}" echo -e "${GREEN}[✓] Gateway restarted.${NC}"
;; ;;
4) 4)
tail -f "$REPO_DIR/picoclaw.log" if [ -f "$LOG_FILE" ]; then
tail -f "$LOG_FILE"
else
echo -e "${YELLOW}[!] No log file found. Start the gateway first.${NC}"
fi
;; ;;
*) return ;; *) return ;;
esac esac
@ -150,12 +230,12 @@ uninstall_picoclaw() {
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
pkill -9 picoclaw pkill -9 picoclaw
tmux kill-session -t picoclaw 2>/dev/null tmux kill-session -t picoclaw 2>/dev/null
rm "$BIN_PATH" rm -f "$BIN_PATH"
rm -rf "$REPO_DIR" rm -rf "$DATA_DIR"
rm -rf "$HOME/.picoclaw"
echo -e "${GREEN}[✓] PicoClaw uninstalled.${NC}" echo -e "${GREEN}[✓] PicoClaw uninstalled.${NC}"
fi fi
read -p "Press Enter to return to menu..." read -p "Press Enter to return to menu..."
}
network_diagnostics() { network_diagnostics() {
echo -e "${BLUE}--- Network Diagnostics ---${NC}" echo -e "${BLUE}--- Network Diagnostics ---${NC}"

View file

@ -120,6 +120,17 @@ func copyDirectory(src, dst string) error {
} }
func main() { func main() {
// Android's Bionic linker duplicates argv[0] when loading PIE executables,
// inserting the full binary path as argv[1] and shifting real arguments.
// For example: argv = ["picoclaw", "/data/.../bin/picoclaw", "onboard"]
// Detect this by checking if argv[1] is a path whose base name matches
// argv[0] (the program name).
if len(os.Args) >= 2 && runtime.GOOS == "android" {
if filepath.Base(os.Args[1]) == filepath.Base(os.Args[0]) && strings.Contains(os.Args[1], "/") {
os.Args = append(os.Args[:1], os.Args[2:]...)
}
}
if len(os.Args) < 2 { if len(os.Args) < 2 {
printHelp() printHelp()
os.Exit(1) os.Exit(1)

View file

@ -26,31 +26,25 @@ If you prefer to set up everything manually, follow these steps:
Open Termux and install the necessary packages: Open Termux and install the necessary packages:
```bash ```bash
pkg update && pkg upgrade pkg update && pkg upgrade
pkg install -y golang git make jq tmux pkg install -y curl jq tmux
``` ```
## 2. Clone and Prepare PicoClaw ## 2. Download Pre-compiled Binary
```bash PicoClaw provides pre-compiled Android binaries for every release — no build tools needed.
git clone https://github.com/sipeed/picoclaw.git ~/.picoclaw-repo
cd ~/.picoclaw-repo
```
### Fix Go Version Requirement
Termux might have a slightly older version of Go than required by `go.mod`. Use this command to automatically adjust the requirement to match your installed Go version:
```bash ```bash
sed -i "s/go 1.25.7/go $(go version | awk '{print $3}' | sed 's/go//' | cut -d. -f1,2,3)/" go.mod # Fetch latest version tag
``` VERSION=$(curl -s https://api.github.com/repos/sipeed/picoclaw/releases/latest | jq -r '.tag_name')
echo "Installing PicoClaw $VERSION..."
### Build and Install PicoClaw # Download the Android arm64 binary (built with GOOS=android for Termux compatibility)
Build the project with CGO disabled for maximum compatibility across different Android architectures, then move it to your system PATH: curl -fSL "https://github.com/sipeed/picoclaw/releases/download/${VERSION}/picoclaw_Android_arm64.tar.gz" -o /tmp/picoclaw.tar.gz
```bash tar -xzf /tmp/picoclaw.tar.gz -C /tmp picoclaw
export CGO_ENABLED=0
make deps
make build
# Install to Termux bin directory # Install to Termux bin directory
cp build/picoclaw-linux-arm64 $PREFIX/bin/picoclaw cp /tmp/picoclaw $PREFIX/bin/picoclaw
chmod +x $PREFIX/bin/picoclaw chmod +x $PREFIX/bin/picoclaw
rm /tmp/picoclaw.tar.gz /tmp/picoclaw
``` ```
Now you can run `picoclaw` from anywhere! Now you can run `picoclaw` from anywhere!

View file

@ -51,7 +51,7 @@ func TestRecordLastChannel(t *testing.T) {
// Create agent loop // Create agent loop
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &mockProvider{} provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
// Test RecordLastChannel // Test RecordLastChannel
testChannel := "test-channel" testChannel := "test-channel"
@ -67,7 +67,7 @@ func TestRecordLastChannel(t *testing.T) {
} }
// Verify persistence by creating a new agent loop // Verify persistence by creating a new agent loop
al2 := NewAgentLoop(cfg, msgBus, provider) al2 := NewAgentLoop(cfg, msgBus, provider, "")
if al2.state.GetLastChannel() != testChannel { if al2.state.GetLastChannel() != testChannel {
t.Errorf("Expected persistent channel '%s', got '%s'", testChannel, al2.state.GetLastChannel()) t.Errorf("Expected persistent channel '%s', got '%s'", testChannel, al2.state.GetLastChannel())
} }
@ -96,7 +96,7 @@ func TestRecordLastChatID(t *testing.T) {
// Create agent loop // Create agent loop
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &mockProvider{} provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
// Test RecordLastChatID // Test RecordLastChatID
testChatID := "test-chat-id-123" testChatID := "test-chat-id-123"
@ -112,7 +112,7 @@ func TestRecordLastChatID(t *testing.T) {
} }
// Verify persistence by creating a new agent loop // Verify persistence by creating a new agent loop
al2 := NewAgentLoop(cfg, msgBus, provider) al2 := NewAgentLoop(cfg, msgBus, provider, "")
if al2.state.GetLastChatID() != testChatID { if al2.state.GetLastChatID() != testChatID {
t.Errorf("Expected persistent chat ID '%s', got '%s'", testChatID, al2.state.GetLastChatID()) t.Errorf("Expected persistent chat ID '%s', got '%s'", testChatID, al2.state.GetLastChatID())
} }
@ -141,7 +141,7 @@ func TestNewAgentLoop_StateInitialized(t *testing.T) {
// Create agent loop // Create agent loop
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &mockProvider{} provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
// Verify state manager is initialized // Verify state manager is initialized
if al.state == nil { if al.state == nil {
@ -176,7 +176,7 @@ func TestToolRegistry_ToolRegistration(t *testing.T) {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &mockProvider{} provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
// Register a custom tool // Register a custom tool
customTool := &mockCustomTool{} customTool := &mockCustomTool{}
@ -222,7 +222,7 @@ func TestToolContext_Updates(t *testing.T) {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &simpleMockProvider{response: "OK"} provider := &simpleMockProvider{response: "OK"}
_ = NewAgentLoop(cfg, msgBus, provider) _ = NewAgentLoop(cfg, msgBus, provider, "")
// Verify that ContextualTool interface is defined and can be implemented // Verify that ContextualTool interface is defined and can be implemented
// This test validates the interface contract exists // This test validates the interface contract exists
@ -253,7 +253,7 @@ func TestToolRegistry_GetDefinitions(t *testing.T) {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &mockProvider{} provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
// Register a test tool and verify it shows up in startup info // Register a test tool and verify it shows up in startup info
testTool := &mockCustomTool{} testTool := &mockCustomTool{}
@ -297,7 +297,7 @@ func TestAgentLoop_GetStartupInfo(t *testing.T) {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &mockProvider{} provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
info := al.GetStartupInfo() info := al.GetStartupInfo()
@ -344,7 +344,7 @@ func TestAgentLoop_Stop(t *testing.T) {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &mockProvider{} provider := &mockProvider{}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
// Note: running is only set to true when Run() is called // Note: running is only set to true when Run() is called
// We can't test that without starting the event loop // We can't test that without starting the event loop
@ -466,7 +466,7 @@ func TestToolResult_SilentToolDoesNotSendUserMessage(t *testing.T) {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &simpleMockProvider{response: "File operation complete"} provider := &simpleMockProvider{response: "File operation complete"}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
helper := testHelper{al: al} helper := testHelper{al: al}
// ReadFileTool returns SilentResult, which should not send user message // ReadFileTool returns SilentResult, which should not send user message
@ -508,7 +508,7 @@ func TestToolResult_UserFacingToolDoesSendMessage(t *testing.T) {
msgBus := bus.NewMessageBus() msgBus := bus.NewMessageBus()
provider := &simpleMockProvider{response: "Command output: hello world"} provider := &simpleMockProvider{response: "Command output: hello world"}
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
helper := testHelper{al: al} helper := testHelper{al: al}
// ExecTool returns UserResult, which should send user message // ExecTool returns UserResult, which should send user message
@ -581,7 +581,7 @@ func TestAgentLoop_ContextExhaustionRetry(t *testing.T) {
successResp: "Recovered from context error", successResp: "Recovered from context error",
} }
al := NewAgentLoop(cfg, msgBus, provider) al := NewAgentLoop(cfg, msgBus, provider, "")
// Inject some history to simulate a full context // Inject some history to simulate a full context
sessionKey := "test-session-context" sessionKey := "test-session-context"