scripts/firecracker-menual/install-ubuntu-roofs.sh
Rama main · réplica de lectura del repositorio original.
#!/usr/bin/env bash
set -euo pipefail
script_dir=$(CDPATH='' cd -- "$(dirname -- "$0")" && pwd -P)
repo_dir=$(CDPATH='' cd -- "${script_dir}/../.." && pwd -P)
script_path="${script_dir}/$(basename -- "$0")"
ubuntu_base_url='https://cdimage.ubuntu.com/ubuntu-base/releases'
ubuntu_series_setting=${M2IMAGE_DISTRO_SERIES:-26.04}
artifact_dir="${repo_dir}/images/rootfs"
kernel_artifact_dir="${repo_dir}/images/kernel"
kernel_image_name=''
extract_vmlinux="${script_dir}/extract-vmlinux"
extract_arm64_image="${script_dir}/extract-arm64-image"
build_dir="${repo_dir}/build/ubuntu-rootfs"
rootfs_image=''
rootfs_link=''
rootfs_size='2G'
rootfs_hostname='firecrab'
m2image_alias=${M2IMAGE_ALIAS:-}
sbom_output=${M2IMAGE_SBOM_OUTPUT:-}
sbom_generator="${repo_dir}/scripts/m2image_sbom.py"
# linux-image-virtual: Ubuntu's own officially-maintained kernel package
# (public-docs/guides/images.md) — replaces the self-built vanilla kernel every
# template used to share, so security patches and driver support follow
# Ubuntu's own release cadence instead of this project having to track
# kernel.org itself.
#
# The *virtual* meta-package, not *generic*, though both resolve to the
# identical binary (both Depend on linux-image-<abi>-generic, so the kernel,
# its cadence and its module set are unchanged). The difference is one line
# of packaging: linux-image-generic additionally carries
# "Depends: linux-firmware | linux-firmware-minimal", and apt always takes
# the first alternative — which is a hard dependency, so
# --no-install-recommends cannot decline it. That dragged in every
# linux-firmware-* subpackage: Qualcomm/Intel/Broadcom/Marvell/Realtek
# wireless, NVIDIA/AMD/Intel graphics, Mellanox/Netronome/QLogic NICs.
# Measured inside the built image with debugfs, that was 656 MB across 5233
# files — 53% of the rootfs's 1.24 GiB of content — for hardware a
# Firecracker microVM cannot have. None of it is reachable at boot either:
# this template ships no initrd (`"initrd": null` in packaging/m2images.json)
# and mounts root=/dev/vda straight from the kernel's built-in
# CONFIG_VIRTIO_BLK/VIRTIO_MMIO/EXT4_FS, so firmware and modules play no part
# in bringing the guest up.
rootfs_boot_packages='systemd systemd-sysv systemd-resolved udev kmod util-linux linux-image-virtual'
rootfs_packages="${rootfs_boot_packages} iproute2 iputils-ping net-tools dnsutils curl ca-certificates procps openssh-server"
mount_dir=''
chroot_mounts=''
info() {
printf '[INFO] %s\n' "$1"
}
fail() {
printf '[FAIL] %s\n' "$1" >&2
exit 1
}
has_command() {
command -v "$1" >/dev/null 2>&1
}
require_command() {
if ! has_command "$1"; then
fail "Required command not found: $1"
fi
}
abs_dir() {
path=$1
mkdir -p "$path"
cd "$path" && pwd -P
}
abs_file() {
path=$1
case "$path" in
/*)
printf '%s\n' "$path"
;;
*)
printf '%s/%s\n' "$(pwd -P)" "$path"
;;
esac
}
link_target_for() {
target=$1
link_path=$2
target_dir=${target%/*}
link_dir=${link_path%/*}
if [ "$target_dir" = "$link_dir" ]; then
printf '%s\n' "${target##*/}"
else
printf '%s\n' "$target"
fi
}
update_symlink() {
target=$1
link_path=$2
if [ "$target" = "$link_path" ]; then
return
fi
if [ -d "$link_path" ] && [ ! -L "$link_path" ]; then
fail "Cannot replace directory with symlink: ${link_path}"
fi
link_target=$(link_target_for "$target" "$link_path")
ln -sfn "$link_target" "$link_path"
printf '%s\n' "$link_target"
}
write_root_file() {
target=$1
cat >"$target"
}
install_authorized_ssh_key() {
local key_source=''
local key_home=''
if [ -n "${SUDO_USER:-}" ] && [ -n "${SUDO_UID:-}" ]; then
key_home=$(getent passwd "$SUDO_UID" | cut -d: -f6)
elif [ -n "${HOME:-}" ]; then
key_home=$HOME
fi
if [ -n "$key_home" ]; then
for key_source in \
"$key_home/.ssh/id_ed25519.pub" \
"$key_home/.ssh/id_ecdsa.pub" \
"$key_home/.ssh/id_rsa.pub"; do
[ -s "$key_source" ] && break
key_source=''
done
fi
if [ -z "$key_source" ]; then
info 'no host SSH public key found; building with serial-console access only'
return
fi
install -d -m 0700 "${mount_dir}/root/.ssh"
install -m 0600 "$key_source" "${mount_dir}/root/.ssh/authorized_keys"
}
cleanup() {
cleanup_chroot_mounts
}
trap cleanup EXIT
detect_ubuntu_arch() {
case "${M2IMAGE_ARCH:-${UBUNTU_ARCH:-$(uname -m 2>/dev/null || printf 'unknown')}}" in
x86_64 | amd64)
printf '%s\n' 'amd64'
;;
aarch64 | arm64)
printf '%s\n' 'arm64'
;;
*)
fail 'Unsupported architecture. Ubuntu Base rootfs creation supports amd64 and arm64.'
;;
esac
}
resolve_ubuntu_base_archive() {
series=$1
release_url="${ubuntu_base_url}/${series}/release"
index_html="${build_dir}/ubuntu-base-index.html"
mkdir -p "$build_dir"
if ! fetch_url "${release_url}/" "$index_html"; then
fail "Could not download Ubuntu Base release index: ${release_url}/"
fi
archive_name=$(find_ubuntu_base_archive "$index_html")
if [ -z "$archive_name" ]; then
fail "Could not find an Ubuntu Base archive for ${series}/${ubuntu_arch}."
fi
printf '%s/%s\n' "$release_url" "$archive_name"
}
find_ubuntu_base_archive() {
index_html=$1
grep -Eo "ubuntu-base-[0-9.]+-base-${ubuntu_arch}\\.tar\\.gz" "$index_html" |
sort -V |
tail -n 1 ||
true
}
fetch_url() {
url=$1
output_path=$2
output_tmp="${output_path}.tmp"
if curl -fsSL "$url" -o "$output_tmp" 2>/dev/null; then
mv "$output_tmp" "$output_path"
return 0
fi
rm -f "$output_tmp"
[ -f "$output_path" ]
}
resolve_ubuntu_series() {
if [ "$ubuntu_series_setting" != 'latest' ]; then
printf '%s\n' "$ubuntu_series_setting"
return
fi
releases_index="${build_dir}/ubuntu-base-releases-index.html"
mkdir -p "$build_dir"
if ! fetch_url "${ubuntu_base_url}/" "$releases_index"; then
fail "Could not download Ubuntu Base releases index: ${ubuntu_base_url}/"
fi
release_candidates=$(
grep -Eo '[0-9]{2}\.[0-9]{2}(\.[0-9]+)?/' "$releases_index" |
sed 's:/$::' |
sort -Vu |
sort -Vr ||
true
)
if [ -z "$release_candidates" ]; then
fail 'Could not resolve the latest Ubuntu Base release.'
fi
for series in $release_candidates; do
release_index="${build_dir}/ubuntu-base-${series}-index.html"
if ! fetch_url "${ubuntu_base_url}/${series}/release/" "$release_index"; then
continue
fi
if [ -n "$(find_ubuntu_base_archive "$release_index")" ]; then
printf '%s\n' "$series"
return
fi
done
fail "Could not find any Ubuntu Base archive for ${ubuntu_arch}."
}
verify_checksum() {
checksum_file="${download_dir}/SHA256SUMS"
checksum_line=$(grep -E "[ *]${archive_name}$" "$checksum_file" || true)
if [ -z "$checksum_line" ]; then
fail "Checksum entry not found for archive: ${archive_name}"
fi
printf '%s\n' "$checksum_line" | (cd "$download_dir" && sha256sum -c -)
}
fetch_checksum() {
checksum_file="${download_dir}/SHA256SUMS"
checksum_tmp="${checksum_file}.tmp"
checksum_url="${ubuntu_base_url}/${ubuntu_series}/release/SHA256SUMS"
if curl -fsSL "$checksum_url" -o "$checksum_tmp" 2>/dev/null; then
mv "$checksum_tmp" "$checksum_file"
return
fi
rm -f "$checksum_tmp"
if [ -f "$checksum_file" ]; then
info "reusing cached checksum file: ${checksum_file}"
return
fi
fail "Could not download Ubuntu Base checksums: ${checksum_url}"
}
download_archive() {
archive_tmp="${archive_path}.tmp"
if [ -f "$archive_path" ]; then
info "reusing Ubuntu Base archive: ${archive_path}"
return
fi
info "downloading Ubuntu Base archive: ${archive_url}"
if ! curl -fsSL "$archive_url" -o "$archive_tmp" 2>/dev/null; then
rm -f "$archive_tmp"
fail "Could not download Ubuntu Base archive: ${archive_url}"
fi
mv "$archive_tmp" "$archive_path"
}
check_rootfs_output() {
if [ -d "$rootfs_image" ] && [ ! -L "$rootfs_image" ]; then
fail "Rootfs image path exists as a directory: ${rootfs_image}"
fi
if [ "$rootfs_link" != "$rootfs_image" ] &&
[ -d "$rootfs_link" ] &&
[ ! -L "$rootfs_link" ]; then
fail "Rootfs symlink path exists as a directory: ${rootfs_link}"
fi
}
print_rootfs_outputs() {
info "Ubuntu rootfs image: ${rootfs_image}"
if [ "$rootfs_link" != "$rootfs_image" ]; then
info "Ubuntu rootfs symlink: ${rootfs_link} -> ${rootfs_link_target}"
fi
}
restore_output_ownership() {
if [ -z "${SUDO_UID:-}" ] || [ -z "${SUDO_GID:-}" ]; then
return
fi
chown "${SUDO_UID}:${SUDO_GID}" "$rootfs_image"
chmod u+rw,go+r "$rootfs_image"
if [ "$rootfs_link" != "$rootfs_image" ] && [ -L "$rootfs_link" ]; then
chown -h "${SUDO_UID}:${SUDO_GID}" "$rootfs_link" 2>/dev/null || true
fi
chown "${SUDO_UID}:${SUDO_GID}" "${kernel_artifact_dir}/${kernel_image_name}"
}
cleanup_chroot_mounts() {
local target
for target in $chroot_mounts; do
umount -R "$target" 2>/dev/null || umount -l "$target" 2>/dev/null || true
done
chroot_mounts=''
}
mount_chroot_fs() {
cp /etc/resolv.conf "${mount_dir}/etc/resolv.conf"
mount -t proc proc "${mount_dir}/proc"
chroot_mounts="${mount_dir}/proc ${chroot_mounts}"
mount --rbind /sys "${mount_dir}/sys"
mount --make-rslave "${mount_dir}/sys"
chroot_mounts="${mount_dir}/sys ${chroot_mounts}"
mount --rbind /dev "${mount_dir}/dev"
mount --make-rslave "${mount_dir}/dev"
chroot_mounts="${mount_dir}/dev ${chroot_mounts}"
}
prepare_rootfs_image() {
mkdir -p "$(dirname "$rootfs_image")"
if [ "$rootfs_link" != "$rootfs_image" ]; then
mkdir -p "$(dirname "$rootfs_link")"
fi
if [ -e "$rootfs_image" ] || [ -L "$rootfs_image" ]; then
rm -f "$rootfs_image"
fi
if [ "$rootfs_link" != "$rootfs_image" ] && { [ -e "$rootfs_link" ] || [ -L "$rootfs_link" ]; }; then
rm -f "$rootfs_link"
fi
# Create a raw image that Firecracker can expose as /dev/vda.
truncate -s "$rootfs_size" "$rootfs_image"
}
configure_rootfs() {
# Add the minimal host identity and mount table expected by the Ubuntu userspace.
write_root_file "${mount_dir}/etc/hostname" <<EOF
${rootfs_hostname}
EOF
write_root_file "${mount_dir}/etc/hosts" <<EOF
127.0.0.1 localhost
127.0.1.1 ${rootfs_hostname}
EOF
write_root_file "${mount_dir}/etc/fstab" <<'EOF'
/dev/vda / ext4 defaults 0 1
EOF
write_root_file "${mount_dir}/etc/machine-id" <<'EOF'
EOF
install_authorized_ssh_key
# Configure the Firecracker guest network interface via DHCP — the host's
# net-helper/dnsmasq hands out the IPAM-allocated address by MAC
# reservation (public-docs/guides/networking.md), so the guest never
# hardcodes an address itself.
install -d -m 0755 "${mount_dir}/etc/systemd/network"
write_root_file "${mount_dir}/etc/systemd/network/10-eth0.network" <<'EOF'
[Match]
Name=eth0
[Network]
DHCP=yes
EOF
install -d -m 0755 "${mount_dir}/etc/systemd/system/multi-user.target.wants"
ln -sf /lib/systemd/system/systemd-networkd.service \
"${mount_dir}/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
install -d -m 0755 "${mount_dir}/etc/systemd/system/sockets.target.wants"
ln -sf /lib/systemd/system/systemd-networkd.socket \
"${mount_dir}/etc/systemd/system/sockets.target.wants/systemd-networkd.socket"
install_network_ready_sentinel
# Enable a serial console getty so Firecracker console output reaches ttyS0.
install -d -m 0755 "${mount_dir}/etc/systemd/system/getty.target.wants"
ln -sf /lib/systemd/system/serial-getty@.service \
"${mount_dir}/etc/systemd/system/getty.target.wants/serial-getty@ttyS0.service"
install -d -m 0755 "${mount_dir}/etc/systemd/system/serial-getty@ttyS0.service.d"
write_root_file "${mount_dir}/etc/systemd/system/serial-getty@ttyS0.service.d/autologin.conf" <<'EOF'
[Unit]
BindsTo=
After=
After=systemd-user-sessions.service getty-pre.target
[Service]
ExecStart=
ExecStart=-/sbin/agetty --autologin root --noclear --keep-baud 115200,57600,38400,9600 %I $TERM
EOF
# Ensure standard runtime directories exist with expected permissions.
install -d -m 0755 "${mount_dir}/dev" "${mount_dir}/proc" "${mount_dir}/sys" "${mount_dir}/run"
install -d -m 1777 "${mount_dir}/tmp"
}
# firecrab-helper's dnsmasq answers DNS on the bridge gateway itself
# (172.30.0.1) for every guest on the VPC subnet, so the guest points at
# that instead of a public resolver — matches how a NAT router's own
# address is typically handed out as the DNS server.
configure_guest_dns() {
write_root_file "${mount_dir}/etc/resolv.conf" <<'EOF'
nameserver 172.30.0.1
EOF
}
# Prints a fixed sentinel line to the serial console (ttyS0, which is
# Firecracker's captured stdout) once DHCP + DNS are confirmed working —
# the signal firecrab-api's start pipeline waits on in place of a guest
# agent event (public-docs/guides/networking.md; guest agent/vsock is
# out of this project's competition scope).
install_network_ready_sentinel() {
install -d -m 0755 "${mount_dir}/usr/local/sbin"
write_root_file "${mount_dir}/usr/local/sbin/firecrab-network-ready.sh" <<'EOF'
#!/bin/sh
set -eu
# network-online only orders unit *starts* — DHCP and DNS may still be
# unfinished. Poll for IPv4, repair a dangling systemd-resolved stub, then
# accept getent or dig@gateway (host dnsmasq).
ipv4=""
for _ in $(seq 1 15); do
ipv4=$(ip -4 -o addr show scope global 2>/dev/null | \
awk '$2 != "lo" { split($4, a, "/"); print a[1]; exit }')
if [ -n "$ipv4" ]; then
break
fi
sleep 1
done
if [ -z "$ipv4" ]; then
echo "FIRECRAB_NETWORK_FAILED no-ipv4-address"
exit 0
fi
gw=$(ip -4 route show default 2>/dev/null | awk '{print $3; exit}')
if [ -n "$gw" ] && [ ! -e /run/systemd/resolve/stub-resolv.conf ]; then
if [ -L /etc/resolv.conf ] || [ ! -s /etc/resolv.conf ]; then
rm -f /etc/resolv.conf
printf 'nameserver %s\n' "$gw" > /etc/resolv.conf
fi
fi
dns_ok() {
getent hosts example.com >/dev/null 2>&1 && return 0
if [ -n "$gw" ] && command -v dig >/dev/null 2>&1; then
ans=$(dig +short +time=2 +tries=1 @"$gw" example.com A 2>/dev/null || true)
[ -n "$ans" ] && return 0
fi
return 1
}
for _ in $(seq 1 15); do
if dns_ok; then
echo "FIRECRAB_NETWORK_READY $ipv4"
exit 0
fi
sleep 1
done
echo "FIRECRAB_NETWORK_FAILED dns-unreachable"
EOF
chmod 0755 "${mount_dir}/usr/local/sbin/firecrab-network-ready.sh"
write_root_file "${mount_dir}/etc/systemd/system/firecrab-network-ready.service" <<'EOF'
[Unit]
Description=Firecrab network readiness sentinel
After=network-online.target
Wants=network-online.target
[Service]
Type=oneshot
StandardOutput=tty
TTYPath=/dev/console
ExecStart=/usr/local/sbin/firecrab-network-ready.sh
[Install]
WantedBy=multi-user.target
EOF
install -d -m 0755 "${mount_dir}/etc/systemd/system/multi-user.target.wants"
ln -sf /etc/systemd/system/firecrab-network-ready.service \
"${mount_dir}/etc/systemd/system/multi-user.target.wants/firecrab-network-ready.service"
}
install_rootfs_packages() {
if [ -z "$rootfs_packages" ]; then
return
fi
info "installing rootfs packages: ${rootfs_packages}"
mount_chroot_fs
chroot "$mount_dir" env DEBIAN_FRONTEND=noninteractive apt-get update
# shellcheck disable=SC2086
chroot "$mount_dir" env DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends $rootfs_packages
chroot "$mount_dir" apt-get clean
rm -rf "${mount_dir}/var/lib/apt/lists/"*
cleanup_chroot_mounts
}
# True when the file is a raw Linux/arm64 Image: ARM64_IMAGE_MAGIC ("ARMd")
# at offset 0x38, the field Firecracker's loader checks before it jumps into
# the image. Read, never written — a wrapped kernel (a UKI or an EFI zboot
# image) with the magic stamped on top passes the loader and then dies
# silently in the guest instead of failing here.
is_arm64_image() {
[ -s "$1" ] && [ "$(od -An -tx1 -j56 -N4 "$1" | tr -d ' \n')" = '41524d64' ]
}
# Pulls the distro kernel out of the rootfs. x86_64 is converted to the
# uncompressed ELF vmlinux Firecracker expects. ARM64 is unwrapped to the raw
# Linux/arm64 Image, the only format Firecracker boots on that architecture:
# Ubuntu's arm64 vmlinuz is a UKI whose .linux section holds an EFI zboot
# image, so neither the file itself nor its first payload is bootable.
extract_kernel() {
vmlinuz_path=$(find "${mount_dir}/boot" -maxdepth 1 -name 'vmlinuz-*' | sort -V | tail -n 1)
if [ -z "$vmlinuz_path" ]; then
fail "linux-image-virtual did not install a vmlinuz under ${mount_dir}/boot"
fi
mkdir -p "$kernel_artifact_dir"
kernel_image_path="${kernel_artifact_dir}/${kernel_image_name}"
kernel_image_tmp="${kernel_image_path}.tmp"
if [ "$ubuntu_arch" = arm64 ]; then
info "extracting raw ARM64 Image from: ${vmlinuz_path}"
if ! "$extract_arm64_image" "$vmlinuz_path" >"$kernel_image_tmp" \
|| ! is_arm64_image "$kernel_image_tmp"; then
rm -f "$kernel_image_tmp"
fail "extract-arm64-image could not extract a raw ARM64 Image from ${vmlinuz_path}"
fi
else
info "extracting ELF vmlinux from: ${vmlinuz_path}"
if ! "$extract_vmlinux" "$vmlinuz_path" >"$kernel_image_tmp"; then
rm -f "$kernel_image_tmp"
fail "extract-vmlinux could not extract an ELF vmlinux from ${vmlinuz_path}"
fi
if ! file "$kernel_image_tmp" | grep -q 'ELF'; then
rm -f "$kernel_image_tmp"
fail "extracted kernel is not an ELF image: ${vmlinuz_path}"
fi
fi
chmod 0644 "$kernel_image_tmp"
mv "$kernel_image_tmp" "$kernel_image_path"
info "Ubuntu kernel image: ${kernel_image_path}"
}
verify_rootfs_content() {
if [ ! -e "${mount_dir}/etc/os-release" ]; then
fail "Ubuntu Base extraction did not create /etc/os-release in ${rootfs_image}"
fi
if [ ! -e "${mount_dir}/bin/sh" ]; then
fail "Ubuntu Base extraction did not create /bin/sh in ${rootfs_image}"
fi
if [ ! -e "${mount_dir}/sbin/init" ]; then
fail "Rootfs did not install /sbin/init. Check packages: ${rootfs_packages}"
fi
if [ ! -e "${mount_dir}/sbin/reboot" ] && [ ! -e "${mount_dir}/usr/sbin/reboot" ]; then
fail "Rootfs did not install reboot. Check packages: ${rootfs_packages}"
fi
if [ ! -e "${mount_dir}/sbin/agetty" ] && [ ! -e "${mount_dir}/usr/sbin/agetty" ]; then
fail "Rootfs did not install agetty. Check packages: ${rootfs_packages}"
fi
if [ ! -e "${mount_dir}/usr/sbin/sshd" ]; then
fail "Rootfs did not install sshd. Check packages: ${rootfs_packages}"
fi
if [ ! -e "${mount_dir}/etc/systemd/network/10-eth0.network" ]; then
fail 'Rootfs did not create /etc/systemd/network/10-eth0.network.'
fi
networkd_service_link="${mount_dir}/etc/systemd/system/multi-user.target.wants/systemd-networkd.service"
if [ ! -L "${networkd_service_link}" ]; then
fail 'Rootfs did not enable systemd-networkd.service.'
fi
networkd_service_target=$(readlink "${networkd_service_link}")
if [ "${networkd_service_target}" != '/lib/systemd/system/systemd-networkd.service' ]; then
fail 'Rootfs did not enable systemd-networkd.service.'
fi
if [ ! -e "${mount_dir}${networkd_service_target}" ]; then
fail 'Rootfs did not enable systemd-networkd.service.'
fi
if [ ! -x "${mount_dir}/usr/local/sbin/firecrab-network-ready.sh" ]; then
fail 'Rootfs did not install an executable firecrab-network-ready.sh.'
fi
if [ ! -e "${mount_dir}/etc/systemd/system/firecrab-network-ready.service" ]; then
fail 'Rootfs did not install firecrab-network-ready.service.'
fi
if [ ! -L "${mount_dir}/etc/systemd/system/multi-user.target.wants/firecrab-network-ready.service" ]; then
fail 'Rootfs did not enable firecrab-network-ready.service.'
fi
if [ ! -e "${mount_dir}/bin/udevadm" ] && [ ! -e "${mount_dir}/usr/bin/udevadm" ] &&
[ ! -e "${mount_dir}/sbin/udevadm" ] && [ ! -e "${mount_dir}/usr/sbin/udevadm" ]; then
fail "Rootfs did not install udevadm. Check packages: ${rootfs_packages}"
fi
if [ ! -e "${mount_dir}/bin/kmod" ] && [ ! -e "${mount_dir}/usr/bin/kmod" ]; then
fail "Rootfs did not install kmod. Check packages: ${rootfs_packages}"
fi
if [ ! -e "${mount_dir}/sbin/ip" ] && [ ! -e "${mount_dir}/usr/sbin/ip" ] &&
[ ! -e "${mount_dir}/bin/ip" ] && [ ! -e "${mount_dir}/usr/bin/ip" ]; then
fail "Rootfs did not install ip. Check packages: ${rootfs_packages}"
fi
if [ ! -e "${mount_dir}/bin/ping" ] && [ ! -e "${mount_dir}/usr/bin/ping" ]; then
fail "Rootfs did not install ping. Check packages: ${rootfs_packages}"
fi
kernel_image_path="${kernel_artifact_dir}/${kernel_image_name}"
if [ ! -s "$kernel_image_path" ]; then
fail "extract_kernel did not produce a kernel image: ${kernel_image_path}"
fi
case "$ubuntu_arch" in
amd64)
file "$kernel_image_path" | grep -q 'ELF' \
|| fail "Extracted kernel image is not ELF: ${kernel_image_path}"
;;
arm64)
is_arm64_image "$kernel_image_path" \
|| fail "ARM64 kernel is not a raw Image: ${kernel_image_path}"
;;
esac
}
main() {
if [ "$#" -ne 0 ]; then
fail 'install-ubuntu-roofs.sh does not accept arguments.'
fi
# Check local tools required to download, verify, stage, and create the rootfs image.
require_command chmod
require_command chown
require_command chroot
require_command cp
require_command curl
require_command file
require_command find
require_command grep
require_command install
require_command ln
require_command mkdir
require_command mkfs.ext4
require_command mount
require_command mv
require_command rm
require_command sed
require_command sha256sum
require_command sort
require_command tail
require_command tar
require_command truncate
require_command uname
require_command umount
require_command od
if [ ! -x "$extract_vmlinux" ]; then
fail "extract-vmlinux helper not found or not executable: ${extract_vmlinux}"
fi
if [ ! -x "$extract_arm64_image" ]; then
fail "extract-arm64-image helper not found or not executable: ${extract_arm64_image}"
fi
if [ "$(id -u)" -ne 0 ]; then
require_command sudo
exec sudo env \
"M2IMAGE_ARCH=${M2IMAGE_ARCH:-}" \
"M2IMAGE_DISTRO_SERIES=${ubuntu_series_setting}" \
"M2IMAGE_DISTRO_VERSION=${M2IMAGE_DISTRO_VERSION:-}" \
"M2IMAGE_ALIAS=${m2image_alias}" \
"M2IMAGE_SBOM_OUTPUT=${sbom_output}" \
"$script_path"
fi
build_dir=$(abs_dir "$build_dir")
artifact_dir=$(abs_dir "$artifact_dir")
ubuntu_arch=$(detect_ubuntu_arch)
ubuntu_series=$(resolve_ubuntu_series)
case "$ubuntu_arch" in
amd64) kernel_image_name="vmlinux-ubuntu-${ubuntu_series}-x86_64" ;;
arm64) kernel_image_name="Image-ubuntu-${ubuntu_series}-aarch64" ;;
esac
if [ -z "$rootfs_image" ]; then
rootfs_image="${artifact_dir}/ubuntu-rootfs-${ubuntu_series}-${ubuntu_arch}.ext4"
else
rootfs_image=$(abs_file "$rootfs_image")
fi
if [ -z "$rootfs_link" ]; then
rootfs_link="${artifact_dir}/ubuntu-rootfs.ext4"
else
rootfs_link=$(abs_file "$rootfs_link")
fi
archive_url=$(resolve_ubuntu_base_archive "$ubuntu_series")
archive_name=${archive_url##*/}
download_dir="${build_dir}/downloads"
archive_path="${download_dir}/${archive_name}"
mount_dir="${build_dir}/mnt"
info "Ubuntu series: ${ubuntu_series}"
info "Ubuntu architecture: ${ubuntu_arch}"
info "rootfs versioned image: ${rootfs_image}"
info "rootfs symlink: ${rootfs_link}"
info "rootfs size: ${rootfs_size}"
if [ -n "$rootfs_packages" ]; then
info "rootfs packages: ${rootfs_packages}"
fi
check_rootfs_output
# Download the Ubuntu Base rootfs tarball and verify it with the official checksum file.
mkdir -p "$download_dir"
download_archive
fetch_checksum
verify_checksum
# Extract Ubuntu Base into a staging directory before building the ext4 image.
prepare_rootfs_image
rm -rf "$mount_dir"
mkdir -p "$mount_dir"
info 'extracting Ubuntu Base into rootfs image'
tar --numeric-owner -xpf "$archive_path" -C "$mount_dir"
configure_rootfs
install_rootfs_packages
extract_kernel
configure_guest_dns
verify_rootfs_content
if [ -n "$sbom_output" ]; then
[ -n "$m2image_alias" ] || fail 'M2IMAGE_ALIAS is required with M2IMAGE_SBOM_OUTPUT'
require_command python3
[ -f "$mount_dir/var/lib/dpkg/status" ] \
|| fail 'Ubuntu dpkg status database missing from built staging root'
case "$ubuntu_arch" in
amd64) sbom_arch=x86_64 ;;
arm64) sbom_arch=aarch64 ;;
esac
python3 "$sbom_generator" \
--format dpkg --distribution ubuntu \
--image-alias "$m2image_alias" --image-version "$ubuntu_series" \
--architecture "$sbom_arch" \
--package-db "$mount_dir/var/lib/dpkg/status" --output "$sbom_output"
if [ -n "${SUDO_UID:-}" ] && [ -n "${SUDO_GID:-}" ]; then
chown "${SUDO_UID}:${SUDO_GID}" "$sbom_output"
fi
fi
info "creating Ubuntu rootfs image: ${rootfs_image}"
# Disable orphan_file when the host's e2fsprogs supports it so images built
# by 1.47 remain readable on hosts/guests that still ship e2fsprogs 1.46.
if grep -qw orphan_file /etc/mke2fs.conf 2>/dev/null; then
mkfs.ext4 -F -O '^orphan_file' -L rootfs -d "$mount_dir" "$rootfs_image" >/dev/null
else
mkfs.ext4 -F -L rootfs -d "$mount_dir" "$rootfs_image" >/dev/null
fi
rootfs_link_target=$(update_symlink "$rootfs_image" "$rootfs_link")
restore_output_ownership
info "Ubuntu rootfs image created: ${rootfs_image}"
print_rootfs_outputs
}
main "$@"