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

PaganLinux/pagbuild.txt main

1834 linii Raw ← Powrót
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291129212931294129512961297129812991300130113021303130413051306130713081309131013111312131313141315131613171318131913201321132213231324132513261327132813291330133113321333133413351336133713381339134013411342134313441345134613471348134913501351135213531354135513561357135813591360136113621363136413651366136713681369137013711372137313741375137613771378137913801381138213831384138513861387138813891390139113921393139413951396139713981399140014011402140314041405140614071408140914101411141214131414141514161417141814191420142114221423142414251426142714281429143014311432143314341435143614371438143914401441144214431444144514461447144814491450145114521453145414551456145714581459146014611462146314641465146614671468146914701471147214731474147514761477147814791480148114821483148414851486148714881489149014911492149314941495149614971498149915001501150215031504150515061507150815091510151115121513151415151516151715181519152015211522152315241525152615271528152915301531153215331534153515361537153815391540154115421543154415451546154715481549155015511552155315541555155615571558155915601561156215631564156515661567156815691570157115721573157415751576157715781579158015811582158315841585158615871588158915901591159215931594159515961597159815991600160116021603160416051606160716081609161016111612161316141615161616171618161916201621162216231624162516261627162816291630163116321633163416351636163716381639164016411642164316441645164616471648164916501651165216531654165516561657165816591660166116621663166416651666166716681669167016711672167316741675167616771678167916801681168216831684168516861687168816891690169116921693169416951696169716981699170017011702170317041705170617071708170917101711171217131714171517161717171817191720172117221723172417251726172717281729173017311732173317341735173617371738173917401741174217431744174517461747174817491750175117521753175417551756175717581759176017611762176317641765176617671768176917701771177217731774177517761777177817791780178117821783178417851786178717881789179017911792179317941795179617971798179918001801180218031804180518061807180818091810181118121813181418151816181718181819182018211822182318241825182618271828182918301831183218331834
#!/usr/bin/env bash
# =============================================================================
# PAGBUILD v2 – PaganOS Package Builder (chroot + PAGBUILD.yaml)
# Usage: pagbuild /path/to/PAGBUILD.yaml [--rootfs /path] [--out /path] [--keep-work]
# =============================================================================
set -euo pipefail

# ── Domyślne ścieżki ──
ROOTFS="/var/lib/pagan-build/rootfs"
OUT_DIR="/var/cache/pagbuild/output"
SRC_CACHE="/var/cache/pagbuild/sources"
BUILD_WORK="${PAGBUILD_WORK:-/var/cache/pagbuild/work}"
# Katalog roboczy WEWNĄTRZ chroot – NIE /tmp: /tmp w chroot to czesto tmpfs
# (RAM), jest czyszczony i mały; duże buildy (rustc, qtbase) potrafią się na
# nim wysypać (OOM) albo zniknąć po restarcie.
CHROOT_WORK="/var/cache/pagbuild/work"
KEEP_WORK=false
RECIPE_FILE=""
GPG_KEY=""
DO_SIGN=false
PKG_EXT=".pag"
STRICT_DEPS=false

# Ograniczenie rownoleglosci kompilacji: VPS ma 12 GB RAM, a ciezkie buildy
# (node/V8, firefox, mariadb) potrafia odpalic ~15x cc1plus i wtedy kernel
# ubija proces OOM-killerem. Domyślnie cap = 4 joby (nadpisz: PAGBUILD_JOBS).
export MAKEFLAGS="-j${PAGBUILD_JOBS:-4}"

# Ograniczenie rownoleglosci kompilacji: VPS ma 12 GB RAM, a ciezkie buildy
# (node/V8, firefox, mariadb) potrafia odpalic ~15x cc1plus i wtedy kernel
# ubija proces OOM-killerem. Domyślnie cap = 4 joby (nadpisz: PAGBUILD_JOBS).

# Katalog diagnostyki po nieudanym buildzie – pagsync --auto-deps czyta stąd
# pliki diagnose-<name>-<ver>.json i automatycznie dobudowuje brakujące
# zależności buildowe, jeśli istnieją dla nich receptury.
DIAG_DIR="${PAGBUILD_DIAG:-/var/cache/pagbuild/diagnostics}"

# ── Minimalne PATH – cron/web może mieć ucięte PATH (brak /usr/sbin) ──
export PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin${PATH:+:$PATH}"

# ── Lokalizacja chroot – bywa poza PATH (np. /usr/sbin/chroot) ──
CHROOT_BIN=""
for _cand in /usr/sbin/chroot /sbin/chroot /bin/chroot /usr/bin/chroot; do
    [[ -x "$_cand" ]] && CHROOT_BIN="$_cand" && break
done
[[ -z "$CHROOT_BIN" ]] && CHROOT_BIN="$(command -v chroot 2>/dev/null || true)"
if [[ -z "$CHROOT_BIN" ]]; then
    echo "❌ Błąd: nie znaleziono binarki chroot (zainstaluj coreutils)."
    exit 1
fi

# ── Sprzątanie po zakończeniu (sukces LUB błąd) ──
_cleanup() {
    local rc=$?
    umount -l "$ROOTFS/dev/pts" 2>/dev/null || true
    umount -l "$ROOTFS/dev" 2>/dev/null || true
    umount -l "$ROOTFS/sys" 2>/dev/null || true
    umount -l "$ROOTFS/proc" 2>/dev/null || true
    # Clean only OUR per-build dirs (no race with other builds)
    rm -rf "$ROOTFS$CHROOT_WORK/src-$SHORT_NAME" "$ROOTFS$CHROOT_WORK/destdir-$SHORT_NAME" "$ROOTFS$CHROOT_WORK/build-$SHORT_NAME.sh" 2>/dev/null || true
    if [[ "$KEEP_WORK" != "true" && -n "${WORK_DIR:-}" ]]; then
        rm -rf "$WORK_DIR" 2>/dev/null || true
    fi
    _cleanup_stale
    exit "$rc"
}

# ── Gruntowne sprzątanie WSZYSTKICH zaległości po buildach ──
# Rootfs jest chronione wyłącznym lockiem (flock powyżej), więc bezpiecznie
# usuwamy katalogi robocze KAŻDEGO pakietu, nie tylko bieżącego. To sprząta
# śmieci po buildzie ubitym SIGKILL-em/crashem (gdy trap EXIT nie zdążył).
_cleanup_stale() {
    # 1) Zaległe katalogi robocze wewnątrz chroot ($CHROOT_WORK — nie /tmp):
    #    src-*, destdir-* (katalogi pagbuild) + build-*.sh
    if [[ -d "$ROOTFS$CHROOT_WORK" ]]; then
        find "$ROOTFS$CHROOT_WORK" -mindepth 1 -maxdepth 1 \
            \( -name 'src-*' -o -name 'destdir-*' -o -name 'build-*.sh' \) \
            -exec rm -rf {} + 2>/dev/null || true
        # Puste katalogi o losowych nazwach – usuwamy TYLKO puste, więc zero
        # ryzyka utraty danych.
        find "$ROOTFS$CHROOT_WORK" -mindepth 1 -maxdepth 1 -type d -empty -delete 2>/dev/null || true
    fi
    # 2) Zaległe katalogi robocze buildów na hoście (zachowaj bieżący
    #    WORK_DIR, gdy KEEP_WORK=true – do debugowania)
    if [[ -d "$BUILD_WORK" ]]; then
        if [[ -n "${WORK_DIR:-}" && -d "$WORK_DIR" ]]; then
            find "$BUILD_WORK" -mindepth 1 -maxdepth 1 ! -path "$WORK_DIR" \
                -exec rm -rf {} + 2>/dev/null || true
        else
            find "$BUILD_WORK" -mindepth 1 -maxdepth 1 \
                -exec rm -rf {} + 2>/dev/null || true
        fi
    fi
    # 3) Porzucone/niekompletne pobierania w cache źródeł (*.tmp)
    if [[ -d "$SRC_CACHE" ]]; then
        find "$SRC_CACHE" -mindepth 1 -maxdepth 1 -name '*.tmp' -delete 2>/dev/null || true
    fi
}
# ── Jedna instancja na współdzielonym rootfs ──
BUILD_LOCK="${ROOTFS}.pagbuild.lock"
exec 9>"$BUILD_LOCK"
if ! flock -n 9; then
    echo "❌ Błąd: rootfs jest już używany przez inny build: $ROOTFS"
    exit 75
fi

# Trzymamy wyłączny lock – posprzątaj zaległości po poprzednich/zerwanych
# buildach, zanim cokolwiek zaczniemy.
_cleanup_stale

# ── trap ustawiany PO zdefiniowaniu SHORT_NAME (linia ~150) ──

# ── Mirror map – gdy główny serwer blokuje IP, próbuj mirrorów ──
declare -A MIRRORS=(
    ["ftp.gnu.org"]="https://mirrors.kernel.org/gnu https://ftp.nluug.nl/pub/gnu https://mirror.ibcp.fr/pub/gnu"
    ["www.openssl.org"]="https://github.com/openssl/openssl/releases"
    ["sourceware.org"]="https://mirrors.kernel.org/sourceware"
    ["www.kernel.org"]="https://mirrors.edge.kernel.org"
)

# ── Parsowanie argumentów ──
while [[ $# -gt 0 ]]; do
    case "$1" in
        --rootfs) ROOTFS="$2"; shift 2 ;;
        --out)    OUT_DIR="$2"; shift 2 ;;
        --keep-work) KEEP_WORK=true; shift ;;
        --sign)   DO_SIGN=true; shift ;;
        --gpg-key) GPG_KEY="$2"; shift 2 ;;
        --strict-deps) STRICT_DEPS=true; shift ;;
        *)        RECIPE_FILE="$1"; shift ;;
    esac
done

if [[ -z "$RECIPE_FILE" ]]; then
    echo "❌ Użycie: pagbuild /path/to/PAGBUILD.yaml [--rootfs /path] [--out /path] [--strict-deps]"
    exit 1
fi

if [[ ! -f "$RECIPE_FILE" ]]; then
    echo "❌ Błąd: Plik przepisu $RECIPE_FILE nie istnieje."
    exit 1
fi

if [[ $EUID -ne 0 ]]; then
    echo "❌ Błąd: pagbuild wymaga uprawnień roota do chroota."
    exit 1
fi

echo "📦 Parsowanie przepisu: $RECIPE_FILE"

# ── Parsowanie YAML (format: pkgname/pkgver/pkgrel/depends/makedepends/source/sha256sums/build/package) ──
eval $(python3 -c "
import yaml, sys, shlex

with open('$RECIPE_FILE') as f:
    data = yaml.safe_load(f)

# Podstawowe metadane
print(f'PKG_NAME={shlex.quote(str(data.get(\"pkgname\", \"\")))}')
print(f'PKG_VER={shlex.quote(str(data.get(\"pkgver\", \"\")))}')
print(f'PKG_REL={shlex.quote(str(data.get(\"pkgrel\", 1)))}')
print(f'PKG_DESC={shlex.quote(str(data.get(\"pkgdesc\", \"\")))}')
print(f'PKG_URL={shlex.quote(str(data.get(\"url\", \"\")))}')
print(f'PKG_ARCH={shlex.quote(str(data.get(\"arch\", \"x86_64\")))}')

# Licencje
licenses = data.get('license', [])
if isinstance(licenses, str):
    licenses = [licenses]
print(f'PKG_LICENSE={shlex.quote(\" \".join(licenses))}')

# Zależności
depends = data.get('depends', [])
if isinstance(depends, str):
    depends = [depends]
print(f'PKG_DEPENDS={shlex.quote(\" \".join(depends))}')

makedepends = data.get('makedepends', [])
if isinstance(makedepends, str):
    makedepends = [makedepends]
print(f'PKG_MAKEDEPENDS={shlex.quote(\" \".join(makedepends))}')

# provides / pkgconfig – wirtualne zależności (pkgconfig(X) mapuje na pakiet;
# klient pag i pagsync --auto-deps rozwiązują przez nie brakujące moduły)
provides = data.get('provides', [])
if isinstance(provides, str):
    provides = [provides]
pkgconfigs = data.get('pkgconfig', [])
if isinstance(pkgconfigs, str):
    pkgconfigs = [pkgconfigs]
provides = provides + [f'pkgconfig({p})' for p in pkgconfigs]
print(f'PKG_PROVIDES={shlex.quote(\" \".join(provides))}')

# Źródła
sources = data.get('source', [])
if isinstance(sources, str):
    sources = [sources]
sha256s = data.get('sha256sums', [])
if isinstance(sha256s, str):
    sha256s = [sha256s]

for i, s in enumerate(sources):
    print(f'SRC_URL_{i}={shlex.quote(s)}')
    if i < len(sha256s):
        print(f'SRC_SHA_{i}={shlex.quote(sha256s[i])}')

print(f'SRC_COUNT={len(sources)}')

# Kategoria – zgadujemy z ścieżki pliku
import os
recipe_dir = os.path.dirname(os.path.abspath('$RECIPE_FILE'))
cat = 'other'
parts = recipe_dir.split('/')
for known in ['core', 'libs', 'drivers', 'net', 'utils', 'gui', 'boot', 'de']:
    if known in parts:
        cat = known
        break
print(f'PKG_CAT={shlex.quote(cat)}')
")

if [[ -z "$PKG_NAME" || -z "$PKG_VER" ]]; then
    echo "❌ Błąd: Brak nazwy (pkgname) lub wersji (pkgver) w $RECIPE_FILE"
    exit 1
fi

FULL_NAME="${PKG_NAME}-${PKG_VER}-${PKG_REL}"
SHORT_NAME="${PKG_NAME}-${PKG_VER}"
echo "🚀 Budowanie: $FULL_NAME [$PKG_CAT]"

# ── Teraz ustaw trap (SHORT_NAME już zdefiniowane) ──
trap _cleanup EXIT INT TERM

# ── Pre-flight: weryfikacja makedepends w rootfs PRZED buildem ──
# Profesjonalny builder (jak makepkg w Archu) sprawdza zależności buildowe
# wcześniej, zamiast pozwolić buildowi paść w połowie na
# "Dependency X not found, tried pkgconfig and cmake".
# Proba jest heurystyczna (narzędzie / *.pc / nagłówek / biblioteka), więc
# domyślnie to OSTRZEŻENIE; --strict-deps podnosi brak do twardego błędu.
probe_rootfs_dep() {
    local dep="$1" rootfs="$2" _p _g _alt _first _rest _lib
    [[ -n "$dep" ]] || return 0
    # 0) Rejestr pakietów zainstalowanych do rootfs przez pagsync --install /
    #    _install_to_rootfs ($ROOTFS/var/lib/pagbuild/installed.json). Probe nie
    #    wykryje po artefaktach pakietów-danych (docbook-xsl w /usr/share/xml),
    #    narzędzi o innej nazwie binarki (gobject-introspection -> g-ir-scanner,
    #    texinfo -> makeinfo) ani modułów o innej nazwie .pc (kerberos -> krb5.pc)
    #    – rejestr rozwiązuje to dokładnie, bez zgadywania.
    if [[ -f "$rootfs/var/lib/pagbuild/installed.json" ]]; then
        grep -q "\"$dep\"" "$rootfs/var/lib/pagbuild/installed.json" && return 0
    fi
    # 1) narzędzie w rootfs
    for _p in usr/local/bin usr/bin usr/sbin bin sbin; do
        [[ -x "$rootfs/$_p/$dep" ]] && return 0
    done
    # 2) moduł pkg-config: <dep>.pc (i <nazwa>.pc z obciętym prefiksem lib –
    #    libsamplerate dostarcza samplerate.pc, libasound -> asound.pc, ...)
    for _p in usr/lib/pkgconfig usr/share/pkgconfig usr/lib64/pkgconfig usr/lib/x86_64-linux-gnu/pkgconfig; do
        [[ -f "$rootfs/$_p/$dep.pc" ]] && return 0
        [[ "$dep" == lib* && "${#dep}" -gt 4 && -f "$rootfs/$_p/${dep#lib}.pc" ]] && return 0
    done
    # 3) nagłówek <dep>.h lub katalog <dep>/
    [[ -f "$rootfs/usr/include/$dep.h" ]] && return 0
    [[ -d "$rootfs/usr/include/$dep" ]] && return 0
    # 4) biblioteka lib<dep>.so* / lib<dep>.a – ORAZ <dep>.so* (gdy dep już ma
    #    prefiks lib: libsamplerate -> libsamplerate.so, a nie liblibsamplerate.so)
    for _p in usr/lib usr/lib64 usr/lib/x86_64-linux-gnu lib lib64; do
        for _g in "$rootfs/$_p/lib$dep".so* "$rootfs/$_p/lib$dep".a \
                  "$rootfs/$_p/$dep".so* "$rootfs/$_p/$dep".a; do
            [[ -e "$_g" ]] && return 0
        done
    done
    # 5) heurystyka xorg-libX11: nazwa pakietu != nazwa biblioteki (libX11.so)
    if [[ "$dep" == xorg-lib* ]]; then
        _alt="${dep#xorg-lib}"
        _first="$(printf '%s' "$_alt" | cut -c1 | tr '[:lower:]' '[:upper:]')"
        _rest="$(printf '%s' "$_alt" | cut -c2-)"
        _lib="lib${_first}${_rest}"
        for _p in usr/lib usr/lib64 usr/lib/x86_64-linux-gnu; do
            for _g in "$rootfs/$_p/$_lib".so* "$rootfs/$_p/$_lib".a; do
                [[ -e "$_g" ]] && return 0
            done
        done
    fi
    # 6) aliasy: nazwa pakietu != nazwa artefaktu (gtk3 -> gtk+-3.0.pc/libgtk-3.so,
    #    glib -> glib-2.0.pc, gdk-pixbuf -> gdk-pixbuf-2.0.pc, ...). Bez tego
    #    pre-flight fałszywie zgłaszał BRAK dla zainstalowanych pakietów.
    case "$dep" in
        glib)           _pc="glib-2.0.pc";           _hdr="glib-2.0/glib.h";          _lib="libglib-2.0" ;;
        gtk3)           _pc="gtk+-3.0.pc";           _hdr="gtk-3.0/gtk/gtk.h";        _lib="libgtk-3" ;;
        gtk2)           _pc="gtk+-2.0.pc";           _hdr="gtk-2.0/gtk/gtk.h";        _lib="libgtk-x11-2.0" ;;
        gtk4)           _pc="gtk4.pc";               _hdr="gtk-4.0/gtk/gtk.h";       _lib="libgtk-4" ;;
        gdk-pixbuf)     _pc="gdk-pixbuf-2.0.pc";     _hdr="gdk-pixbuf-2.0/gdk-pixbuf/gdk-pixbuf.h"; _lib="libgdk_pixbuf-2.0" ;;
        pango)          _pc="pango.pc";              _hdr="pango-1.0/pango/pango.h"; _lib="libpango-1.0" ;;
        cairo)          _pc="cairo.pc";              _hdr="cairo/cairo.h";           _lib="libcairo" ;;
        harfbuzz)       _pc="harfbuzz.pc";           _hdr="harfbuzz/hb.h";           _lib="libharfbuzz" ;;
        freetype2|libfreetype) _pc="freetype2.pc";   _hdr="freetype2/freetype/freetype.h"; _lib="libfreetype" ;;
        libxml2)        _pc="libxml-2.0.pc";         _hdr="libxml2/libxml/parser.h"; _lib="libxml2" ;;
        libpng)         _pc="libpng.pc";             _hdr="png.h";                  _lib="libpng" ;;
        libjpeg-turbo)  _pc="libjpeg.pc";            _hdr="jpeglib.h";              _lib="libjpeg" ;;
        libtiff)        _pc="libtiff-4.pc";          _hdr="tiffio.h";               _lib="libtiff" ;;
        lcms2)          _pc="lcms2.pc";              _hdr="lcms2.h";                _lib="liblcms2" ;;
        libpcap)        _pc="libpcap.pc";            _hdr="pcap.h";                 _lib="libpcap" ;;
        ncurses)        _pc="ncursesw.pc";           _hdr="ncurses.h";              _lib="libncursesw" ;;
        zlib)           _pc="zlib.pc";               _hdr="zlib.h";                 _lib="libz" ;;
        bzip2)          _pc="bzip2.pc";              _hdr="bzlib.h";                _lib="libbz2" ;;
        xz)             _pc="liblzma.pc";            _hdr="lzma.h";                 _lib="liblzma" ;;
        readline)       _pc="readline.pc";           _hdr="readline/readline.h";    _lib="libreadline" ;;
        expat)          _pc="expat.pc";              _hdr="expat.h";                _lib="libexpat" ;;
        sqlite)         _pc="sqlite3.pc";            _hdr="sqlite3.h";              _lib="libsqlite3" ;;
        curl)           _pc="libcurl.pc";            _hdr="curl/curl.h";            _lib="libcurl" ;;
        openssl)        _pc="openssl.pc";            _hdr="openssl/ssl.h";          _lib="libssl" ;;
        libffi)         _pc="libffi.pc";             _hdr="ffi.h";                  _lib="libffi" ;;
        libxcrypt)      _pc="libxcrypt.pc";          _hdr="crypt.h";                _lib="libxcrypt" ;;
        libunistring)   _pc="libunistring.pc";       _hdr="unistring.h";            _lib="libunistring" ;;
        libidn2)        _pc="libidn2.pc";            _hdr="idn2.h";                 _lib="libidn2" ;;
        libgpg-error)   _pc="gpg-error.pc";          _hdr="gpg-error.h";            _lib="libgpg-error" ;;
        gnutls)         _pc="gnutls.pc";             _hdr="gnutls/gnutls.h";        _lib="libgnutls" ;;
        intltool)
            # narzędzie nazywa się intltoolize, nie intltool
            for _t in usr/local/bin usr/bin usr/sbin bin sbin; do
                [[ -x "$rootfs/$_t/intltoolize" ]] && return 0
            done
            _pc="" ;;
        perl-xml-parser)
            # moduł perla XML::Parser – brak binarki/.pc; szukamy pliku modułu
            for _m in "$rootfs"/usr/lib/perl5/*/site_perl/XML/Parser.pm; do
                [[ -f "$_m" ]] && return 0
            done
            _pc="" ;;
        dbus)           _pc="dbus-1.pc";            _hdr="dbus-1.0/dbus/dbus.h";     _lib="libdbus-1" ;;
        kerberos)       _pc="krb5.pc";              _hdr="krb5/krb5.h";              _lib="libkrb5" ;;
        alsa-lib)       _pc="alsa.pc";              _hdr="alsa/asoundlib.h";         _lib="libasound" ;;
        e2fsprogs)      _pc="e2p.pc";               _hdr="ext2fs/ext2fs.h";          _lib="libext2fs" ;;
        gobject-introspection) _pc="gobject-introspection-1.0.pc"; _hdr="girepository-1.0/girepository.h"; _lib="libgirepository-1.0" ;;
        sgml-common)
            # narzędzie nazywa się install-catalog, nie sgml-common
            for _t in usr/local/bin usr/bin usr/sbin bin sbin; do
                [[ -x "$rootfs/$_t/install-catalog" ]] && return 0
            done
            [[ -d "$rootfs/usr/share/sgml" ]] && return 0
            _pc="" ;;
        docbook-xsl)
            # dane XML (stylesheet) – nie binarka/.pc/lib; sprawdzamy katalog
            [[ -d "$rootfs/usr/share/xml/docbook" ]] && return 0
            _pc="" ;;
        */*)            _pc="" ;;
        mesa|libgl)     _pc="gl.pc";                 _hdr="GL/gl.h";                _lib="libGL" ;;
        libxkbcommon)   _pc="xkbcommon.pc";          _hdr="xkbcommon/xkbcommon.h";  _lib="libxkbcommon" ;;
        wayland)        _pc="wayland-client.pc";     _hdr="wayland-client.h";       _lib="libwayland-client" ;;
        libx11|xorg-libx11) _pc="x11.pc";            _hdr="X11/Xlib.h";             _lib="libX11" ;;
        cargo-c)
            # binarki to cargo-cbuild/cargo-capi/cargo-cinstall/cargo-ctest,
            # nie cargo-c – probe po nazwie nie trafiała i pakiet był
            # niepotrzebnie przebudowywany mimo dobrego .pag w repo
            for _t in usr/local/bin usr/bin usr/sbin bin sbin; do
                for _b in cargo-cbuild cargo-capi cargo-cinstall cargo-ctest; do
                    [[ -x "$rootfs/$_t/$_b" ]] && return 0
                done
            done
            _pc="" ;;
        openssh)
            # artefakty to ssh/sshd/ssh-keygen/..., nie „openssh” – probe po
            # nazwie zawsze zgłaszał BRAK mimo zainstalowanego pakietu
            for _t in usr/local/bin usr/bin usr/sbin bin sbin; do
                for _b in ssh sshd ssh-keygen ssh-keyscan ssh-agent ssh-add; do
                    [[ -x "$rootfs/$_t/$_b" ]] && return 0
                done
            done
            _pc="" ;;
        apr)            _pc="apr-1.pc";             _hdr="apr-1/apr.h";              _lib="libapr-1" ;;
        apr-util)       _pc="apr-util-1.pc";        _hdr="apr-1/apu.h";             _lib="libaprutil-1" ;;
        wlroots)        _pc="wlroots-0.20.pc";      _hdr="wlr/types.h";             _lib="libwlroots-0.20" ;;
        pcre2)          _pc="pcre2-8.pc";           _hdr="pcre2.h";                 _lib="libpcre2-8" ;;
        pam)            _pc="pam.pc";               _hdr="security/pam_appl.h";     _lib="libpam" ;;
        tllist)
            # header-only (tllist.h) – bez .pc i biblioteki
            [[ -f "$rootfs/usr/include/tllist.h" ]] && return 0
            _pc="" ;;
        *)              _pc="" ;;
    esac
    if [[ -n "$_pc" ]]; then
        for _p in usr/lib/pkgconfig usr/share/pkgconfig usr/lib64/pkgconfig usr/lib/x86_64-linux-gnu/pkgconfig; do
            [[ -f "$rootfs/$_p/$_pc" ]] && return 0
        done
        [[ -f "$rootfs/usr/include/$_hdr" ]] && return 0
        [[ -d "$rootfs/usr/include/${_hdr%/*}" ]] && return 0
        for _p in usr/lib usr/lib64 usr/lib/x86_64-linux-gnu; do
            for _g in "$rootfs/$_p/$_lib".so* "$rootfs/$_p/$_lib".a; do
                [[ -e "$_g" ]] && return 0
            done
        done
    fi
    # 7) czyste moduły Pythona (python-*): nie mają binarki/.pc/nagłówka/lib,
    #    więc probe po artefaktach zawsze zgłaszał BRAK (nawet po instalacji
    #    do rootfs) i pętla auto-deps w kółko dobudowywała te same pakiety
    #    (python-setuptools, python-build, python-typing_extensions, ...).
    #    Szukamy katalogu modułu oraz dist-info/egg-info w site-packages.
    if [[ "$dep" == python-* || "$dep" == py-* ]]; then
        _mod="${dep#python-}"
        _mod_us="${_mod//-/_}"
        case "$dep" in
            python-xdg) _mod_us="pyxdg" ;;
            python-yaml) _mod_us="yaml" ;;
            python-gobject) _mod_us="gi" ;;
            python-jinja) _mod_us="jinja2" ;;
            python-docutils) _mod_us="docutils" ;;
            python-importlib-metadata) _mod_us="importlib_metadata" ;;
        esac
        for _sp in "$rootfs"/usr/lib/python*/site-packages "$rootfs"/usr/lib64/python*/site-packages; do
            [[ -d "$_sp" ]] || continue
            [[ -e "$_sp/$_mod" || -e "$_sp/$_mod_us" ]] && return 0
            for _hit in "$_sp"/"$_mod_us"-* "$_sp"/"$_mod"-* "$_sp"/"$_mod_us"_*; do
                [[ -e "$_hit" ]] && return 0
            done
        done
        return 1
    fi
    # 8) moduły Perla (perl-*): brak binarki/.pc/lib o tej nazwie – szukamy
    #    pliku/katalogu modułu w site_perl/vendor_perl (perl-xml-simple ->
    #    XML/Simple.pm). Bez tego łańcuchy perl-* (ovmf, perl-libwww) zawsze
    #    zgłaszały BRAK i dobudowywały te same pakiety w kółko.
    if [[ "$dep" == perl-* ]]; then
        _prel=""
        case "$dep" in
            perl-libwww)            _prel="LWP.pm" ;;
            perl-lwp-mediatypes)    _prel="LWP/MediaTypes.pm" ;;
            perl-www-robotrules)    _prel="WWW/RobotRules.pm" ;;
            perl-uri)               _prel="URI.pm" ;;
            perl-gd)                _prel="GD.pm" ;;
            perl-dbi)               _prel="DBI.pm" ;;
            perl-xml-parser)        _prel="XML/Parser.pm" ;;
            perl-xml-simple)        _prel="XML/Simple.pm" ;;
            perl-xml-sax)           _prel="XML/SAX.pm" ;;
            perl-xml-sax-base)      _prel="XML/SAX/Base.pm" ;;
            perl-xml-libxml)        _prel="XML/LibXML.pm" ;;
            perl-xml-namespacesupport) _prel="XML/NamespaceSupport.pm" ;;
            perl-xml-xpath)         _prel="XML/XPath.pm" ;;
            perl-html-parser)       _prel="HTML/Parser.pm" ;;
            perl-html-tagset)       _prel="HTML/Tagset.pm" ;;
            perl-html-tree)         _prel="HTML/Tree.pm" ;;
            perl-http-date)         _prel="HTTP/Date.pm" ;;
            perl-http-message)      _prel="HTTP/Message.pm" ;;
            perl-http-cookies)      _prel="HTTP/Cookies.pm" ;;
            perl-http-daemon)       _prel="HTTP/Daemon.pm" ;;
            perl-http-negotiate)    _prel="HTTP/Negotiate.pm" ;;
            perl-net-http)          _prel="Net/HTTP.pm" ;;
            perl-file-listing)      _prel="File/Listing.pm" ;;
            perl-encode-locale)     _prel="Encode/Locale.pm" ;;
            perl-datemanip)         _prel="Date/Manip.pm" ;;
            *)
                _mod="${dep#perl-}"
                _prel=""
                IFS='-' read -r -a _segs <<< "$_mod"
                for _s in "${_segs[@]}"; do
                    _c="${_s:0:1}"; _c="${_c^^}"
                    _prel="${_prel}${_c}${_s:1}/"
                done
                _prel="${_prel%/}.pm" ;;
        esac
        for _pl in "$rootfs"/usr/lib/perl*/*/site_perl "$rootfs"/usr/lib/perl*/*/vendor_perl; do
            [[ -d "$_pl" ]] || continue
            [[ -f "$_pl/$_prel" || -d "$_pl/${_prel%.pm}" ]] && return 0
        done
        return 1
    fi
    return 1
}

# Zależności buildowe, których pre-flight NIE może sensownie „zaprojektować”
# (brak binarki/.pc/nagłówka/lib o tej samej nazwie) albo które bazowy builder
# dostarcza zawsze (system/base) lub które generują tylko dokumentację.
# Bez tej listy --strict-deps produkowałby FAŁSZYWIE twarde błędy (np.
# linux-api-headers: nagłówki siedzą w /usr/include/linux, nie w
# /usr/include/linux-api-headers) i psuł buildy, które wcześniej działały.
PROBE_SKIP="linux-api-headers kernel-headers glibc libc gcc g++ cc make cmake meson ninja binutils ld ar coreutils bash sh tar gzip xz bzip2 findutils grep sed gawk awk diffutils patch file m4 perl python3 python pkg-config pkgconf autoconf automake libtool flex bison gettext makeinfo which util-linux git texinfo"
# Narzędzia DOC generują tylko dokumentację – build zwykle działa bez nich
# (np. librsvg z -Ddocs=disabled), a łańcuch ich zależności blokuje kolejki.
PROBE_SKIP+=" gi-docgen gtk-doc doxygen help2man itstool yelp-tools gnome-doc-utils python-docutils python-smartypants"

if [[ "${PAGBUILD_NO_PROBE:-}" != "1" ]]; then
    echo "🩺 Pre-flight: sprawdzam makedepends w rootfs (heurystyka: binarka/.pc/nagłówek/lib)..."
    MISSING_MAKEDEPS=()
    for _dep in $PKG_MAKEDEPENDS; do
        [[ -z "$_dep" ]] && continue
        if [[ " $PROBE_SKIP " == *" $_dep "* ]]; then
            echo "   ⏭  makedepends: $_dep (zawsze w builderze / nieprobowalne – pomijam)"
            continue
        fi
        if probe_rootfs_dep "$_dep" "$ROOTFS"; then
            echo "   ✅ makedepends: $_dep"
        else
            echo "   ⚠️  makedepends: $_dep – BRAK w rootfs"
            MISSING_MAKEDEPS+=("$_dep")
        fi
    done
    if [[ ${#MISSING_MAKEDEPS[@]} -gt 0 ]]; then
        echo "   ℹ️  Brakujące w rootfs: ${MISSING_MAKEDEPS[*]}"
        echo "      → pagsync --auto-deps dobuduje je, jeśli istnieją receptury;"
        echo "        w przeciwnym razie: pagsync --install <pakiet> albo zbuduj je najpierw."
        if $STRICT_DEPS; then
            echo "❌ --strict-deps: brakujące makedepends: ${MISSING_MAKEDEPS[*]}"
            exit 1
        fi
    fi
fi

# ── Pomocnicze: pobieranie + walidacja źródeł ──
fetch_source() {
    local url="$1" dest="$2" tmp
    tmp="${dest}.tmp"
    rm -f "$tmp"
    echo "📥 Pobieranie: $url"
    if curl -fsSL --retry 3 --connect-timeout 20 --max-time 900 "$url" -o "$tmp" && mv -f "$tmp" "$dest"; then
        return 0
    fi
    rm -f "$tmp"
    return 1
}

# Sprawdza magic bytes – odrzuca HTML/niekompletne pobrania w cache
is_valid_archive() {
    local f="$1" ext magic
    [[ -f "$f" ]] || return 1
    ext="${f##*.}"
    case "$ext" in
        gz|tgz|xz|txz|bz2|tbz2|zip) ;;
        *) return 0 ;;   # nie-archiwum – bez walidacji
    esac
    magic=$(head -c 8 "$f" | od -An -tx1 | tr -d ' \n')
    case "$ext" in
        gz|tgz)   [[ "$magic" == 1f8b* ]] ;;
        xz|txz)   [[ "$magic" == fd377a585a00* ]] ;;
        bz2|tbz2) [[ "$magic" == 425a68* ]] ;;
        zip)      [[ "$magic" == 504b0304* ]] ;;
    esac
}

extract_fail() {
    echo "❌ Błąd rozpakowywania: $1"
    echo "   Plik w cache jest prawdopodobnie uszkodzony."
    echo "   Usuń: rm -f \"$1\""
    echo "   i uruchom pagbuild ponownie – źródło zostanie pobrane od nowa."
    exit 1
}

# ── 1. Pobieranie / Cache Źródeł ──
mkdir -p "$SRC_CACHE" "$OUT_DIR" "$BUILD_WORK" "$DIAG_DIR"

CACHED_SRCS=()
CACHED_URLS=()  # równoległa lista URL-i (do auto-naprawy uszkodzonego cache)
for ((i=0; i<SRC_COUNT; i++)); do
    SRC_URL_VAR="SRC_URL_$i"
    SRC_URL="${!SRC_URL_VAR}"

    # Podstawianie ${pkgver}/${pkgname}/${pkgrel} w URL
    SRC_URL=$(echo "$SRC_URL" \
        | sed "s/\${pkgver}/$PKG_VER/g" | sed "s/\$pkgver/$PKG_VER/g" \
        | sed "s/\${pkgname}/$PKG_NAME/g" | sed "s/\$pkgname/$PKG_NAME/g" \
        | sed "s/\${pkgrel}/$PKG_REL/g" | sed "s/\$pkgrel/$PKG_REL/g")

    # Mirror fallback – gdy główny serwer blokuje IP.
    # Mirror w mapie wskazuje korzeń drzewa (może mieć prefiks np. /gnu);
    # odejmujemy ten prefiks od ścieżki źródła, by uniknąć zdublowania.
    MIRROR_URLS=()
    for _host in "${!MIRRORS[@]}"; do
        [[ "$SRC_URL" == *"$_host"* ]] || continue
        _orig_path="${SRC_URL#*"$_host"}"        # np. /gnu/bash/bash-5.2.37.tar.gz
        for _m in ${MIRRORS[$_host]}; do
            _m_no_scheme="${_m#*://}"
            if [[ "$_m_no_scheme" == */* ]]; then
                _m_path="/${_m_no_scheme#*/}"     # np. /gnu
            else
                _m_path=""                        # mirror bez prefiksu (host-root)
            fi
            if [[ -z "$_m_path" ]]; then
                MIRROR_URLS+=("$_m$_orig_path")
            elif [[ "$_orig_path" == "$_m_path"* ]]; then
                MIRROR_URLS+=("$_m${_orig_path#"$_m_path"}")
            fi
        done
    done
    if [[ ${#MIRROR_URLS[@]} -eq 0 ]]; then
        _m_url=$(echo "$SRC_URL" | sed 's|https://ftp\.gnu\.org/|https://mirrors.kernel.org/|')
        [[ "$_m_url" != "$SRC_URL" ]] && MIRROR_URLS+=("$_m_url")
    fi

    # Źródła git – nie są archiwami; klonujemy je bezpośrednio do drzewa źródeł
    # (receptury robią `cd $name-$version`; alias w fazie 3 dopasuje katalog).
    # URL może mieć PREFIX git+ oraz FRAGMENT #... (dyrektywy: recursive,
    # branch=, tag=, commit=/rev=) – fragment NIE jest częścią adresu do
    # pobrania, więc test "kończy się na .git" robimy na URL-u bez fragmentu
    # (malcontent.git#recursive wcześniej spadał do ścieżki archiwum i curl
    # ściągał stronę HTML GitLaba jako plik "malcontent.git#recursive").
    _git_base="${SRC_URL%%#*}"
    _git_frag=""
    [[ "$SRC_URL" == *"#"* ]] && _git_frag="${SRC_URL#*#}"
    if [[ "$_git_base" == git+* || "$_git_base" == *.git ]]; then
        CACHED_SRCS+=("GIT:$SRC_URL")
        CACHED_URLS+=("$SRC_URL")
        continue
    fi

    ARCHIVE_NAME=$(basename "$SRC_URL" | sed 's/?.*//')
    # URL-e z query stringiem (np. `git archive?branch=main`) dają basename bez
    # rozszerzenia ("archive") – cache i unpack nie rozpoznałyby archiwum.
    # Weź realną nazwę pliku z nagłówka Content-Disposition (np. pag-main.tar.gz).
    # TYLKO dla prawdziwych URL-i (z ://): lokalne pliki (patche/konfigi) nie są
    # URL-ami i curl by je próbował "pobrać" jako host -> rc 6 (cały build pada).
    if [[ "$SRC_URL" == *"://"* && "$ARCHIVE_NAME" != *.tar.* && "$ARCHIVE_NAME" != *.tgz \
          && "$ARCHIVE_NAME" != *.tbz2 && "$ARCHIVE_NAME" != *.txz \
          && "$ARCHIVE_NAME" != *.zip ]]; then
        # `awk {print $2; exit}` zamyka potok, zanim curl skończy pisać nagłówki
        # → curl dostaje SIGPIPE → przy `set -o pipefail` cały pagbuild pada 141
        # (np. cargo-c: drugie źródło Cargo.lock). `|| true` połyka ten błąd.
        _cdisp=$(curl -fsSI --max-time 20 "$SRC_URL" 2>/dev/null \
                 | tr -d '\r' \
                 | awk -F'filename=' '/[Cc]ontent-[Dd]isposition/ {print $2; exit}') || true
        if [[ -n "$_cdisp" ]]; then
            _cdisp="${_cdisp%%;*}"                 # odetnij ewentualny dalszy parametr
            _cdisp="${_cdisp#\"}"; _cdisp="${_cdisp%\"}"   # zdejmij cudzysłowy
            [[ -n "$_cdisp" ]] && ARCHIVE_NAME=$(basename "$_cdisp")
        fi
    fi
    # Kolizje cache: archiwa tagow GitHub maja generyczne nazwy (v2.4.0.tar.gz,
    # 0.15.0.tar.gz) – dwa rozne pakiety z tym samym tagiem ladowaly do JEDNEGO
    # pliku w cache (np. containerd i uthash: v2.4.0.tar.gz, wygrywal ten, ktory
    # pobral sie pierwszy). Haszujemy TYLKO nazwy „tagowe” (czysty numer wersji,
    # np. v2.4.0.tar.gz, 0.15.0.tar.gz, 2.0.0-beta2.tar.gz) – dodajemy 8-znakowy
    # skrot URL do nazwy pliku w cache. Nazwy z literami (glibmm-2.66.10.tar.gz)
    # sa praktycznie unikalne i zostaja bez zmian (istniejacy cache dziala dalej).
    if [[ "$SRC_URL" == *"://"* ]]; then
        _arch_lc="${ARCHIVE_NAME,,}"
        _pkg_lc="${PKG_NAME,,}"
        if [[ "$_arch_lc" != *"$_pkg_lc"* \
              && "$ARCHIVE_NAME" =~ ^v?[0-9][0-9a-zA-Z._-]*\.(tar\.(gz|xz|bz2)|tgz|zip)$ ]]; then
            _urlhash=$(printf '%s' "$SRC_URL" | sha1sum | cut -c1-8)
            CACHED_SRC="$SRC_CACHE/${_urlhash}-${ARCHIVE_NAME}"
        else
            CACHED_SRC="$SRC_CACHE/$ARCHIVE_NAME"
        fi
    else
        CACHED_SRC="$SRC_CACHE/$ARCHIVE_NAME"
    fi
    CACHED_SRCS+=("$CACHED_SRC")
    CACHED_URLS+=("$SRC_URL")

    # Pliki lokalne z katalogu receptury (patche, configi, ...) – nie są URL-ami.
    if [[ "$SRC_URL" != *"://"* ]]; then
        local_src="$(dirname "$RECIPE_FILE")/$SRC_URL"
        if [[ -f "$local_src" ]]; then
            cp -f "$local_src" "$CACHED_SRC"
            echo "📎 Plik lokalny: $SRC_URL"
        else
            echo "⚠️  Brak lokalnego pliku źródłowego: $SRC_URL (szukano w katalogu receptury)"
        fi
        continue
    fi

    if [[ -n "$SRC_URL" ]]; then
        if [[ ! -f "$CACHED_SRC" ]]; then
            if ! fetch_source "$SRC_URL" "$CACHED_SRC"; then
                _downloaded=0
                for _m in "${MIRROR_URLS[@]}"; do
                    echo "🔄 Próbuję mirror: $_m"
                    if fetch_source "$_m" "$CACHED_SRC"; then
                        _downloaded=1
                        break
                    fi
                done
                if [[ "$_downloaded" -ne 1 ]]; then
                    echo "❌ Błąd: nie udało się pobrać $ARCHIVE_NAME z żadnego źródła."
                    echo "   URL: $SRC_URL"
                    echo "   Sprawdź łączność lub wgraj plik ręcznie do: $SRC_CACHE"
                    exit 1
                fi
            fi
            # Świeże pobranie też waliduj – serwery potrafią zwrócić stronę
            # HTML (404/redirect) zamiast pliku; taki "archiwum" nie ma sensu.
            if ! is_valid_archive "$CACHED_SRC"; then
                echo "⚠️  $ARCHIVE_NAME to nie jest poprawne archiwum (serwer zwrócił HTML/404?)"
                rm -f "$CACHED_SRC"
                _downloaded=0
                for _m in "${MIRROR_URLS[@]}"; do
                    echo "🔄 Próbuję mirror: $_m"
                    if fetch_source "$_m" "$CACHED_SRC" && is_valid_archive "$CACHED_SRC"; then
                        _downloaded=1
                        break
                    fi
                    rm -f "$CACHED_SRC"
                done
                if [[ "$_downloaded" -ne 1 ]]; then
                    echo "❌ Błąd: $ARCHIVE_NAME nie jest poprawnym archiwum (HTML/404 zamiast pliku)."
                    echo "   URL: $SRC_URL"
                    exit 1
                fi
            fi
        elif ! is_valid_archive "$CACHED_SRC"; then
            echo "⚠️  Uszkodzony plik w cache: $ARCHIVE_NAME (to nie jest poprawne archiwum)"
            echo "   Usuwam i pobieram ponownie..."
            rm -f "$CACHED_SRC"
            if ! fetch_source "$SRC_URL" "$CACHED_SRC"; then
                echo "❌ Błąd: nie udało się pobrać $ARCHIVE_NAME."
                exit 1
            fi
        else
            echo "⚡ Cache: $ARCHIVE_NAME"
        fi

        SHA_VAR="SRC_SHA_$i"
        EXPECTED_SHA="${!SHA_VAR:-}"
        if [[ -n "$EXPECTED_SHA" && "$EXPECTED_SHA" =~ ^[0-9a-f]{64}$ ]]; then
            echo "🔐 Weryfikacja SHA256..."
            echo "$EXPECTED_SHA  $CACHED_SRC" | sha256sum -c - || {
                echo "❌ Błąd: Suma SHA256 się nie zgadza dla $ARCHIVE_NAME!"
                echo "   Oczekiwano: $EXPECTED_SHA"
                ACTUAL_SHA=$(sha256sum "$CACHED_SRC" | cut -d' ' -f1)
                echo "   Faktyczna:  $ACTUAL_SHA"
                echo "   ⚠️  Usuwam uszkodzony plik i pobieram ponownie..."
                rm -f "$CACHED_SRC"
                if ! fetch_source "$SRC_URL" "$CACHED_SRC"; then
                    echo "❌ Błąd: nie udało się ponownie pobrać $ARCHIVE_NAME."
                    echo "   Uruchom: pagsync --fix-sha"
                    exit 1
                fi
                # Weryfikuj ponownie po pobraniu
                echo "$EXPECTED_SHA  $CACHED_SRC" | sha256sum -c - || {
                    echo "❌ Błąd fix SHA: Suma nadal się nie zgadza."
                    echo "   Uruchom: pagsync --fix-sha"
                    rm -f "$CACHED_SRC"
                    exit 1
                }
                echo "✅ SHA256 OK po ponownym pobraniu."
            }
        elif [[ -n "$EXPECTED_SHA" ]]; then
            if [[ "$EXPECTED_SHA" =~ ^[Ss][Kk][Ii][Pp] ]]; then
                echo "⏭️  SHA256: SKIP (receptura nie podała sumy – pomijam weryfikację)"
            else
                echo "⚠️  SHA256 pominięty (nieprawidłowy format: $EXPECTED_SHA) – uruchom pagsync --fix-sha"
            fi
        fi
    fi
done

# ── 2. Przygotowanie miejsca pracy ──
WORK_DIR="$BUILD_WORK/$FULL_NAME"
DESTDIR="$WORK_DIR/destdir"
rm -rf "$WORK_DIR"
mkdir -p "$WORK_DIR/src" "$DESTDIR"

# Rozpakowywanie źródeł
unpack_archive() {
    local f="$1" dest="$2"
    case "$f" in
        *.tar.gz|*.tgz)   tar -xzf "$f" -C "$dest" ;;
        *.tar.bz2|*.tbz2) tar -xjf "$f" -C "$dest" ;;
        *.tar.xz|*.txz)   tar -xJf "$f" -C "$dest" ;;
        *.tar)            tar -xf "$f" -C "$dest" ;;
        # Formaty obsługiwane przez bsdtar (zip, lzip, rpm, 7z) – tak samo
        # rozpakowuje je pkgmk cards (receptury zakładają rozpakowane źródła).
        *.zip|*.tar.lz|*.tar.lzma|*.rpm|*.7z)
            bsdtar -p -o -C "$dest" -xf "$f" ;;
        *)                cp "$f" "$dest/" ;;
    esac
}

for ((_ci=0; _ci<${#CACHED_SRCS[@]}; _ci++)); do
    cached="${CACHED_SRCS[$_ci]}"
    url="${CACHED_URLS[$_ci]:-}"
    if [[ "$cached" == GIT:* ]]; then
        git_spec="${cached#GIT:}"
        git_url="${git_spec%%#*}"
        git_frag=""
        [[ "$git_spec" == *"#"* ]] && git_frag="${git_spec#*#}"
        # git+ prefix – opcjonalny znacznik VCS (konwencja makepkg) – zdejmij go
        if [[ "$git_url" == git+* ]]; then
            git_url="${git_url#git+}"
        fi
        _git_dir="${git_url##*/}"
        _git_dir="${_git_dir%.git}"
        _git_dir="${_git_dir%%\?*}"

        declare -a _gargs=(--depth 1)
        _grec=0; _grev=""
        if [[ -n "$git_frag" ]]; then
            IFS='&,' read -ra _dirs <<< "$git_frag"
            for _d in "${_dirs[@]}"; do
                case "$_d" in
                    recursive|submodules) _grec=1 ;;
                    no-recursive)         _grec=0 ;;
                    branch=*|tag=*)       _gargs+=(-b "${_d#*=}") ;;
                    commit=*|rev=*)       _grev="${_d#*=}" ;;
                    "")                   : ;;
                    *) echo "⚠️  Nieznana dyrektywa źródła git: $_d (ignoruję)" >&2 ;;
                esac
            done
        else
            _grec=1   # bez fragmentu zachowujemy dotychczasowe --recursive
        fi
        [[ $_grec -eq 1 ]] && _gargs+=(--recursive)

        if [[ -n "$_grev" ]]; then
            # konkretny commit: płytki clone (gałąź domyślna), potem dobierz SHA
            echo "📦 Klonowanie: $git_url (fragment: ${git_frag}) → checkout $_grev"
            (cd "$WORK_DIR/src" && git clone "${_gargs[@]}" "$git_url") || {
                echo "❌ Błąd klonowania: $git_url"; exit 1
            }
            (cd "$WORK_DIR/src/$_git_dir" \
                && git fetch --depth 1 origin "$_grev" \
                && git checkout FETCH_HEAD) || {
                echo "❌ Nie udało się pobrać commita $_grev z $git_url"; exit 1
            }
        else
            echo "📦 Klonowanie: $git_url (fragment: ${git_frag:-brak})"
            (cd "$WORK_DIR/src" && git clone "${_gargs[@]}" "$git_url") || {
                echo "❌ Błąd klonowania: $git_url"; exit 1
            }
        fi
        continue
    fi
    if [[ -f "$cached" ]]; then
        echo "📦 Rozpakowywanie: $(basename "$cached")"
        # Skopiuj oryginalne archiwum do workspace OPRÓCZ rozpakowania –
        # receptury, które same rozpakowują źródła (np. go robi
        # `bsdtar -xf ./goX.linux-amd64.tar.gz`), znajdą plik w bieżącym
        # katalogu zamiast padać na 'Failed to open ...'.
        _arch_base="$(basename "$cached")"
        case "$_arch_base" in
            *.tar.gz|*.tgz|*.tar.bz2|*.tbz2|*.tar.xz|*.txz|*.tar|*.zip|*.tar.lz|*.tar.lzma|*.rpm|*.7z)
                cp -f "$cached" "$WORK_DIR/src/" 2>/dev/null || true
                ;;
        esac
        if ! unpack_archive "$cached" "$WORK_DIR/src"; then
            # Auto-naprawa: usuń uszkodzony plik, pobierz ponownie, spróbuj raz
            # jeszcze – zamiast kazać użytkownikowi ręcznie rm + ponowny build.
            echo "⚠️  Uszkodzone źródło w cache: $(basename "$cached")"
            rm -f "$cached"
            if [[ "$url" == *"://"* ]] && fetch_source "$url" "$cached"; then
                echo "🔁 Pobrano ponownie, próbuję rozpakować..."
                if unpack_archive "$cached" "$WORK_DIR/src"; then
                    continue
                fi
            fi
            extract_fail "$cached"
        fi
    fi
done

# ── 3. Generowanie skryptu budowania z faz build i package ──
BUILD_SCRIPT="$WORK_DIR/build.sh"

# Nie przekazuj generatora jako python3 -c w podwójnych cudzysłowach. Shell
# rozwija wtedy fragmenty receptury (np. $(...), $VAR i backticki) zanim
# Python je zobaczy, przez co generator kończył się bez utworzenia build.sh.
python3 - "$RECIPE_FILE" "$BUILD_SCRIPT" "$ROOTFS" "$SHORT_NAME" "$CHROOT_WORK" <<'PY'
import os
import re
import shlex
import sys
import yaml

recipe_file, build_script, rootfs, short_name, chroot_work = sys.argv[1:]
with open(recipe_file, encoding="utf-8") as f:
    data = yaml.safe_load(f) or {}

build_phase = data.get("build", "")
package_phase = data.get("package", "")
if isinstance(build_phase, list):
    build_phase = "\n".join(str(x) for x in build_phase)
if isinstance(package_phase, list):
    package_phase = "\n".join(str(x) for x in package_phase)

# Wiele upstreamów odrzuca literalne "disabled"; PAGBUILD używa boolowskich.
# UWAGA: mesonowe opcje typu "feature" (tri-state: enabled/disabled/auto) NIE są
# boolami – zamiana disabled→false wywala setup (np. glib -Dintrospection=disabled:
# "Value false is not one of the choices. Possible choices: enabled, disabled, auto").
# Dla znanych opcji feature zostawiamy literalne "disabled"; resztę zamieniamy na =false.
_meson_feature_opts = {
    "introspection", "docs", "doc", "man-pages", "man", "gtk_doc", "gi-docgen",
    # gegl (0.4.x): opcje feature mrg/maxflow (dodatkowe silniki) – nie boole
    "mrg", "maxflow",
    "docbook_docs", "docbook-xsl",
    "examples", "installed_tests", "unit_tests", "tests", "validate", "doxygen",
    "help", "bash_completion", "systemtap", "dtrace",
    # typowe mesonowe opcje feature (tri-state: enabled/disabled/auto) – częsty
    # wzorzec w recepturach: -Dx11=disabled, -Dwayland=disabled itd.
    "x11", "wayland", "quartz", "win32", "cocoa", "directfb", "sdl2", "sdl",
    "gles2", "opengl", "vulkan", "egl", "gbm", "vaapi", "vdpau", "cups",
    "dbus", "avahi", "systemd", "elogind", "polkit", "xcb", "xkb", "xkbcommon",
    "xinerama", "randr", "xft", "xrender", "xdamage", "xfixes", "xcomposite",
    "xcursor", "gtk", "gtk2", "gtk3", "gtk4", "qt", "qt5", "qt6",
    "cairo", "pango", "freetype", "harfbuzz", "icu", "nls", "locale", "threads",
    "zlib", "bzip2", "lzma", "liblzma", "ssl", "openssl", "libcurl", "curl",
    "libxml2", "libxslt", "python", "python3", "perl", "ruby", "java", "go",
    "rust", "vala", "gudev", "udev", "libudev", "pciutils", "seccomp", "audit",
    "selinux", "apparmor", "acl", "attr", "libcap", "capabilities", "journald",
    "gnome", "kde", "benchmark", "daemon", "inotify", "fam", "smbios", "dmi",
    "server", "client", "gui",
    "xwayland", "icon", "color-management", "colour-management",
    "profiler", "profiling", "trace", "tracing", "debug", "debuginfo",
    "testsuite", "test-suite", "static", "shared", "bundle", "bundled",
    "ldap", "kerberos", "gssapi", "libsecret", "keyring",
    "gstreamer", "gst", "ffmpeg", "libav", "pulse", "pulseaudio", "alsa",
    "jack", "pipewire", "codec2", "vorbis", "opus", "flac", "mp3", "lame",
    "theora", "webp", "jpeg", "png", "tiff", "gif", "svg", "rsvg", "bmp",
    "jxl", "heif", "avif", "raw", "exif", "exiv2", "openexr", "webengine",
    "webkit", "webkit2gtk", "geoclue", "bluetooth", "bluez", "networkmanager",
    "nm", "wifi", "ethernet", "modemmanager", "usb", "libusb", "hidapi",
    "evdev", "libinput", "mtdev", "wacom", "libwacom", "systemd-journal",
    "logind", "upower", "udisks2", "gvfs", "gphoto2", "colord", "lcms", "lcms2",
    "libjpeg", "libpng", "libtiff", "libwebp", "libjxl", "librsvg", "libgsf",
    "brotli", "zstd", "lz4", "snappy", "lzo", "libarchive", "libzip", "xz",
    "gzip", "libdeflate", "nettle", "gcrypt", "gnutls", "mbedtls", "nss",
    "libsodium", "argon2", "pam", "shadow", "sasl", "krb5", "expat",
    "libyaml", "json-glib", "jsoncpp", "jansson", "protobuf", "grpc", "thrift",
    "flatbuffers", "msgpack", "gettext", "libintl", "iconv", "libiconv",
    "libunistring", "libidn", "libidn2", "libpsl", "libproxy", "libsoup",
    "libsoup2", "libsoup3", "rest", "librest", "oauth", "uuid", "libuuid",
    # mesa (gallium/vulkan) – opcje feature, NIE boole: =disabled musi zostać
    # literalne, inaczej meson: "Value false is not one of the choices"
    "microsoft-clc", "libunwind", "android-libbacktrace", "gles1", "gles",
    "gallium-extra-hud", "amdgpu-virtio",
    "gallium-rusticl", "rusticl", "video-codecs", "zink", "lavapipe",
    "opencl-icd", "osmesa", "selinux_module", "selinux-module", "libdrm",
    "libdrm-radeon", "libdrm-amdgpu", "libdrm-intel", "libdrm-nouveau",
    "libdrm-omap", "libdrm-freedreno", "libdrm-etnaviv", "libdrm-vc4",
    "cryptsetup", "lvm2", "device-mapper", "mdadm", "btrfs", "xfs", "nfs",
    "cifs", "smb", "dns", "mdns", "dnssd", "zeroconf", "upnp", "ssdp", "dlna",
    "displayport", "hdmi", "dp", "dri", "dri3", "present", "glx", "opencl",
    "llvm", "clang", "asm", "nasm", "yasm", "intel", "arm", "aarch64",
    "arm64", "x86", "x86_64", "i386", "i686", "ppc", "ppc64", "s390",
    "s390x", "riscv", "riscv64", "mips", "mips64", "loongarch", "loong64",
    "sparc", "sparc64", "hppa", "m68k", "sh", "sh4", "xtensa", "arc",
    "microblaze", "nios2", "e2k", "vax", "bpf", "mmix", "pdp11", "systemz",
}
def _meson_disabled(m):
    if m.group(2) in _meson_feature_opts:
        return m.group(0)
    return m.group(1) + "=false"
build_phase = re.sub(r"(-D([\w.-]+))=disabled\b", _meson_disabled, str(build_phase))
package_phase = re.sub(r"(-D([\w.-]+))=disabled\b", _meson_disabled, str(package_phase))

rootfs_path = os.path.abspath(rootfs)
pkgver = shlex.quote(str(data.get("pkgver", "")))
pkgname = shlex.quote(str(data.get("pkgname", "")))
pkgrel = shlex.quote(str(data.get("pkgrel", 1)))
pkgurl = shlex.quote(str(data.get("url", "")))

# SSL: python/urllib (pobieranie wrapow meson, pip, git https) w chroot nie widzi
# certyfikatow CA – wbudowany openssl szuka /etc/ssl/cert.pem, ktorego w rootfs
# nie ma (jest debianowy /etc/ssl/certs/ca-certificates.crt). Efekt: meson pada
# na CERTIFICATE_VERIFY_FAILED przy pobieraniu subprojektow z wrapdb/github.
# Wskaz bundle wprost, jesli istnieje w rootfs (sciezka WEWNATRZ chroota).
ssl_exports = ""
if os.path.exists(os.path.join(rootfs_path, "etc/ssl/certs/ca-certificates.crt")):
    ssl_exports = (
        "export SSL_CERT_FILE=/etc/ssl/certs/ca-certificates.crt\n"
        "export SSL_CERT_DIR=/etc/ssl/certs\n"
    )

script = f'''#!/bin/bash
set -eo pipefail
# Przy błędzie pokaż numer LINII i dokładne polecenie z receptury, które padło
# (np. `cd -1` zamiast tajemniczego "cd: invalid option" 30 linii dalej).
trap 'echo "❌ Błąd skryptu builda w linii $LINENO: $BASH_COMMAND" >&2' ERR
export PATH=/usr/bin:/usr/sbin:/bin:/sbin:/tools/bin
# gtk-doc/xsltproc (np. xfconf --enable-gtk-doc) importuje docbook-xsl przez
# URLe (docbook.sourceforge.net – martwy od lat). Bez jawnego katalogu XML
# libxml2 IGNORUJE /etc/xml/catalog i próbuje pobierać z sieci -> build pada.
export XML_CATALOG_FILES=/etc/xml/catalog
export PKG_CONFIG_PATH=/usr/lib/pkgconfig:/usr/lib64/pkgconfig:/usr/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig
export PKG_CONFIG_LIBDIR=/usr/lib/pkgconfig:/usr/lib64/pkgconfig:/usr/share/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig
export CMAKE_PREFIX_PATH={rootfs_path}/usr:{rootfs_path}
export CMAKE_POLICY_VERSION_MINIMUM=3.5
# Starsze projekty z cmake_minimum_required < 3.5 wymagają jawnej zgody
# na współczesne polityki CMake (>=3.5) – inaczej konfiguracja się wywala.
# UWAGA: celowo NIE preselectujemy CC/CXX/AR/RANLIB/GREP/SED jako zmiennych
# środowiskowych. Część configurów (np. cdparanoia) pakuje całą detekcję
# kompilatora (w tym ustawienie ac_objext/OBJEXT) w `if test -z "$CC"` – przy
# zadanym CC pomijały tę sekcję i WSZYSTKIE testy configure padały ("No 16 bit
# type found"). Podobnie grep 3.12 nie akceptuje preselectowanego GREP. Ścieżki
# PATH (/usr/bin) i tak wskazują te same binarki.
export LIBRARY_PATH={rootfs_path}/usr/lib/x86_64-linux-gnu:{rootfs_path}/usr/lib:{rootfs_path}/lib/x86_64-linux-gnu:{rootfs_path}/lib:/usr/lib/x86_64-linux-gnu:/usr/lib:/lib/x86_64-linux-gnu:/lib
export FORCE_UNSAFE_CONFIGURE=1
export CFLAGS="-std=gnu17 -Wno-error=implicit-function-declaration -Wno-error=int-conversion -Wno-error=return-type -Wno-error=maybe-uninitialized -Wno-error=incompatible-pointer-types -Wno-error=discarded-qualifiers -Wno-error=implicit-int"
export CXXFLAGS="-Wno-error=return-type -Wno-error=maybe-uninitialized"
export CPPFLAGS=""
export LANG=C.UTF-8
export HOME=/root
export CONFIG_SHELL=/bin/bash
export SHELL=/bin/bash
# Nie pozwalaj configure pobierać ustawień z systemowego config.site.
# /dev/null nie jest poprawną wartością: autoconf próbuje wykonać ten plik
# jako skrypt site i zgłasza błąd. Nieistniejąca ścieżka pomija site script.
export CONFIG_SITE=/nonexistent/pagan-config.site
{ssl_exports}export PKGDIR={chroot_work}/destdir-{short_name}
# UWAGA: celowo NIE eksportujemy DESTDIR jako zmiennej środowiskowej. Receptury
# przekazują je jawnie (make DESTDIR=${{PKGDIR}} install / prefix=${{PKGDIR}}/usr),
# a GNU make i tak dokłada `$(DESTDIR)` do każdego `$(prefix)`/`$(sysconfdir)`.
# Eksport DESTDIR powodował PODWÓJNIE zagnieżdżone ścieżki instalacji
# (destdir/var/cache/.../destdir/usr) w recepturach `prefix=${{PKGDIR}}/usr`
# (elfutils, libvdpau, ...).
# Kontener źródeł (katalog, w którym rozpakowano archiwa). Receptury mogą go
# używać do bezpiecznego `cd` do katalogu SĄSIADA (np. pagan-scripts-*) z
# dowolnej głębokości drzewa – zamiast kruchych względnych ścieżek.
export SRCDIR={chroot_work}/src-{short_name}
export pkgver={pkgver}
export pkgname={pkgname}
export pkgrel={pkgrel}
export url={pkgurl}

cd {chroot_work}/src-{short_name}
# Usuń ewentualny cache odziedziczony ze źródeł albo poprzedniej próby.
find . -type f -name config.cache -delete 2>/dev/null || true
# Odśwież cache dynamicznego loadera PRZED budową. Uwzględnia to pakiety
# rozpakowane do rootfs przez STARSZY pagbuild (bez wywołania ldconfig),
# np. libpkgconf.so.6 po zbudowaniu pkgconf – inaczej configure pada na
# "pkg-config ...: cannot open shared object file".
ldconfig >/dev/null 2>&1 || true
# pip w chroot: wymuś instalację do PKGDIR nawet gdy pakiet „już jest”
# w env (resztki po purgowanych/aktualizowanych pakietach w site-packages
# powodowały „Requirement already satisfied” i PUSTE .pag) i nie rozwiązuj
# zależności (no-deps) – deps są w depends/makedepends i budowane osobno.
mkdir -p /etc
printf '[global]\nignore-installed = true\nno-deps = true\n' > /etc/pip.conf
echo '═══ FAZA BUILD ═══'
{build_phase}

echo '═══ FAZA PACKAGE ═══'
cd {chroot_work}/src-{short_name}
{package_phase}

echo '═══ GOTOWE ═══'
'''

with open(build_script, "w", encoding="utf-8") as out:
    out.write(script)
PY
# Generator Pythona nigdy nie powinien zakończyć się sukcesem bez zapisania
# build.sh. Jeżeli tak się stało (np. stary/niekompletny pagbuild albo błąd
# parsowania YAML), zakończ JAWNYM błędem zamiast mylącym 'chmod: No such file',
# które normale ukrywało przyczynę i przerywało całą natywną warstwę.
if [[ ! -s "$BUILD_SCRIPT" ]]; then
    echo "❌ Błąd: nie wygenerowano skryptu budowania '$BUILD_SCRIPT'."
    echo "   To zwykle oznacza błąd Pythona podczas parsowania $RECIPE_FILE."
    echo "   Sprawdź, czy rootfs ma aktualny pagbuild oraz że python3 posiada PyYAML."
    exit 1
fi
chmod +x "$BUILD_SCRIPT"

# Receptury często zakładają katalog równy nazwie archiwum. Jeśli upstream
# rozpakował inne pojedyncze drzewo (np. containerd-2.4.0 przy `cd ${pkgname}`
# albo icu-release-*), dodaj bezpieczny alias: symlink żądanej nazwy do
# JEDYNEGO rozpakowanego katalogu. Alias ${pkgname} powstaje ZAWSZE (pokrywa
# `cd ${pkgname}`, `cd $SRCDIR/$pkgname`, `make -C ${pkgname}` itd.).
# UWAGA: nie twórz aliasów dla GENERYCZNYCH nazw katalogów (build, builddir, ...)
# ani dla katalogów, które receptura tworzy sama (mkdir): symlink istniałby
# wcześniej i `mkdir` padał na "File exists" (highway, libjxl, ...).
python3 - "$WORK_DIR/src" "$BUILD_SCRIPT" <<'PY'
import re, os, sys
src, script = sys.argv[1:]
_GENERIC = {"build", "builddir", "bld", "src", "source", "sources", "work",
            "dist", "tmp", "out", "debug", "release", ".", ".."}

def _expand(w, pkgname="", pkgver="", pkgrel=""):
    # rozwiń ${pkgname}/${pkgver}/${pkgrel} w nazwie katalogu, np.
    # cd ${pkgname}-${pkgver} -> cd libaio-0.3.113
    w = w.replace("${pkgname}", pkgname).replace("$pkgname", pkgname)
    w = w.replace("${pkgver}", pkgver).replace("$pkgver", pkgver)
    w = w.replace("${pkgrel}", pkgrel).replace("$pkgrel", pkgrel)
    return w

try:
    text = open(script, encoding="utf-8").read()
    # wartości pkgname/pkgver/pkgrel z exportów w skrypcie – do rozwinięcia aliasu
    m = re.search(r"(?m)^export pkgname=(\S+)$", text)
    pkgname = m.group(1).strip("'") if m else ""
    m = re.search(r"(?m)^export pkgver=(\S+)$", text)
    pkgver = m.group(1).strip("'") if m else ""
    m = re.search(r"(?m)^export pkgrel=(\S+)$", text)
    pkgrel = m.group(1).strip("'") if m else ""
    dirs = [d for d in os.listdir(src) if os.path.isdir(os.path.join(src, d))]
    if not dirs:
        sys.exit(0)
    # Katalogi, które receptura tworzy sama (mkdir) – NIE wolno ich aliasować:
    # symlink istniałby przed `mkdir` i build padał na "File exists".
    mkdirs = set()
    for _line in text.splitlines():
        if not re.match(r"\s*mkdir\b", _line):
            continue
        for _tok in re.findall(r"""[^\s;"']+""", _line)[1:]:
            _tok = _expand(_tok, pkgname, pkgver, pkgrel).rstrip("/")
            if _tok and _tok not in _GENERIC and "*" not in _tok:
                mkdirs.add(_tok)

    # Analogicznie `git clone <url> <dest>` – katalog docelowy też powstanie
    # DOPIERO w trakcie builda. Alias-symlink zajmował miejsce i clone padał
    # na "destination path ... already exists" (np. nlohmann-json, gdzie
    # ${pkgname} aliasowano do jedynego rozpakowanego katalogu json_test_data).
    for _line in text.splitlines():
        _toks = _line.split()
        if len(_toks) >= 3 and _toks[0] == "git" and _toks[1] == "clone":
            _dst = _toks[-1]
            if (_dst.startswith("-") or "/" in _dst or ":" in _dst
                    or _dst.endswith(".git")):
                # opcje clone / URL / ścieżka / clone bez dest – nie analizuję
                continue
            _dst = _expand(_dst, pkgname, pkgver, pkgrel).rstrip("/")
            if _dst and _dst not in _GENERIC and "*" not in _dst:
                mkdirs.add(_dst)

    def _alias(wanted):
        wanted = _expand(wanted, pkgname, pkgver, pkgrel).rstrip("/")
        if (not wanted or wanted in _GENERIC or wanted in mkdirs
                or "*" in wanted or "$" in wanted or "/" in wanted
                or wanted.startswith("-") or "=" in wanted):
            # generyczne katalogi build, globy (cd texinfo-*), ścieżki z /,
            # opcje (-Dfoo=bar / --prefix) i własne mkdir receptury nie mogą
            # być aliasami.
            return
        target = os.path.join(src, wanted)
        if not os.path.exists(target) and len(dirs) == 1:
            os.symlink(dirs[0], target)
            print(f"🔗 Alias: {wanted} -> {dirs[0]}", file=sys.stderr)

    for wanted in re.findall(r"""(?m)^\s*cd\s+["']?([^"'\s;]+)""", text):
        _alias(wanted)
    # Katalog źródłowy w `meson setup <src> <build>` / `cmake -S <src>`: wiele
    # receptur (git checkout) pisze `meson setup ... ${pkgname}-${pkgver} builddir`
    # bez `cd` do katalogu, którego realnie nie ma (klon ma inną nazwę) – alias
    # do jedynego rozpakowanego katalogu naprawia to globalnie (loupe, gjs, ...).
    _CMD_WORDS = {"meson", "setup", "ninja", "cmake", "make", "install", "configure", "python3", "python"}
    for _line in text.splitlines():
        _toks = re.findall(r"""[^\s;"']+""", _line)
        if "meson" in _toks and "setup" in _toks:
            for _t in _toks:
                if _t not in _CMD_WORDS:
                    _alias(_t)
        for _i, _t in enumerate(_toks):
            if _t == "-S" and _i + 1 < len(_toks):
                _alias(_toks[_i + 1])
    # Automatyczny alias ${pkgname} -> jedyny rozpakowany katalog: pokrywa
    # `cd ${pkgname}`, `cd $SRCDIR/$pkgname`, `make -C ${pkgname}` itd., nawet
    # gdy nazwa katalogu upstream (np. containerd-2.4.0) różni się od pkgname.
    if pkgname:
        _alias(pkgname)
except OSError:
    pass
PY

# ── 4. Przygotowanie izolowanego workspace ──
# Rootfs jest stagingiem PaganOS, nie może być używany jako niepełna kopia
# systemu hosta: brakujące biblioteki systemowe powodowały kaskadę 128 błędów.
echo "🔨 Przygotowanie workspace buildera (rootfs: $ROOTFS)..."

if [[ ! -d "$ROOTFS/usr/bin" ]]; then
    echo "❌ Błąd: RootFS $ROOTFS nie istnieje lub jest niekompletny."
    exit 1
fi

# Autoconf używa /dev/null przy generowaniu config.status. Jeśli rootfs nie
# ma devtmpfs (np. ręczne uruchomienie), zwykły plik /dev/null "wchłania"
# wyjście diff i wstrzykuje je do config.status. Zatrzymaj się wprost.
if [[ "$ROOTFS" == "/" && ! -c /dev/null ]]; then
    echo "❌ Błąd: /dev/null nie jest urządzeniem znakowym."
    exit 1
fi

# Wiele upstream'owych Makefile'ów woła absolutną ścieżką /bin/true albo
# /bin/false jako "no-op" (np. STRIP=/bin/true, LDCONFIG=/usr/bin/true,
# /bin/false przy blokadach). PaganOS coreutils instaluje je TYLKO do
# /usr/bin (osobne /bin i /usr/bin, jak klasyczny LFS), więc bez tego
# build pada na "make: /bin/true: No such file or directory" (exit 127)
# mimo że katalogi instalacji (destdir) powstają poprawnie.
for _t in true false; do
    if [[ ! -e "$ROOTFS/bin/$_t" ]]; then
        mkdir -p "$ROOTFS/bin"
        if [[ -e "$ROOTFS/usr/bin/$_t" ]]; then
            ln -sf "../usr/bin/$_t" "$ROOTFS/bin/$_t"
        else
            if [[ "$_t" == false ]]; then
                printf '#!/bin/sh\nexit 1\n' > "$ROOTFS/bin/$_t"
            else
                printf '#!/bin/sh\nexit 0\n' > "$ROOTFS/bin/$_t"
            fi
            chmod 755 "$ROOTFS/bin/$_t"
        fi
        echo "➕ Zapewniono $ROOTFS/bin/$_t (brakowało w rootfs)"
    fi
done

# Legacy upstream'owe Makefile'y/RULES wołają absolutne ścieżki /bin/rm, /bin/cp,
# /bin/mkdir itp. (np. cdrtools). PaganOS to klasyczny LFS – osobne /bin i /usr/bin,
# coreutils są tylko w /usr/bin, więc taki build pada na "make: /bin/rm: No such file
# or directory". Zapewnij symlinki dla najczęściej używanych narzędzi.
for _t in rm cp mv mkdir ln ls cat chmod chown echo printf test touch \
          dirname basename sed grep head tail cut sort uniq wc find xargs \
          install strip tar gzip bzip2 xz unxz patch diff cmp date tr awk \
          expr which sleep kill pwd env dd sync df du stat readlink seq; do
    if [[ ! -e "$ROOTFS/bin/$_t" ]] && [[ -e "$ROOTFS/usr/bin/$_t" ]]; then
        ln -s "../usr/bin/$_t" "$ROOTFS/bin/$_t"
    fi
done

HOST_SRC_LINK="$CHROOT_WORK/src-$SHORT_NAME"
HOST_DEST_LINK="$CHROOT_WORK/destdir-$SHORT_NAME"

# ── 5. Wykonanie budowania ──
echo "⚙️ Wykonywanie kompilacji w izolowanym workspace (rootfs: $ROOTFS)..."
MOUNTED_FS=()
if [[ "$ROOTFS" != "/" ]]; then
    # Budowa wewnątrz chroot PaganOS – gwarantuje właściwy ABI (glibc/kompilator
    # PaganOS), a nie biblioteki hosta (np. Debian na VPS).
    rm -rf "$ROOTFS$CHROOT_WORK/src-$SHORT_NAME" "$ROOTFS$CHROOT_WORK/destdir-$SHORT_NAME"
    mkdir -p "$ROOTFS$CHROOT_WORK/src-$SHORT_NAME" "$ROOTFS$CHROOT_WORK/destdir-$SHORT_NAME"
    cp -a "$WORK_DIR/src/." "$ROOTFS$CHROOT_WORK/src-$SHORT_NAME/"
    cp "$BUILD_SCRIPT" "$ROOTFS$CHROOT_WORK/build-$SHORT_NAME.sh"
    # Autoconf wymaga /dev/null jako urządzenia znakowego; sporo buildów chce
    # też /proc i /sys (go, systemd, ...). Podpinamy, jeżeli jeszcze nie ma.
    # /dev montujemy rekurencyjnie (--rbind), żeby w chroot były też sub-mounty
    # /dev/pts i /dev/shm; --make-rslave odcina propagację zdarzeń mount między
    # chrootem a hostem (wzorzec jak w builder-chroot.sh).
    # Najpierw twórz punkty montowania – mount na nieistniejącej ścieżce cicho
    # zawodzi, a build rusza bez /proc. Po próbie WERYFIKUJEMY i sygnalizujemy.
    mkdir -p "$ROOTFS"/{dev,dev/pts,proc,sys}
    # Ustaw CAŁE drzewo mountów rootfs na rslave. Sam --make-rslave na wierzchu
    # NIE wystarcza: sub-mounty rbinda (np. /sys/fs/cgroup) dziedziczą po hoście
    # propagację SHARED, więc `umount -l $ROOTFS/sys` w trapie odbijał się na
    # hoście (znikał zamontowany cgroup2 z /sys/fs/cgroup!).
    _make_rslave_tree() {
        local _base="$1" _m
        while read -r _m; do
            mount --make-rslave "$_m" 2>/dev/null || true
        done < <(findmnt -R -n -o TARGET "$_base" 2>/dev/null || true)
    }
    for d in dev proc sys; do
        if ! mountpoint -q "$ROOTFS/$d" 2>/dev/null; then
            case "$d" in
                dev)
                    mount --rbind /dev "$ROOTFS/dev" 2>/dev/null || true
                    mount --make-rslave "$ROOTFS/dev" 2>/dev/null || true
                    _make_rslave_tree "$ROOTFS/dev"
                    ;;
                proc) mount -t proc proc "$ROOTFS/proc" 2>/dev/null || true ;;
                sys)
                    mount --rbind /sys "$ROOTFS/sys" 2>/dev/null || true
                    mount --make-rslave "$ROOTFS/sys" 2>/dev/null || true
                    _make_rslave_tree "$ROOTFS/sys"
                    ;;
            esac
            if mountpoint -q "$ROOTFS/$d" 2>/dev/null; then
                MOUNTED_FS+=("$ROOTFS/$d")
            else
                echo "⚠️  OSTRZEŻENIE: nie udało się zamontować $d ($ROOTFS/$d) – build może działać niepoprawnie!" >&2
            fi
        fi
    done
    # /dev/pts bywa nieobecne w chroot (np. gdy /dev było już zamontowane bez
    # sub-mountów). lld-wrapper i programy otwierające terminale wymagają
    # devpts – podpinamy wprost, jeśli nadal go brak.
    if ! mountpoint -q "$ROOTFS/dev/pts" 2>/dev/null; then
        mount --bind /dev/pts "$ROOTFS/dev/pts" 2>/dev/null || true
        mount --make-rslave "$ROOTFS/dev/pts" 2>/dev/null || true
        if mountpoint -q "$ROOTFS/dev/pts" 2>/dev/null; then
            MOUNTED_FS+=("$ROOTFS/dev/pts")
        else
            echo "⚠️  OSTRZEŻENIE: brak /dev/pts w chroot – terminale (openpty) mogą nie działać" >&2
        fi
    fi
    # Bezwarunkowa gwarancja /dev/null (configure pada bez urządzenia znakowego).
    if [[ ! -c "$ROOTFS/dev/null" ]]; then
        mkdir -p "$ROOTFS/dev"
        mknod -m 666 "$ROOTFS/dev/null" c 1 3 2>/dev/null || true
    fi
    # Twarda walidacja środowiska builda: bez /proc (lld-wrapper czyta
    # /proc/self/exe) i /dev/null jako urządzenia buildy padają w dziwny sposób.
    # Nie budujemy w niesprawnym środowisku – lepiej jawny błąd niż cicha ruletka.
    if [[ ! -d "$ROOTFS/proc/self" ]] || [[ ! -c "$ROOTFS/dev/null" ]]; then
        echo "❌ Środowisko builda niesprawne: /proc lub /dev/null w rootfs nie działają." >&2
        echo "   Sprawdź uprawnienia mount (root?) i stan rootfs: $ROOTFS" >&2
        exit 1
    fi
    set +e
    LOG_FILE="$WORK_DIR/build-output.log"
    chroot "$ROOTFS" /usr/bin/env -i HOME=/root TERM=linux LANG=C.UTF-8 \
        PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin \
        FORCE_UNSAFE_CONFIGURE=1 bash "$CHROOT_WORK/build-$SHORT_NAME.sh" 2>&1 | tee "$LOG_FILE"
    BUILD_RES=${PIPESTATUS[0]}
    set -e
    for m in "${MOUNTED_FS[@]}"; do umount -l "$m" 2>/dev/null || true; done
else
    # ROOTFS="/" – pagbuild sam działa wewnątrz chroota; budujemy przez
    # symlinki $CHROOT_WORK/src-*, $CHROOT_WORK/destdir-* na katalog roboczy.
    rm -rf "$HOST_SRC_LINK" "$HOST_DEST_LINK"
    ln -s "$WORK_DIR/src" "$HOST_SRC_LINK"
    ln -s "$DESTDIR" "$HOST_DEST_LINK"
    set +e
    LOG_FILE="$WORK_DIR/build-output.log"
    env -i HOME=/root TERM="${TERM:-linux}" PATH="/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin" PKG_CONFIG_PATH="/usr/lib/pkgconfig:/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib64/pkgconfig:/usr/share/pkgconfig:$ROOTFS/usr/lib/pkgconfig:$ROOTFS/usr/lib/x86_64-linux-gnu/pkgconfig:$ROOTFS/usr/lib64/pkgconfig:$ROOTFS/usr/share/pkgconfig" CMAKE_PREFIX_PATH="$ROOTFS/usr:$ROOTFS" CC=/usr/bin/gcc CXX=/usr/bin/g++ AR=/usr/bin/ar RANLIB=/usr/bin/ranlib CFLAGS="-std=gnu17" CPPFLAGS="" FORCE_UNSAFE_CONFIGURE=1 LANG=C.UTF-8 bash "$BUILD_SCRIPT" 2>&1 | tee "$LOG_FILE"
    BUILD_RES=${PIPESTATUS[0]}
    set -e
    rm -f "$HOST_SRC_LINK" "$HOST_DEST_LINK"
fi

if [[ $BUILD_RES -ne 0 ]]; then
    echo "❌ Błąd: Kompilacja nie powiodła się (exit code: $BUILD_RES)."
    if [[ -n "${LOG_FILE:-}" && -f "$LOG_FILE" ]]; then
        echo "   ── ostatnie 30 linii logu builda: ──"
        tail -30 "$LOG_FILE" | sed 's/^/   /'
    fi

    # ── Diagnostyka nieudanego builda ──
    # Rozpoznaje brakujące zależności buildowe (meson/pkg-config/cmake/nagłówki/
    # biblioteki/narzędzia) oraz błędy receptury; zapisuje diagnose-*.json dla
    # pagsync --auto-deps i drukuje zwięzłe, akcyjne podsumowanie.
    if [[ -n "${LOG_FILE:-}" && -f "$LOG_FILE" ]]; then
        DIAG_JSON="$DIAG_DIR/diagnose-$SHORT_NAME.json"
        python3 - "$LOG_FILE" "$DIAG_JSON" "$PKG_NAME" "$PKG_VER" "$PKG_REL" "$BUILD_RES" <<'PY' || echo "  ⚠ Diagnostyka nie powiodła się (python3?)"
import json, os, re, sys

log_path, out_path, pkg_name, pkg_ver, pkg_rel, exit_code = sys.argv[1:7]
try:
    with open(log_path, encoding="utf-8", errors="replace") as _f:
        lines = _f.read().splitlines()
except Exception:
    lines = []

missing = []   # (kind, name)
errors = []    # (type, line)

def add_missing(kind, name):
    if not name:
        return
    t = (kind, name)
    if t not in missing and len(missing) < 8:
        missing.append(t)

def add_error(etype, line):
    if len(errors) < 5:
        errors.append((etype, line.strip()))

for _ln in lines:
    s = _ln.strip()
    if not s:
        continue
    m = re.search(r'Dependency "([^"]+)" not found, tried pkgconfig and cmake', s)
    if m:
        add_missing("pkgconfig-module", m.group(1)); continue
    m = re.search(r"Dependency lookup for (\S+) with method 'pkgconfig'", s)
    if m:
        add_missing("pkgconfig-module", m.group(1)); continue
    m = re.search(r"Package ['\"]([^'\"]+)['\"] not found", s)
    if m:
        add_missing("pkgconfig", m.group(1)); continue
    m = re.search(r"Package (\S+) was not found in the pkg-config search path", s)
    if m:
        add_missing("pkgconfig", m.group(1)); continue
    m = re.search(r"The pkg-config script could not be found or is too old", s)
    if m:
        add_missing("pkgconfig", "pkg-config"); continue
    m = re.search(r"Could NOT find (\S+) \(missing:", s)
    if m:
        add_missing("cmake-module", m.group(1)); continue
    m = re.search(r'Could not find a package configuration file provided by "([^"]+)"', s)
    if m:
        add_missing("cmake-module", m.group(1)); continue
    m = re.search(r"fatal error: (\S+\.h): No such file or directory", s)
    if m:
        add_missing("header", m.group(1)); continue
    m = re.search(r"(\S+\.h): No such file or directory", s)
    if m and not s.startswith("fatal error"):
        add_missing("header", m.group(1)); continue
    m = re.search(r"cannot find -l([\w+-]+)", s)
    if m:
        add_missing("library", m.group(1)); continue
    m = re.search(r"(?:^|:\s)([\w.+-]+): command not found", s)
    if m and m.group(1) not in ("bash", "sh", "cd"):
        add_missing("command", m.group(1)); continue
    m = re.search(r"cd: -?[\w-]*: invalid option", s)
    if m:
        add_error("recipe-bug", s); continue
    m = re.search(r"cd: ([\w./-]+): No such file or directory", s)
    if m:
        add_error("recipe-bug", s); continue
    m = re.search(r"cd: usage:", s)
    if m:
        add_error("recipe-bug", s); continue
    m = re.search(r"^(mv|cp|install|rm|ln|mkdir|touch): cannot (?:stat|create|remove) ['\"]?(\S+)", s)
    if m:
        add_error("recipe-bug", s); continue
    m = re.search(r"configure: error: (\S.*)", s)
    if m:
        add_error("configure", s); continue

X11_SHORT = {"xt", "xmu", "ice", "sm", "x11", "xext", "xrandr", "xfixes",
             "xcursor", "xinerama", "xrender", "xau", "xcb", "xdamage",
             "xcomposite", "xft", "xss", "xvmc", "xres", "xtst",
             "xpresent", "xshmfence"}

ALIAS = {
    "libsoup-3.0": "libsoup3", "libsoup-2.4": "libsoup2",
    "gtk+-3.0": "gtk3", "gtk+-2.0": "gtk2", "gtk4": "gtk4",
    "glib-2.0": "glib", "gobject-2.0": "glib", "gio-2.0": "glib",
    "gdk-pixbuf-2.0": "gdk-pixbuf2", "libpng": "libpng",
    "libjpeg": "libjpeg-turbo", "libtiff-4": "libtiff", "ffi": "libffi",
    "xlib": "xorg-libx11", "xutil": "xorg-libx11",
    "libcrypto": "openssl", "libssl": "openssl",
    "libudev": "systemd", "libmount": "util-linux", "libblkid": "util-linux",
    "uuid": "util-linux", "libexpat": "expat", "libpcre": "pcre",
    "ncursesw": "ncurses", "tinfo": "ncurses", "panel": "ncurses",
    "readline": "readline", "libseccomp": "libseccomp", "pam": "linux-pam",
    "libxcrypt": "libxcrypt", "libcrypt": "libxcrypt", "libnsl": "libnsl",
    "liblz4": "lz4", "bzip2": "bzip2", "zlib": "zlib",
    "libevent": "libevent", "libarchive": "libarchive",
    "sqlite3": "sqlite", "libpq": "postgresql", "mysqlclient": "mariadb",
    "json-c": "json-c", "json-glib-1.0": "json-glib",
    "libunistring": "libunistring", "libidn2": "libidn2", "libpsl": "libpsl",
    "icu-uc": "icu", "icu-i18n": "icu", "icu-io": "icu",
    "gnutls": "gnutls", "nettle": "nettle", "hogweed": "nettle",
    "libgcrypt": "libgcrypt", "libgpg-error": "libgpg-error",
    "libassuan": "libassuan", "libusb-1.0": "libusb", "libusb": "libusb",
    "libgudev-1.0": "libgudev", "gudev-1.0": "libgudev",
    "polkit-gobject-1": "polkit", "polkit-agent-1": "polkit",
    "libpciaccess": "libpciaccess", "pixman-1": "pixman", "libdrm": "libdrm",
    "libva": "libva", "libva-drm": "libva", "libva-x11": "libva",
    "libva-wayland": "libva", "vdpau": "libvdpau", "libvdpau": "libvdpau",
    "libinput": "libinput", "libevdev": "libevdev", "mtdev": "mtdev",
    "gbm": "mesa", "gl": "libglvnd", "egl": "libglvnd",
    "glesv2": "libglvnd", "glx": "libglvnd", "vulkan": "vulkan-loader",
    "libxkbcommon": "libxkbcommon", "xkbcommon": "libxkbcommon",
    "xkbcommon-x11": "libxkbcommon", "xcb": "xorg-libxcb",
    "xcb-util": "xcb-util", "xcb-keysyms": "xcb-util-keysyms",
    "xcb-icccm": "xcb-util-wm", "xcb-cursor": "xcb-util-cursor",
    "xcb-renderutil": "xcb-util-renderutil", "xcb-image": "xcb-util-image",
    "xcb-errors": "xcb-util-errors",
    "wayland-client": "wayland", "wayland-server": "wayland",
    "wayland-cursor": "wayland", "wayland-egl": "wayland",
    "wayland-protocols": "wayland-protocols",
    "gstreamer-1.0": "gstreamer", "gstreamer-base-1.0": "gstreamer",
    "gstreamer-check-1.0": "gstreamer", "gstreamer-controller-1.0": "gstreamer",
    "gstreamer-app-1.0": "gst-plugins-base", "gstreamer-video-1.0": "gst-plugins-base",
    "gstreamer-audio-1.0": "gst-plugins-base", "gstreamer-pbutils-1.0": "gst-plugins-base",
    "gstreamer-fft-1.0": "gst-plugins-base", "gstreamer-riff-1.0": "gst-plugins-base",
    "gstreamer-rtp-1.0": "gst-plugins-base", "gstreamer-rtsp-1.0": "gst-plugins-base",
    "gstreamer-sdp-1.0": "gst-plugins-base", "gstreamer-net-1.0": "gst-plugins-base",
    "gstreamer-gl-1.0": "gst-plugins-base",
    "libpulse": "libpulse", "libpulse-simple": "libpulse",
    "libpulse-mainloop-glib": "libpulse", "alsa": "alsa-lib",
    "jack": "jack2", "libsamplerate": "libsamplerate", "sndfile": "libsndfile",
    "libavcodec": "ffmpeg", "libavformat": "ffmpeg", "libavutil": "ffmpeg",
    "libavfilter": "ffmpeg", "libswscale": "ffmpeg", "libswresample": "ffmpeg",
    "libpostproc": "ffmpeg",
    "SDL2": "sdl2", "SDL": "sdl", "SDL2_image": "sdl2-image",
    "SDL2_ttf": "sdl2-ttf", "SDL2_mixer": "sdl2-mixer", "SDL2_net": "sdl2-net",
    "libpng16": "libpng", "libwebp": "libwebp", "libwebpmux": "libwebp",
    "libwebpdemux": "libwebp", "libopenjp2": "openjpeg2", "lcms2": "lcms2",
    "libheif": "libheif", "libde265": "libde265", "x264": "x264",
    "x265": "x265",
    "gio-unix-2.0": "glib", "gmodule-2.0": "glib", "gthread-2.0": "glib",
    "girepository-2.0": "gobject-introspection",
    "girepository-1.0": "gobject-introspection",
    "libglib-2.0": "glib", "libgobject-2.0": "glib",
    # narzędzia buildowe (komenda → pakiet dostarczający)
    "jam": "ftjam", "pkg-config": "pkgconf", "pkgconfig": "pkgconf",
    "glib-mkenums": "glib", "g-ir-scanner": "gobject-introspection",
    "intltool-update": "intltool", "msgfmt": "gettext", "autopoint": "gettext",
    "xsltproc": "libxslt", "xmllint": "libxml2", "help2man": "help2man",
    "gperf": "gperf", "yacc": "bison", "lex": "flex",
}

def suggest(kind, name):
    n = name
    if n.startswith("pkgconfig(") and n.endswith(")"):
        n = n[len("pkgconfig("):-1]
    if n in ALIAS:
        return ALIAS[n]
    if n.lower() in X11_SHORT:
        return "xorg-lib" + n.lower()
    # Wzorce: moduły Qt (Qt5Core/Qt6Widgets) i GStreamer (gstreamer-video-1.0)
    if n.startswith("Qt5"):
        return "qt5"
    if n.startswith("Qt6"):
        return "qt6"
    if n.startswith("gstreamer-") and n.endswith("-1.0"):
        return "gstreamer"
    if n.startswith("gst-"):
        return "gst-plugins-base"
    m = re.match(r"^(lib[\w+]+)-(\d+)(?:[.-]\d+)*$", n)
    if m:
        return m.group(1) + m.group(2)
    if kind == "header":
        b = os.path.basename(n).replace(".h", "")
        if b.lower() in ALIAS:
            return ALIAS[b.lower()]
        if b.lower() in X11_SHORT:
            return "xorg-lib" + b.lower()
        if re.match(r"^X[A-Za-z]+$", b):
            return "xorg-lib" + b.lower()
        return ("lib" + b) if b else None
    if kind == "library":
        return "lib" + n
    if kind == "command":
        return n
    return n

payload = {
    "package": pkg_name,
    "version": pkg_ver,
    "release": int(pkg_rel or 0),
    "exit_code": int(exit_code or 0),
    "missing": [{"kind": k, "name": n, "suggested": suggest(k, n)} for k, n in missing],
    "errors": [{"type": t, "line": l} for t, l in errors],
    "log_tail": lines[-30:],
}
try:
    os.makedirs(os.path.dirname(out_path), exist_ok=True)
    with open(out_path, "w", encoding="utf-8") as _f:
        json.dump(payload, _f, ensure_ascii=False, indent=2)
except Exception as _e:
    print("  ⚠ Nie udało się zapisać diagnostyki: %s" % _e)

print("  🔍 Diagnoza nieudanego builda:")
if missing:
    print("     ⚠ Brakujące zależności buildowe:")
    for k, n in missing:
        s = suggest(k, n)
        if s and s != n:
            print("        • [%s] %s  →  dodaj do makedepends: %s" % (k, n, s))
        else:
            print("        • [%s] %s" % (k, n))
else:
    print("     • nie wykryto brakujących zależności (wzorce: meson/pkg-config/cmake/nagłówek/lib)")
if errors:
    print("     ⚠ Podejrzane błędy receptury (faza build/package):")
    for t, l in errors:
        print("        • (%s) %s" % (t, l[:140]))
print("     💡 pagsync --auto-deps dobuduje brakujące zależności automatycznie")
PY
        [[ -f "$DIAG_JSON" ]] && echo "   📄 Diagnostyka: $DIAG_JSON"
    fi
    $KEEP_WORK && echo "   Miejsce pracy zachowane: $WORK_DIR"
    exit 1
fi

# ── 6. Kopiowanie wyników z chroota ──
echo "📋 Kopiowanie wyników z chroota..."
cp -r "$ROOTFS$CHROOT_WORK/destdir-$SHORT_NAME/"* "$DESTDIR/" 2>/dev/null || true

# ── 7. Generowanie metadata.json ──
echo "📝 Generowanie metadata.json..."
python3 -c "
import yaml, json, os

with open('$RECIPE_FILE') as f:
    data = yaml.safe_load(f)

depends = data.get('depends', [])
if isinstance(depends, str):
    depends = [depends]
makedepends = data.get('makedepends', [])
if isinstance(makedepends, str):
    makedepends = [makedepends]
licenses = data.get('license', [])
if isinstance(licenses, str):
    licenses = [licenses]

# provides / pkgconfig – wirtualne zależności; klient pag rozwiązuje przez nie
# np. pkgconfig(xt) -> xorg-libxt przy instalacji
provides = data.get('provides', [])
if isinstance(provides, str):
    provides = [provides]
pkgconfig = data.get('pkgconfig', [])
if isinstance(pkgconfig, str):
    pkgconfig = [pkgconfig]
provides = list(provides) + ['pkgconfig(%s)' % p for p in pkgconfig]

pkg_info = {
    'name': data.get('pkgname', '$PKG_NAME'),
    'version': data.get('pkgver', '$PKG_VER'),
    'release': data.get('pkgrel', $PKG_REL),
    'arch': data.get('arch', '$PKG_ARCH'),
    'category': data.get('category', '$PKG_CAT'),
    'description': data.get('pkgdesc', ''),
    'url': data.get('url', ''),
    'license': licenses,
    'depends': depends,
    'makedepends': makedepends,
    'provides': provides,
    'build_date': '$(date -u +%Y-%m-%dT%H:%M:%SZ)',
    'format': 'PAGBUILD-1.0',
}

os.makedirs('$DESTDIR', exist_ok=True)
with open('$DESTDIR/metadata.json', 'w') as f:
    json.dump(pkg_info, f, indent=2)
"

# ── 7a. Analiza ABI (so-name): provides_so / requires_so do metadata.json ──
# Best-effort – brak readelf/ELF = puste listy (nie przerywa budowy).
python3 - "$DESTDIR" "$RECIPE_FILE" <<'PYABI'
import os, sys, json, re, subprocess

dest = sys.argv[1]
recipe_file = sys.argv[2] if len(sys.argv) > 2 else ""
meta_path = os.path.join(dest, "metadata.json")
try:
    with open(meta_path, encoding="utf-8") as f:
        meta = json.load(f)
except Exception:
    sys.exit(0)

# Biblioteki bazowe (glibc/gcc runtime) – nie wymagają pakietu dostawcy
BASE_SO = {
    "libc.so.6", "libm.so.6", "libpthread.so.0", "libdl.so.2", "librt.so.1",
    "libutil.so.1", "libresolv.so.2", "libnsl.so.1", "libcrypt.so.1",
    "ld-linux.so.2", "ld-linux-x86-64.so.2", "ld-linux-aarch64.so.1",
    "libgcc_s.so.1", "linux-vdso.so.1",
}

def _elf_so_info(path):
    """Zwraca ([SONAME], [DT_NEEDED]) z readelf -d."""
    try:
        out = subprocess.run(["readelf", "-d", path],
                             capture_output=True, text=True, timeout=10).stdout
    except Exception:
        return [], []
    soname, needed = [], []
    for line in out.splitlines():
        line = line.strip()
        if "SONAME" in line or "NEEDED" in line:
            m = re.search(r"\[([^\]]+)\]", line)
            if not m:
                continue
            val = m.group(1)
            if "SONAME" in line:
                if val not in soname:
                    soname.append(val)
            elif val not in needed:
                needed.append(val)
    return soname, needed

provides_so, requires_so = [], []
for root, _dirs, files in os.walk(dest):
    for fn in files:
        fp = os.path.join(root, fn)
        if os.path.islink(fp):
            continue
        try:
            with open(fp, "rb") as f:
                magic = f.read(4)
        except OSError:
            continue
        if magic != b"\x7fELF":
            continue
        p, r = _elf_so_info(fp)
        for s in p:
            if s not in provides_so:
                provides_so.append(s)
        for s in r:
            if s not in requires_so:
                requires_so.append(s)

# Pary zadeklarowane WPROST w PAGBUILD.yaml (provides_so/requires_so).
# Uzupełniają auto-analizę ELF – przydatne, gdy w builderze brak readelf,
# a pakiet ma krytyczne zależności .so (np. python → libssl.so.4).
declared_provides, declared_requires = [], []
if recipe_file:
    try:
        import yaml
        with open(recipe_file, encoding="utf-8") as f:
            rdata = yaml.safe_load(f) or {}
        dp = rdata.get("provides_so") or []
        dr = rdata.get("requires_so") or []
        declared_provides = [dp] if isinstance(dp, str) else [str(x) for x in dp]
        declared_requires = [dr] if isinstance(dr, str) else [str(x) for x in dr]
    except Exception:
        pass
for s in declared_provides:
    if s and s not in provides_so:
        provides_so.append(s)
for s in declared_requires:
    if s and s not in requires_so:
        requires_so.append(s)

requires_so = [s for s in requires_so if s not in BASE_SO and s not in provides_so]
meta["provides_so"] = provides_so
meta["requires_so"] = requires_so
with open(meta_path, "w", encoding="utf-8") as f:
    json.dump(meta, f, indent=2)
print(f"   ABI so-name: provides_so={len(provides_so)}, requires_so={len(requires_so)}")
PYABI

# ── 8. Pakowanie do .pag ──
echo "🗜 Pakowanie do ${PKG_EXT}..."
PKG_FILE="$OUT_DIR/${FULL_NAME}${PKG_EXT}"

# Struktura: data.tar.xz + metadata.json na wierzchu
TEMP_PKG="$WORK_DIR/pkg_temp"
rm -rf "$TEMP_PKG"
mkdir -p "$TEMP_PKG"

# data.tar.xz z plikami. UWAGA: --exclude musi być PRZED argumentami pozycyjnymi
# (-C destdir .) – inaczej GNU tar traktuje je jako pozycyjne („no effect”)
# i kończy błędem, a fallback tylko powielał błąd (set -e ubijał pakowanie).
tar -cJf "$TEMP_PKG/data.tar.xz" --exclude=metadata.json --exclude=./metadata.json -C "$DESTDIR" . || \
tar -cJf "$TEMP_PKG/data.tar.xz" -C "$DESTDIR" .

# Kopiuj metadata.json na wierzch
cp "$DESTDIR/metadata.json" "$TEMP_PKG/"

# hooks/ – skrypty pre/post-install/remove z pola hooks: w recepturze
# (pag uruchamia post-install z hooks/ przy instalacji pakietu).
python3 - "$RECIPE_FILE" "$TEMP_PKG/hooks" <<'PY'
import os, sys, yaml
recipe, hooks_dir = sys.argv[1:]
try:
    with open(recipe, encoding="utf-8") as f:
        data = yaml.safe_load(f) or {}
except Exception:
    data = {}
hooks = data.get("hooks")
if not isinstance(hooks, dict):
    hooks = {}
os.makedirs(hooks_dir, exist_ok=True)
for name, body in hooks.items():
    if not isinstance(name, str) or not name or "/" in name or name.startswith("."):
        continue
    script = "#!/bin/bash\nset -e\n" + str(body) + "\n"
    with open(os.path.join(hooks_dir, name), "w", encoding="utf-8") as f:
        f.write(script)
    os.chmod(os.path.join(hooks_dir, name), 0o755)
PY

# Pakiet finalny: metadata.json + data.tar.xz + hooks/ (jeśli są)
# (jawne nazwy – bez prefiksu ./)
if [[ -d "$TEMP_PKG/hooks" ]] && [[ -n "$(ls -A "$TEMP_PKG/hooks" 2>/dev/null)" ]]; then
    tar -cJf "$PKG_FILE" -C "$TEMP_PKG" metadata.json data.tar.xz hooks
else
    tar -cJf "$PKG_FILE" -C "$TEMP_PKG" metadata.json data.tar.xz
fi

echo "✅ Sukces! Pakiet gotowy: $PKG_FILE"
echo "   $(du -h "$PKG_FILE" | cut -f1)"

# ── 8a. Udostępnij świeżo zbudowany pakiet kolejnym recepturom ──
# Nie zastępuje to docelowego menedżera pakietów, ale zapewnia bootstrap:
# następny build widzi nagłówki, biblioteki i pliki pkg-config poprzednika.
tar -xJf "$TEMP_PKG/data.tar.xz" -C "$ROOTFS"

# ── 8a.1 Naprawa pkg-config w rootfs ──
# Gdy ROOTFS!=/ (np. /mnt/pagan-native-rootfs), configure zapisuje w *.pc
# zakodowany prefix=/mnt/pagan-native-rootfs/usr. Wewnątrz chroot (gdzie jest /)
# pkg-config zwraca wtedy nieistniejące ścieżki i każdy pakiet polegający na
# pkg-config pada (np. -I/mnt/.../include/libxml2). Sprowadź prefix do /usr.
# UWAGA: libdir/includedir NIE są wymuszane na /usr/lib – bezwarunkowy zapis
# łamał pakiety z libkami w /usr/lib64 (np. glib: glibconfig.h i libglib-2.0
# w /usr/lib64, a .pc po przepisaniu wskazywał martwe /usr/lib).
if [[ -d "$ROOTFS/usr" ]]; then
    find "$ROOTFS/usr" -name '*.pc' \
        -exec sed -i "s#^prefix=.*#prefix=/usr#; s#^exec_prefix=.*#exec_prefix=/usr#" {} + 2>/dev/null || true
    # libdir/includedir: usuń tylko zakodowaną ścieżkę rootfs (np. libdir=/mnt/.../usr/lib)
    # – poprawny wpis (np. /usr/lib64) zostaje nietknięty.
    if [[ "$ROOTFS" != "/" ]]; then
        _pc_rf=$(printf '%s' "$ROOTFS" | sed 's/[\/&]/\\&/g')
        find "$ROOTFS/usr" -name '*.pc' \
            -exec sed -i "s#^\(libdir\|includedir\)=${_pc_rf}#\1=#" {} + 2>/dev/null || true
    fi
fi

# ── 8a.2 Odśwież cache dynamicznego loadera po rozpakowaniu pakietu ──
# Rozpakowanie data.tar.xz NIE aktualizuje /etc/ld.so.cache. W efekcie świeżo
# zbudowane biblioteki współdzielone nie są widoczne dla ładującego i binaria
# od nich zależne padają przy uruchomieniu (np. pkgconf 2.4 -> libpkgconf.so.6
# podczas configure libxrender: "cannot open shared object file"). Cache w
# rootfs pochodzi zwykle z bootstrapu (stara wersja libki), więc zawiera inne
# SONAME-y. Po każdym rozpakowaniu przebuduj cache:
#   - ROOTFS="/"  -> jesteśmy już wewnątrz chroota, uruchamiamy ldconfig wprost
#   - ROOTFS=/sciezka -> montowany rootfs, wołamy ldconfig przez chroot
echo "🔄 Odświeżanie cache ld.so w rootfs..."
if [[ "$ROOTFS" == "/" ]]; then
    /sbin/ldconfig >/dev/null 2>&1 || echo "⚠ Ostrzeżenie: nie udało się uruchomić /sbin/ldconfig"
else
    chroot "$ROOTFS" /sbin/ldconfig >/dev/null 2>&1 || echo "⚠ Ostrzeżenie: nie udało się uruchomić ldconfig w chroot ($ROOTFS)"
fi

# ── 8b. GPG – podpisywanie pakietu ──
if $DO_SIGN; then
    echo "🔏 Podpisywanie GPG..."
    GPG_BIN=$(command -v gpg2 || command -v gpg || echo "")
    if [[ -z "$GPG_BIN" ]]; then
        echo "⚠ Ostrzeżenie: gpg nie znaleziony – pomijam podpis"
    else
        KEY_ARG=""
        if [[ -n "$GPG_KEY" ]]; then
            KEY_ARG="--local-user $GPG_KEY"
        fi
        # Podpis odłączony (detached signature). UWAGA: gpg nie nadpisuje
        # istniejącego .asc ("File exists") – usuń stary przed podpisaniem,
        # inaczej przy przebudowie tej samej wersji zostaje nieaktualny podpis.
        rm -f "${PKG_FILE}.asc"
        $GPG_BIN --detach-sign --armor $KEY_ARG --batch --no-tty "$PKG_FILE" 2>/dev/null || {
            echo "⚠ Ostrzeżenie: podpisanie GPG nie powiodło się"
        }
        if [[ -f "${PKG_FILE}.asc" ]]; then
            echo "🔏 Podpis: ${PKG_FILE}.asc"
            echo "   $(du -h "${PKG_FILE}.asc" | cut -f1)"
        fi
    fi
fi

# ── 9. Sprzątanie ──
rm -rf "$ROOTFS$CHROOT_WORK/src-$SHORT_NAME" "$ROOTFS$CHROOT_WORK/destdir-$SHORT_NAME" "$ROOTFS$CHROOT_WORK/build-$SHORT_NAME.sh"
$KEEP_WORK || rm -rf "$WORK_DIR"
# Gruntowne sprzątanie wszystkich zaległości (chroot work dir, work diry, cache)
_cleanup_stale

# ── 9a. Cache źródeł – nie trzymamy pobranych archiwów (zajmują GB).
# Po UDANYM buildzie kasujemy pliki użyte przez tę recepturę – w razie
# potrzeby i tak zostaną pobrane ponownie. Wyłącz przez PAGBUILD_KEEP_SOURCES=1
# (np. do debugowania / budowania bez dostępu do sieci).
if [[ "${PAGBUILD_KEEP_SOURCES:-0}" != "1" ]]; then
    _purged=0
    for _cs in "${CACHED_SRCS[@]:-}"; do
        [[ -n "$_cs" && "$_cs" != GIT:* ]] || continue
        # bezpieczeństwo: tylko zwykłe pliki wewnątrz katalogu cache
        if [[ "$_cs" == "$SRC_CACHE/"* && -f "$_cs" ]]; then
            rm -f "$_cs" && _purged=$((_purged + 1))
        fi
    done
    if [[ $_purged -gt 0 ]]; then
        echo "🧹 Usunięto $_purged źródło/a z cache po udanym buildzie"
    fi
fi

echo ""