#!/usr/bin/env bash # ============================================================================ # qcrows-pack — Create a QCrows VM container image archive # # Packages rootfs, initrd, kernel, metadata, menu entry, and build info # into a self-describing .qcrows tar archive for Kata Containers. # # Usage: # qcrows-pack --rootfs rootfs.tar.gz --initrd initrd.img [options] -o image.qcrows # qcrows-pack --from-dir ./build-output/ [options] -o image.qcrows # # See: qcrows-spec.md for the full format specification. # ============================================================================ set -euo pipefail # ─── Version ──────────────────────────────────────────────────────────────── QCROWS_SPEC_VERSION="0.2.0" VERSION="0.2.0" # ─── Colors ───────────────────────────────────────────────────────────────── RED='\033[0;31m' GREEN='\033[0;32m' YELLOW='\033[1;33m' CYAN='\033[0;36m' NC='\033[0m' log() { echo -e "${GREEN}[qcrows]${NC} $*"; } warn() { echo -e "${YELLOW}[qcrows]${NC} WARNING: $*"; } error() { echo -e "${RED}[qcrows]${NC} ERROR: $*" >&2; } die() { error "$@"; exit 1; } # ─── Defaults ─────────────────────────────────────────────────────────────── ROOTFS="" INITRD="" KERNEL="" KERNEL_CONFIG="" BOOT_PARAMS="" METADATA_FILE="" MENU_FILE="" BUILD_FILE="" SPEC_FILE="" FIRMWARE_DIR="" DEVICE_TREE_DIR="" OUTPUT="" FROM_DIR="" NAME="" VERSION_IMG="" DESCRIPTION="" ARCH="" HYPERVISORS="" BUILD_SYSTEM="" DRY_RUN=false COMPRESS=true # ─── Temp directory ──────────────────────────────────────────────────────── TMPDIR="" cleanup() { [[ -n "$TMPDIR" && -d "$TMPDIR" ]] && rm -rf "$TMPDIR" } trap cleanup EXIT # ─── Helpers ──────────────────────────────────────────────────────────────── # Return the first existing file from a list of candidate paths. # Handles glob patterns by expanding them and taking the first match. first_existing_file() { local candidate match for candidate in "$@"; do for match in $candidate; do if [[ -f "$match" ]]; then echo "$match" return 0 fi done done return 1 } # ─── Usage ────────────────────────────────────────────────────────────────── usage() { cat </dev/null || continue ;; rootfs_tar) [[ -n "$file" && -f "$file" ]] || continue tar -tzf "$file" 2>/dev/null | grep -q "$token" || continue ;; esac BUILD_SYSTEM="$system" log "Detected build system: ${label}" return 0 done BUILD_SYSTEM="custom" warn "Could not detect build system. Use --build-system to specify." } # ─── Architecture detection ───────────────────────────────────────────────── detect_arch() { [[ -n "$ARCH" ]] && return local host_arch host_arch="$(uname -m)" case "$host_arch" in x86_64|amd64) ARCH="x86_64" ;; aarch64|arm64) ARCH="aarch64" ;; riscv64) ARCH="riscv64" ;; s390x) ARCH="s390x" ;; ppc64le) ARCH="ppc64le" ;; *) ARCH="$host_arch" ;; esac log "Detected architecture: ${ARCH}" } # ─── Type detection helpers ───────────────────────────────────────────────── detect_rootfs_type() { case "$1" in *.tar.gz|*.tgz) echo "tar-gzip" ;; *.tar.xz|*.txz) echo "tar-xz" ;; *.tar.zst) echo "tar-zst" ;; *) echo "tar-gzip" ;; esac } detect_initrd_type() { case "$1" in *.cpio.gz|*.img) echo "cpio-gzip" ;; *.cpio.lz4) echo "cpio-lz4" ;; *.cpio.xz) echo "cpio-xz" ;; *.cramfs) echo "cramfs" ;; *) if file "$1" 2>/dev/null | grep -q "gzip"; then echo "cpio-gzip" elif file "$1" 2>/dev/null | grep -q "LZ4"; then echo "cpio-lz4" elif file "$1" 2>/dev/null | grep -q "XZ"; then echo "cpio-xz" else echo "cpio-gzip" fi ;; esac } detect_kernel_version() { local kernel_file="$1" [[ -z "$kernel_file" || ! -f "$kernel_file" ]] && { echo "unknown"; return; } # Extract version string from the kernel binary local ver ver=$(strings "$kernel_file" 2>/dev/null | grep -oP '^\d+\.\d+\.\d+' | head -1 || true) [[ -n "$ver" ]] && { echo "$ver"; return; } # Fallback: extract from kernel config if [[ -n "$KERNEL_CONFIG" && -f "$KERNEL_CONFIG" ]]; then local kv kv=$(grep "^CONFIG_VERSION_SIGNATURE=" "$KERNEL_CONFIG" 2>/dev/null | head -1 | grep -oP '\d+\.\d+\.\d+' | head -1 || true) [[ -n "$kv" ]] && { echo "$kv"; return; } fi echo "unknown" } # ─── Generate metadata.toml ──────────────────────────────────────────────── generate_metadata() { local rootfs_name rootfs_name="$(basename "$ROOTFS")" local rootfs_type rootfs_type="$(detect_rootfs_type "$ROOTFS")" local rootfs_size rootfs_size=$(( $(stat --format="%s" "$ROOTFS" 2>/dev/null || stat -f "%z" "$ROOTFS" 2>/dev/null || echo 0) / (1024 * 1024) )) local initrd_included="false" local initrd_type="" local initrd_path="" if [[ -n "$INITRD" ]]; then initrd_included="true" initrd_type="$(detect_initrd_type "$INITRD")" initrd_path="initrd$(echo "$INITRD" | grep -oE '\.[^.]*$' || echo ".img")" fi local kernel_included="false" local kernel_version="unknown" local kernel_path="" local kernel_format="vmlinuz" local kernel_size_mb=0 if [[ -n "$KERNEL" ]]; then kernel_included="true" kernel_version="$(detect_kernel_version "$KERNEL")" # Normalize kernel format based on filename case "$(basename "$KERNEL")" in vmlinux*) kernel_format="vmlinux"; kernel_path="kernel/vmlinux" ;; *) kernel_format="vmlinuz"; kernel_path="kernel/vmlinuz" ;; esac kernel_size_mb=$(( $(stat --format="%s" "$KERNEL" 2>/dev/null || stat -f "%z" "$KERNEL" 2>/dev/null || echo 0) / (1024 * 1024) )) fi local img_name="${NAME:-$(basename "$ROOTFS" | sed 's/\.(tar\.[a-z]*\|tgz)$//')}" local img_version="${VERSION_IMG:-1.0.0}" local img_desc="${DESCRIPTION:-QCrows VM container image built with ${BUILD_SYSTEM}}" local hypervisor_list="${HYPERVISORS:-qemu,cloud-hypervisor}" # Convert comma-separated hypervisors to TOML array local hypervisor_toml hypervisor_toml=$(echo "$hypervisor_list" | tr ',' '\n' | sed 's/^/"/;s/$/",/' | tr '\n' ' ' | sed 's/, *$//') cat </dev/null || true log "Added firmware directory" fi # ── 7. Copy device-tree ───────────────────────────────────────────── if [[ -n "$DEVICE_TREE_DIR" && -d "$DEVICE_TREE_DIR" ]]; then mkdir -p "${staging}/device-tree" cp -r "${DEVICE_TREE_DIR}/"* "${staging}/device-tree/" 2>/dev/null || true log "Added device-tree directory" fi # ── 8. Generate or copy metadata.toml ─────────────────────────────── if [[ -n "$METADATA_FILE" ]]; then cp "$METADATA_FILE" "${staging}/metadata.toml" log "Using provided metadata.toml" else generate_metadata > "${staging}/metadata.toml" log "Generated metadata.toml" fi # ── 9. Generate or copy menu.toml ─────────────────────────────────── if [[ -n "$MENU_FILE" ]]; then cp "$MENU_FILE" "${staging}/menu.toml" log "Using provided menu.toml" else generate_menu > "${staging}/menu.toml" log "Generated menu.toml" fi # ── 10. Generate or copy build.toml ───────────────────────────────── if [[ -n "$BUILD_FILE" ]]; then cp "$BUILD_FILE" "${staging}/build.toml" log "Using provided build.toml" else generate_build > "${staging}/build.toml" log "Generated build.toml" fi # ── 11. Copy spec.md ──────────────────────────────────────────────── [[ -n "$SPEC_FILE" ]] && { cp "$SPEC_FILE" "${staging}/spec.md"; log "Added spec.md"; } # ── 12. Compute hashes ────────────────────────────────────────────── compute_hashes "$staging" > "${staging}/hashes.sha256" log "Generated hashes.sha256" # ── Dry run — direct pipeline, no subshell loop ───────────────────── if [[ "$DRY_RUN" == "true" ]]; then echo "" log "=== DRY RUN — Would include the following files ===" (cd "$staging" && find . -type f -print0 | sort -z | xargs -0 -I{} du -h "{}" | sed "s|\t\./| |") echo "" log "Total size: $(du -sh "$staging" | cut -f1)" return fi # ── 13. Create archive ───────────────────────────────────────────── local output_file="$OUTPUT" if [[ "$COMPRESS" == "true" ]]; then [[ ! "$output_file" == *.gz ]] && output_file="${output_file}.gz" log "Creating compressed archive: ${output_file}" (cd "$staging" && tar czf - ./*) > "$output_file" else log "Creating uncompressed archive: ${output_file}" (cd "$staging" && tar cf - ./*) > "$output_file" fi local final_size final_size="$(du -sh "$output_file" | cut -f1)" log "Archive created: ${output_file} (${final_size})" # ── 14. Summary ───────────────────────────────────────────────────── echo "" echo -e "${GREEN}╔══════════════════════════════════════════════╗${NC}" echo -e "${GREEN}║ QCrows image packed successfully ║${NC}" echo -e "${GREEN}╚══════════════════════════════════════════════╝${NC}" echo "" echo -e " Output: ${CYAN}${output_file}${NC}" echo -e " Size: ${CYAN}${final_size}${NC}" echo -e " Components: ${CYAN}$(find "$staging" -type f ! -name hashes.sha256 | wc -l) files${NC}" echo -e " Build: ${CYAN}${BUILD_SYSTEM}${NC}" echo -e " Arch: ${CYAN}${ARCH}${NC}" echo "" echo -e " Verify: ${CYAN}qcrows-verify ${output_file}${NC}" echo -e " Inspect: ${CYAN}qcrows-inspect ${output_file}${NC}" echo -e " Export: ${CYAN}qcrows-export --format qcow2 ${output_file} -o disk.qcow2${NC}" echo -e " ${CYAN}qcrows-export --format iso ${output_file} -o live.iso${NC}" echo "" } # ─── Entry Point ──────────────────────────────────────────────────────────── parse_args "$@" validate_inputs detect_arch # Discover from directory if specified [[ -n "$FROM_DIR" ]] && discover_from_dir "$FROM_DIR" do_pack