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
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
|
#!/bin/sh
set -eu
out="alpine-avf"
rootfs="$out/rootfs"
branch="${ALPINE_BRANCH:-v3.23}"
arch=aarch64
mirror="https://dl-cdn.alpinelinux.org/alpine"
size="${ROOT_SIZE:-4G}"
tmp=
packages="alpine-base linux-virt mkinitfs e2fsprogs e2fsprogs-extra doas ifupdown-ng ttyd \
avahi avahi-openrc dbus dbus-openrc"
die() { echo "$*" >&2; exit 1; }
cleanup() {
if [ -n "$tmp" ] && [ -d "$tmp" ]; then
rm -rf "$tmp" || true
fi
for m in dev/pts dev sys proc; do
if mountpoint -q "$rootfs/$m"; then
umount "$rootfs/$m" || true
fi
done
}
# cleanup unmounts best-effort only; rm -rf or mkfs -d on a rootfs that still
# has /dev bind-mounted would eat the host's /dev, so verify before either.
assert_unmounted() {
for m in dev/pts dev sys proc; do
! mountpoint -q "$rootfs/$m" \
|| die "$rootfs/$m still mounted; unmount it and rerun"
done
}
trap cleanup EXIT
trap 'exit 129' HUP
trap 'exit 130' INT
trap 'exit 143' TERM
[ "$(id -u)" = 0 ] || die "run as root"
[ "$(uname -m)" = "$arch" ] || [ -e "/proc/sys/fs/binfmt_misc/qemu-$arch" ] \
|| die "need a $arch host or qemu-user-static binfmt"
for c in curl grep head mktemp sha256sum tar sed gzip od tr dd basename mkfs.ext4 openssl mount umount mountpoint chroot; do
command -v "$c" >/dev/null 2>&1 || die "need $c"
done
# Validate inputs before the destructive cleanup below, so a typo fails loudly
# here instead of after curl wipes the old output or deep inside mkfs.ext4.
case "$branch" in
"" | *[!a-zA-Z0-9.-]*) die "ALPINE_BRANCH must look like v3.23, latest-stable, or edge (got '$branch')" ;;
esac
case "${size%[sSkKmMgGtT]}" in
"" | *[!0-9]*) die "ROOT_SIZE must be digits with an optional s/K/M/G/T suffix (got '$size')" ;;
esac
echo "cleaning $out"
cleanup
assert_unmounted
rm -rf "$out"; mkdir -p "$rootfs"
echo "fetching alpine $branch minirootfs"
rel="$mirror/$branch/releases/$arch"
file="$(curl -fsSL "$rel/latest-releases.yaml" | grep -oE "alpine-minirootfs-[0-9.]+-$arch\.tar\.gz" | head -1)"
[ -n "$file" ] || die "cannot determine minirootfs filename"
tmp="$(mktemp -d)"
curl -fsSL -o "$tmp/$file" "$rel/$file"
curl -fsSL -o "$tmp/$file.sha256" "$rel/$file.sha256"
( cd "$tmp" && sha256sum -c "$file.sha256" )
tar -xzf "$tmp/$file" -C "$rootfs"
rm -rf "$tmp"
echo "installing packages"
printf 'nameserver 8.8.8.8\nnameserver 1.1.1.1\n' >"$rootfs/etc/resolv.conf"
printf '%s\n%s\n' "$mirror/$branch/main" "$mirror/$branch/community" >"$rootfs/etc/apk/repositories"
mount -t proc none "$rootfs/proc"
mount -t sysfs none "$rootfs/sys"
mount --bind /dev "$rootfs/dev"
mount --bind /dev/pts "$rootfs/dev/pts"
chroot "$rootfs" /bin/sh -c "apk update && apk add $packages"
echo "configuring"
echo alpine >"$rootfs/etc/hostname"
mkdir -p "$rootfs/mnt/internal" "$rootfs/mnt/shared"
cat >"$rootfs/etc/modules" <<'EOF'
virtio_blk
virtio_pci
virtio_net
virtio_console
virtiofs
vmw_vsock_virtio_transport
EOF
chroot "$rootfs" passwd -d root
chroot "$rootfs" adduser -D -u 1000 -s /bin/ash user
chroot "$rootfs" addgroup user video
chroot "$rootfs" passwd -d user
echo 'permit nopass user' >"$rootfs/etc/doas.conf"
grep -q '^ttyS0::' "$rootfs/etc/inittab" \
|| echo 'ttyS0::respawn:/sbin/getty -L 0 ttyS0 vt100' >>"$rootfs/etc/inittab"
cat >"$rootfs/etc/network/interfaces" <<'EOF'
auto lo
iface lo inet loopback
auto eth0
iface eth0 inet dhcp
EOF
mkdir -p "$rootfs/etc/ttyd"
openssl req -x509 -newkey rsa:2048 -nodes -days 3650 -subj /CN=localhost \
-keyout "$rootfs/etc/ttyd/server.key" -out "$rootfs/etc/ttyd/server.crt" 2>/dev/null
chmod 0600 "$rootfs/etc/ttyd/server.key"
cat >"$rootfs/etc/init.d/ttyd" <<'EOF'
#!/sbin/openrc-run
description="ttyd terminal for the AVF Terminal app"
supervisor=supervise-daemon
command="/usr/bin/ttyd"
command_args="--ssl --ssl-cert /etc/ttyd/server.crt --ssl-key /etc/ttyd/server.key --ssl-ca /mnt/internal/ca.crt -t disableLeaveAlert=true -W login -f user"
respawn_delay=2
depend() { need avf-mounts; after net avahi-daemon; }
EOF
chmod +x "$rootfs/etc/init.d/ttyd"
rm -f "$rootfs"/etc/avahi/services/*.service
cat >"$rootfs/etc/avahi/services/ttyd.service" <<'EOF'
<?xml version="1.0" standalone='no'?>
<!DOCTYPE service-group SYSTEM "avahi-service.dtd">
<service-group>
<name>ttyd</name>
<service><type>_http._tcp</type><port>7681</port></service>
</service-group>
EOF
sed -i 's/^#*use-ipv4=.*/use-ipv4=yes/; s/^#*use-ipv6=.*/use-ipv6=no/' "$rootfs/etc/avahi/avahi-daemon.conf"
cat >"$rootfs/etc/init.d/avf-mounts" <<'EOF'
#!/sbin/openrc-run
description="Load AVF virtio modules and mount virtiofs shares"
depend() { after modules; before net; }
start() {
ebegin "Mounting AVF virtiofs shares"
for m in virtio_net virtiofs vmw_vsock_virtio_transport; do modprobe "$m" 2>/dev/null || true; done
mkdir -p /mnt/internal /mnt/shared
# ownership only shows through if a mount fails and the bare dir is used
chown 1000:1000 /mnt/internal /mnt/shared 2>/dev/null || true
if ! mountpoint -q /mnt/internal && ! mount -t virtiofs internal /mnt/internal; then
eend 1 "failed to mount /mnt/internal"
return 1
fi
if ! mountpoint -q /mnt/shared && ! mount -t virtiofs android /mnt/shared; then
ewarn "failed to mount /mnt/shared"
fi
eend 0
}
EOF
chmod +x "$rootfs/etc/init.d/avf-mounts"
cat >"$rootfs/etc/init.d/avf-clock" <<'EOF'
#!/sbin/openrc-run
description="Set the AVF guest clock from the Terminal app CA certificate"
depend() { need avf-mounts; before net; }
start() {
ebegin "Setting clock from /mnt/internal/ca.crt"
n=0
while [ ! -e /mnt/internal/ca.crt ] && [ "$n" -lt 10 ]; do
sleep 1
n=$((n + 1))
done
if [ ! -e /mnt/internal/ca.crt ]; then
eend 1 "/mnt/internal/ca.crt not found"
return 1
fi
ts="$(date -u -r /mnt/internal/ca.crt '+%Y-%m-%d %H:%M:%S' 2>/dev/null)" || {
eend 1 "could not read /mnt/internal/ca.crt mtime"
return 1
}
date -u -s "$ts" >/dev/null || {
eend 1 "could not set clock"
return 1
}
eend 0
}
EOF
chmod +x "$rootfs/etc/init.d/avf-clock"
cat >"$rootfs/etc/init.d/avf-resize" <<'EOF'
#!/sbin/openrc-run
description="Grow the root filesystem to fill its block device"
depend() { after root; before localmount; }
start() {
ebegin "Growing root filesystem to fill the disk"
dev="$(awk '$2 == "/" { print $1; exit }' /proc/mounts)"
[ -b "$dev" ] || { ewarn "root device not found"; return 0; }
resize2fs "$dev"
eend $?
}
EOF
chmod +x "$rootfs/etc/init.d/avf-resize"
chroot "$rootfs" /bin/sh <<'EOF'
set -e
for s in devfs dmesg mdev hwdrivers cgroups; do rc-update add "$s" sysinit || true; done
for s in modules sysctl hostname bootmisc syslog seedrng localmount networking; do rc-update add "$s" boot || true; done
for s in killprocs mount-ro savecache; do rc-update add "$s" shutdown || true; done
rc-update add avf-mounts boot
rc-update add avf-clock boot
rc-update add avf-resize boot
rc-update add dbus default
rc-update add avahi-daemon default
rc-update add ttyd default
EOF
echo "building image"
echo 'features="base virtio ext4"' >"$rootfs/etc/mkinitfs/mkinitfs.conf"
kver="$(basename "$(ls -d "$rootfs"/lib/modules/*-virt | head -1)")"
[ -n "$kver" ] || die "no -virt kernel modules under $rootfs/lib/modules"
chroot "$rootfs" mkinitfs -o /boot/initramfs-avf "$kver"
cp "$rootfs/boot/initramfs-avf" "$out/initrd.img"
# Alpine ships vmlinuz as an EFI zboot PE wrapping a gzip Image; crosvm needs the
# raw arm64 Image (magic "ARMd" / 41524d64 at offset 0x38).
vmlinuz="$rootfs/boot/vmlinuz-virt"
if [ "$(od -An -c -j4 -N4 "$vmlinuz" | tr -d ' ')" = "zimg" ]; then
off="$(od -An -tu4 -j8 -N4 "$vmlinuz" | tr -d ' ')"
sz="$(od -An -tu4 -j12 -N4 "$vmlinuz" | tr -d ' ')"
dd if="$vmlinuz" bs=1M iflag=skip_bytes,count_bytes skip="$off" count="$sz" 2>/dev/null | gzip -dc >"$out/vmlinuz"
else
cp "$vmlinuz" "$out/vmlinuz"
fi
[ "$(od -An -tx1 -j56 -N4 "$out/vmlinuz" | tr -d ' \n')" = "41524d64" ] \
|| die "extracted kernel is not a raw arm64 Image"
cleanup
assert_unmounted
rm -f "$rootfs/etc/resolv.conf"
# ^orphan_file: Alpine's mke2fs enables it by default; the image must still
# pass the stock VM's e2fsck (the stage 2 gate in install-alpine-avf) and
# online resize2fs under crosvm, so keep the feature set conservative.
mkfs.ext4 -q -F -L ROOT -O ^orphan_file -d "$rootfs" "$out/root_part" "$size"
# keep the stock layout; "name" stays "debian" (anything else is untested
# against the Terminal app).
cat >"$out/vm_config.json" <<EOF
{
"name": "debian",
"disks": [ { "partitions": [
{ "label": "ROOT", "path": "\$PAYLOAD_DIR/root_part", "writable": true, "guid": "$(cat /proc/sys/kernel/random/uuid)" }
], "writable": true } ],
"sharedPath": [ { "sharedPath": "/storage/emulated" }, { "sharedPath": "\$APP_DATA_DIR/files" } ],
"kernel": "\$PAYLOAD_DIR/vmlinuz",
"initrd": "\$PAYLOAD_DIR/initrd.img",
"params": "root=/dev/vda1 rootfstype=ext4 rw console=ttyS0",
"protected": false,
"cpu_topology": "match_host",
"platform_version": "~1.0",
"memory_mib": 4096,
"debuggable": true,
"connect_console": true,
"console_out": true,
"console_input_device": "ttyS0",
"network": true,
"auto_memory_balloon": false,
"gpu": { "backend": "2d" }
}
EOF
# The Terminal app parses the LAST space-separated token of build_id as a 4-digit
# year and reinstalls the stock Debian (InstalledImage.isOlderThanCurrentVersion)
# if it is below the app's release year. Keep the trailing year; do not drop it.
echo "alpine $(date -u +%Y%m%dT%H%M%SZ) $(date -u +%Y)" >"$out/build_id"
echo "done: $out"
|