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: