build.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468
  1. #!/usr/bin/env node
  2. /**
  3. * 多平台构建脚本
  4. * Multi-platform build script for Claude AI Installer
  5. *
  6. * 用法 / Usage:
  7. * node scripts/build.js [options]
  8. *
  9. * 选项 / Options:
  10. * --platform, -p 目标平台: win, mac, linux, all (默认: 当前平台)
  11. * --arch, -a 架构: x64, arm64, all (默认: 当前架构)
  12. * --publish, -P 发布模式: always, onTag, never (默认: never)
  13. * --skip-build 跳过前端构建,仅打包
  14. * --skip-typecheck 跳过 TypeScript 类型检查
  15. * --help, -h 显示帮助信息
  16. */
  17. const { execSync } = require('child_process');
  18. const path = require('path');
  19. const fs = require('fs');
  20. // 颜色输出
  21. const colors = {
  22. reset: '\x1b[0m',
  23. bright: '\x1b[1m',
  24. red: '\x1b[31m',
  25. green: '\x1b[32m',
  26. yellow: '\x1b[33m',
  27. blue: '\x1b[34m',
  28. cyan: '\x1b[36m',
  29. };
  30. const log = {
  31. info: (msg) => console.log(`${colors.blue}ℹ${colors.reset} ${msg}`),
  32. success: (msg) => console.log(`${colors.green}✔${colors.reset} ${msg}`),
  33. warn: (msg) => console.log(`${colors.yellow}⚠${colors.reset} ${msg}`),
  34. error: (msg) => console.log(`${colors.red}✖${colors.reset} ${msg}`),
  35. step: (msg) => console.log(`\n${colors.cyan}${colors.bright}▶ ${msg}${colors.reset}`),
  36. };
  37. // 解析命令行参数
  38. function parseArgs() {
  39. const args = process.argv.slice(2);
  40. const options = {
  41. platform: process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux',
  42. arch: process.arch,
  43. publish: 'never',
  44. skipBuild: false,
  45. skipTypecheck: false,
  46. help: false,
  47. };
  48. for (let i = 0; i < args.length; i++) {
  49. const arg = args[i];
  50. switch (arg) {
  51. case '--platform':
  52. case '-p':
  53. options.platform = args[++i];
  54. break;
  55. case '--arch':
  56. case '-a':
  57. options.arch = args[++i];
  58. break;
  59. case '--publish':
  60. case '-P':
  61. options.publish = args[++i];
  62. break;
  63. case '--skip-build':
  64. case '-s':
  65. options.skipBuild = true;
  66. break;
  67. case '--skip-typecheck':
  68. options.skipTypecheck = true;
  69. break;
  70. case '--help':
  71. case '-h':
  72. options.help = true;
  73. break;
  74. }
  75. }
  76. return options;
  77. }
  78. // 显示帮助信息
  79. function showHelp() {
  80. console.log(`
  81. ${colors.bright}Claude AI Installer 构建脚本${colors.reset}
  82. ${colors.cyan}用法:${colors.reset}
  83. node scripts/build.js [options]
  84. ${colors.cyan}选项:${colors.reset}
  85. --platform, -p <platform> 目标平台
  86. win - Windows (NSIS 安装包)
  87. mac - macOS (DMG)
  88. linux - Linux (AppImage)
  89. all - 所有平台
  90. 默认: 当前平台
  91. --arch, -a <arch> 目标架构
  92. x64 - 64位 Intel/AMD
  93. arm64 - ARM64 (Apple Silicon, etc.)
  94. all - 所有架构
  95. 默认: 当前架构
  96. --publish, -P <mode> 发布模式
  97. always - 总是发布
  98. onTag - 仅在 Git tag 时发布
  99. never - 不发布 (默认)
  100. --skip-build, -s 跳过前端构建,仅执行打包
  101. --skip-typecheck 跳过 TypeScript 类型检查
  102. --help, -h 显示此帮助信息
  103. ${colors.cyan}示例:${colors.reset}
  104. node scripts/build.js # 构建当前平台
  105. node scripts/build.js -p win # 构建 Windows 版本
  106. node scripts/build.js -p mac -a arm64 # 构建 macOS ARM64 版本
  107. node scripts/build.js -p all # 构建所有平台
  108. node scripts/build.js -p win -P always # 构建并发布 Windows 版本
  109. `);
  110. }
  111. // 执行命令
  112. function exec(command, options = {}) {
  113. log.info(`执行: ${command}`);
  114. try {
  115. execSync(command, {
  116. stdio: 'inherit',
  117. cwd: path.resolve(__dirname, '..'),
  118. ...options,
  119. });
  120. return true;
  121. } catch (error) {
  122. log.error(`命令执行失败: ${command}`);
  123. return false;
  124. }
  125. }
  126. // 获取 electron-builder 平台参数
  127. function getPlatformArgs(platform, arch) {
  128. const platformMap = {
  129. win: '--win',
  130. mac: '--mac',
  131. linux: '--linux',
  132. };
  133. const archMap = {
  134. x64: '--x64',
  135. arm64: '--arm64',
  136. all: '--x64 --arm64',
  137. };
  138. let args = [];
  139. if (platform === 'all') {
  140. args.push('--win', '--mac', '--linux');
  141. } else {
  142. args.push(platformMap[platform] || platformMap.win);
  143. }
  144. if (arch && arch !== 'all') {
  145. args.push(archMap[arch] || '');
  146. } else if (arch === 'all') {
  147. args.push('--x64', '--arm64');
  148. }
  149. return args.join(' ');
  150. }
  151. // 清理旧的构建文件
  152. function cleanBuild() {
  153. log.step('清理旧的构建文件...');
  154. const dirsToClean = ['dist', 'dist-electron'];
  155. const projectRoot = path.resolve(__dirname, '..');
  156. for (const dir of dirsToClean) {
  157. const fullPath = path.join(projectRoot, dir);
  158. if (fs.existsSync(fullPath)) {
  159. fs.rmSync(fullPath, { recursive: true, force: true });
  160. log.info(`已删除: ${dir}`);
  161. }
  162. }
  163. // 清空 release 目录内容,但保留目录本身
  164. const releaseDir = path.join(projectRoot, 'release');
  165. if (fs.existsSync(releaseDir)) {
  166. const files = fs.readdirSync(releaseDir);
  167. for (const file of files) {
  168. const filePath = path.join(releaseDir, file);
  169. fs.rmSync(filePath, { recursive: true, force: true });
  170. }
  171. log.info('已清空: release');
  172. }
  173. log.success('清理完成');
  174. }
  175. // 类型检查
  176. function typeCheck() {
  177. log.step('执行类型检查...');
  178. if (!exec('npx vue-tsc --noEmit')) {
  179. throw new Error('类型检查失败');
  180. }
  181. log.success('类型检查通过');
  182. }
  183. // 构建前端
  184. function buildFrontend() {
  185. log.step('构建前端资源...');
  186. if (!exec('npx vite build')) {
  187. throw new Error('前端构建失败');
  188. }
  189. log.success('前端构建完成');
  190. }
  191. // 打包 Electron 应用
  192. function packageApp(options) {
  193. log.step(`打包应用 (平台: ${options.platform}, 架构: ${options.arch})...`);
  194. const platformArgs = getPlatformArgs(options.platform, options.arch);
  195. const publishArg = options.publish !== 'never' ? `--publish ${options.publish}` : '';
  196. const command = `npx electron-builder ${platformArgs} ${publishArg}`.trim();
  197. if (!exec(command)) {
  198. throw new Error('应用打包失败');
  199. }
  200. log.success('应用打包完成');
  201. }
  202. // 显示构建结果
  203. function showResults() {
  204. log.step('构建结果:');
  205. const releaseDir = path.resolve(__dirname, '..', 'release');
  206. if (!fs.existsSync(releaseDir)) {
  207. log.warn('release 目录不存在');
  208. return;
  209. }
  210. const files = fs.readdirSync(releaseDir);
  211. const packages = files.filter(f => {
  212. const ext = path.extname(f).toLowerCase();
  213. return ['.exe', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext);
  214. });
  215. if (packages.length === 0) {
  216. log.warn('未找到打包文件');
  217. return;
  218. }
  219. console.log('');
  220. for (const pkg of packages) {
  221. const filePath = path.join(releaseDir, pkg);
  222. const stats = fs.statSync(filePath);
  223. const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
  224. console.log(` ${colors.green}•${colors.reset} ${pkg} (${sizeMB} MB)`);
  225. }
  226. console.log('');
  227. log.info(`输出目录: ${releaseDir}`);
  228. }
  229. // ==================== Portable 版本构建 ====================
  230. const LAUNCHER_DIR = path.resolve(__dirname, '..', 'launcher');
  231. const RELEASE_DIR = path.resolve(__dirname, '..', 'release');
  232. /**
  233. * 查找 portable exe 文件
  234. */
  235. function findPortableExe() {
  236. const files = fs.readdirSync(RELEASE_DIR);
  237. return files.find(f => f.includes('portable') && f.endsWith('.exe'));
  238. }
  239. /**
  240. * 使用 pkg 打包启动器
  241. */
  242. function buildLauncher() {
  243. log.step('构建启动器...');
  244. // 检查是否安装了 pkg
  245. try {
  246. execSync('npx pkg --version', { stdio: 'ignore' });
  247. } catch {
  248. log.info('安装 pkg...');
  249. execSync('npm install -g pkg', { stdio: 'inherit' });
  250. }
  251. const launcherScript = path.join(LAUNCHER_DIR, 'launcher.js');
  252. const outputPath = path.join(RELEASE_DIR, 'launcher.exe');
  253. // 使用 pkg 打包
  254. execSync(`npx pkg "${launcherScript}" --target node18-win-x64 --output "${outputPath}"`, {
  255. stdio: 'inherit',
  256. cwd: path.resolve(__dirname, '..'),
  257. });
  258. log.success(`启动器构建完成: ${outputPath}`);
  259. return outputPath;
  260. }
  261. /**
  262. * 创建 Portable 目录结构
  263. */
  264. function createPortableStructure(portableExe, launcherExe) {
  265. const packageJson = require(path.resolve(__dirname, '..', 'package.json'));
  266. const version = packageJson.version;
  267. const portableDirName = `Claude-AI-Installer-${version}-portable`;
  268. const portableDir = path.join(RELEASE_DIR, portableDirName);
  269. const appDir = path.join(portableDir, 'app');
  270. const updateDir = path.join(portableDir, 'update');
  271. log.step('创建 Portable 目录结构...');
  272. // 清理旧目录
  273. if (fs.existsSync(portableDir)) {
  274. fs.rmSync(portableDir, { recursive: true });
  275. }
  276. // 创建目录
  277. fs.mkdirSync(appDir, { recursive: true });
  278. fs.mkdirSync(updateDir, { recursive: true });
  279. // 复制启动器
  280. const launcherDest = path.join(portableDir, 'Claude-AI-Installer.exe');
  281. fs.copyFileSync(launcherExe, launcherDest);
  282. log.info(`复制启动器: ${launcherDest}`);
  283. // 复制主程序到 app 目录
  284. const portableExePath = path.join(RELEASE_DIR, portableExe);
  285. const appExeName = `Claude-AI-Installer-${version}.exe`;
  286. const appExeDest = path.join(appDir, appExeName);
  287. fs.copyFileSync(portableExePath, appExeDest);
  288. log.info(`复制主程序: ${appExeDest}`);
  289. // 创建 .gitkeep 文件保持 update 目录
  290. fs.writeFileSync(path.join(updateDir, '.gitkeep'), '');
  291. // 创建 README
  292. const readme = `Claude AI Installer - Portable 版本
  293. 使用方法:
  294. 1. 双击 Claude-AI-Installer.exe 启动程序
  295. 2. 程序会自动检查更新
  296. 3. 更新下载后,重启程序即可完成更新
  297. 目录说明:
  298. - Claude-AI-Installer.exe: 启动器
  299. - app/: 主程序目录
  300. - update/: 更新文件临时目录
  301. 版本: ${version}
  302. `;
  303. fs.writeFileSync(path.join(portableDir, 'README.txt'), readme, 'utf8');
  304. log.success(`Portable 目录创建完成: ${portableDir}`);
  305. // 创建 zip 包
  306. createPortableZip(portableDir, portableDirName);
  307. return portableDir;
  308. }
  309. /**
  310. * 创建 ZIP 压缩包
  311. */
  312. function createPortableZip(sourceDir, name) {
  313. const zipPath = path.join(RELEASE_DIR, `${name}.zip`);
  314. log.step('创建 ZIP 压缩包...');
  315. // 使用 PowerShell 创建 zip
  316. const psCommand = `Compress-Archive -Path "${sourceDir}\\*" -DestinationPath "${zipPath}" -Force`;
  317. execSync(`powershell -Command "${psCommand}"`, { stdio: 'inherit' });
  318. log.success(`ZIP 压缩包创建完成: ${zipPath}`);
  319. }
  320. /**
  321. * 构建 Portable 版本(带启动器)
  322. */
  323. function buildPortableVersion() {
  324. log.step('构建 Portable 版本(带启动器)...');
  325. // 查找 portable exe
  326. const portableExe = findPortableExe();
  327. if (!portableExe) {
  328. throw new Error('未找到 portable exe 文件');
  329. }
  330. log.info(`找到 Portable 文件: ${portableExe}`);
  331. // 构建启动器
  332. const launcherExe = buildLauncher();
  333. // 创建 Portable 目录结构
  334. createPortableStructure(portableExe, launcherExe);
  335. log.success('Portable 版本构建完成');
  336. }
  337. // 主函数
  338. async function main() {
  339. const options = parseArgs();
  340. if (options.help) {
  341. showHelp();
  342. process.exit(0);
  343. }
  344. console.log(`
  345. ${colors.bright}╔════════════════════════════════════════════╗
  346. ║ Claude AI Installer 构建脚本 ║
  347. ╚════════════════════════════════════════════╝${colors.reset}
  348. `);
  349. log.info(`目标平台: ${options.platform}`);
  350. log.info(`目标架构: ${options.arch}`);
  351. log.info(`发布模式: ${options.publish}`);
  352. log.info(`跳过构建: ${options.skipBuild}`);
  353. log.info(`跳过类型检查: ${options.skipTypecheck}`);
  354. const startTime = Date.now();
  355. try {
  356. // 清理旧的构建文件
  357. cleanBuild();
  358. if (!options.skipBuild) {
  359. // 类型检查
  360. if (!options.skipTypecheck) {
  361. typeCheck();
  362. } else {
  363. log.warn('跳过类型检查');
  364. }
  365. // 构建前端
  366. buildFrontend();
  367. } else {
  368. log.warn('跳过前端构建');
  369. }
  370. packageApp(options);
  371. // Windows 平台自动构建带启动器的 Portable 版本
  372. if (options.platform === 'win' || options.platform === 'all') {
  373. buildPortableVersion();
  374. }
  375. showResults();
  376. const duration = ((Date.now() - startTime) / 1000).toFixed(1);
  377. log.success(`构建完成! 耗时: ${duration}s`);
  378. } catch (error) {
  379. log.error(error.message);
  380. process.exit(1);
  381. }
  382. }
  383. main();