mirror of
https://github.com/zhom/donutbrowser.git
synced 2026-08-02 01:08:41 +02:00
235 lines
6.4 KiB
JavaScript
235 lines
6.4 KiB
JavaScript
import { execFileSync } from "node:child_process";
|
|
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
import parseSpdxExpression from "spdx-expression-parse";
|
|
import { XRAY_SOURCE_URL } from "../src-tauri/download-xray.mjs";
|
|
|
|
const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
|
|
const PROJECT_ROOT = resolve(SCRIPT_DIR, "..");
|
|
const OUTPUT_PATH = resolve(PROJECT_ROOT, "src/generated/licenses.json");
|
|
const XRAY_SOURCE_OUTPUT_PATH = resolve(
|
|
PROJECT_ROOT,
|
|
"src/generated/xray-source.json",
|
|
);
|
|
const MAX_COMMAND_OUTPUT = 64 * 1024 * 1024;
|
|
|
|
export const RELEASE_TARGETS = [
|
|
"aarch64-apple-darwin",
|
|
"x86_64-apple-darwin",
|
|
"aarch64-unknown-linux-gnu",
|
|
"x86_64-unknown-linux-gnu",
|
|
"x86_64-pc-windows-msvc",
|
|
];
|
|
|
|
export const MANUAL_LICENSES = [
|
|
{
|
|
name: "Donut Browser",
|
|
license: "AGPL-3.0-only",
|
|
},
|
|
{
|
|
name: "Xray-core",
|
|
license: "MPL-2.0",
|
|
},
|
|
];
|
|
|
|
const LEGACY_LICENSE_EXPRESSIONS = new Map([
|
|
["Apache-2.0 / MIT", "Apache-2.0 OR MIT"],
|
|
["Apache-2.0/MIT", "Apache-2.0 OR MIT"],
|
|
["BSD-3-Clause/MIT", "BSD-3-Clause OR MIT"],
|
|
["MIT/Apache-2.0", "Apache-2.0 OR MIT"],
|
|
["MIT OR Apache-2.0", "Apache-2.0 OR MIT"],
|
|
["Unlicense/MIT", "MIT OR Unlicense"],
|
|
]);
|
|
|
|
const HOST_ONLY_PNPM_NATIVE_PREFIXES = [
|
|
"@img/sharp-",
|
|
"@img/sharp-libvips-",
|
|
"@next/swc-",
|
|
];
|
|
|
|
function validateLicenseExpression(expression) {
|
|
try {
|
|
parseSpdxExpression(expression);
|
|
} catch {
|
|
throw new Error(`Invalid SPDX expression: ${expression}`);
|
|
}
|
|
}
|
|
|
|
export function normalizeLicenseExpression(value) {
|
|
if (typeof value !== "string" || value.trim() === "") {
|
|
throw new Error("Every shipped dependency must declare a license");
|
|
}
|
|
|
|
const expression =
|
|
LEGACY_LICENSE_EXPRESSIONS.get(value.trim()) ?? value.trim();
|
|
validateLicenseExpression(expression);
|
|
return expression;
|
|
}
|
|
|
|
export function collectReachableRustLicenses(metadata) {
|
|
const root = metadata.resolve?.root;
|
|
if (!root) {
|
|
throw new Error("Cargo metadata did not identify the root package");
|
|
}
|
|
|
|
const packages = new Map(
|
|
metadata.packages.map((dependency) => [dependency.id, dependency]),
|
|
);
|
|
const nodes = new Map(metadata.resolve.nodes.map((node) => [node.id, node]));
|
|
const pending = [root];
|
|
const visited = new Set();
|
|
const result = [];
|
|
|
|
while (pending.length > 0) {
|
|
const packageId = pending.pop();
|
|
if (!packageId || visited.has(packageId)) continue;
|
|
visited.add(packageId);
|
|
|
|
if (packageId !== root) {
|
|
const dependency = packages.get(packageId);
|
|
if (!dependency) {
|
|
throw new Error(`Cargo metadata is missing package ${packageId}`);
|
|
}
|
|
result.push({
|
|
name: dependency.name,
|
|
license: dependency.license,
|
|
});
|
|
}
|
|
|
|
const node = nodes.get(packageId);
|
|
if (!node) continue;
|
|
for (const dependency of node.deps) {
|
|
const isRuntimeDependency = dependency.dep_kinds.some(
|
|
({ kind }) => kind === null,
|
|
);
|
|
if (isRuntimeDependency) pending.push(dependency.pkg);
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
export function collectPnpmLicenses(report) {
|
|
return Object.entries(report).flatMap(([groupLicense, dependencies]) =>
|
|
dependencies
|
|
.filter(
|
|
(dependency) =>
|
|
!HOST_ONLY_PNPM_NATIVE_PREFIXES.some((prefix) =>
|
|
dependency.name.startsWith(prefix),
|
|
),
|
|
)
|
|
.map((dependency) => ({
|
|
name: dependency.name,
|
|
license: dependency.license ?? groupLicense,
|
|
})),
|
|
);
|
|
}
|
|
|
|
export function prepareLicenseInventory(entries) {
|
|
const unique = new Map();
|
|
|
|
for (const entry of entries) {
|
|
if (typeof entry.name !== "string" || entry.name.trim() === "") {
|
|
throw new Error("Every shipped dependency must have a name");
|
|
}
|
|
const name = entry.name.trim();
|
|
const license = normalizeLicenseExpression(entry.license);
|
|
unique.set(`${name}\0${license}`, { name, license });
|
|
}
|
|
|
|
return [...unique.values()].sort((left, right) => {
|
|
const leftName = left.name.toLowerCase();
|
|
const rightName = right.name.toLowerCase();
|
|
if (leftName < rightName) return -1;
|
|
if (leftName > rightName) return 1;
|
|
if (left.name < right.name) return -1;
|
|
if (left.name > right.name) return 1;
|
|
return left.license < right.license
|
|
? -1
|
|
: Number(left.license > right.license);
|
|
});
|
|
}
|
|
|
|
function commandOutput(command, args) {
|
|
// pnpm ships only a `pnpm.cmd` batch shim on Windows, and Node refuses to
|
|
// spawn batch files without a shell (CVE-2024-27980), so `execFileSync`
|
|
// fails with EINVAL there. Every argument below is a literal from this file,
|
|
// so routing that one call through cmd.exe interpolates nothing.
|
|
const needsShell = process.platform === "win32" && command === "pnpm";
|
|
return execFileSync(needsShell ? "pnpm.cmd" : command, args, {
|
|
cwd: PROJECT_ROOT,
|
|
encoding: "utf8",
|
|
maxBuffer: MAX_COMMAND_OUTPUT,
|
|
shell: needsShell,
|
|
windowsHide: true,
|
|
});
|
|
}
|
|
|
|
function generateInventory() {
|
|
const entries = [...MANUAL_LICENSES];
|
|
|
|
const pnpmReport = JSON.parse(
|
|
commandOutput("pnpm", [
|
|
"--filter",
|
|
"donutbrowser",
|
|
"licenses",
|
|
"list",
|
|
"--prod",
|
|
"--json",
|
|
]),
|
|
);
|
|
entries.push(...collectPnpmLicenses(pnpmReport));
|
|
|
|
for (const target of RELEASE_TARGETS) {
|
|
const metadata = JSON.parse(
|
|
commandOutput("cargo", [
|
|
"metadata",
|
|
"--locked",
|
|
"--format-version",
|
|
"1",
|
|
"--filter-platform",
|
|
target,
|
|
"--manifest-path",
|
|
"src-tauri/Cargo.toml",
|
|
]),
|
|
);
|
|
entries.push(...collectReachableRustLicenses(metadata));
|
|
}
|
|
|
|
return prepareLicenseInventory(entries);
|
|
}
|
|
|
|
function main() {
|
|
const outputs = [
|
|
{
|
|
path: OUTPUT_PATH,
|
|
contents: `${JSON.stringify(generateInventory(), null, 2)}\n`,
|
|
},
|
|
{
|
|
path: XRAY_SOURCE_OUTPUT_PATH,
|
|
contents: `${JSON.stringify({ sourceUrl: XRAY_SOURCE_URL }, null, 2)}\n`,
|
|
},
|
|
];
|
|
|
|
if (process.argv.includes("--check")) {
|
|
for (const output of outputs) {
|
|
const current = readFileSync(output.path, "utf8");
|
|
if (current !== output.contents) {
|
|
throw new Error(`${output.path} is stale; run pnpm licenses:generate`);
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
for (const output of outputs) {
|
|
mkdirSync(dirname(output.path), { recursive: true });
|
|
writeFileSync(output.path, output.contents);
|
|
}
|
|
}
|
|
|
|
const isDirectRun =
|
|
process.argv[1] &&
|
|
fileURLToPath(import.meta.url) === resolve(process.argv[1]);
|
|
if (isDirectRun) main();
|