aboutsummaryrefslogtreecommitdiff
path: root/README
diff options
context:
space:
mode:
Diffstat (limited to 'README')
-rw-r--r--README99
1 files changed, 99 insertions, 0 deletions
diff --git a/README b/README
new file mode 100644
index 0000000..8c6f603
--- /dev/null
+++ b/README
@@ -0,0 +1,99 @@
+alpine-avf
+==========
+Replace the Debian VM behind Android's official "Linux Terminal" app (AVF) with
+Alpine Linux, from inside the stock VM, with no Android root. The unmodified
+Terminal app then boots Alpine and connects to a "user" shell.
+
+
+How it works
+------------
+The Terminal app boots a crosvm VM from a payload directory it exposes to the
+guest over virtiofs at /mnt/internal/linux (root-writable). build-alpine-avf
+produces that payload; install-alpine-avf swaps it in from inside the running VM.
+
+The app finds the terminal purely over mDNS: it resolves the Avahi service named
+"ttyd" (_http._tcp), connects to https://<guest-ip>:7681, presents a client cert
+(which ttyd verifies against /mnt/internal/ca.crt), and shows ttyd's web UI.
+
+
+Usage
+-----
+In the stock Debian VM, as root:
+
+ sudo ./build-alpine-avf # writes ./alpine-avf
+ sudo ./install-alpine-avf # stage 1: stages the image, reboots
+ # reopen Terminal, then:
+ sudo ./install-alpine-avf # stage 2: installs it, reboots into Alpine
+
+Run both install stages from the same directory: stage 1 records the image path
+in an alpine-avf-install marker file there, and stage 2 reads it back.
+
+The login is "user" (no password, passwordless doas); root is also
+passwordless on the VM console, for debugging. build-alpine-avf takes
+two optional env vars: ALPINE_BRANCH (default v3.23; "latest-stable" tracks the
+newest) and ROOT_SIZE (default 4G). ROOT_SIZE is only the INITIAL filesystem
+size: on first boot Alpine grows the root to fill whatever disk the Terminal app
+provisions (see the "Disk size" note), matching what the stock Debian VM did.
+
+
+Testing
+-------
+build-alpine-avf cross-builds an aarch64 image on an x86 host that has the
+qemu-user-static binfmt handler for aarch64 registered. The result boots under
+qemu-system-aarch64, which smoke-tests the boot chain (kernel extraction,
+initramfs, root mount, OpenRC, DHCP) but not the takeover or ttyd: qemu has
+no AVF virtiofs, so avf-mounts fails loudly and ttyd, which needs it and the
+app's CA under /mnt/internal, stays down.
+
+ sudo ./build-alpine-avf
+ qemu-system-aarch64 -M virt -cpu cortex-a57 -m 3072 -smp 2 \
+ -kernel alpine-avf/vmlinuz -initrd alpine-avf/initrd.img \
+ -append "root=/dev/vda rootfstype=ext4 rw console=ttyAMA0" \
+ -drive file=alpine-avf/root_part,format=raw,if=virtio \
+ -device virtio-gpu-pci -netdev user,id=n -device virtio-net-pci,netdev=n \
+ -nographic
+
+A clean run reaches the default runlevel on the serial console. There is no
+login prompt there: the image's getty is on ttyS0 (crosvm's serial), and the
+qemu virt machine provides ttyAMA0. For a shell inside the image, add
+init=/bin/sh to -append (bypasses OpenRC). Exit qemu with C-a x. Note
+root=/dev/vda here (the raw ext4 image is the whole disk) versus /dev/vda1
+under crosvm (which presents a composite disk).
+
+
+Notes
+-----
+- Kernel: Alpine's vmlinuz is an EFI zboot PE (gzip-wrapped); crosvm's direct
+ boot needs the raw arm64 image, so the build extracts it.
+- Runlevels: a minirootfs ships them empty (no setup-alpine), so the build
+ seeds the standard services explicitly.
+- DNS: the stock Debian VM's /etc/resolv.conf is a stale GCP build-env file, so
+ the build gives the chroot its own resolvers for apk and drops them before
+ mkfs; DHCP supplies the guest's.
+- Mountpoints: /mnt/internal and /mnt/shared are pre-created in the image,
+ since OpenRC mounts them before the root fs is remounted read-write.
+- Clock: The build installs an early avf-clock OpenRC service that sets the
+ clock from /mnt/internal/ca.crt's mtime before networking and ttyd start.
+- Image writes: dd the root image through the new block device, not over
+ virtiofs (a file write over virtiofs corrupts); kernel/initrd/config go over
+ virtiofs but are verified, before the destructive root swap.
+- build_id: the Terminal app parses the last space-separated token of
+ /mnt/internal/linux/build_id as a 4-digit year and, if it is below the app's
+ release year (InstalledImage.isOlderThanCurrentVersion), declares the image
+ outdated and initiates recovery of the stock image, so build_id must
+ therefore end in a current year.
+- Disk size: the app sizes the root disk host-side, not the guest. With storage
+ ballooning on (the usual case) it grows the root_part backing file to ~95% of
+ total phone storage on every boot (VmLauncherService.calculateSparseDiskSize)
+ but leaves the guest filesystem alone, expecting the guest to grow it online;
+ an avf-resize OpenRC service runs resize2fs on the root device at boot to
+ fill it (idempotent; a no-op once filled). install-alpine-avf also sizes the
+ staged partition to the larger of the image and the outgoing root_part, so
+ the device is already large even without ballooning. ext4 online-grows
+ because mkfs keeps resize_inode.
+
+
+Recovery
+--------
+If a VM is left unbootable, reinstall stock Debian from Android Settings >
+Developer options > Linux development environment > reset.