Runtime & PRoot
The Runtime is an immutable Ubuntu 24.04 root filesystem compiled and distributed per ABI. PRoot provides the rootless translation layer enabling this filesystem to execute on Android.
Why PRoot is Used
Standard Android security policies prevent regular applications from executing execve on files inside private directories or using chroot. PRoot intercepts system calls via ptrace, rewriting path requests to a user-space root directory to emulate a standard Linux environment.
Key technical requirements:
- Executable Placement: The PRoot toolchain is packaged as
lib*.sobinaries inside the APK'sjniLibs, installing tonativeLibraryDir—the sole directory permittingexecveunder targetSdk 35+. - Loader Path:
PROOT_LOADERreferences loaders insidenativeLibraryDirto bypass private storage execution restrictions. - SELinux Compatibility: Android lacks standard
/dev/shmmounts and disallows hard link creation. PRoot binds private shared memory directories and enables--link2symlinkfor Python multiprocessing and IPC queues.
Session Contract
App Shell (Parent) ──stdin pipe──▶ proot wrapper ──▶ Runtime Process Group- stdin must remain open. When the parent application dies and closes the pipe (EOF), the wrapper immediately terminates the child process group to eliminate orphan workers.
- Canceling the
ProotHostobserver coroutine only ceases waiting; it must throwCancellationExceptionwithout wiping the activesessionhandle, which is needed to close stdin and await process group termination. - Employs exponential backoff on crashes and purges stale lockfiles before restarting.
Never Spawn via Background Daemon Wrappers
Spawning processes via background wrappers like nohup breaks the parent-child relationship. Orphaned runtimes will continue running in the background, conflicting over configurations and SQLite database locks on subsequent boots.
Provisioning State Machine
ProvisionState:
| State | Description |
|---|---|
Checking | Verifying installed rootfs version |
NotInstalled | Not deployed; awaiting user initiation |
NotBundled | Thin APK used; release lacks matching ABI |
LowDisk | Free storage is below 2 GB |
Downloading | Downloading payload (measured in compressed bytes) |
Extracting | Unpacking payload (measured in compressed bytes) |
Ready | Provisioning verified and ready |
Failed | Deployment failure with user-facing reason |
Installation Validation
isInstalled() validates three conditions simultaneously:
- The version in
.provisionedmatchesBUILD_MANIFESTinsideopt/azurpilot/. - The
rootfs_archmatches the host device ABI. - The Python interpreter binary exists and is executable.
Installed runtimes prompt for updates when new versions appear, but never replace files automatically.
Segmented Download Engine
- Payloads ≥ 8 MiB initiate an HTTP
bytes=0-0range probe before opening 4 parallel connections downloading non-overlapping ranges. - Each chunk validates HTTP 206 responses,
Content-Rangeheaders, byte lengths, and version tags; compression is disabled. - Failure in any segment cancels the pool, waits for thread shutdown, flushes partial files, and retries once via a single stream.
- Mirror switches poll
shouldAbortevery 100 ms to discard partial downloads before re-downloading from the new host. - Verified first against byte sizes, then against SHA-256 hashes.
Stream Extraction Pipeline
- Unpacks into
rootfs.tmp, performing an atomic rename upon completion. - Implements a pure-Java streaming pipeline (commons-compress tar + tukaani xz), avoiding busybox tar bugs with forward hard-link references.
- Maps symlinks via
Os.symlink; materializes hard links as duplicates with second-pass resolution for forward references. - Rejects path traversal attempts and entries referencing symlink targets outside root.
- Reconstructs POSIX executable permissions from tar header modes.
- Preserves
config/,password.txt, andlog/; updates templates and seeds from the incoming image. - Writes the
.provisionedmarker and commits directory rename.
Initial Boot Performance
Stripping __pycache__ minimizes payload sizes. On first boot, the Python interpreter compiles all modules to bytecode, often requiring over 90 seconds. ProotHost logs:
AzurPilot WebUI not ready within 90000msThis is an expected warning; subsequent boots start at normal speeds.
Manifest Metadata
The BUILD_MANIFEST file defines runtime provenance:
| Property | Purpose |
|---|---|
azurpilot_commit | Embedded upstream core commit SHA |
rootfs_version | <upstream-12>-<input-hash-10> identifier |
rootfs_arch | Target ABI architecture |
android_host_commit | Host repository commit used during build |
node_version | Bundled Node.js toolchain version |
hot_update_mode=local | Signals support for on-device frontend compilation |
The rootfs_version hash encompasses build scripts, overlays, seeds, and toolchains. If the hash changes, a full update is required; if the hash matches while commits differ, a script hot-update suffices.
Transactional Hot-Updates
Query /android/update/status
→ Shallow clone target commit to /opt/azurpilot-update/source
→ Compare uv.lock
├── Differing → requiresRuntimeUpdate=true (abort hot-update)
└── Matching → npm ci && npm run build (600s timeout)
→ Write transaction log config/android-update-transaction.json
→ Stop instances → Backup dist → Swap source → Install dist → Update manifest
→ Supervisor reloads and restores instancesRollback mechanisms:
- Pre-swap failure: Candidate directory is pruned; active source and dist remain untouched.
- Mid-swap failure: Restores previous source, dist, and manifest from backup.
- Unclean termination:
android_host.pychecks the transaction log on boot and rolls back state; if rollback fails, it halts boot to avoid corruption.
Overlays and Seed Files
| Directory | Purpose | Rule |
|---|---|---|
rootfs/overlays/ | Resolves Android/PRoot compatibility gaps | Never overwrites user configurations |
rootfs/seeds/ | First-boot configuration templates | Written exclusively during initial setup |
rootfs/build/ | build-azurpilot.sh build scripts | Must execute on target ABI native runners |
Overlay tests reside under rootfs/tests/:
uv run --with psutil python -m unittest discover -s rootfs/testsStorage and Memory Footprint
| Component | Storage Size |
|---|---|
| rootfs Compressed Payload | 300–450 MB |
| Unpacked Installation | 1.5–2.5 GB |
| Hot-Update Staging Directory | Several hundred MB |
Frontend node_modules | Several hundred MB (pruned post-build) |
On-device compilation consumes CPU, RAM, and storage; insufficient resources automatically abort builds while keeping older assets active.