deb-bootc-image-builder/scripts/build-scripts/bootc-image-builder.sh
robojerk 126ee1a849
Some checks failed
particle-os CI / Test particle-os (push) Failing after 1s
particle-os CI / Integration Test (push) Has been skipped
particle-os CI / Security & Quality (push) Failing after 1s
Test particle-os Basic Functionality / test-basic (push) Failing after 1s
particle-os CI / Build and Release (push) Has been skipped
cleanup
2025-08-27 12:30:24 -07:00

432 lines
13 KiB
Bash
Executable file

#!/bin/bash
# deb-bootc-image-builder - Simplified container to disk image converter
# This is a simplified fork of the original Fedora bootc-image-builder
# that uses native Debian tools instead of osbuild
set -euo pipefail
# System tool paths
QEMU_IMG="/usr/bin/qemu-img"
RSYNC="/usr/bin/rsync"
TAR="/bin/tar"
MKDIR="/bin/mkdir"
CP="/bin/cp"
LOSETUP="/usr/sbin/losetup"
PARTED="/usr/sbin/parted"
MOUNT="/bin/mount"
UMOUNT="/bin/umount"
BOOTUPD="/usr/libexec/bootupd"
GRUB_INSTALL="/usr/sbin/grub-install"
GRUB_MKCONFIG="/usr/sbin/grub-mkconfig"
# Default values
CONTAINER_IMAGE=""
OUTPUT_FORMAT="qcow2"
OUTPUT_DIR="/output"
STORE_DIR="/store"
CONTAINER_STORAGE="/var/lib/containers/storage"
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Global variables for cleanup
WORK_DIR=""
LOOP_DEV=""
MOUNT_POINT=""
CONTAINER_FS_DIR=""
DISK_RAW=""
# Logging functions
log_info() {
echo -e "${GREEN}[INFO]${NC} $1"
}
log_warn() {
echo -e "${YELLOW}[WARN]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Help function
show_help() {
cat << EOF
Simplified Debian bootc-image-builder
Usage: $0 [OPTIONS] <container-image>
Options:
-f, --format FORMAT Output format (qcow2, raw, img) [default: qcow2]
-o, --output DIR Output directory [default: /output]
-s, --store DIR Store directory [default: /store]
-h, --help Show this help message
Examples:
$0 simple-cli:latest
$0 -f raw -o /tmp/output simple-cli:latest
$0 --format qcow2 --output /output simple-cli:latest
EOF
}
# Parse command line arguments
while [[ $# -gt 0 ]]; do
case $1 in
-f|--format)
OUTPUT_FORMAT="$2"
shift 2
;;
-o|--output)
OUTPUT_DIR="$2"
shift 2
;;
-s|--store)
STORE_DIR="$2"
shift 2
;;
-h|--help)
show_help
exit 0
;;
-*)
log_error "Unknown option $1"
show_help
exit 1
;;
*)
CONTAINER_IMAGE="$1"
shift
;;
esac
done
# Validate inputs
if [[ -z "$CONTAINER_IMAGE" ]]; then
log_error "Container image is required"
show_help
exit 1
fi
if [[ ! -d "$OUTPUT_DIR" ]]; then
log_error "Output directory $OUTPUT_DIR does not exist"
exit 1
fi
# Validate output format
case "$OUTPUT_FORMAT" in
qcow2|raw|img)
;;
*)
log_error "Unsupported output format: $OUTPUT_FORMAT"
exit 1
;;
esac
log_info "Starting simplified bootc-image-builder"
log_info "Container image: $CONTAINER_IMAGE"
log_info "Output format: $OUTPUT_FORMAT"
log_info "Output directory: $OUTPUT_DIR"
# Create working directory in project directory
WORK_DIR="$(pwd)/work/bootc-builder-$(date +%s)"
mkdir -p "$WORK_DIR" || {
log_error "Failed to create working directory"
exit 1
}
log_info "Working directory: $WORK_DIR"
# Check available space
AVAILABLE_SPACE=$(df "$WORK_DIR" | awk 'NR==2 {print $4}')
if [[ "$AVAILABLE_SPACE" -lt 5000000 ]]; then # Less than 5GB
log_warn "Low disk space available: ${AVAILABLE_SPACE}KB"
log_warn "This might cause issues with large containers"
fi
# Signal handling for cleanup
cleanup_on_exit() {
local exit_code=$?
log_info "Cleaning up resources..."
# Unmount if mounted
if [[ -n "$MOUNT_POINT" && -d "$MOUNT_POINT" ]]; then
if mountpoint -q "$MOUNT_POINT" 2>/dev/null; then
log_info "Unmounting $MOUNT_POINT..."
sudo umount "$MOUNT_POINT" 2>/dev/null || true
fi
fi
# Detach loop device if exists
if [[ -n "$LOOP_DEV" && -b "$LOOP_DEV" ]]; then
log_info "Detaching loop device $LOOP_DEV..."
sudo losetup -d "$LOOP_DEV" 2>/dev/null || true
fi
# Clean up any remaining loop devices that might be ours
if [[ -n "$WORK_DIR" ]]; then
log_info "Checking for orphaned loop devices..."
for loop_dev in /dev/loop*; do
if [[ -b "$loop_dev" ]]; then
local mount_point
mount_point=$(losetup -l "$loop_dev" 2>/dev/null | grep -o ".*/work/bootc-builder-[^[:space:]]*" || true)
if [[ -n "$mount_point" ]]; then
log_info "Detaching orphaned loop device $loop_dev..."
sudo losetup -d "$loop_dev" 2>/dev/null || true
fi
fi
done
fi
# Remove working directory
if [[ -n "$WORK_DIR" && -d "$WORK_DIR" ]]; then
log_info "Removing working directory $WORK_DIR..."
sudo rm -rf "$WORK_DIR" 2>/dev/null || true
fi
# Remove any remaining temporary files
if [[ -n "$DISK_RAW" && -f "$DISK_RAW" ]]; then
log_info "Removing temporary disk image $DISK_RAW..."
sudo rm -f "$DISK_RAW" 2>/dev/null || true
fi
if [[ -n "$CONTAINER_FS_DIR" && -d "$CONTAINER_FS_DIR" ]]; then
log_info "Removing container filesystem directory $CONTAINER_FS_DIR..."
sudo rm -rf "$CONTAINER_FS_DIR" 2>/dev/null || true
fi
log_info "Cleanup completed"
exit "$exit_code"
}
# Set up signal handlers
trap cleanup_on_exit EXIT INT TERM
# Function to clean up any existing loop devices from previous runs
cleanup_existing_loop_devices() {
log_info "Checking for existing loop devices from previous runs..."
for loop_dev in /dev/loop*; do
if [[ -b "$loop_dev" ]]; then
local mount_point
mount_point=$(losetup -l "$loop_dev" 2>/dev/null | grep -o ".*/work/bootc-builder-[^[:space:]]*" || true)
if [[ -n "$mount_point" ]]; then
log_warn "Found existing loop device $loop_dev from previous run, cleaning up..."
# Try to unmount if mounted
if mountpoint -q "$mount_point" 2>/dev/null; then
sudo umount "$mount_point" 2>/dev/null || true
fi
# Detach the loop device
sudo losetup -d "$loop_dev" 2>/dev/null || true
# Remove the mount point directory
sudo rm -rf "$mount_point" 2>/dev/null || true
fi
fi
done
}
# Clean up any existing loop devices at startup
cleanup_existing_loop_devices
# Check if container image exists locally
log_info "Checking if container image exists locally: $CONTAINER_IMAGE"
if podman images --format "{{.Repository}}:{{.Tag}}" | grep -q "^$CONTAINER_IMAGE$"; then
log_info "Container image found locally, skipping pull"
elif podman images --format "{{.Repository}}:{{.Tag}}" | grep -q "simple-cli:latest"; then
log_info "Found simple-cli:latest locally, using that instead"
CONTAINER_IMAGE="simple-cli:latest"
elif podman images --format "{{.Repository}}:{{.Tag}}" | grep -q "localhost/simple-cli:latest"; then
log_info "Found localhost/simple-cli:latest locally, using that instead"
CONTAINER_IMAGE="localhost/simple-cli:latest"
else
log_info "Container image not found locally, attempting to pull: $CONTAINER_IMAGE"
podman pull "$CONTAINER_IMAGE" || {
log_error "Failed to pull container image. Please ensure the image is available."
log_error "You can either:"
log_error "1. Build the image locally first"
log_error "2. Use a fully qualified registry URL"
exit 1
}
fi
# Export the container to a tar file
log_info "Exporting container to tar..."
podman export "$(podman create --rm "$CONTAINER_IMAGE" true)" > "$WORK_DIR/container.tar"
# Create a loopback device and mount the container
log_info "Setting up loopback device..."
cd "$WORK_DIR"
# Extract the container
log_info "Extracting container filesystem..."
CONTAINER_FS_DIR="$WORK_DIR/container-fs"
mkdir -p "$CONTAINER_FS_DIR"
if ! tar -xf container.tar -C "$CONTAINER_FS_DIR"; then
log_error "Failed to extract container. This might be due to:"
log_error "1. Insufficient disk space"
log_error "2. Corrupted container archive"
log_error "3. Permission issues"
exit 1
fi
# Create disk image using qemu-img (no sudo required)
log_info "Creating disk image..."
IMAGE_SIZE="8G" # Increased size for enhanced simple-cli with Bazzite features
# Create a proper disk image with partitions for bootloader installation
log_info "Creating partitioned disk image for bootloader installation..."
# Create raw image file
DISK_RAW="$WORK_DIR/disk.raw"
$QEMU_IMG create -f raw "$DISK_RAW" "$IMAGE_SIZE"
# For bootloader installation, we need to create a proper disk structure
# This requires sudo for partitioning and mounting
log_info "Setting up disk partitions for bootloader installation..."
log_info "Note: This step requires sudo for disk operations"
# Create partition table and filesystem
log_info "Creating partition table and filesystem..."
sudo $PARTED "$DISK_RAW" mklabel msdos
sudo $PARTED "$DISK_RAW" mkpart primary ext4 1MiB 100%
sudo $PARTED "$DISK_RAW" set 1 boot on
# Setup loopback device
log_info "Setting up loopback device..."
LOOP_DEV=$(sudo $LOSETUP --find --show "$DISK_RAW")
log_info "Using loopback device: $LOOP_DEV"
# Force kernel to reread partition table
log_info "Rereading partition table..."
sudo partprobe "$LOOP_DEV" || true
# Get the partition device
PART_DEV="${LOOP_DEV}p1"
log_info "Partition device: $PART_DEV"
# Wait for partition to be available and verify it exists
log_info "Waiting for partition to be available..."
for i in {1..10}; do
if [[ -b "$PART_DEV" ]]; then
log_info "Partition device found: $PART_DEV"
break
fi
log_info "Waiting for partition device... (attempt $i/10)"
sleep 1
done
if [[ ! -b "$PART_DEV" ]]; then
log_error "Partition device not found: $PART_DEV"
log_error "Available devices:"
ls -la /dev/loop* || true
exit 1
fi
# Create filesystem
log_info "Creating ext4 filesystem..."
sudo mkfs.ext4 "$PART_DEV"
# Mount the partition
MOUNT_POINT="$WORK_DIR/mount-point"
mkdir -p "$MOUNT_POINT"
sudo $MOUNT "$PART_DEV" "$MOUNT_POINT"
# Copy container filesystem
log_info "Copying container filesystem to disk image..."
sudo rsync -a "$CONTAINER_FS_DIR/" "$MOUNT_POINT/"
# Install bootloader manually since deb-bootupd doesn't support non-default sysroots
log_info "Installing bootloader manually..."
if [[ -x "$GRUB_INSTALL" ]]; then
log_info "Found grub-install, installing GRUB bootloader..."
# Install GRUB to the mounted filesystem
# Use --root-directory to specify the mount point
# Use --target=i386-pc for BIOS systems
# Use --boot-directory to specify where boot files are located
log_info "Installing GRUB to $MOUNT_POINT..."
sudo "$GRUB_INSTALL" \
--root-directory="$MOUNT_POINT" \
--target=i386-pc \
--boot-directory="$MOUNT_POINT/boot" \
--force \
"$LOOP_DEV" || {
log_warn "GRUB installation failed, continuing without bootloader"
}
# Generate GRUB configuration manually since grub-mkconfig needs host environment
log_info "Generating GRUB configuration manually..."
# Find the actual kernel and initrd files in the container
KERNEL_FILE=$(find "$MOUNT_POINT/boot" -name "vmlinuz-*" | head -1)
INITRD_FILE=$(find "$MOUNT_POINT/boot" -name "initrd.img-*" | head -1)
if [[ -z "$KERNEL_FILE" ]] || [[ -z "$INITRD_FILE" ]]; then
log_error "Kernel or initrd not found in container"
log_error "Available files in /boot:"
ls -la "$MOUNT_POINT/boot/" || true
exit 1
fi
# Extract just the filename without the full path
KERNEL_NAME=$(basename "$KERNEL_FILE")
INITRD_NAME=$(basename "$INITRD_FILE")
log_info "Found kernel: $KERNEL_NAME"
log_info "Found initrd: $INITRD_NAME"
# Create a basic GRUB configuration for the container
cat > "$MOUNT_POINT/boot/grub/grub.cfg" << EOF
# GRUB configuration for simple-cli container
set timeout=1
set default=0
menuentry "Simple CLI" {
set root=(hd0,msdos1)
linux /boot/$KERNEL_NAME root=/dev/sda1 rw console=ttyS0 quiet splash fastboot
initrd /boot/$INITRD_NAME
}
menuentry "Simple CLI (Recovery)" {
set root=(hd0,msdos1)
linux /boot/$KERNEL_NAME root=/dev/sda1 rw single console=ttyS0
initrd /boot/$INITRD_NAME
}
EOF
log_info "GRUB configuration created at $MOUNT_POINT/boot/grub/grub.cfg"
else
log_warn "grub-install not found at $GRUB_INSTALL, skipping bootloader installation"
log_warn "Image will not be bootable without bootloader"
fi
# Unmount
sudo $UMOUNT "$MOUNT_POINT"
# Detach loopback device
sudo $LOSETUP -d "$LOOP_DEV"
# Convert to requested format
log_info "Converting to $OUTPUT_FORMAT format..."
case "$OUTPUT_FORMAT" in
qcow2)
$QEMU_IMG convert -f raw -O qcow2 "$DISK_RAW" "$OUTPUT_DIR/$(basename "$CONTAINER_IMAGE" | tr ':' '_').qcow2"
;;
raw)
cp "$DISK_RAW" "$OUTPUT_DIR/$(basename "$CONTAINER_IMAGE" | tr ':' '_').raw"
;;
img)
cp "$DISK_RAW" "$OUTPUT_DIR/$(basename "$CONTAINER_IMAGE" | tr ':' '_').img"
;;
esac
log_info "Image creation completed successfully!"
log_info "Output file: $OUTPUT_DIR/$(basename "$CONTAINER_IMAGE" | tr ':' '_').$OUTPUT_FORMAT"
# List output files
log_info "Output directory contents:"
ls -la "$OUTPUT_DIR"