Acknowledge all bundled third-party software and satisfy attribution/notice requirements, while keeping it maintainable: - THIRD_PARTY_NOTICES.md: human-readable manifest (component, copyright, SPDX license, link) for fetched, vendored, and runtime/SDK dependencies. - licenses/: verbatim license texts; COLLECT.sh regenerates them from the build trees and system SDK locations. - Bundle the verbatim Qt LGPL-3.0 text and the CUDA Toolkit 12.8 EULA. - frontend: self-contained npm attribution generator (`npm run licenses` -> dist/THIRD_PARTY_LICENSES.txt), wired into the frontend build target. - Install LICENSE + notices + licenses/ into share/doc/jfjoch for every packaged component. - viewer: Help > "Third-party Licenses" window (QTextBrowser) showing a generated, self-contained HTML built from licenses/. - docs/SOFTWARE.md: drop the stale hand-kept dependency lists; point at the manifest. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
122 lines
4.4 KiB
JavaScript
122 lines
4.4 KiB
JavaScript
// Generate frontend/dist/THIRD_PARTY_LICENSES.txt: the third-party attribution file for the
|
|
// npm packages bundled into the built frontend.
|
|
//
|
|
// Self-contained (no extra dependency): it asks npm for the *production* dependency tree, then
|
|
// reads each package's own license file from node_modules and concatenates them. Run via
|
|
// `npm run licenses` (also invoked by the CMake `frontend` build target).
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { readFileSync, readdirSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
|
|
import { join, dirname } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const frontendDir = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
const nodeModules = join(frontendDir, "node_modules");
|
|
const outFile = join(frontendDir, "dist", "THIRD_PARTY_LICENSES.txt");
|
|
|
|
const LICENSE_FILE = /^(licen[sc]e|copying|notice)([.-].*)?$/i;
|
|
|
|
// Set of "name@version" strings that actually ship in production (excludes devDependencies).
|
|
function productionPackages() {
|
|
const json = execFileSync("npm", ["ls", "--omit=dev", "--all", "--json"], {
|
|
cwd: frontendDir,
|
|
encoding: "utf8",
|
|
maxBuffer: 64 * 1024 * 1024,
|
|
});
|
|
const set = new Set();
|
|
const walk = (deps) => {
|
|
for (const [name, info] of Object.entries(deps || {})) {
|
|
if (info.version) set.add(`${name}@${info.version}`);
|
|
walk(info.dependencies);
|
|
}
|
|
};
|
|
walk(JSON.parse(json).dependencies);
|
|
return set;
|
|
}
|
|
|
|
// Map "name@version" -> package directory, by scanning every node_modules in the install tree.
|
|
function installedPackages() {
|
|
const map = new Map();
|
|
const scan = (nm) => {
|
|
if (!existsSync(nm)) return;
|
|
for (const entry of readdirSync(nm)) {
|
|
if (entry.startsWith(".")) continue;
|
|
if (entry.startsWith("@")) {
|
|
for (const sub of readdirSync(join(nm, entry))) record(join(nm, entry, sub));
|
|
} else {
|
|
record(join(nm, entry));
|
|
}
|
|
}
|
|
};
|
|
const record = (dir) => {
|
|
const pkgJson = join(dir, "package.json");
|
|
if (!existsSync(pkgJson)) return;
|
|
try {
|
|
const pkg = JSON.parse(readFileSync(pkgJson, "utf8"));
|
|
if (pkg.name && pkg.version) map.set(`${pkg.name}@${pkg.version}`, { dir, pkg });
|
|
} catch { /* ignore unparseable package.json */ }
|
|
scan(join(dir, "node_modules"));
|
|
};
|
|
scan(nodeModules);
|
|
return map;
|
|
}
|
|
|
|
function findLicenseText(dir) {
|
|
const file = readdirSync(dir).find((f) => LICENSE_FILE.test(f));
|
|
if (!file) return null;
|
|
try {
|
|
return readFileSync(join(dir, file), "utf8").trimEnd();
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
function licenseId(pkg) {
|
|
if (typeof pkg.license === "string") return pkg.license;
|
|
if (pkg.license?.type) return pkg.license.type;
|
|
if (Array.isArray(pkg.licenses)) return pkg.licenses.map((l) => l.type || l).join(" OR ");
|
|
return "UNKNOWN";
|
|
}
|
|
|
|
function repoUrl(pkg) {
|
|
const r = pkg.repository;
|
|
if (typeof r === "string") return r;
|
|
if (r?.url) return r.url.replace(/^git\+/, "").replace(/\.git$/, "");
|
|
return pkg.homepage || "";
|
|
}
|
|
|
|
const prod = productionPackages();
|
|
const installed = installedPackages();
|
|
|
|
const blocks = [];
|
|
const missing = [];
|
|
for (const id of [...prod].sort((a, b) => a.localeCompare(b))) {
|
|
const found = installed.get(id);
|
|
if (!found) continue; // present in tree report but not on disk (e.g. bundled) — skip
|
|
const { dir, pkg } = found;
|
|
const text = findLicenseText(dir);
|
|
if (!text) missing.push(`${id} (${licenseId(pkg)})`);
|
|
const url = repoUrl(pkg);
|
|
blocks.push(
|
|
`${"=".repeat(80)}\n` +
|
|
`${id} — ${licenseId(pkg)}\n` +
|
|
(url ? `${url}\n` : "") +
|
|
`${"=".repeat(80)}\n\n` +
|
|
(text || `License: ${licenseId(pkg)} (no license file shipped in this package)`),
|
|
);
|
|
}
|
|
|
|
const header =
|
|
"Third-party software bundled in the Jungfraujoch frontend\n" +
|
|
"=========================================================\n\n" +
|
|
"The Jungfraujoch frontend is licensed under GPL-3.0. It bundles the following npm packages\n" +
|
|
"(production dependencies). Each package's license and notice text follows.\n\n" +
|
|
`Generated by scripts/generate-third-party-licenses.mjs — ${prod.size} packages.\n` +
|
|
(missing.length
|
|
? `\nPackages without a bundled license file (license field shown above):\n ${missing.join("\n ")}\n`
|
|
: "");
|
|
|
|
mkdirSync(dirname(outFile), { recursive: true });
|
|
writeFileSync(outFile, header + "\n" + blocks.join("\n\n") + "\n", "utf8");
|
|
console.log(`Wrote ${outFile} (${blocks.length} packages, ${missing.length} without license file).`);
|