| 1 | #!/usr/bin/env node |
| 2 | // Lists a desktop artifact (bundle directory, .zip, .tar.gz or .deb) and fails |
| 3 | // unless every member the install layout relies on is present. Linux archives |
| 4 | // also fail when a directory or file would be unreadable to other users. |
| 5 | // |
| 6 | // usage: node desktop/packaging/verify.mjs <artifact> [--kind <kind>] [--list] |
| 7 | import { spawnSync } from "node:child_process"; |
| 8 | import { readdirSync, readFileSync } from "node:fs"; |
| 9 | import { join, resolve } from "node:path"; |
| 10 | import { ARTIFACT_KINDS, WINDOWS_PORTABLE_LAYOUTS, checkEntryModes, checkMembers, inferArtifactKind, isDirectory, listZipEntries, readZipMember, parseVerboseListing, validateMacServiceLink, walkFiles } from "./lib.mjs"; |
| 11 | |
| 12 | const args = process.argv.slice(2); |
| 13 | const artifactArg = args.find((arg) => !arg.startsWith("--")); |
| 14 | const kindIndex = args.indexOf("--kind"); |
| 15 | const kindArg = kindIndex >= 0 ? args[kindIndex + 1] : undefined; |
| 16 | const layoutIndex = args.indexOf("--portable-layout"); |
| 17 | const portableLayout = layoutIndex >= 0 ? args[layoutIndex + 1] : "canonical"; |
| 18 | if (!artifactArg || (kindArg && !ARTIFACT_KINDS.includes(kindArg)) || !WINDOWS_PORTABLE_LAYOUTS.includes(portableLayout)) { |
| 19 | console.error(`usage: verify.mjs <artifact> [--kind <${ARTIFACT_KINDS.join("|")}>] [--portable-layout canonical|legacy-dual] [--list]`); |
| 20 | process.exit(2); |
| 21 | } |
| 22 | const artifact = resolve(artifactArg); |
| 23 | |
| 24 | function tool(command, toolArgs) { |
| 25 | const result = spawnSync(command, toolArgs, { encoding: "utf8", maxBuffer: 64 * 1024 * 1024 }); |
| 26 | if (result.status !== 0) throw new Error(`${command} ${toolArgs.join(" ")} failed: ${result.stderr || result.error?.message || result.status}`); |
| 27 | return result.stdout.split(/\r?\n/).filter((line) => line !== ""); |
| 28 | } |
| 29 | |
| 30 | function listingOf(path) { |
| 31 | if (isDirectory(path)) return { entries: walkFiles(path), rows: [] }; |
| 32 | if (path.endsWith(".zip")) return { entries: listZipEntries(path), rows: [] }; |
| 33 | let rows; |
| 34 | if (path.endsWith(".tar.gz")) rows = parseVerboseListing(tool("tar", ["-tvzf", path])); |
| 35 | else if (path.endsWith(".deb")) rows = parseVerboseListing(tool("dpkg-deb", ["-c", path])); |
| 36 | else throw new Error(`unsupported artifact ${path}`); |
| 37 | return { entries: rows.map((row) => row.name), rows }; |
| 38 | } |
| 39 | |
| 40 | const directory = isDirectory(artifact); |
| 41 | const kind = kindArg ?? inferArtifactKind(artifact, directory, directory ? readdirSync(artifact) : []); |
| 42 | const { entries, rows } = listingOf(artifact); |
| 43 | if (args.includes("--list")) for (const entry of entries) console.log(entry); |
| 44 | const { missing, forbidden } = checkMembers(entries, kind, portableLayout); |
| 45 | const layoutErrors = kind === "darwin-app-dir" ? validateMacServiceLink(artifact) : []; |
| 46 | if (kind === "windows-portable-zip" && missing.length === 0 && forbidden.length === 0) { |
| 47 | const readMember = name => directory ? readFileSync(join(artifact, name)) : readZipMember(artifact, name); |
| 48 | const gui = readMember("Reasonix.exe"); |
| 49 | if (gui.equals(readMember("reasonix-cli.exe"))) layoutErrors.push("GUI entry contains CLI bytes"); |
| 50 | if (portableLayout === "legacy-dual" && !gui.equals(readMember("reasonix-launcher.exe"))) { |
| 51 | layoutErrors.push("legacy GUI entry differs from Reasonix.exe"); |
| 52 | } |
| 53 | } |
| 54 | const modeErrors = checkEntryModes(rows, kind); |
| 55 | for (const name of missing) console.error(`verify: ${kind} is missing ${name}`); |
| 56 | for (const name of forbidden) console.error(`verify: ${kind} must not contain ${name}`); |
| 57 | for (const error of layoutErrors) console.error(`verify: ${kind} ${error}`); |
| 58 | for (const error of modeErrors) console.error(`verify: ${kind} ${error}`); |
| 59 | if (missing.length > 0 || forbidden.length > 0 || layoutErrors.length > 0 || modeErrors.length > 0) process.exit(1); |
| 60 | console.log(`verify: ${kind} ok (${entries.length} entries in ${artifact})`); |
| 61 |