194 lines
7.3 KiB
JavaScript
194 lines
7.3 KiB
JavaScript
import { createHash } from "node:crypto";
|
|
import { copyFile, mkdir, readFile, stat, writeFile } from "node:fs/promises";
|
|
import { basename, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const frontendDir = fileURLToPath(new URL("../", import.meta.url));
|
|
const packageInfo = JSON.parse(await readFile(resolve(frontendDir, "package.json"), "utf8"));
|
|
const failures = [];
|
|
|
|
const args = process.argv.slice(2);
|
|
const values = (name) => {
|
|
const found = [];
|
|
for (let index = 0; index < args.length; index += 1) {
|
|
if (args[index] === name && args[index + 1]) {
|
|
found.push(args[index + 1]);
|
|
index += 1;
|
|
}
|
|
}
|
|
return found;
|
|
};
|
|
const value = (name) => values(name)[0];
|
|
|
|
const fail = (message) => failures.push(message);
|
|
const assert = (condition, message) => {
|
|
if (!condition) fail(message);
|
|
};
|
|
|
|
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",
|
|
);
|
|
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(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 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 {
|
|
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;
|
|
}
|
|
};
|
|
|
|
const sha256 = async (path) => {
|
|
const data = await readFile(path);
|
|
return createHash("sha256").update(data).digest("hex");
|
|
};
|
|
|
|
const normalizedBaseUrl = () => {
|
|
if (!baseUrlRaw) return undefined;
|
|
try {
|
|
const url = new URL(baseUrlRaw.endsWith("/") ? baseUrlRaw : `${baseUrlRaw}/`);
|
|
assert(url.protocol === "https:", "Desktop update artifact base URL must use HTTPS.");
|
|
assert(url.username === "" && url.password === "", "Desktop update artifact base URL must not include credentials.");
|
|
assert(!/[?&]token=/i.test(url.search), "Desktop update artifact base URL must not include token query parameters.");
|
|
assert(url.pathname.includes(packageInfo.version), `Desktop update artifact base URL must include version ${packageInfo.version}.`);
|
|
return url;
|
|
} catch (error) {
|
|
fail(`Desktop update artifact base URL is invalid: ${baseUrlRaw} (${error.message})`);
|
|
return undefined;
|
|
}
|
|
};
|
|
|
|
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: ${signaturePath}`);
|
|
artifactSignatures.set(artifactPath, signatureText);
|
|
}
|
|
}
|
|
|
|
for (const includePath of includes) {
|
|
try {
|
|
const metadata = await stat(includePath);
|
|
assert(metadata.isFile(), `Included release file must be a file: ${includePath}`);
|
|
} catch (error) {
|
|
fail(`Included release file cannot be read: ${includePath} (${error.message})`);
|
|
}
|
|
}
|
|
|
|
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 && baseUrl) {
|
|
await mkdir(outputDir, { recursive: true });
|
|
|
|
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 latest = {
|
|
version: packageInfo.version,
|
|
pub_date: pubDate,
|
|
platforms,
|
|
};
|
|
if (notes) {
|
|
latest.notes = notes;
|
|
}
|
|
|
|
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`);
|
|
copiedFiles.push(latestPath);
|
|
|
|
const checksumLines = [];
|
|
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(` - ${copiedFiles.map((path) => basename(path)).join("\n - ")}`);
|
|
console.log(" - SHA256SUMS.txt");
|
|
}
|
|
|
|
if (failures.length > 0) {
|
|
console.error(`Desktop update feed creation failed:\n${failures.map((item) => ` - ${item}`).join("\n")}`);
|
|
process.exitCode = 1;
|
|
}
|