From bf015ac905b7cc96e1bfdadff8284718ba11719a Mon Sep 17 00:00:00 2001 From: Craig Jennings Date: Mon, 19 Jan 2026 09:30:30 -0600 Subject: Add build-release script for ISO build and distribution Automates the full release workflow: 1. Build ISO (via build.sh) 2. Sanity test (boot in QEMU, manual verification) 3. Distribute to multiple targets: - ~/Downloads/isos (always) - truenas.local:/mnt/vault/isos (if reachable) - ARCHZFS labeled USB drive (detected via blkid, writes via dd) - Ventoy USB drive (detected by label or ventoy/ directory) Options: --skip-build Distribute existing ISO without rebuilding --skip-test Skip the QEMU sanity test --- scripts/build-release | 346 ++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 346 insertions(+) create mode 100755 scripts/build-release diff --git a/scripts/build-release b/scripts/build-release new file mode 100755 index 0000000..0fc3693 --- /dev/null +++ b/scripts/build-release @@ -0,0 +1,346 @@ +#!/bin/bash +# build-release - Build and distribute the archzfs ISO +# +# Usage: +# ./scripts/build-release # Full build, test, and distribute +# ./scripts/build-release --skip-build # Skip build, just distribute existing ISO +# ./scripts/build-release --skip-test # Skip sanity test +# +# Distribution targets: +# - ~/Downloads/isos (always) +# - truenas.local:/mnt/vault/isos (if reachable) +# - USB drive with ARCHZFS label (if present, writes via dd) +# - Ventoy USB drive (if detected, copies ISO file) + +set -e + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +PROJECT_DIR="$(dirname "$SCRIPT_DIR")" + +# Distribution targets +LOCAL_ISO_DIR="$HOME/Downloads/isos" +TRUENAS_HOST="truenas.local" +TRUENAS_PATH="/mnt/vault/isos" + +# Colors +RED='\033[0;31m' +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +CYAN='\033[0;36m' +NC='\033[0m' + +info() { echo -e "${GREEN}[INFO]${NC} $1"; } +warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } +error() { echo -e "${RED}[ERROR]${NC} $1"; exit 1; } +step() { echo -e "\n${CYAN}=== $1 ===${NC}\n"; } + +# Parse arguments +SKIP_BUILD=false +SKIP_TEST=false +while [[ $# -gt 0 ]]; do + case $1 in + --skip-build) SKIP_BUILD=true; shift ;; + --skip-test) SKIP_TEST=true; shift ;; + -h|--help) + echo "Usage: $0 [--skip-build] [--skip-test]" + echo "" + echo "Options:" + echo " --skip-build Skip ISO build, distribute existing ISO" + echo " --skip-test Skip QEMU sanity test" + exit 0 + ;; + *) error "Unknown option: $1" ;; + esac +done + +# Check root for build +check_root() { + if [[ $EUID -ne 0 ]]; then + error "This script must be run as root (for build and dd operations)" + fi +} + +# Find the latest ISO +find_iso() { + ISO_FILE=$(ls -t "$PROJECT_DIR/out/"*.iso 2>/dev/null | head -1) + if [[ -z "$ISO_FILE" ]]; then + error "No ISO found in $PROJECT_DIR/out/" + fi + ISO_NAME=$(basename "$ISO_FILE") + info "Found ISO: $ISO_NAME" + info "Size: $(du -h "$ISO_FILE" | cut -f1)" +} + +# Build the ISO +build_iso() { + step "Building ISO" + cd "$PROJECT_DIR" + ./build.sh + find_iso +} + +# Run sanity test in QEMU +sanity_test() { + step "Sanity Test" + info "Booting ISO in QEMU for verification..." + info "Please verify:" + echo " 1. System boots to login prompt" + echo " 2. ZFS module loads (run 'zpool status' or 'lsmod | grep zfs')" + echo " 3. Custom scripts are present (ls /usr/local/bin/)" + echo "" + + # Start QEMU in background + "$SCRIPT_DIR/test-vm.sh" & + QEMU_PID=$! + + echo "" + read -p "Press Enter once you've verified the ISO works (or Ctrl+C to abort)... " + + # Kill QEMU if still running + if kill -0 $QEMU_PID 2>/dev/null; then + info "Shutting down test VM..." + kill $QEMU_PID 2>/dev/null || true + wait $QEMU_PID 2>/dev/null || true + fi + + info "Sanity test passed!" +} + +# Find ARCHZFS labeled USB drive +find_archzfs_drive() { + ARCHZFS_DEV=$(blkid -L ARCHZFS 2>/dev/null | head -1) + if [[ -n "$ARCHZFS_DEV" ]]; then + # Get the parent device (e.g., /dev/sda from /dev/sda1) + ARCHZFS_DISK=$(lsblk -no PKNAME "$ARCHZFS_DEV" 2>/dev/null | head -1) + if [[ -n "$ARCHZFS_DISK" ]]; then + ARCHZFS_DISK="/dev/$ARCHZFS_DISK" + # Verify it's removable (USB) + REMOVABLE=$(lsblk -no RM "$ARCHZFS_DISK" 2>/dev/null | head -1) + if [[ "$REMOVABLE" == "1" ]]; then + info "Found ARCHZFS USB drive: $ARCHZFS_DISK" + return 0 + else + warn "ARCHZFS label found on $ARCHZFS_DISK but it's not removable, skipping" + fi + fi + fi + ARCHZFS_DISK="" + return 1 +} + +# Find Ventoy USB drive +find_ventoy_drive() { + VENTOY_PART="" + VENTOY_MOUNT="" + + # Method 1: Look for partition labeled "Ventoy" + local ventoy_by_label=$(blkid -L Ventoy 2>/dev/null | head -1) + if [[ -n "$ventoy_by_label" ]]; then + VENTOY_PART="$ventoy_by_label" + info "Found Ventoy drive by label: $VENTOY_PART" + return 0 + fi + + # Method 2: Check mounted or mountable partitions for ventoy/ directory + # Look for removable drives with exfat/ntfs + while read -r dev fstype rm; do + [[ "$rm" != "1" ]] && continue # Skip non-removable + [[ ! "$fstype" =~ ^(exfat|ntfs|vfat)$ ]] && continue # Skip wrong fs types + + local mount_point=$(lsblk -no MOUNTPOINT "/dev/$dev" 2>/dev/null) + local needs_unmount=false + + if [[ -z "$mount_point" ]]; then + # Try mounting temporarily + mount_point=$(mktemp -d) + if mount -o ro "/dev/$dev" "$mount_point" 2>/dev/null; then + needs_unmount=true + else + rmdir "$mount_point" + continue + fi + fi + + # Check for ventoy directory + if [[ -d "$mount_point/ventoy" ]] || [[ -f "$mount_point/ventoy/ventoy.json" ]]; then + VENTOY_PART="/dev/$dev" + info "Found Ventoy drive by content: $VENTOY_PART" + if $needs_unmount; then + umount "$mount_point" + rmdir "$mount_point" + fi + return 0 + fi + + if $needs_unmount; then + umount "$mount_point" + rmdir "$mount_point" + fi + done < <(lsblk -rno NAME,FSTYPE,RM 2>/dev/null | grep -v "^loop") + + return 1 +} + +# Distribute to local directory +distribute_local() { + step "Distributing to Local Directory" + mkdir -p "$LOCAL_ISO_DIR" + + # Remove old archzfs ISOs + local old_isos=$(ls "$LOCAL_ISO_DIR"/archzfs-*.iso 2>/dev/null || true) + if [[ -n "$old_isos" ]]; then + info "Removing old ISOs..." + rm -f "$LOCAL_ISO_DIR"/archzfs-*.iso + fi + + info "Copying to $LOCAL_ISO_DIR/" + cp "$ISO_FILE" "$LOCAL_ISO_DIR/" + info "Done: $LOCAL_ISO_DIR/$ISO_NAME" +} + +# Distribute to TrueNAS +distribute_truenas() { + step "Distributing to TrueNAS" + + # Check if reachable + if ! ping -c 1 -W 2 "$TRUENAS_HOST" &>/dev/null; then + warn "TrueNAS ($TRUENAS_HOST) not reachable, skipping" + return 0 + fi + + info "TrueNAS is reachable" + + # Remove old ISOs and copy new one + info "Removing old ISOs from TrueNAS..." + ssh "root@$TRUENAS_HOST" "rm -f $TRUENAS_PATH/archzfs-*.iso" 2>/dev/null || true + + info "Copying to $TRUENAS_HOST:$TRUENAS_PATH/" + scp "$ISO_FILE" "root@$TRUENAS_HOST:$TRUENAS_PATH/" + info "Done: $TRUENAS_HOST:$TRUENAS_PATH/$ISO_NAME" +} + +# Write to ARCHZFS USB drive +distribute_archzfs_usb() { + step "Writing to ARCHZFS USB Drive" + + if ! find_archzfs_drive; then + warn "No ARCHZFS USB drive found, skipping" + return 0 + fi + + # Confirm before writing + echo "" + warn "About to write ISO to $ARCHZFS_DISK" + warn "This will ERASE ALL DATA on the drive!" + lsblk "$ARCHZFS_DISK" + echo "" + read -p "Type 'yes' to confirm: " confirm + + if [[ "$confirm" != "yes" ]]; then + warn "Skipping USB write" + return 0 + fi + + # Unmount any mounted partitions + for part in $(lsblk -rno NAME "$ARCHZFS_DISK" | tail -n +2); do + umount "/dev/$part" 2>/dev/null || true + done + + info "Writing ISO to $ARCHZFS_DISK..." + dd if="$ISO_FILE" of="$ARCHZFS_DISK" bs=4M status=progress oflag=sync + sync + info "Done writing to $ARCHZFS_DISK" +} + +# Copy to Ventoy USB drive +distribute_ventoy() { + step "Copying to Ventoy USB Drive" + + if ! find_ventoy_drive; then + warn "No Ventoy USB drive found, skipping" + return 0 + fi + + # Mount if needed + local mount_point=$(lsblk -no MOUNTPOINT "$VENTOY_PART" 2>/dev/null) + local needs_unmount=false + + if [[ -z "$mount_point" ]]; then + mount_point=$(mktemp -d) + info "Mounting $VENTOY_PART to $mount_point" + mount "$VENTOY_PART" "$mount_point" + needs_unmount=true + fi + + # Remove old archzfs ISOs + local old_isos=$(ls "$mount_point"/archzfs-*.iso 2>/dev/null || true) + if [[ -n "$old_isos" ]]; then + info "Removing old ISOs from Ventoy..." + rm -f "$mount_point"/archzfs-*.iso + fi + + info "Copying ISO to Ventoy drive..." + cp "$ISO_FILE" "$mount_point/" + sync + info "Done: $mount_point/$ISO_NAME" + + if $needs_unmount; then + info "Unmounting Ventoy drive..." + umount "$mount_point" + rmdir "$mount_point" + fi +} + +# Summary +show_summary() { + step "Distribution Complete" + echo "ISO: $ISO_NAME" + echo "" + echo "Distributed to:" + echo " ✓ $LOCAL_ISO_DIR/" + + if ping -c 1 -W 1 "$TRUENAS_HOST" &>/dev/null; then + echo " ✓ $TRUENAS_HOST:$TRUENAS_PATH/" + else + echo " ✗ $TRUENAS_HOST (not reachable)" + fi + + if [[ -n "$ARCHZFS_DISK" ]]; then + echo " ✓ $ARCHZFS_DISK (USB boot drive)" + else + echo " - ARCHZFS USB drive (not found)" + fi + + if [[ -n "$VENTOY_PART" ]]; then + echo " ✓ $VENTOY_PART (Ventoy)" + else + echo " - Ventoy drive (not found)" + fi +} + +# Main +main() { + check_root + + if $SKIP_BUILD; then + step "Skipping Build" + find_iso + else + build_iso + fi + + if ! $SKIP_TEST; then + sanity_test + else + step "Skipping Sanity Test" + fi + + distribute_local + distribute_truenas + distribute_archzfs_usb + distribute_ventoy + + show_summary +} + +main "$@" -- cgit v1.2.3