2026-07-18 15:14:54 -07:00
|
|
|
#!/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 <tier> <arch> [--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 <tier> <arch> [--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")"
|
|
|
|
|
SNAPSHOT="$(cat "$OS_DIR/SNAPSHOT")"
|
|
|
|
|
SUITE="trixie"
|
|
|
|
|
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; }
|
2026-07-18 17:09:47 -07:00
|
|
|
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
|
|
|
|
|
}
|
2026-07-18 15:14:54 -07:00
|
|
|
|
|
|
|
|
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/<tier>.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"
|
|
|
|
|
|
|
|
|
|
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 \
|
2026-07-18 17:09:47 -07:00
|
|
|
--keyring="$DEBIAN_KEYRING" \
|
2026-07-18 15:14:54 -07:00
|
|
|
--include="$INCLUDE" \
|
|
|
|
|
--aptopt='Acquire::Check-Valid-Until "false"' \
|
|
|
|
|
"${HOOKS[@]}" \
|
|
|
|
|
"$SUITE" "$TAR" "${SOURCES[@]}"
|
|
|
|
|
|
|
|
|
|
echo "built $TAR"
|