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
env:
- CGO_ENABLED=0
ldflags:
- -s -w
goos:
- linux
- windows
@ -28,6 +30,19 @@ builds:
- goos: windows
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:
- id: picoclaw
dockerfile: Dockerfile.goreleaser
@ -45,7 +60,10 @@ dockers_v2:
- linux/riscv64
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`.
name_template: >-
{{ .ProjectName }}_

View file

@ -9,8 +9,10 @@ BLUE='\033[0;34m'
YELLOW='\033[1;33m'
NC='\033[0m' # No Color
GITHUB_REPO="sipeed/picoclaw"
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"
show_header() {
@ -29,9 +31,15 @@ EOF
echo -e "==========================================${NC}"
}
is_android() {
[[ "$(uname -o 2>/dev/null)" == "Android" ]] || [ -n "$ANDROID_ROOT" ] || [ -d "/data/data/com.termux" ]
}
check_dependencies() {
echo -e "${YELLOW}[*] Checking dependencies...${NC}"
deps=("golang" "git" "make" "jq" "tmux")
deps=("curl" "jq" "tmux" "tar")
to_install=()
for dep in "${deps[@]}"; do
if ! command -v "$dep" &> /dev/null; then
@ -47,34 +55,100 @@ check_dependencies() {
fi
}
install_picoclaw() {
check_dependencies
echo -e "${YELLOW}[*] Cloning repository...${NC}"
if [ -d "$REPO_DIR" ]; then
cd "$REPO_DIR" && git pull
else
git clone https://github.com/sipeed/picoclaw.git "$REPO_DIR"
cd "$REPO_DIR"
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
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
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}[*] Installing to bin...${NC}"
cp build/picoclaw-linux-arm64 "$BIN_PATH"
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() {
check_dependencies
echo -e "${YELLOW}[*] Detecting architecture...${NC}"
local arch
arch=$(detect_arch)
if [ $? -ne 0 ] || [ -z "$arch" ]; then
echo -e "${RED}[!] Could not detect architecture. Aborting.${NC}"
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
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
echo -e "${GREEN}[✓] PicoClaw installed successfully!${NC}"
echo -e "${GREEN}[✓] PicoClaw ${version} installed successfully!${NC}"
echo -e "${YELLOW}[*] Running initial setup...${NC}"
picoclaw onboard
fi
read -p "Press Enter to return to menu..."
@ -121,7 +195,8 @@ manage_service() {
if tmux has-session -t picoclaw 2>/dev/null; then
echo -e "${YELLOW}[!] Session 'picoclaw' already exists.${NC}"
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}"
fi
;;
@ -134,11 +209,16 @@ manage_service() {
pkill -9 picoclaw
tmux kill-session -t picoclaw 2>/dev/null
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}"
;;
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 ;;
esac
@ -150,12 +230,12 @@ uninstall_picoclaw() {
if [[ "$confirm" == "y" || "$confirm" == "Y" ]]; then
pkill -9 picoclaw
tmux kill-session -t picoclaw 2>/dev/null
rm "$BIN_PATH"
rm -rf "$REPO_DIR"
rm -rf "$HOME/.picoclaw"
rm -f "$BIN_PATH"
rm -rf "$DATA_DIR"
echo -e "${GREEN}[✓] PicoClaw uninstalled.${NC}"
fi
read -p "Press Enter to return to menu..."
}
network_diagnostics() {
echo -e "${BLUE}--- Network Diagnostics ---${NC}"

View file

@ -120,6 +120,17 @@ func copyDirectory(src, dst string) error {
}
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 {
printHelp()
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:
```bash
pkg update && pkg upgrade
pkg install -y golang git make jq tmux
pkg install -y curl jq tmux
```
## 2. Clone and Prepare PicoClaw
```bash
git clone https://github.com/sipeed/picoclaw.git ~/.picoclaw-repo
cd ~/.picoclaw-repo
```
## 2. Download Pre-compiled Binary
PicoClaw provides pre-compiled Android binaries for every release — no build tools needed.
### 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
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
Build the project with CGO disabled for maximum compatibility across different Android architectures, then move it to your system PATH:
```bash
export CGO_ENABLED=0
make deps
make build
# Download the Android arm64 binary (built with GOOS=android for Termux compatibility)
curl -fSL "https://github.com/sipeed/picoclaw/releases/download/${VERSION}/picoclaw_Android_arm64.tar.gz" -o /tmp/picoclaw.tar.gz
tar -xzf /tmp/picoclaw.tar.gz -C /tmp picoclaw
# Install to Termux bin directory
cp build/picoclaw-linux-arm64 $PREFIX/bin/picoclaw
cp /tmp/picoclaw $PREFIX/bin/picoclaw
chmod +x $PREFIX/bin/picoclaw
rm /tmp/picoclaw.tar.gz /tmp/picoclaw
```
Now you can run `picoclaw` from anywhere!

View file

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