🔒 Repository is read-only – file editing is disabled.

PaganLinux/build-repo.sh main

434 linii Raw ← Powrót
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434
#!/bin/bash
# =============================================================================
# build-repo.sh – budowa pakietów do repo PAG na PC (w chrootcie PaganOS)
# -----------------------------------------------------------------------------
# DLACZEGO NA PC, A NIE NA VPS:
#   VPS buduje przez builder-chroot.sh w chrootcie DEBIANA (suite trixie), więc
#   pakiety linkują się z Debianowymi bibliotekami. To właśnie stąd brały się
#   rozjazdy ABI w repo: `libgsf` z odwołaniem do xmlNanoHTTPClose (w PaganLinux
#   nie ma – libxml2 2.16 tego nie eksportuje) i `libavcodec` z
#   rsvg_handle_render_document (nasze librsvg to 2.40.21, Debian ma 2.46+).
#   Budowa w rootfsie PaganOS daje pakiety spójne z dystrybucją – a PC jest
#   szybszy, więc i tak wygrywa.
#
# Użycie (wymaga roota):
#   sudo ./build-repo.sh --prepare                 # utwórz rootfs buildera i zbuduj domyślne pakiety
#   sudo ./build-repo.sh kernel nvidia-kernel      # zbuduj wybrane pakiety
#   sudo ./build-repo.sh --rootfs /mnt/pagan-build --out ./repo-out kernel
#   sudo ./build-repo.sh --sign --gpg-key ABCD1234 kernel
#   sudo ./build-repo.sh --upload user@vps:/var/www/repo.paganlinux.eu/stable kernel
#
# Kolejność ma znaczenie przy NVIDIA: `kernel` musi pójść pierwszy (skrypt sam
# instaluje go w rootfsie buildera, bo z niego korzysta `nvidia-kernel`).
#
# Czego ten skrypt NIE robi: nie publikuje repo.json (to robi `pagsync --gen-repo`
# na VPS – patrz instrukcja wypisywana na końcu).
# =============================================================================
set -euo pipefail

SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"

ROOTFS="/mnt/pagan-build"                 # rootfs buildera (chroot)
PAGAN_SRC_ROOT="${PAGAN_ROOT:-/mnt/pagan}" # źródłowy rootfs (obraz) do skopiowania
OUT_DIR="$SCRIPT_DIR/repo-out"
SRC_CACHE="/var/cache/pagbuild/sources"
DO_PREPARE=0
DO_SIGN=0
GPG_KEY=""
UPLOAD=""
PKGS=()

# Domyślnie: rolling kernel + gotowe moduły NVIDIA (to, czego brakuje w repo).
DEFAULT_PKGS=(kernel nvidia-kernel)

usage() { sed -n '3,/^set -euo pipefail$/p' "$0" | grep -v '^set -euo pipefail$'; exit "${1:-0}"; }

while [[ $# -gt 0 ]]; do
    case "$1" in
        --rootfs)  ROOTFS="$2"; shift 2 ;;
        --out)     OUT_DIR="$2"; shift 2 ;;
        --pagansrc) PAGAN_SRC_ROOT="$2"; shift 2 ;;
        --prepare) DO_PREPARE=1; shift ;;
        --sign)    DO_SIGN=1; shift ;;
        --gpg-key) GPG_KEY="$2"; shift 2 ;;
        --upload)  UPLOAD="$2"; shift 2 ;;
        -h|--help) usage 0 ;;
        -*)        echo "Nieznana opcja: $1" >&2; usage 2 ;;
        *)         PKGS+=("$1"); shift ;;
    esac
done

[[ $EUID -eq 0 ]] || { echo "❌ Wymagane uprawnienia root (chroot)." >&2; exit 1; }
[[ -x "$SCRIPT_DIR/pagbuild" ]] || { echo "❌ Brak wykonywalnego $SCRIPT_DIR/pagbuild" >&2; exit 1; }
[[ ${#PKGS[@]} -gt 0 ]] || PKGS=("${DEFAULT_PKGS[@]}")

info() { printf '\033[1;36m▸ %s\033[0m\n' "$*"; }
ok()   { printf '\033[1;32m✓ %s\033[0m\n' "$*"; }
warn() { printf '\033[1;33m! %s\033[0m\n' "$*"; }
die()  { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 1; }

ABI_FAILED=()

# ── Montowanie systemowych katalogów w chrootcie (dla `pag install`) ──
_mounted=0
_mount_chroot() {
    local r="$1"
    for d in dev proc sys; do
        mountpoint -q "$r/$d" 2>/dev/null || mount --bind "/$d" "$r/$d" 2>/dev/null || true
    done
    _mounted=1
}
_umount_chroot() {
    [[ $_mounted -eq 1 ]] || return 0
    local r="$1"
    for d in sys proc dev; do
        mountpoint -q "$r/$d" 2>/dev/null && umount -l "$r/$d" 2>/dev/null || true
    done
    _mounted=0
}

# ── (1) Przygotowanie rootfsu buildera ───────────────────────────────────────
# Kopiujemy GOTOWY rootfs PaganOS (ma już gcc/make/bison/flex/perl) zamiast
# stawiać od zera rootfs.sh (godziny). Wyłączamy sources/ – to 10+ GB, a pagbuild
# sam ściąga źródła do /var/cache/pagbuild/work wewnątrz chroota.
if [[ $DO_PREPARE -eq 1 && ! -d "$ROOTFS" ]]; then
    [[ -d "$PAGAN_SRC_ROOT/usr/bin" ]] || die "Brak $PAGAN_SRC_ROOT – podaj --pagansrc"
    info "Tworzę rootfs buildera: $ROOTFS  (z $PAGAN_SRC_ROOT, bez sources/)"
    df -h "$(dirname "$ROOTFS")" | tail -1
    command -v rsync >/dev/null || die "Brak rsync (potrzebny do kopii rootfsu)"
    mkdir -p "$ROOTFS"
    rsync -aHAX --info=progress2 \
        --exclude='/sources/' --exclude='/tmp/' --exclude='/var/cache/pagbuild/' \
        --exclude='/proc/' --exclude='/sys/' --exclude='/dev/' --exclude='/run/' \
        "$PAGAN_SRC_ROOT/" "$ROOTFS/" || die "Kopiowanie rootfsu nieudane"
    ok "Rootfs buildera gotowy: $ROOTFS"
fi

[[ -d "$ROOTFS/usr/bin" ]] || die "Brak rootfsu buildera ($ROOTFS). Użyj --prepare."

# ── (2) Narzędzia budowy, których brakuje w obrazie ────────────────────────
# binutils: ISO ma tylko binutils-pass1 w /tools – w /usr/bin NIE MA `ld`, więc
#   nic się nie zlinkuje (dotyczy też DKMS na docelowym systemie).
# libelf:  jądro robi `pkg-config --exists libelf` (objtool dla
#   CONFIG_UNWINDER_ORC=y). Obraz ma libelf.so, ale BEZ libelf.pc → w chrootcie
#   budowa jądra pada na „Cannot generate ORC metadata … please install
#   libelf-dev”. W buildzie ISO to przechodziło, bo objtool buduje się
#   narzędziami HOSTA (a host ma libelf), nie rootfsu.
# bc/bison/flex: wymagane przez Kbuild; w obrazie są, ale gdy kiedyś znikną…
_pag_install_in_rootfs() {
    local r="$1"; shift
    [[ $# -gt 0 ]] || return 0
    cp -L /etc/resolv.conf "$r/etc/resolv.conf" 2>/dev/null || true
    _mount_chroot "$r"
    trap '_umount_chroot "$ROOTFS"' EXIT
    chroot "$r" /usr/bin/env PAG_YES=1 /usr/bin/pag install -y "$@" \
        || warn "  nie udało się doinstalować: $*"
    _umount_chroot "$r"
    trap - EXIT
}

for _spec in "binutils:/usr/bin/ld" \
             "libelf:/usr/lib/pkgconfig/libelf.pc" \
             "bc:/usr/bin/bc" "bison:/usr/bin/bison" "flex:/usr/bin/flex" \
             "meson:/usr/bin/meson" "ninja:/usr/bin/ninja" \
             "cmake:/usr/bin/cmake" "pkgconf:/usr/bin/pkgconf" \
             "autoconf:/usr/bin/autoconf" "automake:/usr/bin/automake" \
             "libtool:/usr/bin/libtool" "m4:/usr/bin/m4"; do
    _dep="${_spec%%:*}"; _sent="${_spec#*:}"
    if [[ ! -e "$ROOTFS$_sent" ]]; then
        info "Doinstalowuję $_dep w rootfsie buildera (brak $_sent)"
        _pag_install_in_rootfs "$ROOTFS" "$_dep"
    fi
done
[[ -x "$ROOTFS/usr/bin/ld" ]] || warn "Nadal brak $ROOTFS/usr/bin/ld – linker zostanie wzięty z hosta? (nie zadziała w chrootcie)"
# meson OSOBNO i z twardą kontrolą: obraz ISO go nie zawiera (nie jest potrzebny
# w systemie uruchomieniowym), a KAŻDA receptura mesonowa (mesa, libglvnd, libva,
# xf86-video-amdgpu…) padnie na "meson: command not found" (exit 127). Pre-flight
# w pagbuild pomija meson jako „zawsze obecny w builderze”, więc bez tego wpisu
# brak meson nie jest w ogóle raportowany przed buildem.
if [[ ! -x "$ROOTFS/usr/bin/meson" ]]; then
    warn "Brak meson w rootfsie buildera – receptury mesonowe padną na 'meson: command not found'"
fi

# cmake w tym rootfsie bywa zbudowany pod systemowy rhash (NEEDED librhash.so.1),
# a sam pakiet rhash nie jest wciągany jako zależność – po podmianie cmake przez
# `pag install` (np. jako zależności glslang/llvm) KAŻDY build cmake'owy pada na
# „cmake: error while loading shared libraries: librhash.so.1”. Wykrywamy to
# działaniem, a nie obecnością pliku (cmake JEST, tylko zepsuty).
if [[ -x "$ROOTFS/usr/bin/cmake" ]]; then
    if ! grep -qa 'librhash\.so' "$ROOTFS/usr/bin/cmake" 2>/dev/null; then
        :    # cmake bez librhash – OK (self-contained)
    elif [[ ! -e "$ROOTFS/usr/lib/librhash.so.1" && ! -e "$ROOTFS/usr/lib64/librhash.so.1" ]]; then
        info "cmake wymaga librhash.so.1, którego nie ma – doinstalowuję rhash"
        _pag_install_in_rootfs "$ROOTFS" rhash
    fi
fi

# ── (3) Cache źródeł: nie ściągaj ponownie 160 MB kernela ───────────────────
# pagbuild trzyma źródła w /var/cache/pagbuild/sources (nazwa = basename URL-a,
# chyba że to „tagowa” nazwa typu 610.57.04.tar.gz – wtedy z hashem; kernel ma
# nazwę linux-<ver>.tar.xz, więc trafia do cache pod tą samą nazwą).
# Wersję bierzemy z receptury, żeby nie rozjechała się z tym, co budujemy.
mkdir -p "$SRC_CACHE" "$OUT_DIR"
_kernel_ver="$(python3 -c "import yaml,sys;print(yaml.safe_load(open(sys.argv[1]))['pkgver'])" \
    "$SCRIPT_DIR/recipes/utils/kernel/PAGBUILD.yaml" 2>/dev/null || echo 7.2.5)"
for f in "$PAGAN_SRC_ROOT"/sources/linux-"$_kernel_ver".tar.xz; do
    [[ -f "$f" ]] || continue
    b="$(basename "$f")"
    [[ -f "$SRC_CACHE/$b" ]] || { cp -f "$f" "$SRC_CACHE/$b" && ok "Cache źródeł: $b (z $PAGAN_SRC_ROOT/sources)"; }
done

# ── (4) Budowa ──
# Kolejność ma znaczenie: nvidia-kernel buduje się POD ŹRÓDŁA z pakietu kernel
# (/usr/src/kernel), więc po zbudowaniu kernela instalujemy go w rootfsie
# buildera – inaczej Kbuild nie znajdzie drzewa jądra.
#
# Przed każdą recepturą doinstalowujemy jej `makedepends` w rootfsie buildera
# (jedno `pag install` na recepturę). Bez tego ciężkie buildy (mesa z
# -Dvalgrind=enabled, gvfs z libsecret…) padają na konfiguracji, mimo że pakiet
# JEST w repo.
_install_recipe_deps() {
    local recipe="$1" deps
    deps="$(python3 - "$recipe" <<'PY'
import sys, yaml
d = yaml.safe_load(open(sys.argv[1])) or {}
md = d.get("makedepends") or []
if isinstance(md, str):
    md = [md]
# pomijamy wpisy wirtualne (pkgconfig(x), libfoo.so) – to nie nazwy pakietów
print(" ".join(x for x in md if x and not any(c in x for c in "()")))
PY
)"
    [[ -n "$deps" ]] || return 0
    info "  makedepends: $deps"
    _pag_install_in_rootfs "$ROOTFS" $deps
}

# ── Auto-zapis configu jądra po udanym buildzie ──────────────────────────────
# Receptura w fazie build robi `make olddefconfig`, więc w pakiecie leży config
# dopasowany do bieżącego Kconfig (boot/config-<release>). Zapisujemy go z
# powrotem do receptury (+ świeży SHA wpisu `config`). Na PC nie ma pagsync,
# dlatego robimy to tutaj; zmiany w recipes/ trzeba zacommitować.
_save_kernel_config() {
    local recipe="$1" pag="$2"
    [[ -f "$recipe" && -f "$pag" ]] || return 0
    python3 - "$recipe" "$pag" <<'PY'
import io, os, sys, tarfile, hashlib
import yaml
recipe, pag = sys.argv[1:3]
cfg = os.path.join(os.path.dirname(os.path.abspath(recipe)), "config")
if not os.path.isfile(cfg):
    raise SystemExit

def extract(p):
    with tarfile.open(p, "r:xz") as tf:
        fh = tf.extractfile("data.tar.xz")
        if fh is None:
            return None, None
        data = fh.read()
    with tarfile.open(fileobj=io.BytesIO(data), mode="r:xz") as df:
        for ti in df.getmembers():
            nm = ti.name[2:] if ti.name.startswith("./") else ti.name
            parts = nm.split("/")
            if len(parts) == 2 and parts[0] == "boot" and parts[1].startswith("config-"):
                fh = df.extractfile(ti)
                return (fh.read().decode("utf-8", "replace"), parts[1][len("config-"):]) if fh else (None, None)
    return None, None

content, rel = extract(pag)
if not content:
    print("   \u23ed save-kernel-config: brak boot/config-* w " + os.path.basename(pag))
    raise SystemExit
if not content.endswith("\n"):
    content += "\n"
old = open(cfg, encoding="utf-8").read()
if content == old:
    print("   \u2705 config kernela aktualny (" + rel + ")")
    raise SystemExit
open(cfg, "w", encoding="utf-8").write(content)
sha = hashlib.sha256(open(cfg, "rb").read()).hexdigest()
txt = open(recipe, encoding="utf-8").read()
d = yaml.safe_load(txt) or {}
srcs = d.get("source") or []
if isinstance(srcs, str):
    srcs = [srcs]
if "config" in srcs:
    idx = srcs.index("config")
    lines = txt.split("\n"); start = None
    for i, l in enumerate(lines):
        if l.startswith("sha256sums:"):
            start = i; break
    if start is not None:
        n = -1
        for j in range(start + 1, len(lines)):
            l = lines[j]
            if l.startswith("- "):
                n += 1
                if n == idx:
                    lines[j] = "- " + sha; break
            elif l and not l.startswith((" ", "\t")):
                break
        open(recipe, "w", encoding="utf-8").write("\n".join(lines))
print("   \U0001f4be config kernela zaktualizowany (" + rel + ", sha " + sha[:12] + "…) – zacommituj recipes/")
PY
}

# ── Instalacja zbudowanego pakietu w rootfsie buildera ──────────────────────
# PO CO: łańcuchy zależności w JEDNEJ komendzie (libva → intel-media-driver,
# mesa → xf86-video-*). Bez tego drugi pakiet nie widzi nagłówków/.pc pierwszego
# i pada już na konfiguracji („Package 'libva' not found”), mimo że receptura ma
# go w makedepends – pagbuild instaluje makedepends TYLKO z repo, a świeżo
# zbudowanego pakietu w repo jeszcze nie ma.
# Podmieniamy pliki WŁASNYM pakietem zbudowanym w tym samym rootfsie, więc
# builder pozostaje spójny z targetem (żadnego Debiana tu nie mieszamy).
# Wyłączenie: PAGAN_BUILDER_NO_INSTALL=1
_install_in_builder() {
    local pag_file="$1" pkgname="$2" in_chroot_dir _s
    [[ "${PAGAN_BUILDER_NO_INSTALL:-0}" == "1" ]] && return 0
    [[ -f "$pag_file" ]] || return 0
    info "  → instaluję $pkgname w rootfsie buildera (widoczność dla kolejnych buildów)"
    in_chroot_dir="$ROOTFS/var/cache/pagbuild/out"
    mkdir -p "$in_chroot_dir"
    cp -f "$pag_file" "$in_chroot_dir/"
    # pag weryfikuje lokalny plik FAIL-CLOSED – podpis (jeśli jest) musi trafić do chroot razem z pakietem.
    for _s in "$pag_file".asc "$pag_file".sig; do
        [[ -f "$_s" ]] && cp -f "$_s" "$in_chroot_dir/"
    done
    _mount_chroot "$ROOTFS"
    trap '_umount_chroot "$ROOTFS"' EXIT
    # PAG_INSECURE=1: pakiety budowane TU (na PC) nie są podpisane – nie ma tu
    # klucza repo (celowo trzyma go VPS). `pag` jest FAIL-CLOSED dla lokalnych
    # plików, więc bez tego instalacja w builderze kończyła się „Brak podpisu dla
    # pakietu”. To NASZE, świeżo zbudowane pakiety w NASZYM rootfsie budowy, więc
    # pominięcie weryfikacji jest tu bezpieczne i celowe; na systemie docelowym
    # podpisy nadal obowiązują (tam instalacja idzie z podpisanego repo).
    chroot "$ROOTFS" /usr/bin/env PAG_YES=1 PAG_INSECURE=1 /usr/bin/pag install -y \
        "/var/cache/pagbuild/out/$(basename "$pag_file")" \
        || warn "    instalacja $pkgname w builderze nieudana – kolejne buildy mogą go nie widzieć"
    _umount_chroot "$ROOTFS"
    trap - EXIT
}

_built_kernel_pag=""
for pkg in "${PKGS[@]}"; do
    info "Budowanie: $pkg"
    # UWAGA: NIE pisz tego jako `_recipe="$SCRIPT_DIR/recipes"/*/"$pkg"/PAGBUILD.yaml`!
    # Bash NIE rozwija globów w zwykłym przypisaniu (tylko w `arr=( ... )`), więc
    # zmienna zostawała LITERALNYM wzorcem z gwiazdką, `[[ -f $_recipe ]]` był
    # zawsze fałszywy i makedepends NIGDY nie trafiały do rootfsu buildera – buildy
    # padały (meson/cmake/pkg-config nie widziały zależności), a komunikat mówił
    # „brak receptury” mimo że receptura leżała na dysku. `find` jest odporne.
    _recipe="$(find "$SCRIPT_DIR/recipes" -type f -path "*/$pkg/PAGBUILD.yaml" -print -quit 2>/dev/null || true)"
    if [[ -n "$_recipe" && -f "$_recipe" ]]; then
        info "  receptura: ${_recipe#"$SCRIPT_DIR/"}"
        _install_recipe_deps "$_recipe"
    else
        warn "  brak receptury dla '$pkg' w recipes/*/ – pagbuild i tak spróbuje"
        _recipe=""   # nie przekazuj śmieciowego wzorca do _save_kernel_config
    fi
    args=( "$pkg" --rootfs "$ROOTFS" --out "$OUT_DIR" )
    [[ $DO_SIGN -eq 1 ]] && args+=( --sign )
    [[ -n "$GPG_KEY" ]] && args+=( --gpg-key "$GPG_KEY" )
    if ! "$SCRIPT_DIR/pagbuild" "${args[@]}"; then
        warn "Budowa $pkg NIEUDANA – pomijam"
        continue
    fi
    pag_file="$(ls -1t "$OUT_DIR/$pkg"-*.pag 2>/dev/null | head -n1 || true)"
    if [[ -z "$pag_file" ]]; then
        warn "$pkg: pagbuild zwrócił 0, ale nie ma $OUT_DIR/$pkg-*.pag"
        continue
    fi
    ok "$pkg$pag_file ($(du -h "$pag_file" | cut -f1))"
    printf '    sha256: %s\n' "$(sha256sum "$pag_file" | cut -d' ' -f1)"

    # Auto-zapis configu jądra (olddefconfig) z powrotem do receptury.
    case "$pkg" in kernel*) _save_kernel_config "$_recipe" "$pag_file" ;; esac

    # ── Bramka ABI ──────────────────────────────────────────────────────────
    # Pakiet NIE MOŻE odwoływać się do symboli, których nie ma w docelowym
    # PaganLinux. To wyłapuje pakiety zbudowane w chrootcie Debiana na VPS
    # (tak wyszły zepsute libgsf z xmlNanoHTTPClose i libavcodec z
    # rsvg_handle_render_document). Indeks symboli jest cache'owany.
    #
    # TRYB ROZJAZDU (--build-rootfs), a NIE „celu”: pytamy wyłącznie o symbole,
    # które SĄ w środowisku budowy, a których NIE MA w docelowym systemie – dokładnie
    # to znaczy „zbudowano pod inne biblioteki”. Tryb „celu” (bez --build-rootfs)
    # raportuje KAŻDĄ zależność spoza obrazu i kończy się kodem 0 (bez --strict),
    # więc jako bramka był bezużyteczny: zawsze mówił „ABI zgodne”, mimo że dla
    # pakietów repo zależności spoza ISO są normalne (dociąga je `pag`).
    # UWAGA do nazwy cache: „-v2” celowo. Cache v1 (/var/cache/pagbuild/target-symbols.txt)
    # powstał przy skanie z -maxdepth, więc NIE zawiera symboli modułów z głębszych
    # katalogów (np. /usr/lib/xorg/modules/libglamoregl.so → glamor_init) i dawał
    # fałszywe „rozjazdy ABI” dla sterowników Xorg. Nowa wersja skanuje całe drzewo.
    if [[ -x "$SCRIPT_DIR/check-pag-abi.sh" ]]; then
        if PAGAN_ABI_INDEX="${PAGAN_ABI_INDEX:-/var/cache/pagbuild/target-symbols-v2.txt}" \
           "$SCRIPT_DIR/check-pag-abi.sh" --rootfs "$PAGAN_SRC_ROOT" \
               --build-rootfs "$ROOTFS" --quiet "$pag_file" >/dev/null 2>&1; then
            ok "  $pkg: brak rozjazdu ABI względem $PAGAN_SRC_ROOT"
        else
            warn "  $pkg: ROZJAZD ABI z $PAGAN_SRC_ROOT – NIE publikuj tego pakietu!"
            warn "    szczegóły: ./check-pag-abi.sh --rootfs $PAGAN_SRC_ROOT --build-rootfs $ROOTFS $pag_file"
            ABI_FAILED+=("$pkg")
        fi
    fi

    # Każdy zbudowany pakiet ląduje w rootfsie buildera, żeby KOLEJNE buildy z tej
    # samej komendy widziały jego nagłówki/.pc (np. libva → intel-media-driver).
    _install_in_builder "$pag_file" "$pkg"

    # Kernel dodatkowo: gwarancja /usr/src/kernel (SYSSRC dla nvidia-kernel/dkms),
    # nawet gdy `pag install` odmówi (podpis / konflikt symlinku w /usr/lib/modules).
    if [[ "$pkg" == "kernel" ]]; then
        _built_kernel_pag="$pag_file"
        in_chroot_dir="$ROOTFS/var/cache/pagbuild/out"
        mkdir -p "$in_chroot_dir"
        # Gwarancja obecności źródeł jądra (SYSSRC dla nvidia-kernel/dkms).
        # Bezpośrednie rozpakowanie omija bramkę podpisu pag oraz konflikt
        # symlinku w /usr/lib/modules (który nas tu nie interesuje) – bierzemy
        # tylko /usr/src i /boot.
        if [[ ! -d "$ROOTFS/usr/src/kernel" ]]; then
            if tar -xJf "$pag_file" -C "$in_chroot_dir" data.tar.xz 2>/dev/null; then
                tar -xJf "$in_chroot_dir/data.tar.xz" -C "$ROOTFS" ./usr/src ./boot 2>/dev/null \
                    || warn "  niepełne rozpakowanie źródeł jądra"
                rm -f "$in_chroot_dir/data.tar.xz"
            fi
        fi
        [[ -d "$ROOTFS/usr/src/kernel" ]] \
            || warn "  brak $ROOTFS/usr/src/kernel – nvidia-kernel może się nie zbudować"
    fi
done

echo
info "Zbudowane pakiety w: $OUT_DIR"
ls -1sh "$OUT_DIR"/*.pag 2>/dev/null || warn "Brak .pag w $OUT_DIR"

# Bramka ABI: czy któryś pakiet odwołuje się do symboli, których nie ma w obrazie?
if [[ ${#ABI_FAILED[@]} -gt 0 ]]; then
    echo
    warn "UWAGA: ${#ABI_FAILED[@]} pakiet(ów) z niezgodnym ABI: ${ABI_FAILED[*]}"
    warn "Takie pakiety padają na starcie z „undefined symbol”. NIE wgrywaj ich do repo –"
    warn "przebuduj natywnie na PC (ten skrypt) albo popraw zależności w recepturze."
    _abi_rc=1
else
    _abi_rc=0
fi
echo
cat <<EOF
────────────────────────────────────────────────────────────────────────────
PUBLIKACJA (na VPS, tam gdzie stoi repo i pagsync):

  1) wgraj pakiety (z tego PC):
       rsync -av --progress "$OUT_DIR"/*.pag* ${UPLOAD:-user@vps:/var/www/repo.paganlinux.eu/stable/}
  2) na VPS odśwież repo.json (i ewentualnie podpisy):
       sudo pagsync --gen-repo --sign          # sama regeneracja indeksu
       # albo pełny przebieg (skan receptur + kolejka):
       sudo pagsync --once --sign

  Podpisane paczki muszą leżeć obok .pag jako <nazwa>.pag.asc – jeśli budujesz
  z --sign, rsync powyżej zabierze je razem z pakietami.
────────────────────────────────────────────────────────────────────────────
EOF

# Kod wyjścia: 1, gdy którykolwiek pakiet ma niezgodne ABI (łatwe do wykrycia
# w automatyzacji: `./build-repo.sh ... || echo "NIE PUBLIKUJ"`).
exit "$_abi_rc"