// ============================================================ // build.mjs — 构建总管 // // 用法:node build/build.mjs [选项] // // 平台(必选其一): // --win Windows // --mac macOS // --linux Linux // // 版本 & 渠道: // --edition 版本类型:personal(默认)| enterprise // --channel 发布渠道:stable(默认)| beta | alpha // --version 覆盖 package.json 中的版本号 // // 操作(可选组合,默认只执行 --build): // --build 仅构建(默认) // --clean 构建前清理 release/ dist/ dist-electron/ // --sign 代码签名 // --upload 上传安装包到 OSS // --publish 发布版本信息到后端 API // --all 全流程(clean + build + sign + upload + publish) // // 签名(仅 --sign 时可用): // --cert 证书文件路径 (.pfx) // --cert-password

证书密码 // // OSS 路径规则: // Stable → releases/{version}/ (向后兼容,无渠道后缀) // Beta → releases/beta/{version}/ (独立生命周期,可设自动过期策略) // Alpha → releases/{version}/alpha/ (版本优先) // // 示例: // node build/build.mjs --win // node build/build.mjs --win --channel beta --upload --publish // node build/build.mjs --win --edition enterprise --all // node build/build.mjs --win --sign --cert ./cert.pfx --cert-password xxx // ============================================================ import { spawn, execSync } from 'node:child_process'; import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs'; import { join, dirname } from 'node:path'; import { fileURLToPath } from 'node:url'; const __dirname = dirname(fileURLToPath(import.meta.url)); const ROOT = join(__dirname, '..'); const SCRIPTS = join(__dirname, 'scripts'); // ANSI 颜色 const c = (code, text) => `\x1b[${code}m${text}\x1b[0m`; const dim = (t) => c(2, t); const bold = (t) => c(1, t); const cyan = (t) => c(36, t); const green = (t) => c(32, t); const yellow = (t) => c(33, t); // ---------- 参数解析 ---------- const args = process.argv.slice(2); const opts = { win: false, mac: false, linux: false, clean: false, build: false, sign: false, upload: false, publish: false, cert: '', certPassword: '', edition: 'personal', channel: 'stable', version: '', }; let i = 0; while (i < args.length) { const a = args[i]; switch (a) { case '--win': opts.win = true; break; case '--mac': opts.mac = true; break; case '--linux': opts.linux = true; break; case '--clean': opts.clean = true; break; case '--build': opts.build = true; break; case '--sign': opts.sign = true; break; case '--upload': opts.upload = true; break; case '--publish': opts.publish = true; break; case '--all': opts.clean = opts.build = opts.sign = opts.upload = opts.publish = true; break; case '--cert': opts.cert = args[++i] || ''; break; case '--cert-password': opts.certPassword = args[++i] || ''; break; case '--edition': opts.edition = args[++i] || 'personal'; break; case '--channel': opts.channel = args[++i] || 'stable'; break; case '--version': opts.version = args[++i] || ''; break; case '--help': // fallthrough case '-h': printHelp(); process.exit(0); break; default: break; } i++; } // ---------- 校验 ---------- const platformCount = [opts.win, opts.mac, opts.linux].filter(Boolean).length; if (platformCount !== 1) { console.error('错误: 请指定且仅指定一个平台(--win / --mac / --linux)'); process.exit(1); } if (!['personal', 'enterprise'].includes(opts.edition)) { console.error('错误: --edition 必须是 personal 或 enterprise'); process.exit(1); } if (!['stable', 'beta', 'alpha'].includes(opts.channel)) { console.error('错误: --channel 必须是 stable、beta 或 alpha'); process.exit(1); } // 默认执行 build if (!opts.clean && !opts.build && !opts.sign && !opts.upload && !opts.publish) { opts.build = true; } const platform = opts.win ? 'win32' : opts.mac ? 'darwin' : 'linux'; const pkg = JSON.parse(readFileSync(join(ROOT, 'package.json'), 'utf-8')); const version = opts.version || pkg.version; // ---------- Git 元数据 ---------- let gitCommit = ''; let gitBranch = ''; try { gitCommit = execSync('git rev-parse --short HEAD', { cwd: ROOT, encoding: 'utf-8' }).trim(); gitBranch = execSync('git rev-parse --abbrev-ref HEAD', { cwd: ROOT, encoding: 'utf-8' }).trim(); } catch { // 非 git 环境 — 留空 } const buildTimestamp = new Date().toISOString(); // ---------- OSS 路径计算 ---------- /** * OSS 对象路径前缀: * - Beta: releases/beta/{version}/ (独立生命周期,可设 OSS 自动过期策略) * - Alpha: releases/{version}/alpha/ (方案 B:版本优先) * - Stable: releases/{version}/ (无渠道后缀,向后兼容现有布局) */ function getOssPrefix() { if (opts.channel === 'beta') return `releases/beta/${version}`; if (opts.channel === 'alpha') return `releases/${version}/alpha`; return `releases/${version}`; } // electron-builder 输出目录名(与 OSS 路径解耦,加渠道后缀防冲突) function getReleaseDirName() { if (opts.channel === 'stable') return version; return `${version}-${opts.channel}`; } // ---------- 工具函数 ---------- function run(cmd, args, runOpts = {}) { return new Promise((resolve, reject) => { const child = spawn(cmd, args, { cwd: ROOT, stdio: 'inherit', shell: true, ...runOpts, env: { ...process.env, ...(runOpts.env || {}) }, }); child.on('close', (code) => { if (code === 0) resolve(); else reject(new Error(`${cmd} ${args.join(' ')} 退出码: ${code}`)); }); child.on('error', reject); }); } function runScript(name, ...scriptArgs) { const scriptPath = join(SCRIPTS, `${name}.mjs`); return run('node', [scriptPath, ...scriptArgs]); } function step(title) { console.log(`\n${'='.repeat(60)}`); console.log(` ${title}`); console.log(`${'='.repeat(60)}\n`); } // ---------- 主流程 ---------- async function main() { const electronBuilderPlatform = opts.win ? '--win' : opts.mac ? '--mac' : '--linux'; const editionTag = opts.edition === 'enterprise' ? '企业版' : '个人版'; const channelTag = opts.channel === 'stable' ? '' : ` · ${opts.channel} 渠道`; const ossPrefix = getOssPrefix(); const releaseDirName = getReleaseDirName(); console.log('┌──────────────────────────────────────────────┐'); console.log(`│ HeiXiu 构建 v${version} ${platform} ${editionTag}`.padEnd(51) + '│'); if (channelTag) { console.log(`│ ${channelTag.trim()}`.padEnd(51) + '│'); } if (gitCommit) { console.log(`│ git: ${gitBranch}@${gitCommit}`.padEnd(51) + '│'); } console.log('└──────────────────────────────────────────────┘'); console.log(`操作: ${[ opts.clean && '清理', opts.build && '构建', opts.sign && '签名', opts.upload && '上传', opts.publish && '发布', ].filter(Boolean).join(' → ')}`); console.log(`本地产物: release/${releaseDirName}/`); console.log(`OSS 路径: ${ossPrefix}/`); // ── Step 0: 构建前校验 ── if (opts.build) { step('0/ 构建前校验'); try { await runScript('pre-build-check', platform, version, opts.channel); } catch (err) { console.error(`\n❌ 构建前校验失败: ${err.message}`); console.error('如需跳过校验,请使用 --skip-check 参数(或注释 build.mjs 中对应行)'); process.exit(1); } } // ── Step 1: 清理 ── if (opts.clean) { step('1/ 清理构建产物'); await runScript('clean'); } // ── Step 2: 构建 ── if (opts.build) { step('2/ 构建应用'); // 签名环境变量 const signEnv = {}; if (opts.sign) { if (opts.cert) signEnv.CSC_LINK = opts.cert; if (opts.certPassword) signEnv.CSC_KEY_PASSWORD = opts.certPassword; console.log(opts.cert ? `签名证书: ${opts.cert}` : '签名: 使用系统证书存储'); } // 写入构建元数据 const metaPath = join(ROOT, 'release', '.build-meta.json'); const metaDir = join(ROOT, 'release'); if (!existsSync(metaDir)) mkdirSync(metaDir, { recursive: true }); writeFileSync(metaPath, JSON.stringify({ version, platform, edition: opts.edition, channel: opts.channel, gitCommit, gitBranch, buildTimestamp, ossPrefix, releaseDirName, }, null, 2), 'utf-8'); // 2a. TypeScript 编译 console.log('[tsc] 类型检查...'); await run('npx', ['tsc']); // 2b. Vite 构建 + 注入构建元数据 console.log('[vite] 打包前端 + Electron...'); await run('npx', ['vite', 'build'], { env: { VITE_BUILD_VERSION: version, VITE_BUILD_CHANNEL: opts.channel, VITE_BUILD_EDITION: opts.edition, VITE_BUILD_COMMIT: gitCommit, VITE_BUILD_TIMESTAMP: buildTimestamp, }, }); // 2c. electron-builder 打包 console.log(`[electron-builder] ${electronBuilderPlatform} 打包 (${editionTag}${channelTag})...`); await run('npx', [ 'electron-builder', electronBuilderPlatform, '--config', 'build/electron-builder.js', ], { env: { ...signEnv, HEIXIU_EDITION: opts.edition, HEIXIU_CHANNEL: opts.channel, HEIXIU_RELEASE_DIR_NAME: releaseDirName, }, }); // 2d. 生成 update-info.json console.log('[info] 生成 update-info.json...'); await runScript('generate-update-info', platform, version, opts.channel, opts.edition, ossPrefix, releaseDirName); } // ── Step 3: 上传 OSS ── if (opts.upload) { step('3/ 上传 OSS'); await runScript('upload-oss', platform, version, opts.channel, ossPrefix, releaseDirName); } // ── Step 4: 发布到 API ── if (opts.publish) { step('4/ 发布版本到 API'); await runScript('publish-release', platform, version, opts.channel, opts.edition, ossPrefix, releaseDirName); } console.log('\n✅ 全部完成'); // 构建元数据摘要 console.log(dim(`\n构建摘要:`)); console.log(dim(` 版本: ${version}`)); console.log(dim(` 平台: ${platform}`)); console.log(dim(` 版本类型: ${opts.edition}`)); console.log(dim(` 渠道: ${opts.channel}`)); console.log(dim(` OSS: ${ossPrefix}/`)); if (gitCommit) console.log(dim(` Commit: ${gitCommit} (${gitBranch})`)); console.log(dim(` 时间: ${buildTimestamp}`)); } // ---------- 帮助 ---------- function printHelp() { const box = (title) => { const W = 62; const pad = Math.max(0, W - 4 - title.length); console.log(`\n${cyan('┌')}${cyan('─'.repeat(W - 2))}${cyan('┐')}`); console.log(`${cyan('│')} ${bold(title)}${' '.repeat(pad)}${cyan('│')}`); console.log(`${cyan('└')}${cyan('─'.repeat(W - 2))}${cyan('┘')}`); }; const row = (flag, desc) => { console.log(` ${green(flag.padEnd(24))}${dim(desc)}`); }; const hdr = (t) => console.log(`\n ${bold(yellow(t))}`); console.log(''); console.log(cyan(' ╔') + cyan('═══════════════════════════════════════════════════') + cyan('╗')); console.log(cyan(' ║') + bold(' 船长·HeiXiu — 构建工具') + ' ' + cyan('║')); console.log(cyan(' ╚') + cyan('═══════════════════════════════════════════════════') + cyan('╝')); box('用法'); console.log(` ${bold('node build/build.mjs')} ${dim('<平台> [选项...]')}`); console.log(` ${bold('npm run build:win')} ${dim('— 等效快捷命令')}`); hdr('平台(必选其一)'); row('--win', 'Windows'); row('--mac', 'macOS'); row('--linux', 'Linux'); hdr('版本 & 渠道'); row('--edition ', '版本类型(默认 personal)'); row('--channel ', '发布渠道(默认 stable)'); row('--version ', '覆盖 package.json 版本号'); hdr('操作(可选组合,不指定则默认 --build)'); row('--build', '构建应用(校验 → tsc → vite → electron-builder → info)'); row('--clean', '构建前清理 release/ dist/ dist-electron/'); row('--sign', '代码签名(需配合 --cert 或环境变量)'); row('--upload', '上传安装包 + blockmap 到 OSS'); row('--publish', 'POST 版本信息到后端 API'); row('--all', '全流程 = clean + build + sign + upload + publish'); hdr('签名参数(配合 --sign)'); row('--cert ', '证书文件路径(.pfx / .p12)'); row('--cert-password

', '证书密码'); console.log(` ${dim('也可通过 CSC_LINK / CSC_KEY_PASSWORD 环境变量设置')}`); hdr('灰度发布示例'); console.log(` ${dim('# Beta 渠道打包 + 上传 + 发布')}`); console.log(` ${bold('node build/build.mjs --win --channel beta --upload --publish')}`); console.log(` ${dim('# Alpha 渠道仅打包测试')}`); console.log(` ${bold('node build/build.mjs --win --channel alpha')}`); console.log(` ${dim('# 企业版全流程')}`); console.log(` ${bold('node build/build.mjs --win --edition enterprise --all')}`); box('OSS 路径规则'); console.log(` ${yellow('Stable')} ${dim('→ releases/{version}/ (向后兼容)')}`); console.log(` ${yellow('Beta')} ${dim('→ releases/beta/{version}/ (独立生命周期)')}`); console.log(` ${yellow('Alpha')} ${dim('→ releases/{version}/alpha/ (版本优先)')}`); box('npm 快捷命令'); console.log(` ${bold('npm run build:win')} ${dim('→ node build/build.mjs --win')}`); console.log(` ${bold('npm run build:win:enterprise')} ${dim('→ node build/build.mjs --win --edition enterprise')}`); console.log(` ${bold('npm run build:win:beta')} ${dim('→ node build/build.mjs --win --channel beta')}`); console.log(` ${bold('npm run build:mac')} ${dim('→ node build/build.mjs --mac')}`); console.log(` ${bold('npm run build:linux')} ${dim('→ node build/build.mjs --linux')}`); console.log(` ${bold('npm run build:clean')} ${dim('→ node build/scripts/clean.mjs')}`); console.log(` ${bold('npm run version:patch')} ${dim('→ node build/scripts/bump-version.mjs patch')}`); box('环境变量'); console.log(` ${yellow('UPDATE_API_URL')} ${dim('版本检查 API 基地址')}`); console.log(` ${yellow('UPDATE_API_KEY')} ${dim('发布 API 鉴权 Key')}`); console.log(` ${yellow('UPDATE_DOWNLOAD_BASE')} ${dim('OSS 下载基地址')}`); console.log(` ${yellow('OSS_ENDPOINT')} ${dim('阿里云 OSS endpoint')}`); console.log(` ${yellow('OSS_BUCKET')} ${dim('阿里云 OSS bucket')}`); console.log(` ${yellow('OSS_ACCESS_KEY_ID')} ${dim('阿里云 AccessKey ID')}`); console.log(` ${yellow('OSS_ACCESS_KEY_SECRET')} ${dim('阿里云 AccessKey Secret')}`); console.log(` ${yellow('CSC_LINK')} ${dim('签名证书路径')}`); console.log(` ${yellow('CSC_KEY_PASSWORD')} ${dim('签名证书密码')}`); console.log(''); } // ---------- 入口 ---------- main().catch((err) => { console.error(`\n❌ 构建失败: ${err.message}`); process.exit(1); });