# -*- coding: utf-8 -*- """Erzeugt die Lizenzübersicht der mitgelieferten Fremdsoftware. Die Anwendung zeigt diese Übersicht unter „Über diese Software“ an. Das ist keine Höflichkeit, sondern Pflicht: Die verwendeten Lizenzen (MIT, BSD, Apache-2.0, ISC …) verlangen sämtlich, dass Lizenztext und Urheberrechtsvermerk mitgeliefert werden. Ausgabe: content/drittlizenzen.json – wird beim Bauen in die Anwendung kopiert. Aufruf: python tools/lizenzen_erzeugen.py """ from __future__ import annotations import io import json import sys from datetime import date from pathlib import Path sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") WURZEL = Path(__file__).resolve().parent.parent APP = WURZEL / "app" NODE_MODULES = APP / "node_modules" ZIEL = WURZEL / "content" / "drittlizenzen.json" PAKET_SPERRE = APP / "package-lock.json" # Pakete, die zwar Entwicklungsabhängigkeiten sind, aber im Bündel landen: # React wird in den Renderer hineinkompiliert, Electron liefert die Laufzeit. # Sie stehen deshalb fest in der Liste – aus dem Abhängigkeitsbaum ließe sich # das nicht ableiten. GEBUENDELT = [ "electron", "react", "react-dom", ] def produktionsabhaengigkeiten() -> list[str]: """Alle Pakete, die electron-builder in das Paket kopiert. Fest verdrahtet war diese Liste einmal – und lief prompt auseinander: better-sqlite3 zieht node-addon-api nach, das mit ausgeliefert wurde, in der Anzeige aber fehlte. Abgeleitet wird sie deshalb aus package-lock.json, genau wie electron-builder es tut. """ if not PAKET_SPERRE.exists(): return [] daten = json.loads(PAKET_SPERRE.read_text(encoding="utf-8")) namen = [] for pfad, eintrag in (daten.get("packages") or {}).items(): if not pfad.startswith("node_modules/"): continue if eintrag.get("dev") or eintrag.get("devOptional"): continue namen.append(pfad[len("node_modules/"):]) return sorted(namen) # Dateinamen, unter denen Pakete ihren Lizenztext ablegen. LIZENZDATEIEN = [ "LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md", "License", "license", "LICENSE-MIT", "COPYING", ] # Bestandteile, die Electron mitbringt und die eigene Lizenzen tragen. ZUSAETZLICH = [ { "name": "Chromium", "lizenz": "BSD-3-Clause und weitere", "hinweis": ( "In Electron enthaltene Browser-Engine. Der vollständige Lizenztext " "liegt der Installation als LICENSES.chromium.html bei." ), "url": "https://www.chromium.org/", }, { "name": "Node.js", "lizenz": "MIT", "hinweis": "In Electron enthaltene Laufzeitumgebung.", "url": "https://nodejs.org/", }, { "name": "SQLite", "lizenz": "Public Domain", "hinweis": ( "Datenbank hinter better-sqlite3. SQLite ist gemeinfrei und " "unterliegt keinen Lizenzbedingungen." ), "url": "https://www.sqlite.org/copyright.html", }, ] def lies_lizenztext(ordner: Path) -> str | None: for name in LIZENZDATEIEN: pfad = ordner / name if pfad.exists() and pfad.is_file(): text = pfad.read_text(encoding="utf-8", errors="replace").strip() return text if text else None return None def lizenzkennung(meta: dict) -> str: lizenz = meta.get("license") if isinstance(lizenz, dict): return lizenz.get("type", "unbekannt") if not lizenz and meta.get("licenses"): return ", ".join(l.get("type", "?") for l in meta["licenses"]) return lizenz or "unbekannt" def herkunft(meta: dict) -> str | None: repo = meta.get("repository") if isinstance(repo, dict): repo = repo.get("url") return meta.get("homepage") or repo def main() -> int: if not NODE_MODULES.exists(): print(f"FEHLER: {NODE_MODULES} fehlt. Zuerst 'npm install' im Ordner app/ ausführen.") return 1 produktion = produktionsabhaengigkeiten() if not produktion: print(f"FEHLER: {PAKET_SPERRE} fehlt oder nennt keine Produktionsabhängigkeit.") return 1 # Reihenfolge: erst die gebündelten, dann der Produktionsbaum. ausgeliefert = GEBUENDELT + [p for p in produktion if p not in GEBUENDELT] eintraege: list[dict] = [] fehlend: list[str] = [] for paket in ausgeliefert: ordner = NODE_MODULES / paket pkg = ordner / "package.json" if not pkg.exists(): fehlend.append(paket) continue meta = json.loads(pkg.read_text(encoding="utf-8")) text = lies_lizenztext(ordner) if text is None: fehlend.append(f"{paket} (kein Lizenztext gefunden)") eintraege.append({ "name": paket, "version": meta.get("version"), "lizenz": lizenzkennung(meta), "url": herkunft(meta), "lizenztext": text, }) for extra in ZUSAETZLICH: eintraege.append({ "name": extra["name"], "version": None, "lizenz": extra["lizenz"], "url": extra["url"], "lizenztext": None, "hinweis": extra["hinweis"], }) # Jede Produktionsabhängigkeit muss in der Liste stehen. Ohne diese Sperre # bliebe eine Verschiebung im Abhängigkeitsbaum still, bis jemand die # Anzeige mit dem Paketinhalt vergleicht. genannt = {e["name"] for e in eintraege} uebersehen = [p for p in produktion if p not in genannt] if uebersehen: print(f"FEHLER: nicht aufgeführte Produktionsabhängigkeiten: {', '.join(uebersehen)}") return 1 anwendungsversion = json.loads((APP / "package.json").read_text(encoding="utf-8")).get( "version", "unbekannt" ) ZIEL.parent.mkdir(parents=True, exist_ok=True) ZIEL.write_text( json.dumps( { "stand": date.today().isoformat(), "anwendungsversion": anwendungsversion, "pakete": eintraege, }, ensure_ascii=False, indent=2, ), encoding="utf-8", # Das Archiv verlangt LF (.gitattributes). Ohne diese Angabe # schriebe Python auf Windows CRLF: Der Arbeitsbaum sähe # geändert aus, ohne dass sich am Inhalt etwas geändert hätte. newline="\n", ) print(f"Geschrieben: {ZIEL}") for e in eintraege: laenge = len(e["lizenztext"]) if e.get("lizenztext") else 0 version = f" {e['version']}" if e["version"] else "" print(f" {e['name']}{version}: {e['lizenz']}" + (f" ({laenge} Zeichen Lizenztext)" if laenge else " (Sammelhinweis)")) if fehlend: print(f"\nOhne Lizenztext: {', '.join(fehlend)}") print("Diese Pakete vor der Auslieferung prüfen.") return 1 return 0 if __name__ == "__main__": raise SystemExit(main())