Install
openclaw skills install @jagones84/android-arm64-devkit-skillCreate, build and deploy Android APKs on ARM64 Linux (DGX Spark, Raspberry Pi, ARM cloud): native aarch64 aapt2 for Gradle, plus adb install-and-verify over USB or VPN. Use for aarch64 APK work.
openclaw skills install @jagones84/android-arm64-devkit-skillThin wrapper over the scripts in this repo. One skill, two branches: pick your
host and your transport. Every machine-specific value lives in
config.sh (from config.example.sh) — never in this file.
| Host | What changes |
|---|---|
| ARM64 Linux (DGX Spark, Raspberry Pi, ARM server/cloud) | Google's aapt2 / aidl / zipalign are x86-64 only, so Gradle dies with Exec format error → MUST override with the Debian aarch64 build (see "Build branch") |
| x86-64 Linux | Google's toolchain runs natively → skip the aapt2 override; the adb scripts still apply |
| Windows / macOS | These bash scripts do not run there → use the Android Studio workflow instead (see "Cross-references") |
| Situation | Transport |
|---|---|
| Device attached to the machine running the scripts | USB — leave ADB_HOST empty, set DEVICE_SERIAL |
| Device on a VPN (e.g. Tailscale) | TCP — set ADB_HOST to the MagicDNS name; the port is discovered |
| Device attached to another host you can ssh into | remote adb — set ADB_REMOTE_HOST + ADB_REMOTE_BIN |
cp {baseDir}/config.example.sh {baseDir}/config.sh
$EDITOR {baseDir}/config.sh
config.sh is gitignored and is never committed. Host names, IPs, serials,
package ids and personal paths go here — not in this skill, not in any tracked
file. Gradle-side values (sdk.dir, android.aapt2FromMavenOverride) belong to
the project's local.properties / gradle.properties.
{baseDir}/scripts/new-project.sh ~/apps/MyApp com.example.myapp "My App"
cd ~/apps/MyApp && ./gradlew clean assembleDebug
Creates a minimal Gradle + Kotlin project (one Activity, adaptive icon,
minSdk 26) that already carries the aarch64 aapt2 override. It refuses to
overwrite an existing path and validates the package id.
sudo apt-get install -y android-sdk-build-tools android-sdk-platform-tools android-libaapt
{baseDir}/scripts/setup-aapt2-arm64.sh
{baseDir}/scripts/setup-aapt2-arm64.sh --write-gradle-property path/to/project
cd path/to/project && ./gradlew clean assembleDebug
Prepare the transport once — these values are personal, so they go in config.sh
(never committed):
cp {baseDir}/config.example.sh {baseDir}/config.sh
# USB: ADB_HOST="" DEVICE_SERIAL=<serial from `adb devices -l`>
# VPN (TCP): ADB_HOST=<magicdns> ADB_TCP_PORT="" (the port is discovered)
# remote adb: ADB_REMOTE_HOST=<ssh alias> ADB_REMOTE_BIN=<adb path on that host>
Then connect, install, verify, tear down:
{baseDir}/scripts/adb-connect.sh # discovers the port; writes .adb-device
{baseDir}/scripts/adb-install-verify.sh app/build/outputs/apk/debug/app-debug.apk
{baseDir}/scripts/adb-disconnect.sh # add --restart-server to unwedge adb
adb-connect.sh never guesses the port: it reads it from an existing wireless
transport or over USB, and fails loudly if wireless debugging is not exposed.
adb-install-verify.sh prints the install result, the installed version/time,
launches the activity, checks the focused window and the process pid, greps
logcat for crashes, and captures a screenshot (warns below ~30 KB).
Distro/toolchain facts, not host-specific values — values still live in config.sh
or the project.
adb is x86-64 too. platform-tools/adb from the SDK will not run
on aarch64 either; use the distro's adb (the setup step installs
android-sdk-platform-tools, which provides one). Confirm with
file "$(command -v adb)".~/.bashrc starts with
case $- in *i*) ;; *) return;; esac; anything after it — including Android env
exports — is skipped by ssh host 'cmd'. Put the ANDROID_HOME / JAVA_HOME /
GRADLE_HOME exports before that guard, then verify with
ssh host 'echo "$ANDROID_HOME"'.aapt2 is older than AGP's. It parses android.jar for older
platforms only; if a very new compileSdk fails with a resource-table error,
lower compileSdk / targetSdk to a supported platform and re-test.{baseDir} path; never rewrite their
logic inline../gradlew clean assembleDebug and require
N actionable tasks: M executed with M > 0. A build with every task
UP-TO-DATE means aapt2 never ran — it proves nothing.config.sh from config.example.sh before the adb scripts, and
MUST NOT commit it or hardcode its values.5555). OEMs randomize it on
every wireless-debugging restart; let adb-connect.sh discover it.adb-install-verify.sh output as the acceptance test: install +
launch + focus + pid + logcat + screenshot. A screenshot < 30 KB is a cached
black framebuffer (lockscreen), not success.BUILD SUCCESSFUL or install Success alone.cmd
(cmd /c "adb ... > shot.png"); PowerShell's > writes UTF-16 and corrupts
binaries.ADB_REMOTE_HOST / ADB_REMOTE_BIN in config.sh;
do not reimplement the ssh quoting — {baseDir}/scripts/lib/adb.sh handles it.android.aapt2FromMavenOverride=/usr/lib/android-sdk/build-tools/debian/aapt2d8 / apksigner are JVM wrappers → architecture-independent.{baseDir}/tests/run-tests.shSee {baseDir}/docs/troubleshooting.md — every trap was hit for real: Exec format error, UP-TO-DATE false success, dynamic wireless port, unauthorized
key mismatch, device offline, VPN deep sleep, PowerShell UTF-16 corruption,
stale/empty mounts, black screenshot with focus held.
.agent/.Debug APKs only — not for Play Store release signing.