#!/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" < to generate an iPXE script." fi fi