← pag

Commit 7ad8c31

0
plików
+0
dodanych
-0
usuniętych
@@ -1015,7 +1015,8 @@ def save_json(path, data):
1015 1015
1016 1016 class PackageInfo:
1017 1017 __slots__ = ("name","version","description","dependencies",
1018 - "size_bytes","sha256","gpg_fp","repo_url","filename","provides","license")
1018 + "size_bytes","sha256","gpg_fp","repo_url","filename","provides","license",
1019 + "provides_so","requires_so")
1019 1020 def __init__(self, d, repo=""):
1020 1021 self.name = d.get("name","?")
1021 1022 self.version = d.get("version","0")
@@ -1028,6 +1029,8 @@ class PackageInfo:
1028 1029 self.filename = d.get("filename", f"{self.name}-{self.version}{PKG_EXT}")
1029 1030 self.provides = d.get("provides", []) or []
1030 1031 self.license = d.get("license", []) or []
1032 + self.provides_so = d.get("provides_so", []) or []
1033 + self.requires_so = d.get("requires_so", []) or []
1031 1034
1032 1035 # =============================================================================
1033 1036 # REPOZYTORIA (cache, ETag, GPG)
@@ -2099,6 +2102,8 @@ def cmd_install(package_names, as_dep=False, upgrade=False):
2099 2102 "sha256": pkg.sha256, "installed_at": datetime.now().isoformat(),
2100 2103 "repo": "local",
2101 2104 "provides": getattr(pkg, "provides", None) or [],
2105 + "provides_so": getattr(pkg, "provides_so", None) or [],
2106 + "requires_so": getattr(pkg, "requires_so", None) or [],
2102 2107 }
2103 2108 world.add(pkg.name)
2104 2109 failed_local, _fl = _install_local_pkg_files(local_files, _ok)
@@ -2169,6 +2174,11 @@ def cmd_install(package_names, as_dep=False, upgrade=False):
2169 2174 print(f" Zainstaluj brakujące pakiety lub dodaj repozytoria.")
2170 2175 return 1
2171 2176
2177 + so_missing = _verify_so_deps(to_install, repo_pkgs, installed_db)
2178 + if so_missing > 0:
2179 + print(" Zainstaluj dostawcę biblioteki lub zaktualizuj repozytorium.")
2180 + return 1
2181 +
2172 2182 if not to_install:
2173 2183 print(f"✅ {_('all_installed')}"); return 0
2174 2184
@@ -2297,6 +2307,8 @@ def cmd_install(package_names, as_dep=False, upgrade=False):
2297 2307 "sha256": pkg.sha256, "installed_at": datetime.now().isoformat(),
2298 2308 "repo": pkg.repo_url,
2299 2309 "provides": getattr(pkg, "provides", None) or [],
2310 + "provides_so": getattr(pkg, "provides_so", None) or [],
2311 + "requires_so": getattr(pkg, "requires_so", None) or [],
2300 2312 }
2301 2313 if not as_dep and name in package_names:
2302 2314 world.add(name)
@@ -2355,6 +2367,9 @@ def cmd_install(package_names, as_dep=False, upgrade=False):
2355 2367 print(f" 💡 Restart wymagany do przeładowania systemu.")
2356 2368 else:
2357 2369 _refresh_dynamic_linker_cache()
2370 + # Hooki zbiorcze – raz na transakcję (fc-cache itp.), tylko gdy pliki
2371 + # trafiły do realnego systemu (nie do deploymentu).
2372 + _process_triggers(all_installed_files)
2358 2373
2359 2374 print(f"\n✅ {_('installed', len(to_install))}")
2360 2375 return 0
@@ -2429,6 +2444,7 @@ def cmd_remove(package_names):
2429 2444 world = load_world()
2430 2445 snapshot = json.loads(json.dumps(installed_db))
2431 2446 removed = []
2447 + removed_files = []
2432 2448
2433 2449 total = len(package_names)
2434 2450 for i, name in enumerate(package_names, 1):
@@ -2445,10 +2461,11 @@ def cmd_remove(package_names):
2445 2461 # Pre-remove hook (jeśli dostępny w staging)
2446 2462 _run_hook_for_installed(name, "pre-remove")
2447 2463
2448 - count, _ = _safe_remove_files(name, installed_db)
2464 + count, rm_files = _safe_remove_files(name, installed_db)
2449 2465 del installed_db[name]
2450 2466 world.discard(name)
2451 2467 removed.append(name)
2468 + removed_files.extend(rm_files)
2452 2469 print(f"✅ ({count} files)")
2453 2470
2454 2471 # Post-remove hook + sprzątanie zapisanych hooków
@@ -2463,6 +2480,7 @@ def cmd_remove(package_names):
2463 2480
2464 2481 if not removed: return 0
2465 2482 print(f"\n✅ Removed {len(removed)}.")
2483 + _process_triggers(removed_files)
2466 2484
2467 2485 orphans = _find_orphans(installed_db, world)
2468 2486 if orphans:
@@ -2477,6 +2495,76 @@ def _run_hook_for_installed(pkg_name, hook_name):
2477 2495 ver = load_json(INSTALLED_DB).get(pkg_name, {}).get("version", "")
2478 2496 _run_hook(hook_dir, hook_name, PackageInfo({"name": pkg_name, "version": ver}))
2479 2497
2498 +
2499 +# =============================================================================
2500 +# TRIGGERS – hooki zbiorcze (raz na transakcję, nie per pakiet)
2501 +# =============================================================================
2502 +# Wzorem pacman/dpkg: pakiet/administrator deklaruje zainteresowanie ścieżkami,
2503 +# a pasujący trigger uruchamia się DOKŁADNIE RAZ na końcu transakcji
2504 +# (np. fc-cache, glib-compile-schemas, update-desktop-database) zamiast po
2505 +# każdym pakiecie z osobna.
2506 +
2507 +TRIGGERS_DIR = PAG_CONF + "/triggers"
2508 +
2509 +DEFAULT_TRIGGERS = [
2510 + {"name": "font-cache", "paths": ["/usr/share/fonts/", "/usr/local/share/fonts/"],
2511 + "run": "fc-cache -fs"},
2512 + {"name": "glib-schemas", "paths": ["/usr/share/glib-2.0/schemas/"],
2513 + "run": "glib-compile-schemas /usr/share/glib-2.0/schemas"},
2514 + {"name": "desktop-database", "paths": ["/usr/share/applications/"],
2515 + "run": "update-desktop-database -q /usr/share/applications"},
2516 + {"name": "mime-database", "paths": ["/usr/share/mime/"],
2517 + "run": "update-mime-database /usr/share/mime"},
2518 +]
2519 +
2520 +def _load_triggers() -> List[dict]:
2521 + """Ładuje triggery: domyślne (tylko gdy binarka istnieje) + /etc/pag/triggers/*.json."""
2522 + out = []
2523 + for t in DEFAULT_TRIGGERS:
2524 + bin_name = t["run"].split()[0]
2525 + if shutil.which(bin_name):
2526 + out.append(dict(t))
2527 + if os.path.isdir(TRIGGERS_DIR):
2528 + for fn in sorted(os.listdir(TRIGGERS_DIR)):
2529 + if not fn.endswith(".json"):
2530 + continue
2531 + try:
2532 + with open(os.path.join(TRIGGERS_DIR, fn)) as f:
2533 + data = json.load(f)
2534 + except (OSError, json.JSONDecodeError):
2535 + continue
2536 + if isinstance(data, dict):
2537 + data = [data]
2538 + for t in data:
2539 + if isinstance(t, dict) and t.get("name") and t.get("paths") and t.get("run"):
2540 + out.append(t)
2541 + return out
2542 +
2543 +def _process_triggers(touched_paths: List[str]):
2544 + """Uruchamia pasujące triggery RAZ na końcu transakcji (best-effort)."""
2545 + if not touched_paths:
2546 + return
2547 + if os.environ.get("PAG_NO_HOOKS", "") == "1":
2548 + return
2549 + import shlex as _shlex
2550 + matched = []
2551 + for trig in _load_triggers():
2552 + if any(path.startswith(p) for p in trig["paths"] for path in touched_paths):
2553 + matched.append(trig)
2554 + for trig in matched:
2555 + run = trig["run"]
2556 + print(f" ⚡ Trigger: {trig['name']} ({run})")
2557 + try:
2558 + r = subprocess.run(_shlex.split(run), capture_output=True, text=True, timeout=120)
2559 + _audit(f"TRIGGER {trig['name']}: {run} rc={r.returncode}")
2560 + if r.returncode != 0:
2561 + print(f" ⚠ rc={r.returncode}: {(r.stderr or r.stdout or '').strip()[:160]}")
2562 + except subprocess.TimeoutExpired:
2563 + print(f" ⚠ trigger {trig['name']} przekroczył limit czasu (120 s)")
2564 + _audit(f"TRIGGER {trig['name']} TIMEOUT")
2565 + except Exception as e:
2566 + print(f" ⚠ trigger {trig['name']}: {e}")
2567 +
2480 2568 # =============================================================================
2481 2569 # UPDATE / UPGRADE / LIST / SEARCH / INFO / VERIFY
2482 2570 # =============================================================================
@@ -4169,6 +4257,45 @@ def _verify_dependencies(to_install: list, repo: dict, installed: dict) -> int:
4169 4257
4170 4258 return len(all_missing)
4171 4259
4260 +# Biblioteki bazowe (glibc/gcc runtime) – zawsze dostępne, nie wymagają pakietu
4261 +BASE_SO = {
4262 + "libc.so.6", "libm.so.6", "libpthread.so.0", "libdl.so.2", "librt.so.1",
4263 + "libutil.so.1", "libresolv.so.2", "libnsl.so.1", "libcrypt.so.1",
4264 + "ld-linux.so.2", "ld-linux-x86-64.so.2", "ld-linux-aarch64.so.1",
4265 + "libgcc_s.so.1", "linux-vdso.so.1",
4266 +}
4267 +
4268 +def _verify_so_deps(to_install: list, repo: dict, installed: dict) -> int:
4269 + """Sprawdza wymagania ABI (provides_so / requires_so z metadata.json).
4270 +
4271 + Fail-closed TYLKO gdy metadata jawnie deklaruje requires_so, a żaden pakiet
4272 + (bazowy, zainstalowany lub instalowany w tej transakcji) nie dostarcza
4273 + wymaganej wersji biblioteki. Stare pakiety bez tych pól są pomijane.
4274 + """
4275 + provided = set(BASE_SO)
4276 + for n in to_install:
4277 + p = repo.get(n)
4278 + if p:
4279 + provided.update(p.provides_so or [])
4280 + for n, info in installed.items():
4281 + provided.update(info.get("provides_so", []) or [])
4282 +
4283 + missing = []
4284 + for n in sorted(to_install):
4285 + p = repo.get(n)
4286 + if not p:
4287 + continue
4288 + for so in (p.requires_so or []):
4289 + if so not in provided:
4290 + missing.append((n, so))
4291 +
4292 + if missing:
4293 + print(f"\n❌ {_c('red', 'BRAK WYMAGANYCH BIBLIOTEK (ABI so-name)')}:")
4294 + for n, so in missing:
4295 + print(f" {n} → wymaga {_c('red', so)} – żaden pakiet nie dostarcza tej wersji")
4296 + print()
4297 + return len(missing)
4298 +
4172 4299 def _download_pkg(pkg):
4173 4300 url = f"{pkg.repo_url}/{pkg.filename}"
4174 4301 dest = os.path.join(PAG_CACHE, pkg.filename)