incus-contrib/incusos/incusos-iso

976 lines
30 KiB
Bash
Executable File

#!/usr/bin/env bash
# incusos-iso - Build IncusOS ISO/IMG images using the official flasher-tool
#
# Wrapper around the IncusOS flasher-tool that adds cross-architecture support,
# automatic seed generation, client certificate injection, and batch builds.
#
# Usage: incusos-iso [OPTIONS]
# Run 'incusos-iso --help' for full usage information.
set -euo pipefail
readonly VERSION="0.1.0"
readonly SCRIPT_NAME="incusos-iso"
readonly CDN_INDEX="https://images.linuxcontainers.org/os/index.json"
readonly CDN_BASE="https://images.linuxcontainers.org/os"
# ---------------------------------------------------------------------------
# Color and formatting
# ---------------------------------------------------------------------------
setup_colors() {
if [[ -t 1 ]] && [[ "${NO_COLOR:-}" != "1" ]] && [[ "${TERM:-}" != "dumb" ]]; then
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[0;33m'
BLUE='\033[0;34m'
CYAN='\033[0;36m'
BOLD='\033[1m'
DIM='\033[2m'
RESET='\033[0m'
else
RED='' GREEN='' YELLOW='' BLUE='' CYAN='' BOLD='' DIM='' RESET=''
fi
}
info() { echo -e "${BLUE}[info]${RESET} $*"; }
success() { echo -e "${GREEN}[ok]${RESET} $*"; }
warn() { echo -e "${YELLOW}[warn]${RESET} $*" >&2; }
error() { echo -e "${RED}[error]${RESET} $*" >&2; }
step() { echo -e "${CYAN}[step]${RESET} ${BOLD}$*${RESET}"; }
detail() { echo -e "${DIM} $*${RESET}"; }
# ---------------------------------------------------------------------------
# Help
# ---------------------------------------------------------------------------
usage() {
cat <<EOF
${BOLD}${SCRIPT_NAME}${RESET} v${VERSION} - Build IncusOS ISO/IMG images
${BOLD}USAGE${RESET}
${SCRIPT_NAME} [OPTIONS]
${BOLD}OPTIONS${RESET}
${BOLD}-a, --arch${RESET} ARCH Target architecture: ${CYAN}x86_64${RESET}, ${CYAN}aarch64${RESET}
(default: auto-detect current system)
${BOLD}-A, --app${RESET} APP Application: ${CYAN}incus${RESET}, ${CYAN}operations-center${RESET}
(default: incus)
${BOLD}-d, --defaults${RESET} Apply default configuration (ZFS pool, bridge, port 8443)
${BOLD}-c, --cert${RESET} FILE Path to client certificate PEM file
(default: auto-detect from local Incus installation)
${BOLD}--no-cert${RESET} Do not inject any client certificate
${BOLD}-F, --format${RESET} FORMAT Image format: ${CYAN}iso${RESET}, ${CYAN}img${RESET} (default: iso)
${BOLD}-o, --output${RESET} FILE Output filename (default: auto-generated)
${BOLD}-C, --channel${RESET} CHANNEL Update channel: ${CYAN}stable${RESET}, ${CYAN}testing${RESET} (default: stable)
${BOLD}Seed options:${RESET}
${BOLD}-s, --seed${RESET} FILE Use a pre-built seed tar archive (skip seed generation)
${BOLD}--disk-target${RESET} ID Target disk ID pattern (e.g. "virtio-*")
${BOLD}--force-install${RESET} Overwrite existing installation without prompting
${BOLD}--force-reboot${RESET} Automatically reboot after installation
${BOLD}--missing-tpm${RESET} Allow installation without TPM 2.0
${BOLD}--missing-secureboot${RESET} Allow installation without Secure Boot
${BOLD}--hostname${RESET} NAME Set system hostname
${BOLD}Image source:${RESET}
${BOLD}-i, --image${RESET} FILE Use a local base image (skip CDN download)
${BOLD}Batch mode:${RESET}
${BOLD}-b, --batch${RESET} Generate all common ISO variants
${BOLD}--batch-arches${RESET} LIST Architectures for batch mode (default: "x86_64,aarch64")
${BOLD}--batch-apps${RESET} LIST Applications for batch mode (default: "incus,operations-center")
${BOLD}--output-dir${RESET} DIR Output directory for batch mode (default: current directory)
${BOLD}General:${RESET}
${BOLD}--dry-run${RESET} Show what would be done without executing
${BOLD}-q, --quiet${RESET} Suppress informational output
${BOLD}-h, --help${RESET} Show this help message
${BOLD}-V, --version${RESET} Show version
${BOLD}EXAMPLES${RESET}
# Build default ISO for current architecture (Incus, no defaults)
${SCRIPT_NAME}
# Incus with defaults, for x86_64
${SCRIPT_NAME} --arch x86_64 --defaults
# Operations Center for aarch64
${SCRIPT_NAME} --arch aarch64 --app operations-center --defaults
# Use a pre-built seed archive
${SCRIPT_NAME} --seed my-config.tar --arch x86_64
# Build USB image instead of ISO
${SCRIPT_NAME} --format img --defaults
# Generate all common variants
${SCRIPT_NAME} --batch
# Preview what would be built
${SCRIPT_NAME} --batch --dry-run
${BOLD}PREREQUISITES${RESET}
The flasher-tool must be installed. Install with:
go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest
Ensure \$GOPATH/bin (typically ~/go/bin) is in your PATH.
EOF
exit 0
}
# ---------------------------------------------------------------------------
# Argument parsing
# ---------------------------------------------------------------------------
# Defaults
ARCH=""
APP="incus"
APPLY_DEFAULTS=false
CERT_FILE=""
NO_CERT=false
IMG_FORMAT="iso"
OUTPUT=""
CHANNEL="stable"
SEED_FILE=""
LOCAL_IMAGE=""
DISK_TARGET=""
FORCE_INSTALL=false
FORCE_REBOOT=false
MISSING_TPM=false
MISSING_SECUREBOOT=false
HOSTNAME_OPT=""
BATCH=false
BATCH_ARCHES="x86_64,aarch64"
BATCH_APPS="incus,operations-center"
OUTPUT_DIR="."
DRY_RUN=false
QUIET=false
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
-a|--arch)
ARCH="${2:?'--arch requires a value'}"
shift 2
;;
-A|--app)
APP="${2:?'--app requires a value'}"
shift 2
;;
-d|--defaults)
APPLY_DEFAULTS=true
shift
;;
-c|--cert)
CERT_FILE="${2:?'--cert requires a file path'}"
shift 2
;;
--no-cert)
NO_CERT=true
shift
;;
-F|--format)
IMG_FORMAT="${2:?'--format requires a value'}"
shift 2
;;
-o|--output)
OUTPUT="${2:?'--output requires a file path'}"
shift 2
;;
-C|--channel)
CHANNEL="${2:?'--channel requires a value'}"
shift 2
;;
-s|--seed)
SEED_FILE="${2:?'--seed requires a file path'}"
shift 2
;;
-i|--image)
LOCAL_IMAGE="${2:?'--image requires a file path'}"
shift 2
;;
--disk-target)
DISK_TARGET="${2:?'--disk-target requires a value'}"
shift 2
;;
--force-install)
FORCE_INSTALL=true
shift
;;
--force-reboot)
FORCE_REBOOT=true
shift
;;
--missing-tpm)
MISSING_TPM=true
shift
;;
--missing-secureboot)
MISSING_SECUREBOOT=true
shift
;;
--hostname)
HOSTNAME_OPT="${2:?'--hostname requires a value'}"
shift 2
;;
-b|--batch)
BATCH=true
shift
;;
--batch-arches)
BATCH_ARCHES="${2:?'--batch-arches requires a value'}"
shift 2
;;
--batch-apps)
BATCH_APPS="${2:?'--batch-apps requires a value'}"
shift 2
;;
--output-dir)
OUTPUT_DIR="${2:?'--output-dir requires a value'}"
shift 2
;;
--dry-run)
DRY_RUN=true
shift
;;
-q|--quiet)
QUIET=true
shift
;;
-h|--help)
usage
;;
-V|--version)
echo "${SCRIPT_NAME} v${VERSION}"
exit 0
;;
-*)
error "Unknown option: $1"
echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
exit 1
;;
*)
error "Unexpected argument: $1"
echo "Run '${SCRIPT_NAME} --help' for usage information." >&2
exit 1
;;
esac
done
}
# ---------------------------------------------------------------------------
# Validation
# ---------------------------------------------------------------------------
detect_arch() {
local machine
machine=$(uname -m)
case "$machine" in
x86_64) echo "x86_64" ;;
aarch64) echo "aarch64" ;;
arm64) echo "aarch64" ;;
*)
error "Unsupported architecture: ${machine}"
error "IncusOS supports x86_64 and aarch64 only"
exit 1
;;
esac
}
validate_args() {
# Auto-detect architecture if not specified
if [[ -z "$ARCH" ]]; then
ARCH=$(detect_arch)
[[ "$QUIET" != true ]] && info "Auto-detected architecture: ${ARCH}"
fi
case "$ARCH" in
x86_64|aarch64) ;;
*)
error "Invalid architecture: ${ARCH}"
error "Supported: x86_64, aarch64"
exit 1
;;
esac
case "$APP" in
incus|operations-center) ;;
*)
error "Invalid application: ${APP}"
error "Supported: incus, operations-center"
exit 1
;;
esac
case "$IMG_FORMAT" in
iso|img) ;;
*)
error "Invalid image format: ${IMG_FORMAT}"
error "Supported: iso, img"
exit 1
;;
esac
case "$CHANNEL" in
stable|testing) ;;
*)
error "Invalid channel: ${CHANNEL}"
error "Supported: stable, testing"
exit 1
;;
esac
if [[ -n "$CERT_FILE" ]] && [[ "$NO_CERT" == true ]]; then
error "Cannot use both --cert and --no-cert"
exit 1
fi
if [[ -n "$CERT_FILE" ]] && [[ ! -f "$CERT_FILE" ]]; then
error "Certificate file not found: ${CERT_FILE}"
exit 1
fi
if [[ -n "$SEED_FILE" ]] && [[ ! -f "$SEED_FILE" ]]; then
error "Seed file not found: ${SEED_FILE}"
exit 1
fi
if [[ -n "$LOCAL_IMAGE" ]] && [[ ! -f "$LOCAL_IMAGE" ]]; then
error "Local image not found: ${LOCAL_IMAGE}"
exit 1
fi
}
# ---------------------------------------------------------------------------
# Dependency checks
# ---------------------------------------------------------------------------
FLASHER_TOOL=""
find_flasher_tool() {
# Check common locations
local candidates=(
"flasher-tool"
"${GOPATH:-${HOME}/go}/bin/flasher-tool"
"${HOME}/go/bin/flasher-tool"
"/usr/local/bin/flasher-tool"
)
for candidate in "${candidates[@]}"; do
if command -v "$candidate" &>/dev/null; then
FLASHER_TOOL="$candidate"
return
fi
if [[ -x "$candidate" ]]; then
FLASHER_TOOL="$candidate"
return
fi
done
}
check_dependencies() {
step "Checking dependencies"
# flasher-tool
find_flasher_tool
if [[ -z "$FLASHER_TOOL" ]]; then
error "flasher-tool not found"
echo "" >&2
echo " Install the flasher-tool with:" >&2
echo " go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest" >&2
echo "" >&2
echo " Make sure \$GOPATH/bin is in your PATH:" >&2
echo " export PATH=\"\${GOPATH:-\$HOME/go}/bin:\$PATH\"" >&2
echo "" >&2
# Offer to install if Go is available
if command -v go &>/dev/null; then
echo -n " Go is installed. Install flasher-tool now? [Y/n] " >&2
local reply
read -r reply
if [[ "$reply" =~ ^[Yy]?$ ]]; then
install_flasher_tool
else
exit 1
fi
else
error "Go is not installed. Install Go 1.21+ first, then the flasher-tool."
exit 1
fi
else
success "flasher-tool: ${FLASHER_TOOL}"
fi
# curl (needed for cross-arch downloads)
if ! command -v curl &>/dev/null; then
warn "curl not found -- cross-architecture downloads will not be available"
else
detail "curl: $(command -v curl)"
fi
# gzip
if ! command -v gzip &>/dev/null; then
warn "gzip not found -- image decompression may fail"
fi
# tar
if ! command -v tar &>/dev/null; then
error "tar is required but not found"
exit 1
fi
}
install_flasher_tool() {
step "Installing flasher-tool"
info "Running: go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest"
if go install github.com/lxc/incus-os/incus-osd/cmd/flasher-tool@latest; then
find_flasher_tool
if [[ -n "$FLASHER_TOOL" ]]; then
success "flasher-tool installed: ${FLASHER_TOOL}"
else
# Try adding GOPATH/bin to PATH for this session
export PATH="${GOPATH:-${HOME}/go}/bin:${PATH}"
find_flasher_tool
if [[ -n "$FLASHER_TOOL" ]]; then
success "flasher-tool installed: ${FLASHER_TOOL}"
else
error "flasher-tool was installed but could not be found in PATH"
exit 1
fi
fi
else
error "Failed to install flasher-tool"
exit 1
fi
}
# ---------------------------------------------------------------------------
# Certificate detection
# ---------------------------------------------------------------------------
CERT_CONTENT=""
detect_client_cert() {
if [[ "$NO_CERT" == true ]]; then
[[ "$QUIET" != true ]] && info "Skipping certificate injection (--no-cert)"
return
fi
if [[ -n "$CERT_FILE" ]]; then
step "Loading client certificate from ${CERT_FILE}"
CERT_CONTENT=$(<"$CERT_FILE")
if [[ "$CERT_CONTENT" != *"BEGIN CERTIFICATE"* ]]; then
error "File does not appear to be a PEM certificate: ${CERT_FILE}"
exit 1
fi
success "Certificate loaded"
return
fi
# Auto-detection: prefer reading cert files directly (works with all
# Incus versions including 6.0 LTS which lacks 'get-client-certificate').
step "Auto-detecting client certificate"
# 1) Check common on-disk certificate locations first
local cert_paths=(
"${HOME}/.config/incus/client.crt"
"${HOME}/snap/incus/common/config/client.crt"
"${HOME}/.config/lxc/client.crt"
)
for path in "${cert_paths[@]}"; do
if [[ -f "$path" ]]; then
CERT_CONTENT=$(<"$path")
if [[ "$CERT_CONTENT" == *"BEGIN CERTIFICATE"* ]]; then
success "Certificate loaded from ${path}"
return
fi
detail "File exists but does not contain a valid PEM certificate: ${path}"
fi
done
# 2) Try the incus CLI (only works with Incus >= 6.3)
if command -v incus &>/dev/null; then
local cert
if cert=$(incus remote get-client-certificate 2>/dev/null); then
CERT_CONTENT="$cert"
success "Certificate obtained via Incus CLI"
return
fi
# CLI is installed but cert retrieval failed -- check why
local incus_version
incus_version=$(incus version 2>/dev/null | sed -n 's/.*Client version: \([0-9.]*\).*/\1/p')
[[ -z "$incus_version" ]] && incus_version="unknown"
detail "Incus CLI v${incus_version} found but no certificate available"
detail "Generate one with: incus remote list (triggers auto-generation)"
fi
# 3) Nothing found -- report based on context
if [[ "$APP" == "operations-center" ]]; then
error "No client certificate found"
error "Operations Center requires at least one trusted certificate for access"
echo "" >&2
error "Options:"
error " --cert FILE Provide a certificate file directly"
error " Generate one: incus remote list (creates ~/.config/incus/client.crt)"
error " --no-cert Skip certificate injection (you will be locked out!)"
exit 1
fi
warn "No client certificate found"
if command -v incus &>/dev/null; then
warn "Run 'incus remote list' to generate a client certificate, then re-run this script"
else
warn "The Incus client is not installed on this machine"
warn "Install it to enable auto-detection (see: https://github.com/lxc/incus)"
fi
warn "Use --cert FILE to provide a certificate, or --no-cert to suppress this warning"
}
# ---------------------------------------------------------------------------
# CDN operations
# ---------------------------------------------------------------------------
fetch_latest_version() {
local channel="$1"
step "Fetching latest IncusOS version (channel: ${channel})"
local index_file
index_file=$(mktemp "${TMPDIR:-/tmp}/incusos-index-XXXXXX.json")
if ! curl -fsSL "$CDN_INDEX" -o "$index_file" 2>/dev/null; then
error "Failed to fetch CDN index from ${CDN_INDEX}"
rm -f "$index_file"
exit 1
fi
local version=""
# Parse with jq if available, otherwise use python3
if command -v jq &>/dev/null; then
version=$(jq -r \
--arg ch "$channel" \
'[.updates[] | select(.channels[] == $ch)] | sort_by(.version) | last | .version // empty' \
"$index_file")
elif command -v python3 &>/dev/null; then
version=$(python3 -c "
import json, sys
data = json.load(open('$index_file'))
versions = [u['version'] for u in data.get('updates', []) if '$channel' in u.get('channels', [])]
if versions:
print(sorted(versions)[-1])
" 2>/dev/null)
else
error "Either jq or python3 is required to parse the CDN index"
rm -f "$index_file"
exit 1
fi
rm -f "$index_file"
if [[ -z "$version" ]]; then
error "No version found for channel '${channel}'"
exit 1
fi
success "Latest version: ${version}"
echo "$version"
}
download_image() {
local version="$1"
local arch="$2"
local format="$3"
local output_path="$4"
local url="${CDN_BASE}/${version}/${arch}/IncusOS_${version}.${format}.gz"
local gz_path="${output_path}.gz"
step "Downloading IncusOS image"
detail "URL: ${url}"
detail "Target: ${output_path}"
if ! curl -fL --progress-bar "$url" -o "$gz_path"; then
error "Failed to download image from ${url}"
rm -f "$gz_path"
exit 1
fi
step "Decompressing image"
if ! gzip -d "$gz_path"; then
error "Failed to decompress image"
rm -f "$gz_path"
exit 1
fi
success "Image ready: ${output_path}"
}
# ---------------------------------------------------------------------------
# Seed generation (inline, for when incusos-seed is not used separately)
# ---------------------------------------------------------------------------
generate_seed_tar() {
local output_tar="$1"
local workdir
workdir=$(mktemp -d "${TMPDIR:-/tmp}/incusos-seed-XXXXXX")
step "Generating seed archive"
# install.yaml
{
echo "# Generated by ${SCRIPT_NAME} v${VERSION}"
[[ "$FORCE_INSTALL" == true ]] && echo "force_install: true"
[[ "$FORCE_REBOOT" == true ]] && echo "force_reboot: true"
if [[ -n "$DISK_TARGET" ]]; then
echo "target:"
echo " id: \"${DISK_TARGET}\""
fi
if [[ "$MISSING_TPM" == true ]] || [[ "$MISSING_SECUREBOOT" == true ]]; then
echo "security:"
[[ "$MISSING_TPM" == true ]] && echo " missing_tpm: true"
[[ "$MISSING_SECUREBOOT" == true ]] && echo " missing_secure_boot: true"
fi
} > "${workdir}/install.yaml"
detail "install.yaml"
# applications.yaml
{
echo "applications:"
echo " - name: ${APP}"
} > "${workdir}/applications.yaml"
detail "applications.yaml (${APP})"
# App-specific seed
case "$APP" in
incus)
{
echo "apply_defaults: ${APPLY_DEFAULTS}"
if [[ -n "$CERT_CONTENT" ]]; then
echo "preseed:"
echo " server:"
echo " certificates:"
echo " - name: \"auto-injected-client\""
echo " type: \"client\""
echo " certificate: |"
while IFS= read -r line; do
echo " ${line}"
done <<< "$CERT_CONTENT"
fi
} > "${workdir}/incus.yaml"
detail "incus.yaml (defaults=${APPLY_DEFAULTS})"
;;
operations-center)
{
echo "apply_defaults: ${APPLY_DEFAULTS}"
if [[ -n "$CERT_CONTENT" ]]; then
echo "trusted_client_certificates:"
echo " - |"
while IFS= read -r line; do
echo " ${line}"
done <<< "$CERT_CONTENT"
fi
} > "${workdir}/operations-center.yaml"
detail "operations-center.yaml (defaults=${APPLY_DEFAULTS})"
;;
esac
# network.yaml (optional)
if [[ -n "$HOSTNAME_OPT" ]]; then
{
echo "dns:"
echo " hostname: \"${HOSTNAME_OPT}\""
} > "${workdir}/network.yaml"
detail "network.yaml (hostname=${HOSTNAME_OPT})"
fi
# update.yaml (optional)
if [[ "$CHANNEL" != "stable" ]]; then
{
echo "channel: \"${CHANNEL}\""
echo "auto_reboot: false"
echo "check_frequency: \"6h\""
} > "${workdir}/update.yaml"
detail "update.yaml (channel=${CHANNEL})"
fi
# Create tar
tar -cf "$output_tar" -C "$workdir" .
rm -rf "$workdir"
success "Seed archive: ${output_tar}"
}
# ---------------------------------------------------------------------------
# ISO/IMG building
# ---------------------------------------------------------------------------
generate_output_name() {
local arch="$1"
local app="$2"
local defaults="$3"
local format="$4"
local dir="$5"
local defaults_tag
if [[ "$defaults" == true ]]; then
defaults_tag="defaults"
else
defaults_tag="bare"
fi
local app_tag
case "$app" in
incus) app_tag="incus" ;;
operations-center) app_tag="ops-center" ;;
esac
echo "${dir}/incusos-${app_tag}-${defaults_tag}-${arch}.${format}"
}
build_single() {
local arch="$1"
local app="$2"
local defaults="$3"
local format="$4"
local output="$5"
local seed="$6"
local image="$7"
echo ""
echo -e "${BOLD}Building: ${CYAN}${output}${RESET}"
echo -e " Arch: ${arch} App: ${app} Defaults: ${defaults} Format: ${format}"
echo ""
if [[ "$DRY_RUN" == true ]]; then
info "Dry run -- skipping build"
return
fi
local tmpdir
tmpdir=$(mktemp -d "${TMPDIR:-/tmp}/incusos-build-XXXXXX")
trap 'rm -rf "$tmpdir"' RETURN
# Generate seed if not provided
local seed_tar="$seed"
if [[ -z "$seed_tar" ]]; then
seed_tar="${tmpdir}/seed.tar"
# Save and set state for seed generation
local save_app="$APP" save_defaults="$APPLY_DEFAULTS"
APP="$app"
APPLY_DEFAULTS="$defaults"
generate_seed_tar "$seed_tar"
APP="$save_app"
APPLY_DEFAULTS="$save_defaults"
fi
# Determine image path
local image_path="$image"
local needs_download=false
if [[ -z "$image_path" ]]; then
# Check if we need a cross-architecture download
local current_arch
current_arch=$(detect_arch)
if [[ "$arch" != "$current_arch" ]]; then
# Cross-architecture: must download manually
needs_download=true
fi
# Same architecture: flasher-tool can download itself
fi
# Build flasher-tool command
local cmd=("$FLASHER_TOOL")
cmd+=(--format "$format")
cmd+=(--seed "$seed_tar")
cmd+=(--channel "$CHANNEL")
if [[ "$needs_download" == true ]]; then
# Download cross-arch image
local version
version=$(fetch_latest_version "$CHANNEL" | tail -1)
image_path="${tmpdir}/IncusOS_${version}.${format}"
download_image "$version" "$arch" "$format" "$image_path"
cmd+=(--image "$image_path")
elif [[ -n "$image_path" ]]; then
cmd+=(--image "$image_path")
fi
step "Running flasher-tool"
detail "${cmd[*]}"
# The flasher-tool writes to the image in place when --image is given,
# or downloads and writes when --image is not given.
# We need to handle the output file.
if [[ -n "$image_path" ]]; then
# flasher-tool modifies the image in place
"${cmd[@]}"
# Move to final output location
if [[ "$image_path" != "$output" ]]; then
mv "$image_path" "$output"
fi
else
# flasher-tool will download and create output interactively
# We run it and let it produce the file, then rename
"${cmd[@]}"
# Find the produced file (flasher-tool outputs to current directory)
local produced
produced=$(ls -t IncusOS_*.${format} 2>/dev/null | head -1)
if [[ -n "$produced" ]] && [[ "$produced" != "$output" ]]; then
mv "$produced" "$output"
fi
fi
echo ""
success "Image built: ${output}"
local size
if [[ -f "$output" ]]; then
size=$(du -h "$output" | cut -f1)
detail "Size: ${size}"
fi
}
# ---------------------------------------------------------------------------
# Batch mode
# ---------------------------------------------------------------------------
batch_build() {
IFS=',' read -ra arches <<< "$BATCH_ARCHES"
IFS=',' read -ra apps <<< "$BATCH_APPS"
local total=0
local variants=()
# Build matrix: arch x app x defaults(true/false)
for arch in "${arches[@]}"; do
for app in "${apps[@]}"; do
for defaults in true false; do
local outfile
outfile=$(generate_output_name "$arch" "$app" "$defaults" "$IMG_FORMAT" "$OUTPUT_DIR")
variants+=("${arch}|${app}|${defaults}|${outfile}")
total=$((total + 1))
done
done
done
echo -e "${BOLD}Batch build: ${total} variants${RESET}"
echo ""
local i=0
for variant in "${variants[@]}"; do
IFS='|' read -r arch app defaults outfile <<< "$variant"
i=$((i + 1))
echo -e "${BOLD}[${i}/${total}]${RESET} ${outfile}"
if [[ "$DRY_RUN" == true ]]; then
detail "arch=${arch} app=${app} defaults=${defaults} format=${IMG_FORMAT}"
continue
fi
build_single "$arch" "$app" "$defaults" "$IMG_FORMAT" "$outfile" "$SEED_FILE" "$LOCAL_IMAGE"
done
echo ""
if [[ "$DRY_RUN" == true ]]; then
info "Dry run complete -- no images were built"
else
success "Batch build complete: ${total} images generated"
fi
}
# ---------------------------------------------------------------------------
# Summary
# ---------------------------------------------------------------------------
print_summary() {
echo ""
echo -e "${BOLD}Build summary${RESET}"
echo -e " Architecture: ${CYAN}${ARCH}${RESET}"
echo -e " Application: ${CYAN}${APP}${RESET}"
echo -e " Defaults: $(if [[ "$APPLY_DEFAULTS" == true ]]; then echo "${GREEN}yes${RESET}"; else echo "${YELLOW}no${RESET}"; fi)"
echo -e " Certificate: $(if [[ -n "$CERT_CONTENT" ]]; then echo "${GREEN}injected${RESET}"; else echo "${DIM}none${RESET}"; fi)"
echo -e " Format: ${IMG_FORMAT}"
echo -e " Channel: ${CHANNEL}"
echo -e " Output: ${BOLD}${OUTPUT}${RESET}"
if [[ "$MISSING_TPM" == true ]]; then
echo -e " Security: ${YELLOW}TPM not required${RESET}"
fi
echo ""
if [[ -n "$CERT_CONTENT" ]]; then
echo -e "${DIM} The client certificate has been injected into the image.${RESET}"
echo -e "${DIM} After installation, add the remote with:${RESET}"
echo -e " incus remote add <name> <ip-address>"
else
echo -e "${DIM} No certificate was injected. After installation:${RESET}"
echo -e " incus remote add <name> <ip-address> --accept-certificate"
fi
echo ""
}
# ---------------------------------------------------------------------------
# Main
# ---------------------------------------------------------------------------
main() {
setup_colors
parse_args "$@"
validate_args
if [[ "$QUIET" != true ]]; then
echo -e "${BOLD}${SCRIPT_NAME}${RESET} v${VERSION}"
echo ""
fi
if [[ "$DRY_RUN" != true ]]; then
check_dependencies
else
find_flasher_tool
if [[ -z "$FLASHER_TOOL" ]]; then
FLASHER_TOOL="flasher-tool (not installed)"
fi
fi
echo ""
if [[ "$BATCH" == true ]]; then
# In batch mode, cert detection applies to all builds
if [[ -z "$SEED_FILE" ]]; then
detect_client_cert
fi
echo ""
batch_build
return
fi
# Single build mode
if [[ -z "$SEED_FILE" ]]; then
detect_client_cert
fi
# Set default output name
if [[ -z "$OUTPUT" ]]; then
OUTPUT=$(generate_output_name "$ARCH" "$APP" "$APPLY_DEFAULTS" "$IMG_FORMAT" "$OUTPUT_DIR")
fi
build_single "$ARCH" "$APP" "$APPLY_DEFAULTS" "$IMG_FORMAT" "$OUTPUT" "$SEED_FILE" "$LOCAL_IMAGE"
if [[ "$QUIET" != true ]] && [[ "$DRY_RUN" != true ]]; then
print_summary
fi
}
main "$@"