cockpit-kata/qcrows-pack

699 lines
26 KiB
Bash
Executable File

#!/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 <<EOF
qcrows-pack v${VERSION} — Create a QCrows VM container image
USAGE:
qcrows-pack [OPTIONS] -o OUTPUT
INPUT MODES (one required):
--from-dir DIR Discover components from a build output directory
--rootfs FILE Path to rootfs tar archive (required unless --from-dir)
REQUIRED (unless --from-dir):
--rootfs FILE Path to rootfs tar.gz / tar.xz / tar.zst
-o, --output FILE Output .qcrows file path
OPTIONAL COMPONENTS:
--initrd FILE Path to initrd/initramfs/cramfs image
--kernel FILE Path to guest kernel (vmlinuz or vmlinux)
--kernel-config FILE Path to kernel .config file
--boot-params FILE Path to boot-params.conf
--firmware DIR Directory of firmware blobs to include
--device-tree DIR Directory of .dtb files to include
METADATA (generated if not provided):
--metadata FILE Path to metadata.toml (overrides generation)
--menu FILE Path to menu.toml (overrides generation)
--build FILE Path to build.toml (overrides generation)
--spec FILE Path to spec.md build guide
IMAGE METADATA (used for generated metadata.toml):
--name NAME Image name (e.g., "alpine-3.20-kata")
--version VER Image version (e.g., "3.20.1")
--description DESC One-line description
--arch ARCH Target architecture (default: detected)
--hypervisors LIST Comma-separated hypervisor list (default: "qemu,cloud-hypervisor")
--build-system SYS Build system: gentoo|sourcemage|buildroot|lunar|lede|btc|sorcery|custom
OPTIONS:
--no-compress Do not gzip the output tar archive
--dry-run Show what would be included without creating the archive
-h, --help Show this help
-v, --version Show version
BUILD SYSTEM DETECTION (--from-dir):
When using --from-dir, qcrows-pack detects the build system
from the directory structure:
- Buildroot: .config with BR2_* symbols
- LEDE/OpenWrt: .config with CONFIG_TARGET_* symbols
- Gentoo: /etc/portage/ in the rootfs
- Source Mage: /var/state/sorcery/ in the rootfs
- Lunar: /var/state/lunar/ in the rootfs
EXAMPLES:
# From individual files
qcrows-pack \\
--rootfs ./output/rootfs.tar.gz \\
--initrd ./output/initrd.img \\
--kernel ./output/bzImage \\
--kernel-config ./output/.config \\
--name "alpine-3.20-kata" --version "3.20.1" \\
--build-system buildroot \\
--hypervisors "qemu,cloud-hypervisor" \\
-o alpine-3.20-kata.qcrows
# From a Buildroot output directory
qcrows-pack --from-dir ./buildroot/output/ \\
--name "buildroot-kata" --version "2024.02" \\
--build-system buildroot \\
-o buildroot-kata.qcrows
# With pre-written metadata files
qcrows-pack \\
--rootfs ./rootfs.tar.gz --initrd ./initrd.img \\
--metadata ./metadata.toml --menu ./menu.toml --build ./build.toml \\
-o custom-kata.qcrows
EOF
}
# ─── Parse Arguments ────────────────────────────────────────────────────────
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--rootfs) ROOTFS="$2"; shift 2 ;;
--initrd) INITRD="$2"; shift 2 ;;
--kernel) KERNEL="$2"; shift 2 ;;
--kernel-config) KERNEL_CONFIG="$2"; shift 2 ;;
--boot-params) BOOT_PARAMS="$2"; shift 2 ;;
--firmware) FIRMWARE_DIR="$2"; shift 2 ;;
--device-tree) DEVICE_TREE_DIR="$2"; shift 2 ;;
--metadata) METADATA_FILE="$2"; shift 2 ;;
--menu) MENU_FILE="$2"; shift 2 ;;
--build) BUILD_FILE="$2"; shift 2 ;;
--spec) SPEC_FILE="$2"; shift 2 ;;
--from-dir) FROM_DIR="$2"; shift 2 ;;
--name) NAME="$2"; shift 2 ;;
--version) VERSION_IMG="$2"; shift 2 ;;
--description) DESCRIPTION="$2"; shift 2 ;;
--arch) ARCH="$2"; shift 2 ;;
--hypervisors) HYPERVISORS="$2"; shift 2 ;;
--build-system) BUILD_SYSTEM="$2"; shift 2 ;;
--output|-o) OUTPUT="$2"; shift 2 ;;
--no-compress) COMPRESS=false; shift ;;
--dry-run) DRY_RUN=true; shift ;;
--help|-h) usage; exit 0 ;;
--version|-v) echo "qcrows-pack v${VERSION}"; exit 0 ;;
*) die "Unknown argument: $1 (use --help)" ;;
esac
done
}
# ─── Validation (array-driven) ──────────────────────────────────────────────
validate_inputs() {
[[ -z "$OUTPUT" ]] && die "Output file is required (--output or -o)"
[[ -z "$FROM_DIR" && -z "$ROOTFS" ]] && die "Either --rootfs or --from-dir is required"
[[ -z "$FROM_DIR" && -z "$KERNEL" ]] && die "--kernel is required (QCrows v0.2+ requires a bundled kernel). Use --from-dir for discovery."
[[ -n "$FROM_DIR" && ! -d "$FROM_DIR" ]] && die "Directory not found: $FROM_DIR"
# Array-driven file existence checks: "variable_path:label"
local -a file_checks=(
"ROOTFS:rootfs"
"INITRD:initrd"
"KERNEL:kernel"
"KERNEL_CONFIG:kernel config"
"BOOT_PARAMS:boot params"
"METADATA_FILE:metadata"
"MENU_FILE:menu"
"BUILD_FILE:build"
"SPEC_FILE:spec"
)
local entry var_path label val
for entry in "${file_checks[@]}"; do
var_path="${entry%%:*}"
label="${entry##*:}"
val="${!var_path:-}"
[[ -n "$val" && ! -f "$val" ]] && die "${label} file not found: ${val}"
done
# QCrows v0.2: kernel config is required when kernel is provided
if [[ -z "$FROM_DIR" && -n "$KERNEL" && -z "$KERNEL_CONFIG" ]]; then
warn "No --kernel-config provided — kernel/config is required in QCrows v0.2. Will attempt discovery."
fi
}
# ─── Discovery from build directory ─────────────────────────────────────────
discover_from_dir() {
local dir="$1"
log "Discovering components from: ${dir}"
# Rootfs — first match from candidate paths
ROOTFS="$(first_existing_file \
"${dir}/images/rootfs.tar.gz" \
"${dir}/images/rootfs.tar.xz" \
"${dir}/images/rootfs.cpio.gz" \
"${dir}/rootfs.tar.gz" \
"${dir}/rootfs.tar.xz" \
"${dir}/rootfs.cpio.gz" \
"${dir}/target/rootfs.tar.gz" \
|| true)"
[[ -n "$ROOTFS" ]] && log "Found rootfs: ${ROOTFS}" || die "Could not find rootfs in ${dir}. Use --rootfs to specify manually."
# Initrd
INITRD="$(first_existing_file \
"${dir}/images/initrd.img" \
"${dir}/images/initramfs.cpio.gz" \
"${dir}/initrd.img" \
"${dir}/initramfs.cpio.gz" \
"${dir}/images/rootfs.cpio.gz" \
"${dir}/rootfs.cpio.gz" \
|| true)"
[[ -n "$INITRD" ]] && log "Found initrd: ${INITRD}"
# Kernel
KERNEL="$(first_existing_file \
"${dir}/images/bzImage" \
"${dir}/images/vmlinuz" \
"${dir}/images/vmlinux" \
"${dir}/bzImage" \
"${dir}/vmlinuz" \
"${dir}/vmlinux" \
"${dir}/boot/bzImage" \
"${dir}/boot/vmlinuz"* \
|| true)"
[[ -n "$KERNEL" ]] && log "Found kernel: ${KERNEL}" || die "Could not find kernel in ${dir}. QCrows v0.2 requires a bundled kernel. Use --kernel to specify manually."
# Kernel config — includes glob patterns for build subdirectories
KERNEL_CONFIG="$(first_existing_file \
"${dir}/.config" \
"${dir}/images/.config" \
"${dir}/build/linux-*/.config" \
"${dir}/linux/.config" \
|| true)"
[[ -n "$KERNEL_CONFIG" ]] && log "Found kernel config: ${KERNEL_CONFIG}" || warn "No kernel .config found — kernel/config is required in QCrows v0.2"
# Boot params
if [[ -f "${dir}/boot-params.conf" ]]; then
BOOT_PARAMS="${dir}/boot-params.conf"
log "Found boot params: ${BOOT_PARAMS}"
fi
# Detect build system if not specified
[[ -z "$BUILD_SYSTEM" ]] && detect_build_system "$dir"
}
# ─── Build System Detection (table-driven) ──────────────────────────────────
detect_build_system() {
local dir="$1"
# Detection table: method | file | token | system | label
local -a detect_methods=("config_file" "config_file" "rootfs_tar" "rootfs_tar" "rootfs_tar")
local -a detect_files=("${dir}/.config" "${dir}/.config" "$ROOTFS" "$ROOTFS" "$ROOTFS")
local -a detect_tokens=("BR2_" "CONFIG_TARGET_" "etc/portage" "var/state/sorcery" "var/state/lunar")
local -a detect_systems=("buildroot" "lede" "gentoo" "sourcemage" "lunar")
local -a detect_labels=("Buildroot" "LEDE/OpenWrt" "Gentoo (portage in rootfs)" "Source Mage (sorcery in rootfs)" "Lunar Linux (lunar in rootfs)")
local i method file token system label
for i in "${!detect_methods[@]}"; do
method="${detect_methods[$i]}"
file="${detect_files[$i]}"
token="${detect_tokens[$i]}"
system="${detect_systems[$i]}"
label="${detect_labels[$i]}"
case "$method" in
config_file)
[[ -f "$file" ]] || continue
grep -q "$token" "$file" 2>/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 <<EOF
[ qcrows ]
format_version = "${QCROWS_SPEC_VERSION}"
[ image ]
name = "${img_name}"
version = "${img_version}"
description = "${img_desc}"
arch = "${ARCH}"
os = "linux"
created_at = "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
[ image.compatibility ]
hypervisors = [ ${hypervisor_toml} ]
kata_runtime_min = "2.5"
[ image.resources ]
min_vcpus = 1
min_memory_mb = 256
recommended_vcpus = 2
recommended_memory_mb = 1024
[ kernel ]
version = "${kernel_version}"
included = ${kernel_included}
path = "${kernel_path}"
format = "${kernel_format}"
config_path = "kernel/config"
size_mb = ${kernel_size_mb}
[ initrd ]
included = ${initrd_included}
type = "${initrd_type}"
path = "${initrd_path}"
[ rootfs ]
type = "${rootfs_type}"
path = "rootfs${rootfs_name##rootfs}"
size_mb = ${rootfs_size}
[ agent ]
name = "kata-agent"
protocol = "vsock"
[ boot_params ]
included = $([ -n "$BOOT_PARAMS" ] && echo true || echo false)
path = "boot-params.conf"
EOF
}
# ─── Generate menu.toml ────────────────────────────────────────────────────
generate_menu() {
local img_name="${NAME:-custom-kata}"
local category="custom"
# Determine category from build system
case "$BUILD_SYSTEM" in
buildroot|lede) category="embedded" ;;
gentoo) category="hardened" ;;
sourcemage|sorcery|lunar) category="minimal" ;;
esac
# Override category from image name
case "$img_name" in
*minimal*|*alpine*|*busybox*) category="minimal" ;;
*server*|*ubuntu*|*debian*) category="server" ;;
*dev*|*debug*) category="development" ;;
*hardened*|*secure*) category="hardened" ;;
*embedded*|*lede*|*openwrt*) category="embedded" ;;
esac
cat <<EOF
[ menu ]
label = "${img_name}"
category = "${category}"
icon = "box"
priority = 50
[ menu.details ]
init_system = "auto"
package_count = 0
shell = "/bin/sh"
[ menu.tags ]
[ "suitable-for" ]
workloads = []
environments = []
[ menu.actions ]
import = true
deploy = false
edit_config = true
EOF
}
# ─── Generate build.toml ──────────────────────────────────────────────────
generate_build() {
cat <<EOF
[ build ]
system = "${BUILD_SYSTEM}"
timestamp = "$(date -u +%Y-%m-%dT%H:%M:%SZ)"
host_arch = "$(uname -m)"
target_arch = "${ARCH}"
[ build.options ]
# Add build-system-specific options here
# See qcrows-spec.md Section 2.3 (build.toml) for examples
[ build.sources ]
# List source URLs and hashes for reproducibility
[ build.reproducibility ]
reproducible = false
build_id = ""
EOF
}
# ─── Compute SHA-256 hashes ───────────────────────────────────────────────
compute_hashes() {
local staging_dir="$1"
(
cd "$staging_dir"
find . -type f ! -name "hashes.sha256" -print0 | sort -z | xargs -0 sha256sum
)
}
# ─── Main Pack Logic ───────────────────────────────────────────────────────
do_pack() {
TMPDIR="$(mktemp -d /tmp/qcrows-pack-XXXXXX)"
local staging="${TMPDIR}/staging"
mkdir -p "$staging"
log "Staging directory: ${staging}"
# ── 1. Copy rootfs ──────────────────────────────────────────────────
local rootfs_dest
rootfs_dest="rootfs$(echo "$ROOTFS" | grep -oE '\.(tar\.[a-z]+|tgz)$' || echo ".tar.gz")"
cp "$ROOTFS" "${staging}/${rootfs_dest}"
log "Added rootfs: $(du -sh "${staging}/${rootfs_dest}" | cut -f1)"
# ── 2. Copy initrd ──────────────────────────────────────────────────
if [[ -n "$INITRD" ]]; then
local initrd_dest
initrd_dest="initrd$(echo "$INITRD" | grep -oE '\.[^.]*$' || echo ".img")"
cp "$INITRD" "${staging}/${initrd_dest}"
log "Added initrd: $(du -sh "${staging}/${initrd_dest}" | cut -f1)"
fi
# ── 3. Copy kernel ──────────────────────────────────────────────────
if [[ -n "$KERNEL" ]]; then
mkdir -p "${staging}/kernel"
# Normalize name to vmlinuz or vmlinux
case "$(basename "$KERNEL")" in
bzImage|vmlinuz*) cp "$KERNEL" "${staging}/kernel/vmlinuz" ;;
vmlinux*) cp "$KERNEL" "${staging}/kernel/vmlinux" ;;
*) cp "$KERNEL" "${staging}/kernel/$(basename "$KERNEL")" ;;
esac
log "Added kernel: $(basename "$KERNEL")"
fi
# ── 4. Copy kernel config ───────────────────────────────────────────
if [[ -n "$KERNEL_CONFIG" ]]; then
mkdir -p "${staging}/kernel"
cp "$KERNEL_CONFIG" "${staging}/kernel/config"
log "Added kernel config"
fi
# ── 5. Copy boot-params ─────────────────────────────────────────────
[[ -n "$BOOT_PARAMS" ]] && { cp "$BOOT_PARAMS" "${staging}/boot-params.conf"; log "Added boot-params.conf"; }
# ── 6. Copy firmware ────────────────────────────────────────────────
if [[ -n "$FIRMWARE_DIR" && -d "$FIRMWARE_DIR" ]]; then
mkdir -p "${staging}/firmware"
cp -r "${FIRMWARE_DIR}/"* "${staging}/firmware/" 2>/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