183 lines
6.2 KiB
JavaScript
183 lines
6.2 KiB
JavaScript
#!/usr/bin/env node
|
|
/**
|
|
* Patch electron-builder's NSIS extractUsing7za macro to extract directly into
|
|
* $INSTDIR instead of temp + CopyFiles.
|
|
*
|
|
* #1026 enlarged the packaged openclaw runtime; CopyFiles over thousands of
|
|
* files makes assisted installers look frozen (~50%) and often fails with the
|
|
* "app cannot be closed" retry dialog when AV or file locks are involved.
|
|
*
|
|
* Must run before makensis (package:win), not only in afterPack.
|
|
*/
|
|
|
|
import { existsSync, readFileSync, writeFileSync } from 'node:fs';
|
|
import { dirname, join } from 'node:path';
|
|
import { fileURLToPath } from 'node:url';
|
|
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
const ROOT = join(__dirname, '..');
|
|
|
|
export const EXTRACT_APP_PACKAGE_NSH = join(
|
|
ROOT,
|
|
'node_modules',
|
|
'app-builder-lib',
|
|
'templates',
|
|
'nsis',
|
|
'include',
|
|
'extractAppPackage.nsh',
|
|
);
|
|
|
|
const PATCH_MARKER = 'ClawX-patched-v2: extract directly to $INSTDIR and fail closed';
|
|
const LEGACY_PATCH_MARKER = 'ClawX-patched: extract directly to $INSTDIR';
|
|
const LEGACY_CONTINUE_ON_EXTRACT_FAILURE = 'continuing overwrite install anyway';
|
|
const FATAL_EXTRACT_FAILURE_DETAIL = 'Failed to extract ClawX files after multiple attempts.';
|
|
const ROLLBACK_EXTRACT_FAILURE_DETAIL = 'Restoring previous ClawX installation after failed update';
|
|
|
|
const PATCHED_EXTRACT_MACRO = [
|
|
'!macro extractUsing7za FILE',
|
|
` ; ${PATCH_MARKER}.`,
|
|
' StrCpy $R9 0',
|
|
' clawx_extract_attempt:',
|
|
' IntOp $R9 $R9 + 1',
|
|
' DetailPrint "Extracting ClawX application files (attempt $R9, please wait)..."',
|
|
' SetOutPath $INSTDIR',
|
|
' ClearErrors',
|
|
' Nsis7z::Extract "${FILE}"',
|
|
' IfErrors 0 clawx_extract_done',
|
|
' ${if} $R9 < 5',
|
|
' DetailPrint "Releasing file locks before retry..."',
|
|
' nsExec::ExecToStack \'taskkill /F /T /IM "${APP_EXECUTABLE_FILENAME}"\'',
|
|
' Pop $0',
|
|
' Pop $1',
|
|
' nsExec::ExecToStack \'taskkill /F /IM openclaw-gateway.exe\'',
|
|
' Pop $0',
|
|
' Pop $1',
|
|
' Sleep 3000',
|
|
' Goto clawx_extract_attempt',
|
|
' ${endIf}',
|
|
' DetailPrint "Failed to extract ClawX files after multiple attempts."',
|
|
' ${if} $clawxRollbackDir != ""',
|
|
' IfFileExists "$clawxRollbackDir\\" 0 clawx_extract_show_error',
|
|
' DetailPrint "Restoring previous ClawX installation after failed update..."',
|
|
' SetOutPath $TEMP',
|
|
' RMDir /r "$INSTDIR"',
|
|
' Rename "$clawxRollbackDir" "$INSTDIR"',
|
|
' ${endIf}',
|
|
' clawx_extract_show_error:',
|
|
' MessageBox MB_OK|MB_ICONEXCLAMATION "$(decompressionFailed)" /SD IDOK',
|
|
' SetErrorLevel 2',
|
|
' Quit',
|
|
' clawx_extract_done:',
|
|
'!macroend',
|
|
].join('\n');
|
|
|
|
const DECOMPRESS_MACRO = [
|
|
'!macro decompress',
|
|
' !ifdef ZIP_COMPRESSION',
|
|
' nsisunz::Unzip "$PLUGINSDIR\\app-$packageArch.zip" "$INSTDIR"',
|
|
' Pop $R0',
|
|
' StrCmp $R0 "success" +3',
|
|
' MessageBox MB_OK|MB_ICONEXCLAMATION "$(decompressionFailed)$\\n$R0"',
|
|
' Quit',
|
|
' !else',
|
|
' !insertmacro extractUsing7za "$PLUGINSDIR\\app-$packageArch.7z"',
|
|
' !endif',
|
|
'!macroend',
|
|
'',
|
|
].join('\n');
|
|
|
|
const EXTRACT_MACRO_PATTERN = /!macro extractUsing7za FILE[\s\S]*?!macroend/;
|
|
|
|
function countExtractMacros(content) {
|
|
return (content.match(/!macro extractUsing7za FILE/g) || []).length;
|
|
}
|
|
|
|
function isTemplateHealthy(content) {
|
|
return content.includes(PATCH_MARKER)
|
|
&& countExtractMacros(content) === 1
|
|
&& content.includes(FATAL_EXTRACT_FAILURE_DETAIL)
|
|
&& content.includes(ROLLBACK_EXTRACT_FAILURE_DETAIL)
|
|
&& content.includes('$(decompressionFailed)')
|
|
&& content.includes('SetErrorLevel 2')
|
|
&& content.includes('Quit')
|
|
&& !content.includes('$(appCannotBeClosed)')
|
|
&& !content.includes(LEGACY_CONTINUE_ON_EXTRACT_FAILURE);
|
|
}
|
|
|
|
function hasStaleExtractPatch(content) {
|
|
return content.includes(PATCH_MARKER)
|
|
|| content.includes(LEGACY_PATCH_MARKER)
|
|
|| content.includes(LEGACY_CONTINUE_ON_EXTRACT_FAILURE)
|
|
|| content.includes('$(appCannotBeClosed)');
|
|
}
|
|
|
|
/**
|
|
* @param {string} content
|
|
* @returns {string}
|
|
*/
|
|
export function restoreExtractAppPackageTemplate(content) {
|
|
const extractIdx = content.indexOf('!macro extractUsing7za FILE');
|
|
const decompressIdx = content.indexOf('!macro decompress');
|
|
const cutIdx = Math.min(
|
|
extractIdx === -1 ? Number.POSITIVE_INFINITY : extractIdx,
|
|
decompressIdx === -1 ? Number.POSITIVE_INFINITY : decompressIdx,
|
|
);
|
|
if (!Number.isFinite(cutIdx)) {
|
|
return content;
|
|
}
|
|
return `${content.slice(0, cutIdx)}${PATCHED_EXTRACT_MACRO}\n\n${DECOMPRESS_MACRO}`;
|
|
}
|
|
|
|
/**
|
|
* @param {string} [targetPath]
|
|
* @returns {boolean} true when template is patched (or already patched)
|
|
*/
|
|
export function patchNsisExtractTemplate(targetPath = EXTRACT_APP_PACKAGE_NSH) {
|
|
if (!existsSync(targetPath)) {
|
|
console.warn('[patch-nsis-extract] extractAppPackage.nsh not found, skipping.');
|
|
return false;
|
|
}
|
|
|
|
let original = readFileSync(targetPath, 'utf8');
|
|
|
|
if (isTemplateHealthy(original)) {
|
|
return true;
|
|
}
|
|
|
|
if (countExtractMacros(original) !== 1) {
|
|
console.warn('[patch-nsis-extract] Corrupted template detected; restoring tail section.');
|
|
original = restoreExtractAppPackageTemplate(original);
|
|
writeFileSync(targetPath, original, 'utf8');
|
|
if (isTemplateHealthy(original)) {
|
|
console.log('[patch-nsis-extract] Restored extractAppPackage.nsh (overwrite upgrade extract).');
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (hasStaleExtractPatch(original)) {
|
|
console.warn('[patch-nsis-extract] Stale ClawX extract patch detected; replacing with fail-closed patch.');
|
|
} else if (!original.includes('CopyFiles')) {
|
|
console.warn('[patch-nsis-extract] CopyFiles not found — NSIS template may have changed.');
|
|
return false;
|
|
}
|
|
|
|
const patched = original.replace(
|
|
EXTRACT_MACRO_PATTERN,
|
|
() => PATCHED_EXTRACT_MACRO,
|
|
);
|
|
|
|
if (patched === original) {
|
|
console.warn('[patch-nsis-extract] extractUsing7za macro regex did not match.');
|
|
return false;
|
|
}
|
|
|
|
writeFileSync(targetPath, patched, 'utf8');
|
|
console.log('[patch-nsis-extract] Patched extractAppPackage.nsh (direct Nsis7z::Extract to $INSTDIR).');
|
|
return true;
|
|
}
|
|
|
|
if (process.argv[1] === fileURLToPath(import.meta.url)) {
|
|
const ok = patchNsisExtractTemplate();
|
|
process.exit(ok ? 0 : 1);
|
|
}
|