mirror of
https://github.com/zadam/trilium.git
synced 2026-09-14 11:06:04 +05:00
The Update Nix flake workflow has been failing since the bump to Electron 44 landed on main, for two independent reasons. parseShasums treated every entry of ELECTRON_PLATFORM_TAGS as mandatory, and Electron 44 ships no linux-armv7l assets at all — 43.4.1 lists seven armv7l entries in SHASUMS256.txt, 44.0.0 lists none: Error: SHASUMS256.txt for Electron 44.0.0 is missing: electron-v44.0.0-linux-armv7l.zip Drop armv7l-linux from the table, and x86_64-darwin with it: the nixpkgs rev in flake.lock (6d12004) lists only x86_64-linux, armv7l-linux, aarch64-linux and aarch64-darwin in generic.nix's `tags`, so that hash could never have been read. Keep the hard throw on a missing asset — shrinking the platform set is a decision for a human, not something to paper over by silently pinning fewer systems. Refreshing the hashes then got the job as far as the verification step, which failed on its own: patchelf: missing filename The nixpkgs Linux builder rewrites the rpath of Electron's ANGLE libraries with an unguarded `patchelf ... lib*GL*`. Electron 43.4.1 shipped libEGL.so and libGLESv2.so; 44.0.0 links ANGLE into the main binary and ships neither, leaving only libffmpeg.so, libvk_swiftshader.so and libvulkan.so.1, so the glob expands to nothing. There is nothing upstream to pull — nixpkgs master packages Electron 41-43 only and still has the unguarded glob — so override that one command to tolerate an empty match, guarded by a throwIf that fails loudly if the upstream text changes rather than silently no-opping. Linux only, because the Darwin branch of the builder defines no postFixup. `nix build --no-link '.#electron' '.#electron.headers'` now succeeds and the result reports v44.0.0. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
199 lines
7.7 KiB
TypeScript
199 lines
7.7 KiB
TypeScript
/**
|
|
* Keeps the Electron binary pinned in `flake.nix` in sync with the version
|
|
* `apps/desktop/package.json` depends on.
|
|
*
|
|
* nixpkgs lags behind the Electron releases Trilium tracks, so the flake builds
|
|
* the exact pinned version from Electron's official binary release rather than
|
|
* taking `pkgs.electron_<major>` (see the comment above `pinnedElectronVersion`).
|
|
* Every Electron bump therefore has to be mirrored into the flake, or it refuses
|
|
* to evaluate:
|
|
*
|
|
* error: flake.nix pins Electron 43.2.0, but apps/desktop/package.json wants 43.3.0
|
|
*
|
|
* This script is that mirroring, automated: it reads the wanted version, takes
|
|
* the per-platform zip checksums from the release's own `SHASUMS256.txt`,
|
|
* computes the headers hash with `nix-prefetch-url`, and rewrites both
|
|
* `pinnedElectron*` bindings. It is a no-op when the pin already matches, so it
|
|
* is safe to run on a schedule.
|
|
*
|
|
* Run on demand or from the `update-nix-flake.yml` workflow, which opens a PR when
|
|
* the flake changes. Usage:
|
|
*
|
|
* pnpm chore:update-flake-electron
|
|
*
|
|
* Requires `nix-prefetch-url` on PATH — the headers hash is a NAR hash of the
|
|
* unpacked tarball, which only Nix can compute.
|
|
*/
|
|
|
|
import { execFileSync } from "node:child_process";
|
|
import { readFileSync, writeFileSync } from "node:fs";
|
|
import { dirname, join, resolve } from "node:path";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
const SCRIPT_PATH = fileURLToPath(import.meta.url);
|
|
const ROOT = resolve(dirname(SCRIPT_PATH), "..");
|
|
const FLAKE_PATH = join(ROOT, "flake.nix");
|
|
const DESKTOP_PACKAGE_JSON_PATH = join(ROOT, "apps", "desktop", "package.json");
|
|
|
|
export async function main() {
|
|
const flake = readFileSync(FLAKE_PATH, "utf-8");
|
|
const wanted = readWantedElectronVersion(readFileSync(DESKTOP_PACKAGE_JSON_PATH, "utf-8"));
|
|
const pinned = readPinnedElectronVersion(flake);
|
|
|
|
if (wanted === pinned) {
|
|
console.log(`flake.nix already pins Electron ${pinned}; nothing to do.`);
|
|
return;
|
|
}
|
|
|
|
console.log(`Electron ${pinned} -> ${wanted}; refreshing the pin.`);
|
|
const hashes = {
|
|
...parseShasums(await fetchShasums(wanted), wanted),
|
|
headers: prefetchHeadersHash(wanted)
|
|
};
|
|
|
|
writeFileSync(FLAKE_PATH, rewriteFlake(flake, wanted, hashes));
|
|
console.log(`Wrote ${FLAKE_PATH}`);
|
|
for (const [ key, hash ] of Object.entries(hashes)) {
|
|
console.log(` ${key} = ${hash}`);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Nix system → the tag Electron uses in its release asset names, in the order the
|
|
* generated `pinnedElectronHashes` block lists them. A system belongs here when the
|
|
* Electron release ships that zip and `tags` in nixpkgs'
|
|
* `pkgs/development/tools/electron/binary/generic.nix` — the builder the flake
|
|
* reuses — covers it.
|
|
*/
|
|
export const ELECTRON_PLATFORM_TAGS: Record<string, string> = {
|
|
"x86_64-linux": "linux-x64",
|
|
"aarch64-linux": "linux-arm64",
|
|
"aarch64-darwin": "darwin-arm64"
|
|
};
|
|
|
|
/** Per-platform zip checksums plus the `headers` NAR hash, keyed as the flake expects. */
|
|
export type ElectronHashes = Record<string, string> & { headers: string };
|
|
|
|
/**
|
|
* The flake pins one exact build, so a range (`^43.3.0`) has no single answer —
|
|
* refuse rather than guess at what the lockfile happens to resolve to today.
|
|
*/
|
|
export function readWantedElectronVersion(packageJson: string): string {
|
|
const version = JSON.parse(packageJson)?.devDependencies?.electron;
|
|
if (typeof version !== "string") {
|
|
throw new Error("apps/desktop/package.json has no devDependencies.electron entry.");
|
|
}
|
|
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
throw new Error(
|
|
`apps/desktop/package.json must pin an exact Electron version, found "${version}".`
|
|
);
|
|
}
|
|
return version;
|
|
}
|
|
|
|
export function readPinnedElectronVersion(flake: string): string {
|
|
const match = flake.match(/^\s*pinnedElectronVersion = "([^"]+)";$/m);
|
|
if (!match) {
|
|
throw new Error("Could not find the pinnedElectronVersion binding in flake.nix.");
|
|
}
|
|
return match[1];
|
|
}
|
|
|
|
async function fetchShasums(version: string): Promise<string> {
|
|
const url = `https://github.com/electron/electron/releases/download/v${version}/SHASUMS256.txt`;
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(
|
|
`Failed to fetch ${url}: HTTP ${response.status}. Is Electron ${version} released?`
|
|
);
|
|
}
|
|
return response.text();
|
|
}
|
|
|
|
/**
|
|
* Pull the per-platform zip checksums out of a release's `SHASUMS256.txt`, whose
|
|
* lines are `<sha256> *<asset name>`. The file also lists chromedriver, symbol and
|
|
* mas builds, so match asset names exactly rather than by substring.
|
|
*
|
|
* Throw on a missing asset rather than emitting a partial set: Electron dropping a
|
|
* platform has to shrink `ELECTRON_PLATFORM_TAGS` and the flake together, which is a
|
|
* decision for a human, not something to paper over by silently pinning fewer systems.
|
|
*/
|
|
export function parseShasums(shasums: string, version: string): Record<string, string> {
|
|
const byAsset = new Map<string, string>();
|
|
for (const line of shasums.split("\n")) {
|
|
const match = line.match(/^([0-9a-f]{64}) \*(.+)$/);
|
|
if (match) {
|
|
byAsset.set(match[2].trim(), match[1]);
|
|
}
|
|
}
|
|
|
|
const hashes: Record<string, string> = {};
|
|
const missing: string[] = [];
|
|
for (const [ system, tag ] of Object.entries(ELECTRON_PLATFORM_TAGS)) {
|
|
const asset = `electron-v${version}-${tag}.zip`;
|
|
const hash = byAsset.get(asset);
|
|
if (hash) {
|
|
hashes[system] = hash;
|
|
} else {
|
|
missing.push(asset);
|
|
}
|
|
}
|
|
if (missing.length > 0) {
|
|
throw new Error(`SHASUMS256.txt for Electron ${version} is missing: ${missing.join(", ")}`);
|
|
}
|
|
return hashes;
|
|
}
|
|
|
|
/**
|
|
* The headers are consumed with `fetchzip`, so the flake needs the NAR hash of the
|
|
* *unpacked* tarball rather than its sha256 — hence shelling out to Nix.
|
|
*/
|
|
function prefetchHeadersHash(version: string): string {
|
|
const tarball = `node-v${version}-headers.tar.gz`;
|
|
const url = `https://artifacts.electronjs.org/headers/dist/v${version}/${tarball}`;
|
|
try {
|
|
return execFileSync("nix-prefetch-url", [ "--unpack", url ], { encoding: "utf-8" }).trim();
|
|
} catch (err) {
|
|
if ((err as NodeJS.ErrnoException).code === "ENOENT") {
|
|
throw new Error(
|
|
"nix-prefetch-url is not on PATH; install Nix to refresh the headers hash."
|
|
);
|
|
}
|
|
throw err;
|
|
}
|
|
}
|
|
|
|
export function rewriteFlake(flake: string, version: string, hashes: ElectronHashes): string {
|
|
const versionPattern = /^([ \t]*)pinnedElectronVersion = "[^"]*";$/m;
|
|
const hashesPattern = /^([ \t]*)pinnedElectronHashes = \{[\s\S]*?^\1\};$/m;
|
|
if (!versionPattern.test(flake) || !hashesPattern.test(flake)) {
|
|
throw new Error(
|
|
"Could not find the pinnedElectronVersion/pinnedElectronHashes bindings in flake.nix."
|
|
);
|
|
}
|
|
|
|
return flake
|
|
.replace(
|
|
versionPattern,
|
|
(_match, indent: string) => `${indent}pinnedElectronVersion = "${version}";`
|
|
)
|
|
.replace(hashesPattern, (_match, indent: string) => renderHashesBlock(indent, hashes));
|
|
}
|
|
|
|
function renderHashesBlock(indent: string, hashes: ElectronHashes): string {
|
|
const inner = `${indent} `;
|
|
const lines = Object.keys(ELECTRON_PLATFORM_TAGS)
|
|
.map((system) => `${inner}${system} = "${hashes[system]}";`);
|
|
lines.push(`${inner}headers = "${hashes.headers}";`);
|
|
return `${indent}pinnedElectronHashes = {\n${lines.join("\n")}\n${indent}};`;
|
|
}
|
|
|
|
// Only when run as a script — the pure helpers above are imported by the spec.
|
|
if (process.argv[1] === SCRIPT_PATH) {
|
|
main().catch((err) => {
|
|
console.error(err);
|
|
process.exit(1);
|
|
});
|
|
}
|