#!/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' ttyd _http._tcp7681 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" <"$out/build_id" echo "done: $out"