/** * After electron-builder: fail the pack if sharp / @img look missing or truncated * in release unpacked dirs. Catches AV quarantine and incomplete asarUnpack before shipping. */ import fs from 'node:fs'; import path from 'node:path'; import { fileURLToPath, pathToFileURL } from 'node:url'; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..'); /** * @param {string} startDir * @returns {string | null} */ function findAsarUnpackedNodeModules(startDir) { const direct = path.join(startDir, 'resources', 'app.asar.unpacked', 'node_modules'); if (fs.existsSync(direct)) return direct; // macOS: *.app/Contents/Resources/app.asar.unpacked/node_modules if (!fs.existsSync(startDir)) return null; for (const name of fs.readdirSync(startDir)) { if (!name.endsWith('.app')) continue; const macNm = path.join( startDir, name, 'Contents', 'Resources', 'app.asar.unpacked', 'node_modules', ); if (fs.existsSync(macNm)) return macNm; } return null; } /** * @param {string} filePath * @param {number} minBytes */ function assertJsLooksIntact(filePath, minBytes) { if (!fs.existsSync(filePath)) { throw new Error(`[verify-packaged-sharp] missing: ${filePath}`); } const st = fs.statSync(filePath); if (st.size < minBytes) { throw new Error( `[verify-packaged-sharp] truncated (${st.size} B < ${minBytes}): ${filePath}`, ); } const head = fs.readFileSync(filePath, { encoding: 'utf8', flag: 'r' }).slice(0, 120); const trimmed = head.trimStart(); const ok = trimmed.startsWith("'use strict'") || trimmed.startsWith('"use strict"') || head.includes('require(') || head.includes('module.exports'); if (!ok) { throw new Error( `[verify-packaged-sharp] sharp entry does not look like JS (corrupt?): ${filePath}`, ); } } /** * @param {string} dir */ function assertHasNativeBinary(dir) { if (!fs.existsSync(dir)) { throw new Error(`[verify-packaged-sharp] missing native package dir: ${dir}`); } const stack = [dir]; while (stack.length) { const cur = stack.pop(); if (!cur) break; for (const name of fs.readdirSync(cur)) { const p = path.join(cur, name); const st = fs.statSync(p); if (st.isDirectory()) { stack.push(p); continue; } if ( name.endsWith('.node') || name.endsWith('.dll') || name.endsWith('.dylib') || /\.so(\.|$)/u.test(name) ) { if (st.size < 50_000) { throw new Error( `[verify-packaged-sharp] native binary too small (${st.size} B): ${p}`, ); } return; } } } throw new Error(`[verify-packaged-sharp] no .node/.dll/.so under ${dir}`); } /** * @param {{ label: string; unpackedRoot: string; imgDirs: string[] }} probe */ export function verifyUnpackedSharp(probe) { const unpackedNm = findAsarUnpackedNodeModules(probe.unpackedRoot) ?? path.join(probe.unpackedRoot, 'resources', 'app.asar.unpacked', 'node_modules'); const sharpIndex = path.join(unpackedNm, 'sharp', 'lib', 'index.js'); // Real sharp/lib/index.js is typically several KB; empty/AV-quarantined files fail here. assertJsLooksIntact(sharpIndex, 32); for (const img of probe.imgDirs) { assertHasNativeBinary(path.join(unpackedNm, '@img', img)); } } /** @type {{ label: string; unpackedRoot: string; imgDirs: string[] }[]} */ const PROBES = [ { label: 'win-unpacked', unpackedRoot: path.join(root, 'release', 'win-unpacked'), imgDirs: ['sharp-win32-x64'], }, { label: 'mac-arm64', unpackedRoot: path.join(root, 'release', 'mac-arm64'), imgDirs: ['sharp-darwin-arm64'], }, { label: 'mac', unpackedRoot: path.join(root, 'release', 'mac'), imgDirs: ['sharp-darwin-x64', 'sharp-darwin-arm64'], }, { label: 'linux-unpacked', unpackedRoot: path.join(root, 'release', 'linux-unpacked'), imgDirs: ['sharp-linux-x64', 'sharp-linux-arm64'], }, ]; /** * Verify every existing unpacked release dir. At least one must exist. * For multi-arch probes, require at least one listed @img package that is present. */ export function verifyPackagedSharpRelease(releaseRoot = root) { const probes = PROBES.map((p) => ({ ...p, unpackedRoot: path.join( releaseRoot, path.relative(root, p.unpackedRoot), ), })); const existing = probes.filter((p) => fs.existsSync(p.unpackedRoot)); if (existing.length === 0) { throw new Error( '[verify-packaged-sharp] no release unpacked dir found — run electron-builder first', ); } for (const probe of existing) { const nm = findAsarUnpackedNodeModules(probe.unpackedRoot); if (!nm) { throw new Error( `[verify-packaged-sharp] app.asar.unpacked/node_modules missing under ${probe.unpackedRoot}`, ); } const presentImg = probe.imgDirs.filter((d) => fs.existsSync(path.join(nm, '@img', d)), ); if (presentImg.length === 0) { throw new Error( `[verify-packaged-sharp] none of @img/{${probe.imgDirs.join(',')}} under ${nm}`, ); } verifyUnpackedSharp({ ...probe, imgDirs: presentImg }); console.log(`[verify-packaged-sharp] OK: ${probe.label}`); } } function main() { verifyPackagedSharpRelease(); } const entry = process.argv[1] ? pathToFileURL(path.resolve(process.argv[1])).href : ''; if (import.meta.url === entry) { try { main(); } catch (err) { console.error(err instanceof Error ? err.message : err); process.exit(1); } }