## 构建、打包与分发系统重构 (build/) - 构建系统收敛至 build/ 目录:build.mjs + electron-builder.js + 6 个脚本 - electron-builder 配置从 package.json 提取为 build/electron-builder.js - 新增 --edition (personal/enterprise) 和 --channel (stable/beta/alpha) 参数 - OSS 路径按渠道隔离 - 新增 pre-build-check.mjs:Git/CHANGELOG/版本/环境变量校验 - 新增 bump-version.mjs:版本号管理 + CHANGELOG 自动生成 - updater.ts 发送 channel/edition/deviceFingerprint - 新增 src/shared/utils/rollout.ts 灰度测试工具 - 新增 10 条 NPM 脚本 ## VCHSM 五层架构迁移 - 7 个业务模块 + ~500 处导入路径同步 ## 修复 - MediaCardContextMenu 改用共享 ContextMenu 组件 - ComboBox 尺寸参数显示统一 (size-format.ts) ## 文档 - UpdateA.md / build/README.md / README.md / .env.example 更新 Co-Authored-By: Claude <noreply@anthropic.com>
432 lines
16 KiB
JavaScript
432 lines
16 KiB
JavaScript
// ============================================================
|
||
// build.mjs — 构建总管
|
||
//
|
||
// 用法:node build/build.mjs [选项]
|
||
//
|
||
// 平台(必选其一):
|
||
// --win Windows
|
||
// --mac macOS
|
||
// --linux Linux
|
||
//
|
||
// 版本 & 渠道:
|
||
// --edition <e> 版本类型:personal(默认)| enterprise
|
||
// --channel <c> 发布渠道:stable(默认)| beta | alpha
|
||
// --version <v> 覆盖 package.json 中的版本号
|
||
//
|
||
// 操作(可选组合,默认只执行 --build):
|
||
// --build 仅构建(默认)
|
||
// --clean 构建前清理 release/ dist/ dist-electron/
|
||
// --sign 代码签名
|
||
// --upload 上传安装包到 OSS
|
||
// --publish 发布版本信息到后端 API
|
||
// --all 全流程(clean + build + sign + upload + publish)
|
||
//
|
||
// 签名(仅 --sign 时可用):
|
||
// --cert <path> 证书文件路径 (.pfx)
|
||
// --cert-password <p> 证书密码
|
||
//
|
||
// 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|enterprise>', '版本类型(默认 personal)');
|
||
row('--channel <stable|beta|alpha>', '发布渠道(默认 stable)');
|
||
row('--version <x.y.z>', '覆盖 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 <path>', '证书文件路径(.pfx / .p12)');
|
||
row('--cert-password <p>', '证书密码');
|
||
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);
|
||
});
|