1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
|
#!/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
|