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>"),