waffensachkunde
Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.
| 1 | # -*- coding: utf-8 -*- |
| 2 | """Erzeugt die Lizenzübersicht der mitgelieferten Fremdsoftware. |
| 3 | |
| 4 | Die Anwendung zeigt diese Übersicht unter „Über diese Software“ an. Das ist |
| 5 | keine Höflichkeit, sondern Pflicht: Die verwendeten Lizenzen (MIT, BSD, |
| 6 | Apache-2.0, ISC …) verlangen sämtlich, dass Lizenztext und Urheberrechtsvermerk |
| 7 | mitgeliefert werden. |
| 8 | |
| 9 | Ausgabe: content/drittlizenzen.json – wird beim Bauen in die Anwendung kopiert. |
| 10 | |
| 11 | Aufruf: |
| 12 | python tools/lizenzen_erzeugen.py |
| 13 | """ |
| 14 | from __future__ import annotations |
| 15 | |
| 16 | import io |
| 17 | import json |
| 18 | import sys |
| 19 | from datetime import date |
| 20 | from pathlib import Path |
| 21 | |
| 22 | sys.stdout = io.TextIOWrapper(sys.stdout.buffer, encoding="utf-8") |
| 23 | |
| 24 | WURZEL = Path(__file__).resolve().parent.parent |
| 25 | APP = WURZEL / "app" |
| 26 | NODE_MODULES = APP / "node_modules" |
| 27 | ZIEL = WURZEL / "content" / "drittlizenzen.json" |
| 28 | |
| 29 | PAKET_SPERRE = APP / "package-lock.json" |
| 30 | |
| 31 | # Pakete, die zwar Entwicklungsabhängigkeiten sind, aber im Bündel landen: |
| 32 | # React wird in den Renderer hineinkompiliert, Electron liefert die Laufzeit. |
| 33 | # Sie stehen deshalb fest in der Liste – aus dem Abhängigkeitsbaum ließe sich |
| 34 | # das nicht ableiten. |
| 35 | GEBUENDELT = [ |
| 36 | "electron", |
| 37 | "react", |
| 38 | "react-dom", |
| 39 | ] |
| 40 | |
| 41 | |
| 42 | def produktionsabhaengigkeiten() -> list[str]: |
| 43 | """Alle Pakete, die electron-builder in das Paket kopiert. |
| 44 | |
| 45 | Fest verdrahtet war diese Liste einmal – und lief prompt auseinander: |
| 46 | better-sqlite3 zieht node-addon-api nach, das mit ausgeliefert wurde, in |
| 47 | der Anzeige aber fehlte. Abgeleitet wird sie deshalb aus package-lock.json, |
| 48 | genau wie electron-builder es tut. |
| 49 | """ |
| 50 | if not PAKET_SPERRE.exists(): |
| 51 | return [] |
| 52 | |
| 53 | daten = json.loads(PAKET_SPERRE.read_text(encoding="utf-8")) |
| 54 | namen = [] |
| 55 | for pfad, eintrag in (daten.get("packages") or {}).items(): |
| 56 | if not pfad.startswith("node_modules/"): |
| 57 | continue |
| 58 | if eintrag.get("dev") or eintrag.get("devOptional"): |
| 59 | continue |
| 60 | namen.append(pfad[len("node_modules/"):]) |
| 61 | return sorted(namen) |
| 62 | |
| 63 | # Dateinamen, unter denen Pakete ihren Lizenztext ablegen. |
| 64 | LIZENZDATEIEN = [ |
| 65 | "LICENSE", "LICENSE.md", "LICENSE.txt", "LICENCE", "LICENCE.md", |
| 66 | "License", "license", "LICENSE-MIT", "COPYING", |
| 67 | ] |
| 68 | |
| 69 | # Bestandteile, die Electron mitbringt und die eigene Lizenzen tragen. |
| 70 | ZUSAETZLICH = [ |
| 71 | { |
| 72 | "name": "Chromium", |
| 73 | "lizenz": "BSD-3-Clause und weitere", |
| 74 | "hinweis": ( |
| 75 | "In Electron enthaltene Browser-Engine. Der vollständige Lizenztext " |
| 76 | "liegt der Installation als LICENSES.chromium.html bei." |
| 77 | ), |
| 78 | "url": "https://www.chromium.org/", |
| 79 | }, |
| 80 | { |
| 81 | "name": "Node.js", |
| 82 | "lizenz": "MIT", |
| 83 | "hinweis": "In Electron enthaltene Laufzeitumgebung.", |
| 84 | "url": "https://nodejs.org/", |
| 85 | }, |
| 86 | { |
| 87 | "name": "SQLite", |
| 88 | "lizenz": "Public Domain", |
| 89 | "hinweis": ( |
| 90 | "Datenbank hinter better-sqlite3. SQLite ist gemeinfrei und " |
| 91 | "unterliegt keinen Lizenzbedingungen." |
| 92 | ), |
| 93 | "url": "https://www.sqlite.org/copyright.html", |
| 94 | }, |
| 95 | ] |
| 96 | |
| 97 | |
| 98 | def lies_lizenztext(ordner: Path) -> str | None: |
| 99 | for name in LIZENZDATEIEN: |
| 100 | pfad = ordner / name |
| 101 | if pfad.exists() and pfad.is_file(): |
| 102 | text = pfad.read_text(encoding="utf-8", errors="replace").strip() |
| 103 | return text if text else None |
| 104 | return None |
| 105 | |
| 106 | |
| 107 | def lizenzkennung(meta: dict) -> str: |
| 108 | lizenz = meta.get("license") |
| 109 | if isinstance(lizenz, dict): |
| 110 | return lizenz.get("type", "unbekannt") |
| 111 | if not lizenz and meta.get("licenses"): |
| 112 | return ", ".join(l.get("type", "?") for l in meta["licenses"]) |
| 113 | return lizenz or "unbekannt" |
| 114 | |
| 115 | |
| 116 | def herkunft(meta: dict) -> str | None: |
| 117 | repo = meta.get("repository") |
| 118 | if isinstance(repo, dict): |
| 119 | repo = repo.get("url") |
| 120 | return meta.get("homepage") or repo |
| 121 | |
| 122 | |
| 123 | def main() -> int: |
| 124 | if not NODE_MODULES.exists(): |
| 125 | print(f"FEHLER: {NODE_MODULES} fehlt. Zuerst 'npm install' im Ordner app/ ausführen.") |
| 126 | return 1 |
| 127 | |
| 128 | produktion = produktionsabhaengigkeiten() |
| 129 | if not produktion: |
| 130 | print(f"FEHLER: {PAKET_SPERRE} fehlt oder nennt keine Produktionsabhängigkeit.") |
| 131 | return 1 |
| 132 | |
| 133 | # Reihenfolge: erst die gebündelten, dann der Produktionsbaum. |
| 134 | ausgeliefert = GEBUENDELT + [p for p in produktion if p not in GEBUENDELT] |
| 135 | |
| 136 | eintraege: list[dict] = [] |
| 137 | fehlend: list[str] = [] |
| 138 | |
| 139 | for paket in ausgeliefert: |
| 140 | ordner = NODE_MODULES / paket |
| 141 | pkg = ordner / "package.json" |
| 142 | if not pkg.exists(): |
| 143 | fehlend.append(paket) |
| 144 | continue |
| 145 | meta = json.loads(pkg.read_text(encoding="utf-8")) |
| 146 | text = lies_lizenztext(ordner) |
| 147 | if text is None: |
| 148 | fehlend.append(f"{paket} (kein Lizenztext gefunden)") |
| 149 | eintraege.append({ |
| 150 | "name": paket, |
| 151 | "version": meta.get("version"), |
| 152 | "lizenz": lizenzkennung(meta), |
| 153 | "url": herkunft(meta), |
| 154 | "lizenztext": text, |
| 155 | }) |
| 156 | |
| 157 | for extra in ZUSAETZLICH: |
| 158 | eintraege.append({ |
| 159 | "name": extra["name"], |
| 160 | "version": None, |
| 161 | "lizenz": extra["lizenz"], |
| 162 | "url": extra["url"], |
| 163 | "lizenztext": None, |
| 164 | "hinweis": extra["hinweis"], |
| 165 | }) |
| 166 | |
| 167 | # Jede Produktionsabhängigkeit muss in der Liste stehen. Ohne diese Sperre |
| 168 | # bliebe eine Verschiebung im Abhängigkeitsbaum still, bis jemand die |
| 169 | # Anzeige mit dem Paketinhalt vergleicht. |
| 170 | genannt = {e["name"] for e in eintraege} |
| 171 | uebersehen = [p for p in produktion if p not in genannt] |
| 172 | if uebersehen: |
| 173 | print(f"FEHLER: nicht aufgeführte Produktionsabhängigkeiten: {', '.join(uebersehen)}") |
| 174 | return 1 |
| 175 | |
| 176 | anwendungsversion = json.loads((APP / "package.json").read_text(encoding="utf-8")).get( |
| 177 | "version", "unbekannt" |
| 178 | ) |
| 179 | |
| 180 | ZIEL.parent.mkdir(parents=True, exist_ok=True) |
| 181 | ZIEL.write_text( |
| 182 | json.dumps( |
| 183 | { |
| 184 | "stand": date.today().isoformat(), |
| 185 | "anwendungsversion": anwendungsversion, |
| 186 | "pakete": eintraege, |
| 187 | }, |
| 188 | ensure_ascii=False, |
| 189 | indent=2, |
| 190 | ), |
| 191 | encoding="utf-8", |
| 192 | # Das Archiv verlangt LF (.gitattributes). Ohne diese Angabe |
| 193 | # schriebe Python auf Windows CRLF: Der Arbeitsbaum sähe |
| 194 | # geändert aus, ohne dass sich am Inhalt etwas geändert hätte. |
| 195 | newline="\n", |
| 196 | ) |
| 197 | |
| 198 | print(f"Geschrieben: {ZIEL}") |
| 199 | for e in eintraege: |
| 200 | laenge = len(e["lizenztext"]) if e.get("lizenztext") else 0 |
| 201 | version = f" {e['version']}" if e["version"] else "" |
| 202 | print(f" {e['name']}{version}: {e['lizenz']}" |
| 203 | + (f" ({laenge} Zeichen Lizenztext)" if laenge else " (Sammelhinweis)")) |
| 204 | if fehlend: |
| 205 | print(f"\nOhne Lizenztext: {', '.join(fehlend)}") |
| 206 | print("Diese Pakete vor der Auslieferung prüfen.") |
| 207 | return 1 |
| 208 | return 0 |
| 209 | |
| 210 | |
| 211 | if __name__ == "__main__": |
| 212 | raise SystemExit(main()) |