Engineering

How it works

The interesting sentence is not "an IDE on your phone". It's this: Android won't let an app exec a binary from its own storage, and it kills foreground apps under memory pressure. Here's what it took to run Gradle anyway.

1. No proot. Native Bionic processes.

Most "Linux on Android" tooling runs inside proot, a userspace ptrace-based emulation of chroot. It works, and it is slow. Pocket Studio doesn't use it for builds: Gradle, the Kotlin compiler and the Android Gradle Plugin run as real Bionic processes against the platform's own dynamic linker.

The blocker is W^X plus Android's refusal to execute anything from app-private storage. The solution is a small launcher that masquerades as java, javac, jar, keytool and gradle through argv[0], reached through symlinks into the app's native-library directory.

The key insight, and it cost days to find:

Scripts can never exec from app storage. Only symlinks into the native-library directory, pointing at .so files, can.

proot survives only for provisioning, and as a fallback when the native path can't be built.

2. A self-built SDK — because arm64 build-tools didn't exist

The best available arm64 Android build-tools stopped at 34.0.4. To build against newer platforms on-device, aapt2 and aidl were compiled from AOSP source and library-swapped to run against Bionic on a phone.

The result is that on-device builds can target a compile SDK ahead of what any other on-phone IDE offers — currently build-tools 37.0.0, which uses the minor-version platform packaging scheme.

3. Live preview: single-file compile, patched over the built APK

The Live toggle does not run Gradle and does not reinstall. It compiles the one file you edited, dexes it, and patches the resulting dex over the APK already built on the device.

XML edits take a completely different route: an aapt2 overlay compiled against the base APK, using stable IDs harvested from the base dump, applied at runtime through ResourcesLoader with later-wins semantics. No harvest, no build, no install.

Traps found the hard way: a dex-metadata crash produces a silent zero-byte jar; the R8 version must match the build-tools version; class-data sharing cut this path from about five seconds to about two.

4. AppCDS on a phone

Class-data-sharing archives are trained after a build completes, for both the Gradle client and daemon JVMs, per Gradle version. Measured effect on the development device: −22% JVM boot time and −87% private metaspace.

Three landmines, all real:

  • The default archive path is derived from the JVM library, not from java.home — you have to pass both paths.
  • Gradle's -javaagent requires the diagnostic archive flags at dump time and at use time, and the flag is one-directional.
  • The daemon's dump lands after the client exits, so you have to poll for it to settle.

5. Fighting lmkd for the right to finish a build

Android's low-memory killer will kill foreground apps once memory pressure crosses its threshold. A long Gradle build is precisely the allocation storm that gets you there.

The shipped defence has two layers:

  • An orphan reaper. When the app is killed, detached compiler daemons survive and snowball the pressure that killed it. The watchdog reaps them.
  • A memory guard that uses available memory as a leading trigger, aborting the build cleanly before the killer can act.

The reason a naive pressure-stall guard fails is worth writing down: the ten-second pressure average is an exponential moving average — it trails allocation storms. By the time it reads high, the kill has already happened. An earlier freeze-only guard was proven insufficient by three foreground kills with it live.

6. A real JDWP debugger — that has to ship its own adb

Breakpoints, frames, locals and stepping, against a real app process. It can even target an app running on a second phone over the network.

Two things make this harder than it sounds:

  • adb cannot self-daemonize here, because the exec bridge breaks the fork. It runs as a managed foreground server on a non-default port.
  • Shipping adb means shipping its entire dependency closure. The first attempt missed a transitive library and adb died at link time on-device. The shipping pack carries the verified closure.

7. Language intelligence is real LSP, not regex

The language servers for Java, Kotlin, C/C++, Python and Rust are spawned as real processes through the same exec bridge. They install on first use rather than shipping in the APK.

The Kotlin server gets its classpath harvested from a real build, which is what lifts resolution from "unresolved reference" soup to something worth using.

clangd needed its own workaround: it computes its builtin-header directory from /proc/self/exe, which under the exec bridge points at the linker rather than at clangd. The fix is a generated config injecting an explicit resource directory.

Highlighting is backed by 21 tree-sitter grammars compiled for arm64: bash, c, cmake, cpp, css, dockerfile, groovy, html, java, javascript, json, kotlin, markdown, properties, python, rust, toml, tsx, typescript, xml and yaml.

8. The bug collection

The ones other Android developers will recognise as painful:

  • Android's ICU regex engine rejects a bare } that the desktop JVM accepts. Desktop unit tests cannot catch this class of bug.
  • adb shell run-as 'cat > file' silently mangles binaries — the pty rewrites bytes. Use adb exec-in.
  • The Android Gradle Plugin's prefab task fails on any stderr output at all, so the exec shim had to be silenced entirely.
  • An app that installs other apps cannot see the apps it installs without a package-visibility query filter.
  • Reinstalling looks like a store update: the native-library directory moves, and because the NDK path is fingerprinted as a string, cached native build commands go stale invisibly.
  • Running adb install during a build kills the build.
  • Kotlin's deleteRecursively follows directory symlinks — which is catastrophic on a tree made of symlink grafts.

Proof it isn't a toy

Real open-source Android apps, built on the device to a signed APK during development testing: NewPipe, Tusky, AntennaPod, KeePassDX, Thunderbird, Telegram, Element X (100+ modules), Home Assistant, Nextcloud, Termux and CoMaps.

Home Assistant and Nextcloud were the two that forced the memory work above — both were killed at foreground before the second-generation guard, and both green after it.

What isn't true yet

Being precise here is what keeps the rest credible.

  • An in-process Tooling API client is an experiment, not shipped. The numbers are promising; it is not in the shipping build path, and it should be described as a result rather than a feature.
  • rust-analyzer runs degraded on Cargo projects — there is no cargo on-device yet. Rust syntax and code intelligence work; building Rust projects is not a claim we make.
  • Language servers and dev tools are not in the APK. They install on first use.
  • The bundled runtime is OpenJDK (an independent build). We make no Java SE compatibility claim — that requires Oracle's TCK.

Sources. Everything above is drawn from the project's own engineering notes, where each claim is anchored to a specific file or commit. Measured figures come from the development device and will differ on yours — the device benchmarks page exists precisely because a single device's numbers aren't a general claim.