#!/bin/bash # ============================================================================= # install-pag-fix.sh – bootstrap klienta `pag` na systemie, gdzie pag jest # zepsuty lub stary (brak HTTPS przez brak modułu ssl, wersja bez poprawek). # # Co robi (w tej kolejności): # 1. instaluje plik TAM, GDZIE ROBI TO PAKIET (/usr/bin/pag) – bez drugiej # kopii w /usr/local/bin/pag, bo „cień” powoduje, że `pag --version` # pokazuje inną wersję niż pakiet, a `pag upgrade` nie ma czego ruszyć; # 2. normalizuje końce linii (CRLF → LF) i twardo sprawdza shebang – inaczej # kernel przekazuje „python3\r” i dostajemy: env: 'python3\r': No such file; # 3. REJESTRUJE zainstalowaną wersję w bazie pakietów (/var/lib/pag), żeby # pag był zarządzany tak jak każdy inny pakiet i dał się aktualizować # później przez: sudo pag update → sudo pag upgrade # Rejestracja jest OFFLINE (bez repo, bez sieci) i ustawia wersję na tę, # którą naprawdę wgraliśmy – dzięki temu `pag upgrade` nie zrobi # downgrade'u do starszego paga z repo, a podciągnie go, gdy wyjdzie nowszy. # # Użycie: # sudo ./install-pag-fix.sh [--src ŚCIEŻKA] [--keep-local] [--no-register] # [--from-repo] # # --src PATH źródłowy skrypt pag (domyślnie: ./pag/pag obok tego skryptu) # --keep-local świadomie NIE usuwaj cienia /usr/local/bin/pag # --no-register nie dotykaj bazy pakietów (tylko podmiana pliku) # --from-repo zamiast rejestracji offline uruchom `pag update` + # `pag install -f pag` (pełna ścieżka pakietowa: files.db, # hooki, podpis). Wymaga repo z pag >= wgranej wersji, # bo `-f` zainstaluje to, co jest w repo (możliwy downgrade). # ============================================================================= set -euo pipefail SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)" SRC="$SCRIPT_DIR/pag/pag" DEST="/usr/bin/pag" # ścieżka zarządzana przez pakiet (receptura: install -Dm755 pag /usr/bin/pag) KEEP_LOCAL=0 REGISTER=1 FROM_REPO=0 PAG_DB_DIR="${PAG_DB_DIR:-/var/lib/pag}" usage() { sed -n '2,34p' "$0"; } while [[ $# -gt 0 ]]; do case "$1" in --src) SRC="$2"; shift 2 ;; --keep-local) KEEP_LOCAL=1; shift ;; --no-register) REGISTER=0; shift ;; --from-repo) FROM_REPO=1; shift ;; -h|--help) usage; exit 0 ;; *) echo "Nieznany argument: $1" >&2; exit 2 ;; esac done [[ $EUID -eq 0 ]] || { echo "❌ Wymagane uprawnienia root (sudo)." >&2; exit 1; } [[ -f "$SRC" ]] || { echo "❌ Nie znaleziono źródła: $SRC (użyj --src /ścieżka/pag)" >&2; exit 1; } grep -q 'PAG_VERSION = ' "$SRC" || { echo "❌ $SRC nie wygląda na skrypt pag." >&2; exit 1; } VER_SRC="$(grep -m1 'PAG_VERSION = ' "$SRC" | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' || echo '?')" VER_OLD="" [[ -x "$DEST" ]] && VER_OLD="$("$DEST" --version 2>/dev/null || true)" # ── 1. Normalizacja końców linii ──────────────────────────────────────────── # Źródło z CRLF psuje shebang: kernel przekazuje „python3\r” i dostajemy # env: 'python3\r': No such file or directory # Zawsze instalujemy plik z LF, niezależnie od tego, jak zapisano źródło. _tmp="$(mktemp)"; chmod 600 "$_tmp" trap 'rm -f "$_tmp"' EXIT if grep -q $'\r' "$SRC"; then echo "⚠ Źródło ma końce linii CRLF – normalizuję do LF" fi sed 's/\r$//' "$SRC" > "$_tmp" head -1 "$_tmp" | grep -q '^#!/usr/bin/env python3$' \ || { echo "❌ Po normalizacji shebang nieprawidłowy: $(head -1 "$_tmp" | cat -A)" >&2; exit 1; } # ── 2. Kontrola składni PRZED podmianą ────────────────────────────────────── python3 -m py_compile "$_tmp" \ || { echo "❌ Błąd składni w źródle – nie instaluję uszkodzonego pliku." >&2; exit 1; } rm -rf "$(dirname "$_tmp")/__pycache__" 2>/dev/null || true # ── 3. Instalacja atomowa do ścieżki pakietu ──────────────────────────────── echo "→ Instaluję pag $VER_SRC → $DEST${VER_OLD:+ (poprzednio: $VER_OLD)}" install -Dm755 "$_tmp" "$DEST.new" mv -f "$DEST.new" "$DEST" hash -r 2>/dev/null || true # ── 4. Zdejmij cień innej kopii `pag` z PATH ──────────────────────────────── # Każda INNA kopia przesłania wersję pakietową (typowy efekt # „pag --version pokazuje inną wersję niż zainstalowany pakiet”). DEST_REAL="$(readlink -f "$DEST")" if [[ $KEEP_LOCAL -eq 0 ]]; then for cand in /usr/local/bin/pag /usr/local/sbin/pag /usr/sbin/pag /bin/pag; do [[ -e "$cand" ]] || continue [[ "$(readlink -f "$cand" 2>/dev/null)" == "$DEST_REAL" ]] && continue rm -f "$cand" echo " ✓ Usunięto cień: $cand" done while read -r p; do [[ -n "$p" ]] || continue [[ "$(readlink -f "$p" 2>/dev/null)" == "$DEST_REAL" ]] && continue rm -f "$p" echo " ✓ Usunięto cień: $p" done < <(type -a -p pag 2>/dev/null | sort -u || true) fi # ── 5. Rejestracja w bazie pakietów (offline) ─────────────────────────────── # Ustawiamy w installed.json wersję zgodną z faktycznie wgranym plikiem. # `pag upgrade` bierze listę do aktualizacji z installed.json, więc bez tego # pag nie byłby „widziany” przez aktualizacje (albo cofałby się do starszej # wersji z repo). Bez sieci, bez GPG – czysty, lokalny wpis. if [[ $REGISTER -eq 1 && $FROM_REPO -eq 0 ]]; then python3 - "$DEST" "$VER_SRC" "$PAG_DB_DIR" <<'PY' import datetime, hashlib, json, os, shutil, sys dest, ver, db_dir = sys.argv[1], sys.argv[2], sys.argv[3] db_path = os.path.join(db_dir, "installed.json") world_path = os.path.join(db_dir, "world") with open(dest, "rb") as fh: sha = hashlib.sha256(fh.read()).hexdigest() size = os.path.getsize(dest) os.makedirs(db_dir, exist_ok=True) db = {} if os.path.exists(db_path): try: with open(db_path, encoding="utf-8") as fh: db = json.load(fh) if not isinstance(db, dict): db = {} except (OSError, ValueError): db = {} # Kopia bezpieczeństwa przed edycją (baza jest krytyczna dla paga). try: shutil.copy2(db_path, db_path + ".bak") except OSError: pass entry = dict(db.get("pag") or {}) entry.update({ "version": ver, "release": entry.get("release", 1), "description": entry.get("description") or "PaganOS package manager – atomowa instalacja, weryfikacja GPG, i18n (PL/EN), rollback", "dependencies": ["python3", "gnupg"], "size_bytes": size, "sha256": sha, "installed_at": datetime.datetime.now().isoformat(), }) entry.setdefault("repo", "local") entry.setdefault("provides", []) entry.setdefault("provides_so", []) entry.setdefault("requires_so", []) db["pag"] = entry new = db_path + ".new" with open(new, "w", encoding="utf-8") as fh: json.dump(db, fh, indent=2) os.replace(new, db_path) # `world` = pakiety zainstalowane świadomie; pag ma tam być, żeby nie został # uznany za sierotę przy `pag autoremove`. names = set() if os.path.exists(world_path): try: names = {l.strip() for l in open(world_path, encoding="utf-8") if l.strip()} except OSError: names = set() if "pag" not in names: names.add("pag") with open(world_path, "w", encoding="utf-8") as fh: fh.write("\n".join(sorted(names)) + "\n") print(f" ✓ Zarejestrowano pag {ver} w bazie pakietów ({db_path})") PY elif [[ $FROM_REPO -eq 1 ]]; then echo "→ Rejestruję przez repo (pag update + pag install -f pag)..." "$DEST" update || true "$DEST" install -f pag elif [[ $REGISTER -eq 0 ]]; then echo " (pominięto rejestrację w bazie pakietów – --no-register)" fi # ── 6. Weryfikacja ────────────────────────────────────────────────────────── echo echo "✅ Zainstalowano: $DEST ($("$DEST" --version))" if python3 -c 'import ssl' 2>/dev/null; then echo " ✓ HTTPS (moduł ssl) działa" else echo " ⚠ Python bez działającego modułu ssl – HTTPS w pag nie zadziała." echo " Odtwórz openssl: sudo pag install -f openssl" fi other="$(type -a -p pag 2>/dev/null | head -1 || true)" if [[ -n "$other" && "$(readlink -f "$other")" != "$DEST_REAL" ]]; then echo " ⚠ W PATH pierwszy jest inny pag: $other → usuń go (sudo rm $other)" fi echo echo "Test: składnia + symulacja braku GPG..." python3 -m py_compile "$DEST" && echo " ✓ py_compile OK" GNUPGHOME=/tmp/pag-gpg-test PAG_INSECURE=1 python3 - "$DEST" <<'PY' import os, sys, tempfile src = open(sys.argv[1], encoding="utf-8").read() idx = src.find('if __name__ == "__main__":') assert idx != -1, "nie znaleziono bloku __main__" # __file__ musi być w przestrzeni exec – _load_lang_files() używa go przy imporcie ns = {"__file__": os.path.abspath(sys.argv[1]), "__name__": "pag"} exec(compile(src[:idx], "pag", "exec"), ns) ns["GPG_BINARY"] = "/nonexistent/gpg" ns["GPG_HOME"] = tempfile.mkdtemp() res = ns["_gpg_run"]("--verify", "a.asc", "b", capture_output=True, text=True, timeout=10) assert isinstance(res.stderr, str), "stderr nie jest str!" ok, fp = ns["_gpg_verify_fp"]("a.asc", "b") assert ok is False print(" ✓ OK – brak TypeError przy braku gpg") PY # ── 7. Co dalej ───────────────────────────────────────────────────────────── echo if [[ $REGISTER -eq 1 && $FROM_REPO -eq 0 ]]; then echo "pag jest teraz zarządzany przez menedżera pakietów. Gdy repo opublikuje" echo "nowszą wersję paga, wystarczy:" echo " sudo pag update # pokaże: pag X → Y" echo " sudo pag upgrade # podciągnie paga razem z resztą systemu" echo echo "Jeśli chcesz od razu pełną ścieżkę pakietową (files.db + hooki + podpis)," echo "po wypchnięciu 3.3.20 do repo uruchom ten skrypt z --from-repo." else echo "Aby pag był zarządzany przez menedżera pakietów, uruchom bez --no-register." fi echo "W tej powłoce odśwież cache poleceń: hash -r (albo otwórz nowy terminal)."