mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-17 12:42:21 +03:00
The desktop release matrix ran the full Next.js standalone build on all four legs (windows, macos-intel, macos-arm64, linux), duplicating the platform-neutral majority of that work four times and re-exposing every leg to the hosted-runner RAM class of failure that took the linux leg out of v3.8.49. - scripts/build/standaloneTarball.mjs: deterministic, dependency-free tar.gz writer/reader (uid/gid/mtime pinned, sorted entries, symlink + exec-bit preservation; GNU-tar interop covered by tests). - scripts/build/standaloneManifest.mjs: byte-level manifest of .build/next (sha256 + size + symlink target per entry, plus the archive's own digest) catching artifact-transfer corruption before extraction and re-verifying the restored tree byte-for-byte, smuggling included. - scripts/build/standaloneBundle.mjs: pack / restore / hydrate CLI over the two modules above. - scripts/build/hydrateNativeDeps.mjs: swaps install-machine-forked native optionals (@img/sharp-*, @ngrok/ngrok-*, fsevents) from the leg's own npm ci into the restored tree, then verifies the bundled-native closure (koffi triplets, better-sqlite3 prebuilds, wreq-js, onnxruntime with its documented darwin-x64 exemption) services the leg's platform/arch before packaging starts. - .github/workflows/electron-release.yml: new web-build job builds the standalone once on ubuntu with webpack and uploads the bundle; legs download, restore, and hydrate it, skipping the per-leg build. The legacy per-leg build remains as a rollback path via the ELECTRON_SHARED_STANDALONE workflow_dispatch input, and legs fail closed if web-build ran and failed. Regression tests cover archive roundtrip, byte determinism, manifest tamper/smuggle detection, forked-native swaps, and native-closure serviceability.
138 lines
5.1 KiB
JavaScript
138 lines
5.1 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Platform hydration for the shared Next standalone web build (issue #10321,
|
|
* Stage 8).
|
|
*
|
|
* The standalone bundle is built ONCE on ubuntu and restored on every desktop
|
|
* matrix leg. Everything except install-machine-forked optional packages is
|
|
* platform-independent:
|
|
*
|
|
* - Bundled-for-all (verify only): koffi ships every triplet under
|
|
* `build/koffi/<os>_<arch>`, better-sqlite3 v13 ships Node-API prebuilds for
|
|
* 8 platforms, wreq-js ships `rust/wreq-js.<plat>-<arch>[-libc].node`, and
|
|
* onnxruntime-node ships `bin/napi-v6/<os>/<arch>`.
|
|
* - Install-machine-forked (hydrate): `@img/sharp-*`, `@img/sharp-libvips-*`,
|
|
* `@ngrok/ngrok-*` and macOS-only `fsevents` resolve to whichever platform
|
|
* ran `npm ci`. The ubuntu-built tree carries the linux forks; each leg
|
|
* replaces them with the forks from its OWN `npm ci`d node_modules.
|
|
*/
|
|
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
|
|
/** Scope prefixes whose members are install-machine-forked. */
|
|
export const HYDRATED_SCOPES = ["@img/sharp-", "@img/sharp-libvips-", "@ngrok/ngrok-"];
|
|
|
|
/** Standalone packages that are not forked but must never be platform-forked. */
|
|
export const HYDRATED_ROOT_PACKAGES = ["fsevents"];
|
|
|
|
/**
|
|
* onnxruntime-node does not publish a darwin-x64 binary for napi-v6 (only
|
|
* linux/win32 x64 + darwin arm64), so existence cannot be asserted there.
|
|
*/
|
|
export const BUNDLED_EXEMPTIONS = new Set(["onnxruntime-node:darwin-x64"]);
|
|
|
|
function platformTriple(platform, arch) {
|
|
// koffi uses underscore triplets; better-sqlite3/wreq-js/onnx use dashes.
|
|
return { koffi: `${platform}_${arch}`, dash: `${platform}-${arch}` };
|
|
}
|
|
|
|
function rmrf(target) {
|
|
fs.rmSync(target, { recursive: true, force: true });
|
|
}
|
|
|
|
function copyDir(from, to) {
|
|
fs.cpSync(from, to, { recursive: true, verbatimSymlinks: false, force: true });
|
|
}
|
|
|
|
function directMemberNames(nodeModulesDir, scope) {
|
|
const scopeDir = path.join(nodeModulesDir, ...scope.split("/").slice(0, -1));
|
|
const prefix = scope.split("/").pop();
|
|
try {
|
|
return fs
|
|
.readdirSync(scopeDir)
|
|
.filter((name) => name.startsWith(prefix))
|
|
.map((name) => `${scope.slice(0, scope.lastIndexOf("/"))}/${name}`);
|
|
} catch {
|
|
return [];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Replace install-machine-forked packages inside the restored standalone tree
|
|
* with the forks resolved by THIS machine's node_modules.
|
|
*
|
|
* @param {{standaloneNodeModules: string, sourceNodeModules: string}} opts
|
|
* @returns {{replaced: string[], removed: string[], copied: string[]}}
|
|
*/
|
|
export function hydratePlatformNatives({ standaloneNodeModules, sourceNodeModules }) {
|
|
const replaced = [];
|
|
const removed = [];
|
|
const copied = [];
|
|
|
|
const forkedNames = new Set();
|
|
for (const scope of HYDRATED_SCOPES) {
|
|
for (const name of directMemberNames(sourceNodeModules, scope)) forkedNames.add(name);
|
|
for (const name of directMemberNames(standaloneNodeModules, scope)) forkedNames.add(name);
|
|
}
|
|
for (const pkg of HYDRATED_ROOT_PACKAGES) {
|
|
if (fs.existsSync(path.join(sourceNodeModules, pkg))) forkedNames.add(pkg);
|
|
if (fs.existsSync(path.join(standaloneNodeModules, pkg))) forkedNames.add(pkg);
|
|
}
|
|
|
|
for (const name of forkedNames) {
|
|
const standalonePath = path.join(standaloneNodeModules, ...name.split("/"));
|
|
const sourcePath = path.join(sourceNodeModules, ...name.split("/"));
|
|
const hadIt = fs.existsSync(standalonePath);
|
|
const hasIt = fs.existsSync(sourcePath);
|
|
if (hadIt) rmrf(standalonePath);
|
|
if (!hasIt) {
|
|
if (hadIt) removed.push(name);
|
|
continue; // e.g. fsevents on non-darwin legs: simply absent everywhere.
|
|
}
|
|
copyDir(sourcePath, standalonePath);
|
|
copied.push(name);
|
|
if (hadIt) replaced.push(name);
|
|
}
|
|
return { replaced, removed, copied };
|
|
}
|
|
|
|
/**
|
|
* Assert that every bundled native dependency can service `platform`/`arch`.
|
|
*
|
|
* @returns {{ok: true} | {ok: false, errors: string[]}}
|
|
*/
|
|
export function verifyBundledNatives({ nodeModulesDir, platform, arch }) {
|
|
const errors = [];
|
|
const triple = platformTriple(platform, arch);
|
|
|
|
const koffiDir = path.join(nodeModulesDir, "koffi", "build", "koffi", triple.koffi);
|
|
if (!fs.existsSync(koffiDir)) errors.push(`koffi: missing bundled triplet ${triple.koffi}`);
|
|
|
|
const sqlitePrebuild = path.join(
|
|
nodeModulesDir,
|
|
"better-sqlite3",
|
|
"prebuilds",
|
|
`${triple.dash}.node`
|
|
);
|
|
if (!fs.existsSync(sqlitePrebuild))
|
|
errors.push(`better-sqlite3: missing prebuild ${triple.dash}.node`);
|
|
|
|
const wreqDir = path.join(nodeModulesDir, "wreq-js", "rust");
|
|
const wreqNames = fs.existsSync(wreqDir)
|
|
? fs
|
|
.readdirSync(wreqDir)
|
|
.filter((n) => n.startsWith(`wreq-js.${triple.dash}`) && n.endsWith(".node"))
|
|
: [];
|
|
if (wreqNames.length === 0) errors.push(`wreq-js: missing rust binary for ${triple.dash}`);
|
|
|
|
const exempt = BUNDLED_EXEMPTIONS.has(`onnxruntime-node:${triple.dash}`);
|
|
if (!exempt) {
|
|
const onnxDir = path.join(nodeModulesDir, "onnxruntime-node", "bin", "napi-v6", platform, arch);
|
|
if (!fs.existsSync(onnxDir))
|
|
errors.push(`onnxruntime-node: missing ${platform}/${arch} binary`);
|
|
}
|
|
|
|
return errors.length === 0 ? { ok: true } : { ok: false, errors };
|
|
}
|