#!/usr/bin/env bash # Build a Furtka live ISO. # # From the repo root or from iso/ on any host with Docker: # ./iso/build.sh # # The build runs inside a privileged `archlinux:latest` container because # mkarchiso needs root + loop mounts + an Arch package manager, which # Ubuntu doesn't provide natively. Output ISO goes to iso/out/. set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)" OUT_DIR="$SCRIPT_DIR/out" if [[ "${FURTKA_ISO_INNER:-0}" != "1" ]]; then mkdir -p "$OUT_DIR" echo "==> Launching build container" exec docker run --rm --privileged \ -v "$REPO_ROOT:/work" \ -w /work \ -e FURTKA_ISO_INNER=1 \ archlinux:latest \ bash /work/iso/build.sh fi # ---- inside the container from here on ---- echo "==> Syncing pacman, installing archiso" pacman -Syu --noconfirm --needed archiso curl zstd # archinstall build the webinstaller is validated against (see pin step below). ARCHINSTALL_PIN="4.4-1" ARCHINSTALL_SHA256="6cb5da75c108ef2f5d25a7a20baf6c52cc927d5e06890dd10182e6eb4be815cb" PROFILE_SRC="/usr/share/archiso/configs/releng" PROFILE_WORK="/tmp/furtka-profile" BUILD_WORK="/tmp/furtka-build" OUT_IN_CONTAINER="/work/iso/out" rm -rf "$PROFILE_WORK" "$BUILD_WORK" cp -a "$PROFILE_SRC" "$PROFILE_WORK" echo "==> Overlaying Furtka customizations" cat "$SCRIPT_DIR/overlay/packages.extra" >> "$PROFILE_WORK/packages.x86_64" cat "$SCRIPT_DIR/overlay/profiledef.sh" >> "$PROFILE_WORK/profiledef.sh" echo "==> Pinning archinstall ${ARCHINSTALL_PIN}" # The webinstaller imports archinstall internals (build_disk_config) and # sed-patches one of its files at service start, so a silent archinstall # upgrade in the Arch repos can break every install with no change on our # side. Serve the validated build from a local repo listed *before* [core]: # pacman takes a package from the first repo that has it, whatever the # version elsewhere. Bump deliberately: new version + sha256 below, rebuild, # run the smoke VM (see iso/README.md). PINNED_REPO="/tmp/furtka-pinned-repo" rm -rf "$PINNED_REPO" && mkdir -p "$PINNED_REPO" ARCHINSTALL_PKG="archinstall-${ARCHINSTALL_PIN}-any.pkg.tar.zst" curl -fsSL --retry 3 -o "$PINNED_REPO/$ARCHINSTALL_PKG" \ "https://archive.archlinux.org/packages/a/archinstall/$ARCHINSTALL_PKG" echo "${ARCHINSTALL_SHA256} $PINNED_REPO/$ARCHINSTALL_PKG" | sha256sum -c - # archinstall is an "any" package installed under /usr/lib/python3.X; after # Arch moves to a new Python it would install into a dead directory. Fail # the build with a clear message instead of shipping a broken installer. pkg_py=$(tar --zstd -tf "$PINNED_REPO/$ARCHINSTALL_PKG" \ | sed -n 's|^usr/lib/python\(3\.[0-9]*\)/.*|\1|p' | sort -u) repo_py=$(pacman -Si python | awk -F': ' '/^Version/ {split($2, v, "."); print v[1] "." v[2]}') if [ "$pkg_py" != "$repo_py" ]; then echo "ERROR: pinned archinstall ${ARCHINSTALL_PIN} targets Python $pkg_py, Arch now ships $repo_py." >&2 echo " Bump ARCHINSTALL_PIN/ARCHINSTALL_SHA256 in iso/build.sh (see iso/README.md)." >&2 exit 1 fi repo-add -q "$PINNED_REPO/furtka-pinned.db.tar.gz" "$PINNED_REPO/$ARCHINSTALL_PKG" # The sha256 above already pins the exact bytes. sed -i "0,/^\[core\]/s||[furtka-pinned]\nSigLevel = Optional TrustAll\nServer = file://$PINNED_REPO\n\n[core]|" \ "$PROFILE_WORK/pacman.conf" grep -q '^\[furtka-pinned\]' "$PROFILE_WORK/pacman.conf" cp -a "$SCRIPT_DIR/overlay/airootfs/." "$PROFILE_WORK/airootfs/" # releng auto-logs root in on tty1. That turns our /etc/issue banner into a # one-shot snapshot: once agetty has handed tty1 to a shell, `agetty --reload` # from furtka-update-issue has nothing left to redraw, so the IP fallback only # shows if DHCP happened to beat getty. Drop the drop-in: tty1 stays at the # login prompt with a live-refreshed banner (furtka-issue.timer), and a shell # is one `root` + Enter away (releng's root has no password) — the banner says # so. rm -rf "$PROFILE_WORK/airootfs/etc/systemd/system/getty@tty1.service.d" echo "==> Rebranding boot menu (GRUB + syslinux + systemd-boot)" # releng ships menu entries labelled "Arch Linux install medium" across three # bootloader configs (BIOS syslinux, GRUB, systemd-boot for UEFI). Rewrite to # our brand. Done with sed (not a static overlay) so upstream archiso file # moves don't silently leave stale Arch labels behind. # # Also rebrands the syslinux menu header ("MENU TITLE Arch Linux") and the # per-entry HELP text shown at the bottom of the BIOS screen. GRUB/efiboot # don't ship equivalent long descriptions, so menu-entry rename is enough there. find "$PROFILE_WORK/grub" "$PROFILE_WORK/syslinux" "$PROFILE_WORK/efiboot" \ -type f \( -name "*.cfg" -o -name "*.conf" \) -print0 \ | xargs -0 sed -i \ -e 's/Arch Linux install medium/Furtka Live Installer/g' \ -e 's/Arch Linux live medium/Furtka Live Installer/g' \ -e 's/install Arch Linux or perform system maintenance/install Furtka or perform system maintenance/g' \ -e 's/^MENU TITLE Arch Linux$/MENU TITLE Furtka/' # Mark the default entry as (Recommended) so first-time users know which to # pick. Targets the main entry only — speech/accessibility variants stay # unlabeled to avoid suggesting they're the normal choice. sed -i 's/^title Furtka Live Installer (%ARCH%, UEFI)$/title (Recommended) Furtka Live Installer (%ARCH%, UEFI)/' \ "$PROFILE_WORK/efiboot/loader/entries/01-archiso-linux.conf" sed -i 's/^MENU LABEL Furtka Live Installer (%ARCH%, BIOS)$/MENU LABEL (Recommended) Furtka Live Installer (%ARCH%, BIOS)/' \ "$PROFILE_WORK/syslinux/archiso_sys-linux.cfg" sed -i "/--id 'archlinux'/s/menuentry \"Furtka Live Installer/menuentry \"(Recommended) Furtka Live Installer/" \ "$PROFILE_WORK/grub/grub.cfg" "$PROFILE_WORK/grub/loopback.cfg" mkdir -p "$PROFILE_WORK/airootfs/opt/furtka" cp -a "$REPO_ROOT/webinstaller/." "$PROFILE_WORK/airootfs/opt/furtka/" # Ship the post-install asset tree (HTML, CSS, systemd units, scripts, …) # next to webinstaller/app.py so _resolve_assets_dir() finds it at runtime. cp -a "$REPO_ROOT/assets" "$PROFILE_WORK/airootfs/opt/furtka/assets" rm -rf "$PROFILE_WORK/airootfs/opt/furtka/__pycache__" # VERSION next to the webinstaller so the wizard footer can render the # release string at runtime instead of carrying a hardcoded one. Matches # what the resource-manager payload ships in its own VERSION file below. ISO_VERSION=$(grep -E '^version = ' "$REPO_ROOT/pyproject.toml" | head -1 | sed 's/.*= "\(.*\)"/\1/') echo "$ISO_VERSION" > "$PROFILE_WORK/airootfs/opt/furtka/VERSION" # Pack the resource manager (furtka/ Python package + bundled apps/) as a # tarball that webinstaller hands to archinstall via custom_commands. Lives at # a fixed path in the live ISO; the installed system reads it back, untars # into /opt/furtka/versions//, and gets a working `furtka` CLI + the # fileshare app. Same tarball shape as Phase-2 self-update releases, so an # ISO-installed box and an updated box converge on the same layout. echo "==> Bundling resource manager payload" PAYLOAD_STAGE="$(mktemp -d)" cp -a "$REPO_ROOT/furtka" "$PAYLOAD_STAGE/" cp -a "$REPO_ROOT/apps" "$PAYLOAD_STAGE/" # assets/ ships at the tarball root (not inside the Python package) because # Caddy, systemd, and the updater all expect it at /opt/furtka/current/assets/. cp -a "$REPO_ROOT/assets" "$PAYLOAD_STAGE/" find "$PAYLOAD_STAGE" -type d -name __pycache__ -exec rm -rf {} + # VERSION at tarball root: the installer reads it to choose the versions// # directory name and /opt/furtka/current/VERSION reports it at runtime. Same # value we wrote into /opt/furtka/VERSION for the live wizard footer above. echo "$ISO_VERSION" > "$PAYLOAD_STAGE/VERSION" tar -czf "$PROFILE_WORK/airootfs/opt/furtka-resource-manager.tar.gz" \ -C "$PAYLOAD_STAGE" . rm -rf "$PAYLOAD_STAGE" mkdir -p "$PROFILE_WORK/airootfs/etc/systemd/system/avahi-daemon.service.d" ln -sf /usr/lib/systemd/system/avahi-daemon.service \ "$PROFILE_WORK/airootfs/etc/systemd/system/multi-user.target.wants/avahi-daemon.service" echo "==> Building ISO (mkarchiso)" mkdir -p "$OUT_IN_CONTAINER" mkarchiso -v -w "$BUILD_WORK" -o "$OUT_IN_CONTAINER" "$PROFILE_WORK" echo echo "==> Done. ISO(s) in $OUT_IN_CONTAINER (on host: iso/out/):" ls -lh "$OUT_IN_CONTAINER"