← pag

Commit 96fa583

0
plików
+0
dodanych
-0
usuniętych
@@ -54,7 +54,7 @@ from urllib.request import urlopen, Request
54 54 import threading, itertools
55 55
56 56 # Wersja klienta – do porównania z repo.json["pag_version"] (self-update)
57 -PAG_VERSION = "3.3.10a"
57 +PAG_VERSION = "3.3.11"
58 58 from urllib.error import URLError, HTTPError
59 59
60 60 # =============================================================================
@@ -179,9 +179,13 @@ def _gpg_run(*args, timeout: int = 30, **kwargs) -> subprocess.CompletedProcess:
179 179 return subprocess.run([GPG_BINARY, *args], timeout=timeout, env=env, **kwargs)
180 180 except FileNotFoundError:
181 181 # GPG nie jest dostępne – zwróć błąd z komunikatem
182 + # (szanuj text=True – inaczej caller dostaje bytes i może wybuchnąć TypeError)
183 + _text = bool(kwargs.get("text") or kwargs.get("universal_newlines"))
184 + _msg = f"GPG binary not found ({GPG_BINARY})"
182 185 return subprocess.CompletedProcess(
183 186 [GPG_BINARY, *args], 127,
184 - stdout=b"", stderr=f"GPG binary not found ({GPG_BINARY})".encode()
187 + stdout=("" if _text else b""),
188 + stderr=(_msg if _text else _msg.encode()),
185 189 )
186 190 except subprocess.TimeoutExpired:
187 191 return subprocess.CompletedProcess(
@@ -242,7 +246,7 @@ T = {
242 246 "en": {
243 247 "root_required": "pag requires root privileges (sudo).",
244 248 "db_locked": "Another pag instance is running.",
245 - "db_lock_hint": "If this is an error, remove: rm {}",
249 + "db_lock_hint": "If no other pag process is running, wait a moment and retry.",
246 250 "no_index": "Cannot fetch repository indexes. Run 'pag update'.",
247 251 "all_installed": "All packages are already installed.",
248 252 "to_install": "To install: {} packages ({:.2f} MB)",
@@ -322,7 +326,7 @@ T = {
322 326 "pl": {
323 327 "root_required": "pag wymaga uprawnień root (sudo).",
324 328 "db_locked": "Inna instancja pag jest uruchomiona.",
325 - "db_lock_hint": "Jeśli to błąd, usuń: rm {}",
329 + "db_lock_hint": "Jeśli żaden inny proces pag nie działa, poczekaj chwilę i spróbuj ponownie.",
326 330 "no_index": "Nie można pobrać indeksów repozytoriów. Uruchom 'pag update'.",
327 331 "all_installed": "Wszystkie pakiety są już zainstalowane.",
328 332 "to_install": "Do zainstalowania: {} pakietów ({:.2f} MB)",
@@ -471,7 +475,6 @@ ALLOWED_PKG_RE = re.compile(r'^[a-zA-Z0-9][a-zA-Z0-9._+@-]*$')
471 475 AUDIT_LOG = "/var/log/pag/audit.log" # dziennik operacji krytycznych (hooki, self-update)
472 476 TRUST_DB = "/etc/pag/trusted.json" # mapa repo_url → fingerprint klucza podpisującego
473 477 HOOK_API_VERSION = "1" # wersjonowane API hooków (env PKG_HOOK_API)
474 -MAX_PKG_SIZE = 2 * 1024 * 1024 * 1024 # 2 GB – maksymalny rozmiar paczki
475 478
476 479 # =============================================================================
477 480 # IMMUTABLE OS – DEPLOYMENTY
@@ -661,10 +664,11 @@ from contextlib import contextmanager
661 664 @contextmanager
662 665 def _db_session():
663 666 """Context manager – gwarantuje zamknięcie połączenia."""
664 - conn = sqlite3.connect(FILES_DB_SQL)
667 + conn = sqlite3.connect(FILES_DB_SQL, timeout=15)
665 668 conn.execute("PRAGMA journal_mode=WAL")
666 669 conn.execute("PRAGMA synchronous=NORMAL")
667 670 conn.execute("PRAGMA foreign_keys=ON")
671 + conn.execute("PRAGMA busy_timeout=15000")
668 672 conn.row_factory = sqlite3.Row
669 673 try:
670 674 yield conn
@@ -705,7 +709,11 @@ def _db_init():
705 709 def _db_record_files(pkg_name: str, files: List[dict]):
706 710 """Zapisuje pliki do SQLite (obsługuje symlinki)."""
707 711 with _db_session() as db:
708 - # Context manager sam zarządza transakcją atomowo
712 + # Jawna transakcja – atomowość obu zapisów i szybsze wykrycie blokady
713 + try:
714 + db.execute("BEGIN IMMEDIATE")
715 + except sqlite3.OperationalError:
716 + pass # transakcja już otwarta (implicit)
709 717 db.executemany(
710 718 "INSERT OR REPLACE INTO files (path, package, sha256, size, is_symlink, symlink_target) "
711 719 "VALUES (?,?,?,?,?,?)",
@@ -750,35 +758,37 @@ def _db_count_files() -> int:
750 758 # =============================================================================
751 759
752 760 class DatabaseLock:
753 - """Blokada oparta na PID-file – niezawodna, bez flock."""
761 + """Blokada plikowa (flock) – jądro zwalnia ją AUTOMATYCZNIE, gdy proces
762 + ginie (kill -9, twardy reset). Stary PID-file miał race condition: po
763 + śmierci pag PID mógł zostać przydzielony obcemu procesowi (PID reuse)
764 + i pag odmawiał działania na zawsze („baza zablokowana”).
765 + """
754 766 def __init__(self):
755 - self._acquired = False
767 + self._f = None
756 768 def __enter__(self):
757 769 os.makedirs(os.path.dirname(LOCK_FILE), exist_ok=True)
758 - if os.path.exists(LOCK_FILE):
759 - try:
760 - old_pid = int(open(LOCK_FILE).read().strip())
761 - os.kill(old_pid, 0) # sygnał 0 = sprawdź czy proces żyje
762 - except (ValueError, OSError, ProcessLookupError):
763 - # Stary PID nie żyje – usuwamy nieświeżą blokadę
764 - try:
765 - os.remove(LOCK_FILE)
766 - except OSError:
767 - pass
768 - else:
769 - print(f"❌ {_('db_locked')}", file=sys.stderr)
770 - print(f" {_('db_lock_hint', LOCK_FILE)}", file=sys.stderr)
771 - sys.exit(1)
772 - with open(LOCK_FILE, "w") as f:
773 - f.write(str(os.getpid()))
774 - self._acquired = True
770 + self._f = open(LOCK_FILE, "w")
771 + try:
772 + # LOCK_NB: rzuca wyjątek zamiast czekać w nieskończoność
773 + fcntl.flock(self._f, fcntl.LOCK_EX | fcntl.LOCK_NB)
774 + except BlockingIOError:
775 + print(f"❌ {_('db_locked')}", file=sys.stderr)
776 + print(f" {_('db_lock_hint', LOCK_FILE)}", file=sys.stderr)
777 + sys.exit(1)
778 + self._f.write(str(os.getpid()))
779 + self._f.flush()
775 780 return self
776 781 def __exit__(self, *args):
777 - if self._acquired:
782 + if self._f:
778 783 try:
779 - os.remove(LOCK_FILE)
784 + fcntl.flock(self._f, fcntl.LOCK_UN)
780 785 except OSError:
781 786 pass
787 + self._f.close()
788 + self._f = None
789 + # Uwaga: NIE usuwamy pliku blokady. Stały plik + flock na inode to jedyny
790 + # bezpieczny wzorzec – os.remove(), gdy inny proces trzyma blokadę na starym
791 + # inode, otwiera wyścig (nowy proces blokowałby nowo utworzony inode).
782 792
783 793 # =============================================================================
784 794 # POMOCNICZE
@@ -897,9 +907,17 @@ def _safe_extractall(tar: tarfile.TarFile, dest: str, *, preserve_perms: bool =
897 907 print(f" BLOCKED (link escape): {name} -> {link}")
898 908 continue
899 909
900 - # Rozpakuj z zachowaniem metadanych
910 + # Rozpakuj z zachowaniem metadanych. Python 3.12+ wymaga jawnego
911 + # `filter=` (inaczej DeprecationWarning, w 3.14+ błąd) – nasza ręczna
912 + # walidacja powyżej już zabezpiecza ścieżki, więc 'fully_trusted'
913 + # (pomija filtr Pythona i zachowuje SUID/SGID/sticky z preserve_perms).
901 914 try:
902 - tar.extract(member, dest, set_attrs=preserve_perms, numeric_owner=False)
915 + if hasattr(tarfile, 'data_filter'):
916 + # Python 3.12+
917 + tar.extract(member, dest, set_attrs=preserve_perms, numeric_owner=False,
918 + filter='fully_trusted')
919 + else:
920 + tar.extract(member, dest, set_attrs=preserve_perms, numeric_owner=False)
903 921 except Exception as e:
904 922 print(f" ⚠ Nie rozpakowano {name}: {e}")
905 923 continue
@@ -1001,7 +1019,7 @@ class PackageInfo:
1001 1019 self.name = d.get("name","?")
1002 1020 self.version = d.get("version","0")
1003 1021 self.description = d.get("description","")
1004 - self.dependencies = d.get("dependencies",[])
1022 + self.dependencies = d.get("dependencies", d.get("depends", []))
1005 1023 self.size_bytes = d.get("size",0)
1006 1024 self.sha256 = d.get("sha256","")
1007 1025 self.gpg_fp = d.get("gpg_fingerprint","")
@@ -1191,7 +1209,7 @@ def _verify_repo_sig(repo_url, cache_path) -> bool:
1191 1209 # jak apt) – gdy w keyringu brakuje klucza (No public key).
1192 1210 res = _gpg_run("--verify", sig_path, cache_path,
1193 1211 capture_output=True, text=True, timeout=30)
1194 - _stderr = (res.stderr or "")
1212 + _stderr = res.stderr.decode(errors="replace") if isinstance(res.stderr, bytes) else (res.stderr or "")
1195 1213 if "public key" in _stderr.lower() and "no public key" in _stderr.lower():
1196 1214 try:
1197 1215 with urlopen(Request(f"{repo_url}/paganos.asc", headers={"User-Agent": "pag/3.0"}), timeout=20) as r:
@@ -1210,7 +1228,11 @@ def _verify_repo_sig(repo_url, cache_path) -> bool:
1210 1228 print(f" ⚠ {repo_url}: nieprawidłowy podpis GPG (PAG_INSECURE – ignoruję)")
1211 1229 return True
1212 1230 os.remove(cache_path)
1213 - print(f" ❌ {repo_url}: NIEPRAWIDŁOWY PODPIS GPG indeksu repozytorium!")
1231 + if not shutil.which(GPG_BINARY):
1232 + print(f" ❌ {repo_url}: GPG nie jest zainstalowane – nie można zweryfikować podpisu!")
1233 + print(f" Zainstaluj gnupg lub ustaw PAG_INSECURE=1 (niezalecane)")
1234 + else:
1235 + print(f" ❌ {repo_url}: NIEPRAWIDŁOWY PODPIS GPG indeksu repozytorium!")
1214 1236 return False
1215 1237
1216 1238 # --- Wymuś przypięty fingerprint (TOFU + pinning) ---
@@ -2033,6 +2055,7 @@ def cmd_install(package_names, as_dep=False, upgrade=False):
2033 2055 "dependencies": pkg.dependencies, "size_bytes": pkg.size_bytes,
2034 2056 "sha256": pkg.sha256, "installed_at": datetime.now().isoformat(),
2035 2057 "repo": "local",
2058 + "provides": getattr(pkg, "provides", None) or [],
2036 2059 }
2037 2060 world.add(pkg.name)
2038 2061 failed_local, _fl = _install_local_pkg_files(local_files, _ok)
@@ -2066,7 +2089,7 @@ def cmd_install(package_names, as_dep=False, upgrade=False):
2066 2089 # i prośby o potwierdzenie (np. `pag install steam` gdy steam nie istnieje).
2067 2090 not_found = []
2068 2091 for n in package_names:
2069 - real = _resolve_provides(n, repo_pkgs)
2092 + real = _resolve_provides(n, repo_pkgs, installed_db)
2070 2093 if real not in repo_pkgs and real not in installed_db \
2071 2094 and not os.path.exists(os.path.abspath(n)):
2072 2095 not_found.append(n)
@@ -2228,6 +2251,7 @@ def cmd_install(package_names, as_dep=False, upgrade=False):
2228 2251 "dependencies": pkg.dependencies, "size_bytes": pkg.size_bytes,
2229 2252 "sha256": pkg.sha256, "installed_at": datetime.now().isoformat(),
2230 2253 "repo": pkg.repo_url,
2254 + "provides": getattr(pkg, "provides", None) or [],
2231 2255 }
2232 2256 if not as_dep and name in package_names:
2233 2257 world.add(name)
@@ -2461,7 +2485,7 @@ def cmd_self_update():
2461 2485 # Automatyczny import klucza (TOFU) – jak w _verify_repo_sig
2462 2486 res = _gpg_run("--verify", dst_new + ".asc", dst_new,
2463 2487 capture_output=True, text=True)
2464 - _stderr = (res.stderr or "")
2488 + _stderr = res.stderr.decode(errors="replace") if isinstance(res.stderr, bytes) else (res.stderr or "")
2465 2489 if "public key" in _stderr.lower() and "no public key" in _stderr.lower():
2466 2490 try:
2467 2491 with urlopen(Request(f"{base}/paganos.asc", headers={"User-Agent": "pag/3.0"}), timeout=20) as r:
@@ -2638,12 +2662,12 @@ def _smart_search(query: str) -> int:
2638 2662 if query.lower() in n.lower() or query.lower() in p.description.lower()]
2639 2663
2640 2664 # 2. Flathub (jeśli dostępny)
2641 - flat = _flatpak_search_raw(query) if _check_flatpak() else []
2665 + flat = _flatpak_search_raw(query) if _check_flatpak(quiet=True) else []
2642 2666
2643 2667 if not repo_lower and not flat:
2644 2668 print(f"\n ❌ '{query}' — nie znaleziono.")
2645 2669 print(f" Repo PaganOS: pag search {query}")
2646 - if _check_flatpak():
2670 + if _check_flatpak(quiet=True):
2647 2671 print(f" Flathub: pag flatpak search {query}")
2648 2672 print(f" Dodaj repo: pag repo-add <url>")
2649 2673 return 1
@@ -2818,21 +2842,206 @@ PROVIDES_MAP = {
2818 2842 "pkgconfig(harfbuzz)": "harfbuzz",
2819 2843 "pkgconfig(cairo)": "cairo",
2820 2844 "pkgconfig(pango)": "pango",
2845 + "pkgconfig(xt)": "xorg-libxt",
2846 + "pkgconfig(xmu)": "xorg-libxmu",
2847 + "pkgconfig(ice)": "xorg-libice",
2848 + "pkgconfig(sm)": "xorg-libsm",
2849 + "pkgconfig(x11)": "xorg-libx11",
2850 + "pkgconfig(xext)": "xorg-libxext",
2851 + "pkgconfig(xrandr)": "xorg-libxrandr",
2852 + "pkgconfig(xfixes)": "xorg-libxfixes",
2853 + "pkgconfig(xcursor)": "xorg-libxcursor",
2854 + "pkgconfig(xinerama)": "xorg-libxinerama",
2855 + "pkgconfig(xrender)": "xorg-libxrender",
2856 + "pkgconfig(xau)": "xorg-libxau",
2857 + "pkgconfig(xcb)": "xorg-libxcb",
2858 + "pkgconfig(xdamage)": "xorg-libxdamage",
2859 + "pkgconfig(xcomposite)": "xorg-libxcomposite",
2860 + "pkgconfig(xft)": "xorg-libxft",
2861 + "pkgconfig(xss)": "xorg-libxss",
2862 + "pkgconfig(libsoup-3.0)": "libsoup3",
2863 + "pkgconfig(libsoup-2.4)": "libsoup2",
2864 + "pkgconfig(gdk-pixbuf-2.0)": "gdk-pixbuf2",
2865 + "pkgconfig(libpng)": "libpng",
2866 + "pkgconfig(libjpeg)": "libjpeg-turbo",
2867 + "pkgconfig(libtiff-4)": "libtiff",
2868 + "pkgconfig(ffi)": "libffi",
2869 + # ── system / baza ──
2870 + "pkgconfig(libcrypto)": "openssl",
2871 + "pkgconfig(libssl)": "openssl",
2872 + "pkgconfig(libudev)": "systemd",
2873 + "pkgconfig(libmount)": "util-linux",
2874 + "pkgconfig(libblkid)": "util-linux",
2875 + "pkgconfig(uuid)": "util-linux",
2876 + "pkgconfig(libexpat)": "expat",
2877 + "pkgconfig(libpcre)": "pcre",
2878 + "pkgconfig(ncursesw)": "ncurses",
2879 + "pkgconfig(tinfo)": "ncurses",
2880 + "pkgconfig(panel)": "ncurses",
2881 + "pkgconfig(readline)": "readline",
2882 + "pkgconfig(libseccomp)": "libseccomp",
2883 + "pkgconfig(pam)": "linux-pam",
2884 + "pkgconfig(libxcrypt)": "libxcrypt",
2885 + "pkgconfig(libcrypt)": "libxcrypt",
2886 + "pkgconfig(libnsl)": "libnsl",
2887 + "pkgconfig(liblz4)": "lz4",
2888 + "pkgconfig(libevent)": "libevent",
2889 + "pkgconfig(libarchive)": "libarchive",
2890 + "pkgconfig(sqlite3)": "sqlite",
2891 + "pkgconfig(libpq)": "postgresql",
2892 + "pkgconfig(mysqlclient)": "mariadb",
2893 + "pkgconfig(json-c)": "json-c",
2894 + "pkgconfig(json-glib-1.0)": "json-glib",
2895 + "pkgconfig(libunistring)": "libunistring",
2896 + "pkgconfig(libidn2)": "libidn2",
2897 + "pkgconfig(libpsl)": "libpsl",
2898 + "pkgconfig(icu-uc)": "icu",
2899 + "pkgconfig(icu-i18n)": "icu",
2900 + "pkgconfig(icu-io)": "icu",
2901 + "pkgconfig(gnutls)": "gnutls",
2902 + "pkgconfig(nettle)": "nettle",
2903 + "pkgconfig(hogweed)": "nettle",
2904 + "pkgconfig(libgcrypt)": "libgcrypt",
2905 + "pkgconfig(libgpg-error)": "libgpg-error",
2906 + "pkgconfig(libassuan)": "libassuan",
2907 + "pkgconfig(libusb-1.0)": "libusb",
2908 + "pkgconfig(libusb)": "libusb",
2909 + "pkgconfig(libgudev-1.0)": "libgudev",
2910 + "pkgconfig(gudev-1.0)": "libgudev",
2911 + "pkgconfig(polkit-gobject-1)": "polkit",
2912 + "pkgconfig(polkit-agent-1)": "polkit",
2913 + "pkgconfig(libpciaccess)": "libpciaccess",
2914 + "pkgconfig(pixman-1)": "pixman",
2915 + "pkgconfig(libdrm)": "libdrm",
2916 + "pkgconfig(libva)": "libva",
2917 + "pkgconfig(libva-drm)": "libva",
2918 + "pkgconfig(libva-x11)": "libva",
2919 + "pkgconfig(libva-wayland)": "libva",
2920 + "pkgconfig(vdpau)": "libvdpau",
2921 + "pkgconfig(libvdpau)": "libvdpau",
2922 + "pkgconfig(libinput)": "libinput",
2923 + "pkgconfig(libevdev)": "libevdev",
2924 + "pkgconfig(mtdev)": "mtdev",
2925 + # ── grafika / GL / multimedia ──
2926 + "pkgconfig(gbm)": "mesa",
2927 + "pkgconfig(gl)": "libglvnd",
2928 + "pkgconfig(egl)": "libglvnd",
2929 + "pkgconfig(glesv2)": "libglvnd",
2930 + "pkgconfig(glx)": "libglvnd",
2931 + "pkgconfig(vulkan)": "vulkan-loader",
2932 + "pkgconfig(libxkbcommon)": "libxkbcommon",
2933 + "pkgconfig(xkbcommon)": "libxkbcommon",
2934 + "pkgconfig(xkbcommon-x11)": "libxkbcommon",
2935 + "pkgconfig(xcb)": "xorg-libxcb",
2936 + "pkgconfig(xcb-util)": "xcb-util",
2937 + "pkgconfig(xcb-keysyms)": "xcb-util-keysyms",
2938 + "pkgconfig(xcb-icccm)": "xcb-util-wm",
2939 + "pkgconfig(xcb-cursor)": "xcb-util-cursor",
2940 + "pkgconfig(xcb-renderutil)": "xcb-util-renderutil",
2941 + "pkgconfig(xcb-image)": "xcb-util-image",
2942 + "pkgconfig(xcb-errors)": "xcb-util-errors",
2943 + "pkgconfig(wayland-client)": "wayland",
2944 + "pkgconfig(wayland-server)": "wayland",
2945 + "pkgconfig(wayland-cursor)": "wayland",
2946 + "pkgconfig(wayland-egl)": "wayland",
2947 + "pkgconfig(wayland-protocols)": "wayland-protocols",
2948 + "pkgconfig(gstreamer-1.0)": "gstreamer",
2949 + "pkgconfig(gstreamer-base-1.0)": "gstreamer",
2950 + "pkgconfig(gstreamer-check-1.0)": "gstreamer",
2951 + "pkgconfig(gstreamer-controller-1.0)": "gstreamer",
2952 + "pkgconfig(gstreamer-app-1.0)": "gst-plugins-base",
2953 + "pkgconfig(gstreamer-video-1.0)": "gst-plugins-base",
2954 + "pkgconfig(gstreamer-audio-1.0)": "gst-plugins-base",
2955 + "pkgconfig(gstreamer-pbutils-1.0)": "gst-plugins-base",
2956 + "pkgconfig(gstreamer-fft-1.0)": "gst-plugins-base",
2957 + "pkgconfig(gstreamer-riff-1.0)": "gst-plugins-base",
2958 + "pkgconfig(gstreamer-rtp-1.0)": "gst-plugins-base",
2959 + "pkgconfig(gstreamer-rtsp-1.0)": "gst-plugins-base",
2960 + "pkgconfig(gstreamer-sdp-1.0)": "gst-plugins-base",
2961 + "pkgconfig(gstreamer-net-1.0)": "gst-plugins-base",
2962 + "pkgconfig(gstreamer-gl-1.0)": "gst-plugins-base",
2963 + "pkgconfig(libpulse)": "libpulse",
2964 + "pkgconfig(libpulse-simple)": "libpulse",
2965 + "pkgconfig(libpulse-mainloop-glib)": "libpulse",
2966 + "pkgconfig(alsa)": "alsa-lib",
2967 + "pkgconfig(jack)": "jack2",
2968 + "pkgconfig(libsamplerate)": "libsamplerate",
2969 + "pkgconfig(sndfile)": "libsndfile",
2970 + "pkgconfig(libavcodec)": "ffmpeg",
2971 + "pkgconfig(libavformat)": "ffmpeg",
2972 + "pkgconfig(libavutil)": "ffmpeg",
2973 + "pkgconfig(libavfilter)": "ffmpeg",
2974 + "pkgconfig(libswscale)": "ffmpeg",
2975 + "pkgconfig(libswresample)": "ffmpeg",
2976 + "pkgconfig(libpostproc)": "ffmpeg",
2977 + "pkgconfig(SDL2)": "sdl2",
2978 + "pkgconfig(SDL)": "sdl",
2979 + "pkgconfig(SDL2_image)": "sdl2-image",
2980 + "pkgconfig(SDL2_ttf)": "sdl2-ttf",
2981 + "pkgconfig(SDL2_mixer)": "sdl2-mixer",
2982 + "pkgconfig(SDL2_net)": "sdl2-net",
2983 + "pkgconfig(libpng16)": "libpng",
2984 + "pkgconfig(libwebp)": "libwebp",
2985 + "pkgconfig(libwebpmux)": "libwebp",
2986 + "pkgconfig(libwebpdemux)": "libwebp",
2987 + "pkgconfig(libopenjp2)": "openjpeg2",
2988 + "pkgconfig(lcms2)": "lcms2",
2989 + "pkgconfig(libheif)": "libheif",
2990 + "pkgconfig(libde265)": "libde265",
2991 + "pkgconfig(x264)": "x264",
2992 + "pkgconfig(x265)": "x265",
2993 + # ── glib / gio ──
2994 + "pkgconfig(gio-unix-2.0)": "glib",
2995 + "pkgconfig(gmodule-2.0)": "glib",
2996 + "pkgconfig(gthread-2.0)": "glib",
2997 + "pkgconfig(girepository-2.0)": "gobject-introspection",
2998 + "pkgconfig(girepository-1.0)": "gobject-introspection",
2999 + "pkgconfig(libglib-2.0)": "glib",
3000 + "pkgconfig(libgobject-2.0)": "glib",
2821 3001 }
2822 3002
2823 -def _resolve_provides(name: str, repo: dict) -> str:
2824 - """Rozwija wirtualną nazwę pakietu do rzeczywistej nazwy z repo."""
3003 +def _resolve_provides(name: str, repo: dict, installed: Optional[dict] = None) -> str:
3004 + """Rozwija wirtualną nazwę pakietu do rzeczywistej nazwy.
3005 +
3006 + Kolejność: repo → PROVIDES_MAP → wzorce → provides z repo.json →
3007 + provides ZAINSTALOWANYCH pakietów (lokalnie zbudowane poza repo też
3008 + dostarczają wirtualne zależności) → fallback pkgconfig (czyszczenie nazwy).
3009 + """
2825 3010 if name in repo:
2826 3011 return name
2827 3012 if name in PROVIDES_MAP:
2828 3013 real = PROVIDES_MAP[name]
2829 3014 if real in repo:
2830 3015 return real
3016 + # Wzorce: moduły Qt (Qt5Core/Qt6Widgets) i GStreamer (gstreamer-video-1.0)
3017 + if name.startswith("pkgconfig(Qt5"):
3018 + real = "qt5"
3019 + if real in repo:
3020 + return real
3021 + if name.startswith("pkgconfig(Qt6"):
3022 + real = "qt6"
3023 + if real in repo:
3024 + return real
3025 + if name.startswith("pkgconfig(gstreamer-") and name.endswith("-1.0)"):
3026 + real = "gstreamer"
3027 + if real in repo:
3028 + return real
3029 + if name.startswith("pkgconfig(gst-"):
3030 + real = "gst-plugins-base"
3031 + if real in repo:
3032 + return real
2831 3033 # Dynamiczne provides z repo.json (sekcja provides: w PAGBUILD.yaml)
2832 3034 for _pkg_name, _pkg in repo.items():
2833 3035 _provs = getattr(_pkg, "provides", None) or []
2834 3036 if name in _provs:
2835 3037 return _pkg_name
3038 + # provides ZAINSTALOWANYCH pakietów – lokalnie zbudowane (pagbuild, poza
3039 + # repo) też dostarczają wirtualne zależności i muszą być rozpoznawane.
3040 + if installed:
3041 + for _pkg_name, _meta in installed.items():
3042 + _provs = _meta.get("provides") or [] if isinstance(_meta, dict) else []
3043 + if name in _provs:
3044 + return _pkg_name
2836 3045 clean = name
2837 3046 if name.startswith("pkgconfig(") and ")" in name:
2838 3047 clean = name.split("(", 1)[1].rstrip(")")
@@ -2958,9 +3167,11 @@ def cmd_repo_add(url, name=None):
2958 3167 def cmd_repo_list():
2959 3168 for i, url in enumerate(get_repos(), 1): print(f" {i}. {url}")
2960 3169
2961 -def _check_flatpak():
3170 +def _check_flatpak(quiet: bool = False):
2962 3171 if not shutil.which("flatpak"):
2963 - print(f"❌ {_('flatpak_missing')}"); return False
3172 + if not quiet:
3173 + print(f"❌ {_('flatpak_missing')}")
3174 + return False
2964 3175 r = subprocess.run(["flatpak","remotes"], capture_output=True, text=True)
2965 3176 if "flathub" not in r.stdout:
2966 3177 print(f"⚠ {_('flatpak_adding')}")
@@ -3537,9 +3748,16 @@ def _rebuild_initramfs(deploy_dir: str = "") -> bool:
3537 3748 dst_sub = os.path.join(modules_dst, sub)
3538 3749 os.makedirs(os.path.dirname(dst_sub), exist_ok=True)
3539 3750 if os.path.isdir(src_sub):
3540 - shutil.copytree(src_sub, dst_sub, dirs_exist_ok=True, symlinks=True)
3751 + try:
3752 + shutil.copytree(src_sub, dst_sub, dirs_exist_ok=True, symlinks=True,
3753 + ignore_dangling_symlinks=True)
3754 + except (FileNotFoundError, PermissionError):
3755 + print(f" ⚠ Pomijam niedostępne pliki: {sub}")
3541 3756 else:
3542 - shutil.copy2(src_sub, dst_sub)
3757 + try:
3758 + shutil.copy2(src_sub, dst_sub)
3759 + except (FileNotFoundError, PermissionError):
3760 + print(f" ⚠ Pomijam niedostępny plik: {sub}")
3543 3761
3544 3762 # Pakuj do initramfs.img
3545 3763 initramfs_path = os.path.join(boot_dir, "initramfs.img")
@@ -3564,13 +3782,15 @@ def _rebuild_initramfs(deploy_dir: str = "") -> bool:
3564 3782
3565 3783
3566 3784 def _run_cpio_pipeline(tmpdir: str, out):
3567 - """find . | cpio -oH newc | gzip — bez shell=True.
3785 + """find . -print0 | cpio --null -oH newc | gzip — bez shell=True.
3568 3786
3569 3787 Buduje pipeline przez subprocess.Popen, unikając pośrednika powłoki
3570 3788 (brak ryzyka injection i niepotrzebnego procesu sh). Wykonuje się w cwd=tmpdir.
3789 + Separatory NUL (\0): plik/katalog ze znakiem nowej linii w nazwie nie
3790 + rozjeżdża cpio (inaczej uszkodzone archiwum → kernel panic przy rozruchu).
3571 3791 """
3572 - find = subprocess.Popen(["find", "."], cwd=tmpdir, stdout=subprocess.PIPE)
3573 - cpio = subprocess.Popen(["cpio", "-oH", "newc"], cwd=tmpdir,
3792 + find = subprocess.Popen(["find", ".", "-print0"], cwd=tmpdir, stdout=subprocess.PIPE)
3793 + cpio = subprocess.Popen(["cpio", "--null", "-oH", "newc"], cwd=tmpdir,
3574 3794 stdin=find.stdout, stdout=subprocess.PIPE)
3575 3795 find.stdout.close() # zwolnij uchwyt – cpio dostanie SIGPIPE po zakończeniu find
3576 3796 gzip = subprocess.Popen(["gzip"], stdin=cpio.stdout, stdout=out)
@@ -3824,13 +4044,13 @@ def _resolve_deps(names, repo, installed):
3824 4044 if name in visited: return
3825 4045
3826 4046 # Rozwijanie wirtualnych zależności przez provides
3827 - target = _resolve_provides(name, repo)
4047 + target = _resolve_provides(name, repo, installed)
3828 4048
3829 4049 if target in visited: return
3830 4050 visited.add(target)
3831 4051 if target in repo:
3832 4052 for dep in repo[target].dependencies:
3833 - real_dep = _resolve_provides(dep, repo)
4053 + real_dep = _resolve_provides(dep, repo, installed)
3834 4054 real_target = real_dep if real_dep in repo else dep
3835 4055
3836 4056 # Sprawdź czy zależność jest dostępna
@@ -3877,7 +4097,7 @@ def _verify_dependencies(to_install: list, repo: dict, installed: dict) -> int:
3877 4097 for dep in pkg.dependencies:
3878 4098 if dep in SYSTEM_BASE:
3879 4099 continue # bazowy system dostarcza tę zależność
3880 - real_dep = _resolve_provides(dep, repo)
4100 + real_dep = _resolve_provides(dep, repo, installed)
3881 4101 # Sprawdź czy zależność jest dostępna (w repo lub już zainstalowana)
3882 4102 in_repo = real_dep in repo
3883 4103 in_installed = real_dep in installed