desktop/updater-manifest.mjs
Rama main · réplica de lectura del repositorio original.
// Adds the update bundle `tauri build` just made to latest.json, the manifest
// installed apps poll for updates, and says what to publish where.
//
// The URLs point at the GitHub release for the version, which must exist
// with the bundles attached before the manifest lands on main.
import { execFileSync } from "node:child_process";
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
import { basename, dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { arch, platform } from "node:process";
const here = dirname(fileURLToPath(import.meta.url));
const manifestPath = join(here, "latest.json");
const metadata = JSON.parse(
execFileSync("cargo", ["metadata", "--format-version", "1", "--no-deps", "--manifest-path", join(here, "src-tauri/Cargo.toml")], { encoding: "utf8" }),
);
const version = metadata.packages.find((p) => p.name === "wispers-access-desktop").version;
const bundleDir = join(metadata.target_directory, "release", "bundle");
const config = JSON.parse(readFileSync(join(here, "src-tauri", "tauri.conf.json"), "utf8"));
// The base config names the staging app; release.mjs builds with the
// production overlay, whose product name the bundles carry.
const productName = JSON.parse(readFileSync(join(here, "src-tauri", "tauri.prod.conf.json"), "utf8")).productName;
/** The bundle in the bundle directory's `dir` whose name ends in `suffix`.
* `tauri build` leaves older builds' bundles in the same directory, and
* `0.1.1` and `1.0.0-rc1` sort before `1.0.0`, so a bundle's name must
* contain the version being released. The macOS tarball carries no version
* in its name, so for it the newest file wins. */
function find(dir, suffix) {
const path = join(bundleDir, dir);
if (!existsSync(path)) return undefined;
const candidates = readdirSync(path)
.filter((f) => f.endsWith(suffix) && (f === `${productName}${suffix}` || f.includes(`_${version}_`)))
.map((f) => join(path, f))
.sort((a, b) => statSync(b).mtimeMs - statSync(a).mtimeMs);
return candidates[0];
}
/** The update bundles and the platform keys the updater looks them up under.
* Linux has one per package format, since an installed app updates in its own
* format: the updater looks for `linux-<arch>-<format>` first. */
function hostBundles() {
const cpu = arch === "arm64" ? "aarch64" : "x86_64";
switch (platform) {
case "darwin":
return [{ key: `darwin-${cpu}`, bundle: find("macos", ".app.tar.gz") }];
case "win32":
return [{ key: `windows-${cpu}`, bundle: find("nsis", "-setup.exe") }];
case "linux":
return [
{ key: `linux-${cpu}-appimage`, bundle: find("appimage", ".AppImage") },
{ key: `linux-${cpu}-deb`, bundle: find("deb", ".deb") },
];
default:
return [];
}
}
const hosts = hostBundles();
if (hosts.length === 0) {
console.error(`no update bundles for ${platform}`);
process.exit(1);
}
for (const host of hosts) {
if (!host.bundle || !existsSync(`${host.bundle}.sig`)) {
console.error(`no signed ${host.key} update bundle under ${bundleDir}; build with the updater key set`);
process.exit(1);
}
// The bundler only warns when the private key isn't the public key's half,
// and installed apps would reject the update.
host.signature = readFileSync(`${host.bundle}.sig`, "utf8").trim();
if (keyId(host.signature) !== keyId(config.plugins.updater.pubkey)) {
console.error(`${host.bundle}.sig is not by the key in tauri.conf.json; build with the updater key`);
process.exit(1);
}
}
/** The key ID in a base64'd minisign key or signature file. */
function keyId(file) {
const line = Buffer.from(file, "base64").toString("utf8").split("\n")[1];
return Buffer.from(line, "base64").subarray(2, 10).toString("hex");
}
const release = `desktop-v${version}`;
// GitHub turns the spaces in asset names into dots.
const asset = (bundle) => basename(bundle).replaceAll(" ", ".");
const hostKeys = hosts.map((host) => host.key);
let manifest = existsSync(manifestPath) ? JSON.parse(readFileSync(manifestPath, "utf8")) : {};
if (manifest.version !== version) {
// Each platform's machine adds its own entry, so the other's must already
// be in this checkout when it was first.
console.warn(`latest.json is for ${manifest.version}, starting it over for ${version}. If another platform has released ${version} already, pull main and run this again.`);
manifest = { version, platforms: {} };
}
// The first platform's machine creates the release, the next ones add to it.
const releaseExists = Object.keys(manifest.platforms).some((key) => !hostKeys.includes(key));
manifest.pub_date = new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
for (const host of hosts) {
manifest.platforms[host.key] = {
url: `https://github.com/s-te-ch/wispers-access/releases/download/${release}/${asset(host.bundle)}`,
signature: host.signature,
};
}
writeFileSync(manifestPath, `${JSON.stringify(manifest, null, 2)}\n`);
const extra = platform === "darwin" ? readdirSync(join(bundleDir, "dmg")).filter((f) => f.endsWith(".dmg")).map((f) => join(bundleDir, "dmg", f)) : [];
const files = [...extra, ...hosts.flatMap((host) => [host.bundle, `${host.bundle}.sig`])].map((f) => `"${f}"`).join(" ");
const publish = releaseExists
? `gh release upload ${release} --clobber ${files}`
: `gh release create ${release} --title "Wispers Access desktop ${version}" --generate-notes ${files}`;
console.log(`
Wrote desktop/latest.json for ${version} (${Object.keys(manifest.platforms).join(", ")}). To publish:
${publish}
then commit desktop/latest.json to main.
`);