#!/bin/bash # ============================================================================= # check-pag-abi.sh – wykrywa pakiety .pag, które NIE ZADZIAŁAJĄ na PaganLinux # ----------------------------------------------------------------------------- # PO CO: pakiety budowane w chrootcie Debiana (VPS: builder-chroot.sh, suite # trixie) linkują się z Debianowymi bibliotekami. Jeśli Debian ma INNĄ wersję # zależności niż PaganLinux, w pakiecie zostają odwołania do symboli, których w # naszym systemie nie ma → program/plugin pada na starcie z „undefined symbol”. # Dwa realne przypadki z naszego repo: # * libgsf-1.so.114 → U xmlNanoHTTPClose (Debian ma, libxml2 2.16 nie) # * libavcodec.so.63 → U rsvg_handle_render_document (Debian ma librsvg 2.46+, # my mamy 2.40.21) # # JAK DZIAŁA: buduje indeks WSZYSTKICH symboli eksportowanych przez biblioteki # docelowego rootfsu i sprawdza, czy każdy nierozwiązany symbol z pakietu jest # w tym indeksie (albo w samym pakiecie). To wyłapuje przypadki „na pewno # zepsute”; nie wykryje zmiany semantyki symbolu o tej samej nazwie. # # Użycie: # # 1) TRYB ROZJAZDU (precyzyjny – do bramki w build-repo.sh): porównuje symbole # # z ŚRODOWISKA BUDOWY z symbolem docelowego systemu. # ./check-pag-abi.sh --build-rootfs /mnt/pagan-build --rootfs /mnt/pagan --strict *.pag # # # 2) TRYB CELU (bez --build-rootfs): pokazuje, jakich zależności brakuje w # # obrazie (to NORMALNE dla pakietów repo – instalator dociąga je z repo). # ./check-pag-abi.sh --rootfs /mnt/pagan --dir repo-out # # Kod wyjścia: 1 = problem (w trybie rozjazdu zawsze; w trybie celu tylko z --strict). # ============================================================================= set -uo pipefail ROOTFS="${PAGAN_ROOT:-/mnt/pagan}" BUILD_ROOTFS="" STRICT=0 QUIET=0 VERBOSE=0 PKGS=() SCAN_DIR="" usage() { sed -n '2,40p' "$0"; exit "${1:-0}"; } while [[ $# -gt 0 ]]; do case "$1" in --rootfs) ROOTFS="$2"; shift 2 ;; --build-rootfs) BUILD_ROOTFS="$2"; shift 2 ;; --dir) SCAN_DIR="$2"; shift 2 ;; --strict) STRICT=1; shift ;; --quiet|-q) QUIET=1; shift ;; --verbose|-v) VERBOSE=1; shift ;; -h|--help) usage 0 ;; -*) echo "Nieznana opcja: $1" >&2; usage 2 ;; *) PKGS+=("$1"); shift ;; esac done [[ -d "$ROOTFS/usr/lib" ]] || { echo "❌ Brak rootfsu PaganLinux: $ROOTFS/usr/lib" >&2; exit 2; } command -v nm >/dev/null || { echo "❌ Brak nm (binutils) na hoście" >&2; exit 2; } msg() { [[ $QUIET -eq 1 ]] || printf '%s\n' "$*"; } die() { printf '\033[1;31m✗ %s\033[0m\n' "$*" >&2; exit 2; } TMP="$(mktemp -d)" trap 'rm -rf "$TMP"' EXIT # ── 1) Indeks symboli eksportowanych przez docelowy system ────────────────── # Bierzemy wszystkie .so* z rootfsu (to one mogą rozwiązać symbole pakietu). # Indeks można zcache-ować (env PAGAN_ABI_INDEX=/ścieżka) – dla 1800 pakietów # liczenie go za każdym razem byłoby marnotrawstwem. if [[ -n "${PAGAN_ABI_INDEX:-}" && -s "${PAGAN_ABI_INDEX}" ]]; then cp -f "$PAGAN_ABI_INDEX" "$TMP/target.syms" msg "▸ Indeks symboli z cache: $PAGAN_ABI_INDEX ($(wc -l < "$TMP/target.syms") symboli)" else msg "▸ Buduję indeks symboli z $ROOTFS (biblioteki .so)…" : > "$TMP/target.syms" # BEZ -maxdepth: moduły Xorg potrafią leżeć głęboko (np. # /usr/lib/xorg/modules/libglamoregl.so eksportuje glamor_init, glamor_create_pixmap…). # Przy -maxdepth 2 te pliki nie trafiały do indeksu, więc sterowniki Xorg # (amdgpu/intel) były fałszywie zgłaszane jako „rozjazd ABI” – ich symbole # glamor_* są w tym module, a nie w samym sterowniku. while IFS= read -r -d '' lib; do nm -D --defined-only --format=posix "$lib" 2>/dev/null | awk '{print $1}' done < <(find "$ROOTFS/usr/lib" "$ROOTFS/lib" -name '*.so*' -type f -print0 2>/dev/null) \ | sed 's/@.*$//' | sort -u > "$TMP/target.syms" msg " symboli w systemie docelowym: $(wc -l < "$TMP/target.syms")" if [[ -n "${PAGAN_ABI_INDEX:-}" ]]; then mkdir -p "$(dirname "$PAGAN_ABI_INDEX")" 2>/dev/null || true cp -f "$TMP/target.syms" "$PAGAN_ABI_INDEX" 2>/dev/null || true msg " indeks zapisany w cache: $PAGAN_ABI_INDEX" fi fi # Symbole, które wolno mieć nierozwiązane (są opcjonalne / dostarczane przez # mechanizmy runtime): słabe odwołania i kilka znanych wstawek linkera. cat > "$TMP/ignore.syms" <<'EOF' __gmon_start__ __cxa_finalize _ITM_deregisterTMCloneTable _ITM_registerTMCloneTable _ITM_addTMCloneTable _Jv_RegisterClasses EOF # ── 1b) Indeks symboli ze ŚRODOWISKA BUDOWY (tryb rozjazdu) ──────────────── # To jest sedno odpowiedzi na „czy repo będzie spójne”: jeśli symbol jest # dostępny w środowisku, w którym pakiet powstawał, a NIE MA go w systemie # docelowym, to pakiet został zbudowany pod inne biblioteki (np. Debianowy # chroot na VPS) i padnie u użytkownika z „undefined symbol”. if [[ -n "$BUILD_ROOTFS" ]]; then [[ -d "$BUILD_ROOTFS/usr/lib" || -d "$BUILD_ROOTFS/lib" ]] || \ die "Brak środowiska budowy: $BUILD_ROOTFS" msg "▸ Buduję indeks symboli ze ŚRODOWISKA BUDOWY: $BUILD_ROOTFS…" : > "$TMP/build.syms" # Tu również bez -maxdepth (ten sam powód co w indeksie docelowym wyżej). while IFS= read -r -d '' lib; do nm -D --defined-only --format=posix "$lib" 2>/dev/null | awk '{print $1}' done < <(find "$BUILD_ROOTFS/usr/lib" "$BUILD_ROOTFS/lib" -name '*.so*' -type f -print0 2>/dev/null \ ) | sed 's/@.*$//' | sort -u > "$TMP/build.syms" msg " symboli w środowisku budowy: $(wc -l < "$TMP/build.syms")" # Mapa „który soname dostarcza dany symbol” (środowisko budowy) + soname'y # obecne w systemie docelowym. Pozwala odróżnić DWIE sytuacje, które wcześniej # wyglądały identycznie (i dawały fałszywe „ROZJAZD ABI”): # * DRIFT ABI – biblioteka o TYM SAMYM soname JEST w targetcie, ale bez # potrzebnego symbolu (np. zbudowane pod nowsze Debianowe libxml2/librsvg) # → naprawdę zepsuty pakiet, # * BRAK ZALEŻNOŚCI – takiej biblioteki nie ma w targetcie WCALE, więc jest # to zwykła zależność z repo (np. cmake → librhash.so.1, libuv.so.1, # libjsoncpp.so.27). To NIE rozjazd ABI, ale MUSI trafić do `depends:`. msg "▸ Buduję mapę dostawców symboli (symbol → soname)…" : > "$TMP/build.prov" while IFS= read -r -d '' lib; do _sn="$(readelf -d "$lib" 2>/dev/null | sed -n 's/.*SONAME.*\[\(.*\)\].*/\1/p' | head -n1)" [[ -n "$_sn" ]] || _sn="$(basename "$lib")" nm -D --defined-only --format=posix "$lib" 2>/dev/null | awk -v s="$_sn" 'NF{print $1 "\t" s}' done < <(find "$BUILD_ROOTFS/usr/lib" "$BUILD_ROOTFS/lib" -name '*.so*' -type f -print0 2>/dev/null) \ | sed 's/@.*$//' | sort -u > "$TMP/build.prov" : > "$TMP/target.sonames" while IFS= read -r -d '' lib; do _sn="$(readelf -d "$lib" 2>/dev/null | sed -n 's/.*SONAME.*\[\(.*\)\].*/\1/p' | head -n1)" [[ -n "$_sn" ]] && printf '%s\n' "$_sn" basename "$lib" done < <(find "$ROOTFS/usr/lib" "$ROOTFS/lib" -name '*.so*' -type f -print0 2>/dev/null) \ | sort -u > "$TMP/target.sonames" fi # Katalogi wtyczek: te pliki .so są dlopen()owane przez proces-gospodarza # (Xorg, pipewire, GIO, gdk-pixbuf…) i legalnie rozwiązują symbole z BINARKI # gospodarza, której nie ma w żadnym .so – w trybie celu dawałyoby to szum. PLUGIN_SKIP='/(xorg/modules|pipewire-0\.3|spa-0\.2|gio/modules|gdk-pixbuf-2\.0|dri|vulkan|gstreamer-1\.0|xfce4/panel/plugins|xfce4/settings-manager|tumbler-1/plugins|NetworkManager)/' # Drzewa ŹRÓDEŁ w pakiecie (np. kernel: /usr/src/kernel/...) zawierają narzędzia # BUILD-TIME (objtool) linkowane z libbfd/libopcodes z binutils. Są używane # dopiero przy budowie modułów (gdy binutils i tak musi być obecny), a NIE w # runtime obrazu – więc nie są rozjazdem ABI. Pomijamy je w obu trybach. SKIP_ALWAYS='/(usr/src)/' if [[ -n "$SCAN_DIR" ]]; then for f in "$SCAN_DIR"/*.pag; do [[ -f "$f" ]] && PKGS+=("$f") done fi [[ ${#PKGS[@]} -gt 0 ]] || die "Nie podano żadnego pakietu .pag (ani --dir)" # ── 2) Sprawdzenie każdego pakietu ────────────────────────────────────────── _broken=0 _warned=0 _depwarn=0 _checked=0 : > "$TMP/dep_all.txt" for pag in "${PKGS[@]}"; do [[ -f "$pag" ]] || { msg " ! pomijam (brak pliku): $pag"; continue; } # Rozpakuj: .pag = tar.xz( metadata.json + data.tar.xz + hooks/ ) rm -rf "$TMP/x"; mkdir -p "$TMP/x" if ! tar -xJf "$pag" -C "$TMP/x" data.tar.xz 2>/dev/null; then msg " ! $pag: nie mogę odczytać data.tar.xz – pomijam" continue fi mkdir -p "$TMP/x/data" tar -xJf "$TMP/x/data.tar.xz" -C "$TMP/x/data" 2>/dev/null || { msg " ! $pag: data.tar.xz uszkodzony – pomijam"; continue; } # Zbiór symboli dostarczanych przez sam pakiet (biblioteki w pakiecie) : > "$TMP/pkg.syms" while IFS= read -r -d '' elf; do nm -D --defined-only --format=posix "$elf" 2>/dev/null | awk '{print $1}' done < <(find "$TMP/x/data" -name '*.so*' -type f -print0 2>/dev/null) \ | sed 's/@.*$//' | sort -u > "$TMP/pkg.syms" _pkg_missing="$TMP/missing.txt" : > "$_pkg_missing" # Soname'y bibliotek spoza obrazu (zależności runtime) – liczone od nowa dla # każdego pakietu. : > "$TMP/dep_sonames.txt" # Każdy plik ELF: nierozwiązane symbole (BEZ słabych – te są opcjonalne) while IFS= read -r -d '' f; do # tylko prawdziwe ELF-y (pierwsze 4 bajty: \x7fELF) [[ "$(head -c4 "$f" 2>/dev/null | od -An -c | tr -d ' \n')" == *177ELF* ]] || continue # ZAWSZE pomijaj drzewa źródeł (kernel /usr/src/kernel/tools/objtool itd.). [[ "$f" =~ $SKIP_ALWAYS ]] && continue # W trybie CELU pomijamy wtyczki (rozwiązują symbole z procesu-gospodarza). if [[ -z "$BUILD_ROOTFS" ]] && [[ "$f" =~ $PLUGIN_SKIP ]]; then continue fi nm -D --undefined-only "$f" 2>/dev/null \ | awk '$1 ~ /^[Uu]$/ {print $2} $2 ~ /^U$/ {print $3}' \ | sed 's/@.*$//' | sort -u > "$TMP/u.syms" [[ -s "$TMP/u.syms" ]] || continue # odrzuć: własne symbole pakietu + listę wyjątków comm -23 "$TMP/u.syms" <(sort -u "$TMP/pkg.syms" "$TMP/ignore.syms") > "$TMP/u2.syms" if [[ -n "$BUILD_ROOTFS" ]]; then # TRYB ROZJAZDU: kandydaci to symbole, które środowisko budowy MA, # a system docelowy NIE MA. Potem rozdzielamy drift od zależności. comm -12 "$TMP/u2.syms" "$TMP/build.syms" | comm -23 - "$TMP/target.syms" > "$TMP/cand.syms" awk -F'\t' -v tfile="$TMP/target.sonames" ' BEGIN { while ((getline l < tfile) > 0) inT[l] = 1 } NR == FNR { prov[$1] = prov[$1] (prov[$1] ? "," : "") $2; next } { if (!($1 in prov)) { print $1 "\tDRIFT\t?"; next } n = split(prov[$1], a, ","); hit = 0; miss = "" for (i = 1; i <= n; i++) { if (a[i] in inT) hit = 1 else miss = miss (miss ? "," : "") a[i] } if (hit) print $1 "\tDRIFT\t" miss else print $1 "\tDEP\t" miss }' "$TMP/build.prov" "$TMP/cand.syms" > "$TMP/classified.txt" awk -F'\t' '$2 == "DRIFT" { print $1 }' "$TMP/classified.txt" > "$TMP/u3.syms" awk -F'\t' '$2 == "DEP" && $3 != "" { n = split($3, a, ","); for (i = 1; i <= n; i++) print a[i] }' \ "$TMP/classified.txt" >> "$TMP/dep_sonames.txt" else # TRYB CELU: wszystko, czego nie ma w obrazie (informacyjnie). comm -23 "$TMP/u2.syms" "$TMP/target.syms" > "$TMP/u3.syms" fi while IFS= read -r sym; do [[ -n "$sym" ]] && printf '%s\t%s\n' "${f#"$TMP/x/data"}" "$sym" >> "$_pkg_missing" done < "$TMP/u3.syms" done < <(find "$TMP/x/data" -type f -print0 2>/dev/null) _checked=$((_checked + 1)) _has_drift=0; [[ -s "$_pkg_missing" ]] && _has_drift=1 _has_dep=0; [[ -s "$TMP/dep_sonames.txt" ]] && _has_dep=1 if [[ $_has_drift -eq 1 ]]; then if [[ -n "$BUILD_ROOTFS" ]]; then _broken=$((_broken + 1)) printf '\033[1;31m✗ %s\033[0m – ROZJAZD ABI: %s symboli jest w środowisku budowy, a nie ma w %s:\n' \ "$(basename "$pag")" "$(wc -l < "$_pkg_missing")" "$ROOTFS" else _warned=$((_warned + 1)) printf '\033[1;33m· %s\033[0m – %s symboli spoza obrazu (pakiet potrzebuje zależności z repo):\n' \ "$(basename "$pag")" "$(wc -l < "$_pkg_missing")" fi head -10 "$_pkg_missing" | while IFS=$'\t' read -r p s; do printf ' %-52s %s\n' "$s" "$p" done if [[ "$(wc -l < "$_pkg_missing")" -gt 10 ]]; then printf ' … i %s dalszych\n' "$(( $(wc -l < "$_pkg_missing") - 10 ))" if [[ $VERBOSE -eq 1 ]]; then tail -n +11 "$_pkg_missing" | while IFS=$'\t' read -r p s; do printf ' %-52s %s\n' "$s" "$p" done fi fi if [[ -n "$BUILD_ROOTFS" ]]; then printf ' ↳ symbol jest w środowisku budowy, a brak go w bibliotece o TYM SAMYM\n' printf ' sonamie w %s – pakiet zbudowano pod inne biblioteki\n' "$ROOTFS" printf ' (np. chroot Debiana na VPS). Przebuduj natywnie:\n' printf ' ./build-repo.sh --rootfs %s %s\n' "$BUILD_ROOTFS" \ "$(basename "$pag" | sed 's/-[0-9][^-]*-[0-9]*\.pag$//')" fi fi # Brakujące zależności runtime: bibliotek, których w obrazie nie ma WCALE. # To nie rozjazd ABI (dociągnie je `pag` z repo), ale muszą być w `depends:` # receptury – inaczej użytkownik dostanie „error while loading shared # libraries”. Tak wyszło przy cmake: brakowało librhash/libuv/libjsoncpp. if [[ $_has_dep -eq 1 ]]; then _depwarn=$((_depwarn + 1)) sort -u "$TMP/dep_sonames.txt" >> "$TMP/dep_all.txt" printf '\033[1;33m· %s\033[0m – %s bibliotek spoza obrazu (zależności runtime – dopisz do depends:):\n' \ "$(basename "$pag")" "$(sort -u "$TMP/dep_sonames.txt" | wc -l)" printf ' %s\n' "$(sort -u "$TMP/dep_sonames.txt" | tr '\n' ' ')" fi if [[ $_has_drift -eq 0 && $_has_dep -eq 0 ]]; then msg " ✓ $(basename "$pag") – bez zarzutów" fi done echo if [[ -n "$BUILD_ROOTFS" && $_broken -gt 0 ]]; then printf '\033[1;31m✗ %d z %d pakietów zbudowano pod inne biblioteki niż %s – NIE publikuj ich\033[0m\n' \ "$_broken" "$_checked" "$ROOTFS" exit 1 fi if [[ -n "$BUILD_ROOTFS" ]]; then if [[ $_depwarn -gt 0 ]]; then printf '\033[1;33m! %d pakiet(ów) używa bibliotek spoza obrazu: %s\033[0m\n' \ "$_depwarn" "$(sort -u "$TMP/dep_all.txt" 2>/dev/null | tr '\n' ' ')" printf ' To normalne dla repo (`pag` dociąga je z repo), ale każda z nich musi być w `depends:` receptury.\n' if [[ $STRICT -eq 1 ]]; then printf '\033[1;31m✗ --strict: brakujące zależności traktowane jak błąd\033[0m\n' exit 1 fi fi printf '\033[1;32m✓ %d pakietów sprawdzonych – bez rozjazdu ABI (środowisko budowy == %s)\033[0m\n' \ "$_checked" "$ROOTFS" else printf '\033[1;32m✓ %d pakietów sprawdzonych (%d z zależnościami spoza ISO – to normalne dla repo)\033[0m\n' \ "$_checked" "$_warned" fi if [[ $STRICT -eq 1 && $_warned -gt 0 ]]; then exit 1 fi exit 0