import { mkdtempSync, rmSync, existsSync, cpSync, mkdirSync, readdirSync, statSync } from 'node:fs'; import os from 'node:os'; import path from 'node:path'; import { fileURLToPath } from 'node:url'; import { spawnSync } from 'node:child_process'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); const desktopRoot = path.resolve(__dirname, '..'); const localToolsRoot = path.join(desktopRoot, 'local-tools'); const args = new Set(process.argv.slice(2)); const dryRun = args.has('--dry-run'); const showHelp = args.has('--help') || args.has('-h'); const projectCacheRoot = path.join(desktopRoot, 'src-tauri', 'target', '.tauri'); const userCacheRoot = process.env.LOCALAPPDATA ? path.join(process.env.LOCALAPPDATA, 'tauri') : null; const cacheRoots = [projectCacheRoot, userCacheRoot].filter(Boolean); const nsisUtilsDllName = 'nsis_tauri_utils.dll'; function log(message) { console.log(`[preseed-tauri-tools] ${message}`); } function fail(message) { console.error(`[preseed-tauri-tools] ${message}`); process.exit(1); } function ensureDir(dirPath) { mkdirSync(dirPath, { recursive: true }); } function findNsisRoot(dirPath) { return findDirectoryContaining(dirPath, (current, entries) => { const names = new Set(entries.map((entry) => entry.name)); return names.has('makensis.exe') || names.has('Bin'); }); } function findWixRoot(dirPath) { return findDirectoryContaining(dirPath, (current, entries) => { const names = new Set(entries.map((entry) => entry.name)); return names.has('candle.exe') || names.has('light.exe'); }); } function directoryHasToolSignature(toolName, dirPath) { if (!existsSync(dirPath) || !statSync(dirPath).isDirectory()) { return false; } const match = toolName === 'NSIS' ? findNsisRoot(dirPath) : findWixRoot(dirPath); return Boolean(match); } function directoryHasReadyNsisLayout(dirPath) { const root = findNsisRoot(dirPath); if (!root) { return false; } return existsSync(path.join(root, 'Plugins', 'x86-unicode', nsisUtilsDllName)) || existsSync(path.join(root, 'Plugins', 'x86-unicode', 'additional', nsisUtilsDllName)); } function copyDirectoryContents(sourceDir, destinationDir) { ensureDir(destinationDir); for (const entry of readdirSync(sourceDir, { withFileTypes: true })) { const sourcePath = path.join(sourceDir, entry.name); const destinationPath = path.join(destinationDir, entry.name); cpSync(sourcePath, destinationPath, { recursive: true, force: true }); } } function expandZip(zipPath, destinationDir) { const command = [ '-NoProfile', '-Command', `Expand-Archive -LiteralPath '${zipPath.replace(/'/g, "''")}' -DestinationPath '${destinationDir.replace(/'/g, "''")}' -Force`, ]; const result = spawnSync('powershell', command, { stdio: 'inherit', shell: process.platform === 'win32', }); if (typeof result.status === 'number' && result.status !== 0) { process.exit(result.status); } if (result.error) { throw result.error; } } function findDirectoryContaining(rootDir, predicate) { const queue = [rootDir]; while (queue.length > 0) { const current = queue.shift(); const entries = readdirSync(current, { withFileTypes: true }); if (predicate(current, entries)) { return current; } for (const entry of entries) { if (entry.isDirectory()) { queue.push(path.join(current, entry.name)); } } } return null; } function firstExistingFile(candidates) { for (const candidate of candidates.filter(Boolean).map((value) => path.resolve(value))) { if (existsSync(candidate) && statSync(candidate).isFile()) { return candidate; } } return null; } function resolveNsisSupportDll() { return firstExistingFile([ process.env.ZCLAW_TAURI_NSIS_TAURI_UTILS_DLL, path.join(localToolsRoot, nsisUtilsDllName), path.join(localToolsRoot, 'nsis_tauri_utils-v0.5.3', nsisUtilsDllName), path.join(localToolsRoot, 'nsis_tauri_utils-v0.5.2', nsisUtilsDllName), ]); } function resolveSource(toolName) { if (toolName === 'NSIS') { const dirCandidates = [ process.env.ZCLAW_TAURI_NSIS_DIR, path.join(localToolsRoot, 'NSIS'), ].filter(Boolean).map((value) => path.resolve(value)); for (const candidate of dirCandidates) { if (directoryHasReadyNsisLayout(candidate)) { return { kind: 'dir', path: candidate }; } } const supportDll = resolveNsisSupportDll(); for (const candidate of dirCandidates) { if (directoryHasToolSignature('NSIS', candidate)) { return { kind: 'nsis-base-dir', path: candidate, supportDll }; } } const zipCandidates = [ process.env.ZCLAW_TAURI_NSIS_ZIP, path.join(localToolsRoot, 'nsis.zip'), path.join(localToolsRoot, 'nsis-3.11.zip'), path.join(localToolsRoot, 'nsis-3.08.zip'), ].filter(Boolean).map((value) => path.resolve(value)); for (const candidate of zipCandidates) { if (existsSync(candidate) && statSync(candidate).isFile()) { return { kind: 'nsis-base-zip', path: candidate, supportDll }; } } return null; } const envDirKey = toolName === 'NSIS' ? 'ZCLAW_TAURI_NSIS_DIR' : 'ZCLAW_TAURI_WIX_DIR'; const envZipKey = toolName === 'NSIS' ? 'ZCLAW_TAURI_NSIS_ZIP' : 'ZCLAW_TAURI_WIX_ZIP'; const localZipCandidates = toolName === 'NSIS' ? [path.join(localToolsRoot, 'nsis.zip'), path.join(localToolsRoot, 'nsis-3.11.zip')] : [ path.join(localToolsRoot, 'wix.zip'), path.join(localToolsRoot, 'wix314-binaries.zip'), path.join(localToolsRoot, 'wix311-binaries.zip'), ]; const localDirCandidates = toolName === 'NSIS' ? [path.join(localToolsRoot, toolName)] : [path.join(localToolsRoot, 'WixTools314'), path.join(localToolsRoot, 'WixTools')]; const dirCandidates = [process.env[envDirKey], ...localDirCandidates].filter(Boolean).map((value) => path.resolve(value)); for (const candidate of dirCandidates) { if (directoryHasToolSignature(toolName, candidate)) { return { kind: 'dir', path: candidate }; } } const zipCandidates = [process.env[envZipKey], ...localZipCandidates].filter(Boolean).map((value) => path.resolve(value)); for (const candidate of zipCandidates) { if (existsSync(candidate) && statSync(candidate).isFile()) { return { kind: 'zip', path: candidate }; } } return null; } function normalizeToolSource(toolName, source) { if (toolName === 'NSIS' && source.kind !== 'dir') { const tempRoot = mkdtempSync(path.join(os.tmpdir(), 'zclaw-tauri-tool-')); const assembledRoot = path.join(tempRoot, 'NSIS'); ensureDir(assembledRoot); if (source.kind === 'nsis-base-dir') { const baseRoot = findNsisRoot(source.path); if (!baseRoot) { fail(`NSIS 目录未找到 makensis:${source.path}`); } copyDirectoryContents(baseRoot, assembledRoot); } else if (source.kind === 'nsis-base-zip') { const extractedRoot = path.join(tempRoot, 'extract'); ensureDir(extractedRoot); expandZip(source.path, extractedRoot); const baseRoot = findNsisRoot(extractedRoot); if (!baseRoot) { fail(`NSIS zip 解压后未找到 makensis:${source.path}`); } copyDirectoryContents(baseRoot, assembledRoot); } if (!source.supportDll) { fail(`检测到 NSIS 基础包,但缺少 ${nsisUtilsDllName}。请放到 desktop/local-tools/${nsisUtilsDllName} 或设置 ZCLAW_TAURI_NSIS_TAURI_UTILS_DLL。`); } const pluginsDir = path.join(assembledRoot, 'Plugins', 'x86-unicode'); const additionalPluginsDir = path.join(pluginsDir, 'additional'); ensureDir(pluginsDir); ensureDir(additionalPluginsDir); cpSync(source.supportDll, path.join(pluginsDir, nsisUtilsDllName), { force: true }); cpSync(source.supportDll, path.join(additionalPluginsDir, nsisUtilsDllName), { force: true }); return { tempRoot, path: assembledRoot }; } if (source.kind === 'dir') { return source.path; } const tempRoot = mkdtempSync(path.join(os.tmpdir(), 'zclaw-tauri-tool-')); const extractedRoot = path.join(tempRoot, 'extract'); ensureDir(extractedRoot); expandZip(source.path, extractedRoot); const normalized = toolName === 'NSIS' ? findNsisRoot(extractedRoot) : findWixRoot(extractedRoot); if (!normalized) { fail(`${toolName} zip 解压后未找到有效工具目录:${source.path}`); } return { tempRoot, path: normalized }; } function printUsage() { console.log('Usage: node scripts/preseed-tauri-tools.mjs [--dry-run]'); console.log('Sources:'); console.log(' ZCLAW_TAURI_NSIS_DIR / desktop/local-tools/NSIS'); console.log(' ZCLAW_TAURI_NSIS_ZIP / desktop/local-tools/nsis.zip or nsis-3.11.zip'); console.log(` ZCLAW_TAURI_NSIS_TAURI_UTILS_DLL / desktop/local-tools/${nsisUtilsDllName}`); console.log(' ZCLAW_TAURI_WIX_DIR / desktop/local-tools/WixTools314 or WixTools'); console.log(' ZCLAW_TAURI_WIX_ZIP / desktop/local-tools/wix.zip or wix314-binaries.zip'); } if (showHelp) { printUsage(); process.exit(0); } for (const toolName of ['NSIS', 'WixTools']) { const source = resolveSource(toolName); if (!source) { log(`${toolName} 未提供本地预置源,跳过。`); continue; } let normalized = null; try { normalized = normalizeToolSource(toolName, source); const sourcePath = typeof normalized === 'string' ? normalized : normalized.path; for (const cacheRoot of cacheRoots) { const destinationNames = toolName === 'WixTools' ? ['WixTools314', 'WixTools'] : [toolName]; for (const destinationName of destinationNames) { const destination = path.join(cacheRoot, destinationName); log(`${toolName}: ${source.path} -> ${destination}`); if (!dryRun) { ensureDir(cacheRoot); rmSync(destination, { recursive: true, force: true }); copyDirectoryContents(sourcePath, destination); } } } } finally { if (normalized && typeof normalized !== 'string' && normalized.tempRoot) { rmSync(normalized.tempRoot, { recursive: true, force: true }); } } } if (dryRun) { log('Dry run 完成,未写入任何文件。'); }