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