← pag

Commit 22e6c47

0
plików
+0
dodanych
-0
usuniętych
@@ -52,9 +52,10 @@ from typing import Dict, List, Optional, Tuple, Set
52 52 from concurrent.futures import ThreadPoolExecutor, as_completed
53 53 from urllib.request import urlopen, Request
54 54 import threading, itertools
55 +import uuid # serialNumber SBOM (CycloneDX)
55 56
56 57 # Wersja klienta – do porównania z repo.json["pag_version"] (self-update)
57 -PAG_VERSION = "3.3.11"
58 +PAG_VERSION = "3.3.12"
58 59 from urllib.error import URLError, HTTPError
59 60
60 61 # =============================================================================
@@ -1014,7 +1015,7 @@ def save_json(path, data):
1014 1015
1015 1016 class PackageInfo:
1016 1017 __slots__ = ("name","version","description","dependencies",
1017 - "size_bytes","sha256","gpg_fp","repo_url","filename","provides")
1018 + "size_bytes","sha256","gpg_fp","repo_url","filename","provides","license")
1018 1019 def __init__(self, d, repo=""):
1019 1020 self.name = d.get("name","?")
1020 1021 self.version = d.get("version","0")
@@ -1026,6 +1027,7 @@ class PackageInfo:
1026 1027 self.repo_url = repo
1027 1028 self.filename = d.get("filename", f"{self.name}-{self.version}{PKG_EXT}")
1028 1029 self.provides = d.get("provides", []) or []
1030 + self.license = d.get("license", []) or []
1029 1031
1030 1032 # =============================================================================
1031 1033 # REPOZYTORIA (cache, ETag, GPG)
@@ -2038,6 +2040,40 @@ def _install_local_pkg_files(paths, install_succeeded):
2038 2040 return failed, all_files
2039 2041
2040 2042
2043 +def _preflight_disk(total_bytes: int) -> bool:
2044 + """Pre-flight przed transakcją: wolne miejsce + mount read-only.
2045 +
2046 + Zwraca False (przerywa instalację) gdy na partycji docelowej brakuje
2047 + miejsca na pakiety albo katalog stagingu jest zamontowany read-only
2048 + (inaczej instalacja rwałaby się w połowie, zostawiając uszkodzony system).
2049 + """
2050 + target = PAG_ROOT or "/"
2051 + try:
2052 + st = os.statvfs(target)
2053 + free = st.f_bavail * st.f_frsize
2054 + except OSError:
2055 + return True # nie da się sprawdzić – nie blokuj
2056 + need_mb = total_bytes // 1048576
2057 + free_mb = free // 1048576
2058 + if free < total_bytes:
2059 + print(f" ❌ Za mało miejsca na dysku: potrzeba ~{need_mb} MB, "
2060 + f"wolne {free_mb} MB ({target})")
2061 + return False
2062 + if free < total_bytes * 3:
2063 + print(f" ⚠ Mało miejsca na dysku: wolne {free_mb} MB, "
2064 + f"pakiety ~{need_mb} MB (rozpakowane zajmą więcej)")
2065 + # Wykryj mount read-only (test zapisu w stagingu)
2066 + try:
2067 + probe = os.path.join(STAGING_DIR, ".pag-probe")
2068 + with open(probe, "w") as f:
2069 + f.write("x")
2070 + os.remove(probe)
2071 + except OSError:
2072 + print(f" ❌ {target} jest zamontowane tylko-do-odczytu – nie można instalować.")
2073 + return False
2074 + return True
2075 +
2076 +
2041 2077 def cmd_install(package_names, as_dep=False, upgrade=False):
2042 2078 ensure_dirs()
2043 2079 installed_db = load_json(INSTALLED_DB)
@@ -2049,6 +2085,13 @@ def cmd_install(package_names, as_dep=False, upgrade=False):
2049 2085 local_files = [p for p in package_names if p.endswith(PKG_EXT) or
2050 2086 (os.sep in p and os.path.isfile(os.path.abspath(p)))]
2051 2087 if local_files:
2088 + _local_need = sum(
2089 + os.path.getsize(os.path.abspath(p))
2090 + for p in local_files if os.path.isfile(os.path.abspath(p))
2091 + )
2092 + if not _preflight_disk(_local_need):
2093 + return 1
2094 +
2052 2095 def _ok(pkg, files):
2053 2096 installed_db[pkg.name] = {
2054 2097 "version": pkg.version, "description": pkg.description,
@@ -2140,6 +2183,8 @@ def cmd_install(package_names, as_dep=False, upgrade=False):
2140 2183 print(f" {_("sec_toobig", size_mb=mb, max_mb=MAX_MB)}")
2141 2184 return 1
2142 2185 total_size = sum(repo_pkgs[n].size_bytes for n in to_install if n in repo_pkgs)
2186 + if not _preflight_disk(total_size):
2187 + return 1
2143 2188 print(f"\n📦 {_('to_install', len(to_install), total_size/1048576)}")
2144 2189 for name in to_install:
2145 2190 p = repo_pkgs.get(name)
@@ -4215,6 +4260,87 @@ def _find_orphans(installed, world):
4215 4260 # MAIN
4216 4261 # =============================================================================
4217 4262
4263 +def cmd_sbom(argv):
4264 + """pag sbom export [spdx|cyclonedx] – manifest SBOM zainstalowanych pakietów.
4265 +
4266 + Wypisuje na stdout JSON (SPDX 2.3 lub CycloneDX 1.5) z listą
4267 + zainstalowanych pakietów, wersji, licencji i sum SHA256.
4268 + """
4269 + fmt = (argv[0] if argv else "spdx").lower()
4270 + if fmt not in ("spdx", "cyclonedx"):
4271 + print("❌ Format: spdx | cyclonedx")
4272 + return 1
4273 + installed = load_json(INSTALLED_DB)
4274 + if not installed:
4275 + print("{}") if fmt == "cyclonedx" else print("{\"packages\": []}")
4276 + return 0
4277 + # metadata repo (licencje) – best-effort
4278 + try:
4279 + repo = fetch_all_packages()
4280 + except Exception:
4281 + repo = {}
4282 + names = sorted(installed)
4283 + created = datetime.now().astimezone().isoformat(timespec="seconds")
4284 +
4285 + def _license_of(name):
4286 + p = repo.get(name)
4287 + lic = getattr(p, "license", None) or []
4288 + if isinstance(lic, list):
4289 + lic = ", ".join(x for x in lic if x)
4290 + return lic or "NOASSERTION"
4291 +
4292 + if fmt == "spdx":
4293 + doc = {
4294 + "spdxVersion": "SPDX-2.3",
4295 + "dataLicense": "CC0-1.0",
4296 + "SPDXID": "SPDXRef-DOCUMENT",
4297 + "name": "PaganOS-installed",
4298 + "documentNamespace": f"https://repo.paganlinux.eu/sbom/installed-{int(time.time())}",
4299 + "creationInfo": {
4300 + "created": created,
4301 + "creators": [f"Tool: pag-{PAG_VERSION}"],
4302 + },
4303 + "packages": [],
4304 + }
4305 + for i, n in enumerate(names):
4306 + info = installed[n]
4307 + doc["packages"].append({
4308 + "SPDXID": f"SPDXRef-Package-{i+1}",
4309 + "name": n,
4310 + "versionInfo": info.get("version", ""),
4311 + "downloadLocation": info.get("repo", "NOASSERTION"),
4312 + "filesAnalyzed": False,
4313 + "licenseConcluded": _license_of(n),
4314 + "checksums": [{"algorithm": "SHA256", "checksumValue": info.get("sha256", "")}],
4315 + })
4316 + else: # cyclonedx
4317 + doc = {
4318 + "bomFormat": "CycloneDX",
4319 + "specVersion": "1.5",
4320 + "serialNumber": f"urn:uuid:{str(uuid.uuid4())}",
4321 + "version": 1,
4322 + "metadata": {
4323 + "timestamp": created,
4324 + "tools": [{"vendor": "PaganOS", "name": "pag", "version": PAG_VERSION}],
4325 + },
4326 + "components": [],
4327 + }
4328 + for n in names:
4329 + info = installed[n]
4330 + lic = _license_of(n)
4331 + comp = {
4332 + "type": "library",
4333 + "name": n,
4334 + "version": info.get("version", ""),
4335 + "hashes": [{"alg": "SHA-256", "content": info.get("sha256", "")}],
4336 + }
4337 + if lic != "NOASSERTION":
4338 + comp["licenses"] = [{"license": {"id": lic}}]
4339 + doc["components"].append(comp)
4340 + print(json.dumps(doc, indent=2, ensure_ascii=False))
4341 + return 0
4342 +
4343 +
4218 4344 USAGE_EN = """pag v3 – Pagan Linux Package Manager
4219 4345
4220 4346 BASIC:
@@ -4251,6 +4377,7 @@ ADVANCED:
4251 4377 pag remove-orphans Remove orphaned deps
4252 4378 pag repo-add <url> [name] Add repository (drop-in /etc/pag/repos/)
4253 4379 pag repo-list List repositories
4380 + pag sbom export [fmt] SBOM manifest (spdx|cyclonedx)
4254 4381
4255 4382 FLATPAK:
4256 4383 pag flatpak [<query>] Search & install (smart)
@@ -4305,6 +4432,7 @@ ZAAWANSOWANE:
4305 4432 pag remove-orphans Usuń osierocone zależności
4306 4433 pag repo-add <url> [nazwa] Dodaj repozytorium (drop-in w /etc/pag/repos/)
4307 4434 pag repo-list Lista repozytoriów
4435 + pag sbom export [fmt] Manifest SBOM (spdx|cyclonedx)
4308 4436
4309 4437 FLATPAK:
4310 4438 pag flatpak [<query>] Szukaj i instaluj
@@ -4358,6 +4486,7 @@ def main():
4358 4486 "flatpak-info": lambda: cmd_flatpak_info(args[0]) if args else print("Usage: pag flatpak-info <id>"),
4359 4487 "deploy-list": cmd_deploy_list,
4360 4488 "deploy": cmd_deploy_list,
4489 + "sbom": lambda: cmd_sbom(args),
4361 4490 }
4362 4491
4363 4492 if cmd in READ_ONLY: