← pag

Commit bf32327

0
plików
+0
dodanych
-0
usuniętych
@@ -1,7 +1,7 @@
1 1 #!/usr/bin/env python3
2 2 """
3 3 ╔══════════════════════════════════════════════════════════════════════════════╗
4 -║ PAG - Pagan Linux Package Manager v3.2.1 ║
4 +║ PAG - Pagan Linux Package Manager v3.3.0 ║
5 5 ║ Produkcyjny menedżer pakietów – atomowy, bezpieczny, i18n ║
6 6 ╚══════════════════════════════════════════════════════════════════════════════╝
7 7
@@ -30,6 +30,10 @@ from datetime import datetime, timezone
30 30 from typing import Dict, List, Optional, Tuple, Set
31 31 from concurrent.futures import ThreadPoolExecutor, as_completed
32 32 from urllib.request import urlopen, Request
33 +import threading, itertools
34 +
35 +# Wersja klienta – do porównania z repo.json["pag_version"] (self-update)
36 +PAG_VERSION = "3.3.0"
33 37 from urllib.error import URLError, HTTPError
34 38
35 39 # =============================================================================
@@ -726,7 +730,7 @@ def save_json(path, data):
726 730
727 731 class PackageInfo:
728 732 __slots__ = ("name","version","description","dependencies",
729 - "size_bytes","sha256","gpg_fp","repo_url","filename")
733 + "size_bytes","sha256","gpg_fp","repo_url","filename","provides")
730 734 def __init__(self, d, repo=""):
731 735 self.name = d.get("name","?")
732 736 self.version = d.get("version","0")
@@ -737,6 +741,7 @@ class PackageInfo:
737 741 self.gpg_fp = d.get("gpg_fingerprint","")
738 742 self.repo_url = repo
739 743 self.filename = d.get("filename", f"{self.name}-{self.version}{PKG_EXT}")
744 + self.provides = d.get("provides", []) or []
740 745
741 746 # =============================================================================
742 747 # REPOZYTORIA (cache, ETag, GPG)
@@ -859,12 +864,32 @@ def _verify_repo_sig(repo_url, cache_path) -> bool:
859 864 "--verify", sig_path, cache_path,
860 865 capture_output=True, text=True, timeout=30)
861 866 if result.returncode != 0:
862 - if insecure:
863 - print(f" ⚠ {repo_url}: nieprawidłowy podpis GPG (PAG_INSECURE – ignoruję)")
864 - return True
865 - os.remove(cache_path)
866 - print(f" ❌ {repo_url}: NIEPRAWIDŁOWY PODPIS GPG indeksu repozytorium!")
867 - return False
867 + # Automatyczny import klucza repo przy pierwszym uruchomieniu (TOFU,
868 + # jak apt) – gdy w keyringu brakuje klucza (No public key).
869 + _stderr = (result.stderr or "")
870 + if "public key" in _stderr.lower() and "no public key" in _stderr.lower():
871 + try:
872 + with urlopen(Request(f"{repo_url}/paganos.asc", headers={"User-Agent": "pag/3.0"}), timeout=20) as r:
873 + keydata = r.read()
874 + with tempfile.NamedTemporaryFile(delete=False, suffix=".asc") as tmp:
875 + tmp.write(keydata)
876 + tmp.flush()
877 + _gpg_run("--no-default-keyring", "--keyring", GPG_KEYRING,
878 + "--import", tmp.name, capture_output=True, timeout=30)
879 + os.unlink(tmp.name)
880 + print(f" 🔑 Importowano klucz repo z {repo_url}/paganos.asc")
881 + result = _gpg_run("--no-default-keyring", "--keyring", GPG_KEYRING,
882 + "--verify", sig_path, cache_path,
883 + capture_output=True, text=True, timeout=30)
884 + except Exception:
885 + pass
886 + if result.returncode != 0:
887 + if insecure:
888 + print(f" ⚠ {repo_url}: nieprawidłowy podpis GPG (PAG_INSECURE – ignoruję)")
889 + return True
890 + os.remove(cache_path)
891 + print(f" ❌ {repo_url}: NIEPRAWIDŁOWY PODPIS GPG indeksu repozytorium!")
892 + return False
868 893
869 894 return True
870 895
@@ -1537,6 +1562,50 @@ def _run_hook_for_installed(pkg_name, hook_name):
1537 1562 # UPDATE / UPGRADE / LIST / SEARCH / INFO / VERIFY
1538 1563 # =============================================================================
1539 1564
1565 +def cmd_self_update():
1566 + """Aktualizuje samego klienta pag z repo (podpisany /stable/pag)."""
1567 + repos = get_repos()
1568 + if not repos:
1569 + print("❌ Brak repozytoriów w konfiguracji.")
1570 + return 1
1571 + base = repos[0]
1572 + print(f"🔄 Sprawdzam aktualizację pag z {base}...")
1573 + tmp_pag = "/tmp/pag.new"
1574 + tmp_sig = "/tmp/pag.new.asc"
1575 + try:
1576 + with urlopen(Request(f"{base}/pag", headers={"User-Agent": "pag/3.0"}), timeout=20) as r:
1577 + data = r.read()
1578 + with urlopen(Request(f"{base}/pag.asc", headers={"User-Agent": "pag/3.0"}), timeout=20) as r:
1579 + sig = r.read()
1580 + except Exception as e:
1581 + print(f" ❌ Nie można pobrać pag: {e}")
1582 + return 1
1583 + with open(tmp_pag, "wb") as f:
1584 + f.write(data)
1585 + with open(tmp_sig, "wb") as f:
1586 + f.write(sig)
1587 +
1588 + # Weryfikacja podpisu GPG – bez tego nie instalujemy
1589 + res = _gpg_run("--no-default-keyring", "--keyring", GPG_KEYRING,
1590 + "--verify", tmp_sig, tmp_pag, capture_output=True, text=True)
1591 + if res.returncode != 0:
1592 + print(" ❌ Nieprawidłowy podpis aktualizacji – nie aktualizuję.")
1593 + return 1
1594 +
1595 + m = re.search(rb"v\d+\.\d+\.\d+", data[:3000])
1596 + new_ver = m.group(0).decode().lstrip("v") if m else "?"
1597 + print(f" ✅ Pobrano pag {new_ver} (obecny {PAG_VERSION}), podpis zweryfikowany")
1598 +
1599 + dst = "/usr/local/bin/pag"
1600 + if os.path.exists(dst):
1601 + shutil.copy2(dst, dst + ".bak")
1602 + shutil.copy2(tmp_pag, dst)
1603 + os.chmod(dst, 0o755)
1604 + print(f" ✅ Zainstalowano nowy pag. Stary zachowany jako {dst}.bak")
1605 + print(" Uruchom ponownie pag, aby użyć nowej wersji.")
1606 + return 0
1607 +
1608 +
1540 1609 def cmd_update():
1541 1610 force = "--force" in sys.argv
1542 1611 print(f"🔄 {'Forced refresh' if force else 'Updating'} indexes...")
@@ -1555,6 +1624,18 @@ def cmd_update():
1555 1624 pass
1556 1625 print(f"✅ {_('updated_done', total)}")
1557 1626
1627 + # Powiadomienie o nowszej wersji pag (repo.json["pag_version"])
1628 + try:
1629 + for r in get_repos():
1630 + cp = _repo_cache_path(r)
1631 + if os.path.exists(cp):
1632 + d = json.load(open(cp))
1633 + rv = d.get("pag_version", "")
1634 + if rv and rv != PAG_VERSION:
1635 + print(f" ⚠ Nowa wersja pag {rv} dostępna – uruchom: pag self-update")
1636 + except Exception:
1637 + pass
1638 +
1558 1639 def cmd_upgrade():
1559 1640 ensure_dirs()
1560 1641 installed = load_json(INSTALLED_DB)
@@ -1801,10 +1882,17 @@ PROVIDES_MAP = {
1801 1882
1802 1883 def _resolve_provides(name: str, repo: dict) -> str:
1803 1884 """Rozwija wirtualną nazwę pakietu do rzeczywistej nazwy z repo."""
1885 + if name in repo:
1886 + return name
1804 1887 if name in PROVIDES_MAP:
1805 1888 real = PROVIDES_MAP[name]
1806 1889 if real in repo:
1807 1890 return real
1891 + # Dynamiczne provides z repo.json (sekcja provides: w PAGBUILD.yaml)
1892 + for _pkg_name, _pkg in repo.items():
1893 + _provs = getattr(_pkg, "provides", None) or []
1894 + if name in _provs:
1895 + return _pkg_name
1808 1896 clean = name
1809 1897 if name.startswith("pkgconfig(") and ")" in name:
1810 1898 clean = name.split("(", 1)[1].rstrip(")")
@@ -1930,15 +2018,41 @@ def _check_flatpak():
1930 2018 "https://flathub.org/repo/flathub.flatpakrepo"], check=False)
1931 2019 return True
1932 2020
2021 +def _spinner(msg: str):
2022 + """Prosty spinner „myślenia” w osobnym wątku. Zwraca funkcję stop()."""
2023 + stop = threading.Event()
2024 + def _spin():
2025 + for c in itertools.cycle("|/-\\"):
2026 + if stop.is_set():
2027 + break
2028 + sys.stdout.write(f"\r {msg} {c}")
2029 + sys.stdout.flush()
2030 + time.sleep(0.1)
2031 + t = threading.Thread(target=_spin, daemon=True)
2032 + t.start()
2033 + def _stop():
2034 + stop.set()
2035 + t.join(timeout=0.3)
2036 + sys.stdout.write("\r" + " " * (len(msg) + 4) + "\r")
2037 + sys.stdout.flush()
2038 + return _stop
2039 +
2040 +
1933 2041 def _flatpak_search_raw(query: str) -> List[dict]:
1934 2042 """Szuka we Flathub i zwraca listę wyników jako słowniki."""
1935 2043 if not _check_flatpak():
1936 2044 return []
2045 + stop = _spinner("Szukam we Flathub...")
1937 2046 try:
1938 - r = subprocess.run(
1939 - ["flatpak", "search", "--columns=name:description:application:version:branch:remotes", query],
1940 - capture_output=True, text=True, timeout=30
1941 - )
2047 + try:
2048 + r = subprocess.run(
2049 + ["flatpak", "search", "--columns=name,description,application,version,branch,remotes", query],
2050 + capture_output=True, text=True, timeout=120
2051 + )
2052 + finally:
2053 + stop()
2054 + if r.returncode != 0 and "No matches found" not in r.stdout and not r.stdout.strip():
2055 + print(f" ⚠ flatpak search: {r.stderr.strip()[:150]}")
1942 2056 results = []
1943 2057 for line in r.stdout.strip().split("\n"):
1944 2058 parts = line.split("\t")
@@ -2222,7 +2336,7 @@ def _flatpak_smart_remove(names: list) -> int:
2222 2336 return 1 if failed else 0
2223 2337
2224 2338 def cmd_flatpak_search(q: str):
2225 - """Wyszukuje we Flathub i wyświetla wyniki."""
2339 + """Wyszukuje we Flathub i wyświetla wyniki (z możliwością wyboru do instalacji)."""
2226 2340 if not _check_flatpak():
2227 2341 return 1
2228 2342 results = _flatpak_search_raw(q)
@@ -2230,9 +2344,10 @@ def cmd_flatpak_search(q: str):
2230 2344 print(f" ❌ '{q}' – {_('flatpak_not_found')}")
2231 2345 return 1
2232 2346 print(f"\n {_('flatpak_found', len(results))}")
2233 - for r in results[:30]: # max 30 wyników
2347 + shown = results[:30] # max 30 wyników
2348 + for i, r in enumerate(shown, 1):
2234 2349 installed = "📦 " if _flatpak_is_installed(r["app_id"]) else " "
2235 - print(f" {installed}{_c('bold', r['name'])} ({r['app_id']})")
2350 + print(f" {i:>2}. {installed}{_c('bold', r['name'])} ({r['app_id']})")
2236 2351 if r["version"]:
2237 2352 print(f" {_('flatpak_info_version')}: {r['version']} | {_('flatpak_info_branch')}: {r['branch']}")
2238 2353 if r["description"]:
@@ -2240,6 +2355,20 @@ def cmd_flatpak_search(q: str):
2240 2355 print(f" {_c('dim', desc)}")
2241 2356 if len(results) > 30:
2242 2357 print(f" ... i {len(results) - 30} więcej. Doprecyzuj zapytanie.")
2358 +
2359 + # Interaktywny wybór – wpisz numer, aby zainstalować (Enter = anuluj)
2360 + try:
2361 + ans = input(f"\n Wybierz numer do zainstalowania (1-{len(shown)}) lub Enter aby anulować: ").strip()
2362 + except (EOFError, KeyboardInterrupt):
2363 + return 0
2364 + if ans:
2365 + try:
2366 + idx = int(ans) - 1
2367 + if 0 <= idx < len(shown):
2368 + return _flatpak_do_install(shown[idx]["app_id"])
2369 + print(_("cancelled"))
2370 + except (ValueError, IndexError):
2371 + print(_("cancelled"))
2243 2372 return 0
2244 2373
2245 2374 def cmd_flatpak_list():
@@ -2969,7 +3098,7 @@ def main():
2969 3098 WRITE_CMDS = {
2970 3099 "install", "remove", "update", "upgrade", "clean", "download",
2971 3100 "autoremove", "remove-orphans", "pin", "unpin", "rollback",
2972 - "repo-add", "key-add", "key-remove",
3101 + "repo-add", "key-add", "key-remove", "self-update",
2973 3102 "flatpak", "flatpak-install", "flatpak-remove", "flatpak-update",
2974 3103 "deploy-rollback", "deploy-cleanup", "initramfs-update", "grub-update",
2975 3104 }
@@ -2998,6 +3127,7 @@ def main():
2998 3127 "repo-add": lambda: cmd_repo_add(args[0]) if args else print("Usage: pag repo-add <url>"),
2999 3128 "key-add": lambda: cmd_key_add(args[0]) if args else print("Usage: pag key-add <url|file>"),
3000 3129 "key-remove": lambda: cmd_key_remove(args[0]) if args else print("Usage: pag key-remove <id>"),
3130 + "self-update": cmd_self_update,
3001 3131 "flatpak": lambda: cmd_flatpak(args),
3002 3132 "flatpak-install": lambda: _flatpak_smart_install(args) if args else print("Usage: pag flatpak-install <app>"),
3003 3133 "flatpak-remove": lambda: _flatpak_smart_remove(args) if args else print("Usage: pag flatpak-remove <app>"),