Dotfiles/setup/archiso/build.sh

300 lines
14 KiB
Bash
Executable File

#!/usr/bin/env bash
# =============================================================================
# build.sh — Build the M-Archy Arch Linux ISO + netboot artifacts
#
# PURPOSE:
# This is the primary build entry point for the M-Archy custom Arch Linux
# installer ISO. It takes the official archiso "releng" base profile and
# merges the M-Archy overlay on top of it, embedding custom installer
# scripts and an optional pre-filled answerfile for fully automated
# unattended installation. It also generates iPXE scripts for netboot-based
# deployment (PXE/WDS environments).
#
# WORKFLOW OVERVIEW:
# 1. Parse arguments (optional answerfile path, netboot URL, output dir).
# 2. Install archiso if missing.
# 3. Copy the upstream releng profile to a working directory.
# 4. Apply the M-Archy overlay (custom scripts, profiledef, mkinitcpio config).
# 5. Merge extra packages (packages.extra) into the releng package list.
# 6. Embed the installer shell scripts into the ISO's /root/installer/.
# 7. Optionally embed an answerfile.json for automated installs.
# 8. Run mkarchiso to produce the final .iso and netboot tarball.
# 9. Optionally write an iPXE chainload script for netboot.xyz / WDS.
#
# USAGE:
# bash build.sh [--preconf [FILE]] [--netboot-url URL] [OUT_DIR]
#
# --preconf Embed ~/answerfile.json into the ISO at /answerfile.json
# --preconf FILE Embed the specified answerfile instead
# --netboot-url URL Base URL where netboot artifacts will be served
# (generates m-archy-netboot.ipxe in OUT_DIR)
# OUT_DIR Output directory (default: ~/m-archy-out, or $OUT_DIR env)
# =============================================================================
set -euo pipefail
# set -e → abort on any command that exits non-zero
# set -u → abort if an unset variable is referenced (catches typos)
# set -o pipefail → a pipe fails if any command in it fails (not just the last)
# Resolve this script's directory and the dotfiles root, regardless of where
# the caller's working directory is. Using BASH_SOURCE[0] rather than $0 is
# safer when the script is sourced or called with a relative path.
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
DOTFILES_DIR="$(cd "$SCRIPT_DIR/../.." && pwd)"
# ── Argument parsing ───────────────────────────────────────────────────────────
# We support both --flag value and --flag=value syntax.
# PRECONF_FILE → path to JSON answerfile to embed (empty = no answerfile)
# NETBOOT_URL → HTTP base URL for generated iPXE script (empty = skip)
# OUT_ARG → positional output directory override
PRECONF_FILE=""
NETBOOT_URL=""
OUT_ARG=""
while [[ $# -gt 0 ]]; do
case "$1" in
--preconf)
# If the next argument looks like a path (doesn't start with -),
# treat it as the answerfile path; otherwise use the default location.
if [[ $# -gt 1 && "${2:0:1}" != "-" ]]; then
PRECONF_FILE="$2"; shift
else
PRECONF_FILE="$HOME/answerfile.json"
fi
shift
;;
--preconf=*)
# Handle --preconf=/path/to/file syntax (strip prefix up to =)
PRECONF_FILE="${1#--preconf=}"
shift
;;
--netboot-url)
[[ $# -gt 1 ]] || { echo "ERROR: --netboot-url requires a URL argument" >&2; exit 1; }
NETBOOT_URL="$2"; shift 2
;;
--netboot-url=*)
NETBOOT_URL="${1#--netboot-url=}"
shift
;;
-*)
echo "Unknown flag: $1" >&2; exit 1
;;
*)
# Any non-flag argument is treated as the output directory
OUT_ARG="$1"; shift
;;
esac
done
# WORK_DIR: scratch space where mkarchiso does its work (can be huge — squashfs
# compression of the full airootfs happens here).
WORK_DIR="${WORK_DIR:-$HOME/m-archy-build}"
# OUT_DIR: where the final .iso and netboot tarball are written.
# Priority: positional arg > $OUT_DIR env var > default ~/m-archy-out
OUT_DIR="${OUT_ARG:-${OUT_DIR:-$HOME/m-archy-out}}"
# PROFILE: the assembled profile directory passed to mkarchiso.
# We copy releng here and then apply our overlay on top.
PROFILE="$WORK_DIR/profile"
# RELENG: the official Arch Linux "release engineering" base profile, installed
# with the archiso package. It provides sane defaults for pacman.conf, EFI
# boot entries, and a working live environment.
RELENG="/usr/share/archiso/configs/releng"
# ── Ensure archiso is installed ───────────────────────────────────────────────
# mkarchiso is the tool that actually assembles the ISO from a profile directory.
# It is only available after installing the archiso package.
if ! command -v mkarchiso &>/dev/null; then
echo "Installing archiso..."
sudo pacman -S --noconfirm archiso
fi
# Sanity check: the releng base profile must exist.
# If archiso installed correctly, this path will always be present.
[[ -d "$RELENG" ]] || { echo "ERROR: $RELENG not found — is archiso installed?"; exit 1; }
# ── Validate answerfile early (fail fast before the long build) ───────────────
# If --preconf was given, verify the file exists and is valid JSON now rather
# than discovering the problem after a 10-minute build.
if [[ -n "$PRECONF_FILE" ]]; then
[[ -f "$PRECONF_FILE" ]] \
|| { echo "ERROR: answerfile not found: $PRECONF_FILE"; exit 1; }
# jq empty parses JSON and exits 0 if valid, non-zero if malformed.
# Fall through gracefully if jq is not installed.
command -v jq &>/dev/null \
&& jq empty "$PRECONF_FILE" \
|| echo "Warning: jq not available — skipping answerfile JSON validation"
echo "Answerfile to embed: $PRECONF_FILE"
fi
# ── Clean and create working directories ─────────────────────────────────────
# Remove any previous build artifacts to ensure a clean, reproducible build.
# mkarchiso can behave unexpectedly if the profile or work directory has stale state.
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR" "$OUT_DIR"
# ── Assemble the profile from releng + M-Archy overlay ───────────────────────
echo "Copying releng base profile..."
# Start with a full copy of upstream releng so we inherit all its boot
# entries, pacman.conf, syslinux/systemd-boot configs, etc.
cp -r "$RELENG" "$PROFILE"
echo "Applying M-Archy overlay..."
# Merge our custom airootfs overlay ON TOP of the releng copy.
# Files in our overlay replace or extend the releng defaults.
# The trailing /. ensures we copy the directory contents, not the directory itself.
cp -r "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE/airootfs/"
echo "Replacing profiledef..."
# profiledef.sh controls ISO metadata (name, label, build modes, boot modes,
# compression settings, and file permissions in the final image).
# We replace the releng default entirely with our customized version.
cp "$SCRIPT_DIR/overlay/profiledef.sh" "$PROFILE/profiledef.sh"
echo "Adding extra packages..."
# packages.extra lists additional packages beyond releng's defaults.
# We merge them into packages.x86_64 (the file mkarchiso reads for pacman).
# Strategy: read line-by-line, skip blank lines and comments (#),
# and append each package only if it is not already in the list (idempotent).
while IFS= read -r pkg || [[ -n "$pkg" ]]; do
[[ -z "$pkg" || "$pkg" == \#* ]] && continue
grep -qxF "$pkg" "$PROFILE/packages.x86_64" || echo "$pkg" >> "$PROFILE/packages.x86_64"
done < "$SCRIPT_DIR/overlay/packages.extra"
# ── Embed installer scripts ────────────────────────────────────────────────────
# These three scripts live in the main setup/ directory of the dotfiles repo
# and implement the actual Arch Linux installation logic. They are placed in
# /root/installer/ on the live ISO so the auto-launch scripts can find them.
echo "Embedding installer scripts..."
mkdir -p "$PROFILE/airootfs/root/installer"
# Guided interactive installer — walks the user through partitioning, locale, etc.
cp "$DOTFILES_DIR/setup/archbaseos-guided-install.sh" "$PROFILE/airootfs/root/installer/"
# Automated unattended installer — reads /answerfile.json and installs silently.
cp "$DOTFILES_DIR/setup/arch-autoinstall.sh" "$PROFILE/airootfs/root/installer/"
# Reset script — wipes and reinstalls the system while preserving /home.
cp "$DOTFILES_DIR/setup/reset-arch.sh" "$PROFILE/airootfs/root/installer/"
echo "Embedding resources (branding assets used by post-install modules)..."
# resources/ contains shared assets (SVGs, etc.) referenced by installer modules
# such as the Plymouth splash logo. Embedding them here means the ISO carries
# everything needed so post-install steps never require the user to supply files.
mkdir -p "$PROFILE/airootfs/root/installer/resources"
cp -r "$DOTFILES_DIR/resources/." "$PROFILE/airootfs/root/installer/resources/"
# Make all scripts executable. The archiso tool preserves these bits in the
# squashfs, so they will be executable on the live system too.
chmod 755 \
"$PROFILE/airootfs/root/launch.sh" \
"$PROFILE/airootfs/root/.automated_script.sh" \
"$PROFILE/airootfs/usr/local/bin/install-arch" \
"$PROFILE/airootfs/root/installer/"*.sh
# ── Embed answerfile (--preconf) ───────────────────────────────────────────────
# An answerfile baked into the ISO lets machines boot and install completely
# hands-free — useful for bulk deployment via PXE. Without it, the guided
# installer starts automatically instead.
if [[ -n "$PRECONF_FILE" ]]; then
echo "Embedding answerfile: $PRECONF_FILE → /answerfile.json"
# install -m 644 creates the destination with the given permissions,
# equivalent to cp + chmod but in one atomic step.
install -m 644 "$PRECONF_FILE" "$PROFILE/airootfs/answerfile.json"
fi
echo "Building ISO (this may take a while)..."
# mkarchiso needs root to mount loopback devices, write to /proc-style paths,
# and set file ownership inside the squashfs correctly.
# -v: verbose (shows progress)
# -w: work directory (large temporary build area)
# -o: output directory for the final .iso and netboot tarball
sudo mkarchiso -v -w "$WORK_DIR/mkarchiso" -o "$OUT_DIR" "$PROFILE"
# After the build, mkarchiso leaves files owned by root. Fix ownership so the
# calling user can manipulate the output without sudo.
sudo chmod -R 777 "$WORK_DIR" "$OUT_DIR"
sudo chown -R "$(id -u):$(id -g)" "$WORK_DIR" "$OUT_DIR"
echo
echo "Done."
# List the produced ISO file(s). The || true prevents the script from failing
# when no .iso files exist (e.g., if only netboot mode was built).
ls -lh "$OUT_DIR/"*.iso 2>/dev/null || true
# Inform the user whether an automated or guided install will start on boot.
if [[ -n "$PRECONF_FILE" ]]; then
echo "Answerfile embedded — automated install will activate on boot."
else
echo "No answerfile — guided (manual) installer will start automatically on boot."
fi
# ── How to write the ISO to USB ────────────────────────────────────────────────
# This is an isohybrid image: it MUST be written block-for-block. Copying the
# file onto a mounted USB, or using Rufus "ISO" mode / UNetbootin, rewrites the
# layout and the BIOS bootloader then fails with "failed to load ldlinux.c32".
# Glob (not ls) so filenames are handled safely; date-named ISOs sort ascending,
# so the last match is the newest build.
_isos=("$OUT_DIR"/*.iso)
_built_iso="${_isos[-1]}"
if [[ -e "$_built_iso" ]]; then
echo
echo "To write it to a USB stick (block-level — required for BIOS boot):"
echo " bash $SCRIPT_DIR/write-usb.sh \"$_built_iso\" /dev/sdX"
echo " or directly: sudo dd if=\"$_built_iso\" of=/dev/sdX bs=4M conv=fsync status=progress"
echo " Do NOT drag-and-drop the .iso onto the USB, and avoid Rufus 'ISO' mode"
echo " (use 'DD' mode) — those cause 'failed to load ldlinux.c32' at boot."
fi
# ── Netboot artifacts ──────────────────────────────────────────────────────────
# mkarchiso's 'netboot' build mode (set in profiledef.sh) produces a tarball
# containing the kernel, initramfs, and airootfs.sfs ready for HTTP-based PXE
# boot. We detect it here and optionally generate an iPXE chainload script.
NETBOOT_TARBALL="$(ls "$OUT_DIR/"*-netboot-*.tar.gz 2>/dev/null | head -n1 || true)"
if [[ -n "$NETBOOT_TARBALL" ]]; then
echo
echo "Netboot artifact: $NETBOOT_TARBALL"
echo " Extract and serve its contents from an HTTP server, then boot via PXE."
echo " Internal layout (relative to tarball root):"
# Show the tarball contents indented for readability.
tar -tzf "$NETBOOT_TARBALL" | sed 's/^/ /'
if [[ -n "$NETBOOT_URL" ]]; then
# Strip trailing slash so our URL concatenations are consistent.
BASE_URL="${NETBOOT_URL%/}"
IPXE_FILE="$OUT_DIR/m-archy-netboot.ipxe"
# Generate an iPXE script that can be chainloaded from netboot.xyz or
# served directly from a WDS/TFTP server. The script tells iPXE where
# to find the kernel, initramfs, and squashfs root over HTTP.
cat > "$IPXE_FILE" <<IPXE
#!ipxe
# M-Archy Arch Linux Installer — PXE Boot
# Generated by build.sh on $(date -u +%Y-%m-%dT%H:%M:%SZ)
#
# Deploy:
# 1. Extract the netboot tarball to the web root at: ${BASE_URL}/
# 2. Add this script as a custom entry in netboot.xyz, or chainload it directly.
#
# netboot.xyz custom menu entry (paste into your netboot.xyz config):
# item m-archy M-Archy Arch Linux Installer
# goto m-archy
# :m-archy
# chain ${BASE_URL}/m-archy-netboot.ipxe
set base-url ${BASE_URL}
kernel \${base-url}/arch/boot/x86_64/vmlinuz-linux \\
archiso_http_srv=\${base-url}/ \\
archisobasedir=arch \\
ip=dhcp
initrd \${base-url}/arch/boot/x86_64/initramfs-linux.img
boot
IPXE
echo
echo "iPXE script written to: $IPXE_FILE"
echo " Serve it alongside the netboot tarball contents at: ${BASE_URL}/"
else
echo
echo "Tip: rerun with --netboot-url <http://your-server/path> to generate an iPXE script."
fi
fi