#!/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--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" </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 "$@"