fix(pack): lazy-load sharp and verify unpacked natives
Avoid crashing Electron at startup when sharp is corrupt, and fail pack if asarUnpack natives look truncated. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
* Visually lossless re-encode for imported raster images (same pixel dimensions).
|
* Visually lossless re-encode for imported raster images (same pixel dimensions).
|
||||||
* Node-only; shared by the main app and ../project-converter (monorepo sibling).
|
* Node-only; shared by the main app and ../project-converter (monorepo sibling).
|
||||||
*/
|
*/
|
||||||
import sharp from 'sharp';
|
import { getSharp } from './sharpRuntime.mjs';
|
||||||
|
|
||||||
/** @typedef {import('node:buffer').Buffer} Buffer */
|
/** @typedef {import('node:buffer').Buffer} Buffer */
|
||||||
|
|
||||||
@@ -102,6 +102,7 @@ function makePassthrough(buf, meta) {
|
|||||||
* @param {number} h0
|
* @param {number} h0
|
||||||
*/
|
*/
|
||||||
async function sameDimensionsOrThrow(outBuf, w0, h0) {
|
async function sameDimensionsOrThrow(outBuf, w0, h0) {
|
||||||
|
const sharp = getSharp();
|
||||||
const m = await sharp(outBuf).metadata();
|
const m = await sharp(outBuf).metadata();
|
||||||
if ((m.width ?? 0) !== w0 || (m.height ?? 0) !== h0) {
|
if ((m.width ?? 0) !== w0 || (m.height ?? 0) !== h0) {
|
||||||
const err = new Error('encode changed dimensions');
|
const err = new Error('encode changed dimensions');
|
||||||
@@ -120,6 +121,13 @@ export async function optimizeImageBufferVisuallyLossless(src) {
|
|||||||
return makePassthrough(input, { width: 0, height: 0, format: 'png' });
|
return makePassthrough(input, { width: 0, height: 0, format: 'png' });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let sharp;
|
||||||
|
try {
|
||||||
|
sharp = getSharp();
|
||||||
|
} catch {
|
||||||
|
return makePassthrough(input, null);
|
||||||
|
}
|
||||||
|
|
||||||
let meta0;
|
let meta0;
|
||||||
try {
|
try {
|
||||||
meta0 = await sharp(input, { failOn: 'error', unlimited: true }).metadata();
|
meta0 = await sharp(input, { failOn: 'error', unlimited: true }).metadata();
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ import path from 'node:path';
|
|||||||
import { promisify } from 'node:util';
|
import { promisify } from 'node:util';
|
||||||
|
|
||||||
import ffmpegStatic from 'ffmpeg-static';
|
import ffmpegStatic from 'ffmpeg-static';
|
||||||
import sharp from 'sharp';
|
|
||||||
|
import { getSharp } from './sharpRuntime.mjs';
|
||||||
|
|
||||||
const execFileAsync = promisify(execFile);
|
const execFileAsync = promisify(execFile);
|
||||||
|
|
||||||
@@ -21,6 +22,7 @@ export async function generateScenePreviewThumbnailBytes(
|
|||||||
kind: 'image' | 'video',
|
kind: 'image' | 'video',
|
||||||
): Promise<Buffer | null> {
|
): Promise<Buffer | null> {
|
||||||
try {
|
try {
|
||||||
|
const sharp = getSharp();
|
||||||
if (kind === 'image') {
|
if (kind === 'image') {
|
||||||
return await sharp(source)
|
return await sharp(source)
|
||||||
.rotate()
|
.rotate()
|
||||||
|
|||||||
@@ -0,0 +1,51 @@
|
|||||||
|
/**
|
||||||
|
* Lazy `sharp` load so a corrupt/missing native install does not crash Electron at import time.
|
||||||
|
* Call only from image-processing paths; errors are recoverable for the rest of the app.
|
||||||
|
*/
|
||||||
|
import { createRequire } from 'node:module';
|
||||||
|
|
||||||
|
const require = createRequire(import.meta.url);
|
||||||
|
|
||||||
|
/** @type {typeof import('sharp') | null} */
|
||||||
|
let cached = null;
|
||||||
|
/** @type {Error | null} */
|
||||||
|
let loadError = null;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param {unknown} err
|
||||||
|
* @returns {Error}
|
||||||
|
*/
|
||||||
|
export function sharpLoadFailure(err) {
|
||||||
|
const detail = err instanceof Error ? err.message : String(err);
|
||||||
|
return new Error(
|
||||||
|
[
|
||||||
|
'Не удалось загрузить модуль обработки изображений (sharp).',
|
||||||
|
'Переустановите приложение полностью (удалите и поставьте заново)',
|
||||||
|
'или исключите папку установки из проверки антивируса.',
|
||||||
|
detail ? `Детали: ${detail}` : '',
|
||||||
|
]
|
||||||
|
.filter(Boolean)
|
||||||
|
.join(' '),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @returns {typeof import('sharp')}
|
||||||
|
*/
|
||||||
|
export function getSharp() {
|
||||||
|
if (cached) return cached;
|
||||||
|
if (loadError) throw loadError;
|
||||||
|
try {
|
||||||
|
cached = require('sharp');
|
||||||
|
return cached;
|
||||||
|
} catch (err) {
|
||||||
|
loadError = sharpLoadFailure(err);
|
||||||
|
throw loadError;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Reset cache (tests only). */
|
||||||
|
export function __resetSharpRuntimeForTests() {
|
||||||
|
cached = null;
|
||||||
|
loadError = null;
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import {
|
||||||
|
__resetSharpRuntimeForTests,
|
||||||
|
getSharp,
|
||||||
|
sharpLoadFailure,
|
||||||
|
} from './sharpRuntime.mjs';
|
||||||
|
|
||||||
|
void test('getSharp: loads sharp when install is healthy', () => {
|
||||||
|
__resetSharpRuntimeForTests();
|
||||||
|
const sharp = getSharp();
|
||||||
|
assert.equal(typeof sharp, 'function');
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('sharpLoadFailure: includes reinstall hint', () => {
|
||||||
|
const err = sharpLoadFailure(new Error('SyntaxError: Unexpected end of input'));
|
||||||
|
assert.match(err.message, /переустановите/i);
|
||||||
|
assert.match(err.message, /SyntaxError/);
|
||||||
|
});
|
||||||
+5
-5
@@ -10,15 +10,15 @@
|
|||||||
"build:obfuscate": "node scripts/build.mjs --production --obfuscate",
|
"build:obfuscate": "node scripts/build.mjs --production --obfuscate",
|
||||||
"lint": "eslint . --max-warnings 0",
|
"lint": "eslint . --max-warnings 0",
|
||||||
"typecheck": "tsc -p tsconfig.eslint.json --noEmit",
|
"typecheck": "tsc -p tsconfig.eslint.json --noEmit",
|
||||||
"test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/shared/ui/secondaryWindows.stability.test.ts app/renderer/npcs/tiptapEditorSafe.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/fileDrop.test.ts app/shared/graph/sceneListOrder.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/renderer/editor/help/helpLinkify.test.ts app/renderer/editor/sceneDescriptionHtml.test.ts app/shared/graph/storylineExportImport.test.ts app/shared/foundry/foundryGraph.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/shared/fieldEffectEraser.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/control/controlApp.audioPerf.networkRegression.test.ts app/renderer/control/controlApp.brushPerf.networkRegression.test.ts app/renderer/shared/useAssetImageUrl.cache.test.ts app/main/sessionIpc.phase6.networkRegression.test.ts app/renderer/shared/sceneOverlay/sceneOverlayHost.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/renderer/shared/traps/trapActivation.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/safeConsole.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/sceneTraps/sceneTrapsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts app/main/license/machineFingerprint.test.ts app/shared/types/appPlayers.test.ts app/shared/types/npcDisposition.test.ts app/shared/types/sceneGrid.test.ts app/shared/types/sceneGridSnap.test.ts app/main/tokens/tokenGridSnapSessionStore.test.ts app/shared/players/playerTeams.test.ts app/shared/players/launchPlayersSelection.test.ts app/main/players/playersStore.test.ts app/main/players/sceneNpcTokensSessionStore.test.ts app/main/players/scenePlayerTokensSessionStore.test.ts app/shared/ipc/contracts.players.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs scripts/release-native-prep.test.mjs",
|
"test": "tsx --test app/renderer/shared/ui/controls.tooltip.test.ts app/renderer/shared/ui/secondaryWindows.stability.test.ts app/renderer/npcs/tiptapEditorSafe.test.ts app/renderer/editor/state/projectState.race.test.ts app/renderer/editor/fileDrop.test.ts app/shared/graph/sceneListOrder.test.ts app/renderer/editor/graph/sceneCardById.test.ts app/renderer/editor/i18n/editorMessages.locale.test.ts app/renderer/editor/help/helpLinkify.test.ts app/renderer/editor/sceneDescriptionHtml.test.ts app/shared/graph/storylineExportImport.test.ts app/shared/foundry/foundryGraph.test.ts app/shared/ipc/contracts.mediaRemoval.test.ts app/shared/effectEraserHitTest.test.ts app/shared/fieldEffectEraser.test.ts app/renderer/control/controlApp.effectsPanel.test.ts app/renderer/control/controlApp.audioPerf.networkRegression.test.ts app/renderer/control/controlApp.brushPerf.networkRegression.test.ts app/renderer/shared/useAssetImageUrl.cache.test.ts app/main/sessionIpc.phase6.networkRegression.test.ts app/renderer/shared/sceneOverlay/sceneOverlayHost.test.ts app/renderer/shared/effects/PxiEffectsOverlay.pointer.test.ts app/renderer/shared/traps/trapActivation.test.ts app/main/windows/createWindows.editorClose.test.ts app/main/safeConsole.test.ts app/main/windows/bootWindow.test.ts app/main/effects/effectsStore.test.ts app/main/effects/sceneDarknessStore.test.ts app/main/sceneTraps/sceneTrapsStore.test.ts app/main/project/assetPrune.test.ts app/main/project/optimizeImageImport.test.ts app/main/project/scenePreviewThumbnail.test.ts app/main/project/fsRetry.test.ts app/main/project/zipRead.test.ts app/main/project/replaceFileAtomic.test.ts app/main/project/zipStore.legacyContract.test.ts app/shared/package.build.test.ts app/shared/license/canonicalJson.test.ts app/shared/license/productKey.test.ts app/shared/license/licenseService.networkRegression.test.ts app/shared/video/videoPlaybackPerf.networkRegression.test.ts app/shared/video/videoPlaybackLoop.networkRegression.test.ts app/main/license/verifyLicenseToken.test.ts app/main/license/machineFingerprint.test.ts app/shared/types/appPlayers.test.ts app/shared/types/npcDisposition.test.ts app/shared/types/sceneGrid.test.ts app/shared/types/sceneGridSnap.test.ts app/main/tokens/tokenGridSnapSessionStore.test.ts app/shared/players/playerTeams.test.ts app/shared/players/launchPlayersSelection.test.ts app/main/players/playersStore.test.ts app/main/players/sceneNpcTokensSessionStore.test.ts app/main/players/scenePlayerTokensSessionStore.test.ts app/shared/ipc/contracts.players.test.ts && node --test scripts/build-env.test.mjs scripts/obfuscate-main.test.mjs scripts/release-mac-prep.test.mjs scripts/release-native-prep.test.mjs scripts/verify-packaged-sharp.test.mjs app/main/project/sharpRuntime.test.mjs",
|
||||||
"format": "prettier . --check",
|
"format": "prettier . --check",
|
||||||
"format:write": "prettier . --write",
|
"format:write": "prettier . --write",
|
||||||
"postinstall": "patch-package",
|
"postinstall": "patch-package",
|
||||||
"release:info": "node scripts/print-release-info.mjs",
|
"release:info": "node scripts/print-release-info.mjs",
|
||||||
"pack": "npm run build && node scripts/release-win-prep.mjs && electron-builder",
|
"pack": "npm run build && node scripts/release-win-prep.mjs && electron-builder && node scripts/verify-packaged-sharp.mjs",
|
||||||
"pack:dir": "npm run build && node scripts/release-win-prep.mjs && electron-builder --dir",
|
"pack:dir": "npm run build && node scripts/release-win-prep.mjs && electron-builder --dir && node scripts/verify-packaged-sharp.mjs",
|
||||||
"pack:mac": "npm run build && node scripts/release-mac-prep.mjs && electron-builder --mac",
|
"pack:mac": "npm run build && node scripts/release-mac-prep.mjs && electron-builder --mac && node scripts/verify-packaged-sharp.mjs",
|
||||||
"pack:win": "npm run build && node scripts/release-win-prep.mjs && electron-builder --win",
|
"pack:win": "npm run build && node scripts/release-win-prep.mjs && electron-builder --win && node scripts/verify-packaged-sharp.mjs",
|
||||||
"pack:linux": "node scripts/release-linux-pack.mjs",
|
"pack:linux": "node scripts/release-linux-pack.mjs",
|
||||||
"release": "powershell -ExecutionPolicy Bypass -File scripts/ttrpg-release/release.ps1",
|
"release": "powershell -ExecutionPolicy Bypass -File scripts/ttrpg-release/release.ps1",
|
||||||
"release:all": "powershell -ExecutionPolicy Bypass -File scripts/ttrpg-release/release-all.ps1",
|
"release:all": "powershell -ExecutionPolicy Bypass -File scripts/ttrpg-release/release-all.ps1",
|
||||||
|
|||||||
@@ -71,3 +71,4 @@ run('npm', ['run', 'build']);
|
|||||||
ensureReleaseNativeDeps(projectRoot, 'linux');
|
ensureReleaseNativeDeps(projectRoot, 'linux');
|
||||||
run('electron-builder', ['--linux']);
|
run('electron-builder', ['--linux']);
|
||||||
normalizeLinuxReleaseNames();
|
normalizeLinuxReleaseNames();
|
||||||
|
run('node', [path.join(projectRoot, 'scripts', 'verify-packaged-sharp.mjs')]);
|
||||||
|
|||||||
@@ -0,0 +1,192 @@
|
|||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,89 @@
|
|||||||
|
import assert from 'node:assert/strict';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import os from 'node:os';
|
||||||
|
import path from 'node:path';
|
||||||
|
import test from 'node:test';
|
||||||
|
|
||||||
|
import { verifyUnpackedSharp } from './verify-packaged-sharp.mjs';
|
||||||
|
|
||||||
|
void test('verifyUnpackedSharp: accepts intact layout', () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dnd-sharp-verify-'));
|
||||||
|
try {
|
||||||
|
const nm = path.join(root, 'resources', 'app.asar.unpacked', 'node_modules');
|
||||||
|
const sharpLib = path.join(nm, 'sharp', 'lib');
|
||||||
|
fs.mkdirSync(sharpLib, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(sharpLib, 'index.js'),
|
||||||
|
"'use strict';\nmodule.exports = require('./constructor');\n",
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const imgDir = path.join(nm, '@img', 'sharp-win32-x64');
|
||||||
|
fs.mkdirSync(imgDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(imgDir, 'sharp.node'), Buffer.alloc(60_000, 1));
|
||||||
|
|
||||||
|
verifyUnpackedSharp({
|
||||||
|
label: 'fixture',
|
||||||
|
unpackedRoot: root,
|
||||||
|
imgDirs: ['sharp-win32-x64'],
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('verifyUnpackedSharp: rejects truncated sharp index', () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dnd-sharp-verify-'));
|
||||||
|
try {
|
||||||
|
const nm = path.join(root, 'resources', 'app.asar.unpacked', 'node_modules');
|
||||||
|
const sharpLib = path.join(nm, 'sharp', 'lib');
|
||||||
|
fs.mkdirSync(sharpLib, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(sharpLib, 'index.js'), 'x', 'utf8');
|
||||||
|
const imgDir = path.join(nm, '@img', 'sharp-win32-x64');
|
||||||
|
fs.mkdirSync(imgDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(imgDir, 'sharp.node'), Buffer.alloc(60_000, 1));
|
||||||
|
|
||||||
|
assert.throws(
|
||||||
|
() =>
|
||||||
|
verifyUnpackedSharp({
|
||||||
|
label: 'fixture',
|
||||||
|
unpackedRoot: root,
|
||||||
|
imgDirs: ['sharp-win32-x64'],
|
||||||
|
}),
|
||||||
|
/truncated|corrupt/i,
|
||||||
|
);
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
void test('verifyUnpackedSharp: accepts mac .app layout', () => {
|
||||||
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), 'dnd-sharp-verify-mac-'));
|
||||||
|
try {
|
||||||
|
const nm = path.join(
|
||||||
|
root,
|
||||||
|
'TTRPGPlayer.app',
|
||||||
|
'Contents',
|
||||||
|
'Resources',
|
||||||
|
'app.asar.unpacked',
|
||||||
|
'node_modules',
|
||||||
|
);
|
||||||
|
const sharpLib = path.join(nm, 'sharp', 'lib');
|
||||||
|
fs.mkdirSync(sharpLib, { recursive: true });
|
||||||
|
fs.writeFileSync(
|
||||||
|
path.join(sharpLib, 'index.js'),
|
||||||
|
"'use strict';\nmodule.exports = {};\n",
|
||||||
|
'utf8',
|
||||||
|
);
|
||||||
|
const imgDir = path.join(nm, '@img', 'sharp-darwin-arm64');
|
||||||
|
fs.mkdirSync(imgDir, { recursive: true });
|
||||||
|
fs.writeFileSync(path.join(imgDir, 'sharp.node'), Buffer.alloc(60_000, 1));
|
||||||
|
|
||||||
|
verifyUnpackedSharp({
|
||||||
|
label: 'mac-fixture',
|
||||||
|
unpackedRoot: root,
|
||||||
|
imgDirs: ['sharp-darwin-arm64'],
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
fs.rmSync(root, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user