#!/usr/bin/env bash # narOS rootfs builder (NAROS.md §2.2): mmdebstrap against the pinned Debian snapshot, # plus (optionally) the local Nucleic package pool. Produces a rootfs tar that CI turns # into an OCI image (naros-base/…) or the VM payload tarball. # # build-rootfs.sh [--pool DIR] [--out DIR] [--channel edge|stable] # # Needs mmdebstrap and either root or an unshare-capable user. The snapshot mirror serves # stale Release files by design, hence Check-Valid-Until off (standard snapshot practice). set -euo pipefail OS_DIR="$(cd "$(dirname "$0")/.." && pwd)" TIER="${1:?usage: build-rootfs.sh [--pool DIR] [--out DIR] [--channel C]}" ARCH="${2:?missing arch (arm64|amd64)}" shift 2 POOL="" OUT="$OS_DIR/dist" CHANNEL="${NAROS_CHANNEL:-edge}" while [ $# -gt 0 ]; do case "$1" in --pool) POOL="$(cd "$2" && pwd)"; shift 2 ;; --out) OUT="$2"; shift 2 ;; --channel) CHANNEL="$2"; shift 2 ;; *) echo "unknown arg: $1" >&2; exit 2 ;; esac done VERSION="$(cat "$OS_DIR/VERSION")" # Per-flavor base (NAROS.md §7.4): the VM DESKTOP flavor (N5) builds entirely from a pinned # Debian FORKY (testing) snapshot for GNOME 50 — trixie ships only GNOME 48 — while every # other tier, including the HEADLESS VM, stays on the trixie snapshot. One coherent pocket per # rootfs, no trixie/forky ABI mixing. VARIANT is the os-release identity class # (base/agent/runner/vm, §2.3); FLAVOR distinguishes the two VM rootfs builds within # VARIANT=vm (headless vs desktop). case "$TIER" in vm-desktop) SUITE="forky"; VARIANT="vm"; FLAVOR="desktop" SNAPSHOT="$(cat "$OS_DIR/SNAPSHOT.forky" 2>/dev/null || cat "$OS_DIR/SNAPSHOT")" ;; vm) SUITE="trixie"; VARIANT="vm"; FLAVOR="headless" SNAPSHOT="$(cat "$OS_DIR/SNAPSHOT")" ;; *) SUITE="trixie"; VARIANT="$TIER"; FLAVOR="" SNAPSHOT="$(cat "$OS_DIR/SNAPSHOT")" ;; esac MIRROR="https://snapshot.debian.org/archive/debian/${SNAPSHOT}/" PROFILE="$OS_DIR/mkimage/profiles/$TIER.pkgs" [ -f "$PROFILE" ] || { echo "no profile for tier '$TIER' ($PROFILE)" >&2; exit 2; } DEBIAN_KEYRING="${DEBIAN_ARCHIVE_KEYRING:-/usr/share/keyrings/debian-archive-keyring.gpg}" [ -r "$DEBIAN_KEYRING" ] || { echo "Debian archive keyring not found at $DEBIAN_KEYRING" >&2 echo "install a current debian-archive-keyring or set DEBIAN_ARCHIVE_KEYRING" >&2 exit 2 } pkg_list() { grep -vE '^\s*(#|$)' "$1" | tr '\n' ',' | sed 's/,$//'; } INCLUDE="$(pkg_list "$PROFILE")" SOURCES=("deb [check-valid-until=no] $MIRROR $SUITE main") if [ -n "$POOL" ]; then # Flat file:// repo over the pool; trusted because it is CI's own just-built artifact — # end-user trust comes from the signed hosted repo (repo/publish.sh), not this path. if [ ! -f "$POOL/Packages" ] || [ -n "$(find "$POOL" -name '*.deb' -newer "$POOL/Packages" 2>/dev/null)" ]; then (cd "$POOL" && dpkg-scanpackages --multiversion . > Packages) fi # Late packages (profiles/.late-pkgs) install via a finish hook AFTER every # Debian package is configured — mandatory for nash-default-shell: apt's configure # order is not deterministic, so a mid-transaction divert would run the remaining # Debian postinsts under nash (observed: ca-certificates' UTF-8 filenames mangled). # The divert must be the image's final configure step. LATE_DEBS=() LPROFILE="$OS_DIR/mkimage/profiles/$TIER.late-pkgs" if [ -f "$LPROFILE" ]; then while IFS= read -r pkg; do deb="$(ls "$POOL/${pkg}_"*.deb 2>/dev/null | head -1)" if [ -n "$deb" ]; then LATE_DEBS+=("$deb") elif [ "${NAROS_STRICT:-0}" = "1" ]; then echo "ERROR: late package $pkg absent from pool" >&2; exit 1 else echo "WARNING: late package $pkg absent from pool — building WITHOUT it" >&2 fi done < <(grep -vE '^\s*(#|$)' "$LPROFILE") fi NPROFILE="$OS_DIR/mkimage/profiles/$TIER.naros-pkgs" if [ -f "$NPROFILE" ]; then # Only request packages the pool actually carries: local builds legitimately lack # some (e.g. naros-keyring without the signing key). Loud per-package warning; # NAROS_STRICT=1 (CI) turns any gap into a hard failure. while IFS= read -r pkg; do if grep -qx "Package: $pkg" "$POOL/Packages"; then INCLUDE="$INCLUDE,$pkg" elif [ "${NAROS_STRICT:-0}" = "1" ]; then echo "ERROR: $pkg requested by $TIER tier but absent from pool" >&2; exit 1 else echo "WARNING: $pkg absent from pool — building WITHOUT it" >&2 fi done < <(grep -vE '^\s*(#|$)' "$NPROFILE") fi SOURCES+=("deb [trusted=yes] copy://$POOL ./") fi mkdir -p "$OUT" TAR="$OUT/naros-$TIER-$VERSION-$ARCH.tar" export NAROS_TIER="$TIER" NAROS_VERSION="$VERSION" NAROS_ARCH="$ARCH" \ NAROS_CHANNEL="$CHANNEL" NAROS_SNAPSHOT="$SNAPSHOT" NAROS_SUITE="$SUITE" \ NAROS_VARIANT="$VARIANT" NAROS_FLAVOR="$FLAVOR" HOOKS=() if [ "${#LATE_DEBS[@]}" -gt 0 ]; then LATE_ARGS="" for deb in "${LATE_DEBS[@]}"; do b="$(basename "$deb")" HOOKS+=(--customize-hook="copy-in $deb /tmp") LATE_ARGS="$LATE_ARGS /tmp/$b" done HOOKS+=(--customize-hook="chroot \"\$1\" dpkg -i$LATE_ARGS") HOOKS+=(--customize-hook="rm -f$(printf ' "$1"%s' $LATE_ARGS)") fi HOOKS+=(--customize-hook="$OS_DIR/mkimage/hooks/00-identity.sh"' "$1"') echo "narOS $VERSION/$CHANNEL: tier=$TIER arch=$ARCH snapshot=$SNAPSHOT pool=${POOL:-none}" mmdebstrap \ --architectures="$ARCH" \ --variant=apt \ --keyring="$DEBIAN_KEYRING" \ --include="$INCLUDE" \ --aptopt='Acquire::Check-Valid-Until "false"' \ "${HOOKS[@]}" \ "$SUITE" "$TAR" "${SOURCES[@]}" echo "built $TAR"