返回 DeepSeek-Reasonix
check-windows-uninstaller.mjs
根目录 / scripts / check-windows-uninstaller.mjs
1 #!/usr/bin/env node
2 import assert from "node:assert/strict";
3 import { copyFileSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
4 import { tmpdir } from "node:os";
5 import { dirname, join } from "node:path";
6 import { fileURLToPath } from "node:url";
7 import { spawnSync } from "node:child_process";
8 import { nsisProjectDefines } from "../desktop/packaging/lib.mjs";
9
10 const root = dirname(dirname(fileURLToPath(import.meta.url)));
11 const temp = mkdtempSync(join(tmpdir(), "reasonix nsis contract "));
12 try {
13 const installer = join(temp, "windows", "installer");
14 mkdirSync(join(temp, "bin"), { recursive: true });
15 mkdirSync(installer, { recursive: true });
16 copyFileSync(join(root, "desktop/build/windows/installer/project.nsi"), join(installer, "project.nsi"));
17 copyFileSync(join(root, "desktop/build/windows/icon.ico"), join(temp, "windows", "icon.ico"));
18 writeFileSync(join(installer, "reasonix_project.nsh"), nsisProjectDefines({
19 projectName: "reasonix-desktop", companyName: "Reasonix", productName: "Reasonix", copyright: "Reasonix",
20 }, "v0.0.0-ci"));
21 if (process.platform !== "win32") {
22 writeFileSync(join(installer, "reasonix_host.nsh"), `!define REASONIX_UNINST_FINALIZE '/bin/cp -f "%1" "reasonix-uninstall.exe"'\n`);
23 }
24 for (const arch of ["AMD64", "ARM64"]) {
25 mkdirSync(join(installer, "app"), { recursive: true });
26 for (const name of ["reasonix-desktop.exe", "reasonix-launcher.exe", "reasonix-guard.exe", "reasonix-update-helper.exe", "reasonix-cli.exe", "app/Reasonix.exe"]) {
27 writeFileSync(join(installer, name), Buffer.alloc(1024, 42));
28 }
29 const output = join(installer, "reasonix-uninstall.exe");
30 function compile(only, success = true) {
31 rmSync(output, { force: true });
32 const args = [`-DARG_REASONIX_${arch}_BINARY=reasonix-desktop.exe`];
33 if (only) args.push("-DARG_REASONIX_UNINSTALLER_ONLY");
34 const result = spawnSync("makensis", [...args, "project.nsi"], { cwd: installer, encoding: "utf8" });
35 assert.ifError(result.error);
36 if (!success) {
37 assert.notEqual(result.status, 0, "normal installer must require its payload");
38 return;
39 }
40 assert.equal(result.status, 0, result.stdout + result.stderr);
41 const bytes = readFileSync(output);
42 assert.equal(bytes.subarray(0, 2).toString(), "MZ");
43 const preprocessed = spawnSync("makensis", ["-PPO", ...args, "project.nsi"], { cwd: installer, encoding: "utf8" });
44 assert.equal(preprocessed.status, 0, preprocessed.stderr);
45 const start = preprocessed.stdout.search(/^Section\s+"?uninstall"?\s*$/m);
46 assert.notEqual(start, -1, "preprocessed uninstall section is missing");
47 // LogicLib allocates private jump labels across the entire installer.
48 // Renumber by first occurrence while preserving every jump relationship.
49 const labels = new Map();
50 const instructions = preprocessed.stdout.slice(start).replace(/_LogicLib_[A-Za-z]+_[0-9]+/g, label => {
51 if (!labels.has(label)) labels.set(label, label.replace(/[0-9]+$/, String(labels.size)));
52 return labels.get(label);
53 });
54 return { bytes, instructions };
55 }
56 const original = compile(false);
57 const bootstrap = compile(true);
58 assert.equal(bootstrap.instructions, original.instructions, `${arch}: uninstall instructions changed`);
59 rmSync(join(installer, "app"), { recursive: true });
60 rmSync(join(installer, "reasonix-desktop.exe"));
61 assert.deepEqual(compile(true), bootstrap, `${arch}: uninstaller depends on install payload`);
62 compile(false, false);
63 console.log(`${arch}: shared uninstall instructions preserved; bootstrap needs no Electron payload`);
64 }
65 } finally {
66 rmSync(temp, { recursive: true, force: true });
67 }
68
68 lines Plain Text