lsa-planer

LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.

/ scripts lizenztexte.mjs

8,6 KB Rohdatei
scripts/lizenztexte.mjs — 228 Zeilen
1 import { readFile, readdir, mkdir, writeFile } from 'node:fs/promises';
2 import { fileURLToPath, URL } from 'node:url';
3 import { dirname, join } from 'node:path';
4
5 /**
6 * Lizenztexte der gebuendelten Fremdbestandteile - eingesammelt, nicht gepflegt.
7 *
8 * Wozu das gut ist: Der MIT-Text verlangt, dass Urheber- und Genehmigungsvermerk
9 * mit jeder Kopie gehen. Seit 5.14.0 stehen sie im uebersetzten Renderer selbst -
10 * `lizenzbanner` in vite.config.ts setzt jedem Ausgabebrocken die Lizenztexte der
11 * Bibliotheken voran, deren Kode wirklich darin steckt (nachgemessen in
12 * docs/fremdbestandteile.md unter "Was aussteht"). Diese Datei traegt sie
13 * zusaetzlich geschlossen neben die Programmdatei, dorthin, wo electron-builder
14 * auch LICENSE.electron.txt und LICENSES.chromium.html ablegt: Wer nachsehen
15 * will, unter welchen Bedingungen er das Programm bekommen hat, soll das nicht
16 * in einer verkuerzten Ausgabedatei tun muessen.
17 *
18 * DIE REGEL FUER DEN DATEINAMEN STEHT AN ZWEI STELLEN. Hier als LIZENZDATEI und
19 * ebenso in vite.config.ts; beide lesen dieselben Dateien aus node_modules. Ein
20 * gemeinsamer Bezug ginge nur ueber eine Deklarationsdatei oder `allowJs` in
21 * tsconfig.json - vite.config.ts wird typgeprueft und darf keine .mjs einbinden.
22 * Zusammengehalten werden die beiden stattdessen von zwei Wachen in
23 * tests/tools/werkbank.test.ts: Sie suchen die Lizenzdateien mit einer
24 * dritten Fassung derselben Regel und verlangen, dass JEDE davon woertlich im
25 * Banner ihrer Ausgabedatei steht und mit ihrem Rechteinhaber in der
26 * Sammeldatei. Faellt eine der drei Regeln aus dem Tritt, wird eine der beiden
27 * rot.
28 *
29 * WARUM ERZEUGT UND NICHT GESCHRIEBEN: Ein von Hand gefuehrtes Lizenzverzeichnis
30 * veraltet beim naechsten `npm install` still. Die Liste der Bibliotheken kommt
31 * deshalb aus den QUELLKARTEN des gebauten Renderers - also aus dem, was Vite
32 * wirklich hineinuebersetzt hat, nicht aus einer Aufstellung, die jemand pflegen
33 * muesste -, und der Text je Bibliothek woertlich aus dem installierten Paket.
34 *
35 * WARUM ES LIEBER ABBRICHT: Findet es zu einer Bibliothek keinen Text, schreibt
36 * es nichts. Eine Sammeldatei, in der eine fehlt, ist schlimmer als keine - sie
37 * sieht nach Erfuellung aus.
38 *
39 * Aufruf (beide Angaben haben Vorgabewerte und werden selten gebraucht):
40 *
41 * node scripts/lizenztexte.mjs [--bau <ordner>] [--ziel <datei>]
42 *
43 * --bau Ordner mit den Ausgabedateien und Quellkarten des Renderers.
44 * Vorgabe: build/renderer/assets
45 * --ziel Zieldatei. Vorgabe: build/DRITTANBIETER-LIZENZEN.txt
46 *
47 * `npm run build` ruft es ohne Angaben; `build.extraFiles` in package.json legt
48 * das Ergebnis danach neben die Programmdatei.
49 */
50
51 const WURZEL = fileURLToPath(new URL('..', import.meta.url));
52
53 /** Eine Angabe von der Kommandozeile, oder der Vorgabewert. */
54 function angabe(name, vorgabe) {
55 const stelle = process.argv.indexOf(`--${name}`);
56 if (stelle < 0) return vorgabe;
57 const wert = process.argv[stelle + 1];
58 if (wert === undefined || wert.startsWith('--')) {
59 console.error(`Zu --${name} fehlt die Angabe.`);
60 process.exit(1);
61 }
62 return wert;
63 }
64
65 const BAUABLAGE = angabe('bau', join(WURZEL, 'build', 'renderer', 'assets'));
66 const ZIEL = angabe('ziel', join(WURZEL, 'build', 'DRITTANBIETER-LIZENZEN.txt'));
67
68 /** Lizenzdatei, Lizenzhinweis, COPYING - so heissen die Texte in den Paketen. */
69 const LIZENZDATEI = /^(licen[sc]e|copying)/i;
70
71 function abbruch(satz) {
72 console.error(satz);
73 process.exit(1);
74 }
75
76 /*
77 * Welche Pakete stecken im gebauten Renderer?
78 *
79 * Die Quellkarten fuehren in `sources` jede Ursprungsdatei; alles unterhalb von
80 * node_modules/ ist ein Fremdbestandteil. Ein Bereichsname (@babel/runtime)
81 * besteht aus zwei Abschnitten, jeder andere aus einem.
82 */
83 let karten;
84 try {
85 karten = (await readdir(BAUABLAGE)).filter((d) => d.endsWith('.js.map'));
86 } catch {
87 abbruch(`Kein Ordner "${BAUABLAGE}". Bauen Sie zuerst den Renderer, etwa mit "npm run build".`);
88 }
89
90 if (karten.length === 0) {
91 abbruch(
92 `Keine Quellkarten in "${BAUABLAGE}". Ohne sie ist nicht feststellbar, welche Bibliotheken gebuendelt wurden.`,
93 );
94 }
95
96 const namen = new Set();
97 for (const karte of karten) {
98 const inhalt = JSON.parse(await readFile(join(BAUABLAGE, karte), 'utf8'));
99 for (const quelle of inhalt.sources ?? []) {
100 const stelle = quelle.lastIndexOf('node_modules/');
101 if (stelle < 0) continue;
102 const teile = quelle.slice(stelle + 'node_modules/'.length).split('/');
103 namen.add(teile[0].startsWith('@') ? `${teile[0]}/${teile[1]}` : teile[0]);
104 }
105 }
106
107 if (namen.size === 0) {
108 abbruch(
109 `Die Quellkarten in "${BAUABLAGE}" nennen keine Bibliothek. Das ist kein Ergebnis, sondern ein Fehler - abgebrochen.`,
110 );
111 }
112
113 /*
114 * Je Paket: Fassung, Lizenzangabe, Herkunft und der Text selbst - alles aus dem
115 * installierten Paket, nichts aus dieser Datei.
116 */
117 const eintraege = [];
118 for (const name of [...namen].sort()) {
119 const ablage = join(WURZEL, 'node_modules', name);
120 let paket;
121 try {
122 paket = JSON.parse(await readFile(join(ablage, 'package.json'), 'utf8'));
123 } catch {
124 abbruch(`Kein installiertes Paket "${name}" - abgebrochen, nichts geschrieben.`);
125 }
126
127 let dateien;
128 try {
129 dateien = (await readdir(ablage)).filter((d) => LIZENZDATEI.test(d)).sort();
130 } catch {
131 dateien = [];
132 }
133 if (dateien.length === 0) {
134 abbruch(
135 `Keine Lizenzdatei in node_modules/${name} - abgebrochen, nichts geschrieben. Eine Sammeldatei ohne diesen Text sieht nach Erfuellung aus und ist keine.`,
136 );
137 }
138
139 const texte = [];
140 for (const datei of dateien) {
141 // Zeilenenden vereinheitlichen: Die Pakete bringen beides mit, und eine
142 // gemischte Datei laesst ein Textfenster unter Windows Zeilen verschlucken.
143 const text = (await readFile(join(ablage, datei), 'utf8')).replace(/\r\n/g, '\n').trim();
144 if (text === '') {
145 abbruch(`node_modules/${name}/${datei} ist leer - abgebrochen, nichts geschrieben.`);
146 }
147 texte.push({ datei, text });
148 }
149
150 const herkunft =
151 typeof paket.homepage === 'string'
152 ? paket.homepage
153 : typeof paket.repository === 'string'
154 ? paket.repository
155 : typeof paket.repository?.url === 'string'
156 ? paket.repository.url
157 : '';
158
159 eintraege.push({
160 name,
161 fassung: String(paket.version),
162 lizenz: String(paket.license ?? '(keine Angabe im Paket)'),
163 herkunft,
164 texte,
165 });
166 }
167
168 const paketdatei = JSON.parse(await readFile(join(WURZEL, 'package.json'), 'utf8'));
169 const trennlinie = '='.repeat(76);
170 const unterstrich = '-'.repeat(76);
171
172 const kopf = [
173 `LSA-Planer Professional ${paketdatei.version} – Lizenztexte der mitgelieferten Bibliotheken`,
174 '',
175 'Der LSA-Planer übersetzt fremde Bibliotheken in sein Programm hinein. Ihre',
176 'Lizenzen verlangen, dass Urheber- und Genehmigungsvermerk mit jeder Kopie',
177 'gehen. Diese Datei trägt sie – wörtlich so, wie sie dem jeweiligen Paket',
178 'beiliegen.',
179 '',
180 'Electron und Chromium stehen nicht hier. Ihre Texte liegen als',
181 'LICENSE.electron.txt und LICENSES.chromium.html daneben.',
182 '',
183 'Was diese Bibliotheken sind, woher sie kommen und wozu das Programm sie',
184 'benutzt, verzeichnet FREMDBESTANDTEILE.md daneben.',
185 '',
186 'Diese Datei ist erzeugt, nicht gepflegt: scripts/lizenztexte.mjs nimmt die',
187 'Liste aus den Quellkarten des gebauten Renderers – also aus dem, was wirklich',
188 'gebündelt wurde – und den Text je Bibliothek aus dem installierten Paket.',
189 '',
190 'Das ist keine Rechtsauskunft.',
191 '',
192 `Es folgen ${eintraege.length} Texte.`,
193 '',
194 ];
195
196 const abschnitte = [];
197 for (const e of eintraege) {
198 abschnitte.push(trennlinie);
199 abschnitte.push(`${e.name} ${e.fassung} – ${e.lizenz}`);
200 if (e.herkunft !== '') abschnitte.push(e.herkunft);
201 abschnitte.push(trennlinie);
202 for (const t of e.texte) {
203 abschnitte.push('');
204 abschnitte.push(`(node_modules/${e.name}/${t.datei})`);
205 abschnitte.push(unterstrich);
206 abschnitte.push('');
207 abschnitte.push(t.text);
208 }
209 abschnitte.push('');
210 abschnitte.push('');
211 }
212
213 /*
214 * Mit Bytefolgemarke, aus demselben Grund wie bei den Pruefsummen: Der Text
215 * enthaelt Umlaute, und ein Windows-Editor, der die Datei ohne Marke als ANSI
216 * liest, zeigt daraus Buchstabensalat. Der Dateiname bleibt ASCII.
217 *
218 * Als Fluchtfolge und nicht als Zeichen: Ein unsichtbares U+FEFF im Quelltext
219 * beanstandet der Linter (no-irregular-whitespace), und zu Recht - man sieht
220 * ihm nicht an, dass er da ist.
221 */
222 await mkdir(dirname(ZIEL), { recursive: true });
223 await writeFile(ZIEL, `\uFEFF${[...kopf, ...abschnitte].join('\n')}`, 'utf8');
224
225 console.log(
226 `Lizenztexte für ${eintraege.length} Bibliotheken: ${eintraege.map((e) => e.name).join(', ')}`,
227 );
228 console.log(`geschrieben: ${ZIEL}`);