#!/bin/sh set -eu vm="/mnt/internal/linux" marker="alpine-avf-install" extras="vmlinuz initrd.img build_id vm_config.json" # everything but root_part die() { echo "$*" >&2; exit 1; } [ "$(id -u)" = 0 ] || die "run as root" [ -e /sdcard ] && die "run this INSIDE the Terminal VM, not Android" [ -d "$vm" ] || die "$vm not found; is this the Terminal VM?" for c in python3 truncate stat blockdev findmnt e2fsck cmp dd sync reboot; do command -v "$c" >/dev/null 2>&1 || die "need $c" done if [ -f "$marker" ]; then stage=2; img="$(cat "$marker")" else stage=1; img="$(CDPATH='' cd -- alpine-avf && pwd)" fi for f in root_part $extras; do [ -f "$img/$f" ] || die "missing $img/$f" done if [ "$stage" = 1 ]; then echo "stage 1: staging $img" # Size the new partition to the larger of our image and the outgoing Debian # root_part, so Alpine inherits the full disk. The file is sparse, so the # larger apparent size costs no real space; avf-resize grows ext4 into it. size="$(stat -c %s "$img/root_part")" cur="$(stat -c %s "$vm/root_part" 2>/dev/null || echo 0)" [ "$cur" -gt "$size" ] && size="$cur" guid="$(cat /proc/sys/kernel/random/uuid)" # Hide the stock backup while the install is in flight, so the app cannot # restore it over the staged root; stage 2 puts it back after the swap. [ -e "$vm/root_part_backup" ] && mv -f "$vm/root_part_backup" "$vm/root_part_backup.aside" truncate -s "$size" "$vm/alpine_root" # Append a temporary ALPINE partition; crosvm exposes it as a new /dev/vdaN. python3 - "$vm/vm_config.json" "$guid" <<'PY' import json, os, sys p, guid = sys.argv[1], sys.argv[2] c = json.load(open(p)); parts = c["disks"][0]["partitions"] if not any(str(x.get("path", "")).endswith("/alpine_root") for x in parts): parts.append({"label": "ALPINE", "path": "$PAYLOAD_DIR/alpine_root", "writable": True, "guid": guid}) t = p + ".new" with open(t, "w") as f: json.dump(c, f, indent=4) f.write("\n") os.replace(t, p) PY printf '%s\n' "$img" > "$marker" echo "staged; rebooting. Reopen Terminal, then run this again." sync; reboot exit 0 fi echo "stage 2: installing $img" target="$(python3 - "$vm/vm_config.json" <<'PY' import json, sys parts = json.load(open(sys.argv[1]))["disks"][0]["partitions"] i = next((n for n, x in enumerate(parts, 1) if str(x.get("path", "")).endswith("/alpine_root")), None) if i is None: sys.exit("no ALPINE partition in vm_config.json; rm ./alpine-avf-install and rerun stage 1") print("/dev/vda%d" % i) PY )" echo "target: $target" n=0; while [ ! -b "$target" ] && [ "$n" -lt 20 ]; do sleep 1; n=$((n + 1)); done [ -b "$target" ] || die "$target absent; reboot and rerun, or rm ./$marker and rerun stage 1" [ "$target" != "$(findmnt -no SOURCE /)" ] || die "$target is the live root; refusing" want="$(stat -c %s "$img/root_part")" [ "$(blockdev --getsize64 "$target")" -ge "$want" ] || die "target smaller than source" # dd through the block device (writing the file over virtiofs corrupts). # Chunked with a sync per chunk to bound dirty page cache in the VM's RAM. echo "copying root_part -> $target" chunk=$((250 * 1024 * 1024)); n=0 while [ $((n * chunk)) -lt "$want" ]; do dd if="$img/root_part" of="$target" bs="$chunk" count=1 skip="$n" seek="$n" conv=notrunc iflag=fullblock status=none sync; n=$((n + 1)) done # The copy of a freshly built image must check out clean; even "errors # corrected" (rc 1/2) means the copy diverged from the verified source. e2fsck -fy "$target" || die "e2fsck rc $? on $target: copy diverged; rerun stage 2" # Stage kernel/initrd/config over virtiofs, verified; fail before the swap. for f in $extras; do rm -f "$vm/$f.new" n=0 until cmp -s "$img/$f" "$vm/$f.new"; do [ "$n" -lt 3 ] || die "could not write $vm/$f.new reliably; aborted before swap" dd if="$img/$f" of="$vm/$f.new" bs=1M conv=fsync status=none sync n=$((n + 1)) done done echo "swapping root" rm -f "$vm/root_part.previous" mv -f "$vm/root_part" "$vm/root_part.previous" mv -f "$vm/alpine_root" "$vm/root_part" || { mv -f "$vm/root_part.previous" "$vm/root_part" die "could not replace root_part" } for f in $extras; do mv -f "$vm/$f.new" "$vm/$f" done rm -f "$vm/root_part.previous" # stock Debian's extra-modules image; the new vm_config.json does not use it rm -f "$vm/kernel_extras_part" [ -e "$vm/root_part_backup.aside" ] && mv -f "$vm/root_part_backup.aside" "$vm/root_part_backup" rm -f "$marker" echo "installed; rebooting into Alpine. Reopen the Terminal app." # the pause lets virtiofs settle the renames host-side before the VM dies sync; sleep 2; reboot