waffensachkunde

Waffensachkunde – Lernsoftware für die Sachkundeprüfung nach § 7 WaffG. Barrierefrei, offline, EUPL-1.2.

/ tools lizenzen_erzeugen.py

6,8 KB Rohdatei
tools/lizenzen_erzeugen.py — 212 Zeilen
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())