lsa-planer
LSA-Planer Professional – Planungssoftware für Lichtsignalanlagen nach RiLSA 2015 und § 45 StVO. EUPL-1.2.
| 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}`); |