返回 DeepSeek-Reasonix
package.mjs
根目录 / desktop / packaging / package.mjs
1 #!/usr/bin/env node
2 // Builds the UI for Electron, builds the shell and packages both with
3 // @electron/packager into desktop/build/electron/<os>-<arch>/. The Go binaries
4 // are added afterwards by scripts/desktop-build.sh, which owns signing and the
5 // per-platform artifacts.
6 //
7 // usage: node desktop/packaging/package.mjs <os/arch> <version> [channel]
8 import { defaultSanitizePackageJson, packager } from "@electron/packager";
9 import { execFileSync } from "node:child_process";
10 import { createHash } from "node:crypto";
11 import { chmodSync, cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
12 import { tmpdir } from "node:os";
13 import { basename, dirname, join } from "node:path";
14 import { fileURLToPath } from "node:url";
15 import {
16 buildInfo,
17 nsisProjectDefines,
18 packagerOptions,
19 parseTarget,
20 PRODUCT,
21 readProductIdentity,
22 runBuildScript,
23 sanitizeShellPackageJson,
24 signingFileList,
25 versionTag,
26 walkFiles
27 } from "./lib.mjs";
28 import { verifyFrontendArtifact } from "../frontend/scripts/artifact-identity.mjs";
29
30 const desktop = dirname(dirname(fileURLToPath(import.meta.url)));
31 const repo = dirname(desktop);
32 const [spec, version, channel = "stable"] = process.argv.slice(2);
33 if (!spec || !version) {
34 console.error("usage: package.mjs <os/arch> <version> [channel]");
35 process.exit(2);
36 }
37 const target = parseTarget(spec);
38 versionTag(version);
39 const identity = readProductIdentity();
40 const electronVersion = JSON.parse(readFileSync(join(desktop, "electron", "node_modules", "electron", "package.json"), "utf8")).version;
41 const commit = (process.env.REASONIX_COMMIT ?? "").trim() || gitCommit();
42 const buildTime = (process.env.REASONIX_BUILD_TIME ?? "").trim() || new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
43
44 function gitCommit() {
45 try {
46 return execFileSync("git", ["-C", repo, "rev-parse", "HEAD"], { encoding: "utf8", }).trim();
47 } catch {
48 return "unknown";
49 }
50 }
51
52 function sha256File(path) {
53 return createHash("sha256").update(readFileSync(path)).digest("hex");
54 }
55
56 function require(path, what) {
57 if (!existsSync(path)) throw new Error(`${what} is missing: ${path}`);
58 }
59
60 const frontendDist = join(desktop, "frontend", "dist");
61 if (process.env.REASONIX_PACKAGE_REUSE_FRONTEND === "1") {
62 const pnpmVersion = (process.env.REASONIX_FRONTEND_PNPM_VERSION ?? "").trim();
63 if (!pnpmVersion) throw new Error("REASONIX_FRONTEND_PNPM_VERSION is required when reusing a frontend artifact");
64 verifyFrontendArtifact({
65 root: repo,
66 dist: frontendDist,
67 manifest: process.env.REASONIX_FRONTEND_ARTIFACT_MANIFEST || join(desktop, "frontend", ".reasonix-frontend-artifact.json"),
68 shell: "electron",
69 channel,
70 sourceSHA: process.env.GITHUB_SHA || undefined,
71 runId: process.env.GITHUB_RUN_ID || undefined,
72 attempt: process.env.GITHUB_RUN_ATTEMPT || undefined,
73 pnpmVersion,
74 });
75 console.log(`==> reusing ${frontendDist}`);
76 } else {
77 console.log(`==> frontend build:electron (channel ${channel})`);
78 runBuildScript(join(desktop, "frontend"), "build-for-shell.mjs", ["electron"], { REASONIX_CHANNEL: channel, REASONIX_COMMIT: commit });
79 }
80 require(join(frontendDist, "index.html"), "frontend dist");
81
82 console.log("==> shell build");
83 runBuildScript(join(desktop, "electron"), "build.mjs");
84 const shellDist = join(desktop, "electron", "dist");
85 for (const name of ["main.cjs", "preload.cjs"]) require(join(shellDist, name), "shell bundle");
86 if (!existsSync(join(shellDist, "desktopContract.json")) && process.env.REASONIX_ELECTRON_ALLOW_MISSING_CONTRACT !== "1") {
87 throw new Error(`desktop contract is missing from ${shellDist}; run: cd desktop && go run . -emit-contract frontend/src/generated`);
88 }
89
90 const sourceMapDir = join(desktop, "build", "sourcemaps", target.key);
91 rmSync(sourceMapDir, { recursive: true, force: true });
92 mkdirSync(sourceMapDir, { recursive: true });
93 for (const name of readdirSync(shellDist).filter((name) => name.endsWith(".map"))) {
94 cpSync(join(shellDist, name), join(sourceMapDir, name));
95 }
96 const frontendMapArchive = join(desktop, "frontend", "sourcemaps", commit);
97 require(join(frontendMapArchive, "manifest.json"), "frontend source map archive manifest");
98 const frontendMapManifest = JSON.parse(readFileSync(join(frontendMapArchive, "manifest.json"), "utf8"));
99 if (frontendMapManifest.commit !== commit) throw new Error(`frontend source map commit ${frontendMapManifest.commit} does not match ${commit}`);
100 if (!Array.isArray(frontendMapManifest.maps) || frontendMapManifest.maps.length === 0) throw new Error("frontend source map archive manifest has no maps");
101 for (const record of frontendMapManifest.maps) {
102 if (!record || typeof record.archive !== "string" || typeof record.map !== "string" || typeof record.bundle !== "string") {
103 throw new Error("frontend source map archive manifest has an invalid map record");
104 }
105 if (
106 !record.map.endsWith(".map") ||
107 record.map.startsWith("/") ||
108 record.map.includes("\\") ||
109 record.map.split("/").includes("..") ||
110 record.archive.includes("/") ||
111 record.archive.includes("\\") ||
112 record.archive.includes("..") ||
113 record.bundle !== record.map.slice(0, -4)
114 ) {
115 throw new Error(`frontend source map archive manifest has an unsafe map record: ${JSON.stringify(record)}`);
116 }
117 const archivedMap = join(frontendMapArchive, record.archive);
118 require(archivedMap, `archived frontend source map ${record.map}`);
119 const destination = join(sourceMapDir, "frontend", record.map);
120 mkdirSync(dirname(destination), { recursive: true });
121 cpSync(archivedMap, destination);
122 }
123
124 const sourceMapEntries = [];
125 for (const mapPath of walkFiles(sourceMapDir)
126 .filter((name) => name.endsWith(".map"))
127 .sort()) {
128 const shellMap = !mapPath.startsWith("frontend/");
129 const bundleRelative = shellMap ? mapPath.slice(0, -4) : mapPath.slice("frontend/".length, -4);
130 if (!bundleRelative || bundleRelative.includes("..")) throw new Error(`source map has an invalid bundle path: ${mapPath}`);
131 const mapJSON = JSON.parse(readFileSync(join(sourceMapDir, mapPath), "utf8"));
132 if (mapJSON.file && String(mapJSON.file) !== basename(bundleRelative)) throw new Error(`source map bundle identity does not match ${mapPath}`);
133 const bundlePath = shellMap ? join(shellDist, bundleRelative) : join(frontendDist, bundleRelative);
134 require(bundlePath, `bundle for source map ${mapPath}`);
135 sourceMapEntries.push({
136 bundle: shellMap ? `electron/${bundleRelative}` : bundleRelative,
137 bundleHash: `sha256:${sha256File(bundlePath)}`,
138 map: mapPath,
139 mapHash: `sha256:${sha256File(join(sourceMapDir, mapPath))}`,
140 });
141 }
142 for (const requiredMap of ["main.cjs.map", "preload.cjs.map"]) {
143 if (!sourceMapEntries.some((entry) => entry.map === requiredMap)) throw new Error(`required Electron source map is missing: ${requiredMap}`);
144 }
145 if (!sourceMapEntries.some((entry) => entry.map.startsWith("frontend/"))) throw new Error("frontend source maps are missing");
146 writeFileSync(
147 join(sourceMapDir, "manifest.json"),
148 JSON.stringify(
149 {
150 schemaVersion: 1,
151 commit: (process.env.GITHUB_SHA ?? "").trim() || gitCommit(),
152 target: target.spec,
153 channel,
154 createdAt: buildTime,
155 entries: sourceMapEntries,
156 },
157 null,
158 2,
159 ) + "\n",
160 );
161
162 const staging = mkdtempSync(join(tmpdir(), "reasonix-package-"));
163 const outDir = join(desktop, "build", "electron", target.key);
164 try {
165 cpSync(frontendDist, join(staging, "app"), {
166 recursive: true,
167 filter: (source) => !source.endsWith(".map"),
168 });
169 mkdirSync(join(staging, "icons"), { recursive: true });
170 cpSync(join(desktop, "build", "appicon.png"), join(staging, "icons", "appicon.png"));
171 // Packaged launches always read this identity, including the full version
172 // tag. Environment overrides belong only to the unpackaged development shell.
173 writeFileSync(join(staging, "build.json"), JSON.stringify(buildInfo({ version, channel, commit, electronVersion, target, buildTime, }), null, 2,) + "\n",);
174
175 const icon = { darwin: join(desktop, "build", "darwin", "icon.icns"), win32: join(desktop, "build", "windows", "icon.ico"), }[target.packagerPlatform];
176 if (icon) require(icon, "application icon");
177 const options = packagerOptions({
178 target,
179 version,
180 identity,
181 root: desktop,
182 electronVersion,
183 extraResources: [join(staging, "app"), join(staging, "icons"), join(staging, "build.json")],
184 icon,
185 });
186 options.sanitizePackageJson = [defaultSanitizePackageJson, (pkg) => sanitizeShellPackageJson(pkg, { version, productName: PRODUCT.name })];
187 rmSync(options.out, { recursive: true, force: true });
188 rmSync(outDir, { recursive: true, force: true });
189
190 console.log(`==> packaging ${PRODUCT.name} ${version} for ${target.spec} with Electron ${electronVersion}`);
191 const [finalPath] = await packager(options);
192 mkdirSync(outDir, { recursive: true });
193 const bundle = target.os === "darwin" ? join(outDir, `${PRODUCT.name}.app`) : join(outDir, "app");
194 renameSync(target.os === "darwin" ? join(finalPath, `${PRODUCT.name}.app`) : finalPath, bundle);
195 // The packager stages the app tree in a mkdtemp directory (0700) and renames
196 // it into place; dpkg installs that mode as root:root, hiding app/ from users.
197 if (target.os !== "darwin") chmodSync(bundle, 0o755);
198 rmSync(options.out, { recursive: true, force: true });
199
200 if (target.os === "windows") {
201 const installer = join(desktop, "build", "windows", "installer");
202 mkdirSync(installer, { recursive: true });
203 writeFileSync(join(installer, "reasonix_project.nsh"), nsisProjectDefines(identity, version));
204 const signing = signingFileList(walkFiles(bundle).map((name) => `app/${name}`));
205 writeFileSync(join(outDir, "signing-files.txt"), signing.join("\n") + "\n");
206 console.log(`==> ${signing.length} Electron PE files need Authenticode (${join(outDir, "signing-files.txt")})`);
207 }
208 writeFileSync(join(outDir, "summary.json"), JSON.stringify({ target: target.spec, version, channel, commit, electronVersion, bundle, }, null, 2,) + "\n",);
209 console.log(`==> packaged ${bundle}`);
210 } finally {
211 rmSync(staging, { recursive: true, force: true });
212 }
213
213 lines Plain Text