699 lines
26 KiB
Bash
Executable File
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
|