#!/bin/bash # Unified inner-build script — runs INSIDE the docker container (as root). # Invoked by iso/build.sh via 'docker run'. # # Replaces: _inner-build.sh, _inner-build-live.sh, _inner-build-niri-live.sh # # Required env: # ARCH REPO_URL KEYMAP LOCALE ISO_PKGS ISO_TITLE OUT_ISO_REL INCLUDE_DIR_REL # BUILD_TYPE — installer | live # DESKTOP — cinnamon | niri # KERNEL_PKG — linux | linux-mainline # # Optional env: # NOCTALIA_REPO — CDN URL for noctalia-shell/noctalia-qs (niri only) # NIX_PACKAGES_PREBAKE — space-separated nix package attrs (live only) # BOOT_CMDLINE — extra kernel command-line # HOST_UID / HOST_GID — fix output ownership for host user (default 1000) set -Eeuo pipefail # ── validate required env ───────────────────────────────────────────────── : "${ARCH:?}"; : "${REPO_URL:?}"; : "${KEYMAP:?}"; : "${LOCALE:?}" : "${ISO_PKGS:?}"; : "${ISO_TITLE:?}"; : "${OUT_ISO_REL:?}"; : "${INCLUDE_DIR_REL:?}" : "${BUILD_TYPE:?}"; : "${DESKTOP:?}"; : "${KERNEL_PKG:?}" # ── paths ───────────────────────────────────────────────────────────────── CACHE_DIR=/cache PROJECT_DIR=/work INCLUDE_DIR="$PROJECT_DIR/$INCLUDE_DIR_REL" OUT_ISO="$PROJECT_DIR/$OUT_ISO_REL" # Niri profiles use a separate mklive clone to avoid races on parallel builds. case "$DESKTOP" in niri) MKLIVE_DIR="$CACHE_DIR/void-mklive-niri" ;; *) MKLIVE_DIR="$CACHE_DIR/void-mklive" ;; esac # xbps package cache dir (per-desktop, same reason as above) case "$DESKTOP" in niri) XBPS_CACHE="$CACHE_DIR/xbps-niri-pkgs" ;; *) XBPS_CACHE="$CACHE_DIR/xbps-live-pkgs" ;; esac export PATH="$CACHE_DIR/xbps-static/usr/bin:$PATH" # ── sanity checks ───────────────────────────────────────────────────────── [[ -d "$MKLIVE_DIR" ]] || { echo "ERROR: $MKLIVE_DIR missing (should have been cloned on host)"; exit 1; } [[ -d "$INCLUDE_DIR" ]] || { echo "ERROR: $INCLUDE_DIR missing (staging failed on host)"; exit 1; } command -v xbps-install.static >/dev/null \ || { echo "ERROR: xbps-install.static not on PATH"; exit 1; } mkdir -p "$(dirname "$OUT_ISO")" "$XBPS_CACHE" # ═════════════════════════════════════════════════════════════════════════════ # 1) NIX PREBAKE (live ISOs only) # ═════════════════════════════════════════════════════════════════════════════ if [[ "$BUILD_TYPE" == "live" && -n "${NIX_PACKAGES_PREBAKE:-}" ]]; then echo ">>> pre-baking nix packages" read -r -a _NIX_PKGS <<< "$NIX_PACKAGES_PREBAKE" _NIX_CACHE="$CACHE_DIR/nix-prebake" _CACHE_KEY="$_NIX_CACHE/.done.$(printf '%s\n' "${_NIX_PKGS[@]}" | sort | md5sum | cut -c1-8)" mkdir -p "$_NIX_CACHE" if [[ -f "$_CACHE_KEY" && -d "$_NIX_CACHE/store" && -f "$_NIX_CACHE/.profile-path" ]]; then echo " restoring cached nix store ($(du -sh "$_NIX_CACHE/store" 2>/dev/null | cut -f1))" mkdir -p /nix rsync -a "$_NIX_CACHE/" /nix/ 2>&1 | tail -1 else echo " installing nix (single-user, no-daemon) ..." rm -rf /nix ~/.nix-profile ~/.nix-defexpr ~/.nix-channels mkdir -m 0755 -p /nix export NIX_CONFIG="build-users-group = " curl -fsSL https://nixos.org/nix/install | \ NIX_INSTALLER_TRUST_INSTALLER=1 sh -s -- --no-daemon --no-channel-add # shellcheck disable=SC1091 . /root/.nix-profile/etc/profile.d/nix.sh 2>/dev/null || true export PATH="/root/.nix-profile/bin:/nix/var/nix/profiles/default/bin:$PATH" export NIXPKGS_ALLOW_UNFREE=1 echo " nix profile install: ${_NIX_PKGS[*]}" nix profile add --extra-experimental-features "nix-command flakes" \ --impure "${_NIX_PKGS[@]}" 2>&1 readlink -f /root/.nix-profile > "$_NIX_CACHE/.profile-path" rsync -a /nix/ "$_NIX_CACHE/" 2>&1 | tail -1 touch "$_CACHE_KEY" echo " cached nix store: $(du -sh "$_NIX_CACHE/store" 2>/dev/null | cut -f1)" fi echo " staging /nix into overlay ($(du -sh /nix/store 2>/dev/null | cut -f1))" mkdir -p "$INCLUDE_DIR/nix" rsync -a /nix/ "$INCLUDE_DIR/nix/" 2>&1 | tail -1 # Single-user nix: live user (uid 1000) owns the store. chown -R 1000:1000 "$INCLUDE_DIR/nix" _STORE_PROFILE=$(cat "$_NIX_CACHE/.profile-path" 2>/dev/null \ || readlink -f /root/.nix-profile 2>/dev/null || echo "") if [[ -n "$_STORE_PROFILE" && -d "$_STORE_PROFILE" ]]; then mkdir -p "$INCLUDE_DIR/etc/skel" ln -sf "$_STORE_PROFILE" "$INCLUDE_DIR/etc/skel/.nix-profile" echo " skel/.nix-profile → $_STORE_PROFILE" else echo " WARNING: could not determine nix store profile path" fi fi # ═════════════════════════════════════════════════════════════════════════════ # 2) NOCTALIA LOCAL SIGNED REPO (niri profiles only) # # noctalia-qs has a broken .sig2 on the CDN; noctalia-shell's CDN key import # also fails intermittently (EAGAIN). Workaround: download both .xbps archives # directly (curl skips sig checks), create a LOCAL SIGNED repo with a fresh RSA # keypair, and register the public key in mklive/keys/ so copy_void_keys # pre-trusts it in the rootfs. Our local repo is passed last to mklive.sh # (-r last = HIGHEST priority) so xbps resolves and verifies both packages # against our trusted key. # ═════════════════════════════════════════════════════════════════════════════ _NOC_LOCAL="" if [[ "$DESKTOP" == "niri" && -n "${NOCTALIA_REPO:-}" ]]; then echo ">>> building local signed noctalia XBPS repo (CDN .sig2 workaround)" _NOC_LOCAL="/tmp/noctalia-local" _NOC_HOME="/tmp/noc-sign-home" mkdir -p "$_NOC_LOCAL" "$_NOC_HOME" export HOME="$_NOC_HOME" # Discover exact package versions from CDN repodata _NOC_VERS=$(python3 - <<'PYEOF' 2>/dev/null import urllib.request, plistlib, tarfile, io, os repo = os.environ.get("NOCTALIA_REPO", "https://universalrepo.r1xelelo.workers.dev/void") arch = os.environ.get("ARCH", "x86_64") want = {"noctalia-qs", "noctalia-shell"} found = {} for ua in ("curl/8.0", "xbps/0.59.2", "Mozilla/5.0 (X11; Linux x86_64)"): try: req = urllib.request.Request(f"{repo}/{arch}-repodata", headers={"User-Agent": ua}) data = urllib.request.urlopen(req, timeout=15).read() tf = tarfile.open(fileobj=io.BytesIO(data)) idx = plistlib.loads(tf.extractfile("index.plist").read()) for pkgname, meta in idx.items(): if isinstance(meta, dict) and pkgname in want: found[pkgname] = meta.get("pkgver", pkgname) if len(found) >= len(want): break except Exception: pass # Fallback to versions confirmed by previous builds defaults = {"noctalia-qs": "noctalia-qs-0.0.12_0", "noctalia-shell": "noctalia-shell-4.7.6_1"} for pkg, ver in defaults.items(): if pkg not in found: found[pkg] = ver for ver in found.values(): print(ver) PYEOF ) for _ver in $_NOC_VERS; do _fname="${_ver}.${ARCH}.xbps" # Clear any cached bad sig2 from a previous failed build rm -f "$CACHE_DIR/xbps-niri-pkgs/${_ver}"* 2>/dev/null || true if [[ ! -f "$_NOC_LOCAL/$_fname" ]]; then echo " downloading $_fname ..." curl -fsSL "$NOCTALIA_REPO/$_fname" -o "$_NOC_LOCAL/$_fname" \ || { echo " WARNING: failed to download $_fname"; rm -f "$_NOC_LOCAL/$_fname"; } else echo " cached: $_fname" fi done xbps-rindex.static -a "$_NOC_LOCAL"/*.xbps mkdir -p "$_NOC_HOME/.xbps-sign" openssl genrsa -out "$_NOC_HOME/.xbps-sign/privkey.pem" 4096 2>/dev/null [[ -s "$_NOC_HOME/.xbps-sign/privkey.pem" ]] \ || { echo "ERROR: openssl genrsa failed"; exit 1; } _NOC_PRIVKEY="$_NOC_HOME/.xbps-sign/privkey.pem" xbps-rindex.static --sign --privkey "$_NOC_PRIVKEY" \ --signedby "noctalia-local" "$_NOC_LOCAL" for _pkg in "$_NOC_LOCAL"/*.xbps; do xbps-rindex.static --sign-pkg --privkey "$_NOC_PRIVKEY" \ --signedby "noctalia-local" "$_pkg" done openssl rsa -in "$_NOC_PRIVKEY" \ -pubout -outform DER -out "$_NOC_HOME/pubkey.der" 2>/dev/null _FINGERPRINT=$(md5sum "$_NOC_HOME/pubkey.der" \ | cut -d' ' -f1 | sed 's/../&:/g; s/:$//') _PUBKEY_B64=$(openssl rsa -in "$_NOC_PRIVKEY" \ -pubout 2>/dev/null | base64 -w 0) cat > "$MKLIVE_DIR/keys/$_FINGERPRINT.plist" < public-key $_PUBKEY_B64 public-key-size 4096 signature-by noctalia-local KEOF echo " local signed noctalia repo ready — key: $_FINGERPRINT" unset HOME fi # ═════════════════════════════════════════════════════════════════════════════ # 3) DCONF POSTSETUP SCRIPT (live ISOs only — both cinnamon and niri) # # Compile system-db AND the skel user dconf db using Void's own binary inside # the mklive rootfs chroot. No cross-distro format mismatch possible. # ═════════════════════════════════════════════════════════════════════════════ _DCONF_POSTSETUP="" if [[ "$BUILD_TYPE" == "live" ]]; then _DCONF_POSTSETUP="$(mktemp -p "$MKLIVE_DIR" postsetup-dconf.XXXXX.sh)" cat > "$_DCONF_POSTSETUP" <<'PSEOF' #!/bin/bash ROOTFS="$1" if [[ -x "$ROOTFS/usr/bin/dconf" ]] && [[ -d "$ROOTFS/etc/dconf/db/local.d" ]]; then chroot "$ROOTFS" dconf compile /etc/dconf/db/local /etc/dconf/db/local.d \ && echo "postsetup-dconf: system-db compiled ($(chroot "$ROOTFS" dconf --version 2>/dev/null))" \ || echo "postsetup-dconf: system-db compile failed (non-fatal)" mkdir -p "$ROOTFS/etc/skel/.config/dconf" chroot "$ROOTFS" dconf compile /etc/skel/.config/dconf/user /etc/dconf/db/local.d \ && echo "postsetup-dconf: skel user dconf db compiled" \ || echo "postsetup-dconf: skel user dconf db compile failed (non-fatal)" else echo "postsetup-dconf: dconf or keyfile dir not found in rootfs — skipping" fi PSEOF chmod +x "$_DCONF_POSTSETUP" fi # ═════════════════════════════════════════════════════════════════════════════ # 4) CLEANUP TRAP # ═════════════════════════════════════════════════════════════════════════════ cd "$MKLIVE_DIR" _cleanup_mklive_builds() { local d sub for d in "$MKLIVE_DIR"/mklive-build.*/; do [[ -d "$d" ]] || continue for sub in tmp-rootfs/sys tmp-rootfs/proc tmp-rootfs/dev tmp-rootfs/run \ image/rootfs/sys image/rootfs/proc image/rootfs/dev image/rootfs/run; do [[ -d "$d$sub" ]] && umount -R --lazy "$d$sub" 2>/dev/null || true done rm -rf "$d" 2>/dev/null || true done } trap _cleanup_mklive_builds EXIT # ═════════════════════════════════════════════════════════════════════════════ # 5) BUILD MKLIVE ARGS # ═════════════════════════════════════════════════════════════════════════════ _MKLIVE_ARGS=( -a "$ARCH" -r "$REPO_URL" -r "${REPO_URL%/current}/current/nonfree" -c "$XBPS_CACHE" -H "$CACHE_DIR/xbps-host-pkgs" -k "$KEYMAP" -l "$LOCALE" -T "$ISO_TITLE" -p "$ISO_PKGS" -I "$INCLUDE_DIR" -C "${BOOT_CMDLINE:-}" -o "$OUT_ISO" ) # Mainline kernel: pass as primary kernel so mklive builds the right initramfs. # On dual-kernel ISOs the patched mklive loop also builds an entry for 'linux'. [[ "$KERNEL_PKG" == "linux-mainline" ]] && _MKLIVE_ARGS+=( -v linux-mainline ) # Noctalia repos (niri only). # Order matters: LAST -r = HIGHEST priority. # We add CDN first, then our locally-signed repo so xbps resolves noctalia-* # from the trusted local copy. if [[ "$DESKTOP" == "niri" && -n "${NOCTALIA_REPO:-}" ]]; then _MKLIVE_ARGS+=( -r "$NOCTALIA_REPO" ) [[ -n "$_NOC_LOCAL" ]] && _MKLIVE_ARGS+=( -r "$_NOC_LOCAL" ) fi # NVIDIA postsetup (all builds) _MKLIVE_ARGS+=( -x "$PROJECT_DIR/iso/postsetup-nvidia.sh" ) # dconf postsetup (live ISOs only) [[ -n "$_DCONF_POSTSETUP" ]] && _MKLIVE_ARGS+=( -x "$_DCONF_POSTSETUP" ) # ═════════════════════════════════════════════════════════════════════════════ # 6) RUN MKLIVE # ═════════════════════════════════════════════════════════════════════════════ echo ">>> running mklive.sh" echo " type : $BUILD_TYPE" echo " desktop : $DESKTOP" echo " kernel : $KERNEL_PKG" echo " output : $OUT_ISO" ./mklive.sh "${_MKLIVE_ARGS[@]}" # Fix ownership so the host user can clean up without sudo. chmod -R u+rwX "$INCLUDE_DIR" 2>/dev/null || true chown -R "${HOST_UID:-1000}:${HOST_GID:-1000}" "$INCLUDE_DIR" 2>/dev/null || true chown -R "${HOST_UID:-1000}:${HOST_GID:-1000}" "$OUT_ISO" "${OUT_ISO}".* 2>/dev/null || true