#!/usr/bin/env bash # ============================================================================= # build-native-builder-rootfs.sh # # Buduje natywną warstwę BUILD-DEPS PaganOS w rootfs utworzonym przez rootfs.sh. # Nie używa Debianowego ABI: pagbuild uruchamiany jest przez chroot PaganOS. # # Użycie: # sudo ./build-native-builder-rootfs.sh \ # --rootfs /mnt/pagan-native-rootfs \ # --recipes ./recipes \ # --rebuild-base # ============================================================================= set -Eeuo pipefail # Nigdy nie pozwól, aby biblioteki PaganOS zastąpiły biblioteki hosta # podczas wywołań mount/umount/chroot. unset LD_LIBRARY_PATH SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" ROOTFS="${NATIVE_ROOTFS:-/mnt/pagan-native-rootfs}" RECIPES="${NATIVE_RECIPES:-$SCRIPT_DIR/recipes}" REBUILD_BASE=0 KEEP_MOUNTS=0 LOG="/var/log/pagan-native-builder.log" usage() { sed -n '1,20p' "$0"; exit "${1:-0}"; } while [[ $# -gt 0 ]]; do case "$1" in --rootfs) ROOTFS="$2"; shift 2 ;; --recipes) RECIPES="$2"; shift 2 ;; --rebuild-base) REBUILD_BASE=1; shift ;; --keep-mounts) KEEP_MOUNTS=1; shift ;; -h|--help) usage ;; *) echo "Nieznany argument: $1" >&2; usage 2 ;; esac done [[ $EUID -eq 0 ]] || { echo "Uruchom jako root" >&2; exit 1; } [[ "$ROOTFS" != "/" && -n "$ROOTFS" ]] || { echo "Niebezpieczny ROOTFS" >&2; exit 1; } [[ -d "$RECIPES" ]] || { echo "Brak recipes: $RECIPES" >&2; exit 1; } mkdir -p "$(dirname "$LOG")" exec > >(tee -a "$LOG") 2>&1 log() { printf '[native-rootfs] %s\n' "$*"; } die() { log "BŁĄD: $*"; exit 1; } # Etap 1: bazowy, natywny PaganOS rootfs/toolchain. if [[ $REBUILD_BASE -eq 1 || ! -x "$ROOTFS/usr/bin/gcc" ]]; then log "Buduję bazowy PaganOS rootfs przez rootfs.sh" PAGAN_ROOT="$ROOTFS" REBUILD_FROM_SCRATCH=1 "$SCRIPT_DIR/rootfs.sh" --rebuild fi for x in bash gcc g++ make tar curl python3; do [[ -x "$ROOTFS/usr/bin/$x" || -x "$ROOTFS/bin/$x" ]] || die "Brak natywnego narzędzia w rootfs: $x" done [[ -x "$ROOTFS/usr/bin/pagbuild" ]] || die "Brak /usr/bin/pagbuild w rootfs" mkdir -p "$ROOTFS/dev" "$ROOTFS/proc" "$ROOTFS/sys" "$ROOTFS/run" MOUNTS=() unmount_all() { for m in "${MOUNTS[@]:-}"; do umount -l "$m" 2>/dev/null || true; done; MOUNTS=(); } cleanup() { local rc=$?; [[ $KEEP_MOUNTS -eq 0 ]] && unmount_all; exit "$rc"; } trap cleanup EXIT INT TERM mount -t proc proc "$ROOTFS/proc"; MOUNTS=("$ROOTFS/proc" "${MOUNTS[@]}" ) mount --rbind /sys "$ROOTFS/sys"; mount --make-rslave "$ROOTFS/sys"; MOUNTS=("$ROOTFS/sys" "${MOUNTS[@]}" ) mount --rbind /dev "$ROOTFS/dev"; mount --make-rslave "$ROOTFS/dev"; MOUNTS=("$ROOTFS/dev" "${MOUNTS[@]}" ) mount --rbind /run "$ROOTFS/run"; mount --make-rslave "$ROOTFS/run"; MOUNTS=("$ROOTFS/run" "${MOUNTS[@]}" ) # Autoconf intensywnie korzysta z /dev/null podczas generowania config.status. # Zwykły plik zamiast urządzenia znakowego wstrzykuje wyjście diff do # config.status i psuje build. Upewnij się, że /dev/null jest urządzeniem. if [[ ! -c "$ROOTFS/dev/null" ]]; then log "Naprawiam nieprawidłowe $ROOTFS/dev/null" rm -f "$ROOTFS/dev/null" mknod -m 666 "$ROOTFS/dev/null" c 1 3 fi # Niektóre bazowe nagłówki glibc (np. regex.h) mogą nie być zachowane podczas # bootstrapu, mimo że libc udostępnia funkcje (regcomp/regexec). if [[ ! -f "$ROOTFS/usr/include/regex.h" && -f /usr/include/regex.h ]]; then install -m644 /usr/include/regex.h "$ROOTFS/usr/include/regex.h" log "Uzupełniono brakujący nagłówek glibc: /usr/include/regex.h" fi # Loader musi znać /usr/lib64 (tam trafia wiele pakietów GTK/Qt). Bez tego # custom meson commands nie ładują świeżo zbudowanych bibliotek. if [[ ! -f "$ROOTFS/etc/ld.so.conf" ]] || ! grep -q '/usr/lib64' "$ROOTFS/etc/ld.so.conf" 2>/dev/null; then printf '/usr/lib\n/usr/lib64\n/lib\n/lib64\n' > "$ROOTFS/etc/ld.so.conf" chroot "$ROOTFS" /sbin/ldconfig >/dev/null 2>&1 || true log "Ustawiono /etc/ld.so.conf z /usr/lib64 i przebudowano cache ld.so" fi rm -f "$ROOTFS/etc/resolv.conf"; cp -L /etc/resolv.conf "$ROOTFS/etc/resolv.conf" mkdir -p "$ROOTFS/var/lib/pagan-sync/recipes" "$ROOTFS/var/cache/pagbuild/output" "$ROOTFS/var/cache/pagbuild/sources" rsync -a --delete "$RECIPES/" "$ROOTFS/var/lib/pagan-sync/recipes/" # Zawsze synchronizuj AKTUALNY builder do rootfs przed budowaniem. # Stary/niekompletny pagbuild w rootfs potrafił cicho nie wygenerować # build.sh ('chmod: No such file'), co przerywało całą natywną warstwę # i pozostawiało mylący log. Wymuszamy świeżą kopię, identyczną z repozytorium. if [[ -f "$SCRIPT_DIR/pagbuild" ]]; then cp -f "$SCRIPT_DIR/pagbuild" "$ROOTFS/usr/bin/pagbuild" chmod +x "$ROOTFS/usr/bin/pagbuild" log "Zsynchronizowano aktualny pagbuild ($(stat -c%s "$ROOTFS/usr/bin/pagbuild") B) do rootfs" else die "Brak $SCRIPT_DIR/pagbuild – nie można zagwarantować aktualnego buildera" fi if [[ -f "$SCRIPT_DIR/pagsync" ]]; then install -m755 "$SCRIPT_DIR/pagsync" "$ROOTFS/opt/pagan/pagsync" fi # Wyznacz kolejność zależności. Uwzględniamy również makedepends, ponieważ # biblioteki developerskie muszą być zainstalowane przed konsumentem. ORDER_FILE=$(mktemp) python3 - "$RECIPES" > "$ORDER_FILE" <<'PY' import os, sys, yaml from pathlib import Path base=Path(sys.argv[1]); items={}; deps={} for p in sorted(base.rglob("PAGBUILD.yaml")): try: d=yaml.safe_load(p.read_text()) or {} except Exception: continue n=str(d.get("pkgname") or p.parent.name) items[n]=p all_deps=d.get("depends",[])+d.get("makedepends",[]) if isinstance(all_deps,str): all_deps=[all_deps] # Uwzględnij tylko zależności, które są też recepturami w tym repo. # Zależności bazowe (glibc, systemd, bash...), które już są w rootfs, # nie są budowane tutaj – ich obecność w deps nigdy by nie pozwoliła # węzłowi stać się "ready" (zmuszały algorytm do fallbacku na # sorted(deps)[0], co psuło kolejność budowania). deps[n]={str(x) for x in all_deps if str(x) in items} for n in items: deps.setdefault(n,set()) order=[] while deps: ready=sorted(n for n,v in deps.items() if not (v & items.keys())) if not ready: ready=[sorted(deps)[0]] for n in ready: order.append(items[n]); deps.pop(n,None) for v in deps.values(): v.discard(n) for p in order: print(p) PY TOTAL=$(wc -l < "$ORDER_FILE") N=0 SKIPPED=0 # Katalog, do którego pagbuild składa gotowe pakiety (wewnątrz rootfs). PKG_OUT="$ROOTFS/var/cache/pagbuild/output" # Zwraca nazwę pliku pakietu ("pkgname-pkgver-pkgrel.pag") albo pusty ciąg. pkg_file_for() { python3 - "$1" <<'PY' import sys, yaml try: with open(sys.argv[1], encoding="utf-8") as f: d = yaml.safe_load(f) or {} name = d.get("pkgname"); ver = d.get("pkgver"); rel = d.get("pkgrel", 1) if name and ver: print(f"{name}-{ver}-{rel}.pag") except Exception: pass PY } # Zwraca pkgname receptury albo pusty ciąg. pkg_name_for() { python3 - "$1" <<'PY' import sys, yaml try: with open(sys.argv[1], encoding="utf-8") as f: d = yaml.safe_load(f) or {} print(d.get("pkgname", "") or "") except Exception: pass PY } # Wypisuje nazwy depends+makedepends receptury (po jednej na linię). deps_of() { python3 - "$1" <<'PY' import sys, yaml try: with open(sys.argv[1], encoding="utf-8") as f: d = yaml.safe_load(f) or {} except Exception: raise SystemExit out = (d.get("depends") or []) + (d.get("makedepends") or []) for n in out: print(str(n)) PY } # Mapa pkgname → ścieżka receptury (do wyszukiwania zależności po nazwie). declare -A RECIPE_BY_NAME while IFS= read -r rr; do nn="$(pkg_name_for "$rr")" [[ -n "$nn" ]] && RECIPE_BY_NAME["$nn"]="$rr" done < <(find "$RECIPES" -name PAGBUILD.yaml) # Instaluje zbudowany pakiet .pag do działającego rootfs (pliki + ldconfig + # hooki post-install). pagbuild tylko PAKUJE do output/ – bez tego kroku # nagłówki/biblioteki zbudowane w tej sesji nie byłyby widoczne dla kolejnych # receptur, przez co zależne pakiety padały na etapie configure/kompilacji. install_pkg_to_rootfs() { # $1 = ścieżka receptury local recipe="$1" pf tmp mark pf="$(pkg_file_for "$recipe")" [[ -n "$pf" && -s "$PKG_OUT/$pf" ]] || return 1 mark="$PKG_OUT/.installed" if grep -qxF "$pf" "$mark" 2>/dev/null; then return 0 fi tmp=$(mktemp -d) tar -xJf "$PKG_OUT/$pf" -C "$tmp" data.tar.xz 2>/dev/null || { rm -rf "$tmp"; return 1; } tar -xJf "$tmp/data.tar.xz" -C "$ROOTFS" 2>/dev/null || { rm -rf "$tmp"; return 1; } # Hooki post-install (np. make-ca, update-mime-database) – w chroot, cwd=/. if [[ -f "$tmp/hooks/post-install" ]]; then local name name="$(pkg_name_for "$recipe")" install -m755 "$tmp/hooks/post-install" "$ROOTFS/tmp/.pag-hook-post-install" chroot "$ROOTFS" /bin/bash -c "cd / && timeout 120 /bin/bash /tmp/.pag-hook-post-install" \ || log "Uwaga: hook post-install $name zwrócił błąd (kontynuuję)" rm -f "$ROOTFS/tmp/.pag-hook-post-install" fi rm -rf "$tmp" chroot "$ROOTFS" /sbin/ldconfig >/dev/null 2>&1 || true echo "$pf" >> "$mark" log "[inst] Zainstalowano w rootfs: $pf" } # Pakiety "bazowe" rdzenia systemu, które rootfs.sh buduje już w LFS (Ch. 6). # Instalowanie ich ponownie do działającego rootfs tworzy drugą, niespójną kopię # (np. glibc w /lib64 vs /usr/lib) i psuje cały system. Lista przez BASE_KEEP. BASE_KEEP="${BASE_KEEP:-glibc}" IFS=' ' read -r -a BASE_KEEP_ARR <<< "$BASE_KEEP" # Wykonuje faktyczny build pakietu w chroot (jako root, w tym chroot). run_pagbuild() { # $1 = rel path w chroot (np. /var/lib/pagan-sync/...) chroot "$ROOTFS" /usr/bin/env -i \ HOME=/root TERM=linux LANG=C LC_ALL=C \ PATH=/usr/local/bin:/usr/bin:/usr/sbin:/bin:/sbin \ CC=/usr/bin/gcc CXX=/usr/bin/g++ AR=/usr/bin/ar RANLIB=/usr/bin/ranlib \ PKG_CONFIG_PATH=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/lib64/pkgconfig:/usr/share/pkgconfig \ PKG_CONFIG_LIBDIR=/usr/lib/x86_64-linux-gnu/pkgconfig:/usr/lib/pkgconfig:/usr/lib64/pkgconfig:/usr/share/pkgconfig \ PKG_CONFIG_SYSROOT_DIR=/ CMAKE_PREFIX_PATH=/usr:/ \ FORCE_UNSAFE_CONFIGURE=1 CFLAGS=-D_GNU_SOURCE CPPFLAGS=-D_GNU_SOURCE \ PAGBUILD_WORK=/var/cache/pagbuild/work \ /usr/bin/pagbuild "$1" --rootfs / --out /var/cache/pagbuild/output } # REKURENCYJNA RESOLUCJA ZALEŻNOŚCI. # Buduje depends/makedepends (szukając receptur po nazwie we wszystkich # katalogach recipes: core/gui/utils), instaluje je do rootfs i dopiero wtedy # buduje pakiet docelowy. Zwraca 0 = gotowe, 1 = nie udało się (błąd budowy # pakietu lub którejś zależności). declare -A DEP_VISIT FAILED_PKGS=() build_with_deps() { # $1 = hostowa ścieżka receptury local recipe="$1" [[ -f "$recipe" ]] || return 0 local pkg_name pkg_file pkg_name="$(pkg_name_for "$recipe")" # Pomijamy pakiety bazowe (stay w rootfs). if [[ -n "$pkg_name" ]] && printf '%s\n' "${BASE_KEEP_ARR[@]}" | grep -qx -- "$pkg_name"; then return 0 fi pkg_file="$(pkg_file_for "$recipe")" # Już zbudowany → nic nie robimy (ale dopilnuj instalacji w rootfs). if [[ -n "$pkg_file" && -s "$PKG_OUT/$pkg_file" ]]; then install_pkg_to_rootfs "$recipe" || true return 0 fi # Chronimy przed cyklami / wielokrotnym wejściem. if [[ ${DEP_VISIT["$recipe"]+x} ]]; then return 0 fi DEP_VISIT["$recipe"]=1 local rel="/var/lib/pagan-sync/recipes/${recipe#"$RECIPES"/}" local dep dep_rec # 1) Rekurencyjnie buduj zależności (szukane po nazwie w core/gui/utils). while IFS= read -r dep; do [[ -z "$dep" ]] && continue dep_rec="${RECIPE_BY_NAME[$dep]:-}" if [[ -n "$dep_rec" ]] && ! build_with_deps "$dep_rec"; then log "[fail] Zależność '$dep' dla $rel nie powstała – pomijam" FAILED_PKGS+=("$rel (zależność: $dep)") return 1 fi done < <(deps_of "$recipe") # 2) Zbuduj pakiet (jeśli nadal brak .pag). if [[ -n "$pkg_file" && -s "$PKG_OUT/$pkg_file" ]]; then install_pkg_to_rootfs "$recipe" || true return 0 fi log "[depe] Buduję zależność: $rel" if ! run_pagbuild "$rel"; then log "[fail] Nie udało się zbudować: $rel" FAILED_PKGS+=("$rel") return 1 fi if ! install_pkg_to_rootfs "$recipe"; then log "[fail] Nie udało się zainstalować do rootfs: $rel" FAILED_PKGS+=("$rel (instalacja)") return 1 fi return 0 } while IFS= read -r recipe; do [[ -f "$recipe" ]] || continue N=$((N+1)); rel="/var/lib/pagan-sync/recipes/${recipe#"$RECIPES"/}" pkg_name="$(pkg_name_for "$recipe")" # Pomiń pakiety bazowe (bez nadpisywania działającego systemu). if [[ -n "$pkg_name" ]] && printf '%s\n' "${BASE_KEEP_ARR[@]}" | grep -qx -- "$pkg_name"; then SKIPPED=$((SKIPPED+1)) log "[$N/$TOTAL] Pomijam bazowy (już w rootfs, nie nadpisuję): $rel" continue fi pkg_file="$(pkg_file_for "$recipe")" if [[ -n "$pkg_file" && -s "$PKG_OUT/$pkg_file" ]]; then SKIPPED=$((SKIPPED+1)) log "[$N/$TOTAL] Pomijam (już zbudowany): $rel" install_pkg_to_rootfs "$recipe" || true continue fi log "[$N/$TOTAL] Natywny build: $rel" # Build z rekurencyjną kontrolą depends/makedepends (zależności są # wyszukiwane po nazwie w core/gui/utils, budowane i instalowane). if ! build_with_deps "$recipe"; then log "[$N/$TOTAL] ❌ POMINIĘTO (błąd powyżej): $rel" continue fi # Po budowie sprawdź, czy pakiet faktycznie powstał. if [[ -n "$pkg_file" && -s "$PKG_OUT/$pkg_file" ]]; then install_pkg_to_rootfs "$recipe" || log "Uwaga: nie udało się zainstalować $rel do rootfs" log "[$N/$TOTAL] OK: $rel" else log "[$N/$TOTAL] ❌ Brak pakietu po buildzie: $rel" FAILED_PKGS+=("$rel (brak pakietu)") fi done < "$ORDER_FILE" rm -f "$ORDER_FILE" if [[ $SKIPPED -gt 0 ]]; then log "Pominięto $SKIPPED już zbudowanych/bazowych pakietów (usuń ich .pag, aby przebudować)" fi # ── RAPORT: pakiety, których nie udało się zbudować ── if [[ ${#FAILED_PKGS[@]} -gt 0 ]]; then log "❌ NIE ZBUDOWANO (${#FAILED_PKGS[@]}):" printf '%s\n' "${FAILED_PKGS[@]}" | sort -u | sed 's/^/ - /' | tee -a "$LOG" log "Wznów skrypt, aby ponowić próbę – zbudowane pakiety zostaną pominięte." else log "✅ Wszystkie pakiety zbudowane pomyślnie." fi log "Walidacja natywnego sysroota" chroot "$ROOTFS" /usr/bin/env -i PATH=/usr/local/bin:/usr/bin:/bin HOME=/root \ bash -lc ' set -e for x in gcc g++ make cmake meson ninja pkg-config python3 perl xsltproc xmllint gperf pagbuild; do command -v "$x" >/dev/null; done pkg-config --exists glib-2.0 pkg-config --exists dbus-1 pkg-config --exists gtk+-3.0 printf "native-builder-rootfs:ok\\n" ' || log "⚠️ Walidacja sysroota niekompletna (nie wszystko się zbudowało – patrz raport powyżej)" # ── Gotowe repo: tylko aktualna wersja per-pakiet + podpisy + repo.json ── REPO_DIR="${NATIVE_REPO_DIR:-$ROOTFS/var/www/repo.paganlinux.eu/stable}" if [[ -d "$PKG_OUT" ]] && compgen -G "$PKG_OUT"/*.pag >/dev/null 2>&1; then log "Buduję gotowe repo: $REPO_DIR" mkdir -p "$REPO_DIR" python3 - "$PKG_OUT" "$REPO_DIR" <<'PY' import os, sys, glob, re, shutil out, repo = sys.argv[1], sys.argv[2] def vkey(v): parts = re.split(r"[.\-_+]", v.lstrip("vV")) k = [] for p in parts: m = re.match(r"^(\d+)(.*)$", p) k.append((1, int(m.group(1)), m.group(2)) if m else (0, p, "")) return tuple(k) best = {} for f in glob.glob(os.path.join(out, "*.pag")): base = os.path.basename(f) m = re.match(r"^(.+)-(\d[\w.\-]*)-(\d+)\.pag$", base) if not m: continue name, ver, rel = m.group(1), m.group(2), int(m.group(3)) cur = best.get(name) if cur is None or (vkey(ver), rel) > (vkey(cur[1]), cur[2]): best[name] = (base, ver, rel) for name, (base, ver, rel) in best.items(): for old in glob.glob(os.path.join(repo, name + "-*.pag")): if os.path.basename(old) == base: continue os.remove(old) for sfx in (".asc", ".sig"): if os.path.exists(old + sfx): os.remove(old + sfx) shutil.copy2(os.path.join(out, base), os.path.join(repo, base)) print(f"[native-rootfs] Repo: {len(best)} pakietów (tylko aktualne wersje)") PY IDX_ARGS=(--out "$REPO_DIR" --repo "$REPO_DIR") if [[ -n "${PAGAN_GPG_KEY:-}" ]]; then IDX_ARGS+=(--gpg-key "$PAGAN_GPG_KEY" --sign-all) log "Podpisywanie pakietów kluczem $PAGAN_GPG_KEY" fi if [[ -x "$SCRIPT_DIR/repo-index.sh" ]]; then "$SCRIPT_DIR/repo-index.sh" "${IDX_ARGS[@]}" || log "Uwaga: repo-index.sh zwrócił błąd" else log "Brak $SCRIPT_DIR/repo-index.sh – pomijam indeks repo" fi else log "Brak pakietów w $PKG_OUT – pomijam generowanie repo" fi log "Natywny rootfs jest gotowy: $ROOTFS" log "Nie podmieniam automatycznie VPS. Spakuj po akceptacji:" log "tar -cJf ${ROOTFS%/}.tar.xz -C $ROOTFS --exclude=./proc --exclude=./sys --exclude=./dev --exclude=./run --exclude=./tmp ."