From 67cb8f862db5a20ec51aecc8d5f5c33140d58d11 Mon Sep 17 00:00:00 2001 From: "David M. Klein" <4+leander19961@noreply.localhost> Date: Thu, 27 Aug 2026 13:46:39 +0200 Subject: [PATCH] =?UTF-8?q?docker/syncer/appstream-gen.py=20gel=C3=B6scht?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker/syncer/appstream-gen.py | 328 --------------------------------- 1 file changed, 328 deletions(-) delete mode 100644 docker/syncer/appstream-gen.py diff --git a/docker/syncer/appstream-gen.py b/docker/syncer/appstream-gen.py deleted file mode 100644 index 4742fd3..0000000 --- a/docker/syncer/appstream-gen.py +++ /dev/null @@ -1,328 +0,0 @@ -#!/usr/bin/env python3 -""" -Erzeugt einen AppStream-Katalog (Collection-XML) aus packages.yaml. - -Der Katalog referenziert die gespiegelten RPMs direkt ueber . -Es werden keine Wrapper-Pakete gebaut. Das Ergebnis wird per -`modifyrepo_c --mdtype=appstream` in die Repo-Metadaten eingehaengt, -damit Discover / GNOME Software die Pakete findet. - -Aufruf: - appstream-gen.py packages.yaml -o appstream.xml.gz \ - [--available-packages vorhanden.txt] [--origin firmen-repo] -""" - -import argparse -import gzip -import re -import sys -import xml.etree.ElementTree as ET -from xml.dom import minidom - -import yaml - -# AppStream-Spezifikationsversion des Katalogformats. -CATALOG_VERSION = "0.14" - -DEFAULT_TYPE = "console-application" -DEFAULT_ICON = "application-x-executable" -DEFAULT_NOTE = ( - "Bereitgestellt über das Unternehmens-Repo der IT-Abteilung. " - "Fragen und Paketwünsche bitte an den IT-Service-Desk." -) - - -def die(msg): - sys.stdout.flush() - print(f"[appstream-gen] FEHLER: {msg}", file=sys.stderr, flush=True) - sys.exit(1) - - -def warn(msg): - sys.stdout.flush() - print(f"[appstream-gen] WARNUNG: {msg}", file=sys.stderr, flush=True) - - -def info(msg): - print(f"[appstream-gen] {msg}", flush=True) - - -def sanitize_id_part(value): - """Macht aus einem Paketnamen ein gueltiges AppStream-ID-Segment.""" - part = re.sub(r"[^A-Za-z0-9]+", "-", value).strip("-") - if not part: - die(f"Paketname ergibt kein gueltiges ID-Segment: {value!r}") - if part[0].isdigit(): - part = "pkg-" + part - return part - - -def prettify_name(pkgname): - """Fallback-Anzeigename, wenn in der YAML keiner gesetzt ist. - - dotnet-sdk-9.0 -> Dotnet Sdk 9.0 - """ - words = re.split(r"[-_]", pkgname) - return " ".join(w[:1].upper() + w[1:] if w else w for w in words) - - -def as_list(value): - if value is None: - return [] - if isinstance(value, (list, tuple)): - return list(value) - return [value] - - -def normalize_entry(entry, source_name): - """Akzeptiert sowohl 'paketname' als auch ein Mapping mit Metadaten.""" - if isinstance(entry, str): - return {"name": entry, "_source": source_name} - if isinstance(entry, dict): - if "name" not in entry: - die(f"Paketeintrag in Quelle {source_name!r} ohne 'name': {entry!r}") - data = dict(entry) - data["_source"] = source_name - return data - die(f"Unerwarteter Paketeintrag in Quelle {source_name!r}: {entry!r}") - - -def pick(entry, defaults, key, fallback=None): - """Wert aus dem Paketeintrag, sonst aus defaults, sonst fallback.""" - if key in entry and entry[key] is not None: - return entry[key] - if key in defaults and defaults[key] is not None: - return defaults[key] - return fallback - - -def build_component(entry, defaults, id_prefix): - pkgname = entry["name"] - - comp_type = pick(entry, defaults, "type", DEFAULT_TYPE) - component = ET.Element("component", {"type": comp_type}) - - comp_id = entry.get("id") or f"{id_prefix}.{sanitize_id_part(pkgname)}" - ET.SubElement(component, "id").text = comp_id - ET.SubElement(component, "pkgname").text = pkgname - - display_name = entry.get("display_name") or entry.get("name_display") - if not display_name: - display_name = prettify_name(pkgname) - ET.SubElement(component, "name").text = display_name - - note = pick(entry, defaults, "note", DEFAULT_NOTE) - summary = entry.get("summary") or note - ET.SubElement(component, "summary").text = summary - - # Beschreibung: eigener Text plus Hinweis auf die Herkunft. - description = ET.SubElement(component, "description") - body = entry.get("description") - if body: - for paragraph in [p.strip() for p in str(body).split("\n\n") if p.strip()]: - ET.SubElement(description, "p").text = paragraph - if note and note != summary: - ET.SubElement(description, "p").text = note - elif not body and note == summary: - ET.SubElement(description, "p").text = note - - icon = pick(entry, defaults, "icon", DEFAULT_ICON) - if icon: - ET.SubElement(component, "icon", {"type": "stock"}).text = icon - - categories = as_list(pick(entry, defaults, "categories")) - if categories: - cat_el = ET.SubElement(component, "categories") - for category in categories: - ET.SubElement(cat_el, "category").text = str(category) - - keywords = as_list(entry.get("keywords")) - if keywords: - kw_el = ET.SubElement(component, "keywords") - for keyword in keywords: - ET.SubElement(kw_el, "keyword").text = str(keyword) - - binaries = as_list(entry.get("binary") or entry.get("binaries")) - if binaries: - provides = ET.SubElement(component, "provides") - for binary in binaries: - ET.SubElement(provides, "binary").text = str(binary) - - homepage = entry.get("homepage") or defaults.get("homepage") - if homepage: - ET.SubElement(component, "url", {"type": "homepage"}).text = str(homepage) - - help_url = entry.get("help_url") or defaults.get("help_url") - if help_url: - ET.SubElement(component, "url", {"type": "help"}).text = str(help_url) - - developer = entry.get("developer") or defaults.get("developer") - if developer: - # Das id-Attribut ist seit AppStream 1.0 erwartet; ohne es meldet - # appstreamcli 'developer-id-missing'. - dev_id = entry.get("developer_id") or defaults.get("developer_id") - attrs = {"id": str(dev_id)} if dev_id else {} - dev_el = ET.SubElement(component, "developer", attrs) - ET.SubElement(dev_el, "name").text = str(developer) - - project_license = entry.get("project_license") or defaults.get("project_license") - if project_license: - ET.SubElement(component, "project_license").text = str(project_license) - - # desktop-application braucht einen Launchable-Eintrag, sonst ignoriert - # Discover die Komponente beim Starten der installierten App. - if comp_type == "desktop-application": - desktop_id = entry.get("desktop_id") - if desktop_id: - ET.SubElement( - component, "launchable", {"type": "desktop-id"} - ).text = str(desktop_id) - else: - warn( - f"{pkgname}: type=desktop-application ohne 'desktop_id'. " - "Discover kann die Anwendung dann nicht starten." - ) - - return comp_id, component - - -def load_available(path): - if not path: - return None - try: - with open(path, "r", encoding="utf-8") as handle: - names = {line.strip() for line in handle if line.strip()} - except OSError as exc: - die(f"Konnte Paketliste {path!r} nicht lesen: {exc}") - info(f"{len(names)} tatsaechlich vorhandene Pakete eingelesen") - return names - - -def main(): - parser = argparse.ArgumentParser( - description="Erzeugt einen AppStream-Katalog aus packages.yaml" - ) - parser.add_argument("packages_yaml", help="Pfad zur packages.yaml") - parser.add_argument( - "-o", "--output", default="appstream.xml.gz", - help="Zieldatei (.xml.gz oder .xml), Standard: appstream.xml.gz", - ) - parser.add_argument( - "--origin", - help="Origin des Katalogs. Ueberschreibt 'appstream.origin' aus der YAML.", - ) - parser.add_argument( - "--available-packages", - help="Datei mit den tatsaechlich gespiegelten Paketnamen (einer pro Zeile). " - "Nicht vorhandene Pakete werden dann uebersprungen.", - ) - parser.add_argument( - "--source", - help="Nur Pakete dieser einen Quelle beruecksichtigen. Fuer Setups " - "mit einem Repo-Verzeichnis pro Quelle.", - ) - parser.add_argument( - "--strict", action="store_true", - help="Mit Fehler abbrechen, wenn ein gelistetes Paket nicht vorhanden ist.", - ) - args = parser.parse_args() - - try: - with open(args.packages_yaml, "r", encoding="utf-8") as handle: - config = yaml.safe_load(handle) or {} - except OSError as exc: - die(f"Konnte {args.packages_yaml!r} nicht lesen: {exc}") - except yaml.YAMLError as exc: - die(f"YAML-Fehler in {args.packages_yaml!r}: {exc}") - - appstream_cfg = config.get("appstream") or {} - defaults = appstream_cfg.get("defaults") or {} - origin = args.origin or appstream_cfg.get("origin") or "firmen-repo" - id_prefix = appstream_cfg.get("id_prefix") or "de.firma.repo" - - # Falls kein developer_id gesetzt ist, die ersten beiden Segmente des - # id_prefix verwenden: de.firma.repo -> de.firma - if "developer_id" not in defaults or not defaults.get("developer_id"): - segments = id_prefix.split(".") - if len(segments) >= 2: - defaults = dict(defaults) - defaults["developer_id"] = ".".join(segments[:2]) - - available = load_available(args.available_packages) - - sources = config.get("sources") or [] - if not sources: - die("Keine 'sources' in der packages.yaml gefunden.") - - if args.source: - sources = [s for s in sources if s.get("name") == args.source] - if not sources: - die(f"Keine Quelle mit dem Namen {args.source!r} in der YAML gefunden.") - info(f"Beschraenke auf Quelle {args.source!r}") - - root = ET.Element("components", {"version": CATALOG_VERSION, "origin": origin}) - - seen_ids = {} - seen_pkgs = set() - skipped = [] - count = 0 - - for source in sources: - source_name = source.get("name", "") - for raw_entry in source.get("packages") or []: - entry = normalize_entry(raw_entry, source_name) - pkgname = entry["name"] - - if entry.get("appstream") is False or entry.get("hidden") is True: - info(f"{pkgname}: per Konfiguration von AppStream ausgenommen") - continue - - if available is not None and pkgname not in available: - skipped.append(pkgname) - continue - - if pkgname in seen_pkgs: - warn(f"{pkgname}: mehrfach gelistet, spaetere Eintraege ignoriert") - continue - seen_pkgs.add(pkgname) - - comp_id, component = build_component(entry, defaults, id_prefix) - if comp_id in seen_ids: - die( - f"Doppelte AppStream-ID {comp_id!r} " - f"({seen_ids[comp_id]} und {pkgname}). " - "Setze 'id:' fuer eines der beiden Pakete." - ) - seen_ids[comp_id] = pkgname - root.append(component) - count += 1 - - if skipped: - message = ( - f"{len(skipped)} gelistete Pakete sind nicht im Repo vorhanden " - f"und wurden uebersprungen: {', '.join(sorted(skipped))}" - ) - if args.strict: - die(message) - warn(message) - - raw = ET.tostring(root, encoding="utf-8") - pretty = minidom.parseString(raw).toprettyxml(indent=" ", encoding="UTF-8") - - try: - if args.output.endswith(".gz"): - with gzip.open(args.output, "wb") as handle: - handle.write(pretty) - else: - with open(args.output, "wb") as handle: - handle.write(pretty) - except OSError as exc: - die(f"Konnte {args.output!r} nicht schreiben: {exc}") - - info(f"{count} Komponente(n) nach {args.output} geschrieben (origin={origin})") - if count == 0: - warn("Katalog ist leer. Discover wird nichts anzeigen.") - - -if __name__ == "__main__": - main() \ No newline at end of file