#!/usr/bin/env node // Windows 外壳一键构建 (开发用): 安装包 (NSIS) + 便携版 (portable zip) // // 用法: // npm run build:win # 完整流程 // node scripts/build-windows.mjs --skip-check # 跳过版本一致性校验 // node scripts/build-windows.mjs --debug # debug 构建 (快速验证, 产物带 -debug 后缀) // // 流程: // 1) 版本一致性校验 (scripts/ci/check-version.mjs, 外壳三件套) // 2) 前端构建 (build:shell, 注入 VITE_API_BASE 指向远端后端) // 3) tauri build --bundles nsis → 编译一次 release exe + NSIS 安装包 // 4) 复用同一个 exe 组装便携版目录并压成 zip (不二次编译) // // 产物 (artifacts/windows/): // iBoard__x64-setup.exe 安装包 (当前用户安装, 免管理员) // iBoard__x64-portable.zip 便携版 (解压即用) // portable/ 便携版解压目录 (iBoard.exe + README.txt) // // 说明: 便携版不内置 WebView2 —— 目标机需 Win10 19041+ / Win11 (系统自带 WebView2 Runtime)。 import { spawnSync } from 'node:child_process' import fs from 'node:fs' import path from 'node:path' import { fileURLToPath } from 'node:url' const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') const args = process.argv.slice(2) const skipCheck = args.includes('--skip-check') const isDebug = args.includes('--debug') const profile = isDebug ? 'debug' : 'release' const conf = JSON.parse(fs.readFileSync(path.join(root, 'src-tauri', 'tauri.conf.json'), 'utf8')) const version = String(conf.version || '0.0.0') const product = conf.productName || 'iBoard' const suffix = isDebug ? '-debug' : '' const artifactsDir = path.join(root, 'artifacts', 'windows') const targetDir = path.join(root, 'src-tauri', 'target', profile) function log(msg) { console.log(`\x1b[36m[build-win]\x1b[0m ${msg}`) } function fail(msg) { console.error(`\x1b[31m[build-win] 失败: ${msg}\x1b[0m`) process.exit(1) } function run(cmd, cmdArgs, opts = {}) { log(`$ ${cmd} ${cmdArgs.join(' ')}`) const res = spawnSync(cmd, cmdArgs, { cwd: root, stdio: 'inherit', shell: process.platform === 'win32', ...opts, }) if (res.status !== 0) fail(`${cmd} ${cmdArgs.join(' ')} 退出码 ${res.status}`) } // ---------- 0. 前置检查 ---------- if (process.platform !== 'win32') fail('本脚本仅支持 Windows') if (!fs.existsSync(path.join(root, 'node_modules'))) fail('缺少 node_modules, 请先运行 npm ci') log(`项目 ${product} 外壳版本 ${version} (${profile})`) // ---------- 1. 版本一致性 ---------- if (!skipCheck) { log('校验版本一致性 (外壳 tauri.conf.json / Cargo.toml / Cargo.lock)') run('node', ['scripts/ci/check-version.mjs'], { env: { ...process.env, BRANCH: process.env.BRANCH || '' }, }) } else { log('跳过版本一致性校验 (--skip-check)') } // ---------- 2. 前端构建 ---------- log('构建前端 (build:shell, VITE_API_BASE 指向远端后端)') run('npm', ['run', 'build:shell']) // ---------- 3. 安装包 (NSIS) ---------- const tauriArgs = ['tauri', 'build'] if (isDebug) tauriArgs.push('--debug') // 只出 NSIS: 避免 bundle.targets="all" 在 Windows 上拉 WiX 打 MSI tauriArgs.push('--bundles', 'nsis') log('编译外壳并生成 NSIS 安装包') run('npx', tauriArgs) // ---------- 4. 便携版 ---------- const exePath = path.join(targetDir, `iboard${suffix}.exe`) if (!fs.existsSync(exePath)) fail(`未找到构建产物 ${exePath}`) fs.rmSync(artifactsDir, { recursive: true, force: true }) fs.mkdirSync(artifactsDir, { recursive: true }) const portableDir = path.join(artifactsDir, 'portable') fs.mkdirSync(portableDir, { recursive: true }) const portableExe = path.join(portableDir, `${product}.exe`) fs.copyFileSync(exePath, portableExe) const readme = `${product} ${version} 便携版 (Windows x64) ================================================= 免安装: 双击 ${product}.exe 即可运行。 系统要求 - Windows 10 19041+ 或 Windows 11 (x64) - WebView2 Runtime: Win10/11 系统自带; 若提示缺失, 请安装 https://developer.microsoft.com/microsoft-edge/webview2/ 使用说明 - 关闭窗口 = 隐藏到托盘, 不会退出; 退出请用托盘右键菜单「退出」。 - 窗口贴屏幕边缘会自动收起为半圆悬浮球, 鼠标停留或单击唤回。 - 托盘菜单可开关「开机启动」「边缘吸附」。 - 配置保存在 %APPDATA%\\com.iboard.app\\shell-config.json (删除即恢复默认)。 注意: 便携版不写注册表、不装服务; 但若开启了「开机启动」, 会在 HKCU\\...\\Run 写入自启动项, 关闭开关即可移除。 ` fs.writeFileSync(path.join(portableDir, 'README.txt'), readme, 'utf8') const zipName = `${product}_${version}_x64-portable${suffix}.zip` const zipPath = path.join(artifactsDir, zipName) log('打包便携版 zip') const psZip = `Compress-Archive -Path '${portableDir}\\*' -DestinationPath '${zipPath}' -Force` const res = spawnSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', psZip], { cwd: root, stdio: 'inherit', }) if (res.status !== 0) fail('压缩便携版失败 (Compress-Archive)') // ---------- 5. 汇总 ---------- const setupExe = path.join( targetDir, 'bundle', 'nsis', `${product}_${version}_x64-setup${suffix}.exe`, ) const outputs = [] if (fs.existsSync(setupExe)) { const dest = path.join(artifactsDir, path.basename(setupExe)) fs.copyFileSync(setupExe, dest) outputs.push(['安装包 (NSIS)', dest]) } else { outputs.push(['安装包 (NSIS)', `未找到: ${setupExe}`]) } outputs.push(['便携版 (zip)', zipPath]) outputs.push(['便携版 (目录)', `${portableDir} (${fs.readdirSync(portableDir).length} 个文件)`]) log('构建完成:') for (const [label, p] of outputs) { const size = fs.existsSync(p) ? `${(fs.statSync(p).size / 1024 / 1024).toFixed(1)} MB` : '' console.log(` ${label.padEnd(14)} ${p}${size ? ' (' + size + ')' : ''}`) } process.exit(0)