Add legacy Electron app
build-winui / winui (push) Has been cancelled

This commit is contained in:
QWQLwToo
2026-06-26 13:29:02 +08:00
parent 079ee4eaeb
commit 46a3674381
115 changed files with 55280 additions and 0 deletions
+670
View File
@@ -0,0 +1,670 @@
// build-scripts/build.js
const { minify } = require('terser');
const fs = require('fs-extra');
const path = require('path');
// [修复] 设置控制台输出编码为 UTF-8,解决乱码问题
if (process.platform === 'win32') {
try {
// Windows 下设置控制台编码
process.stdout.setDefaultEncoding('utf8');
process.stderr.setDefaultEncoding('utf8');
// 尝试设置控制台代码页为 UTF-8
const { execSync } = require('child_process');
try {
execSync('chcp 65001 >nul 2>&1', { encoding: 'utf8' });
} catch (e) {
// 忽略编码设置错误
}
} catch (e) {
// 忽略编码设置错误
}
}
// ====================================================================================
// --- 🚀 核心配置区 ---
// ====================================================================================
const rootDir = path.join(__dirname, '..');
const outputDir = path.join(rootDir, 'app_dist');
// ====================================================================================
// --- 🔍 自动化识别需要保留的内容 ---
// ====================================================================================
/**
* [自动化] 从 preload.js 和 main.js 中提取所有 IPC 通信频道名称
* @returns {Promise<Set<string>>} - 返回所有频道名称的集合
*/
async function extractIpcChannels() {
const channels = new Set();
try {
// 从 preload.js 提取
const preloadPath = path.join(rootDir, 'preload.js');
if (await fs.pathExists(preloadPath)) {
const preloadContent = await fs.readFile(preloadPath, 'utf8');
// 匹配 ipcRenderer.invoke('channel'), ipcRenderer.send('channel'), ipcRenderer.on('channel')
const preloadRegex = /ipcRenderer\.(?:invoke|send|on)\(['"]([^'"]+)['"]/g;
let match;
while ((match = preloadRegex.exec(preloadContent)) !== null) {
channels.add(match[1]);
}
}
// 从 main.js 提取
const mainPath = path.join(rootDir, 'main.js');
if (await fs.pathExists(mainPath)) {
const mainContent = await fs.readFile(mainPath, 'utf8');
// 匹配 ipcMain.handle('channel'), ipcMain.on('channel')
const mainRegex = /ipcMain\.(?:handle|on)\(['"]([^'"]+)['"]/g;
let match;
while ((match = mainRegex.exec(mainContent)) !== null) {
channels.add(match[1]);
}
}
return channels;
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取 IPC 频道失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取 IPC 频道失败:', error);
}
return channels;
}
}
/**
* [自动化] 从 contextBridge.exposeInMainWorld 中提取所有暴露的 API 名称
* @returns {Promise<Set<string>>} - 返回所有 API 名称的集合
*/
async function extractExposedApis() {
const apis = new Set();
try {
const preloadPath = path.join(rootDir, 'preload.js');
if (await fs.pathExists(preloadPath)) {
const content = await fs.readFile(preloadPath, 'utf8');
// 匹配 contextBridge.exposeInMainWorld('electronAPI', { ... })
const exposeRegex = /contextBridge\.exposeInMainWorld\(['"]([^'"]+)['"]/g;
let match;
while ((match = exposeRegex.exec(content)) !== null) {
apis.add(match[1]);
}
// 提取对象中的所有方法名
const objectRegex = /contextBridge\.exposeInMainWorld\([^,]+,\s*\{([^}]+)\}/s;
const objectMatch = content.match(objectRegex);
if (objectMatch) {
const objectContent = objectMatch[1];
// 匹配方法名: methodName:
const methodRegex = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:/g;
let methodMatch;
while ((methodMatch = methodRegex.exec(objectContent)) !== null) {
apis.add(methodMatch[1]);
}
}
}
return apis;
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取暴露 API 失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取暴露 API 失败:', error);
}
return apis;
}
}
/**
* [自动化] 从所有 JS 文件中提取 export 的类名、函数名和变量名
* @returns {Promise<{names: Set<string>, strings: Set<string>}>} - 返回标识符和关键字符串
*/
async function extractExportedIdentifiers() {
const names = new Set();
const strings = new Set();
// 添加 ES6 模块关键字
['export', 'default', 'import', 'from', 'as', 'module', 'exports'].forEach(s => strings.add(s));
try {
const jsDir = path.join(rootDir, 'src', 'js');
const files = await findJsFiles(jsDir);
for (const filePath of files) {
try {
const content = await fs.readFile(filePath, 'utf8');
// 提取 export default class ClassName
const classExportRegex = /export\s+default\s+class\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
let match;
while ((match = classExportRegex.exec(content)) !== null) {
names.add(match[1]);
}
// 提取 export default new ClassName()
const newExportRegex = /export\s+default\s+new\s+([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
while ((match = newExportRegex.exec(content)) !== null) {
names.add(match[1]);
}
// 提取 export default functionName
const funcExportRegex = /export\s+default\s+(?:function\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)/g;
while ((match = funcExportRegex.exec(content)) !== null) {
names.add(match[1]);
}
// 提取 export { name1, name2 }
const namedExportRegex = /export\s+\{([^}]+)\}/g;
while ((match = namedExportRegex.exec(content)) !== null) {
const exports = match[1].split(',').map(s => s.trim().split(/\s+as\s+/)[0].trim());
exports.forEach(name => {
if (name && /^[a-zA-Z_$]/.test(name)) {
names.add(name);
}
});
}
// 提取 window.xxx = 或 window['xxx'] =
const windowAssignRegex = /window\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*=/g;
while ((match = windowAssignRegex.exec(content)) !== null) {
names.add(match[1]);
strings.add(match[1]);
}
// 提取 window['xxx'] =
const windowBracketRegex = /window\[['"]([^'"]+)['"]\]\s*=/g;
while ((match = windowBracketRegex.exec(content)) !== null) {
names.add(match[1]);
strings.add(match[1]);
}
} catch (err) {
// 忽略单个文件的错误
}
}
return { names, strings };
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取导出标识符失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取导出标识符失败:', error);
}
return { names, strings };
}
}
/**
* [自动化] 从代码中提取所有类的方法名(通过分析类定义)
* @returns {Promise<Set<string>>} - 返回所有方法名的集合
*/
async function extractClassMethods() {
const methods = new Set();
try {
const jsDir = path.join(rootDir, 'src', 'js');
const files = await findJsFiles(jsDir);
for (const filePath of files) {
try {
const content = await fs.readFile(filePath, 'utf8');
// 提取类方法: 匹配 class 内部的方法定义
// 1. 匹配 async methodName() { 或 methodName() {
const classMethodRegex = /(?:async\s+)?([a-zA-Z_$][a-zA-Z0-9_$]*)\s*\([^)]*\)\s*\{/g;
let match;
while ((match = classMethodRegex.exec(content)) !== null) {
const methodName = match[1];
// 排除常见的关键字和内置方法
if (!['constructor', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'catch', 'then', 'finally'].includes(methodName)) {
methods.add(methodName);
}
}
// 2. 也匹配对象方法: methodName: function() { 或 methodName: () => {
const objectMethodRegex = /([a-zA-Z_$][a-zA-Z0-9_$]*)\s*:\s*(?:function|\([^)]*\)\s*=>)/g;
while ((match = objectMethodRegex.exec(content)) !== null) {
const methodName = match[1];
if (!['constructor', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf'].includes(methodName)) {
methods.add(methodName);
}
}
// 3. 匹配 this.methodName 调用,提取方法名
const thisMethodRegex = /this\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\(|\.bind)/g;
while ((match = thisMethodRegex.exec(content)) !== null) {
const methodName = match[1];
if (!['constructor', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf'].includes(methodName)) {
methods.add(methodName);
}
}
} catch (err) {
// 忽略单个文件的错误
}
}
return methods;
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取类方法失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取类方法失败:', error);
}
return methods;
}
}
/**
* [自动化] 从 main.js 中提取所有使用的 Node.js 内置模块方法名
* @returns {Promise<Set<string>>} - 返回所有方法名的集合
*/
async function extractNodeBuiltinMethods() {
const methods = new Set();
try {
const mainPath = path.join(rootDir, 'main.js');
if (await fs.pathExists(mainPath)) {
const content = await fs.readFile(mainPath, 'utf8');
// 提取 path.xxx, fs.xxx, crypto.xxx, os.xxx 等方法调用
const builtinModuleRegex = /(?:path|fs|crypto|os|https|http|stream|child_process|worker_threads|electron)\.([a-zA-Z_$][a-zA-Z0-9_$]*)\s*(?:\(|\[)/g;
let match;
while ((match = builtinModuleRegex.exec(content)) !== null) {
methods.add(match[1]);
}
}
// 添加常用的 Node.js 内置模块方法(确保不被压缩)
const commonNodeMethods = [
// path 模块
'join', 'resolve', 'dirname', 'basename', 'extname', 'parse', 'format', 'normalize', 'isAbsolute',
// fs 模块
'readFile', 'writeFile', 'readFileSync', 'writeFileSync', 'existsSync', 'mkdirSync', 'statSync', 'readdirSync',
'createReadStream', 'createWriteStream', 'access', 'accessSync', 'unlink', 'unlinkSync', 'rmdir', 'rmdirSync',
// crypto 模块
'createHash', 'createHmac', 'randomBytes', 'createCipheriv', 'createDecipheriv',
// os 模块
'platform', 'arch', 'homedir', 'tmpdir', 'uptime', 'totalmem', 'freemem', 'cpus', 'networkInterfaces',
// stream 模块
'pipeline', 'Readable', 'Writable', 'Transform',
// child_process 模块
'exec', 'execSync', 'spawn', 'spawnSync',
// https/http 模块
'get', 'request', 'createServer',
// worker_threads 模块
'Worker', 'isMainThread', 'parentPort',
// Electron 模块
'app', 'BrowserWindow', 'ipcMain', 'ipcRenderer', 'dialog', 'shell', 'nativeTheme', 'screen', 'session',
'getPath', 'isPackaged', 'getVersion', 'getLocale', 'quit', 'exit', 'relaunch',
'on', 'handle', 'send', 'invoke', 'webContents', 'loadURL', 'loadFile', 'show', 'hide', 'minimize', 'maximize', 'close',
'showMessageBox', 'showOpenDialog', 'showSaveDialog', 'openExternal', 'openPath',
'shouldUseDarkColors', 'themeSource', 'getAllDisplays', 'getPrimaryDisplay',
'defaultSession', 'fromPartition', 'webRequest', 'onHeadersReceived', 'setPreloads'
];
commonNodeMethods.forEach(method => methods.add(method));
return methods;
} catch (error) {
try {
process.stderr.write(Buffer.from(`❌ 提取 Node.js 内置方法失败: ${error.message || error}\n`, 'utf8'));
} catch (e) {
console.error('❌ 提取 Node.js 内置方法失败:', error);
}
return methods;
}
}
/**
* 递归查找所有 JS 文件
*/
async function findJsFiles(dir) {
let results = [];
try {
const list = await fs.readdir(dir);
for (const file of list) {
const filePath = path.join(dir, file);
const stat = await fs.stat(filePath);
if (stat && stat.isDirectory()) {
results = results.concat(await findJsFiles(filePath));
} else if (filePath.endsWith('.js')) {
results.push(filePath);
}
}
} catch (error) {
// 忽略错误
}
return results;
}
// ====================================================================================
// --- ⚙️ 构建脚本执行区 ---
// ====================================================================================
const sourcesToProcess = [
'main.js',
'preload.js',
'config',
'src'
];
/**
* 判断是否为关键文件(需要特殊处理)
*/
function isCriticalFile(filePath) {
const criticalPatterns = [
/main\.js$/,
/preload\.js$/,
/configManager\.js$/,
/mainPage\.js$/,
/uiManager\.js$/,
/toolStatusManager\.js$/,
/toolHealthChecker\.js$/,
/toolHealthCheckModal\.js$/
];
return criticalPatterns.some(pattern => pattern.test(filePath));
}
/**
* 使用 Terser 压缩代码(轻量级,不会导致方法调用问题)
* @param {string} code - 原始代码
* @param {string} filePath - 文件路径
* @param {Object} reservedData - 预提取的保留数据
* @returns {Promise<string>} - 压缩后的代码
*/
async function minifyCode(code, filePath, reservedData) {
try {
const { reservedNames, reservedStrings } = reservedData;
const result = await minify(code, {
compress: {
drop_console: false, // 保留 console,方便调试
drop_debugger: true,
pure_funcs: [], // 不删除任何函数调用
passes: 2 // 压缩次数
},
mangle: {
reserved: Array.from(reservedNames), // 保留的标识符
// [修复] 完全禁用属性压缩,避免类方法被压缩导致的问题
properties: false // 禁用属性压缩,确保 this.methodName 不被压缩
},
format: {
comments: false // 删除注释
},
keep_classnames: true, // 保留类名
keep_fnames: true, // [修复] 保留函数名,确保类方法名不被压缩
safari10: true // Safari 10 兼容性
});
return result.code || code;
} catch (error) {
const errorMsg = `⚠️ 压缩 ${path.relative(outputDir, filePath)} 失败,使用原始代码: ${error.message}`;
try {
process.stderr.write(Buffer.from(errorMsg + '\n', 'utf8'));
} catch (e) {
console.error(errorMsg);
}
return code;
}
}
/**
* 主构建函数
*/
async function run() {
// [修复] 确保输出使用 UTF-8 编码
const log = (message) => {
try {
process.stdout.write(Buffer.from(message + '\n', 'utf8'));
} catch (e) {
console.log(message);
}
};
log('🧹 [1/6] 清理旧的构建目录...');
await fs.remove(outputDir);
await fs.ensureDir(outputDir);
log('🔄 [2/6] 复制并清理文件用于发布...');
const packageJsonPath = path.join(rootDir, 'package.json');
const packageJson = await fs.readJson(packageJsonPath);
const prodPackageJson = {
name: packageJson.name,
version: packageJson.version,
productName: packageJson.productName,
description: packageJson.description,
main: packageJson.main,
author: packageJson.author,
dependencies: packageJson.dependencies
};
await fs.writeJson(path.join(outputDir, 'package.json'), prodPackageJson, { spaces: 2 });
log('- 已清理并写入 package.json');
// 复制 lang 目录
const langSrc = path.join(rootDir, 'lang');
const langDest = path.join(outputDir, 'lang');
if (await fs.pathExists(langSrc)) {
await fs.copy(langSrc, langDest);
log('- 已复制 lang 目录到 app_dist');
} else {
throw new Error(`❌ 找不到 lang 目录:${langSrc}`);
}
// 复制 config-template.ini
const configTemplatePath = path.join(rootDir, 'build-scripts', 'config-template.ini');
const configTemplateDest = path.join(outputDir, 'config-template.ini');
if (await fs.pathExists(configTemplatePath)) {
await fs.copy(configTemplatePath, configTemplateDest);
log('- 已复制 config-template.ini 到 app_dist');
} else {
throw new Error(`❌ 找不到 config-template.ini${configTemplatePath}`);
}
// 复制其他源文件
for (const source of sourcesToProcess) {
const sourcePath = path.join(rootDir, source);
const destPath = path.join(outputDir, source);
if (await fs.pathExists(sourcePath)) {
await fs.copy(sourcePath, destPath);
}
}
log('- 其他所有源文件已复制。');
// 自动化识别需要保留的内容(只执行一次)
log('🔍 [3/6] 自动化识别需要保留的内容...');
const ipcChannels = await extractIpcChannels();
const exposedApis = await extractExposedApis();
const exportedIds = await extractExportedIdentifiers();
const classMethods = await extractClassMethods();
const nodeBuiltinMethods = await extractNodeBuiltinMethods();
// 合并所有需要保留的内容
const reservedNames = new Set();
const reservedStrings = new Set();
// 添加 IPC 频道到字符串保留列表
ipcChannels.forEach(ch => reservedStrings.add(ch));
// 添加暴露的 API
exposedApis.forEach(api => {
reservedNames.add(api);
reservedStrings.add(api);
});
// 添加导出标识符
exportedIds.names.forEach(name => reservedNames.add(name));
exportedIds.strings.forEach(str => reservedStrings.add(str));
// 添加类方法名
classMethods.forEach(method => reservedNames.add(method));
// [修复] 添加 MainPage 的关键方法名到保留列表(防止方法被压缩)
// 这些方法名必须保留,否则 this.methodName.bind() 会失败
const mainPageMethods = [
'renderWelcomePage',
'renderToolHealthCheckPage',
'renderLogsPage',
'renderSettingsPage',
'updateActiveNavButton',
'navigateTo',
'initializePage',
'init',
'bindWindowControls',
'bindNavigationEvents',
'bindThemeToggle',
'addRippleEffectListener',
'bindGlobalKeyListener',
'loadLanguage',
'autoCheckUpdates',
'listenForDownloadProgress',
'listenForGlobalNetworkSpeed',
'handleSecretTrigger',
'renderTrafficChart',
'handleLanguageChange',
'updateLockedToolsList',
'updateToolHealthStats',
'bindSettingsPageEvents'
];
mainPageMethods.forEach(method => {
reservedNames.add(method);
reservedStrings.add(method);
});
// 添加 UIManager 的关键方法名到保留列表
const uiManagerMethods = [
'getHealthIndicator',
'renderToolboxPage',
'_renderCurrentPage',
'_filterAndRenderTools'
];
uiManagerMethods.forEach(method => {
reservedNames.add(method);
reservedStrings.add(method);
});
// 添加 Node.js 内置模块方法名(重要!防止 path.join 等被压缩)
nodeBuiltinMethods.forEach(method => {
reservedNames.add(method);
reservedStrings.add(method);
});
// 添加 JavaScript 原生方法
['bind', 'call', 'apply', 'toString', 'valueOf', 'hasOwnProperty', 'isPrototypeOf', 'propertyIsEnumerable'].forEach(name => {
reservedNames.add(name);
reservedStrings.add(name);
});
// 添加 Electron 和浏览器相关
['electronAPI', 'window', 'document', 'require', 'module', 'exports', 'ipcRenderer', 'ipcMain', 'contextBridge', 'BrowserWindow', 'app', 'process', 'global', 'console'].forEach(name => {
reservedNames.add(name);
reservedStrings.add(name);
});
// 添加 Node.js 内置模块名
['path', 'fs', 'crypto', 'os', 'https', 'http', 'stream', 'child_process', 'worker_threads', 'electron', 'util', 'events', 'buffer'].forEach(name => {
reservedNames.add(name);
reservedStrings.add(name);
});
log(`- 识别完成: ${ipcChannels.size} 个 IPC 频道, ${exposedApis.size} 个暴露 API, ${exportedIds.names.size} 个导出标识符, ${classMethods.size} 个类方法, ${nodeBuiltinMethods.size} 个 Node.js 内置方法`);
log(`- 总计保留: ${reservedNames.size} 个标识符, ${reservedStrings.size} 个字符串`);
// 准备保留数据对象
const reservedData = { reservedNames, reservedStrings };
log('📦 [4/6] 压缩 JavaScript 文件...');
const jsFiles = await findJsFiles(outputDir);
// 分离关键文件和普通文件
const criticalFiles = [];
const normalFiles = [];
for (const filePath of jsFiles) {
if (isCriticalFile(filePath)) {
criticalFiles.push(filePath);
} else {
normalFiles.push(filePath);
}
}
log(`- 发现 ${criticalFiles.length} 个关键文件,${normalFiles.length} 个普通文件`);
// 处理所有文件
let successCount = 0;
let failCount = 0;
for (const filePath of jsFiles) {
try {
const code = await fs.readFile(filePath, 'utf8');
const minified = await minifyCode(code, filePath, reservedData);
await fs.writeFile(filePath, minified);
successCount++;
if (successCount % 10 === 0) {
try {
process.stdout.write(Buffer.from(`\r- 已处理: ${successCount}/${jsFiles.length}`, 'utf8'));
} catch (e) {
process.stdout.write(`\r- 已处理: ${successCount}/${jsFiles.length}`);
}
}
} catch (error) {
const relativePath = path.relative(outputDir, filePath);
const errorMsg = `\n❌ 处理 ${relativePath} 失败: ${error.message || error}`;
try {
process.stderr.write(Buffer.from(errorMsg + '\n', 'utf8'));
} catch (e) {
console.error(errorMsg);
}
failCount++;
}
}
log(`\n- 处理完成: ${successCount} 成功, ${failCount} 失败`);
log('📊 [5/6] 生成构建报告...');
const buildReport = {
timestamp: new Date().toISOString(),
version: packageJson.version,
filesProcessed: {
total: jsFiles.length,
critical: criticalFiles.length,
normal: normalFiles.length,
success: successCount,
failed: failCount
},
security: {
minificationEnabled: true,
reservedIdentifiers: {
ipcChannels: ipcChannels.size,
exposedApis: exposedApis.size,
exportedIdentifiers: exportedIds.names.size,
classMethods: classMethods.size,
nodeBuiltinMethods: nodeBuiltinMethods.size,
total: reservedNames.size
},
autoDetectionEnabled: true
}
};
await fs.writeJson(
path.join(outputDir, 'build-report.json'),
buildReport,
{ spaces: 2 }
);
log('- 构建报告已生成');
log('✅ [6/6] 构建过程成功完成!');
log(`📦 最终的应用文件已准备就绪,位于: ${outputDir}`);
log(`🔐 安全措施: 代码压缩 + 自动识别保留内容`);
log(`📈 处理统计: ${jsFiles.length} 个文件 (${successCount} 成功, ${failCount} 失败)`);
}
// 运行构建
run().catch(err => {
const errorMsg = `❌ 在构建过程中发生错误:\n${err.stack || err.message || err}`;
try {
process.stderr.write(Buffer.from(errorMsg + '\n', 'utf8'));
} catch (e) {
console.error(errorMsg);
}
process.exit(1);
});
+20
View File
@@ -0,0 +1,20 @@
; YMhut Box Configuration
[Settings]
; App Version - DO NOT EDIT
Version=${VERSION}
; Language setting
; auto = Use system language (default)
; en-US = English
; zh-CN = Chinese (Simplified)
Language=auto
CheckForUpdates=true
[Paths]
; Path for language packs (relative to app root)
LanguagePath=resources/lang
; Path for plugins (reserved for future use)
PluginPath=plugins
+189
View File
@@ -0,0 +1,189 @@
; ============================================================================
; 终极修复版 V2 (Root-Bin 架构 | 修复文件名后缀问题)
; ============================================================================
!include "FileFunc.nsh"
!include "LogicLib.nsh"
!include "MUI2.nsh"
; ----------------------------------------------------------------------------
; 1. 定义启动函数 (修复编译报错 & 路径指向 bin)
; ----------------------------------------------------------------------------
!ifdef NSIS_WIN32_MAKENSIS
!ifndef BUILD_UNINSTALLER
Function LaunchBinApp
; [修复] 添加 .exe 后缀
ExecShell "" "$INSTDIR\bin\${APP_FILENAME}.exe"
FunctionEnd
; ---------------- [修改开始] ----------------
; 注释掉以下三行以移除安装完成后的“运行”选项
; !define MUI_FINISHPAGE_RUN
; !define MUI_FINISHPAGE_RUN_TEXT "运行 ${PRODUCT_NAME}"
; !define MUI_FINISHPAGE_RUN_FUNCTION "LaunchBinApp"
; ---------------- [修改结束] ----------------
!endif
!endif
; ----------------------------------------------------------------------------
; [阶段 1] 初始化
; ----------------------------------------------------------------------------
!macro customInit
; 读取注册表
ReadRegStr $0 HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_GUID}" "InstallLocation"
${If} $0 != ""
StrCpy $INSTDIR $0
${EndIf}
; 防套娃检查 (检查 .exe 是否存在)
${GetFileName} "$INSTDIR" $R0
${If} $R0 == "${PRODUCT_FILENAME}"
${GetParent} "$INSTDIR" $R1
IfFileExists "$R1\config.ini" fix_path 0
IfFileExists "$R1\data" fix_path 0
Goto check_done
fix_path:
StrCpy $INSTDIR "$R1"
${EndIf}
check_done:
!macroend
; ----------------------------------------------------------------------------
; [阶段 2] 安装执行 (核心:文件移动)
; ----------------------------------------------------------------------------
!macro customInstall
SetOutPath "$INSTDIR"
; 1. 建立目录
CreateDirectory "$INSTDIR\bin"
CreateDirectory "$INSTDIR\data"
; 2. 处理 config.ini
IfFileExists "$INSTDIR\config.ini" config_ready 0
IfFileExists "$INSTDIR\resources\config-template.ini" 0 try_root_template
CopyFiles "$INSTDIR\resources\config-template.ini" "$INSTDIR\config.ini"
Goto patch_version
try_root_template:
IfFileExists "$INSTDIR\config-template.ini" 0 config_ready
CopyFiles "$INSTDIR\config-template.ini" "$INSTDIR\config.ini"
patch_version:
nsExec::ExecToStack 'powershell -Command "(Get-Content -Path \"$INSTDIR\config.ini\" -Raw) -replace \"Version=.*\", \"Version=${VERSION}\" | Set-Content -Path \"$INSTDIR\config.ini\" -Encoding utf8"'
config_ready:
; 3. 移动文件夹
RMDir /r "$INSTDIR\bin\resources"
Rename "$INSTDIR\resources" "$INSTDIR\bin\resources"
RMDir /r "$INSTDIR\bin\locales"
Rename "$INSTDIR\locales" "$INSTDIR\bin\locales"
RMDir /r "$INSTDIR\bin\lang"
Rename "$INSTDIR\lang" "$INSTDIR\bin\lang"
; 4. [关键修复] 移动主程序 EXE 到 Bin (显式添加 .exe)
; 先清理目标
Delete "$INSTDIR\bin\${APP_FILENAME}.exe"
; 尝试重命名移动
Rename "$INSTDIR\${APP_FILENAME}.exe" "$INSTDIR\bin\${APP_FILENAME}.exe"
; [双重保险] 如果 Rename 失败(极少见),使用 Copy + Delete
IfFileExists "$INSTDIR\bin\${APP_FILENAME}.exe" +3 0
CopyFiles "$INSTDIR\${APP_FILENAME}.exe" "$INSTDIR\bin\${APP_FILENAME}.exe"
Delete "$INSTDIR\${APP_FILENAME}.exe"
; 5. 移动依赖文件
; 清理旧文件
Delete "$INSTDIR\bin\*.dll"
Delete "$INSTDIR\bin\*.pak"
Delete "$INSTDIR\bin\*.bin"
Delete "$INSTDIR\bin\*.dat"
Delete "$INSTDIR\bin\*.json"
Delete "$INSTDIR\bin\LICENSE*"
Delete "$INSTDIR\bin\version"
Delete "$INSTDIR\bin\*.ico"
; 移动新文件
CopyFiles "$INSTDIR\*.dll" "$INSTDIR\bin"
Delete "$INSTDIR\*.dll"
CopyFiles "$INSTDIR\*.pak" "$INSTDIR\bin"
Delete "$INSTDIR\*.pak"
CopyFiles "$INSTDIR\*.bin" "$INSTDIR\bin"
Delete "$INSTDIR\*.bin"
CopyFiles "$INSTDIR\*.dat" "$INSTDIR\bin"
Delete "$INSTDIR\*.dat"
CopyFiles "$INSTDIR\*.json" "$INSTDIR\bin"
Delete "$INSTDIR\*.json"
CopyFiles "$INSTDIR\LICENSE*" "$INSTDIR\bin"
Delete "$INSTDIR\LICENSE*"
CopyFiles "$INSTDIR\version" "$INSTDIR\bin"
Delete "$INSTDIR\version"
CopyFiles "$INSTDIR\*.ico" "$INSTDIR\bin"
Delete "$INSTDIR\*.ico"
; 清理临时文件
Delete "$INSTDIR\config-template.ini"
!macroend
; ----------------------------------------------------------------------------
; [阶段 3] 卸载逻辑
; ----------------------------------------------------------------------------
!macro customUninstall
; 1. 暴力清理 bin
RMDir /r "$INSTDIR\bin"
; 2. 清理根目录残留 (添加 .exe)
Delete "$INSTDIR\${APP_FILENAME}.exe"
Delete "$INSTDIR\Uninstall ${PRODUCT_NAME}.exe"
Delete "$INSTDIR\Uninstall.exe"
; 3. 删除根目录
RMDir "$INSTDIR"
!macroend
; ----------------------------------------------------------------------------
; [阶段 4] 拦截默认快捷方式
; ----------------------------------------------------------------------------
!macro customCreateShortcut
; 留空,手动创建
!macroend
; ----------------------------------------------------------------------------
; [阶段 5] 安装后处理
; ----------------------------------------------------------------------------
Function .onInstSuccess
; 1. 移动卸载程序
Delete "$INSTDIR\bin\Uninstall.exe"
IfFileExists "$INSTDIR\Uninstall ${PRODUCT_NAME}.exe" 0 try_short_name
Rename "$INSTDIR\Uninstall ${PRODUCT_NAME}.exe" "$INSTDIR\bin\Uninstall.exe"
Goto reg_fix
try_short_name:
Rename "$INSTDIR\Uninstall.exe" "$INSTDIR\bin\Uninstall.exe"
reg_fix:
; 2. 修正注册表 (添加 .exe)
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_GUID}" "DisplayIcon" "$INSTDIR\bin\${APP_FILENAME}.exe"
WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${APP_GUID}" "UninstallString" "$INSTDIR\bin\Uninstall.exe"
; 3. 创建快捷方式
Delete "$DESKTOP\${PRODUCT_NAME}.lnk"
Delete "$SMPROGRAMS\${PRODUCT_NAME}.lnk"
SetOutPath "$INSTDIR\bin"
; [关键修复] 指向 bin 下的 .exe
CreateShortCut "$DESKTOP\${PRODUCT_NAME}.lnk" "$INSTDIR\bin\${APP_FILENAME}.exe"
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${PRODUCT_NAME}.lnk" "$INSTDIR\bin\${APP_FILENAME}.exe"
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\bin\Uninstall.exe"
FunctionEnd