#!/bin/bash
#
# build-containers.sh — Build prebuilt .webcontainer images for WebCommander
# using systemd-nspawn with a musl/LLVM/libc++/compiler-rt toolchain.
#
# Produces: postgresql.webcontainer, authdb.webcontainer, mediadb.webcontainer,
#           blogi.webcontainer, blogi-editor.webcontainer
#
# Each .webcontainer is a tar containing:
#   data.tar.xz  — compressed rootfs with all binaries/config pre-installed
#   info.xml     — metadata (name, version, architecture, packages, services)
#
# Usage: sudo ./packaging/build-containers.sh [--clean] [PHASE...]
# Phases: chroot, toolchain, libraries, postgresql, authdb, mediadb, blogi,
#         blogi-editor, package, all (default)
#
# Requires: systemd-container, debootstrap, wget
#
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
PROJECT_DIR="$(cd "$SCRIPT_DIR/.." && pwd)"

# Configuration
ARCH="${ARCH:-amd64}"
CHROOT_BASE="/var/lib/fenriros-buildroot"
CHROOT_DIR="$CHROOT_BASE/rootfs"
BUILD_DIR="/build"
# Output must live on a local filesystem: this script runs as root for nspawn,
# and PROJECT_DIR is typically under an NFS home with root_squash, where root
# is mapped to "nobody" and cannot write. Default to the local buildroot and
# allow override via the OUTPUT_DIR env var.
OUTPUT_DIR="${OUTPUT_DIR:-$CHROOT_BASE/output}"
CONTAINER_DIR="$CHROOT_BASE/containers"
MUSL_VERSION="1.2.5"
LLVM_BOOTSTRAP="18"
LLVM_VERSION="22"
LLVM_TAG="llvmorg-${LLVM_VERSION}.1.0"
PG_BRANCH="REL_17_STABLE"
PG_VERSION="17"
# Build tools shipped inside the chroot toolchain: makeplus (GNU Make
# compatible build system in C++17) replaces GNU make, and CMake is rebuilt
# from source using makeplus so the whole toolchain is self-hosted on musl.
CMAKE_VERSION="3.31.6"
CMAKE_TAG="v${CMAKE_VERSION}"

# ── Final system image (finalize / iso phase) ──────────────────────────────
# The finalization step assembles a *runnable, GNU-free* system image: musl +
# toybox userland, LLVM "binutils" (llvm-objcopy/llvm-strip/...), makeplus as
# make, and the prebuilt .webcontainer payloads. It is booted via the limine
# bootloader (BSD-licensed, BIOS+UEFI) and mastered with xorriso.
LINUX_BRANCH="${LINUX_BRANCH:-v6.18}"          # kernel tag, built with LLVM=1
# Realtek NIC firmware (rtl_nic/*.fw, incl. rtl8127a-1.fw for the RTL8127A
# 10GbE controller — driven in-tree by r8169 since 6.16). Pulled from the
# upstream linux-firmware tree and staged into /lib/firmware so request_firmware
# finds the PHY blobs at runtime. GPL/redistributable per WHENCE.
LINUX_FIRMWARE_REPO="${LINUX_FIRMWARE_REPO:-https://git.kernel.org/pub/scm/linux/kernel/git/firmware/linux-firmware.git}"
LIMINE_VERSION="${LIMINE_VERSION:-v8.x-binary}" # limine prebuilt-binary branch
ISO_LABEL="${ISO_LABEL:-FENRIROS}"
# libelf: the modern elfutils libelf (kernel/objtool detect it via
# `pkg-config libelf`). Built GNU-free from source with a custom CMakeLists,
# replacing the ancient 2009 Michael Riepe libelf 0.8.x. Cloned at a pinned
# tag so the build is reproducible; override ELFUTILS_TAG to track another.
LIBELF_VERSION="${LIBELF_VERSION:-0.193}"
ELFUTILS_TAG="${ELFUTILS_TAG:-elfutils-${LIBELF_VERSION}}"
# zlib is a hard dependency of elfutils libelf (compressed ELF sections).
# zlib's license is permissive (GNU-free) and it ships its own CMakeLists.
ZLIB_VERSION="${ZLIB_VERSION:-1.3.1}"
ZLIB_TAG="${ZLIB_TAG:-v${ZLIB_VERSION}}"
# LibreSSL provides a GNU-free libcrypto/libssl (and the legacy 1.0 APIs that
# the kernel's certs/extract-cert host tool relies on, which OpenSSL 3.x
# removed). Built into the musl sysroot so the musl clang resolves -lcrypto to
# it instead of the host's glibc OpenSSL.
LIBRESSL_VERSION="${LIBRESSL_VERSION:-4.0.0}"

# OpenSSH (portable) provides a GNU-free, BSD-licensed SSH server/client. Built
# against musl with its bundled crypto (--without-openssl) so it has no LibreSSL
# version coupling and ships ed25519 host keys + chacha20-poly1305 by default.
OPENSSH_VERSION="${OPENSSH_VERSION:-9.9p2}"
# Out-of-the-box root SSH credential for the system image. CHANGE THIS: override
# with ROOT_PASSWORD=... at build time, or set SSH_AUTHORIZED_KEY='ssh-ed25519 ...'
# for key-only login. Empty passwords are never permitted by sshd_config.
ROOT_PASSWORD="${ROOT_PASSWORD:-fenriros}"
SSH_AUTHORIZED_KEY="${SSH_AUTHORIZED_KEY:-}"

# e2fsprogs (ext2/3/4 userspace: mke2fs, e2fsck, tune2fs, resize2fs, ...) and
# dosfstools (FAT: mkfs.fat, fsck.fat, fatlabel). Both are built with the
# musl/LLVM toolchain through a GNU-free CMake build (files/<name>/CMakeLists.txt)
# instead of their autotools/GNU-make build.
E2FSPROGS_VERSION="${E2FSPROGS_VERSION:-1.47.4}"
DOSFSTOOLS_VERSION="${DOSFSTOOLS_VERSION:-4.2}"

# gptfdisk (sgdisk) is the scriptable GPT partitioner the installer drives; popt
# is its command-line-parsing dependency. Both are built GNU-free via CMake and
# statically link the e2fsprogs libuuid, so no util-linux is needed.
GPTFDISK_VERSION="${GPTFDISK_VERSION:-1.0.10}"

# rsync is a fast, versatile file-copying tool (GPL-licensed). Built with the
# musl/LLVM toolchain through a GNU-free CMake build (files/rsync/CMakeLists.txt)
# after a one-time ./configure probe for config.h.
RSYNC_VERSION="${RSYNC_VERSION:-3.4.1}"

# ncurses is the standard terminal UI library (MIT/X11 license). Built as
# a wide-character static library (ncursesw) with CMake after a one-time
# ./configure probe for ncurses_cfg.h.
NCURSES_VERSION="${NCURSES_VERSION:-6.5}"

# Lua is a lightweight embeddable scripting language. Used for the FenrirOS
# installer and other system scripts where toybox sh is too limited.
LUA_VERSION="${LUA_VERSION:-5.4.7}"

# Git repositories
GIT_BASE="https://git.tuxist.de/git"
declare -A REPOS=(
    # System/third-party
    [json-c]="https://github.com/json-c/json-c.git"
    [libyaml]="https://github.com/yaml/libyaml.git"
    [brotli]="https://github.com/google/brotli.git"
    [tinyxml2]="https://github.com/leethomason/tinyxml2.git"
    [libxml2]="https://github.com/GNOME/libxml2.git"
    [sqlite]="https://github.com/sqlite/sqlite.git"
    [postgres]="https://github.com/postgres/postgres.git"
    [libressl]="https://ftp.openbsd.org/pub/OpenBSD/LibreSSL/libressl-4.0.0.tar.gz"
    [openssh]="https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-9.9p2.tar.gz"
    [toybox]="https://github.com/landley/toybox.git"
    [e2fsprogs]="https://git.kernel.org/pub/scm/fs/ext2/e2fsprogs.git"
    [dosfstools]="https://github.com/dosfstools/dosfstools.git"
    [gptfdisk]="https://git.code.sf.net/p/gptfdisk/code"
    [popt]="https://github.com/rpm-software-management/popt.git"
    [rsync]="https://download.samba.org/pub/rsync/src/"
    [cmake]="https://github.com/Kitware/CMake.git"
    [makeplus]="$GIT_BASE/tuxist/makeplus.git"
    [zlib]="https://github.com/madler/zlib.git"
    [elfutils]="https://sourceware.org/git/elfutils.git"
    # Custom libraries
    [libcmdplus]="$GIT_BASE/jan.koester/libcmdplus.git"
    [uuidplus]="$GIT_BASE/tuxist/uuidplus.git"
    [libnetplus]="$GIT_BASE/jan.koester/libnetplus.git"
    [libhtmlpp]="$GIT_BASE/jan.koester/libhtmlpp.git"
    [libhttppp]="$GIT_BASE/jan.koester/libhttppp.git"
    [libconfplus]="$GIT_BASE/jan.koester/libconfplus.git"
    [libdbpp]="$GIT_BASE/jan.koester/libdbpp.git"
    [libgameinfoplus]="$GIT_BASE/jan.koester/libgameinfoplus.git"
    [libparitypp]="$GIT_BASE/tuxist/libparitypp.git"
    [authdb]="$GIT_BASE/tuxist/authdb.git"
    [mediadb]="$GIT_BASE/tuxist/mediadb.git"
    [smtpclient]="https://github.com/Tuxist/CPP-SMTPClient-library.git"
    [blogi]="$GIT_BASE/jan.koester/blogi.git"
    [syslite]="$GIT_BASE/tuxist/syslite.git"
    # Package management
    [zstd]="https://github.com/facebook/zstd.git"
    [file]="https://github.com/file/file.git"
    [libarchive]="https://github.com/libarchive/libarchive.git"
    [rpm]="https://github.com/rpm-software-management/rpm.git"
    # Final system image (kernel + bootloader, both GNU-free)
    [linux]="https://github.com/torvalds/linux.git"
    [limine]="https://github.com/limine-bootloader/limine.git"
)

# Parse arguments
CLEAN=0
DEBUG=0
PHASES=()
while [[ $# -gt 0 ]]; do
    case "$1" in
        --clean) CLEAN=1; shift ;;
        --debug) DEBUG=1; shift ;;
        --arch) ARCH="$2"; shift 2 ;;
        -h|--help)
            echo "Usage: $0 [--clean] [--debug] [--arch amd64] [PHASE...]"
            echo ""
            echo "Options:"
            echo "  --clean    Remove existing chroot before bootstrap"
            echo "  --debug    Build with debug symbols, include lldb + clang in containers"
            echo ""
            echo "Phases: chroot, toolchain, buildtools, libraries, zlib, libelf,"
            echo "        postgresql, authdb, mediadb, blogi, blogi-editor, syslite,"
            echo "        package, devel, shell, devel-tar, kernel, iso, finalize,"
            echo "        all (default)"
            echo ""
            echo "  buildtools — build toybox (sed/awk/sh), makeplus (make) and a"
            echo "               musl-native CMake (built with makeplus) into the chroot"
            echo "  devel      — prepare a full development chroot (chroot + toolchain +"
            echo "               buildtools + libraries) and drop into an interactive shell"
            echo "  shell      — enter an interactive shell in the existing chroot without"
            echo "               (re)building anything"
            echo "  devel-tar  — pack the development chroot rootfs into a tarball"
            echo "               (\$OUTPUT_DIR/fenriros-devel-<arch>.tar.zst by default)"
            echo "  zlib       — build zlib (musl) into the toolchain (/usr/local)"
            echo "  libelf     — build elfutils libelf (musl, needs zlib) into /usr/local"
            echo "  libressl   — build LibreSSL (musl) into /usr/local/musl (kernel certs)"
            echo "  openssh    — build the OpenSSH server/client (musl) for the system image"
            echo "  dosfstools — build FAT tools mkfs.fat/fsck.fat/fatlabel (musl, CMake)"
            echo "  e2fsprogs  — build ext2/3/4 tools mke2fs/e2fsck/resize2fs/... (musl, CMake)"
            echo "  popt       — build static libpopt.a (sgdisk CLI dependency, musl, CMake)"
            echo "  gptfdisk   — build the sgdisk GPT partitioner (musl, CMake, GNU-free)"
            echo "  rsync      — build the rsync file-copying tool (musl, CMake, GNU-free)"
            echo "  lua        — build the Lua interpreter (musl, static, GNU-free)"
            echo "  ncurses    — build the ncurses terminal UI library (musl, CMake, GNU-free)"
            echo "  zstd       — build zstd compression library (musl, CMake)"
            echo "  file       — build file/libmagic (file type detection, musl, CMake)"
            echo "  libarchive — build libarchive (archive handling, musl, CMake)"
            echo "  rpm        — build RPM 6.0.1 package manager (musl, LLVM, GNU-free)"
            echo "  kernel     — build the Linux kernel with the LLVM toolchain (no GNU)"
            echo "  iso        — master a bootable, GNU-free system ISO from the build"
            echo "  finalize   — full pipeline: toolchain..containers + libelf + kernel + ISO"
            exit 0
            ;;
        chroot|toolchain|buildtools|libraries|zlib|libelf|libressl|openssh|dosfstools|e2fsprogs|popt|gptfdisk|rsync|lua|ncurses|zstd|file|libarchive|rpm|installer|postgresql|authdb|mediadb|blogi|blogi-editor|syslite|package|devel|shell|devel-tar|kernel|iso|finalize|all)
            PHASES+=("$1"); shift ;;
        *) echo "Unknown option: $1"; exit 1 ;;
    esac
done

[[ ${#PHASES[@]} -eq 0 ]] && PHASES=("all")

# Set build type based on --debug flag
if [[ $DEBUG -eq 1 ]]; then
    BUILD_TYPE="Debug"
    echo "*** DEBUG BUILD: debug symbols enabled, lldb/clang will be included in containers ***"
else
    BUILD_TYPE="Release"
fi

# nspawn wrapper — use --register=no to avoid machine name conflicts
NSPAWN="systemd-nspawn --register=no"

if [[ $EUID -ne 0 ]]; then
    echo "This script must be run as root (for nspawn)."
    exit 1
fi

# ═══════════════════════════════════════════════════════════════
# Phase 1: Create minimal Debian chroot
# ═══════════════════════════════════════════════════════════════
setup_chroot() {
    if [[ $CLEAN -eq 1 ]] && [[ -d "$CHROOT_DIR" ]]; then
        echo "==> Cleaning existing chroot..."
        rm -rf "$CHROOT_DIR"
    fi

    if [[ -d "$CHROOT_DIR/usr/bin" ]]; then
        echo "==> Chroot already exists, skipping bootstrap"
        return
    fi

    echo "==> Creating Debian chroot at $CHROOT_DIR ..."
    mkdir -p "$CHROOT_DIR"
    debootstrap --variant=minbase --arch="$ARCH" trixie "$CHROOT_DIR" http://deb.debian.org/debian

    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c '
        apt-get update
        apt-get install -y --no-install-recommends \
            ca-certificates wget git cmake ninja-build pkg-config autoconf automake libtool \
            xz-utils python3 file bison flex libreadline-dev zlib1g-dev make
    '
}

# ═══════════════════════════════════════════════════════════════
# Phase 2: LLVM + musl + libc++ toolchain
# ═══════════════════════════════════════════════════════════════
ensure_kernel_headers() {
    if [[ -f "$CHROOT_DIR/usr/local/musl/include/asm/types.h" ]]; then
        echo "==> Kernel headers already present"
        return
    fi

    echo "==> Installing Linux kernel headers into musl sysroot..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c '
        set -e
        apt-get install -y --no-install-recommends linux-libc-dev
        rm -rf /usr/local/musl/include/linux /usr/local/musl/include/asm-generic /usr/local/musl/include/asm
        cp -aL /usr/include/linux /usr/local/musl/include/
        cp -aL /usr/include/asm-generic /usr/local/musl/include/
        MULTIARCH=$(dpkg-architecture -qDEB_HOST_MULTIARCH 2>/dev/null || echo "x86_64-linux-gnu")
        if [ -d "/usr/include/${MULTIARCH}/asm" ]; then
            cp -aL "/usr/include/${MULTIARCH}/asm" /usr/local/musl/include/
        elif [ -d /usr/include/asm ]; then
            cp -a /usr/include/asm /usr/local/musl/include/
        else
            mkdir -p /usr/local/musl/include/asm
            for f in /usr/local/musl/include/asm-generic/*.h; do
                name=$(basename "$f")
                echo "#include <asm-generic/${name}>" > "/usr/local/musl/include/asm/${name}"
            done
        fi
        ls -la /usr/local/musl/include/asm/types.h || { echo "FATAL: asm/types.h not found"; exit 1; }
    '
}

setup_toolchain() {
    if [[ -f "$CHROOT_DIR/usr/local/musl/lib/libc++.a" ]]; then
        echo "==> Runtimes already built, skipping LLVM/musl/runtimes build"
        ensure_kernel_headers
    else
        echo "==> Setting up LLVM ${LLVM_VERSION} + musl + libc++ toolchain..."
        echo "    Bootstrap compiler: clang-${LLVM_BOOTSTRAP} (Debian)"
        echo "    Target toolchain:   LLVM ${LLVM_VERSION} (source-built against musl)"

        # ── Stage 1: Install bootstrap compiler from Debian ──
        local musl_cmake="$SCRIPT_DIR/files/musl/CMakeLists.txt"
        if [[ ! -f "$musl_cmake" ]]; then
            echo "ERROR: missing $musl_cmake (musl CMake build file)." >&2
            exit 1
        fi
        $NSPAWN -D "$CHROOT_DIR" --bind-ro="$musl_cmake:/build/musl-CMakeLists.txt" --pipe -- bash -c "
            set -e
            apt-get install -y --no-install-recommends \
                clang-${LLVM_BOOTSTRAP} lld-${LLVM_BOOTSTRAP} \
                llvm-${LLVM_BOOTSTRAP}-dev \
                libc++-${LLVM_BOOTSTRAP}-dev libc++abi-${LLVM_BOOTSTRAP}-dev \
                libunwind-${LLVM_BOOTSTRAP}-dev \
                libclang-rt-${LLVM_BOOTSTRAP}-dev

            update-alternatives --install /usr/bin/clang clang /usr/bin/clang-${LLVM_BOOTSTRAP} 100
            update-alternatives --install /usr/bin/clang++ clang++ /usr/bin/clang++-${LLVM_BOOTSTRAP} 100
            update-alternatives --install /usr/bin/lld lld /usr/bin/lld-${LLVM_BOOTSTRAP} 100
            update-alternatives --install /usr/bin/ld.lld ld.lld /usr/bin/ld.lld-${LLVM_BOOTSTRAP} 100
            ln -sf /usr/bin/clang-${LLVM_BOOTSTRAP} /usr/bin/cc
            ln -sf /usr/bin/clang++-${LLVM_BOOTSTRAP} /usr/bin/c++

            # ── Stage 2: Build musl libc (CMake, no autotools/GNU make) ──
            cd /tmp
            if [ ! -f musl-${MUSL_VERSION}.tar.gz ]; then
                wget -q https://musl.libc.org/releases/musl-${MUSL_VERSION}.tar.gz
            fi
            rm -rf musl-${MUSL_VERSION}
            tar xf musl-${MUSL_VERSION}.tar.gz
            cd musl-${MUSL_VERSION}
            # Drop in the GNU-free CMake build (overrides any upstream build files).
            cp /build/musl-CMakeLists.txt CMakeLists.txt
            cmake -S . -B build-cmake -G Ninja \
                -DCMAKE_C_COMPILER=clang-${LLVM_BOOTSTRAP} \
                -DCMAKE_ASM_COMPILER=clang-${LLVM_BOOTSTRAP} \
                -DCMAKE_BUILD_TYPE=Release \
                -DCMAKE_INSTALL_PREFIX=/usr/local/musl \
                -DMUSL_ARCH=x86_64 \
                -DMUSL_SYSLIBDIR=/usr/local/musl/lib
            cmake --build build-cmake -j\$(nproc)
            cmake --install build-cmake
            # Ensure the dynamic-linker symlink exists (belt and suspenders).
            ln -sf /usr/local/musl/lib/libc.so /usr/local/musl/lib/ld-musl-x86_64.so.1
            echo '==> musl installed (CMake build)'
        "

        ensure_kernel_headers

        # ── Clone LLVM source once (shared by Stage 3, 4, 6) ──
        echo "==> Ensuring LLVM ${LLVM_VERSION} source is available..."
        $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
            set -e
            mkdir -p /build
            if [ ! -d /build/llvm-project ]; then
                echo '  [clone] LLVM ${LLVM_VERSION} source...'
                git clone --depth 1 --branch ${LLVM_TAG} \
                    https://github.com/llvm/llvm-project.git /build/llvm-project
            else
                echo '  [skip] LLVM source already present'
            fi
        "

        # ── Stage 3: Build glibc-linked clang-22 (host compiler for runtimes) ──
        echo "==> Building glibc-linked LLVM ${LLVM_VERSION} (host compiler for musl runtimes)..."
        $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
            set -e
            LLVM_SRC=/build/llvm-project

            if [ -x /build/llvm-host/bin/clang ]; then
                echo '  [skip] Host clang-${LLVM_VERSION} already built'
                exit 0
            fi

            rm -rf /build/build-llvm-host
            mkdir -p /build/build-llvm-host && cd /build/build-llvm-host
            cmake \${LLVM_SRC}/llvm -G Ninja \
                -DCMAKE_C_COMPILER=clang-${LLVM_BOOTSTRAP} \
                -DCMAKE_CXX_COMPILER=clang++-${LLVM_BOOTSTRAP} \
                -DCMAKE_BUILD_TYPE=Release \
                -DCMAKE_INSTALL_PREFIX=/build/llvm-host \
                -DLLVM_ENABLE_PROJECTS='clang;lld' \
                -DLLVM_TARGETS_TO_BUILD=X86 \
                -DLLVM_DEFAULT_TARGET_TRIPLE=x86_64-linux-gnu \
                -DLLVM_ENABLE_TERMINFO=OFF \
                -DLLVM_ENABLE_ZLIB=OFF \
                -DLLVM_ENABLE_ZSTD=OFF \
                -DLLVM_ENABLE_LIBXML2=OFF \
                -DLLVM_ENABLE_BINDINGS=OFF \
                -DLLVM_BUILD_EXAMPLES=OFF \
                -DLLVM_INCLUDE_TESTS=OFF \
                -DLLVM_INCLUDE_BENCHMARKS=OFF \
                -DLLVM_INCLUDE_EXAMPLES=OFF \
                -DLLVM_BUILD_TOOLS=ON
            ninja -j\$(nproc) clang lld
            ninja install-clang install-lld install-clang-resource-headers 2>/dev/null || ninja install
            echo '==> Host clang-${LLVM_VERSION} (glibc-linked) built at /build/llvm-host'
            file /build/llvm-host/bin/clang
        "

        # Verify Stage 3 succeeded
        if [[ ! -x "$CHROOT_DIR/build/llvm-host/bin/clang" ]]; then
            echo "ERROR: Stage 3 failed — /build/llvm-host/bin/clang not found. Cannot proceed."
            exit 1
        fi

        # ── Stage 4: Build LLVM ${LLVM_VERSION} runtimes against musl (using host clang-22) ──
        echo "==> Building LLVM ${LLVM_VERSION} runtimes (compiler-rt, libunwind, libc++abi, libc++) against musl..."
        $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
            set -e
            LLVM_SRC=/build/llvm-project

            HOST_CLANG=/build/llvm-host/bin/clang
            HOST_CLANGXX=/build/llvm-host/bin/clang++
            # Use bootstrap clang-18's compiler-rt builtins for linking
            RT_PATH=\$(find /usr/lib/llvm-${LLVM_BOOTSTRAP} -name 'libclang_rt.builtins*' -printf '%h' -quit 2>/dev/null)
            if [ -z \"\${RT_PATH}\" ]; then
                echo \"ERROR: Cannot find clang-${LLVM_BOOTSTRAP} builtins\"
                find /usr/lib/llvm-${LLVM_BOOTSTRAP} -name '*clang_rt*' 2>/dev/null || true
                exit 1
            fi
            echo \"  Using RT_PATH=\${RT_PATH}\"

            rm -rf /build/build-runtimes
            mkdir -p /build/build-runtimes && cd /build/build-runtimes
            cmake \${LLVM_SRC}/runtimes -G Ninja \
                -DCMAKE_C_COMPILER=\${HOST_CLANG} \
                -DCMAKE_CXX_COMPILER=\${HOST_CLANGXX} \
                -DCMAKE_C_COMPILER_WORKS=TRUE \
                -DCMAKE_CXX_COMPILER_WORKS=TRUE \
                -DCMAKE_C_COMPILER_TARGET=x86_64-linux-musl \
                -DCMAKE_CXX_COMPILER_TARGET=x86_64-linux-musl \
                -DCMAKE_C_FLAGS=\"--target=x86_64-linux-musl --sysroot=/usr/local/musl -fPIC\" \
                -DCMAKE_CXX_FLAGS=\"--target=x86_64-linux-musl --sysroot=/usr/local/musl -fPIC\" \
                -DCMAKE_EXE_LINKER_FLAGS=\"-fuse-ld=lld --target=x86_64-linux-musl -L/usr/local/musl/lib -L\${RT_PATH} -rtlib=compiler-rt -nostdlib -lc\" \
                -DCMAKE_SHARED_LINKER_FLAGS=\"-fuse-ld=lld --target=x86_64-linux-musl -L/usr/local/musl/lib -L\${RT_PATH} -rtlib=compiler-rt\" \
                -DCMAKE_INSTALL_PREFIX=/usr/local/musl \
                -DLLVM_ENABLE_RUNTIMES=\"compiler-rt;libunwind;libcxxabi;libcxx\" \
                -DCOMPILER_RT_BUILD_BUILTINS=ON \
                -DCOMPILER_RT_BUILD_SANITIZERS=OFF \
                -DCOMPILER_RT_BUILD_XRAY=OFF \
                -DCOMPILER_RT_BUILD_LIBFUZZER=OFF \
                -DCOMPILER_RT_BUILD_PROFILE=OFF \
                -DCOMPILER_RT_BUILD_ORC=OFF \
                -DCOMPILER_RT_BUILD_MEMPROF=OFF \
                -DCOMPILER_RT_DEFAULT_TARGET_ONLY=ON \
                -DLIBUNWIND_ENABLE_SHARED=OFF \
                -DLIBUNWIND_USE_COMPILER_RT=ON \
                -DLIBCXXABI_ENABLE_SHARED=OFF \
                -DLIBCXXABI_USE_COMPILER_RT=ON \
                -DLIBCXXABI_USE_LLVM_UNWINDER=ON \
                -DLIBCXX_ENABLE_SHARED=OFF \
                -DLIBCXX_USE_COMPILER_RT=ON \
                -DLIBCXX_CXX_ABI=libcxxabi \
                -DLIBCXX_HAS_MUSL_LIBC=ON \
                -DLIBCXX_ENABLE_STATIC_ABI_LIBRARY=ON \
                -DLIBCXX_INCLUDE_TESTS=OFF \
                -DLIBCXX_INCLUDE_BENCHMARKS=OFF \
                -DLIBCXXABI_INCLUDE_TESTS=OFF \
                -DLIBUNWIND_INCLUDE_TESTS=OFF \
                -DCOMPILER_RT_INCLUDE_TESTS=OFF
            ninja -j\$(nproc)
            ninja install
            echo '==> Runtimes built and installed'
        "
    fi

    # ── Stage 5: Generate initial wrappers (needed before building LLVM tools) ──
    echo "==> Generating compiler wrappers and toolchain file..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e

        # Find compiler-rt builtins — prefer musl-built, fall back to host clang-22, then bootstrap
        if [ -f /usr/local/musl/lib/linux/libclang_rt.builtins-x86_64.a ]; then
            RT_PATH=/usr/local/musl/lib/linux
        elif [ -d /build/llvm-host ]; then
            RT_PATH=\$(dirname \$(find /build/llvm-host -name 'libclang_rt.builtins-x86_64.a' | head -1))
        else
            RT_PATH=\$(dirname \$(find /usr/lib/llvm-${LLVM_BOOTSTRAP} -name 'libclang_rt.builtins-x86_64.a' | head -1))
        fi
        CLANG_RESOURCE=\$(clang-${LLVM_BOOTSTRAP} -print-resource-dir)

        # Determine which clang to exec — musl-native if available, then host clang-22, otherwise bootstrap
        if [ -x /usr/local/musl/bin/clang-${LLVM_VERSION} ]; then
            CLANG_C=/usr/local/musl/bin/clang-${LLVM_VERSION}
            CLANG_CXX=/usr/local/musl/bin/clang++-${LLVM_VERSION}
            CLANG_RESOURCE=\$(/usr/local/musl/bin/clang-${LLVM_VERSION} -print-resource-dir 2>/dev/null || echo \$CLANG_RESOURCE)
            RT_PATH=\$(dirname \$(find /usr/local/musl -name 'libclang_rt.builtins-x86_64.a' | head -1) 2>/dev/null || echo \$RT_PATH)
        elif [ -x /build/llvm-host/bin/clang ]; then
            CLANG_C=/build/llvm-host/bin/clang
            CLANG_CXX=/build/llvm-host/bin/clang++
            CLANG_RESOURCE=\$(/build/llvm-host/bin/clang -print-resource-dir 2>/dev/null || echo \$CLANG_RESOURCE)
        else
            CLANG_C=clang-${LLVM_BOOTSTRAP}
            CLANG_CXX=clang++-${LLVM_BOOTSTRAP}
        fi

        cat > /usr/local/musl/bin/musl-clang++ << WRAPPER
#!/bin/sh
LINKING=1
SHARED=0
for arg in \"\\\$@\"; do
    case \"\\\$arg\" in
        -c|-S|-E|-M|-MM) LINKING=0 ;;
        -shared) SHARED=1 ;;
    esac
done
COMPILE_FLAGS=\"--target=x86_64-linux-musl --sysroot=/usr/local/musl -nostdinc++ -isystem /usr/local/musl/include/c++/v1 -isystem \${CLANG_RESOURCE}/include -isystem /usr/local/musl/include -fPIC\"
if [ \"\\\$LINKING\" = \"1\" ]; then
    CRT_OBJS=\"\"
    if [ \"\\\$SHARED\" = \"0\" ]; then
        CRT_OBJS=\"/usr/local/musl/lib/crt1.o\"
    fi
    exec \${CLANG_CXX} \\\$COMPILE_FLAGS \\\\
        -fuse-ld=lld \\\\
        -L/usr/local/musl/lib -L/usr/local/lib -L\${RT_PATH} \\\\
        --unwindlib=none -nostdlib \\\\
        \\\$CRT_OBJS /usr/local/musl/lib/crti.o \\\\
        -lc++ -lc++abi -lunwind -lc -lclang_rt.builtins-x86_64 \\\\
        /usr/local/musl/lib/crtn.o \\\\
        -Wl,-rpath,/usr/local/musl/lib -Wl,-rpath,/usr/local/lib \\\\
        \"\\\$@\"
else
    exec \${CLANG_CXX} \\\$COMPILE_FLAGS \"\\\$@\"
fi
WRAPPER
        chmod +x /usr/local/musl/bin/musl-clang++

        cat > /usr/local/musl/bin/musl-clang << WRAPPER
#!/bin/sh
LINKING=1
SHARED=0
for arg in \"\\\$@\"; do
    case \"\\\$arg\" in
        -c|-S|-E|-M|-MM) LINKING=0 ;;
        -shared) SHARED=1 ;;
    esac
done
COMPILE_FLAGS=\"--target=x86_64-linux-musl --sysroot=/usr/local/musl -isystem \${CLANG_RESOURCE}/include -isystem /usr/local/musl/include -fPIC\"
if [ \"\\\$LINKING\" = \"1\" ]; then
    CRT_OBJS=\"\"
    if [ \"\\\$SHARED\" = \"0\" ]; then
        CRT_OBJS=\"/usr/local/musl/lib/crt1.o\"
    fi
    exec \${CLANG_C} \\\$COMPILE_FLAGS \\\\
        -fuse-ld=lld \\\\
        -L/usr/local/musl/lib -L/usr/local/lib -L\${RT_PATH} \\\\
        -nostdlib \\\\
        \\\$CRT_OBJS /usr/local/musl/lib/crti.o \\\\
        -lc -lclang_rt.builtins-x86_64 \\\\
        /usr/local/musl/lib/crtn.o \\\\
        -Wl,-rpath,/usr/local/musl/lib -Wl,-rpath,/usr/local/lib \\\\
        \"\\\$@\"
else
    exec \${CLANG_C} \\\$COMPILE_FLAGS \"\\\$@\"
fi
WRAPPER
        chmod +x /usr/local/musl/bin/musl-clang

        # Determine linker path
        if [ -x /usr/local/musl/bin/ld.lld ]; then
            LLD_PATH=/usr/local/musl/bin/ld.lld
        else
            LLD_PATH=/usr/bin/ld.lld-${LLVM_BOOTSTRAP}
        fi

        mkdir -p /usr/local/musl/share
        cat > /usr/local/musl/share/musl-llvm-toolchain.cmake << CMAKE
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER /usr/local/musl/bin/musl-clang)
set(CMAKE_CXX_COMPILER /usr/local/musl/bin/musl-clang++)
set(CMAKE_LINKER \${LLD_PATH})
set(CMAKE_C_FLAGS_INIT \"-mcx16\")
set(CMAKE_CXX_FLAGS_INIT \"-mcx16\")
set(CMAKE_FIND_ROOT_PATH /usr/local/musl /usr/local)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
CMAKE
        echo '==> Wrappers and toolchain file generated'
    "

    # ── Stage 6: Build LLVM ${LLVM_VERSION} tools (clang, lld, lldb) as musl binaries ──
    if [[ -f "$CHROOT_DIR/usr/local/musl/bin/clang-${LLVM_VERSION}" ]]; then
        echo "==> LLVM ${LLVM_VERSION} tools already built, skipping"
    else
        # Ensure musl dynamic linker is accessible at the standard path
        if [[ ! -f "$CHROOT_DIR/lib/ld-musl-x86_64.so.1" ]]; then
            mkdir -p "$CHROOT_DIR/lib"
            ln -sf /usr/local/musl/lib/ld-musl-x86_64.so.1 "$CHROOT_DIR/lib/ld-musl-x86_64.so.1"
        fi

        echo "==> Building LLVM ${LLVM_VERSION} tools (clang, lld, lldb) against musl..."
        echo "    This will take a while even on a Threadripper — building clang+lld+lldb from source."
        $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
            set -e
            LLVM_SRC=/build/llvm-project

            rm -rf /build/build-llvm-tools
            mkdir -p /build/build-llvm-tools && cd /build/build-llvm-tools

            # Use musl-clang wrappers to cross-build LLVM itself
            cmake \${LLVM_SRC}/llvm -G Ninja \
                -DCMAKE_C_COMPILER=/usr/local/musl/bin/musl-clang \
                -DCMAKE_CXX_COMPILER=/usr/local/musl/bin/musl-clang++ \
                -DCMAKE_BUILD_TYPE=Release \
                -DCMAKE_INSTALL_PREFIX=/usr/local/musl \
                -DLLVM_ENABLE_PROJECTS='clang;lld;lldb' \
                -DLLVM_TARGETS_TO_BUILD=X86 \
                -DLLVM_DEFAULT_TARGET_TRIPLE=x86_64-linux-musl \
                -DLLVM_HOST_TRIPLE=x86_64-linux-musl \
                -DLLVM_ENABLE_TERMINFO=OFF \
                -DLLVM_ENABLE_ZLIB=OFF \
                -DLLVM_ENABLE_ZSTD=OFF \
                -DLLVM_ENABLE_LIBXML2=OFF \
                -DLLVM_ENABLE_BINDINGS=OFF \
                -DLLVM_BUILD_EXAMPLES=OFF \
                -DLLVM_INCLUDE_TESTS=OFF \
                -DLLVM_INCLUDE_BENCHMARKS=OFF \
                -DLLVM_INCLUDE_EXAMPLES=OFF \
                -DLLVM_BUILD_TOOLS=ON \
                -DCLANG_DEFAULT_RTLIB=compiler-rt \
                -DCLANG_DEFAULT_UNWINDLIB=libunwind \
                -DCLANG_DEFAULT_CXX_STDLIB=libc++ \
                -DCLANG_DEFAULT_LINKER=lld \
                -DLLDB_ENABLE_PYTHON=OFF \
                -DLLDB_ENABLE_LUA=OFF \
                -DLLDB_ENABLE_LIBEDIT=OFF \
                -DLLDB_ENABLE_CURSES=OFF \
                -DLLDB_ENABLE_LZMA=OFF \
                -DLLDB_ENABLE_LIBXML2=OFF
            ninja -j\$(nproc) clang lld lldb lldb-server
            ninja install-clang install-lld install-lldb install-lldb-server \
                  install-clang-resource-headers 2>/dev/null || ninja install

            # Create version-suffixed symlinks
            for tool in clang clang++ lld ld.lld lldb lldb-server; do
                if [ -f /usr/local/musl/bin/\${tool} ] && [ ! -f /usr/local/musl/bin/\${tool}-${LLVM_VERSION} ]; then
                    ln -sf \${tool} /usr/local/musl/bin/\${tool}-${LLVM_VERSION}
                fi
            done

            echo '==> LLVM ${LLVM_VERSION} tools built and installed to /usr/local/musl'
            file /usr/local/musl/bin/clang
            file /usr/local/musl/bin/lldb
        "

        # ── Stage 7: Regenerate wrappers to use musl-native clang ──
        echo "==> Regenerating wrappers to use musl-native LLVM ${LLVM_VERSION}..."
        # Re-run the wrapper generation — it will now detect /usr/local/musl/bin/clang-${LLVM_VERSION}
        # and use that instead of the bootstrap clang-${LLVM_BOOTSTRAP}
        $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
            set -e
            CLANG_C=/usr/local/musl/bin/clang-${LLVM_VERSION}
            CLANG_CXX=/usr/local/musl/bin/clang++-${LLVM_VERSION}
            CLANG_RESOURCE=\$(\${CLANG_C} -print-resource-dir)
            RT_PATH=\$(dirname \$(find /usr/local/musl -name 'libclang_rt.builtins-x86_64.a' | head -1))

            cat > /usr/local/musl/bin/musl-clang++ << WRAPPER
#!/bin/sh
LINKING=1
SHARED=0
for arg in \"\\\$@\"; do
    case \"\\\$arg\" in
        -c|-S|-E|-M|-MM) LINKING=0 ;;
        -shared) SHARED=1 ;;
    esac
done
COMPILE_FLAGS=\"--target=x86_64-linux-musl --sysroot=/usr/local/musl -nostdinc++ -isystem /usr/local/musl/include/c++/v1 -isystem \${CLANG_RESOURCE}/include -isystem /usr/local/musl/include -fPIC\"
if [ \"\\\$LINKING\" = \"1\" ]; then
    CRT_OBJS=\"\"
    if [ \"\\\$SHARED\" = \"0\" ]; then
        CRT_OBJS=\"/usr/local/musl/lib/crt1.o\"
    fi
    exec \${CLANG_CXX} \\\$COMPILE_FLAGS \\\\
        -fuse-ld=lld \\\\
        -L/usr/local/musl/lib -L/usr/local/lib -L\${RT_PATH} \\\\
        --unwindlib=none -nostdlib \\\\
        \\\$CRT_OBJS /usr/local/musl/lib/crti.o \\\\
        -lc++ -lc++abi -lunwind -lc -lclang_rt.builtins-x86_64 \\\\
        /usr/local/musl/lib/crtn.o \\\\
        -Wl,-rpath,/usr/local/musl/lib -Wl,-rpath,/usr/local/lib \\\\
        \"\\\$@\"
else
    exec \${CLANG_CXX} \\\$COMPILE_FLAGS \"\\\$@\"
fi
WRAPPER
            chmod +x /usr/local/musl/bin/musl-clang++

            cat > /usr/local/musl/bin/musl-clang << WRAPPER
#!/bin/sh
LINKING=1
SHARED=0
for arg in \"\\\$@\"; do
    case \"\\\$arg\" in
        -c|-S|-E|-M|-MM) LINKING=0 ;;
        -shared) SHARED=1 ;;
    esac
done
COMPILE_FLAGS=\"--target=x86_64-linux-musl --sysroot=/usr/local/musl -isystem \${CLANG_RESOURCE}/include -isystem /usr/local/musl/include -fPIC\"
if [ \"\\\$LINKING\" = \"1\" ]; then
    CRT_OBJS=\"\"
    if [ \"\\\$SHARED\" = \"0\" ]; then
        CRT_OBJS=\"/usr/local/musl/lib/crt1.o\"
    fi
    exec \${CLANG_C} \\\$COMPILE_FLAGS \\\\
        -fuse-ld=lld \\\\
        -L/usr/local/musl/lib -L/usr/local/lib -L\${RT_PATH} \\\\
        -nostdlib \\\\
        \\\$CRT_OBJS /usr/local/musl/lib/crti.o \\\\
        -lc -lclang_rt.builtins-x86_64 \\\\
        /usr/local/musl/lib/crtn.o \\\\
        -Wl,-rpath,/usr/local/musl/lib -Wl,-rpath,/usr/local/lib \\\\
        \"\\\$@\"
else
    exec \${CLANG_C} \\\$COMPILE_FLAGS \"\\\$@\"
fi
WRAPPER
            chmod +x /usr/local/musl/bin/musl-clang

            cat > /usr/local/musl/share/musl-llvm-toolchain.cmake << CMAKE
set(CMAKE_SYSTEM_NAME Linux)
set(CMAKE_SYSTEM_PROCESSOR x86_64)
set(CMAKE_C_COMPILER /usr/local/musl/bin/musl-clang)
set(CMAKE_CXX_COMPILER /usr/local/musl/bin/musl-clang++)
set(CMAKE_LINKER /usr/local/musl/bin/ld.lld)
set(CMAKE_C_FLAGS_INIT \"-mcx16\")
set(CMAKE_CXX_FLAGS_INIT \"-mcx16\")
set(CMAKE_FIND_ROOT_PATH /usr/local/musl /usr/local)
set(CMAKE_FIND_ROOT_PATH_MODE_PROGRAM NEVER)
set(CMAKE_FIND_ROOT_PATH_MODE_LIBRARY ONLY)
set(CMAKE_FIND_ROOT_PATH_MODE_INCLUDE ONLY)
CMAKE
            echo '==> Wrappers updated to use musl-native LLVM ${LLVM_VERSION}'
        "
    fi
}

# ═══════════════════════════════════════════════════════════════
# Phase 2b: Build self-hosted build tools into the chroot toolchain
#   - toybox  : musl-static busybox-like userland (sed, awk, sh, ...)
#   - makeplus: GNU Make compatible build system (C++17) used as `make`
#   - cmake   : rebuilt from source using makeplus as the make program
# ═══════════════════════════════════════════════════════════════
build_toybox() {
    if [[ -f "$CHROOT_DIR/usr/local/bin/toybox" ]]; then
        echo "==> toybox already built, skipping"
        return
    fi
    echo "==> Building toybox (static, musl) with sed/awk/sh enabled..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        cd /build
        if [ ! -d toybox ]; then
            git clone --depth 1 '${REPOS[toybox]}' toybox
        fi
        cd toybox
        git pull --ff-only || true
        make defconfig
        # Explicitly enable the applets we depend on (sh for initdb, sed/awk
        # for the build scripts and runtime helpers, the modutils so the shipped
        # system can load kernel modules without GNU kmod, and the network
        # applets — dhcp client + route — so the system can get an IP at boot).
        for opt in CONFIG_SH CONFIG_SED CONFIG_AWK \
                   CONFIG_MODPROBE CONFIG_INSMOD CONFIG_RMMOD CONFIG_LSMOD \
                   CONFIG_MODINFO CONFIG_DEPMOD \
                   CONFIG_DHCP CONFIG_DHCPD CONFIG_ROUTE CONFIG_IFCONFIG \
                   CONFIG_GETTY CONFIG_LOGIN CONFIG_PASSWD CONFIG_SU \
                   CONFIG_VI; do
            sed -i \"s/# \${opt} is not set/\${opt}=y/\" .config 2>/dev/null || true
            grep -q \"^\${opt}=y\" .config || echo \"\${opt}=y\" >> .config
        done
        make oldconfig KCONFIG_ALLCONFIG=.config
        # Static build with the musl/LLVM toolchain
        LDFLAGS='--static' CC='/usr/local/musl/bin/musl-clang' make -j\$(nproc)
        cp generated/unstripped/toybox /usr/local/bin/toybox
        /usr/local/musl/bin/llvm-strip /usr/local/bin/toybox 2>/dev/null || strip /usr/local/bin/toybox
        echo '==> toybox installed at /usr/local/bin/toybox'
        /usr/local/bin/toybox sed --version 2>/dev/null | head -1 || true
        /usr/local/bin/toybox awk 'BEGIN{print \"awk applet OK\"}' || true
    "
}

build_makeplus() {
    if [[ -x "$CHROOT_DIR/usr/local/musl/bin/makeplus" ]]; then
        echo "==> makeplus already built, skipping"
        return
    fi
    echo "==> Building makeplus (GNU Make compatible, musl/LLVM)..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        cd /build
        if [ ! -d makeplus ]; then
            git clone --depth 1 '${REPOS[makeplus]}' makeplus
        fi
        cd makeplus
        git pull --ff-only || true
        # Bootstrap: compile the sources directly with the musl-clang++ wrapper.
        CXX='/usr/local/musl/bin/musl-clang++' \
        CXXFLAGS='-std=c++17 -O2 -pthread' \
            /usr/local/musl/bin/musl-clang++ -std=c++17 -O2 -pthread \
            -o makeplus main.cpp parser.cpp executor.cpp
        install -m 755 makeplus /usr/local/musl/bin/makeplus
        # Provide a 'make' alias so downstream build systems pick up makeplus.
        ln -sf /usr/local/musl/bin/makeplus /usr/local/musl/bin/make
        echo '==> makeplus installed at /usr/local/musl/bin/makeplus (make -> makeplus)'
        /usr/local/musl/bin/makeplus --version 2>/dev/null || true
    "
}

build_cmake() {
    if [[ -x "$CHROOT_DIR/usr/local/bin/cmake" ]] && \
       "$CHROOT_DIR/usr/local/bin/cmake" --version 2>/dev/null | grep -q "${CMAKE_VERSION}"; then
        echo "==> musl-native CMake ${CMAKE_VERSION} already built, skipping"
        return
    fi
    echo "==> Building CMake ${CMAKE_VERSION} from source using makeplus..."
    # Bind-mount host CMake patches (if any) read-only into the chroot so they
    # can be applied with toybox patch (no GNU patch required).
    local cmake_patch_bind=""
    [[ -d "$SCRIPT_DIR/patches/cmake" ]] && cmake_patch_bind="--bind-ro=$SCRIPT_DIR/patches/cmake:/build/patches/cmake"
    $NSPAWN -D "$CHROOT_DIR" $cmake_patch_bind --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:\$PATH
        cd /build
        if [ ! -d cmake-src ]; then
            git clone --depth 1 --branch ${CMAKE_TAG} '${REPOS[cmake]}' cmake-src
        fi
        cd cmake-src

        # Apply CMake patches (musl / GNU-free fixes) using toybox patch so no
        # GNU patch is pulled in. Each patch is applied at most once.
        if [ -d /build/patches/cmake ]; then
            for p in /build/patches/cmake/*.patch; do
                [ -e \"\$p\" ] || continue
                marker=\".patched-\$(/usr/local/bin/toybox basename \"\$p\")\"
                [ -f \"\$marker\" ] && continue
                echo \"  [patch] \$(/usr/local/bin/toybox basename \"\$p\")\"
                /usr/local/bin/toybox patch -p1 < \"\$p\"
                : > \"\$marker\"
            done
        fi

        # Use makeplus as the make program for CMake's bootstrap. The bootstrap
        # honours the MAKE environment variable.
        export MAKE=/usr/local/musl/bin/makeplus
        export CC=/usr/local/musl/bin/musl-clang
        export CXX=/usr/local/musl/bin/musl-clang++

        echo '  [bootstrap] CMake (make = makeplus)'
        ./bootstrap --prefix=/usr/local --parallel=\$(nproc) \
            --no-qt-gui -- -DCMAKE_USE_OPENSSL=OFF

        echo '  [build] CMake (makeplus)'
        /usr/local/musl/bin/makeplus -j\$(nproc)

        /usr/local/musl/bin/makeplus install
        echo '==> CMake installed at /usr/local/bin/cmake'
        /usr/local/bin/cmake --version | head -1
    "
}

NINJA_VERSION="1.12.1"

build_ninja() {
    if [[ -x "$CHROOT_DIR/usr/local/bin/ninja" ]] && \
       "$CHROOT_DIR/usr/local/bin/ninja" --version 2>/dev/null | grep -q "${NINJA_VERSION}"; then
        echo "==> musl-native ninja ${NINJA_VERSION} already built, skipping"
        return
    fi
    echo "==> Building ninja ${NINJA_VERSION} from source..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake

        cd /build
        if [ ! -d ninja-src ]; then
            git clone --depth 1 --branch v${NINJA_VERSION} https://github.com/ninja-build/ninja.git ninja-src
        fi
        cd ninja-src
        rm -rf build

        # Use Unix Makefiles — can't use ninja to build ninja
        cmake -B build -G 'Unix Makefiles' \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_BUILD_TYPE=Release \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_INTERPROCEDURAL_OPTIMIZATION=OFF \
            -DBUILD_TESTING=OFF \
            -Wno-dev

        cmake --build build -- -j\$(nproc)
        install -m 755 build/ninja /usr/local/bin/ninja
        echo '==> ninja installed at /usr/local/bin/ninja'
        /usr/local/bin/ninja --version
    "
}

build_tools() {
    echo "==> Building self-hosted build tools (toybox, makeplus, cmake, ninja)..."
    build_toybox
    build_makeplus
    build_cmake
    build_ninja
    echo "==> Build tools ready (toybox + makeplus + musl-native cmake + ninja)"
}

# ═══════════════════════════════════════════════════════════════
# Phase 3: Build all dependency libraries
# ═══════════════════════════════════════════════════════════════
build_libraries() {
    echo "==> Building dependency libraries inside nspawn..."

    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        PREFIX=/usr/local

        mkdir -p /build/libs && cd /build/libs

        # Build pkgconf (musl-native pkg-config implementation)
        if [ ! -f /build/libs/.pkgconf.done ]; then
            echo '  [build] pkgconf'
            if [ ! -d pkgconf ]; then
                git clone --depth 1 https://github.com/pkgconf/pkgconf.git pkgconf
            fi
            cd pkgconf
            ./autogen.sh
            CC=/usr/local/musl/bin/musl-clang \
            LDFLAGS='--static' \
            ./configure --prefix=/usr/local \
                --disable-shared \
                --enable-static \
                --with-pkg-config-dir=/usr/local/lib/pkgconfig:/usr/local/share/pkgconfig \
                --with-system-libdir=/usr/local/lib \
                --with-system-includedir=/usr/local/include
            make -j\$(nproc)
            make install
            # Symlink so scripts using 'pkg-config' find it
            ln -sf /usr/local/bin/pkgconf /usr/local/bin/pkg-config
            cd /build/libs
            touch .pkgconf.done
        else
            echo '  [skip] pkgconf already built'
        fi

        build_lib() {
            local name=\$1
            local url=\$2
            local extra_args=\${3:-}

            if [ -f /build/libs/.\${name}.done ]; then
                echo \"  [skip] \$name already built\"
                return
            fi

            echo \"  [build] \$name\"
            if [ ! -d \$name ]; then
                git clone --depth 1 \$url \$name
            fi
            mkdir -p \$name/build && cd \$name/build
            cmake .. -G Ninja \
                -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
                -DCMAKE_INSTALL_PREFIX=\$PREFIX \
                -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
                -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
                \$extra_args
            ninja
            ninja install
            cd /build/libs
            touch .\${name}.done
        }

        # ── System/third-party dependencies ──
        build_lib json-c '${REPOS[json-c]}' '-DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=OFF -DDISABLE_WERROR=ON'
        build_lib libyaml '${REPOS[libyaml]}' '-DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=OFF -DINSTALL_CMAKE_DIR=lib/cmake/yaml'
        build_lib brotli '${REPOS[brotli]}' '-DBUILD_SHARED_LIBS=OFF -DBUILD_TESTING=OFF'
        build_lib tinyxml2 '${REPOS[tinyxml2]}' '-DBUILD_SHARED_LIBS=OFF -Dtinyxml2_BUILD_TESTING=OFF'
        build_lib libxml2 '${REPOS[libxml2]}' '-DBUILD_SHARED_LIBS=OFF -DLIBXML2_WITH_PYTHON=OFF -DLIBXML2_WITH_TESTS=OFF -DLIBXML2_WITH_LZMA=OFF -DLIBXML2_WITH_ZLIB=OFF -DLIBXML2_WITH_ICONV=OFF'

        # SQLite (autoconf-based)
        if [ ! -f /build/libs/.sqlite.done ]; then
            echo \"  [build] sqlite\"
            if [ ! -d sqlite ]; then
                git clone --depth 1 '${REPOS[sqlite]}' sqlite
            fi
            cd sqlite
            CC=/usr/local/musl/bin/musl-clang ./configure --prefix=\$PREFIX --disable-shared --enable-static
            make -j\$(nproc)
            make install
            cd /build/libs
            touch .sqlite.done
        else
            echo \"  [skip] sqlite already built\"
        fi

        # LibreSSL
        if [ ! -f /build/libs/.libressl.done ]; then
            echo \"  [build] libressl\"
            if [ ! -d libressl ]; then
                wget -q '${REPOS[libressl]}' -O libressl.tar.gz
                tar xf libressl.tar.gz
                mv libressl-4.0.0 libressl
            fi
            mkdir -p libressl/build && cd libressl/build
            cmake .. -G Ninja \
                -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
                -DCMAKE_INSTALL_PREFIX=\$PREFIX \
                -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
                -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
                -DBUILD_SHARED_LIBS=OFF \
                -DLIBRESSL_APPS=OFF \
                -DLIBRESSL_TESTS=OFF
            ninja
            ninja install
            cd /build/libs
            touch .libressl.done
        else
            echo \"  [skip] libressl already built\"
        fi

        # ── Custom libraries ──
        build_lib libcmdplus '${REPOS[libcmdplus]}'
        build_lib uuidplus '${REPOS[uuidplus]}'
        build_lib libnetplus '${REPOS[libnetplus]}'
        build_lib libhtmlpp '${REPOS[libhtmlpp]}'
        build_lib libhttppp '${REPOS[libhttppp]}'
        build_lib libconfplus '${REPOS[libconfplus]}'
        build_lib libdbpp '${REPOS[libdbpp]}' '-DPostgreSQL_ROOT=/usr/local -DPostgreSQL_TYPE_INCLUDE_DIR=/usr/local/include'
        build_lib libgameinfoplus '${REPOS[libgameinfoplus]}'
        build_lib libparitypp '${REPOS[libparitypp]}'
        build_lib authdb '${REPOS[authdb]}' '-DBUILD_BINDINGS=OFF'
        build_lib mediadb '${REPOS[mediadb]}' '-DCLIENT_ONLY=ON'
        build_lib smtpclient '${REPOS[smtpclient]}' '-DCMAKE_INSTALL_LIBDIR=/usr/local/lib -DCMAKE_INSTALL_INCLUDEDIR=/usr/local/include'

        echo '==> All libraries built successfully'
    "
}

# ═══════════════════════════════════════════════════════════════
# Helper: Create a minimal rootfs for a container
# ═══════════════════════════════════════════════════════════════
create_minimal_rootfs() {
    local name="$1"
    local rootfs="$CONTAINER_DIR/$name/rootfs"

    echo "  [rootfs] Creating minimal rootfs for $name..." >&2
    rm -rf "$rootfs"
    mkdir -p "$rootfs"/{bin,sbin,lib,usr/bin,usr/sbin,usr/lib,usr/local/bin,usr/local/lib,usr/local/share}
    mkdir -p "$rootfs"/{etc,var/lib,var/log,var/run,tmp,run,proc,sys,dev,mnt}
    mkdir -p "$rootfs"/etc/systemd/system

    # Copy musl dynamic linker and libc
    cp "$CHROOT_DIR/usr/local/musl/lib/libc.so" "$rootfs/lib/ld-musl-x86_64.so.1"
    ln -sf /lib/ld-musl-x86_64.so.1 "$rootfs/usr/lib/libc.musl-x86_64.so.1"
    # Binaries have RUNPATH /usr/local/musl/lib — create libc.so symlink there
    mkdir -p "$rootfs/usr/local/musl/lib"
    ln -sf /lib/ld-musl-x86_64.so.1 "$rootfs/usr/local/musl/lib/libc.so"

    # Minimal /etc
    echo "root:x:0:0:root:/root:/bin/sh" > "$rootfs/etc/passwd"
    echo "root:x:0:" > "$rootfs/etc/group"
    echo "$name" > "$rootfs/etc/hostname"
    echo "nameserver 10.0.0.1" > "$rootfs/etc/resolv.conf"

    # Copy toybox for shell + coreutils (from chroot). It is normally built in
    # the 'buildtools' phase; build it on demand here as a fallback.
    if [[ ! -f "$CHROOT_DIR/usr/local/bin/toybox" ]]; then
        build_toybox >&2
    fi
    cp "$CHROOT_DIR/usr/local/bin/toybox" "$rootfs/bin/toybox"
    # Symlink common tools
    for cmd in sh ls cat mkdir rm cp mv ln chmod chown id sleep date echo grep sed awk \
               ps kill mount umount ip ifconfig hostname env basename dirname head tail wc \
               sort uniq tr test expr true false pwd; do
        ln -sf /bin/toybox "$rootfs/bin/$cmd"
    done
    ln -sf /bin/toybox "$rootfs/usr/bin/env"

    echo "$rootfs"
}

# ═══════════════════════════════════════════════════════════════
# Helper: Copy shared libraries into a container rootfs
# ═══════════════════════════════════════════════════════════════
copy_shared_libs() {
    local rootfs="$1"
    echo "  [libs] Copying shared libraries into rootfs..."
    local libs=(
        "libhttppp.so*" "libhtmlpp.so*" "libnetplus.so*" "libparitypp.so*"
        "libuuidp.so*" "libconfplus.so*" "libcmdplus.so*" "libsmtpclient.so*"
        "libdbpp.so*" "libdbpp.a" "libpq.so*" "libgameinfoplus.so*"
    )
    for lib in "${libs[@]}"; do
        find "$CHROOT_DIR/usr/local/lib" -maxdepth 1 -name "$lib" -exec cp -a {} "$rootfs/usr/local/lib/" \;
    done
    # Also check pgsql lib path (if PostgreSQL installed under /usr/local/pgsql)
    if [[ -d "$CHROOT_DIR/usr/local/pgsql/lib" ]]; then
        find "$CHROOT_DIR/usr/local/pgsql/lib" -maxdepth 1 -name "libpq.so*" -exec cp -a {} "$rootfs/usr/local/lib/" \;
    fi
    if [[ -d "$CHROOT_DIR/usr/local/lib" ]]; then
        find "$CHROOT_DIR/usr/local/lib" -maxdepth 1 -name "libpq.so*" -exec cp -a {} "$rootfs/usr/local/lib/" \;
    fi
    # Tell musl dynamic linker where to find libs
    echo "/usr/local/lib" > "$rootfs/etc/ld-musl-x86_64.path"
}

# ═══════════════════════════════════════════════════════════════
# Helper: Copy debug tools (lldb, clang) into container rootfs
# ═══════════════════════════════════════════════════════════════
copy_debug_tools() {
    local rootfs="$1"
    if [[ $DEBUG -ne 1 ]]; then
        return
    fi
    echo "  [debug] Copying musl-native lldb and clang into rootfs..."

    mkdir -p "$rootfs/usr/local/musl/bin" "$rootfs/usr/local/musl/lib"

    # Copy musl-native LLVM tools (these are musl binaries, no glibc needed)
    for bin in lldb lldb-server clang clang++ lld ld.lld; do
        if [[ -f "$CHROOT_DIR/usr/local/musl/bin/$bin" ]]; then
            cp -a "$CHROOT_DIR/usr/local/musl/bin/$bin" "$rootfs/usr/local/musl/bin/"
        fi
    done

    # Symlink into PATH
    for bin in lldb lldb-server clang clang++ lld ld.lld; do
        if [[ -f "$rootfs/usr/local/musl/bin/$bin" ]]; then
            ln -sf "/usr/local/musl/bin/$bin" "$rootfs/usr/local/bin/$bin"
        fi
    done

    # Copy clang resource directory (builtins, headers)
    local resource_dir
    resource_dir=$($NSPAWN -D "$CHROOT_DIR" --pipe -- \
        /usr/local/musl/bin/clang -print-resource-dir 2>/dev/null | tr -d '\n')
    if [[ -n "$resource_dir" ]] && [[ -d "$CHROOT_DIR$resource_dir" ]]; then
        mkdir -p "$rootfs$resource_dir"
        cp -a "$CHROOT_DIR$resource_dir/"* "$rootfs$resource_dir/"
    fi

    # Copy LLVM shared libraries if any were built
    find "$CHROOT_DIR/usr/local/musl/lib" -maxdepth 1 -name 'libLLVM*.so*' -exec cp -a {} "$rootfs/usr/local/musl/lib/" \; 2>/dev/null || true
    find "$CHROOT_DIR/usr/local/musl/lib" -maxdepth 1 -name 'libclang*.so*' -exec cp -a {} "$rootfs/usr/local/musl/lib/" \; 2>/dev/null || true
    find "$CHROOT_DIR/usr/local/musl/lib" -maxdepth 1 -name 'liblldb*.so*' -exec cp -a {} "$rootfs/usr/local/musl/lib/" \; 2>/dev/null || true

    # Add musl/lib to library search path
    if [[ -f "$rootfs/etc/ld-musl-x86_64.path" ]]; then
        if ! grep -q '/usr/local/musl/lib' "$rootfs/etc/ld-musl-x86_64.path"; then
            echo "/usr/local/musl/lib" >> "$rootfs/etc/ld-musl-x86_64.path"
        fi
    fi

    echo "  [debug] musl-native debug tools installed (LLVM ${LLVM_VERSION})"
}

# ═══════════════════════════════════════════════════════════════
# Helper: Write info.xml
# ═══════════════════════════════════════════════════════════════
write_info_xml() {
    local name="$1"
    local release="$2"
    local description="$3"
    local service="$4"
    local port="$5"
    local outdir="$CONTAINER_DIR/$name"

    # Version scheme: YYYYMMDD-N (build date + running serial number).
    # The serial auto-increments for builds on the same day and resets to 1
    # on a new day. State is kept per-container in the build dir (not the repo).
    local date_iso date_compact serial serial_file prev_date prev_serial
    date_iso="$(date +%Y-%m-%d)"
    date_compact="$(date +%Y%m%d)"

    mkdir -p "$outdir"
    serial_file="$outdir/.serial"
    serial=1
    if [[ -f "$serial_file" ]]; then
        read -r prev_date prev_serial < "$serial_file" || true
        if [[ "$prev_date" == "$date_compact" && -n "$prev_serial" ]]; then
            serial=$((prev_serial + 1))
        fi
    fi
    echo "$date_compact $serial" > "$serial_file"

    local version="${date_compact}-${serial}"

    cat > "$outdir/info.xml" << EOF
<?xml version="1.0" encoding="UTF-8"?>
<container>
    <name>${name}</name>
    <version>${version}</version>
    <date>${date_iso}</date>
    <serial>${serial}</serial>
    <release>${release}</release>
    <architecture>${ARCH}</architecture>
    <description>${description}</description>
    <service>${service}</service>
    <port>${port}</port>
    <toolchain>musl-${MUSL_VERSION} + LLVM ${LLVM_VERSION}</toolchain>
    <built>$(date -u +%Y-%m-%dT%H:%M:%SZ)</built>
</container>
EOF
}

# ═══════════════════════════════════════════════════════════════
# Helper: Package a container directory into .webcontainer
# ═══════════════════════════════════════════════════════════════
package_container() {
    local name="$1"
    local outdir="$CONTAINER_DIR/$name"
    local rootfs="$outdir/rootfs"

    echo "  [package] Creating ${name}.webcontainer..."
    mkdir -p "$OUTPUT_DIR"

    # Embed info.xml inside the container rootfs so the version is queryable
    # from within the running container (e.g. /etc/webcommander/info.xml).
    if [[ -f "$outdir/info.xml" ]]; then
        mkdir -p "$rootfs/etc/webcommander"
        cp "$outdir/info.xml" "$rootfs/etc/webcommander/info.xml"
    fi

    # Create data.tar.xz from rootfs
    tar -C "$rootfs" -cJf "$outdir/data.tar.xz" .

    # Checksum the rootfs tarball and record it inside info.xml so the image's
    # payload can be verified independently of the outer .webcontainer.
    local rootfs_sha
    rootfs_sha="$(sha256sum "$outdir/data.tar.xz" | awk '{print $1}')"
    if [[ -f "$outdir/info.xml" ]]; then
        # Insert a <rootfs> block just before the closing </container> tag.
        sed -i "s:</container>:    <rootfs>\n        <file>data.tar.xz</file>\n        <sha256>${rootfs_sha}</sha256>\n    </rootfs>\n</container>:" "$outdir/info.xml"
    fi

    # Create .webcontainer (tar containing data.tar.xz + info.xml)
    tar -C "$outdir" -cf "$OUTPUT_DIR/${name}.webcontainer" data.tar.xz info.xml

    echo "  [done] $OUTPUT_DIR/${name}.webcontainer"
}

# ═══════════════════════════════════════════════════════════════
# Helper: Generate a bundled repository index.xml (apt-like)
# Aggregates every published container with version, date, serial,
# file size, container checksum and rootfs (data.tar.xz) checksum
# into a single index for WebCommander.
# ═══════════════════════════════════════════════════════════════
generate_index_xml() {
    local index="$OUTPUT_DIR/index.xml"
    [[ -d "$OUTPUT_DIR" ]] || return 0

    echo "==> Generating repository index.xml..."

    {
        echo '<?xml version="1.0" encoding="UTF-8"?>'
        echo '<repository>'
        echo "    <generated>$(date -u +%Y-%m-%dT%H:%M:%SZ)</generated>"
        echo "    <architecture>${ARCH}</architecture>"
        echo '    <containers>'

        local img name info size sha
        for img in "$OUTPUT_DIR"/*.webcontainer; do
            [[ -f "$img" ]] || continue
            name="$(basename "$img" .webcontainer)"
            # Metadata source is the build-dir info.xml (already contains the
            # rootfs checksum). No standalone info.xml is published to the repo.
            info="$CONTAINER_DIR/$name/info.xml"

            size="$(stat -c %s "$img" 2>/dev/null || echo 0)"
            sha="$(sha256sum "$img" | awk '{print $1}')"

            # Pull version metadata + rootfs checksum from info.xml if present.
            local version="" date="" serial="" release="" rootfs_sha=""
            if [[ -f "$info" ]]; then
                version="$(sed -n 's:.*<version>\(.*\)</version>.*:\1:p' "$info" | head -n1)"
                date="$(sed -n 's:.*<date>\(.*\)</date>.*:\1:p' "$info" | head -n1)"
                serial="$(sed -n 's:.*<serial>\(.*\)</serial>.*:\1:p' "$info" | head -n1)"
                release="$(sed -n 's:.*<release>\(.*\)</release>.*:\1:p' "$info" | head -n1)"
                rootfs_sha="$(sed -n 's:.*<sha256>\(.*\)</sha256>.*:\1:p' "$info" | head -n1)"
            fi

            echo '        <container>'
            echo "            <name>${name}</name>"
            echo "            <version>${version}</version>"
            echo "            <date>${date}</date>"
            echo "            <serial>${serial}</serial>"
            echo "            <release>${release}</release>"
            echo "            <file>${name}.webcontainer</file>"
            echo "            <size>${size}</size>"
            echo "            <sha256>${sha}</sha256>"
            echo "            <rootfs>"
            echo "                <file>data.tar.xz</file>"
            echo "                <sha256>${rootfs_sha}</sha256>"
            echo "            </rootfs>"
            echo '        </container>'
        done

        echo '    </containers>'
        echo '</repository>'
    } > "$index"

    # Checksum the index itself so it can be verified like the containers.
    sha256sum "$index" | awk '{print $1}' > "$index.sha256"

    echo "  [done] $index"
}

# ═══════════════════════════════════════════════════════════════
# Phase 4: Build PostgreSQL server from source
# ═══════════════════════════════════════════════════════════════
build_postgresql() {
    echo "==> Building PostgreSQL ${PG_BRANCH} ..."
    build_m4
    build_flex

    local pg_cmake="$SCRIPT_DIR/files/postgresql/CMakeLists.txt"

    # Build full PostgreSQL server inside the chroot
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$pg_cmake:/build/pg-CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake

        if [ -f /build/libs/.postgresql-server.done ]; then
            echo '  [skip] PostgreSQL server already built'
            exit 0
        fi

        cd /build/libs

        if [ ! -d postgres ]; then
            echo '  [clone] PostgreSQL'
            git clone --depth 1 --branch ${PG_BRANCH} '${REPOS[postgres]}' postgres
        fi

        cd postgres

        # Copy self-contained CMakeLists.txt — it handles configure/bison/flex/perl
        cp /build/pg-CMakeLists.txt CMakeLists.txt

        echo '  [build] PostgreSQL server (cmake+ninja)'
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DPG_USE_OPENSSL=OFF
        ninja
        ninja install

        touch /build/libs/.postgresql-server.done
        echo '  [done] PostgreSQL server built and installed to /usr/local'
    "

    # libdbpp is built in the 'libraries' phase, BEFORE PostgreSQL exists, so its
    # pgsql backend (pgsql.so) gets skipped — only sqlite.so is produced. Now
    # that libpq is installed, rebuild libdbpp so the pgsql backend is created.
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        PREFIX=/usr/local

        if [ -f /usr/local/lib/dbpp/pgsql.so ]; then
            echo '  [skip] libdbpp pgsql backend already built'
            exit 0
        fi

        echo '  [rebuild] libdbpp (with PostgreSQL/libpq now available)'
        cd /build/libs
        if [ ! -d libdbpp ]; then
            git clone --depth 1 '${REPOS[libdbpp]}' libdbpp
        fi
        rm -rf libdbpp/build
        mkdir -p libdbpp/build && cd libdbpp/build
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=\$PREFIX \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
            -DPostgreSQL_ROOT=/usr/local \
            -DPostgreSQL_TYPE_INCLUDE_DIR=/usr/local/include
        ninja
        ninja install

        if [ -f /usr/local/lib/dbpp/pgsql.so ]; then
            echo '  [done] libdbpp pgsql backend built'
        else
            echo '  [warn] libdbpp rebuilt but pgsql.so still missing — check PostgreSQL detection' >&2
        fi
    "

    # Create container rootfs
    local rootfs
    rootfs=$(create_minimal_rootfs "postgresql")

    # Copy PostgreSQL binaries and libs from chroot build
    # Check both /usr/local/ and /usr/local/pgsql/ (depending on --prefix used)
    local pg_prefix="$CHROOT_DIR/usr/local"
    if [[ ! -f "$pg_prefix/bin/postgres" ]] && [[ -f "$CHROOT_DIR/usr/local/pgsql/bin/postgres" ]]; then
        pg_prefix="$CHROOT_DIR/usr/local/pgsql"
        echo "  [note] Using PostgreSQL from $pg_prefix"
    fi
    cp -a "$pg_prefix/bin/postgres" "$rootfs/usr/local/bin/" 2>/dev/null || true
    cp -a "$pg_prefix/bin/pg_"* "$rootfs/usr/local/bin/" 2>/dev/null || true
    cp -a "$pg_prefix/bin/initdb" "$rootfs/usr/local/bin/" 2>/dev/null || true
    cp -a "$pg_prefix/bin/psql" "$rootfs/usr/local/bin/" 2>/dev/null || true
    cp -a "$pg_prefix/bin/createdb" "$rootfs/usr/local/bin/" 2>/dev/null || true
    cp -a "$pg_prefix/bin/createuser" "$rootfs/usr/local/bin/" 2>/dev/null || true
    cp -a "$pg_prefix/bin/dropdb" "$rootfs/usr/local/bin/" 2>/dev/null || true
    cp -a "$pg_prefix/bin/dropuser" "$rootfs/usr/local/bin/" 2>/dev/null || true
    cp -a "$pg_prefix/lib/libpq"* "$rootfs/usr/local/lib/" 2>/dev/null || true
    # Copy PostgreSQL extension modules ($libdir = /usr/local/lib/postgresql/)
    if [[ -d "$pg_prefix/lib/postgresql" ]]; then
        mkdir -p "$rootfs/usr/local/lib/postgresql"
        cp -a "$pg_prefix/lib/postgresql/"* "$rootfs/usr/local/lib/postgresql/" 2>/dev/null || true
    fi
    # Also copy any .so files directly in lib/ (fallback for older prefix layouts)
    find "$pg_prefix/lib" -maxdepth 1 -name "*.so" -exec cp -a {} "$rootfs/usr/local/lib/" \; 2>/dev/null || true
    # Copy pkgconfig files
    if [[ -d "$pg_prefix/lib/pkgconfig" ]]; then
        mkdir -p "$rootfs/usr/local/lib/pkgconfig"
        cp -a "$pg_prefix/lib/pkgconfig/"* "$rootfs/usr/local/lib/pkgconfig/" 2>/dev/null || true
    fi
    if [[ -d "$pg_prefix/share/postgresql" ]]; then
        cp -a "$pg_prefix/share/postgresql" "$rootfs/usr/local/share/"
    fi
    # Also copy share files installed directly under prefix/share (timezone, extension, tsearch_data)
    if [[ -d "$pg_prefix/share" ]]; then
        for d in timezone timezonesets tsearch_data extension; do
            if [[ -d "$pg_prefix/share/$d" ]]; then
                mkdir -p "$rootfs/usr/local/share/$d"
                cp -a "$pg_prefix/share/$d/"* "$rootfs/usr/local/share/$d/" 2>/dev/null || true
            fi
        done
        # Copy loose share files (*.sql, *.sample, *.bki, etc.)
        find "$pg_prefix/share" -maxdepth 1 -type f -exec cp -a {} "$rootfs/usr/local/share/" \; 2>/dev/null || true
    fi

    # Verify key binaries exist
    for bin in postgres pg_ctl initdb psql; do
        if [[ ! -f "$rootfs/usr/local/bin/$bin" ]]; then
            echo "WARNING: $bin not found in rootfs"
        fi
    done

    # Build setuidgid helper (statically linked with musl)
    local script_dir
    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    if command -v musl-gcc &>/dev/null; then
        musl-gcc -static -O2 -o "$rootfs/usr/local/bin/setuidgid" "$script_dir/setuidgid.c"
    else
        # script_dir is typically on an NFS home with root_squash, so the
        # compiler output must land on the local rootfs, not back in script_dir.
        mkdir -p "$rootfs/usr/local/bin"
        $NSPAWN -D "$CHROOT_DIR" \
            --bind-ro="$script_dir:/build/packaging" \
            --bind="$rootfs/usr/local/bin:/build/out" --pipe -- bash -c "
            /usr/local/musl/bin/musl-clang -static -O2 -o /build/out/setuidgid /build/packaging/setuidgid.c
        "
    fi
    chmod +x "$rootfs/usr/local/bin/setuidgid"

    # Create postgres user
    echo "postgres:x:999:999:PostgreSQL:/var/lib/postgresql:/bin/sh" >> "$rootfs/etc/passwd"
    echo "postgres:x:999:" >> "$rootfs/etc/group"
    mkdir -p "$rootfs/var/lib/postgresql/data"
    mkdir -p "$rootfs/var/run/postgresql"
    chown -R 999:999 "$rootfs/var/lib/postgresql"
    chown -R 999:999 "$rootfs/var/run/postgresql"

    # Create systemd service
    cat > "$rootfs/etc/systemd/system/postgresql.service" << 'EOF'
[Unit]
Description=PostgreSQL Database Server
After=network.target

[Service]
Type=forking
User=postgres
Group=postgres
ExecStartPre=/bin/sh -c 'if [ ! -f /var/lib/postgresql/data/PG_VERSION ]; then /usr/local/bin/initdb -D /var/lib/postgresql/data; fi'
ExecStart=/usr/local/bin/pg_ctl -D /var/lib/postgresql/data -l /var/log/postgresql.log start
ExecStop=/usr/local/bin/pg_ctl -D /var/lib/postgresql/data stop -m fast
ExecReload=/usr/local/bin/pg_ctl -D /var/lib/postgresql/data reload
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

    # Enable service
    mkdir -p "$rootfs/etc/systemd/system/multi-user.target.wants"
    ln -sf /etc/systemd/system/postgresql.service "$rootfs/etc/systemd/system/multi-user.target.wants/postgresql.service"

    write_info_xml "postgresql" "$PG_VERSION" "PostgreSQL database server (musl/LLVM static build)" "postgresql" "5432"
    package_container "postgresql"
}

# ═══════════════════════════════════════════════════════════════
# Phase 5: Build AuthDB
# ═══════════════════════════════════════════════════════════════
build_authdb() {
    echo "==> Building AuthDB container..."

    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake

        if [ -f /build/libs/.authdb-server.done ]; then
            echo '  [skip] AuthDB server already built'
            exit 0
        fi

        cd /build/libs

        if [ ! -d authdb-full ]; then
            git clone --depth 1 '${REPOS[authdb]}' authdb-full
        fi

        mkdir -p authdb-full/build && cd authdb-full/build
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
            -DBUILD_BINDINGS=OFF \
            -DDATAPATH=/usr/local/share/authdb
        ninja
        ninja install

        touch /build/libs/.authdb-server.done
        echo '  [done] AuthDB server built'
    "

    local rootfs
    rootfs=$(create_minimal_rootfs "authdb")

    # Copy authdb binary
    cp "$CHROOT_DIR/usr/local/bin/authdb" "$rootfs/usr/local/bin/"
    copy_shared_libs "$rootfs"
    copy_debug_tools "$rootfs"
    # Copy confplus backend plugins (yaml.cfg.so etc.)
    if [[ -d "$CHROOT_DIR/usr/local/lib/confplus/backend" ]]; then
        mkdir -p "$rootfs/usr/local/lib/confplus/backend"
        cp -a "$CHROOT_DIR/usr/local/lib/confplus/backend/"*.so "$rootfs/usr/local/lib/confplus/backend/"
    fi
    # Copy data files if any
    if [[ -d "$CHROOT_DIR/usr/local/share/authdb" ]]; then
        cp -a "$CHROOT_DIR/usr/local/share/authdb" "$rootfs/usr/local/share/"
    fi
    # Copy authdb plugins
    if [[ -d "$CHROOT_DIR/usr/local/lib/authdb" ]]; then
        cp -a "$CHROOT_DIR/usr/local/lib/authdb" "$rootfs/usr/local/lib/"
    fi

    # Create user
    echo "authdb:x:999:999:AuthDB:/var/lib/authdb:/bin/sh" >> "$rootfs/etc/passwd"
    echo "authdb:x:999:" >> "$rootfs/etc/group"
    mkdir -p "$rootfs/var/lib/authdb"
    mkdir -p "$rootfs/etc/authdb"

    # Service
    cat > "$rootfs/etc/systemd/system/authdb.service" << 'EOF'
[Unit]
Description=AuthDB Authentication Service
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/bin/authdb -c yaml:/etc/authdb/config.yaml
Restart=on-failure
RestartSec=5
User=authdb
Group=authdb

[Install]
WantedBy=multi-user.target
EOF
    mkdir -p "$rootfs/etc/systemd/system/multi-user.target.wants"
    ln -sf /etc/systemd/system/authdb.service "$rootfs/etc/systemd/system/multi-user.target.wants/authdb.service"

    write_info_xml "authdb" "0.1" "AuthDB authentication service (musl/LLVM static build)" "authdb" "9090"
    package_container "authdb"
}

# ═══════════════════════════════════════════════════════════════
# Phase 6: Build MediaDB
# ═══════════════════════════════════════════════════════════════
build_mediadb() {
    echo "==> Building MediaDB container..."

    # MediaDB needs ffmpeg/libav — build from source with musl
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake

        # Build nasm (needed by ffmpeg for x86 asm optimizations)
        if [ ! -f /build/libs/.nasm.done ]; then
            echo '  [build] nasm'
            cd /build/libs
            if [ ! -d nasm ]; then
                git clone --depth 1 https://github.com/netwide-assembler/nasm.git nasm
            fi
            cd nasm
            sh autogen.sh
            ./configure --prefix=/usr/local
            make -j\$(nproc)
            install -m 755 nasm ndisasm /usr/local/bin/
            cd /build/libs
            touch .nasm.done
        else
            echo '  [skip] nasm already built'
        fi

        # Build ffmpeg with all common codecs
        if [ ! -f /build/libs/.ffmpeg.done ]; then
            echo '  [build] ffmpeg codec dependencies'
            cd /build/libs

            # Build zlib (needed by PNG encoder)
            if [ ! -f /usr/local/lib/libz.a ]; then
                echo '    -> zlib'
                if [ ! -d zlib ]; then
                    git clone --depth 1 https://github.com/madler/zlib.git zlib
                fi
                cd zlib
                CC=/usr/local/musl/bin/musl-clang CFLAGS=-fPIC \
                    ./configure --prefix=/usr/local --static
                make -j\$(nproc)
                make install
                cd /build/libs
            fi

            # Build x264 (H.264 encoder)
            if [ ! -f /usr/local/lib/libx264.so ]; then
                echo '    -> x264'
                if [ ! -d x264 ]; then
                    git clone --depth 1 https://code.videolan.org/videolan/x264.git x264
                fi
                cd x264
                CC=/usr/local/musl/bin/musl-clang ./configure \
                    --prefix=/usr/local \
                    --enable-shared \
                    --disable-static \
                    --enable-pic \
                    --disable-cli \
                    --disable-opencl
                make -j\$(nproc)
                make install
                cd /build/libs
            fi

            # Build x265 (H.265/HEVC encoder)
            if [ ! -f /usr/local/lib/libx265.so ]; then
                echo '    -> x265'
                if [ ! -d x265 ]; then
                    git clone --depth 1 https://bitbucket.org/multicoreware/x265_git.git x265
                fi
                rm -rf x265/build/linux
                mkdir -p x265/build/linux && cd x265/build/linux
                cmake ../../source -G Ninja \
                    -DCMAKE_C_COMPILER=/usr/local/musl/bin/musl-clang \
                    -DCMAKE_CXX_COMPILER=/usr/local/musl/bin/musl-clang++ \
                    -DCMAKE_INSTALL_PREFIX=/usr/local \
                    -DCMAKE_BUILD_TYPE=Release \
                    -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
                    -DENABLE_SHARED=ON \
                    -DENABLE_STATIC=OFF \
                    -DENABLE_PIC=ON \
                    -DENABLE_CLI=OFF
                ninja -j\$(nproc)
                ninja install
                # x265 cmake sometimes fails to install shared lib and .pc
                if [ ! -f /usr/local/lib/libx265.so ]; then
                    echo '      -> manually installing x265 shared lib'
                    cp -a libx265.so* /usr/local/lib/ 2>/dev/null || true
                    ls libx265.so.* 2>/dev/null | head -1 | xargs -I{} ln -sf {} /usr/local/lib/libx265.so 2>/dev/null || true
                fi
                if [ ! -f /usr/local/lib/pkgconfig/x265.pc ]; then
                    echo '      -> manually creating x265.pc'
                    mkdir -p /usr/local/lib/pkgconfig
                    X265_VER=$(grep '#define X265_VERSION' /usr/local/include/x265_config.h 2>/dev/null | sed 's/.*"\(.*\)".*/\1/' || echo '4.1')
                    cat > /usr/local/lib/pkgconfig/x265.pc << 'PCEOF'
prefix=/usr/local
exec_prefix=\${prefix}
libdir=\${exec_prefix}/lib
includedir=\${prefix}/include

Name: x265
Description: H.265/HEVC video encoder
Version: 4.1
Libs: -L\${libdir} -lx265
Libs.private: -lc++ -lc++abi -lpthread
Cflags: -I\${includedir}
PCEOF
                fi
                echo '      -> x265 verify:' && ls -la /usr/local/lib/libx265* /usr/local/lib/pkgconfig/x265.pc 2>/dev/null || true
                cd /build/libs
            fi

            # Build libaom (AV1 encoder/decoder)
            if [ ! -f /usr/local/lib/libaom.so ]; then
                echo '    -> libaom (AV1)'
                if [ ! -d aom ]; then
                    git clone --depth 1 https://aomedia.googlesource.com/aom aom
                fi
                mkdir -p aom/build && cd aom/build
                cmake .. -G Ninja \
                    -DCMAKE_C_COMPILER=/usr/local/musl/bin/musl-clang \
                    -DCMAKE_CXX_COMPILER=/usr/local/musl/bin/musl-clang++ \
                    -DCMAKE_INSTALL_PREFIX=/usr/local \
                    -DCMAKE_BUILD_TYPE=Release \
                    -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
                    -DBUILD_SHARED_LIBS=ON \
                    -DENABLE_TESTS=OFF \
                    -DENABLE_EXAMPLES=OFF \
                    -DENABLE_TOOLS=OFF \
                    -DENABLE_DOCS=OFF \
                    -DAOM_TARGET_CPU=x86_64
                ninja -j\$(nproc)
                ninja install
                cd /build/libs
            fi

            # Build libwebp (WebP encoder/decoder)
            if [ ! -f /usr/local/lib/libwebp.so ]; then
                echo '    -> libwebp'
                if [ ! -d libwebp ]; then
                    git clone --depth 1 https://chromium.googlesource.com/webm/libwebp libwebp
                fi
                mkdir -p libwebp/build && cd libwebp/build
                cmake .. -G Ninja \
                    -DCMAKE_C_COMPILER=/usr/local/musl/bin/musl-clang \
                    -DCMAKE_INSTALL_PREFIX=/usr/local \
                    -DCMAKE_BUILD_TYPE=Release \
                    -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
                    -DBUILD_SHARED_LIBS=ON \
                    -DWEBP_BUILD_ANIM_UTILS=OFF \
                    -DWEBP_BUILD_CWEBP=OFF \
                    -DWEBP_BUILD_DWEBP=OFF \
                    -DWEBP_BUILD_GIF2WEBP=OFF \
                    -DWEBP_BUILD_IMG2WEBP=OFF \
                    -DWEBP_BUILD_VWEBP=OFF \
                    -DWEBP_BUILD_WEBPINFO=OFF \
                    -DWEBP_BUILD_EXTRAS=OFF
                ninja -j\$(nproc)
                ninja install
                cd /build/libs
            fi

            # CMake's libwebp.pc has Requires.private: libsharpyuv but
            # does not generate libsharpyuv.pc — create it so pkg-config
            # --static resolution works for ffmpeg
            if [ ! -f /usr/local/lib/pkgconfig/libsharpyuv.pc ]; then
                echo '    -> creating missing libsharpyuv.pc'
                cat > /usr/local/lib/pkgconfig/libsharpyuv.pc << 'EOF'
prefix=/usr/local
exec_prefix=\${prefix}
libdir=\${exec_prefix}/lib
includedir=\${prefix}/include

Name: libsharpyuv
Description: Library for sharp RGB to YUV conversion
Version: 1.5.0
Cflags: -I\${includedir}
Libs: -L\${libdir} -lsharpyuv
Libs.private: -lm -lpthread
EOF
            fi

            echo '  [build] ffmpeg (full codecs)'
            cd /build/libs

            # Verify pkg-config can find libwebp before proceeding
            echo '    -> testing pkg-config for libwebp...'
            /usr/local/bin/pkg-config --modversion libwebp || {
                echo 'FATAL: pkg-config cannot find libwebp'
                ls -la /usr/local/lib/pkgconfig/libwebp.pc /usr/local/lib/pkgconfig/libsharpyuv.pc 2>/dev/null || true
                /usr/local/bin/pkg-config --debug libwebp 2>&1 | head -30
                exit 1
            }

            if [ ! -d ffmpeg ]; then
                git clone --depth 1 https://git.ffmpeg.org/ffmpeg.git ffmpeg
            fi
            cd ffmpeg
            export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig
            export PKG_CONFIG_LIBDIR=/usr/local/lib/pkgconfig
            ./configure \
                --prefix=/usr/local \
                --cc=/usr/local/musl/bin/musl-clang \
                --cxx=/usr/local/musl/bin/musl-clang++ \
                --pkg-config=/usr/local/bin/pkg-config \
                --enable-cross-compile \
                --target-os=linux \
                --arch=x86_64 \
                --enable-shared \
                --disable-static \
                --disable-programs \
                --disable-doc \
                --disable-network \
                --disable-autodetect \
                --disable-iconv \
                --enable-pic \
                --enable-zlib \
                --enable-libx264 \
                --enable-libx265 \
                --enable-libaom \
                --enable-libwebp \
                --enable-gpl \
                --enable-encoder=libx264 \
                --enable-encoder=libx265 \
                --enable-encoder=libaom_av1 \
                --enable-encoder=libwebp \
                --enable-encoder=mjpeg \
                --enable-encoder=png \
                --enable-encoder=gif \
                --enable-encoder=webp \
                --enable-decoder=mjpeg \
                --enable-decoder=png \
                --enable-decoder=gif \
                --enable-decoder=webp \
                --enable-decoder=h264 \
                --enable-decoder=hevc \
                --enable-decoder=av1 \
                --enable-decoder=bmp \
                --enable-decoder=tiff \
                --enable-demuxer=image2 \
                --enable-demuxer=mov \
                --enable-demuxer=matroska \
                --enable-demuxer=avi \
                --enable-demuxer=gif \
                --enable-demuxer=png_pipe \
                --enable-demuxer=webp_pipe \
                --enable-muxer=image2 \
                --enable-muxer=mjpeg \
                --enable-muxer=webp \
                --enable-muxer=gif \
                --enable-muxer=mp4 \
                --enable-filter=scale \
                --enable-filter=thumbnail \
                --enable-filter=select \
                --enable-protocol=file \
                --extra-cflags=-I/usr/local/include \
                --extra-ldflags=-L/usr/local/lib
            make -j\$(nproc)
            make install
            cd /build/libs
            touch .ffmpeg.done
        else
            echo '  [skip] ffmpeg already built'
        fi

        # Build mediadb server
        if [ -f /build/libs/.mediadb-server.done ]; then
            echo '  [skip] MediaDB server already built'
            exit 0
        fi

        cd /build/libs
        if [ ! -d mediadb-full ]; then
            git clone --depth 1 '${REPOS[mediadb]}' mediadb-full
        fi

        mkdir -p mediadb-full/build && cd mediadb-full/build
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DCMAKE_POSITION_INDEPENDENT_CODE=ON
        ninja
        ninja install

        touch /build/libs/.mediadb-server.done
        echo '  [done] MediaDB server built'
    "

    local rootfs
    rootfs=$(create_minimal_rootfs "mediadb")

    cp "$CHROOT_DIR/usr/local/bin/mediadb" "$rootfs/usr/local/bin/"
    copy_shared_libs "$rootfs"
    copy_debug_tools "$rootfs"
    # Copy ffmpeg and codec shared libraries
    for lib in libavformat.so* libavcodec.so* libavutil.so* libswscale.so* libswresample.so* \
               libx264.so* libx265.so* libaom.so* libwebp.so* libwebpmux.so* libwebpdemux.so* libsharpyuv.so*; do
        find "$CHROOT_DIR/usr/local/lib" -maxdepth 1 -name "$lib" -exec cp -a {} "$rootfs/usr/local/lib/" \;
    done
    # Copy confplus backend plugins (yaml.cfg.so etc.)
    if [[ -d "$CHROOT_DIR/usr/local/lib/confplus/backend" ]]; then
        mkdir -p "$rootfs/usr/local/lib/confplus/backend"
        cp -a "$CHROOT_DIR/usr/local/lib/confplus/backend/"*.so "$rootfs/usr/local/lib/confplus/backend/"
    fi
    if [[ -d "$CHROOT_DIR/usr/local/share/mediadb" ]]; then
        cp -a "$CHROOT_DIR/usr/local/share/mediadb" "$rootfs/usr/local/share/"
    fi

    echo "mediadb:x:999:999:MediaDB:/var/lib/mediadb:/bin/sh" >> "$rootfs/etc/passwd"
    echo "mediadb:x:999:" >> "$rootfs/etc/group"
    mkdir -p "$rootfs/var/lib/mediadb"
    mkdir -p "$rootfs/etc/mediadb"

    cat > "$rootfs/etc/systemd/system/mediadb.service" << 'EOF'
[Unit]
Description=MediaDB Media Service
After=network.target

[Service]
Type=forking
ExecStart=/usr/local/bin/mediadb -c yaml:/etc/mediadb/config.yaml
Restart=on-failure
RestartSec=5
User=mediadb
Group=mediadb

[Install]
WantedBy=multi-user.target
EOF
    mkdir -p "$rootfs/etc/systemd/system/multi-user.target.wants"
    ln -sf /etc/systemd/system/mediadb.service "$rootfs/etc/systemd/system/multi-user.target.wants/mediadb.service"

    write_info_xml "mediadb" "0.1" "MediaDB media service (musl/LLVM static build)" "mediadb" "8080"
    package_container "mediadb"
}

# ═══════════════════════════════════════════════════════════════
# Phase 7: Build Blogi
# ═══════════════════════════════════════════════════════════════
# Build the static setuidgid helper into <rootfs>/usr/local/bin so a container
# service can drop to the unprivileged 999:999 user at exec time (no NSS/PAM).
build_setuidgid() {
    local rootfs="$1"
    local script_dir
    script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
    mkdir -p "$rootfs/usr/local/bin"
    if command -v musl-gcc &>/dev/null; then
        musl-gcc -static -O2 -o "$rootfs/usr/local/bin/setuidgid" "$script_dir/setuidgid.c"
    else
        $NSPAWN -D "$CHROOT_DIR" \
            --bind-ro="$script_dir:/build/packaging" \
            --bind="$rootfs/usr/local/bin:/build/out" --pipe -- bash -c "
            /usr/local/musl/bin/musl-clang -static -O2 -o /build/out/setuidgid /build/packaging/setuidgid.c
        "
    fi
    chmod +x "$rootfs/usr/local/bin/setuidgid"
}

build_blogi() {
    echo "==> Building Blogi container..."

    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake

        if [ -f /build/libs/.blogi-server.done ]; then
            echo '  [skip] Blogi server already built'
            exit 0
        fi

        cd /build/libs
        if [ ! -d blogi ]; then
            git clone --depth 1 '${REPOS[blogi]}' blogi
        fi

        mkdir -p blogi/build && cd blogi/build
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
            -DBUILD_EDITOR=OFF
        ninja
        ninja install

        touch /build/libs/.blogi-server.done
        echo '  [done] Blogi server built'
    "

    local rootfs
    rootfs=$(create_minimal_rootfs "blogi")

    cp "$CHROOT_DIR/usr/local/bin/blogi" "$rootfs/usr/local/bin/"

    # Build setuidgid helper so the service can drop to the blogi user (999:999)
    # at exec time instead of starting as root.
    build_setuidgid "$rootfs"

    copy_shared_libs "$rootfs"
    copy_debug_tools "$rootfs"
    # Copy confplus backend plugins (yaml.cfg.so etc.)
    if [[ -d "$CHROOT_DIR/usr/local/lib/confplus/backend" ]]; then
        mkdir -p "$rootfs/usr/local/lib/confplus/backend"
        cp -a "$CHROOT_DIR/usr/local/lib/confplus/backend/"*.so "$rootfs/usr/local/lib/confplus/backend/"
    fi
    # Copy dbpp backend plugins (pgsql.so, sqlite.so)
    if [[ -d "$CHROOT_DIR/usr/local/lib/dbpp" ]]; then
        mkdir -p "$rootfs/usr/local/lib/dbpp"
        cp -a "$CHROOT_DIR/usr/local/lib/dbpp/"*.so "$rootfs/usr/local/lib/dbpp/"
    fi
    # Copy blogi plugins
    if [[ -d "$CHROOT_DIR/usr/local/lib/blogi/plugins" ]]; then
        mkdir -p "$rootfs/usr/local/lib/blogi/plugins"
        cp -a "$CHROOT_DIR/usr/local/lib/blogi/plugins/"*.so "$rootfs/usr/local/lib/blogi/plugins/"
    fi
    if [[ -d "$CHROOT_DIR/usr/local/share/blogi" ]]; then
        cp -a "$CHROOT_DIR/usr/local/share/blogi" "$rootfs/usr/local/share/"
    fi

    echo "blogi:x:999:999:Blogi:/var/lib/blogi:/bin/sh" >> "$rootfs/etc/passwd"
    echo "blogi:x:999:" >> "$rootfs/etc/group"
    mkdir -p "$rootfs/var/lib/blogi"
    mkdir -p "$rootfs/etc/blogi"
    mkdir -p "$rootfs/var/log/blogi"
    chown 999:999 "$rootfs/var/lib/blogi" "$rootfs/var/log/blogi"

    cat > "$rootfs/etc/systemd/system/blogi.service" << 'EOF'
[Unit]
Description=Blogi Blog Engine
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/setuidgid 999:999 /usr/local/bin/blogi -F true -c yaml:/etc/blogi/config.yaml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
    mkdir -p "$rootfs/etc/systemd/system/multi-user.target.wants"
    ln -sf /etc/systemd/system/blogi.service "$rootfs/etc/systemd/system/multi-user.target.wants/blogi.service"

    write_info_xml "blogi" "0.1" "Blogi blog engine (musl/LLVM static build)" "blogi" "8080"
    package_container "blogi"
}

# ═══════════════════════════════════════════════════════════════
# Phase 8: Build Blogi Editor
# ═══════════════════════════════════════════════════════════════
build_blogi_editor() {
    echo "==> Building Blogi Editor container..."

    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake

        if [ -f /build/libs/.blogi-editor.done ]; then
            echo '  [skip] Blogi Editor already built'
            exit 0
        fi

        cd /build/libs
        # Reuse blogi source (editor is a subdirectory)
        if [ ! -d blogi ]; then
            git clone --depth 1 '${REPOS[blogi]}' blogi
        fi

        mkdir -p blogi/editor/build && cd blogi/editor/build
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DCMAKE_POSITION_INDEPENDENT_CODE=ON
        ninja
        ninja install

        touch /build/libs/.blogi-editor.done
        echo '  [done] Blogi Editor built'
    "

    local rootfs
    rootfs=$(create_minimal_rootfs "blogi-editor")

    cp "$CHROOT_DIR/usr/local/bin/blogi-editor" "$rootfs/usr/local/bin/"
    # Build setuidgid helper so the editor can drop to the blogi-editor user
    # (999:999) at exec time instead of starting as root.
    build_setuidgid "$rootfs"
    copy_shared_libs "$rootfs"
    copy_debug_tools "$rootfs"
    # Copy confplus backend plugins (yaml.cfg.so etc.)
    if [[ -d "$CHROOT_DIR/usr/local/lib/confplus/backend" ]]; then
        mkdir -p "$rootfs/usr/local/lib/confplus/backend"
        cp -a "$CHROOT_DIR/usr/local/lib/confplus/backend/"*.so "$rootfs/usr/local/lib/confplus/backend/"
    fi
    # Copy editor plugins (webedit widgets)
    if [[ -d "$CHROOT_DIR/usr/lib/blogi/plugins/webedit" ]]; then
        mkdir -p "$rootfs/usr/lib/blogi/plugins"
        cp -a "$CHROOT_DIR/usr/lib/blogi/plugins/webedit" "$rootfs/usr/lib/blogi/plugins/"
    elif [[ -d "$CHROOT_DIR/usr/local/lib/blogi/plugins/webedit" ]]; then
        mkdir -p "$rootfs/usr/lib/blogi/plugins"
        cp -a "$CHROOT_DIR/usr/local/lib/blogi/plugins/webedit" "$rootfs/usr/lib/blogi/plugins/"
    fi
    # Copy HTML assets
    if [[ -d "$CHROOT_DIR/usr/share/blogi-editor/html" ]]; then
        mkdir -p "$rootfs/usr/share/blogi-editor"
        cp -a "$CHROOT_DIR/usr/share/blogi-editor/html" "$rootfs/usr/share/blogi-editor/"
    elif [[ -d "$CHROOT_DIR/usr/local/share/blogi-editor/html" ]]; then
        mkdir -p "$rootfs/usr/share/blogi-editor"
        cp -a "$CHROOT_DIR/usr/local/share/blogi-editor/html" "$rootfs/usr/share/blogi-editor/"
    elif [[ -d "$CHROOT_DIR/blogi-editor/html" ]]; then
        mkdir -p "$rootfs/usr/share/blogi-editor"
        cp -a "$CHROOT_DIR/blogi-editor/html" "$rootfs/usr/share/blogi-editor/"
    fi
    # Copy dbpp backend plugins (pgsql.so, sqlite.so)
    if [[ -d "$CHROOT_DIR/usr/local/lib/dbpp" ]]; then
        mkdir -p "$rootfs/usr/local/lib/dbpp"
        cp -a "$CHROOT_DIR/usr/local/lib/dbpp/"*.so "$rootfs/usr/local/lib/dbpp/"
    fi
    # Copy libpq for pgsql.so (check both prefix locations)
    for prefix_dir in "$CHROOT_DIR/usr/local/lib" "$CHROOT_DIR/usr/local/pgsql/lib"; do
        for f in "$prefix_dir/libpq.so"*; do
            [[ -e "$f" ]] && cp -a "$f" "$rootfs/usr/local/lib/"
        done
    done
    # Copy blogidev static lib (for plugins)
    if [[ -f "$CHROOT_DIR/usr/local/lib/blogi/libblogidev.a" ]]; then
        mkdir -p "$rootfs/usr/lib/blogi"
        cp "$CHROOT_DIR/usr/local/lib/blogi/libblogidev.a" "$rootfs/usr/lib/blogi/"
    fi

    echo "blogi-editor:x:999:999:Blogi Editor:/var/lib/blogi-editor:/bin/sh" >> "$rootfs/etc/passwd"
    echo "blogi-editor:x:999:" >> "$rootfs/etc/group"
    mkdir -p "$rootfs/var/lib/blogi-editor"
    mkdir -p "$rootfs/etc/blogi-editor"
    chown 999:999 "$rootfs/var/lib/blogi-editor"

    cat > "$rootfs/etc/systemd/system/blogi-editor.service" << 'EOF'
[Unit]
Description=Blogi Editor Web Server
After=network.target

[Service]
Type=simple
ExecStart=/usr/local/bin/setuidgid 999:999 /usr/local/bin/blogi-editor -c yaml:/etc/blogi-editor/config.yaml
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF
    mkdir -p "$rootfs/etc/systemd/system/multi-user.target.wants"
    ln -sf /etc/systemd/system/blogi-editor.service "$rootfs/etc/systemd/system/multi-user.target.wants/blogi-editor.service"

    write_info_xml "blogi-editor" "0.1" "Blogi Editor web-based editor (musl/LLVM static build)" "blogi-editor" "8090"
    package_container "blogi-editor"
}

# ═══════════════════════════════════════════════════════════════
# Phase: Build syslite (lightweight init system)
# ═══════════════════════════════════════════════════════════════
build_syslite() {
    echo "==> Building syslite init system..."

    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake

        if [ -f /build/libs/.syslite.done ]; then
            echo '  [skip] syslite already built'
            exit 0
        fi

        cd /build/libs
        if [ ! -d syslite ]; then
            git clone --depth 1 '${REPOS[syslite]}' syslite
        fi

        mkdir -p syslite/build && cd syslite/build
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
            -DSYSLITE_UNIT_DIR=/etc/syslite/services \
            -DSYSLITE_DEFAULT_TARGET=multi-user.target \
            -DSYSLITE_TTY_COUNT=3
        ninja
        ninja install

        touch /build/libs/.syslite.done
        echo '  [done] syslite built'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: Package only (re-package existing builds)
# ═══════════════════════════════════════════════════════════════
package_all() {
    echo "==> Packaging all containers..."
    for name in postgresql authdb mediadb blogi blogi-editor; do
        if [[ -d "$CONTAINER_DIR/$name/rootfs" ]]; then
            package_container "$name"
        else
            echo "  [skip] $name not built yet"
        fi
    done
}

# ═══════════════════════════════════════════════════════════════
# Phase: Development chroot
#   Prepare a full toolchain + libraries chroot and drop into an
#   interactive shell so you can build/develop against the musl/LLVM
#   toolchain by hand. Sources and build artifacts live under /build.
# ═══════════════════════════════════════════════════════════════
enter_chroot_shell() {
    if [[ ! -d "$CHROOT_DIR/usr/bin" ]]; then
        echo "ERROR: chroot at $CHROOT_DIR does not exist yet."
        echo "       Run '$0 chroot' (or 'devel') first."
        exit 1
    fi

    # Make sure the musl dynamic linker is reachable at the standard path so
    # musl-built binaries (clang, makeplus, cmake, ...) can run in the shell.
    if [[ -f "$CHROOT_DIR/usr/local/musl/lib/ld-musl-x86_64.so.1" ]] && \
       [[ ! -e "$CHROOT_DIR/lib/ld-musl-x86_64.so.1" ]]; then
        mkdir -p "$CHROOT_DIR/lib"
        ln -sf /usr/local/musl/lib/ld-musl-x86_64.so.1 "$CHROOT_DIR/lib/ld-musl-x86_64.so.1"
    fi

    echo ""
    echo "╔══════════════════════════════════════════════════════════════╗"
    echo "║  Development chroot — musl + LLVM ${LLVM_VERSION} toolchain"
    echo "╚══════════════════════════════════════════════════════════════╝"
    echo "  Toolchain : /usr/local/musl/bin (musl-clang, musl-clang++, lld, lldb)"
    echo "  Buildtools: makeplus (make), cmake, ninja, toybox"
    echo "  CMake tc  : /usr/local/musl/share/musl-llvm-toolchain.cmake"
    echo "  Sources   : /build (and /build/libs for dependency checkouts)"
    echo "  Type 'exit' to leave the chroot."
    echo ""

    # Interactive shell: no --pipe (we want a tty). Preconfigure the
    # environment so the musl/LLVM toolchain and makeplus are picked up.
    systemd-nspawn --register=no -D "$CHROOT_DIR" \
        --setenv=PATH=/usr/local/musl/bin:/usr/local/bin:/usr/bin:/bin:/usr/local/sbin:/usr/sbin:/sbin \
        --setenv=CC=/usr/local/musl/bin/musl-clang \
        --setenv=CXX=/usr/local/musl/bin/musl-clang++ \
        --setenv=MAKE=/usr/local/musl/bin/makeplus \
        --setenv=CMAKE_TOOLCHAIN_FILE=/usr/local/musl/share/musl-llvm-toolchain.cmake \
        --setenv=PKG_CONFIG_PATH=/usr/local/lib/pkgconfig \
        --setenv=PS1='(devel) \w # ' \
        --chdir=/build \
        /bin/bash
}

setup_devel() {
    echo "==> Preparing development chroot (toolchain + buildtools + libraries)..."
    setup_chroot
    setup_toolchain
    build_tools
    build_libraries
    echo "==> Development chroot ready."
    enter_chroot_shell
}

# ═══════════════════════════════════════════════════════════════
# Phase: Pack the development chroot into a tarball
#   Produces a compressed archive of the full dev chroot rootfs so it can
#   be copied to other machines and unpacked / re-entered with nspawn.
#   Override the destination with DEVEL_TAR=/path/to/file.tar.zst
# ═══════════════════════════════════════════════════════════════
package_devel_chroot() {
    if [[ ! -d "$CHROOT_DIR/usr/bin" ]]; then
        echo "ERROR: chroot at $CHROOT_DIR does not exist yet."
        echo "       Run '$0 devel' (or 'chroot toolchain buildtools libraries') first."
        exit 1
    fi

    mkdir -p "$OUTPUT_DIR"

    # Pick a compressor: prefer zstd (fast, good ratio), fall back to xz.
    local tar_file comp_flag
    if command -v zstd &>/dev/null; then
        tar_file="${DEVEL_TAR:-$OUTPUT_DIR/fenriros-devel-${ARCH}.tar.zst}"
        comp_flag="--zstd"
    else
        tar_file="${DEVEL_TAR:-$OUTPUT_DIR/fenriros-devel-${ARCH}.tar.xz}"
        comp_flag="-J"
    fi

    echo "==> Packing development chroot into $tar_file ..."
    echo "    Source: $CHROOT_DIR"

    # Exclude transient build trees and VCS checkouts to keep the archive small;
    # the installed toolchain (/usr/local/musl) and libraries (/usr/local) stay.
    # Use numeric owners and one-file-system so nothing outside the rootfs leaks.
    tar --numeric-owner --one-file-system $comp_flag \
        --exclude='./build/llvm-project' \
        --exclude='./build/build-llvm-host' \
        --exclude='./build/build-llvm-tools' \
        --exclude='./build/build-runtimes' \
        --exclude='./build/llvm-host' \
        --exclude='./build/libs' \
        --exclude='./tmp/*' \
        --exclude='./var/cache/apt/archives/*.deb' \
        --exclude='./var/lib/apt/lists/*' \
        --exclude='./proc/*' --exclude='./sys/*' --exclude='./dev/*' \
        --exclude='./run/*' \
        -C "$CHROOT_DIR" -cf "$tar_file" .

    local size sha
    size="$(stat -c %s "$tar_file" 2>/dev/null || echo 0)"
    sha="$(sha256sum "$tar_file" | awk '{print $1}')"
    echo "$sha" > "$tar_file.sha256"

    echo "  [done] $tar_file"
    echo "         size:   $(numfmt --to=iec "$size" 2>/dev/null || echo "${size} bytes")"
    echo "         sha256: $sha"
    echo ""
    echo "  Unpack on the target with:"
    echo "    mkdir -p rootfs && tar -C rootfs -xf $(basename "$tar_file")"
    echo "    sudo systemd-nspawn -D rootfs /bin/bash"
}

# ═══════════════════════════════════════════════════════════════
# Phase: libelf (musl)
#   Built into the toolchain prefix so kernel/module tooling and anything that
#   needs <libelf.h> can link against a musl-native libelf.
# ═══════════════════════════════════════════════════════════════
# zlib (permissive licence, GNU-free) — hard dependency of elfutils libelf.
build_zlib() {
    if [[ -f "$CHROOT_DIR/usr/local/musl/lib/libz.so" || -f "$CHROOT_DIR/usr/local/musl/lib/libz.a" ]]; then
        echo "==> zlib already built, skipping"
        return
    fi
    echo "==> Building zlib ${ZLIB_VERSION} (musl, upstream CMake) ..."
    # NOTE: install into /usr/local/musl (the musl sysroot), NOT /usr/local.
    # /usr/local/lib is on glibc's global ld.so path; a musl-built libz.so there
    # gets loaded into glibc system tools (cmake, git, ar, ...) and crashes them.
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d zlib ]; then
            git clone --depth 1 --branch ${ZLIB_TAG} '${REPOS[zlib]}' zlib
        fi
        cd zlib
        rm -rf build && mkdir -p build && cd build
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local/musl \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE}
        ninja
        ninja install
        echo '==> zlib installed to /usr/local/musl'
    "
}

build_libelf() {
    if [[ -f "$CHROOT_DIR/usr/local/musl/lib/libelf.so" ]]; then
        echo "==> libelf already built, skipping"
        return
    fi
    # elfutils libelf needs zlib.
    build_zlib
    echo "==> Building elfutils libelf ${LIBELF_VERSION} (musl, CMake, GNU-free) ..."
    # Provide our GNU-free CMakeLists + musl compat shim (no autoconf/GNU make).
    local libelf_cmake="$SCRIPT_DIR/files/libelf/CMakeLists.txt"
    local libelf_compat="$SCRIPT_DIR/files/libelf/musl-compat.h"
    if [[ ! -f "$libelf_cmake" || ! -f "$libelf_compat" ]]; then
        echo "ERROR: missing $libelf_cmake / $libelf_compat (libelf build files)." >&2
        exit 1
    fi
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$libelf_cmake:/build/libelf-CMakeLists.txt" \
        --bind-ro="$libelf_compat:/build/libelf-musl-compat.h" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d elfutils ]; then
            git clone --depth 1 --branch ${ELFUTILS_TAG} '${REPOS[elfutils]}' elfutils || \
            git clone --depth 1 '${REPOS[elfutils]}' elfutils
        fi
        # Drop our GNU-free CMake build at the elfutils source root (next to
        # libelf/ and lib/), overriding the autotools build entirely.
        cp /build/libelf-CMakeLists.txt /build/libs/elfutils/CMakeLists.txt
        cp /build/libelf-musl-compat.h  /build/libs/elfutils/musl-compat.h
        cd /build/libs/elfutils
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local/musl \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DLIBELF_VERSION=${LIBELF_VERSION}
        ninja
        ninja install
        echo '==> elfutils libelf installed to /usr/local/musl (libelf.pc provided)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: LibreSSL (musl) — GNU-free libcrypto/libssl for the kernel host tools
#   The kernel's certs/extract-cert host program links libcrypto and uses the
#   legacy OPENSSL_add_all_algorithms_noconf / ERR_load_crypto_strings APIs.
#   OpenSSL 3.x (the host's glibc lib) dropped those, so the host link must
#   resolve -lcrypto to LibreSSL inside the musl sysroot instead. Installing it
#   into /usr/local/musl makes the musl clang prefer it over /lib/<triplet>.
# ═══════════════════════════════════════════════════════════════
build_libressl() {
    if [[ -f "$CHROOT_DIR/usr/local/musl/lib/libcrypto.a" ]]; then
        echo "==> LibreSSL (musl sysroot) already built, skipping"
        return
    fi
    echo "==> Building LibreSSL ${LIBRESSL_VERSION} into /usr/local/musl (musl) ..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d libressl-musl ]; then
            if [ ! -f libressl.tar.gz ]; then
                wget -q '${REPOS[libressl]}' -O libressl.tar.gz
            fi
            tar xf libressl.tar.gz
            mv libressl-${LIBRESSL_VERSION} libressl-musl
        fi
        rm -rf libressl-musl/build-musl
        mkdir -p libressl-musl/build-musl && cd libressl-musl/build-musl
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local/musl \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DCMAKE_POSITION_INDEPENDENT_CODE=ON \
            -DBUILD_SHARED_LIBS=OFF \
            -DLIBRESSL_APPS=ON \
            -DLIBRESSL_TESTS=OFF
        ninja
        ninja install
        echo '==> LibreSSL installed to /usr/local/musl (libcrypto.a/libssl.a)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: OpenSSH server/client (musl, GNU-free)
#   Portable OpenSSH built with its bundled crypto (--without-openssl) so there
#   is no LibreSSL version coupling. Installed under /usr/local so the compiled-in
#   helper paths (sshd-session, sftp-server) land on the standard PATH/libexec
#   dirs and stay valid when the tree is copied into the system image.
# ═══════════════════════════════════════════════════════════════
build_openssh() {
    if [[ -x "$CHROOT_DIR/usr/local/sbin/sshd" ]]; then
        echo "==> OpenSSH already built, skipping"
        return
    fi
    # libssh links zlib (CHANNELLIBS=-lz); make sure it is in the musl sysroot.
    build_zlib
    echo "==> Building OpenSSH ${OPENSSH_VERSION} into /usr/local (musl, CMake, GNU-free) ..."
    # GNU-free CMake build file (mirrors the musl/libelf pattern). It replaces
    # OpenSSH's GNU-make build; ./configure is still run once as a POSIX-sh
    # platform probe to emit config.h (it is not a GNU build tool).
    local openssh_cmake="$SCRIPT_DIR/files/openssh/CMakeLists.txt"
    if [[ ! -f "$openssh_cmake" ]]; then
        echo "ERROR: missing $openssh_cmake (OpenSSH CMake build file)." >&2
        exit 1
    fi
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$openssh_cmake:/build/openssh-CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        # wget for the download; no GNU make needed (CMake + Ninja drive the build).
        apt-get install -y --no-install-recommends wget || true
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d openssh-musl ]; then
            if [ ! -f openssh-${OPENSSH_VERSION}.tar.gz ]; then
                wget -q 'https://cdn.openbsd.org/pub/OpenBSD/OpenSSH/portable/openssh-${OPENSSH_VERSION}.tar.gz' \
                    -O openssh-${OPENSSH_VERSION}.tar.gz
            fi
            tar xf openssh-${OPENSSH_VERSION}.tar.gz
            mv openssh-${OPENSSH_VERSION} openssh-musl
        fi
        cd openssh-musl
        # zlib ships its libz in the musl sysroot (/usr/local/musl/lib) but its
        # header was installed to the libc-agnostic /usr/local/include. Stage it
        # into the musl sysroot include dir (same pattern as libelf) so both the
        # configure probe and the CMake build find the header next to the lib.
        [ -f /usr/local/musl/include/zlib.h ] || \
            cp /usr/local/include/zlib.h /usr/local/include/zconf.h /usr/local/musl/include/ 2>/dev/null || true
        # One-time platform probe → config.h (autoconf /bin/sh script, NOT GNU
        # make). Bundled crypto (--without-openssl) keeps the build self-contained.
        if [ ! -f config.h ]; then
            CC=/usr/local/musl/bin/musl-clang \
            CFLAGS='-O2' \
            ./configure \
                --prefix=/usr/local \
                --sysconfdir=/etc/ssh \
                --with-privsep-path=/var/empty \
                --with-privsep-user=sshd \
                --with-zlib=/usr/local/musl \
                --without-openssl \
                --without-pam \
                --disable-strip
        fi
        # Drop in the GNU-free CMakeLists and build/link every target via Ninja.
        cp /build/openssh-CMakeLists.txt CMakeLists.txt
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE}
        ninja
        ninja install
        echo '==> OpenSSH installed to /usr/local (sshd + clients, CMake build)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: dosfstools (FAT filesystem tools — GNU-free CMake build)
#   mkfs.fat / fsck.fat / fatlabel for FAT12/16/32. dosfstools ships only an
#   autotools configure.ac (no configure script and no config.h), so the CMake
#   build probes the platform itself; no ./configure / autoreconf is run.
# ═══════════════════════════════════════════════════════════════
build_dosfstools() {
    if [[ -x "$CHROOT_DIR/usr/local/sbin/mkfs.fat" ]]; then
        echo "==> dosfstools already built, skipping"
        return
    fi
    echo "==> Building dosfstools ${DOSFSTOOLS_VERSION} into /usr/local (musl, CMake, GNU-free) ..."
    local dft_cmake="$SCRIPT_DIR/files/dosfstools/CMakeLists.txt"
    if [[ ! -f "$dft_cmake" ]]; then
        echo "ERROR: missing $dft_cmake (dosfstools CMake build file)." >&2
        exit 1
    fi
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$dft_cmake:/build/dosfstools-CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d dosfstools ]; then
            git clone --depth 1 '${REPOS[dosfstools]}' dosfstools
        fi
        cd dosfstools
        # GNU-free CMake build (no configure.ac/autoreconf): the CMakeLists
        # probes headers/symbols itself and generates src/version.h.
        cp /build/dosfstools-CMakeLists.txt CMakeLists.txt
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DPACKAGE_VERSION=${DOSFSTOOLS_VERSION}
        ninja
        ninja install
        echo '==> dosfstools installed to /usr/local (mkfs.fat/fsck.fat/fatlabel)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: e2fsprogs (ext2/3/4 filesystem tools — GNU-free CMake build)
#   mke2fs / e2fsck / tune2fs / dumpe2fs / resize2fs / badblocks / blkid / ...
#   e2fsprogs ships a committed ./configure (a POSIX-sh platform probe, not a
#   GNU build tool); it is run once to emit config.h and the *_types.h headers
#   plus compile_et, then the GNU-free CMakeLists builds everything via Ninja.
# ═══════════════════════════════════════════════════════════════
build_e2fsprogs() {
    # Skip only when both the tools AND the static libuuid export are present
    # (older installs predate the libuuid export that gptfdisk/sgdisk links).
    if [[ -x "$CHROOT_DIR/usr/local/sbin/mke2fs" && -f "$CHROOT_DIR/usr/local/lib/libuuid.a" ]]; then
        echo "==> e2fsprogs already built, skipping"
        return
    fi
    echo "==> Building e2fsprogs ${E2FSPROGS_VERSION} into /usr/local (musl, CMake, GNU-free) ..."
    local e2fs_cmake="$SCRIPT_DIR/files/e2fsprogs/CMakeLists.txt"
    if [[ ! -f "$e2fs_cmake" ]]; then
        echo "ERROR: missing $e2fs_cmake (e2fsprogs CMake build file)." >&2
        exit 1
    fi
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$e2fs_cmake:/build/e2fsprogs-CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        # awk is needed by the CMake build (compile_et / profile-to-c.awk).
        apt-get install -y --no-install-recommends gawk pkg-config || true
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d e2fsprogs ]; then
            git clone --depth 1 --branch v${E2FSPROGS_VERSION} '${REPOS[e2fsprogs]}' e2fsprogs \
                || git clone --depth 1 '${REPOS[e2fsprogs]}' e2fsprogs
        fi
        cd e2fsprogs
        # One-time POSIX-sh platform probe → lib/config.h, *_types.h, dirpaths.h,
        # lib/et/compile_et. --without-libarchive stubs out the libarchive path;
        # --disable-nls/--disable-fuse2fs keep the build minimal and GNU-free.
        if [ ! -f lib/config.h ]; then
            CC=/usr/local/musl/bin/musl-clang \
            CFLAGS='-O2' \
            ./configure \
                --prefix=/usr/local \
                --without-libarchive \
                --disable-nls \
                --disable-fuse2fs \
                --disable-defrag
        fi
        # Drop in the GNU-free CMakeLists (it regenerates ext2_err/prof_err/
        # crc32c_table.h/default_profile.c) and build via Ninja.
        # lib/dirpaths.h embeds the install prefix and is only regenerated when
        # absent; drop any stale copy so it picks up the current prefix.
        rm -f lib/dirpaths.h
        cp /build/e2fsprogs-CMakeLists.txt CMakeLists.txt
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DE2FSPROGS_VERSION=${E2FSPROGS_VERSION}
        ninja
        ninja install
        echo '==> e2fsprogs installed to /usr/local (mke2fs/e2fsck/tune2fs/resize2fs/...)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: popt (command-line parser — GNU-free CMake build)
#   Static libpopt.a only; it is the CLI-parsing dependency of sgdisk. Built
#   from files/popt/CMakeLists.txt (a minimal replacement for popt's own shared
#   build) so nothing is linked into the system image at runtime.
# ═══════════════════════════════════════════════════════════════
build_popt() {
    if [[ -f "$CHROOT_DIR/usr/local/lib/libpopt.a" ]]; then
        echo "==> popt already built, skipping"
        return
    fi
    echo "==> Building popt into /usr/local (static, musl, CMake, GNU-free) ..."
    local popt_cmake="$SCRIPT_DIR/files/popt/CMakeLists.txt"
    if [[ ! -f "$popt_cmake" ]]; then
        echo "ERROR: missing $popt_cmake (popt CMake build file)." >&2
        exit 1
    fi
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$popt_cmake:/build/popt-CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d popt ]; then
            git clone --depth 1 '${REPOS[popt]}' popt
        fi
        cd popt
        # GNU-free CMake build (the upstream top-level CMakeLists pulls in
        # gettext/iconv/version-script/exports we do not want).
        cp /build/popt-CMakeLists.txt CMakeLists.txt
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE}
        ninja
        ninja install
        echo '==> popt installed to /usr/local (static libpopt.a + popt.h)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: gptfdisk / sgdisk (GPT partitioner — GNU-free CMake build)
#   Builds only the scriptable `sgdisk`, statically linked against the
#   e2fsprogs libuuid + libpopt. This is the partitioner install.sh drives.
# ═══════════════════════════════════════════════════════════════
build_gptfdisk() {
    if [[ -x "$CHROOT_DIR/usr/local/sbin/sgdisk" ]]; then
        echo "==> gptfdisk already built, skipping"
        return
    fi
    # sgdisk links the static libuuid (from e2fsprogs) and libpopt; make sure
    # both are present in /usr/local before building.
    build_e2fsprogs
    build_popt
    echo "==> Building gptfdisk ${GPTFDISK_VERSION} (sgdisk) into /usr/local (musl, CMake, GNU-free) ..."
    local gpt_cmake="$SCRIPT_DIR/files/gptfdisk/CMakeLists.txt"
    if [[ ! -f "$gpt_cmake" ]]; then
        echo "ERROR: missing $gpt_cmake (gptfdisk CMake build file)." >&2
        exit 1
    fi
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$gpt_cmake:/build/gptfdisk-CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d gptfdisk ]; then
            git clone --depth 1 '${REPOS[gptfdisk]}' gptfdisk
        fi
        cd gptfdisk
        # GNU-free CMake build (gptfdisk ships only a GNU Makefile).
        cp /build/gptfdisk-CMakeLists.txt CMakeLists.txt
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DGPTFDISK_VERSION=${GPTFDISK_VERSION}
        ninja
        ninja install
        echo '==> gptfdisk installed to /usr/local/sbin/sgdisk'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: rsync (fast file-copying tool — GNU-free CMake build)
#   rsync ships a POSIX-sh ./configure (autoconf, not GNU make); it is run once
#   to emit config.h. Several headers are generated by awk scripts before the
#   CMake build compiles and links the rsync binary via Ninja.
# ═══════════════════════════════════════════════════════════════
build_rsync() {
    if [[ -x "$CHROOT_DIR/usr/local/bin/rsync" ]]; then
        echo "==> rsync already built, skipping"
        return
    fi
    # rsync links zlib for compression; make sure it is in the musl sysroot.
    build_zlib
    echo "==> Building rsync ${RSYNC_VERSION} into /usr/local (musl, CMake, GNU-free) ..."
    local rsync_cmake="$SCRIPT_DIR/files/rsync/CMakeLists.txt"
    if [[ ! -f "$rsync_cmake" ]]; then
        echo "ERROR: missing $rsync_cmake (rsync CMake build file)." >&2
        exit 1
    fi
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$rsync_cmake:/build/rsync-CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        # wget for the download; gawk for the header-generation scripts.
        apt-get install -y --no-install-recommends wget gawk || true
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d rsync-musl ]; then
            if [ ! -f rsync-${RSYNC_VERSION}.tar.gz ]; then
                wget -q 'https://download.samba.org/pub/rsync/src/rsync-${RSYNC_VERSION}.tar.gz' \
                    -O rsync-${RSYNC_VERSION}.tar.gz
            fi
            tar xf rsync-${RSYNC_VERSION}.tar.gz
            mv rsync-${RSYNC_VERSION} rsync-musl
        fi
        cd rsync-musl
        # Stage zlib headers into musl sysroot (same pattern as openssh).
        [ -f /usr/local/musl/include/zlib.h ] || \
            cp /usr/local/include/zlib.h /usr/local/include/zconf.h /usr/local/musl/include/ 2>/dev/null || true
        # One-time platform probe → config.h. Disable optional features that
        # pull in extra dependencies (xxhash, zstd, lz4, openssl).
        if [ ! -f config.h ]; then
            CC=/usr/local/musl/bin/musl-clang \
            CFLAGS='-O2' \
            ./configure \
                --prefix=/usr/local \
                --disable-xxhash \
                --disable-zstd \
                --disable-lz4 \
                --disable-openssl \
                --disable-md2man \
                --with-included-popt=yes \
                --with-included-zlib=no
        fi
        # Generate awk-derived headers that the GNU Makefile normally produces.
        AWK=\$(command -v gawk || command -v awk)
        [ -f daemon-parm.h ]          || \$AWK -f daemon-parm.awk daemon-parm.txt > daemon-parm.h
        [ -f help-rsync.h ]           || \$AWK -f help-from-md.awk -v hfile=help-rsync.h rsync.1.md
        [ -f help-rsyncd.h ]          || \$AWK -f help-from-md.awk -v hfile=help-rsyncd.h rsync.1.md
        [ -f default-cvsignore.h ]    || \$AWK -f define-from-md.awk -v hfile=default-cvsignore.h rsync.1.md
        [ -f default-dont-compress.h ]|| \$AWK -f define-from-md.awk -v hfile=default-dont-compress.h rsync.1.md
        # git-version.h (static for tarball releases; mkgitver creates it).
        if [ ! -f git-version.h ]; then
            if [ -x mkgitver ]; then
                ./mkgitver
            else
                echo '#define RSYNC_GITVER \"\"' > git-version.h
            fi
        fi
        # rounding.h: compile and run a test program to determine rounding.
        if [ ! -f rounding.h ]; then
            for r in 0 1 3; do
                if /usr/local/musl/bin/musl-clang -O2 -o rounding -DEXTRA_ROUNDING=\$r -I. rounding.c 2>/dev/null; then
                    echo \"#define EXTRA_ROUNDING \$r\" > rounding.h
                    rm -f rounding
                    break
                fi
            done
            [ -f rounding.h ] || echo '#define EXTRA_ROUNDING 0' > rounding.h
        fi
        # proto.h: extract function prototypes from the C sources.
        if [ ! -f proto.h ]; then
            \$AWK -f mkproto.awk *.c lib/compat.c daemon-parm.h > proto.h-tstamp
            if [ -s proto.h-tstamp ]; then cp proto.h-tstamp proto.h; fi
        fi
        # Drop in the GNU-free CMakeLists and build/link via Ninja.
        cp /build/rsync-CMakeLists.txt CMakeLists.txt
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE}
        ninja
        ninja install
        echo '==> rsync installed to /usr/local/bin/rsync'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: Lua (lightweight scripting language — musl/LLVM, statically linked)
#   Lua is tiny (~300 KB) and has zero dependencies beyond libc. Used by the
#   FenrirOS installer and other system scripts where toybox sh is unreliable.
# ═══════════════════════════════════════════════════════════════
build_lua() {
    if [[ -x "$CHROOT_DIR/usr/local/bin/lua" ]]; then
        echo "==> Lua already built, skipping"
        return
    fi
    echo "==> Building Lua ${LUA_VERSION} into /usr/local (musl, static, GNU-free) ..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        apt-get install -y --no-install-recommends wget || true
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d lua-${LUA_VERSION} ]; then
            if [ ! -f lua-${LUA_VERSION}.tar.gz ]; then
                wget -q 'https://www.lua.org/ftp/lua-${LUA_VERSION}.tar.gz' \
                    -O lua-${LUA_VERSION}.tar.gz
            fi
            tar xf lua-${LUA_VERSION}.tar.gz
        fi
        cd lua-${LUA_VERSION}
        # Build with musl-clang, target 'linux' (uses dlopen for loadlib).
        # Link statically so lua binary has no shared deps beyond musl libc.
        make -j\$(nproc) \
            CC=/usr/local/musl/bin/musl-clang \
            AR='llvm-ar rcu' \
            RANLIB=llvm-ranlib \
            MYCFLAGS='-O2' \
            MYLDFLAGS='' \
            MYLIBS='' \
            linux
        make install INSTALL_TOP=/usr/local
        echo '==> Lua installed to /usr/local/bin/lua'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: ncurses (terminal UI library, wide-character, static, musl/LLVM)
#   Built GNU-free: ./configure (POSIX-sh probe) -> cmake + ninja.
#   Produces libncursesw.a, libpanelw.a, libformw.a, libmenuw.a,
#   tic, infocmp, tput, clear, tset/reset.
# ═══════════════════════════════════════════════════════════════
build_ncurses() {
    if [[ -f "$CHROOT_DIR/usr/local/lib/libncursesw.a" ]]; then
        echo "==> ncurses already built, skipping"
        return
    fi
    echo "==> Building ncurses ${NCURSES_VERSION} into /usr/local (musl, CMake, GNU-free) ..."
    local ncurses_cmake="$SCRIPT_DIR/files/ncurses/CMakeLists.txt"
    if [[ ! -f "$ncurses_cmake" ]]; then
        echo "ERROR: missing $ncurses_cmake (ncurses CMake build file)." >&2
        exit 1
    fi
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$ncurses_cmake:/build/ncurses-CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        apt-get install -y --no-install-recommends wget gawk || true
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d ncurses-musl ]; then
            if [ ! -f ncurses-${NCURSES_VERSION}.tar.gz ]; then
                wget -q 'https://invisible-mirror.net/archives/ncurses/ncurses-${NCURSES_VERSION}.tar.gz' \
                    -O ncurses-${NCURSES_VERSION}.tar.gz
            fi
            tar xf ncurses-${NCURSES_VERSION}.tar.gz
            mv ncurses-${NCURSES_VERSION} ncurses-musl
        fi
        cd ncurses-musl
        # One-time platform probe -> include/ncurses_cfg.h + generated headers.
        if [ ! -f include/ncurses_cfg.h ]; then
            CC=/usr/local/musl/bin/musl-clang \
            CXX=/usr/local/musl/bin/musl-clang++ \
            CFLAGS='-O2' \
            ./configure \
                --prefix=/usr/local \
                --without-shared \
                --without-debug \
                --without-ada \
                --without-cxx-binding \
                --without-tests \
                --without-manpages \
                --enable-widec \
                --enable-ext-colors \
                --enable-ext-mouse \
                --enable-pc-files \
                --with-pkg-config-libdir=/usr/local/lib/pkgconfig \
                --with-terminfo-dirs=/usr/local/share/terminfo \
                --with-default-terminfo-dir=/usr/local/share/terminfo \
                --disable-db-install \
                --with-fallbacks=linux,vt100,xterm,xterm-256color,screen,screen-256color,dumb
        fi
        # CMakeLists.txt handles all header/source generation internally.
        cp /build/ncurses-CMakeLists.txt CMakeLists.txt
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE}
        ninja
        ninja install
        # Compile the built-in terminfo database.
        mkdir -p /usr/local/share/terminfo
        if [ -f ../misc/terminfo.src ]; then
            /usr/local/bin/tic -sx ../misc/terminfo.src 2>/dev/null || true
        fi
        echo '==> ncurses installed to /usr/local (libncursesw.a + tools)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: zstd (Zstandard compression — needed by RPM and libarchive)
# ═══════════════════════════════════════════════════════════════
build_zstd() {
    if [[ -f "$CHROOT_DIR/usr/local/lib/libzstd.a" ]]; then
        echo "==> zstd already built, skipping"
        return
    fi
    echo "==> Building zstd into /usr/local (static, musl, CMake) ..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d zstd ]; then
            git clone --depth 1 --branch v1.5.6 '${REPOS[zstd]}' zstd
        fi
        cd zstd/build/cmake
        rm -rf builddir && mkdir -p builddir && cd builddir
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DZSTD_BUILD_PROGRAMS=ON \
            -DZSTD_BUILD_STATIC=ON \
            -DZSTD_BUILD_SHARED=OFF \
            -DZSTD_BUILD_TESTS=OFF
        ninja
        ninja install
        echo '==> zstd installed to /usr/local (libzstd.a + zstd CLI)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: file / libmagic (file type detection — needed by RPM)
# ═══════════════════════════════════════════════════════════════
build_file() {
    if [[ -f "$CHROOT_DIR/usr/local/lib/libmagic.a" ]]; then
        echo "==> file/libmagic already built, skipping"
        return
    fi
    echo "==> Building file (libmagic) into /usr/local (static, musl, CMake) ..."
    local file_cmake="$SCRIPT_DIR/files/file/CMakeLists.txt"
    local file_configh="$SCRIPT_DIR/files/file/cmake_config.h.in"
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$file_cmake:/build/file-CMakeLists.txt" \
        --bind-ro="$file_configh:/build/file-config.h.in" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d file ]; then
            git clone --depth 1 --branch FILE5_46 '${REPOS[file]}' file
        fi
        cd file
        cp /build/file-CMakeLists.txt CMakeLists.txt
        cp /build/file-config.h.in cmake_config.h.in
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE}
        ninja
        ninja install
        echo '==> file/libmagic installed to /usr/local'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: libarchive (archive handling — needed by RPM for cpio/tar)
# ═══════════════════════════════════════════════════════════════
build_libarchive() {
    if [[ -f "$CHROOT_DIR/usr/local/lib/libarchive.a" ]]; then
        echo "==> libarchive already built, skipping"
        return
    fi
    build_zstd
    echo "==> Building libarchive into /usr/local (static, musl, CMake) ..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d libarchive ]; then
            git clone --depth 1 --branch v3.7.7 '${REPOS[libarchive]}' libarchive
        fi
        cd libarchive
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DBUILD_SHARED_LIBS=OFF \
            -DENABLE_TEST=OFF \
            -DENABLE_TAR=OFF \
            -DENABLE_CPIO=OFF \
            -DENABLE_CAT=OFF \
            -DENABLE_UNZIP=OFF \
            -DENABLE_ZSTD=ON \
            -DENABLE_LIBXML2=OFF \
            -DENABLE_EXPAT=OFF \
            -DENABLE_LZMA=OFF \
            -DENABLE_BZip2=OFF \
            -DZSTD_LIBRARY=/usr/local/lib/libzstd.a \
            -DZSTD_INCLUDE_DIR=/usr/local/include
        ninja
        ninja install
        echo '==> libarchive installed to /usr/local (libarchive.a)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: m4 macro processor (BSD C++ port from OpenBSD, GNU-free)
#   Needed by flex at runtime to process scanner skeletons.
#   Single C++ source file, no dependencies beyond libc + libm.
# ═══════════════════════════════════════════════════════════════
build_m4() {
    if [[ -x "$CHROOT_DIR/usr/local/bin/m4" ]]; then
        echo "==> m4 already built, skipping"
        return
    fi
    echo "==> Building m4 (BSD C++ port) into /usr/local ..."
    local m4_src="$SCRIPT_DIR/files/m4"
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$m4_src/m4.cpp:/build/m4-src/m4.cpp" \
        --bind-ro="$m4_src/CMakeLists.txt:/build/m4-src/CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/m4-build && cd /build/m4-build
        cmake /build/m4-src -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE}
        ninja
        ninja install
        echo '==> m4 installed to /usr/local/bin/m4'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: flex 2.6.4 (fast lexical analyser generator, BSD-licensed)
#   Builds flex + libfl.a using pre-generated scan.c/parse.c from
#   the release tarball. Requires m4 at runtime. configure is run
#   once as a POSIX-sh probe to emit config.h, then cmake+ninja.
# ═══════════════════════════════════════════════════════════════
FLEX_VERSION="2.6.4"

build_flex() {
    if [[ -x "$CHROOT_DIR/usr/local/bin/flex" ]]; then
        echo "==> flex already built, skipping"
        return
    fi
    build_m4  # flex needs m4 at runtime
    echo "==> Building flex ${FLEX_VERSION} ..."
    local flex_cmake="$SCRIPT_DIR/files/flex/CMakeLists.txt"
    $NSPAWN -D "$CHROOT_DIR" \
        --bind-ro="$flex_cmake:/build/flex-cmake/CMakeLists.txt" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake

        # Download + extract if needed
        if [[ ! -d /build/flex-${FLEX_VERSION} ]]; then
            cd /build
            wget -q https://github.com/westes/flex/releases/download/v${FLEX_VERSION}/flex-${FLEX_VERSION}.tar.gz
            tar xzf flex-${FLEX_VERSION}.tar.gz
            rm -f flex-${FLEX_VERSION}.tar.gz
        fi

        cd /build/flex-${FLEX_VERSION}

        # Run configure once to generate src/config.h
        if [[ ! -f src/config.h ]]; then
            CC=/usr/local/musl/bin/musl-clang \
            ./configure --prefix=/usr/local --disable-nls --disable-shared 2>&1 | tail -3
        fi

        cp /build/flex-cmake/CMakeLists.txt .
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE}
        ninja
        ninja install
        # Create lex -> flex symlink
        ln -sf flex /usr/local/bin/lex
        echo '==> flex installed to /usr/local/bin/flex'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: RPM 6.0.1 (package manager — GNU-free build with musl/LLVM)
#   Built with WITH_SEQUOIA=OFF (uses OpenSSL/LibreSSL for crypto instead of
#   Rust-based Sequoia), no Python bindings, no scdoc (skip man pages).
#   Dependencies: popt, sqlite3, lua, zlib, zstd, libarchive, libmagic, OpenSSL
# ═══════════════════════════════════════════════════════════════
RPM_TAG="rpm-6.0.1-release"

build_rpm() {
    if [[ -x "$CHROOT_DIR/usr/local/bin/rpm" ]]; then
        echo "==> RPM already built, skipping"
        return
    fi
    # Ensure all dependencies are built
    build_popt
    build_zstd
    build_file
    build_libarchive
    build_lua
    echo "==> Building RPM 6.0.1 into /usr/local (musl, LLVM, GNU-free) ..."
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        export PKG_CONFIG_PATH=/usr/local/lib/pkgconfig:/usr/local/musl/lib/pkgconfig
        TOOLCHAIN=/usr/local/musl/share/musl-llvm-toolchain.cmake
        mkdir -p /build/libs && cd /build/libs
        if [ ! -d rpm ]; then
            git clone --depth 1 --branch ${RPM_TAG} '${REPOS[rpm]}' rpm
        fi
        cd rpm
        # Apply musl/clang patches (idempotent: guard prevents re-application)
        if ! grep -q '#ifdef GLOB_BRACE' rpmio/rpmglob.cc; then
            sed -i 's/^    gflags |= GLOB_BRACE;/#ifdef GLOB_BRACE\n    gflags |= GLOB_BRACE;\n#endif/' rpmio/rpmglob.cc
            sed -i 's/gflags |= GLOB_TILDE;/#ifdef GLOB_TILDE\n        gflags |= GLOB_TILDE;\n#endif/' rpmio/rpmglob.cc
        fi
        if grep -q '^static rpmRC rpm::delete_key_compat' lib/keystore.cc; then
            sed -i 's/^static rpmRC rpm::delete_key_compat/rpmRC rpm::delete_key_compat/' lib/keystore.cc
        fi
        rm -rf build-cmake && mkdir -p build-cmake && cd build-cmake
        cmake .. -G Ninja \
            -DCMAKE_TOOLCHAIN_FILE=\$TOOLCHAIN \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=${BUILD_TYPE} \
            -DWITH_SEQUOIA=OFF \
            -DWITH_OPENSSL=ON \
            -DWITH_BZIP2=OFF \
            -DWITH_ICONV=OFF \
            -DWITH_LIBDW=OFF \
            -DWITH_LIBELF=OFF \
            -DWITH_LIBLZMA=OFF \
            -DWITH_READLINE=OFF \
            -DWITH_CAP=OFF \
            -DWITH_ACL=OFF \
            -DWITH_SELINUX=OFF \
            -DWITH_DBUS=OFF \
            -DWITH_AUDIT=OFF \
            -DWITH_FSVERITY=OFF \
            -DWITH_IMAEVM=OFF \
            -DWITH_FAPOLICYD=OFF \
            -DWITH_ZSTD=ON \
            -DENABLE_PYTHON=OFF \
            -DENABLE_TESTSUITE=OFF \
            -DENABLE_NLS=OFF \
            -DENABLE_OPENMP=OFF \
            -DENABLE_PLUGINS=OFF \
            -DENABLE_CUTF8=OFF \
            -DENABLE_NDB=OFF \
            -DENABLE_BDB_RO=OFF \
            -DWITH_DOXYGEN=OFF \
            -DCMAKE_EXE_LINKER_FLAGS="-L/usr/local/lib -L/usr/local/musl/lib -lz -lzstd -lcrypto -lssl -lm -ldl -lpthread"
        ninja
        ninja install
        echo '==> RPM 6.0.1 installed to /usr/local (rpm, rpmbuild, rpmkeys, ...)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: Build FenrirOS installer (C++, uses musl crypt() for SHA-512)
#   Replaces the Lua installer. Statically links against musl so the binary
#   has no runtime dependencies beyond the kernel. Uses crypt_r() for password
#   hashing (SHA-512 via $6$ salt).
# ═══════════════════════════════════════════════════════════════
build_installer() {
    if [[ -x "$CHROOT_DIR/usr/local/sbin/fenriros-install" ]]; then
        echo "==> Installer already built, skipping"
        return
    fi
    echo "==> Building FenrirOS installer (C++, musl/LLVM) ..."
    # Stage source into chroot
    mkdir -p "$CHROOT_DIR/build/libs/installer"
    cp -a "$SCRIPT_DIR/files/installer/install.cpp" "$CHROOT_DIR/build/libs/installer/"
    cp -a "$SCRIPT_DIR/files/installer/CMakeLists.txt" "$CHROOT_DIR/build/libs/installer/"

    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:/usr/bin:\$PATH
        cd /build/libs/installer
        rm -rf build && mkdir build && cd build
        cmake .. \
            -DCMAKE_C_COMPILER=/usr/local/musl/bin/musl-clang \
            -DCMAKE_CXX_COMPILER=/usr/local/musl/bin/musl-clang++ \
            -DCMAKE_INSTALL_PREFIX=/usr/local \
            -DCMAKE_BUILD_TYPE=Release \
            -G Ninja
        ninja -j\$(nproc)
        ninja install
        echo '  [done] installer built (/usr/local/sbin/fenriros-install)'
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: RPM Packaging — repackage all built components as RPMs
#   Runs rpmbuild inside the chroot against spec files that copy
#   already-installed binaries/libs from /usr/local into /usr.
#   Creates a local RPM repository with createrepo-like index.
# ═══════════════════════════════════════════════════════════════
package_rpms() {
    echo "==> Packaging all FenrirOS components as RPMs ..."

    # Stage spec files + macros into chroot
    sudo mkdir -p "$CHROOT_DIR/build/rpmbuild/SPECS"
    sudo mkdir -p "$CHROOT_DIR/build/rpmbuild/"{BUILD,BUILDROOT,RPMS,SRPMS,SOURCES}
    sudo cp "$SCRIPT_DIR/files/rpm/specs/"*.spec "$CHROOT_DIR/build/rpmbuild/SPECS/"
    sudo mkdir -p "$CHROOT_DIR/usr/local/lib/rpm/macros.d"
    sudo cp "$SCRIPT_DIR/files/rpm/macros.fenriros" "$CHROOT_DIR/usr/local/lib/rpm/macros.d/"

    # List of all specs to build (order does not matter — no build step, just packaging)
    local specs=(
        musl clang lld llvm-tools lldb libcxx
        toybox makeplus cmake ninja openssh e2fsprogs dosfstools gptfdisk
        rsync lua ncurses zstd file libressl zlib libelf syslite
        fenriros-installer postgresql fenriros-libs m4 flex rpm
    )

    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/bin:/usr/local/sbin:\$PATH
        export HOME=/root

        for spec in ${specs[*]}; do
            echo \"  [rpm] Building \$spec ...\"
            rpmbuild -bb --noclean /build/rpmbuild/SPECS/\${spec}.spec 2>&1 \
                | tail -3
            echo \"  [rpm] \$spec done\"
        done

        echo ''
        echo '==> Built RPMs:'
        find /build/rpmbuild/RPMS -name '*.rpm' -exec basename {} \;
    "
}

# ═══════════════════════════════════════════════════════════════
# Phase: Create RPM repository from built packages
#   Generates a simple rpm repo structure that can be added to the
#   ISO image. Uses rpm --rebuilddb + a plain directory listing
#   (no createrepo needed — rpm can scan a directory directly).
# ═══════════════════════════════════════════════════════════════
create_rpm_repo() {
    local repo_dir="$CHROOT_DIR/build/rpm-repo"
    echo "==> Creating RPM repository at $repo_dir ..."

    sudo mkdir -p "$repo_dir"
    # Collect all built RPMs into a flat repo directory
    sudo find "$CHROOT_DIR/build/rpmbuild/RPMS" -name '*.rpm' \
        -exec cp {} "$repo_dir/" \;

    # Generate a simple package list (manifest)
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/bin:\$PATH
        cd /build/rpm-repo
        echo '==> RPM repository contents:'
        for f in *.rpm; do
            rpm -qip \"\$f\" 2>/dev/null | grep -E '^(Name|Version|Release|Architecture|Summary)' || true
            echo '---'
        done
        echo \"Total: \$(ls -1 *.rpm | wc -l) packages\"
    "
    echo "==> RPM repository ready: $repo_dir"
}

# ═══════════════════════════════════════════════════════════════
# Phase: Linux kernel (LLVM toolchain, no GNU)
#   Built with LLVM=1 (clang + lld + llvm-objcopy/...) so the whole pipeline
#   stays GNU-free. Supply a prebuilt image via KERNEL_IMAGE=/path/bzImage to
#   skip the build entirely.
# ═══════════════════════════════════════════════════════════════
build_kernel() {
    if [[ -n "${KERNEL_IMAGE:-}" && -f "${KERNEL_IMAGE}" ]]; then
        echo "==> Using prebuilt kernel: $KERNEL_IMAGE"
        return
    fi
    if [[ -s "$CHROOT_DIR/build/linux/arch/x86/boot/bzImage" ]]; then
        echo "==> Kernel already built, skipping"
        return
    fi
    # Remove any stale 0-byte bzImage left behind by a failed build so the
    # build actually re-runs instead of silently skipping.
    rm -f "$CHROOT_DIR/build/linux/arch/x86/boot/bzImage"
    echo "==> Building Linux ${LINUX_BRANCH} with the LLVM toolchain (no GNU) ..."
    echo "    Note: the Kbuild system relies on advanced GNU-make features; if"
    echo "          makeplus cannot drive it, supply KERNEL_IMAGE=/path/bzImage."
    # Kernel build prerequisites (host tools used by Kbuild). libelf comes from
    # our own CMake build; the rest are small host helpers, not part of the
    # shipped system. Make sure libelf is present first.
    build_libelf
    # certs/extract-cert links libcrypto and uses legacy OpenSSL-1.0 APIs that
    # OpenSSL 3.x removed. Provide LibreSSL inside the musl sysroot so both its
    # headers and static libcrypto are picked up consistently (instead of the
    # host's glibc OpenSSL).
    build_libressl
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        apt-get install -y --no-install-recommends \
            flex bison bc perl kmod cpio || true
    "
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        cd /build
        if [ ! -d linux ]; then
            git clone --depth 1 --branch ${LINUX_BRANCH} '${REPOS[linux]}' linux
        fi
        cd linux
        # Fully LLVM-driven build: clang, lld, and llvm-* as binutils. Point the
        # build at our musl-native libelf (lib in /usr/local/musl/lib, kept off
        # the glibc ld.so path; its headers — libelf.h/gelf.h — live in the
        # shared dev include dir /usr/local/include alongside the other libs).
        export HOST_EXTRACFLAGS='-I/usr/local/include -I/usr/local/musl/include'
        export HOST_LIBELF_LIBS='-L/usr/local/musl/lib -lelf'
        # Make Kbuild's host pkg-config resolve libcrypto to the musl LibreSSL
        # (headers + static lib in /usr/local/musl), not the host glibc OpenSSL.
        export HOSTPKG_CONFIG=/usr/local/bin/pkg-config
        export PKG_CONFIG_PATH=/usr/local/musl/lib/pkgconfig:/usr/local/lib/pkgconfig
        # Host helper tools (fixdep, sorttable, objtool, extract-cert, ...) must
        # compile AND link against musl consistently. Plain clang pulls glibc
        # headers from /usr/include (redirecting stat->stat64, strtol->
        # __isoc23_strtol, ...) but then links musl libc, which lacks those
        # symbols. The musl-clang/musl-clang++ wrappers set --sysroot=/usr/local/
        # musl and -L/usr/local/musl/lib, so host tools stay GNU-free and also
        # find our musl libelf (-lelf) for objtool.
        # clang-22 promotes several new diagnostics to errors that the kernel's
        # CONFIG_WERROR then treats as fatal: const objects left default-init by
        # the typecheck() macro (default-const-init-{var,field}-unsafe) and
        # fixed-size char arrays initialized from a same-length string literal
        # with no room for a NUL (unterminated-string-initialization). These are
        # benign for the kernel's intended semantics; downgrade them to warnings.
        export KCFLAGS='-Wno-error=default-const-init-var-unsafe -Wno-error=default-const-init-field-unsafe -Wno-error=unterminated-string-initialization'
        KARGS='LLVM=1 LLVM_IAS=1 CC=/usr/local/musl/bin/clang HOSTCC=/usr/local/musl/bin/musl-clang HOSTCXX=/usr/local/musl/bin/musl-clang++ LD=/usr/local/musl/bin/ld.lld'
        # --no-print-directory: the kernel top Makefile re-invokes itself once
        # (need-sub-make) unless this flag is already in MAKEFLAGS. makeplus does
        # not round-trip srctree/abs_srctree through that particular self-recursion
        # (the inner make sees an empty \$(srctree) -> \"/scripts/basic/Makefile:
        # No such file\"). Passing the flag up front makes the kernel skip that
        # wrapper recursion and build in-process; the internal \$(MAKE) descends
        # (scripts/kconfig, etc.) work fine.
        MAKE='/usr/local/musl/bin/makeplus --no-print-directory'
        \$MAKE \$KARGS defconfig
        # x86_64 defconfig ships with CONFIG_FB unset, so a Limine/GOP handoff
        # leaves the kernel running with a live framebuffer it cannot draw to
        # (symptom: keyboard responds, screen stays black). Turn on the generic
        # simple-framebuffer/simpledrm path plus fbcon so the boot console shows
        # up on bare metal. Also enable SQUASHFS (+zstd): the system ships as a
        # zstd squashfs the initramfs loop-mounts; without it the rootfs never
        # mounts (\"no system image found\") and boot drops to the bare initramfs
        # shell (which has no clang/userland). olddefconfig resolves the deps.
        # Realtek NICs built-in (=y) so DHCP-at-boot works without module
        # autoload: r8169 covers RTL8125/8126 and (>=6.16) the RTL8127A 10GbE
        # part; REALTEK_PHY is its required MODULE_SOFTDEP(\"pre: realtek\") PHY
        # driver. Firmware (rtl_nic/rtl8127a-1.fw) is staged into /lib/firmware.
        ./scripts/config --file .config -e SYSFB -e SYSFB_SIMPLEFB -e DRM -e DRM_SIMPLEDRM -e DRM_FBDEV_EMULATION -e FB -e FB_CORE -e FB_SIMPLE -e FRAMEBUFFER_CONSOLE -e FRAMEBUFFER_CONSOLE_DETECT_PRIMARY -e SQUASHFS -e SQUASHFS_ZSTD -e SQUASHFS_XATTR -e SQUASHFS_FILE_DIRECT -e NETDEVICES -e ETHERNET -e NET_VENDOR_REALTEK -e R8169 -e PHYLIB -e REALTEK_PHY -e BLK_DEV_NVME -e NVME_CORE
        \$MAKE \$KARGS olddefconfig
        \$MAKE \$KARGS -j\$(nproc) bzImage
        # Build loadable modules (if any) and stage them for the system rootfs.
        # A monolithic defconfig may produce none; that is fine (dir stays empty).
        \$MAKE \$KARGS -j\$(nproc) modules || true
        rm -rf /build/linux-modules && mkdir -p /build/linux-modules
        \$MAKE \$KARGS INSTALL_MOD_PATH=/build/linux-modules \\
            INSTALL_MOD_STRIP=1 DEPMOD=/usr/local/bin/depmod modules_install || true
        echo '==> Kernel built: arch/x86/boot/bzImage (+ staged modules)'
    "
    # Fetch Realtek NIC firmware (rtl_nic/*.fw) from linux-firmware. The r8169
    # driver calls request_firmware() at runtime for the PHY blobs (e.g.
    # rtl8127a-1.fw for the RTL8127A 10GbE part); these are staged into the
    # system image's /lib/firmware by assemble_system_rootfs. A sparse, shallow
    # checkout keeps it to just the rtl_nic dir instead of the multi-GB tree.
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        export PATH=/usr/local/musl/bin:/usr/local/bin:\$PATH
        cd /build
        if [ ! -f linux-firmware/rtl_nic/rtl8127a-1.fw ]; then
            rm -rf linux-firmware
            git clone --depth 1 --filter=blob:none --sparse \\
                '${LINUX_FIRMWARE_REPO}' linux-firmware
            cd linux-firmware
            git sparse-checkout set rtl_nic
        fi
        echo '==> Realtek NIC firmware staged:'
        ls -1 /build/linux-firmware/rtl_nic/ | grep -iE 'rtl81(25|26|27)' || true
    "
}

# ═══════════════════════════════════════════════════════════════
# Helper: assemble a GNU-free system rootfs (musl + toybox + LLVM + makeplus)
# ═══════════════════════════════════════════════════════════════
assemble_system_rootfs() {
    local rootfs
    rootfs=$(create_minimal_rootfs "system")

    echo "  [system] Populating GNU-free system rootfs ..." >&2

    mkdir -p "$rootfs/usr/local/musl/lib" "$rootfs/usr/local/lib" "$rootfs/usr/local/bin" >&2
    # musl/LLVM runtime shared libraries
    find "$CHROOT_DIR/usr/local/musl/lib" -maxdepth 1 -name '*.so*' \
        -exec cp -a {} "$rootfs/usr/local/musl/lib/" \; 2>/dev/null || true

    # LLVM "binutils": ship llvm-* and symlink the conventional names so the
    # system never needs GNU binutils.
    local t
    for t in llvm-objcopy llvm-strip llvm-ar llvm-nm llvm-ranlib llvm-objdump \
             llvm-readelf llvm-addr2line llvm-size llvm-strings; do
        [[ -f "$CHROOT_DIR/usr/local/musl/bin/$t" ]] && \
            cp -a "$CHROOT_DIR/usr/local/musl/bin/$t" "$rootfs/usr/local/bin/" 2>/dev/null || true
    done
    declare -A bmap=( [objcopy]=llvm-objcopy [strip]=llvm-strip [ar]=llvm-ar
                      [nm]=llvm-nm [ranlib]=llvm-ranlib [objdump]=llvm-objdump
                      [readelf]=llvm-readelf [addr2line]=llvm-addr2line
                      [size]=llvm-size [strings]=llvm-strings )
    local name
    for name in "${!bmap[@]}"; do
        [[ -f "$rootfs/usr/local/bin/${bmap[$name]}" ]] && \
            ln -sf "${bmap[$name]}" "$rootfs/usr/local/bin/$name"
    done

    # makeplus as the only 'make'
    if [[ -x "$CHROOT_DIR/usr/local/musl/bin/makeplus" ]]; then
        cp -a "$CHROOT_DIR/usr/local/musl/bin/makeplus" "$rootfs/usr/local/bin/makeplus"
        ln -sf makeplus "$rootfs/usr/local/bin/make"
    fi
    # musl-native compiler + cmake (on-system dev tooling, all GNU-free)
    # Full self-hosting toolchain: clang/lld/cmake + sysroot so FenrirOS can build itself.
    if [[ -f "$CHROOT_DIR/usr/local/musl/bin/clang-22" ]]; then
        cp -a "$CHROOT_DIR/usr/local/musl/bin/clang-22" "$rootfs/usr/local/bin/"
        ln -sf clang-22 "$rootfs/usr/local/bin/clang"
        ln -sf clang-22 "$rootfs/usr/local/bin/clang++"
        mkdir -p "$rootfs/usr/local/musl/bin"
        ln -sf /usr/local/bin/clang-22 "$rootfs/usr/local/musl/bin/clang-22"
        ln -sf /usr/local/bin/clang-22 "$rootfs/usr/local/musl/bin/clang"
        ln -sf /usr/local/bin/clang-22 "$rootfs/usr/local/musl/bin/clang++"
    fi
    for t in lld musl-clang musl-clang++; do
        if [[ -f "$CHROOT_DIR/usr/local/musl/bin/$t" ]]; then
            cp -a "$CHROOT_DIR/usr/local/musl/bin/$t" "$rootfs/usr/local/bin/" 2>/dev/null || true
            mkdir -p "$rootfs/usr/local/musl/bin"
            ln -sf /usr/local/bin/$t "$rootfs/usr/local/musl/bin/$t" 2>/dev/null || true
        fi
    done
    if [[ -f "$rootfs/usr/local/bin/lld" ]]; then
        ln -sf lld "$rootfs/usr/local/bin/ld.lld"
        ln -sf /usr/local/bin/lld "$rootfs/usr/local/musl/bin/ld.lld" 2>/dev/null || true
    fi
    # Clang resource directory (builtin headers, compiler-rt)
    if [[ -d "$CHROOT_DIR/usr/local/musl/lib/clang/22" ]]; then
        mkdir -p "$rootfs/usr/local/musl/lib/clang"
        cp -a "$CHROOT_DIR/usr/local/musl/lib/clang/22" "$rootfs/usr/local/musl/lib/clang/"
    fi
    # musl sysroot: headers + CRT objects + libc (static+dynamic)
    if [[ -d "$CHROOT_DIR/usr/local/musl/include" ]]; then
        cp -a "$CHROOT_DIR/usr/local/musl/include" "$rootfs/usr/local/musl/"
    fi
    for f in crt1.o crti.o crtn.o Scrt1.o rcrt1.o libc.a libc.so; do
        [[ -f "$CHROOT_DIR/usr/local/musl/lib/$f" ]] && \
            cp -a "$CHROOT_DIR/usr/local/musl/lib/$f" "$rootfs/usr/local/musl/lib/" 2>/dev/null || true
    done
    # libc++ / libc++abi / libunwind (static archives for C++ builds)
    for f in libc++.a libc++abi.a libunwind.a; do
        [[ -f "$CHROOT_DIR/usr/local/musl/lib/$f" ]] && \
            cp -a "$CHROOT_DIR/usr/local/musl/lib/$f" "$rootfs/usr/local/musl/lib/" 2>/dev/null || true
    done
    # compiler-rt builtins (clang needs this for linking)
    if [[ -d "$CHROOT_DIR/usr/local/musl/lib/linux" ]]; then
        mkdir -p "$rootfs/usr/local/musl/lib/linux"
        cp -a "$CHROOT_DIR/usr/local/musl/lib/linux/"* "$rootfs/usr/local/musl/lib/linux/" 2>/dev/null || true
    fi
    # cmake binary + Modules/Templates data
    if [[ -x "$CHROOT_DIR/usr/local/bin/cmake" ]]; then
        cp -a "$CHROOT_DIR/usr/local/bin/cmake" "$rootfs/usr/local/bin/"
        if [[ -d "$CHROOT_DIR/usr/local/share/cmake-3.31" ]]; then
            mkdir -p "$rootfs/usr/local/share"
            cp -a "$CHROOT_DIR/usr/local/share/cmake-3.31" "$rootfs/usr/local/share/"
        fi
    fi
    # ninja build system
    [[ -x "$CHROOT_DIR/usr/local/bin/ninja" ]] && \
        cp -a "$CHROOT_DIR/usr/local/bin/ninja" "$rootfs/usr/local/bin/" 2>/dev/null || true
    # musl toolchain file (for cmake -DCMAKE_TOOLCHAIN_FILE=...)
    if [[ -f "$CHROOT_DIR/usr/local/musl/share/musl-llvm-toolchain.cmake" ]]; then
        mkdir -p "$rootfs/usr/local/musl/share"
        cp -a "$CHROOT_DIR/usr/local/musl/share/musl-llvm-toolchain.cmake" "$rootfs/usr/local/musl/share/"
    fi
    # pkg-config
    [[ -x "$CHROOT_DIR/usr/local/bin/pkg-config" ]] && \
        cp -a "$CHROOT_DIR/usr/local/bin/pkg-config" "$rootfs/usr/local/bin/" 2>/dev/null || true
    # Static libraries + headers from /usr/local (for building against installed libs)
    if [[ -d "$CHROOT_DIR/usr/local/include" ]]; then
        cp -a "$CHROOT_DIR/usr/local/include" "$rootfs/usr/local/"
    fi
    find "$CHROOT_DIR/usr/local/lib" -maxdepth 1 -name '*.a' \
        -exec cp -a {} "$rootfs/usr/local/lib/" \; 2>/dev/null || true
    # pkgconfig files
    if [[ -d "$CHROOT_DIR/usr/local/lib/pkgconfig" ]]; then
        mkdir -p "$rootfs/usr/local/lib/pkgconfig"
        cp -a "$CHROOT_DIR/usr/local/lib/pkgconfig/"* "$rootfs/usr/local/lib/pkgconfig/" 2>/dev/null || true
    fi
    if [[ -d "$CHROOT_DIR/usr/local/musl/lib/pkgconfig" ]]; then
        mkdir -p "$rootfs/usr/local/musl/lib/pkgconfig"
        cp -a "$CHROOT_DIR/usr/local/musl/lib/pkgconfig/"* "$rootfs/usr/local/musl/lib/pkgconfig/" 2>/dev/null || true
    fi
    # cmake package configs (Find*.cmake, *Config.cmake)
    if [[ -d "$CHROOT_DIR/usr/local/lib/cmake" ]]; then
        mkdir -p "$rootfs/usr/local/lib/cmake"
        cp -a "$CHROOT_DIR/usr/local/lib/cmake/"* "$rootfs/usr/local/lib/cmake/" 2>/dev/null || true
    fi

    # OpenSSH server + clients. Built straight into the shared /usr/local prefix,
    # so copy sshd/clients/helpers from /usr/local/{sbin,bin,libexec} onto the
    # standard PATH dirs (the compiled-in helper paths already point there).
    if [[ -x "$CHROOT_DIR/usr/local/sbin/sshd" ]]; then
        echo "  [system] Installing OpenSSH server ..." >&2
        mkdir -p "$rootfs/usr/local/sbin" "$rootfs/usr/local/bin" "$rootfs/usr/local/libexec" \
                 "$rootfs/etc/ssh" "$rootfs/var/empty" "$rootfs/run/sshd" "$rootfs/root/.ssh"
        cp -a "$CHROOT_DIR/usr/local/sbin/sshd" "$rootfs/usr/local/sbin/sshd"
        local sb
        for sb in ssh scp sftp ssh-keygen ssh-keyscan ssh-add ssh-agent; do
            [[ -e "$CHROOT_DIR/usr/local/bin/$sb" ]] && \
                cp -a "$CHROOT_DIR/usr/local/bin/$sb" "$rootfs/usr/local/bin/$sb"
        done
        for sb in sshd-session sftp-server ssh-keysign ssh-pkcs11-helper ssh-sk-helper ssh-askpass; do
            [[ -e "$CHROOT_DIR/usr/local/libexec/$sb" ]] && \
                cp -a "$CHROOT_DIR/usr/local/libexec/$sb" "$rootfs/usr/local/libexec/$sb"
        done
        chmod 711 "$rootfs/var/empty"
        chmod 700 "$rootfs/root/.ssh"

        # Privilege-separation account required by sshd.
        if ! grep -q '^sshd:' "$rootfs/etc/passwd" 2>/dev/null; then
            echo 'sshd:x:74:74:sshd privsep:/var/empty:/bin/false' >> "$rootfs/etc/passwd"
            echo 'sshd:x:74:' >> "$rootfs/etc/group"
        fi

        # Minimal, no-empty-password sshd_config (host keys generated at boot).
        # The root fs is a read-only squashfs on the live image, so host keys are
        # generated into the writable /run/sshd tmpfs; point HostKey there.
        cat > "$rootfs/etc/ssh/sshd_config" << 'SSHD'
Port 22
AddressFamily any
HostKey /run/sshd/ssh_host_ed25519_key
PermitRootLogin yes
PubkeyAuthentication yes
PasswordAuthentication yes
PermitEmptyPasswords no
PrintMotd no
SetEnv PATH=/usr/local/sbin:/usr/local/musl/bin:/usr/local/bin:/bin:/sbin
Subsystem sftp /usr/local/libexec/sftp-server
SSHD

        # Root credential. A password (hashed with the musl LibreSSL openssl) is
        # set so login works out of the box; an optional authorized key is added
        # when SSH_AUTHORIZED_KEY is provided. Override ROOT_PASSWORD at build.
        local roothash=""
        if [[ -n "${ROOT_PASSWORD:-}" ]] && [[ -x "$CHROOT_DIR/usr/local/musl/bin/openssl" ]]; then
            # LibreSSL's `openssl passwd` only offers -1/-apr1/-crypt; musl's
            # crypt() understands the resulting $1$ (MD5) hash, so use -1.
            roothash=$($NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c \
                "/usr/local/musl/bin/openssl passwd -1 '$ROOT_PASSWORD'" 2>/dev/null \
                | tr -d '\r\n ')
        fi
        if [[ -n "$roothash" ]]; then
            {
                echo "root:${roothash}:19000:0:99999:7:::"
                echo "sshd:!:19000:0:99999:7:::"
            } > "$rootfs/etc/shadow"
            chmod 600 "$rootfs/etc/shadow"
            echo "  [security] root SSH password set to default ROOT_PASSWORD" \
                 "(value '$ROOT_PASSWORD') — override with ROOT_PASSWORD=... or" \
                 "SSH_AUTHORIZED_KEY=..." >&2
        else
            echo "  [security] no root password set (openssl unavailable) —" \
                 "SSH login requires a key via SSH_AUTHORIZED_KEY=..." >&2
        fi
        if [[ -n "${SSH_AUTHORIZED_KEY:-}" ]]; then
            echo "$SSH_AUTHORIZED_KEY" > "$rootfs/root/.ssh/authorized_keys"
            chmod 600 "$rootfs/root/.ssh/authorized_keys"
        fi
    fi

    # Root password for TTY login (getty/login). The shadow file is also used by
    # sshd above, but TTY login needs it regardless of whether SSH is installed.
    # If the shadow file was not yet created (no sshd), generate it here.
    if [[ ! -f "$rootfs/etc/shadow" ]]; then
        local roothash=""
        if [[ -n "${ROOT_PASSWORD:-}" ]] && [[ -x "$CHROOT_DIR/usr/local/musl/bin/openssl" ]]; then
            roothash=$($NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c \
                "/usr/local/musl/bin/openssl passwd -1 '$ROOT_PASSWORD'" 2>/dev/null \
                | tr -d '\r\n ')
        fi
        if [[ -n "$roothash" ]]; then
            echo "root:${roothash}:19000:0:99999:7:::" > "$rootfs/etc/shadow"
            chmod 600 "$rootfs/etc/shadow"
            echo "  [security] root password set (for TTY + SSH login)" >&2
        else
            # No password possible — allow passwordless root login on TTY only
            echo "root::19000:0:99999:7:::" > "$rootfs/etc/shadow"
            chmod 600 "$rootfs/etc/shadow"
            echo "  [security] no root password set — TTY login with empty password" >&2
        fi
    fi
    # Ensure /root home directory exists (for TTY shell)
    mkdir -p "$rootfs/root"

    # Filesystem tools: ext2/3/4 (e2fsprogs) and FAT (dosfstools). Both were built
    # straight into the shared /usr/local prefix, so copy the binaries (and the
    # install-time symlinks: mkfs.ext*/fsck.ext*, mkfs.vfat/mkdosfs, ...) from
    # /usr/local/{sbin,bin} so mkfs/fsck work out of the box on the standard PATH.
    if [[ -x "$CHROOT_DIR/usr/local/sbin/mke2fs" ]]; then
        echo "  [system] Installing e2fsprogs (ext2/3/4 tools) ..." >&2
        mkdir -p "$rootfs/usr/local/sbin" "$rootfs/usr/local/bin"
        local e2b
        for e2b in mke2fs e2fsck tune2fs dumpe2fs badblocks resize2fs e2image e2undo blkid \
                   mklost+found logsave mkfs.ext2 mkfs.ext3 mkfs.ext4 \
                   fsck.ext2 fsck.ext3 fsck.ext4 e2label; do
            [[ -e "$CHROOT_DIR/usr/local/sbin/$e2b" ]] && \
                cp -a "$CHROOT_DIR/usr/local/sbin/$e2b" "$rootfs/usr/local/sbin/$e2b"
        done
        for e2b in filefrag chattr lsattr; do
            [[ -e "$CHROOT_DIR/usr/local/bin/$e2b" ]] && \
                cp -a "$CHROOT_DIR/usr/local/bin/$e2b" "$rootfs/usr/local/bin/$e2b"
        done
    fi
    if [[ -x "$CHROOT_DIR/usr/local/sbin/mkfs.fat" ]]; then
        echo "  [system] Installing dosfstools (FAT tools) ..." >&2
        mkdir -p "$rootfs/usr/local/sbin"
        local fft
        for fft in mkfs.fat fsck.fat fatlabel mkfs.msdos mkfs.vfat mkdosfs \
                   fsck.msdos fsck.vfat dosfsck dosfslabel; do
            [[ -e "$CHROOT_DIR/usr/local/sbin/$fft" ]] && \
                cp -a "$CHROOT_DIR/usr/local/sbin/$fft" "$rootfs/usr/local/sbin/$fft"
        done
    fi

    # GPT partitioner: sgdisk (gptfdisk) drives install.sh. Statically linked
    # (libuuid + libpopt baked in) so just the one binary is needed.
    if [[ -x "$CHROOT_DIR/usr/local/sbin/sgdisk" ]]; then
        echo "  [system] Installing gptfdisk (sgdisk GPT partitioner) ..." >&2
        mkdir -p "$rootfs/usr/local/sbin"
        cp -a "$CHROOT_DIR/usr/local/sbin/sgdisk" "$rootfs/usr/local/sbin/sgdisk"
    fi

    # rsync: fast, versatile file-copying tool for remote and local files.
    if [[ -x "$CHROOT_DIR/usr/local/bin/rsync" ]]; then
        echo "  [system] Installing rsync ..." >&2
        mkdir -p "$rootfs/usr/local/bin"
        cp -a "$CHROOT_DIR/usr/local/bin/rsync" "$rootfs/usr/local/bin/rsync"
    fi

    # Lua: lightweight scripting language for system scripts.
    if [[ -x "$CHROOT_DIR/usr/local/bin/lua" ]]; then
        echo "  [system] Installing Lua interpreter ..." >&2
        mkdir -p "$rootfs/usr/local/bin"
        cp -a "$CHROOT_DIR/usr/local/bin/lua" "$rootfs/usr/local/bin/lua"
    fi

    # OS installer: C++ disk installer built against musl (SHA-512 password
    # hashing via crypt()). Deployed to /usr/local/sbin/fenriros-install.
    if [[ -x "$CHROOT_DIR/usr/local/sbin/fenriros-install" ]]; then
        echo "  [system] Installing OS installer ..." >&2
        mkdir -p "$rootfs/usr/local/sbin"
        cp -a "$CHROOT_DIR/usr/local/sbin/fenriros-install" "$rootfs/usr/local/sbin/fenriros-install"
        chmod 755 "$rootfs/usr/local/sbin/fenriros-install"
    fi

    # Terminfo database — needed by the ncurses-based installer (and any
    # terminal program).  Only deploy the common entries to save space.
    if [[ -d "$CHROOT_DIR/usr/local/share/terminfo" ]]; then
        echo "  [system] Installing terminfo database ..." >&2
        mkdir -p "$rootfs/usr/local/share/terminfo"
        for entry in l/linux v/vt100 v/vt220 x/xterm x/xterm-256color a/ansi d/dumb; do
            dir="$rootfs/usr/local/share/terminfo/${entry%/*}"
            mkdir -p "$dir"
            if [[ -f "$CHROOT_DIR/usr/local/share/terminfo/$entry" ]]; then
                cp -a "$CHROOT_DIR/usr/local/share/terminfo/$entry" "$dir/"
            fi
        done
    fi

    # Project shared libraries + libelf
    copy_shared_libs "$rootfs" >&2
    find "$CHROOT_DIR/usr/local/lib" -maxdepth 1 -name 'libelf.so*' \
        -exec cp -a {} "$rootfs/usr/local/lib/" \; 2>/dev/null || true

    # Container payloads (prebuilt .webcontainer images) for the host to run.
    if [[ -d "$OUTPUT_DIR" ]]; then
        mkdir -p "$rootfs/var/lib/webcommander"
        find "$OUTPUT_DIR" -maxdepth 1 -name '*.webcontainer' \
            -exec cp -a {} "$rootfs/var/lib/webcommander/" \; 2>/dev/null || true
        [[ -f "$OUTPUT_DIR/index.xml" ]] && \
            cp -a "$OUTPUT_DIR/index.xml" "$rootfs/var/lib/webcommander/" 2>/dev/null || true
    fi

    # Ship the GNU-free build recipes + patches so the system is reproducible /
    # self-hosting: musl & libelf CMakeLists, plus any CMake patches.
    if [[ -d "$SCRIPT_DIR/files" ]]; then
        mkdir -p "$rootfs/usr/local/src/fenriros"
        cp -a "$SCRIPT_DIR/files" "$rootfs/usr/local/src/fenriros/" 2>/dev/null || true
    fi
    if [[ -d "$SCRIPT_DIR/patches" ]]; then
        mkdir -p "$rootfs/usr/local/src/fenriros"
        cp -a "$SCRIPT_DIR/patches" "$rootfs/usr/local/src/fenriros/" 2>/dev/null || true
    fi

    # Kernel modules staged by build_kernel (modules_install). Copy them into
    # the system so toybox modprobe/depmod can load them at runtime. Skip when
    # the kernel is monolithic (no /lib/modules produced).
    if [[ -d "$CHROOT_DIR/build/linux-modules/lib/modules" ]] && \
       [[ -n "$(ls -A "$CHROOT_DIR/build/linux-modules/lib/modules" 2>/dev/null)" ]]; then
        echo "  [system] Installing kernel modules ..." >&2
        mkdir -p "$rootfs/lib/modules"
        cp -a "$CHROOT_DIR/build/linux-modules/lib/modules/." "$rootfs/lib/modules/" 2>/dev/null || true
    fi

    # Realtek NIC firmware (rtl_nic/*.fw) fetched by build_kernel. The r8169
    # driver request_firmware()s these at runtime (e.g. rtl8127a-1.fw for the
    # RTL8127A 10GbE controller); install them under /lib/firmware so the PHY
    # comes up. Skipped if the firmware was not fetched.
    if [[ -d "$CHROOT_DIR/build/linux-firmware/rtl_nic" ]] && \
       [[ -n "$(ls -A "$CHROOT_DIR/build/linux-firmware/rtl_nic" 2>/dev/null)" ]]; then
        echo "  [system] Installing Realtek NIC firmware ..." >&2
        mkdir -p "$rootfs/lib/firmware/rtl_nic"
        cp -a "$CHROOT_DIR/build/linux-firmware/rtl_nic/." \
            "$rootfs/lib/firmware/rtl_nic/" 2>/dev/null || true
    fi

    # musl dynamic-linker search path
    {
        echo "/usr/local/musl/lib"
        echo "/usr/local/lib"
    } > "$rootfs/etc/ld-musl-x86_64.path"

    # System-wide shell environment: put the musl/LLVM toolchain on PATH (clang,
    # makeplus, llvm-* binutils all live in /usr/local/musl/bin) and expose the
    # musl sysroot lib dir so dynamically-linked tools resolve their libraries.
    cat > "$rootfs/etc/profile" << 'PROFILE'
export PATH=/usr/local/sbin:/usr/local/musl/bin:/usr/local/bin:/bin:/sbin
export LD_LIBRARY_PATH=/usr/local/musl/lib:/usr/local/lib
export PKG_CONFIG_PATH=/usr/local/musl/lib/pkgconfig:/usr/local/lib/pkgconfig
export ENV=/etc/profile
PROFILE

    # Ensure root home exists and sources /etc/profile
    mkdir -p "$rootfs/root"
    cat > "$rootfs/root/.profile" << 'RPROFILE'
. /etc/profile
RPROFILE

    # GNU-free kernel-module tooling: use toybox's modutils applets instead of
    # GNU kmod so the system can load/unload modules (modprobe, insmod, ...).
    local mtool
    for mtool in modprobe insmod rmmod lsmod modinfo depmod; do
        ln -sf /bin/toybox "$rootfs/sbin/$mtool" 2>/dev/null || true
        ln -sf /bin/toybox "$rootfs/bin/$mtool" 2>/dev/null || true
    done

    # Login / TTY tooling: symlink toybox getty, login, passwd, su so syslite
    # can spawn getty processes that authenticate users against /etc/shadow.
    local ltool
    for ltool in getty login passwd su; do
        ln -sf /bin/toybox "$rootfs/bin/$ltool" 2>/dev/null || true
        ln -sf /bin/toybox "$rootfs/sbin/$ltool" 2>/dev/null || true
    done

    # LibreSSL openssl CLI tool: needed by the installer (install.lua) to hash
    # the root password into /etc/shadow at install time.
    if [[ -x "$CHROOT_DIR/usr/local/musl/bin/openssl" ]]; then
        echo "  [system] Installing LibreSSL openssl tool ..." >&2
        cp -a "$CHROOT_DIR/usr/local/musl/bin/openssl" "$rootfs/usr/local/musl/bin/openssl"
    fi

    # ── syslite init system ──────────────────────────────────────────────────
    # syslite is a lightweight C++ init (PID 1) that parses systemd-compatible
    # .service unit files and manages TTYs. It replaces the old toybox shell
    # init script.
    if [[ -x "$CHROOT_DIR/usr/local/sbin/syslite" ]]; then
        echo "  [system] Installing syslite init system ..." >&2
        cp -a "$CHROOT_DIR/usr/local/sbin/syslite" "$rootfs/sbin/syslite"
        ln -sf /sbin/syslite "$rootfs/sbin/init"
        ln -sf /sbin/syslite "$rootfs/init"
        # netdev control tool
        if [[ -x "$CHROOT_DIR/usr/local/sbin/netdev" ]]; then
            cp -a "$CHROOT_DIR/usr/local/sbin/netdev" "$rootfs/sbin/netdev"
        fi
    else
        echo "  [warn] syslite not built, falling back to shell init" >&2
        # Fallback: minimal shell init (toybox-based)
        cat > "$rootfs/init" << 'INIT'
#!/bin/sh
export PATH=/usr/local/musl/bin:/usr/local/bin:/bin:/sbin
export LD_LIBRARY_PATH=/usr/local/musl/lib:/usr/local/lib
/bin/toybox mount -t proc proc /proc 2>/dev/null
/bin/toybox mount -t sysfs sys /sys 2>/dev/null
/bin/toybox mount -t devtmpfs dev /dev 2>/dev/null
/bin/toybox mount -t tmpfs tmp /tmp 2>/dev/null
/bin/toybox mount -t tmpfs run /run 2>/dev/null
/bin/toybox hostname fenriros 2>/dev/null
echo "FenrirOS — musl/LLVM system (no GNU tools)"
exec /bin/sh
INIT
        chmod +x "$rootfs/init"
        ln -sf /init "$rootfs/sbin/init" 2>/dev/null || true
    fi

    # ── syslite configuration ────────────────────────────────────────────────
    # syslite uses compile-time config (config.h) — no runtime YAML needed.
    # Only the service unit directory and /etc/hostname are needed at runtime.
    mkdir -p "$rootfs/etc/syslite/services"
    mkdir -p "$rootfs/etc/systemd/network"
    echo "fenriros" > "$rootfs/etc/hostname"

    # ── Network configuration (systemd-networkd compatible .network files) ───
    # syslite parses these natively using Linux netlink API (no toybox needed)
    cat > "$rootfs/etc/systemd/network/20-wired.network" << 'EOF'
[Match]
Name=en* eth*

[Network]
DHCP=yes
EOF

    # ── Service unit files (systemd-compatible, parsed by syslite) ───────────

    # SSH server
    cat > "$rootfs/etc/syslite/services/sshd.service" << 'EOF'
[Unit]
Description=OpenSSH Server

[Service]
Type=simple
ExecStartPre=/bin/sh -c '/bin/toybox mkdir -p /run/sshd /var/empty; [ -f /run/sshd/ssh_host_ed25519_key ] || /usr/local/bin/ssh-keygen -q -t ed25519 -f /run/sshd/ssh_host_ed25519_key -N ""'
ExecStart=/usr/local/sbin/sshd -D
Restart=on-failure
RestartSec=5

[Install]
WantedBy=multi-user.target
EOF

    echo "$rootfs"
}

# ═══════════════════════════════════════════════════════════════
# Phase: master a bootable, GNU-free system ISO
#   limine (BSD-licensed bootloader) + xorriso, toybox cpio/squashfs payload.
#   Override the output with SYSTEM_ISO=/path/to/file.iso
# ═══════════════════════════════════════════════════════════════
build_system_iso() {
    echo "==> Finalizing: building bootable FenrirOS system ISO (GNU-free) ..."

    # Ensure the SSH server is built so it gets baked into the system image even
    # when this phase is run standalone (cheap no-op if already built).
    build_openssh

    # Filesystem tools (ext + FAT) baked into the image (cheap no-op if built).
    build_dosfstools
    build_e2fsprogs

    # GPT partitioner + its popt dependency (drives install.sh). build_gptfdisk
    # also ensures e2fsprogs libuuid + libpopt are present.
    build_gptfdisk

    # rsync file-copying tool (cheap no-op if already built).
    build_rsync

    # Lua scripting language (used by system scripts).
    build_lua

    # C++ disk installer (uses musl crypt() for SHA-512 password hashing).
    build_installer

    # Build-time-only ISO tooling (NOT shipped in the image). None of these are
    # part of the GNU coreutils/binutils/make set.
    $NSPAWN -D "$CHROOT_DIR" --pipe -- bash -c "
        set -e
        need=''
        command -v xorriso   >/dev/null 2>&1 || need=\"\$need xorriso\"
        command -v mksquashfs>/dev/null 2>&1 || need=\"\$need squashfs-tools\"
        command -v zstd      >/dev/null 2>&1 || need=\"\$need zstd\"
        if [ -n \"\$need\" ]; then apt-get install -y --no-install-recommends \$need; fi
    "

    # Assemble the GNU-free rootfs on the host.
    local rootfs
    rootfs=$(assemble_system_rootfs)
    local workdir="$CONTAINER_DIR/system"
    local isoroot="$workdir/iso"
    rm -rf "$isoroot"
    mkdir -p "$isoroot/boot/limine" "$isoroot/EFI/BOOT"

    # 1) Kernel
    local kimg="${KERNEL_IMAGE:-$CHROOT_DIR/build/linux/arch/x86/boot/bzImage}"
    if [[ -f "$kimg" ]]; then
        cp -a "$kimg" "$isoroot/boot/vmlinuz"
        # Also stage the kernel inside the system rootfs so the disk installer
        # (install.sh) can copy it onto the target ESP.
        mkdir -p "$rootfs/boot"
        cp -a "$kimg" "$rootfs/boot/vmlinuz"
    else
        echo "  [warn] No kernel found ($kimg) — set KERNEL_IMAGE=... or run the 'kernel' phase." >&2
    fi

    # 2) Initramfs: toybox + /init that mounts the squashfs and switch_root.
    echo "  [iso] Building initramfs (toybox cpio) ..."
    local initdir="$workdir/initramfs"
    rm -rf "$initdir"; mkdir -p "$initdir"/{bin,proc,sys,dev,newroot}
    cp -a "$CHROOT_DIR/usr/local/bin/toybox" "$initdir/bin/toybox"
    ln -sf toybox "$initdir/bin/sh"
    # e2fsck for early-boot filesystem repair (prevents orphan cleanup hangs
    # after a crash). It is a dynamic musl binary from the e2fsprogs build,
    # so we also ship the musl dynamic linker/libc.
    if [[ -x "$CHROOT_DIR/usr/local/sbin/e2fsck" ]]; then
        cp -a "$CHROOT_DIR/usr/local/sbin/e2fsck" "$initdir/bin/e2fsck"
        ln -sf e2fsck "$initdir/bin/fsck.ext4"
        ln -sf e2fsck "$initdir/bin/fsck.ext3"
        ln -sf e2fsck "$initdir/bin/fsck.ext2"
        # musl dynamic linker needed by e2fsck (libc.so == ld-musl-*.so.1)
        mkdir -p "$initdir/lib"
        local musl_libc="$CHROOT_DIR/usr/local/musl/lib/libc.so"
        if [[ -f "$musl_libc" ]]; then
            cp "$musl_libc" "$initdir/lib/ld-musl-x86_64.so.1"
            ln -sf ld-musl-x86_64.so.1 "$initdir/lib/libc.so"
        fi
    fi
    # GNU-free module tooling for early boot (toybox modutils).
    local mtool
    for mtool in modprobe insmod rmmod lsmod modinfo depmod; do
        ln -sf toybox "$initdir/bin/$mtool"
    done
    cat > "$initdir/init" << 'EINIT'
#!/bin/toybox sh
/bin/toybox mount -t proc proc /proc
/bin/toybox mount -t sysfs sys /sys
/bin/toybox mount -t devtmpfs dev /dev
/bin/toybox mkdir -p /mnt /newroot

# ── Installed-system boot ────────────────────────────────────────────────────
# If the kernel cmdline names a root filesystem (root=UUID=... or root=/dev/...),
# this is a disk-installed FenrirOS: mount that ext4 root and switch into it.
# The live ISO passes no root= and falls through to the squashfs scan below.
ROOTSPEC=""
ROOTFLAGS="rw"
for tok in $(/bin/toybox cat /proc/cmdline 2>/dev/null); do
    case "$tok" in
        root=*) ROOTSPEC="${tok#root=}" ;;
        ro)     ROOTFLAGS="ro" ;;
        rw)     ROOTFLAGS="rw" ;;
    esac
done
if [ -n "$ROOTSPEC" ]; then
    # Resolve the root device (retry while disks/NVMe enumerate, ~30s).
    rootdev=""
    i=0
    while [ -z "$rootdev" ] && [ "$i" -lt 30 ]; do
        case "$ROOTSPEC" in
            UUID=*) rootdev=$(/bin/toybox blkid -U "${ROOTSPEC#UUID=}" 2>/dev/null) ;;
            *)      [ -b "$ROOTSPEC" ] && rootdev="$ROOTSPEC" ;;
        esac
        [ -n "$rootdev" ] && break
        /bin/toybox sleep 1; i=$((i + 1))
    done
    if [ -n "$rootdev" ]; then
        # Run e2fsck before mounting to repair any damage from a crash (orphaned
        # inodes, dirty journal). -p = automatic safe repair, exit 0/1 = OK.
        if [ -x /bin/e2fsck ]; then
            echo "FenrirOS: checking $rootdev ..."
            /bin/e2fsck -p "$rootdev"
            # e2fsck exit: 0=clean, 1=corrected, 2=reboot needed, 4+=errors
            frc=$?
            if [ "$frc" -ge 4 ]; then
                echo "FenrirOS: filesystem errors on $rootdev (e2fsck exit $frc)"
                echo "          Attempting forced repair ..."
                /bin/e2fsck -y "$rootdev" || true
            fi
        fi
        # Always mount root READ-ONLY first — the kernel may refuse a rw mount
        # on a dirty filesystem (orphaned inodes, unclean journal). syslite
        # remounts / rw once it takes over as PID 1.
        if /bin/toybox mount -t ext4 -o ro "$rootdev" /newroot 2>/dev/null; then
            exec /bin/toybox switch_root /newroot /sbin/syslite
        fi
        echo "FenrirOS: mount $rootdev failed"
    fi
    echo "FenrirOS: root=$ROOTSPEC not found — dropping to initramfs shell"
    exec /bin/toybox sh
fi

# ── Live (ISO) boot ──────────────────────────────────────────────────────────
# Probe every block device (CD, USB stick, SATA/NVMe) for the boot medium that
# carries the read-only squashfs system image, then loop-mount it and hand off.
# Hardcoding /dev/sr0 only works for optical media; USB boot enumerates as
# /dev/sd*, so scan whole disks AND their partitions instead.
try_boot() {
    for dev in $(/bin/toybox ls /sys/block 2>/dev/null); do
        case "$dev" in loop*|ram*|zram*|dm-*) continue ;; esac
        for node in "/dev/$dev" /dev/"$dev"*; do
            [ -b "$node" ] || continue
            # Only try iso9660/udf — the squashfs lives on the ISO medium, never
            # on a raw ext4 partition. Avoids expensive ext4 orphan cleanup spam.
            /bin/toybox mount -t iso9660 -o ro "$node" /mnt 2>/dev/null \
              || /bin/toybox mount -t udf -o ro "$node" /mnt 2>/dev/null \
              || continue
            if [ -f /mnt/boot/filesystem.squashfs ]; then
                # The system image is a regular file, so it has to go through a
                # loop device. Newer kernels (new mount API) report
                # "<file>: Can't lookup blockdev" (visible in dmesg) and return
                # ENOTBLK when a filesystem is mounted directly on a file, so we
                # MUST attach an explicit loop device first.
                #
                # Do NOT use losetup's -f auto-find: it needs /dev/loop-control
                # and, when it can't allocate, prints "-f: No such file or
                # directory" and returns an EMPTY device — which then drops us
                # into the failing `mount -o loop` path. Instead create explicit
                # /dev/loopN nodes (devtmpfs normally provides them) and use the
                # reliable `losetup [-r] DEVICE FILE` form, trying each in turn.
                # -r opens the backing file READ-ONLY (it lives on the read-only
                # iso9660 medium, so O_RDWR would fail EROFS).
                [ -e /dev/loop-control ] || /bin/toybox mknod /dev/loop-control c 10 237 2>/dev/null
                ln=0
                while [ "$ln" -lt 8 ]; do
                    [ -e "/dev/loop$ln" ] || /bin/toybox mknod "/dev/loop$ln" b 7 "$ln" 2>/dev/null
                    if /bin/toybox losetup -r "/dev/loop$ln" /mnt/boot/filesystem.squashfs 2>/dev/null \
                       && /bin/toybox mount -t squashfs -o ro "/dev/loop$ln" /newroot 2>/dev/null; then
                        exec /bin/toybox switch_root /newroot /sbin/syslite
                    fi
                    /bin/toybox losetup -d "/dev/loop$ln" 2>/dev/null
                    ln=$((ln + 1))
                done
            fi
            /bin/toybox umount /mnt 2>/dev/null
        done
    done
    return 1
}

# USB/SATA may need a moment to enumerate; retry for ~30s before giving up.
i=0
while [ "$i" -lt 30 ]; do
    try_boot
    /bin/toybox sleep 1
    i=$((i + 1))
done

# ── Fallback: find an installed system (ext4 root with /sbin/syslite) ────────
# If no squashfs was found AND no root= was on the cmdline, try to find an
# installed FenrirOS on any ext4 partition. This handles the case where the
# bootloader's limine.conf lacks a root= parameter.
try_installed() {
    for dev in $(/bin/toybox ls /sys/block 2>/dev/null); do
        case "$dev" in loop*|ram*|zram*|dm-*) continue ;; esac
        for node in /dev/"$dev"[0-9]* /dev/"$dev"p[0-9]*; do
            [ -b "$node" ] || continue
            /bin/toybox mount -t ext4 -o ro "$node" /newroot 2>/dev/null || continue
            if [ -x /newroot/sbin/syslite ]; then
                echo "FenrirOS: found installed system on $node"
                /bin/toybox umount /newroot 2>/dev/null
                # Remount read-only for clean switch_root
                /bin/toybox mount -t ext4 -o ro "$node" /newroot 2>/dev/null \
                    && exec /bin/toybox switch_root /newroot /sbin/syslite
            fi
            /bin/toybox umount /newroot 2>/dev/null
        done
    done
    return 1
}
try_installed

echo "FenrirOS initramfs shell (no system image found)"
exec /bin/toybox sh
EINIT
    chmod +x "$initdir/init"
    mkdir -p "$initdir/mnt" "$initdir/newroot" "$initdir/proc" "$initdir/sys" "$initdir/dev"
    $NSPAWN -D "$CHROOT_DIR" --bind="$workdir:/build/system" --pipe -- bash -c "
        set -e
        cd /build/system/initramfs
        find . | /usr/local/bin/toybox cpio -o -H newc > /build/system/iso/boot/initramfs.cpio
        zstd -19 -f /build/system/iso/boot/initramfs.cpio -o /build/system/iso/boot/initramfs.zst
        rm -f /build/system/iso/boot/initramfs.cpio
    "

    # 3) Bootloader: limine (BSD-licensed, BIOS+UEFI, no GNU dependency).
    echo "  [iso] Installing limine bootloader ..."
    $NSPAWN -D "$CHROOT_DIR" --bind="$workdir:/build/system" --pipe -- bash -c "
        set -e
        cd /build
        if [ ! -d limine ]; then
            git clone --depth 1 --branch ${LIMINE_VERSION} '${REPOS[limine]}' limine || \
            git clone --depth 1 '${REPOS[limine]}' limine
        fi
        cp -a limine/limine-bios.sys    /build/system/iso/boot/limine/ 2>/dev/null || true
        cp -a limine/limine-bios-cd.bin /build/system/iso/boot/limine/ 2>/dev/null || true
        cp -a limine/limine-uefi-cd.bin /build/system/iso/boot/limine/ 2>/dev/null || true
        cp -a limine/BOOTX64.EFI        /build/system/iso/EFI/BOOT/    2>/dev/null || true
    "

    cat > "$isoroot/boot/limine/limine.conf" << LIMCFG
timeout: 3
/FenrirOS (musl/LLVM, no GNU tools)
    protocol: linux
    kernel_path: boot():/boot/vmlinuz
    module_path: boot():/boot/initramfs.zst
    cmdline: console=tty0
LIMCFG

    # 4) Stage boot artifacts into the system rootfs so the disk installer
    #    (install.sh) can lay down a self-booting EFI system on the target:
    #      /boot/vmlinuz (already staged above), /boot/initramfs.zst, and the
    #      limine UEFI payload under /usr/local/share/limine.
    echo "  [iso] Staging boot artifacts into the system rootfs (installer) ..."
    mkdir -p "$rootfs/boot" "$rootfs/usr/local/share/limine"
    cp -a "$isoroot/boot/initramfs.zst" "$rootfs/boot/initramfs.zst" 2>/dev/null || true
    cp -a "$isoroot/EFI/BOOT/BOOTX64.EFI" "$rootfs/usr/local/share/limine/BOOTX64.EFI" 2>/dev/null || true

    # 5a) RPM repository — include built RPMs in the ISO
    local rpm_repo="$CHROOT_DIR/build/rpm-repo"
    if [[ -d "$rpm_repo" ]] && ls "$rpm_repo"/*.rpm >/dev/null 2>&1; then
        echo "  [iso] Including RPM repository in system image ..."
        mkdir -p "$rootfs/var/lib/fenriros/rpms"
        cp -a "$rpm_repo/"*.rpm "$rootfs/var/lib/fenriros/rpms/"
        echo "  [iso] $(ls -1 "$rpm_repo"/*.rpm | wc -l) RPMs staged"
    else
        echo "  [iso] No RPM repo found — run 'rpmrepo' phase first to include packages"
    fi

    # 5) Squash the (now boot-complete) system rootfs (zstd) — read-only image.
    echo "  [iso] Creating filesystem.squashfs ..."
    $NSPAWN -D "$CHROOT_DIR" --bind="$workdir:/build/system" --pipe -- bash -c "
        set -e
        rm -f /build/system/iso/boot/filesystem.squashfs
        mksquashfs /build/system/rootfs /build/system/iso/boot/filesystem.squashfs \
            -comp zstd -noappend -no-progress
    " || echo "  [warn] mksquashfs failed (system will rely on initramfs only)" >&2

    # 6) Master the hybrid (BIOS+UEFI) ISO with xorriso, then limine bios-install.
    echo "  [iso] Mastering ISO with xorriso ..."
    local iso_out iso_name
    iso_out="${SYSTEM_ISO:-$OUTPUT_DIR/fenriros-${ARCH}.iso}"
    iso_name="$(basename "$iso_out")"
    mkdir -p "$OUTPUT_DIR"
    $NSPAWN -D "$CHROOT_DIR" \
        --bind="$workdir:/build/system" \
        --bind="$OUTPUT_DIR:/build/out" --pipe -- bash -c "
        set -e
        xorriso -as mkisofs \
            -R -r -J \
            -b boot/limine/limine-bios-cd.bin \
            -no-emul-boot -boot-load-size 4 -boot-info-table \
            --efi-boot boot/limine/limine-uefi-cd.bin \
            -efi-boot-part --efi-boot-image --protective-msdos-label \
            -volid ${ISO_LABEL} \
            /build/system/iso -o /build/out/${iso_name}
        if [ -x /build/limine/limine ]; then
            /build/limine/limine bios-install /build/out/${iso_name} || true
        fi
    "

    if [[ -f "$iso_out" ]]; then
        local size sha
        size="$(stat -c %s "$iso_out" 2>/dev/null || echo 0)"
        sha="$(sha256sum "$iso_out" | awk '{print $1}')"
        echo "$sha" > "$iso_out.sha256"
        echo "  [done] $iso_out"
        echo "         size:   $(numfmt --to=iec "$size" 2>/dev/null || echo "${size} bytes")"
        echo "         sha256: $sha"
    else
        echo "  [warn] ISO was not produced — check kernel/limine availability." >&2
    fi
}

# ═══════════════════════════════════════════════════════════════
# Phase: finalize — full GNU-free pipeline ending in a runnable system ISO
# ═══════════════════════════════════════════════════════════════
finalize() {
    echo "==> Finalization: assembling a runnable, GNU-free FenrirOS image ..."
    setup_chroot
    setup_toolchain
    build_tools
    build_libelf
    build_libraries
    build_postgresql
    build_authdb
    build_mediadb
    build_blogi
    build_blogi_editor
    build_syslite
    # Build the Linux kernel inside the chroot with the LLVM toolchain. If
    # Kbuild cannot be driven by makeplus, set KERNEL_IMAGE=/path/bzImage to
    # supply a prebuilt image for the ISO instead.
    build_kernel
    build_openssh
    build_dosfstools
    build_e2fsprogs
    build_popt
    build_gptfdisk
    build_rsync
    build_lua
    build_ncurses
    build_installer
    build_system_iso
    echo "==> Finalization complete."
}

# ═══════════════════════════════════════════════════════════════
# Main
# ═══════════════════════════════════════════════════════════════
run_phase() {
    local phase="$1"
    case "$phase" in
        chroot)        setup_chroot ;;
        toolchain)     setup_toolchain ;;
        buildtools)    build_tools ;;
        libraries)     build_libraries ;;
        postgresql)    build_postgresql ;;
        authdb)        build_authdb ;;
        mediadb)       build_mediadb ;;
        blogi)         build_blogi ;;
        blogi-editor)  build_blogi_editor ;;
        syslite)       build_syslite ;;
        package)       package_all ;;
        devel)         setup_devel ;;
        shell)         enter_chroot_shell ;;
        devel-tar)     package_devel_chroot ;;
        zlib)          build_zlib ;;
        libelf)        build_libelf ;;
        libressl)      build_libressl ;;
        openssh)       build_openssh ;;
        dosfstools)    build_dosfstools ;;
        e2fsprogs)     build_e2fsprogs ;;
        popt)          build_popt ;;
        gptfdisk)      build_gptfdisk ;;
        rsync)         build_rsync ;;
        lua)           build_lua ;;
        ncurses)       build_ncurses ;;
        zstd)          build_zstd ;;
        file)          build_file ;;
        m4)            build_m4 ;;
        flex)          build_flex ;;
        ninja)         build_ninja ;;
        libarchive)    build_libarchive ;;
        rpm)           build_rpm ;;
        rpmpkg)        package_rpms ;;
        rpmrepo)       package_rpms; create_rpm_repo ;;
        installer)     build_installer ;;
        kernel)        build_kernel ;;
        iso)           build_system_iso ;;
        finalize)      finalize ;;
        all)
            setup_chroot
            setup_toolchain
            build_tools
            build_libraries
            build_postgresql
            build_authdb
            build_mediadb
            build_blogi
            build_blogi_editor
            build_syslite
            ;;
    esac
}

echo "╔══════════════════════════════════════════════════════════════╗"
echo "║  WebCommander container build — musl + LLVM libc++          ║"
echo "╚══════════════════════════════════════════════════════════════╝"
echo ""
echo "Containers: postgresql, authdb, mediadb, blogi, blogi-editor"
echo "Output:     $OUTPUT_DIR/*.webcontainer"
echo ""

for phase in "${PHASES[@]}"; do
    run_phase "$phase"
done

# Skip the repository summary for interactive-only runs (devel/shell): the
# index/output listing is irrelevant when we just dropped into a dev shell.
INTERACTIVE_ONLY=1
for phase in "${PHASES[@]}"; do
    case "$phase" in
        devel|shell|devel-tar) ;;
        *) INTERACTIVE_ONLY=0 ;;
    esac
done

if [[ $INTERACTIVE_ONLY -eq 0 ]]; then
    echo ""
    generate_index_xml

    echo ""
    echo "==> All done."
    if [[ -d "$OUTPUT_DIR" ]]; then
        echo ""
        echo "Output files:"
        ls -lh "$OUTPUT_DIR/"*.webcontainer 2>/dev/null || echo "  (none yet)"
        ls -lh "$OUTPUT_DIR/"*.iso 2>/dev/null || true
    fi
fi
