build(release): 加固 v0.1.0 桌面发布链路

This commit is contained in:
Cheng Zhou
2026-07-17 09:23:23 +08:00
parent 1837ceff58
commit 9fd80cab50
16 changed files with 895 additions and 323 deletions
+81 -44
View File
@@ -1,6 +1,6 @@
import { createHash } from "node:crypto";
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
import { basename, resolve } from "node:path";
import { createHash } from "node:crypto";
import { fileURLToPath } from "node:url";
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
@@ -25,7 +25,7 @@ const assert = (condition, message) => {
if (!condition) fail(message);
};
const artifactPath = value("--artifact") || process.env.DESKTOP_UPDATE_ARTIFACT;
const macosArtifactPath = value("--artifact") || process.env.DESKTOP_UPDATE_ARTIFACT;
const outputDir = resolve(
frontendDir,
value("--output-dir") || process.env.DESKTOP_UPDATE_OUTPUT_DIR || "src-tauri/target/release/desktop-update-feed",
@@ -34,18 +34,40 @@ const baseUrlRaw = value("--base-url") || process.env.DESKTOP_UPDATE_BASE_URL;
const pubDate = value("--date") || process.env.DESKTOP_UPDATE_PUB_DATE || new Date().toISOString();
const notes = value("--notes") || process.env.DESKTOP_UPDATE_NOTES;
const includes = values("--include").map((path) => resolve(frontendDir, path));
const platformArtifactSpecs = values("--platform-artifact");
const platformPattern = /^(?:darwin|windows|linux)-(?:x86_64|aarch64|i686|armv7)$/;
assert(Boolean(artifactPath), "--artifact or DESKTOP_UPDATE_ARTIFACT is required.");
assert(Boolean(macosArtifactPath), "--artifact or DESKTOP_UPDATE_ARTIFACT is required for the Universal macOS updater artifact.");
assert(Boolean(baseUrlRaw), "--base-url or DESKTOP_UPDATE_BASE_URL is required.");
const resolvedArtifactPath = artifactPath ? resolve(frontendDir, artifactPath) : undefined;
const artifactName = resolvedArtifactPath ? basename(resolvedArtifactPath) : undefined;
const signaturePath = resolvedArtifactPath ? `${resolvedArtifactPath}.sig` : undefined;
const uploadFiles = [];
const platformArtifacts = new Map();
if (macosArtifactPath) {
const resolvedMacosArtifact = resolve(frontendDir, macosArtifactPath);
platformArtifacts.set("darwin-aarch64", resolvedMacosArtifact);
platformArtifacts.set("darwin-x86_64", resolvedMacosArtifact);
}
for (const spec of platformArtifactSpecs) {
const separator = spec.indexOf("=");
if (separator <= 0 || separator === spec.length - 1) {
fail(`--platform-artifact must use <os>-<arch>=<path>: ${spec}`);
continue;
}
const platform = spec.slice(0, separator);
const path = spec.slice(separator + 1);
assert(platformPattern.test(platform), `Unsupported updater platform key: ${platform}`);
assert(!platformArtifacts.has(platform), `Updater platform is configured more than once: ${platform}`);
if (platformPattern.test(platform) && !platformArtifacts.has(platform)) {
platformArtifacts.set(platform, resolve(frontendDir, path));
}
}
const readRequiredFile = async (path, description) => {
try {
return await readFile(path);
const data = await readFile(path);
assert(data.length > 0, `${description} must not be empty: ${path}`);
return data;
} catch (error) {
fail(`${description} cannot be read: ${path} (${error.message})`);
return undefined;
@@ -72,20 +94,16 @@ const normalizedBaseUrl = () => {
}
};
const copyIntoOutput = async (sourcePath) => {
const destination = resolve(outputDir, basename(sourcePath));
if (sourcePath !== destination) {
await copyFile(sourcePath, destination);
}
return destination;
};
if (resolvedArtifactPath && signaturePath) {
await readRequiredFile(resolvedArtifactPath, "Updater artifact");
const artifactSignatures = new Map();
const uniqueArtifactPaths = [...new Set(platformArtifacts.values())];
for (const artifactPath of uniqueArtifactPaths) {
const signaturePath = `${artifactPath}.sig`;
await readRequiredFile(artifactPath, "Updater artifact");
const signature = await readRequiredFile(signaturePath, "Updater artifact signature");
if (signature) {
const signatureText = signature.toString("utf8").trim();
assert(signatureText.length > 80, "Updater artifact signature is unexpectedly short.");
assert(signatureText.length > 80, `Updater artifact signature is unexpectedly short: ${signaturePath}`);
artifactSignatures.set(artifactPath, signatureText);
}
}
@@ -98,56 +116,75 @@ for (const includePath of includes) {
}
}
for (const [platform, artifactPath] of platformArtifacts) {
if (platform.startsWith("darwin-")) {
assert(artifactPath.endsWith(".app.tar.gz"), `${platform} must use a macOS .app.tar.gz updater artifact.`);
}
if (platform === "windows-x86_64") {
assert(artifactPath.endsWith(".nsis.zip"), "windows-x86_64 must use an NSIS .nsis.zip updater artifact.");
}
}
const uploadSources = [
...uniqueArtifactPaths.flatMap((path) => [path, `${path}.sig`]),
...includes,
];
const sourceByName = new Map();
for (const sourcePath of uploadSources) {
const name = basename(sourcePath);
const existing = sourceByName.get(name);
assert(!existing || existing === sourcePath, `Release files must have unique basenames: ${name}`);
sourceByName.set(name, sourcePath);
}
const baseUrl = normalizedBaseUrl();
if (failures.length === 0 && resolvedArtifactPath && signaturePath && artifactName && baseUrl) {
if (failures.length === 0 && baseUrl) {
await mkdir(outputDir, { recursive: true });
const artifactUrl = new URL(artifactName, baseUrl).toString();
assert(!artifactUrl.endsWith("/latest.json"), "Updater artifact URL must not point at latest.json.");
assert(!/[?&]token=/i.test(new URL(artifactUrl).search), "Updater artifact URL must not include token query parameters.");
const platforms = {};
for (const [platform, artifactPath] of platformArtifacts) {
const artifactUrl = new URL(basename(artifactPath), baseUrl).toString();
assert(!artifactUrl.endsWith("/latest.json"), "Updater artifact URL must not point at latest.json.");
assert(!/[?&]token=/i.test(new URL(artifactUrl).search), "Updater artifact URL must not include token query parameters.");
platforms[platform] = {
signature: artifactSignatures.get(artifactPath),
url: artifactUrl,
};
}
const signature = (await readFile(signaturePath, "utf8")).trim();
const latest = {
version: packageInfo.version,
pub_date: pubDate,
platforms: {
"darwin-aarch64": {
signature,
url: artifactUrl,
},
"darwin-x86_64": {
signature,
url: artifactUrl,
},
},
platforms,
};
if (notes) {
latest.notes = notes;
}
uploadFiles.push(await copyIntoOutput(resolvedArtifactPath));
uploadFiles.push(await copyIntoOutput(signaturePath));
for (const includePath of includes) {
uploadFiles.push(await copyIntoOutput(includePath));
const copiedFiles = [];
for (const sourcePath of sourceByName.values()) {
const destination = resolve(outputDir, basename(sourcePath));
if (sourcePath !== destination) {
await copyFile(sourcePath, destination);
}
copiedFiles.push(destination);
}
const latestPath = resolve(outputDir, "latest.json");
await writeFile(latestPath, `${JSON.stringify(latest, null, 2)}\n`);
uploadFiles.push(latestPath);
copiedFiles.push(latestPath);
const uniqueFiles = [...new Map(uploadFiles.map((path) => [basename(path), path])).values()];
const checksumLines = [];
for (const filePath of uniqueFiles) {
for (const filePath of copiedFiles) {
checksumLines.push(`${await sha256(filePath)} ${basename(filePath)}`);
}
const checksumPath = resolve(outputDir, "SHA256SUMS.txt");
await writeFile(checksumPath, `${checksumLines.join("\n")}\n`);
console.log(`Desktop update feed created in ${outputDir}`);
console.log(` - ${uniqueFiles.map((path) => basename(path)).join("\n - ")}`);
console.log(` - SHA256SUMS.txt`);
console.log(` - ${copiedFiles.map((path) => basename(path)).join("\n - ")}`);
console.log(" - SHA256SUMS.txt");
}
if (failures.length > 0) {