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:
parent
0ede534b45
commit
3d274ce16d
5 changed files with 164 additions and 61 deletions
|
|
@ -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 }}_
|
||||
|
|
|
|||
|
|
@ -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}"
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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!
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue