Files
DroidDeck/tools/desktop-kde/closure.py
Kurt Himebauch eaf26c8a20 feat: replace the LXQt desktop with SteamOS-style KDE Plasma (#473)
* feat: replace the LXQt desktop with SteamOS-style KDE Plasma

* feat: leaving the KDE desktop logs out through Plasma and returns to gaming mode

* feat: DroidDeck logo wallpaper as the KDE desktop default

* feat: build the KDE wallpaper from the DroidDeck mark artwork

* ci: keep the built desktop package as a short-lived artifact for device testing

* fix: the desktop loading screen no longer promises Steam on the desktop

* docs: the output resize comments name the desktop's KWin

* fix: the desktop package verifies every package's Arch Linux ARM signature and never replaces a published tag

* fix: programs sent to the GPU from the desktop drop its software rendering overrides

* fix: an interrupted desktop install no longer counts as installed

* feat: pin the desktop package to its DroidDeck-Components release instead of the catalog
2026-10-09 10:39:50 -04:00

154 lines
5.5 KiB
Python
Executable File

#!/usr/bin/env python3
"""The packages the KDE desktop package ships: the dependency closure of the seeds over Arch Linux
ARM's extracted repository databases, minus what the runtime already has at a version that serves.
closure.py <db dir with x_core, x_extra, x_alarm> <runtime-packages.txt> <seeds.txt>
Prints one "repo/filename sha256" per package to ship (the sha256 the repository database gives).
"""
import collections
import os
import re
import sys
REPOS = ("core", "extra", "alarm")
def vercmp_part(a, b):
# rpmvercmp, as pacman compares version strings.
if a == b:
return 0
sa = re.findall(r"\d+|[a-zA-Z]+", a)
sb = re.findall(r"\d+|[a-zA-Z]+", b)
for x, y in zip(sa, sb):
if x.isdigit() and y.isdigit():
x, y = int(x), int(y)
if x != y:
return 1 if x > y else -1
elif x.isdigit():
return 1
elif y.isdigit():
return -1
elif x != y:
return 1 if x > y else -1
if len(sa) == len(sb):
return 0
return 1 if len(sa) > len(sb) else -1
def vercmp(a, b):
def split(v):
epoch, _, rest = v.rpartition(":") if ":" in v else ("0", "", v)
ver, _, rel = rest.partition("-")
return int(epoch or 0), ver, rel
ea, va, ra = split(a)
eb, vb, rb = split(b)
if ea != eb:
return 1 if ea > eb else -1
c = vercmp_part(va, vb)
if c or not ra or not rb:
return c
return vercmp_part(ra, rb)
def parse_dep(d):
m = re.match(r"^([^<>=]+)(>=|<=|=|>|<)?(.*)$", d)
return m.group(1), m.group(2), m.group(3)
def satisfies(version, op, want):
if not op:
return True
c = vercmp(version, want)
return {"=": c == 0, ">=": c >= 0, "<=": c <= 0, ">": c > 0, "<": c < 0}[op]
def main():
dbdir, runtime_file, seeds_file = sys.argv[1:4]
runtime = {}
for line in open(runtime_file):
parts = line.split()
if len(parts) == 2:
runtime[parts[0]] = parts[1]
seeds = [l.strip() for l in open(seeds_file) if l.strip() and not l.lstrip().startswith("#")]
pkgs, providers = {}, collections.defaultdict(list)
for repo in REPOS:
root = os.path.join(dbdir, "x_" + repo)
for entry in sorted(os.listdir(root)):
fields, key = {}, None
for fname in ("desc", "depends"):
path = os.path.join(root, entry, fname)
if not os.path.exists(path):
continue
for line in open(path, encoding="utf-8", errors="replace"):
line = line.rstrip("\n")
if line.startswith("%") and line.endswith("%"):
key = line.strip("%")
fields[key] = []
elif line == "":
key = None
elif key:
fields[key].append(line)
name = fields.get("NAME", [None])[0]
if not name or name in pkgs:
continue
provides = [parse_dep(p) for p in fields.get("PROVIDES", [])]
pkgs[name] = {"repo": repo, "file": fields["FILENAME"][0], "version": fields["VERSION"][0],
"sha256": fields.get("SHA256SUM", [""])[0],
"depends": fields.get("DEPENDS", []), "provides": provides}
providers[name].append((name, pkgs[name]["version"]))
for pname, _, pver in provides:
providers[pname].append((name, pver or pkgs[name]["version"]))
def resolve(dep):
"""The package to take for a dependency string, and whether the runtime's copy serves."""
name, op, want = parse_dep(dep)
# A runtime package that is the dependency or provides it, at a version that serves.
if name in runtime and satisfies(runtime[name], op, want):
return name, True
candidates = providers.get(name, [])
for real, _ in candidates:
if real in runtime:
# A provided name (a soname, mostly): the runtime's build serves as long as it is
# the repository's own, or nothing asks for a particular version.
if not op or runtime[real] == pkgs[real]["version"]:
return real, True
if satisfies_provider(real, name, op, want):
return real, False
for real, pver in candidates:
if satisfies(pver, op, want):
return real, False
return None, False
def satisfies_provider(real, name, op, want):
if real == name:
return satisfies(pkgs[real]["version"], op, want)
return any(p == name and satisfies(v or pkgs[real]["version"], op, want) for p, _, v in pkgs[real]["provides"])
ship, seen, missing = [], set(), []
queue = list(seeds)
while queue:
dep = queue.pop()
real, served = resolve(dep)
if real is None:
missing.append(dep)
continue
if served or real in seen:
continue
seen.add(real)
ship.append(real)
queue.extend(pkgs[real]["depends"])
if missing:
sys.exit("unresolved: " + " ".join(sorted(set(missing))))
upgraded = sorted(n for n in ship if n in runtime)
if upgraded:
print("replacing runtime packages: " + " ".join(f"{n} {runtime[n]} -> {pkgs[n]['version']}" for n in upgraded),
file=sys.stderr)
for name in sorted(ship):
print(pkgs[name]["repo"] + "/" + pkgs[name]["file"], pkgs[name]["sha256"])
if __name__ == "__main__":
main()