#!/usr/bin/env node /** * Tauri 构建脚本 * Build script for Claude AI Installer (Tauri) * * 用法 / Usage: * node scripts/build.js [options] * * 选项 / Options: * --debug, -d 调试模式构建 * --skip-frontend 跳过前端构建 * --help, -h 显示帮助信息 */ import { execSync } from 'child_process'; import path from 'path'; import fs from 'fs'; import { fileURLToPath } from 'url'; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); // 颜色输出 const colors = { reset: '\x1b[0m', bright: '\x1b[1m', red: '\x1b[31m', green: '\x1b[32m', yellow: '\x1b[33m', blue: '\x1b[34m', cyan: '\x1b[36m', }; const log = { info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`), success: (msg) => console.log(`${colors.green}✔${colors.reset} ${msg}`), warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`), error: (msg) => console.log(`${colors.red}✖${colors.reset} ${msg}`), step: (msg) => console.log(`\n${colors.cyan}${colors.bright}▶ ${msg}${colors.reset}`), }; // 项目路径 const PROJECT_ROOT = path.resolve(__dirname, '..'); const TAURI_DIR = path.join(PROJECT_ROOT, 'src-tauri'); const RELEASE_DIR = path.join(PROJECT_ROOT, 'release'); const LAUNCHER_DIR = path.join(PROJECT_ROOT, 'launcher'); // 解析命令行参数 function parseArgs() { const args = process.argv.slice(2); const options = { debug: false, skipFrontend: false, help: false, }; for (let i = 0; i < args.length; i++) { const arg = args[i]; switch (arg) { case '--debug': case '-d': options.debug = true; break; case '--skip-frontend': case '-s': options.skipFrontend = true; break; case '--help': case '-h': options.help = true; break; } } return options; } // 显示帮助信息 function showHelp() { console.log(` ${colors.bright}Claude AI Installer 构建脚本 (Tauri)${colors.reset} ${colors.cyan}用法:${colors.reset} node scripts/build.js [options] ${colors.cyan}选项:${colors.reset} --debug, -d 以调试模式构建 --skip-frontend, -s 跳过前端构建 --help, -h 显示此帮助信息 ${colors.cyan}示例:${colors.reset} node scripts/build.js # 完整发布构建 node scripts/build.js --debug # 调试构建 node scripts/build.js -s # 跳过前端,仅构建 Tauri ${colors.cyan}输出:${colors.reset} release/ # 最终发布目录 ├── *-nsis.exe # NSIS 安装程序 ├── *-portable.exe # 便携版 └── Claude-AI-Installer-x.x.x-portable/ # 带启动器的便携版 └── Claude-AI-Installer-x.x.x-portable.zip # 便携版压缩包 `); } // 执行命令 function exec(command, options = {}) { log.info(`执行: ${command}`); try { execSync(command, { stdio: 'inherit', cwd: PROJECT_ROOT, ...options, }); return true; } catch (error) { log.error(`命令执行失败: ${command}`); return false; } } // 获取版本号 function getVersion() { const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json'); const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8')); return tauriConf.version; } // 获取产品名称 function getProductName() { const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json'); const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8')); return tauriConf.productName || 'Claude AI Installer'; } // 清理旧的构建文件 function cleanBuild() { log.step('清理旧的构建文件...'); // 清空 release 目录 if (fs.existsSync(RELEASE_DIR)) { fs.rmSync(RELEASE_DIR, { recursive: true, force: true }); } fs.mkdirSync(RELEASE_DIR, { recursive: true }); log.success('清理完成'); } // 构建 Tauri 应用 function buildTauri(options) { log.step(`构建 Tauri 应用 (${options.debug ? '调试' : '发布'}模式)...`); let command = 'npm run tauri build'; if (options.debug) { command += ' -- --debug'; } if (!exec(command)) { throw new Error('Tauri 构建失败'); } log.success('Tauri 构建完成'); } // 获取 Tauri 生成的 exe 文件名(小写,空格转连字符) function getTauriExeName() { const productName = getProductName(); // Tauri 将产品名称转换为小写并用连字符替换空格 return productName.toLowerCase().replace(/\s+/g, '-') + '.exe'; } // 复制构建产物到 release 目录 function copyArtifacts(options) { log.step('复制构建产物到 release 目录...'); const mode = options.debug ? 'debug' : 'release'; const bundleDir = path.join(TAURI_DIR, 'target', mode, 'bundle'); const nsisDir = path.join(bundleDir, 'nsis'); // 复制 NSIS 安装程序 if (fs.existsSync(nsisDir)) { const files = fs.readdirSync(nsisDir); for (const file of files) { if (file.endsWith('.exe')) { const src = path.join(nsisDir, file); const dest = path.join(RELEASE_DIR, file); fs.copyFileSync(src, dest); log.info(`复制: ${file}`); } } } // 复制独立 exe (portable) const exeName = getTauriExeName(); const portableExe = path.join(TAURI_DIR, 'target', mode, exeName); if (fs.existsSync(portableExe)) { const version = getVersion(); const portableName = `Claude-AI-Installer_${version}_x64-portable.exe`; const dest = path.join(RELEASE_DIR, portableName); fs.copyFileSync(portableExe, dest); log.info(`复制便携版: ${portableName}`); } else { log.warn(`未找到便携版 exe: ${portableExe}`); } log.success('构建产物复制完成'); } // ==================== Portable 版本构建(带启动器)==================== /** * 查找 portable exe 文件 */ function findPortableExe() { const files = fs.readdirSync(RELEASE_DIR); return files.find(f => f.includes('portable') && f.endsWith('.exe')); } /** * 使用 pkg 打包启动器 */ function buildLauncher() { log.step('构建启动器...'); // 检查 launcher.js 是否存在 const launcherScript = path.join(LAUNCHER_DIR, 'launcher.js'); if (!fs.existsSync(launcherScript)) { log.warn('launcher.js 不存在,跳过启动器构建'); return null; } // 检查是否安装了 pkg try { execSync('npx pkg --version', { stdio: 'ignore' }); } catch { log.info('安装 pkg...'); execSync('npm install -g pkg', { stdio: 'inherit' }); } const outputPath = path.join(RELEASE_DIR, 'launcher.exe'); // 使用 pkg 打包 execSync(`npx pkg "${launcherScript}" --target node18-win-x64 --output "${outputPath}"`, { stdio: 'inherit', cwd: PROJECT_ROOT, }); log.success(`启动器构建完成: ${outputPath}`); return outputPath; } /** * 创建 Portable 目录结构 */ function createPortableStructure(portableExe, launcherExe) { const version = getVersion(); const portableDirName = `Claude-AI-Installer-${version}-portable`; const portableDir = path.join(RELEASE_DIR, portableDirName); const appDir = path.join(portableDir, 'app'); const updateDir = path.join(portableDir, 'update'); log.step('创建 Portable 目录结构...'); // 清理旧目录 if (fs.existsSync(portableDir)) { fs.rmSync(portableDir, { recursive: true }); } // 创建目录 fs.mkdirSync(appDir, { recursive: true }); fs.mkdirSync(updateDir, { recursive: true }); // 复制启动器 const launcherDest = path.join(portableDir, 'Claude-AI-Installer.exe'); fs.copyFileSync(launcherExe, launcherDest); log.info(`复制启动器: ${launcherDest}`); // 复制主程序到 app 目录 const portableExePath = path.join(RELEASE_DIR, portableExe); const appExeName = `Claude-AI-Installer-${version}.exe`; const appExeDest = path.join(appDir, appExeName); fs.copyFileSync(portableExePath, appExeDest); log.info(`复制主程序: ${appExeDest}`); // 创建 .gitkeep 文件保持 update 目录 fs.writeFileSync(path.join(updateDir, '.gitkeep'), ''); // 创建 README const readme = `Claude AI Installer - Portable 版本 使用方法: 1. 双击 Claude-AI-Installer.exe 启动程序 2. 程序会自动检查更新 3. 更新下载后,重启程序即可完成更新 目录说明: - Claude-AI-Installer.exe: 启动器 - app/: 主程序目录 - update/: 更新文件临时目录 版本: ${version} `; fs.writeFileSync(path.join(portableDir, 'README.txt'), readme, 'utf8'); log.success(`Portable 目录创建完成: ${portableDir}`); // 创建 zip 包 createPortableZip(portableDir, portableDirName); return portableDir; } /** * 创建 ZIP 压缩包 */ function createPortableZip(sourceDir, name) { const zipPath = path.join(RELEASE_DIR, `${name}.zip`); log.step('创建 ZIP 压缩包...'); // 使用 PowerShell 创建 zip const psCommand = `Compress-Archive -Path "${sourceDir}\\*" -DestinationPath "${zipPath}" -Force`; execSync(`powershell -Command "${psCommand}"`, { stdio: 'inherit' }); log.success(`ZIP 压缩包创建完成: ${zipPath}`); } /** * 构建 Portable 版本(带启动器) */ function buildPortableVersion() { log.step('构建 Portable 版本(带启动器)...'); // 查找 portable exe const portableExe = findPortableExe(); if (!portableExe) { log.warn('未找到 portable exe 文件,跳过带启动器的 Portable 版本构建'); return; } log.info(`找到 Portable 文件: ${portableExe}`); // 构建启动器 const launcherExe = buildLauncher(); if (!launcherExe) { log.warn('启动器构建失败,跳过带启动器的 Portable 版本构建'); return; } // 创建 Portable 目录结构 createPortableStructure(portableExe, launcherExe); // 清理临时的 launcher.exe fs.unlinkSync(launcherExe); log.success('Portable 版本构建完成'); } // 显示构建结果 function showResults() { log.step('构建结果:'); if (!fs.existsSync(RELEASE_DIR)) { log.warn('release 目录不存在'); return; } const files = fs.readdirSync(RELEASE_DIR); const packages = files.filter(f => { const ext = path.extname(f).toLowerCase(); return ['.exe', '.zip'].includes(ext); }); if (packages.length === 0) { log.warn('未找到打包文件'); return; } console.log(''); for (const pkg of packages) { const filePath = path.join(RELEASE_DIR, pkg); const stats = fs.statSync(filePath); const sizeMB = (stats.size / (1024 * 1024)).toFixed(2); console.log(` ${colors.green}•${colors.reset} ${pkg} (${sizeMB} MB)`); } console.log(''); log.info(`输出目录: ${RELEASE_DIR}`); } // 主函数 async function main() { const options = parseArgs(); if (options.help) { showHelp(); process.exit(0); } console.log(` ${colors.bright}╔════════════════════════════════════════════╗ ║ Claude AI Installer 构建脚本 ║ ║ (Tauri 2.0) ║ ╚════════════════════════════════════════════╝${colors.reset} `); log.info(`构建模式: ${options.debug ? '调试' : '发布'}`); log.info(`跳过前端: ${options.skipFrontend}`); const startTime = Date.now(); try { // 清理旧的构建文件 cleanBuild(); // 构建 Tauri 应用 buildTauri(options); // 复制构建产物 copyArtifacts(options); // 构建带启动器的 Portable 版本 buildPortableVersion(); // 显示结果 showResults(); const duration = ((Date.now() - startTime) / 1000).toFixed(1); log.success(`构建完成! 耗时: ${duration}s`); } catch (error) { log.error(error.message); process.exit(1); } } main();