1
0

build.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. #!/usr/bin/env node
  2. /**
  3. * Tauri 构建脚本 (跨平台)
  4. * Build script for Claude AI Installer (Tauri)
  5. *
  6. * 用法 / Usage:
  7. * node scripts/build.js [options]
  8. *
  9. * 选项 / Options:
  10. * --debug, -d 调试模式构建
  11. * --target, -t 指定目标平台 (如 aarch64-apple-darwin)
  12. * --help, -h 显示帮助信息
  13. */
  14. import { execSync } from 'child_process';
  15. import path from 'path';
  16. import fs from 'fs';
  17. import { fileURLToPath } from 'url';
  18. const __filename = fileURLToPath(import.meta.url);
  19. const __dirname = path.dirname(__filename);
  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. const PROJECT_ROOT = path.resolve(__dirname, '..');
  39. const TAURI_DIR = path.join(PROJECT_ROOT, 'src-tauri');
  40. const BUILT_DIR = path.join(PROJECT_ROOT, 'built');
  41. // 检测当前平台
  42. function detectPlatform() {
  43. const platform = process.platform;
  44. const arch = process.arch;
  45. if (platform === 'win32') {
  46. return { os: 'windows', arch: 'x64', target: 'x86_64-pc-windows-msvc' };
  47. } else if (platform === 'darwin') {
  48. return {
  49. os: 'macos',
  50. arch: arch === 'arm64' ? 'aarch64' : 'x64',
  51. target: arch === 'arm64' ? 'aarch64-apple-darwin' : 'x86_64-apple-darwin',
  52. };
  53. } else if (platform === 'linux') {
  54. return { os: 'linux', arch: 'x64', target: 'x86_64-unknown-linux-gnu' };
  55. }
  56. return { os: platform, arch, target: null };
  57. }
  58. // 从 target 获取平台信息
  59. function getPlatformFromTarget(target) {
  60. if (target.includes('windows') || target.includes('msvc')) {
  61. return { os: 'windows', arch: 'x64' };
  62. } else if (target.includes('apple-darwin')) {
  63. const arch = target.includes('aarch64') ? 'aarch64' : 'x64';
  64. return { os: 'macos', arch };
  65. } else if (target.includes('linux')) {
  66. return { os: 'linux', arch: 'x64' };
  67. }
  68. return { os: 'unknown', arch: 'unknown' };
  69. }
  70. // 解析命令行参数
  71. function parseArgs() {
  72. const args = process.argv.slice(2);
  73. const options = {
  74. debug: false,
  75. help: false,
  76. target: null,
  77. };
  78. for (let i = 0; i < args.length; i++) {
  79. const arg = args[i];
  80. switch (arg) {
  81. case '--debug':
  82. case '-d':
  83. options.debug = true;
  84. break;
  85. case '--help':
  86. case '-h':
  87. options.help = true;
  88. break;
  89. case '--target':
  90. case '-t':
  91. options.target = args[++i];
  92. break;
  93. }
  94. }
  95. return options;
  96. }
  97. // 显示帮助信息
  98. function showHelp() {
  99. console.log(`
  100. ${colors.bright}Claude AI Installer 构建脚本 (Tauri - 跨平台)${colors.reset}
  101. ${colors.cyan}用法:${colors.reset}
  102. node scripts/build.js [options]
  103. ${colors.cyan}选项:${colors.reset}
  104. --debug, -d 以调试模式构建
  105. --target, -t <target> 指定目标平台
  106. --help, -h 显示此帮助信息
  107. ${colors.cyan}支持的目标平台:${colors.reset}
  108. x86_64-pc-windows-msvc Windows x64
  109. aarch64-apple-darwin macOS Apple Silicon
  110. x86_64-apple-darwin macOS Intel
  111. universal-apple-darwin macOS 通用二进制
  112. x86_64-unknown-linux-gnu Linux x64
  113. ${colors.cyan}示例:${colors.reset}
  114. node scripts/build.js # 当前平台发布构建
  115. node scripts/build.js --debug # 调试构建
  116. node scripts/build.js --target aarch64-apple-darwin # macOS ARM 构建
  117. ${colors.cyan}输出 (Windows):${colors.reset}
  118. built/
  119. ├── *-portable.exe # 便携版
  120. ├── *-setup.exe # NSIS 安装程序
  121. └── *.msi # MSI 安装程序
  122. ${colors.cyan}输出 (macOS):${colors.reset}
  123. built/
  124. ├── *.dmg # DMG 安装包
  125. └── *.app.tar.gz # 压缩的应用程序
  126. ${colors.cyan}输出 (Linux):${colors.reset}
  127. built/
  128. ├── *.deb # Debian 包
  129. └── *.AppImage # AppImage
  130. `);
  131. }
  132. // 执行命令
  133. function exec(command, options = {}) {
  134. log.info(`执行: ${command}`);
  135. try {
  136. execSync(command, {
  137. stdio: 'inherit',
  138. cwd: PROJECT_ROOT,
  139. ...options,
  140. });
  141. return true;
  142. } catch (error) {
  143. log.error(`命令执行失败: ${command}`);
  144. return false;
  145. }
  146. }
  147. // 获取版本号
  148. function getVersion() {
  149. const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json');
  150. const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
  151. return tauriConf.version;
  152. }
  153. // 获取产品名称
  154. function getProductName() {
  155. const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json');
  156. const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
  157. return tauriConf.productName || 'Claude AI Installer';
  158. }
  159. // 清理之前的 portable 版本 (仅 Windows)
  160. function cleanPortable(options) {
  161. const platform = options.target
  162. ? getPlatformFromTarget(options.target)
  163. : detectPlatform();
  164. if (platform.os !== 'windows') return;
  165. const { bundleDir } = getBuildPaths(options);
  166. if (fs.existsSync(bundleDir)) {
  167. const portableFiles = fs.readdirSync(bundleDir).filter(f => f.endsWith('-portable.exe'));
  168. for (const file of portableFiles) {
  169. const filePath = path.join(bundleDir, file);
  170. fs.unlinkSync(filePath);
  171. log.info(`已删除旧的便携版: ${file}`);
  172. }
  173. }
  174. }
  175. // 构建 Tauri 应用
  176. function buildTauri(options) {
  177. log.step(`构建 Tauri 应用 (${options.debug ? '调试' : '发布'}模式)...`);
  178. let command = 'npm run tauri build';
  179. const extraArgs = [];
  180. if (options.debug) {
  181. extraArgs.push('--debug');
  182. }
  183. if (options.target) {
  184. extraArgs.push('--target', options.target);
  185. }
  186. if (extraArgs.length > 0) {
  187. command += ' -- ' + extraArgs.join(' ');
  188. }
  189. if (!exec(command)) {
  190. throw new Error('Tauri 构建失败');
  191. }
  192. log.success('Tauri 构建完成');
  193. }
  194. // 获取构建目录路径
  195. function getBuildPaths(options) {
  196. const mode = options.debug ? 'debug' : 'release';
  197. let targetDir;
  198. if (options.target) {
  199. targetDir = path.join(TAURI_DIR, 'target', options.target, mode);
  200. } else {
  201. targetDir = path.join(TAURI_DIR, 'target', mode);
  202. }
  203. const bundleDir = path.join(targetDir, 'bundle');
  204. return { targetDir, bundleDir, mode };
  205. }
  206. // 获取平台标识符用于文件命名
  207. function getPlatformSuffix(options) {
  208. const platform = options.target
  209. ? getPlatformFromTarget(options.target)
  210. : detectPlatform();
  211. if (platform.os === 'windows') {
  212. return 'win-x64';
  213. } else if (platform.os === 'macos') {
  214. if (options.target && options.target.includes('universal')) {
  215. return 'mac-universal';
  216. }
  217. return platform.arch === 'aarch64' ? 'mac-arm64' : 'mac-x64';
  218. } else if (platform.os === 'linux') {
  219. return 'linux-x64';
  220. }
  221. return 'unknown';
  222. }
  223. // 重命名构建产物,使用统一的命名格式
  224. function renameArtifacts(options) {
  225. log.step('重命名构建产物...');
  226. const { targetDir, bundleDir } = getBuildPaths(options);
  227. const version = getVersion();
  228. const platformSuffix = getPlatformSuffix(options);
  229. const platform = options.target
  230. ? getPlatformFromTarget(options.target)
  231. : detectPlatform();
  232. // Windows 平台
  233. if (platform.os === 'windows') {
  234. // 创建 portable 版本
  235. const exeName = 'claude-ai-installer.exe';
  236. const exePath = path.join(targetDir, exeName);
  237. if (fs.existsSync(exePath)) {
  238. if (!fs.existsSync(bundleDir)) {
  239. fs.mkdirSync(bundleDir, { recursive: true });
  240. }
  241. const portableName = `Claude-AI-Installer-${version}-${platformSuffix}-portable.exe`;
  242. const portablePath = path.join(bundleDir, portableName);
  243. if (fs.existsSync(portablePath)) {
  244. fs.unlinkSync(portablePath);
  245. }
  246. fs.copyFileSync(exePath, portablePath);
  247. log.success(`创建便携版: ${portableName}`);
  248. }
  249. // 重命名 NSIS 安装程序
  250. const nsisDir = path.join(bundleDir, 'nsis');
  251. if (fs.existsSync(nsisDir)) {
  252. const files = fs.readdirSync(nsisDir).filter(f => f.endsWith('.exe'));
  253. for (const file of files) {
  254. const oldPath = path.join(nsisDir, file);
  255. const newName = `Claude-AI-Installer-${version}-${platformSuffix}-setup.exe`;
  256. const newPath = path.join(nsisDir, newName);
  257. if (file !== newName) {
  258. if (fs.existsSync(newPath)) fs.unlinkSync(newPath);
  259. fs.renameSync(oldPath, newPath);
  260. log.success(`重命名: ${file} -> ${newName}`);
  261. }
  262. }
  263. }
  264. // 重命名 MSI 安装程序
  265. const msiDir = path.join(bundleDir, 'msi');
  266. if (fs.existsSync(msiDir)) {
  267. const files = fs.readdirSync(msiDir).filter(f => f.endsWith('.msi'));
  268. for (const file of files) {
  269. const oldPath = path.join(msiDir, file);
  270. const newName = `Claude-AI-Installer-${version}-${platformSuffix}.msi`;
  271. const newPath = path.join(msiDir, newName);
  272. if (file !== newName) {
  273. if (fs.existsSync(newPath)) fs.unlinkSync(newPath);
  274. fs.renameSync(oldPath, newPath);
  275. log.success(`重命名: ${file} -> ${newName}`);
  276. }
  277. }
  278. }
  279. }
  280. // macOS 平台
  281. if (platform.os === 'macos') {
  282. // 重命名 DMG
  283. const dmgDir = path.join(bundleDir, 'dmg');
  284. if (fs.existsSync(dmgDir)) {
  285. const files = fs.readdirSync(dmgDir).filter(f => f.endsWith('.dmg'));
  286. for (const file of files) {
  287. const oldPath = path.join(dmgDir, file);
  288. const newName = `Claude-AI-Installer-${version}-${platformSuffix}.dmg`;
  289. const newPath = path.join(dmgDir, newName);
  290. if (file !== newName) {
  291. if (fs.existsSync(newPath)) fs.unlinkSync(newPath);
  292. fs.renameSync(oldPath, newPath);
  293. log.success(`重命名: ${file} -> ${newName}`);
  294. }
  295. }
  296. }
  297. // 重命名 .app.tar.gz (macos 目录)
  298. const macosDir = path.join(bundleDir, 'macos');
  299. if (fs.existsSync(macosDir)) {
  300. const files = fs.readdirSync(macosDir).filter(f => f.endsWith('.tar.gz'));
  301. for (const file of files) {
  302. const oldPath = path.join(macosDir, file);
  303. const newName = `Claude-AI-Installer-${version}-${platformSuffix}.app.tar.gz`;
  304. const newPath = path.join(macosDir, newName);
  305. if (file !== newName) {
  306. if (fs.existsSync(newPath)) fs.unlinkSync(newPath);
  307. fs.renameSync(oldPath, newPath);
  308. log.success(`重命名: ${file} -> ${newName}`);
  309. }
  310. }
  311. }
  312. }
  313. // Linux 平台
  314. if (platform.os === 'linux') {
  315. // 重命名 deb
  316. const debDir = path.join(bundleDir, 'deb');
  317. if (fs.existsSync(debDir)) {
  318. const files = fs.readdirSync(debDir).filter(f => f.endsWith('.deb'));
  319. for (const file of files) {
  320. const oldPath = path.join(debDir, file);
  321. const newName = `Claude-AI-Installer-${version}-${platformSuffix}.deb`;
  322. const newPath = path.join(debDir, newName);
  323. if (file !== newName) {
  324. if (fs.existsSync(newPath)) fs.unlinkSync(newPath);
  325. fs.renameSync(oldPath, newPath);
  326. log.success(`重命名: ${file} -> ${newName}`);
  327. }
  328. }
  329. }
  330. // 重命名 AppImage
  331. const appimageDir = path.join(bundleDir, 'appimage');
  332. if (fs.existsSync(appimageDir)) {
  333. const files = fs.readdirSync(appimageDir).filter(f => f.endsWith('.AppImage'));
  334. for (const file of files) {
  335. const oldPath = path.join(appimageDir, file);
  336. const newName = `Claude-AI-Installer-${version}-${platformSuffix}.AppImage`;
  337. const newPath = path.join(appimageDir, newName);
  338. if (file !== newName) {
  339. if (fs.existsSync(newPath)) fs.unlinkSync(newPath);
  340. fs.renameSync(oldPath, newPath);
  341. log.success(`重命名: ${file} -> ${newName}`);
  342. }
  343. }
  344. }
  345. // 重命名 rpm
  346. const rpmDir = path.join(bundleDir, 'rpm');
  347. if (fs.existsSync(rpmDir)) {
  348. const files = fs.readdirSync(rpmDir).filter(f => f.endsWith('.rpm'));
  349. for (const file of files) {
  350. const oldPath = path.join(rpmDir, file);
  351. const newName = `Claude-AI-Installer-${version}-${platformSuffix}.rpm`;
  352. const newPath = path.join(rpmDir, newName);
  353. if (file !== newName) {
  354. if (fs.existsSync(newPath)) fs.unlinkSync(newPath);
  355. fs.renameSync(oldPath, newPath);
  356. log.success(`重命名: ${file} -> ${newName}`);
  357. }
  358. }
  359. }
  360. }
  361. }
  362. // 显示构建结果
  363. function showResults(options) {
  364. log.step('构建结果:');
  365. const { targetDir, bundleDir } = getBuildPaths(options);
  366. console.log('');
  367. // 显示 built 目录中的文件
  368. if (fs.existsSync(BUILT_DIR)) {
  369. const files = fs.readdirSync(BUILT_DIR);
  370. for (const file of files) {
  371. const filePath = path.join(BUILT_DIR, file);
  372. const stats = fs.statSync(filePath);
  373. const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
  374. console.log(` ${colors.green}•${colors.reset} ${file} (${sizeMB} MB)`);
  375. }
  376. }
  377. console.log('');
  378. log.info(`输出目录: ${targetDir}`);
  379. if (fs.existsSync(bundleDir)) {
  380. log.info(`安装包目录: ${bundleDir}`);
  381. }
  382. log.info(`构建产物汇总: ${BUILT_DIR}`);
  383. }
  384. // 复制构建产物到 built 目录
  385. function copyToBuiltDir(options) {
  386. log.step('复制构建产物到 built 目录...');
  387. const { bundleDir } = getBuildPaths(options);
  388. const platform = options.target
  389. ? getPlatformFromTarget(options.target)
  390. : detectPlatform();
  391. // 确保 built 目录存在
  392. if (!fs.existsSync(BUILT_DIR)) {
  393. fs.mkdirSync(BUILT_DIR, { recursive: true });
  394. }
  395. // 清空 built 目录
  396. const existingFiles = fs.readdirSync(BUILT_DIR);
  397. for (const file of existingFiles) {
  398. const filePath = path.join(BUILT_DIR, file);
  399. fs.unlinkSync(filePath);
  400. }
  401. let copiedCount = 0;
  402. // 辅助函数:复制目录中的文件
  403. const copyFilesFromDir = (dir, extensions) => {
  404. if (!fs.existsSync(dir)) return;
  405. const files = fs.readdirSync(dir).filter(f =>
  406. extensions.some(ext => f.endsWith(ext))
  407. );
  408. for (const file of files) {
  409. const srcPath = path.join(dir, file);
  410. const destPath = path.join(BUILT_DIR, file);
  411. fs.copyFileSync(srcPath, destPath);
  412. log.success(`复制: ${file}`);
  413. copiedCount++;
  414. }
  415. };
  416. // Windows 平台
  417. if (platform.os === 'windows') {
  418. // 复制便携版
  419. if (fs.existsSync(bundleDir)) {
  420. copyFilesFromDir(bundleDir, ['-portable.exe']);
  421. }
  422. // 复制 NSIS 安装程序
  423. copyFilesFromDir(path.join(bundleDir, 'nsis'), ['.exe']);
  424. // 复制 MSI 安装程序
  425. copyFilesFromDir(path.join(bundleDir, 'msi'), ['.msi']);
  426. }
  427. // macOS 平台
  428. if (platform.os === 'macos') {
  429. // 复制 DMG
  430. copyFilesFromDir(path.join(bundleDir, 'dmg'), ['.dmg']);
  431. // 复制 .app.tar.gz
  432. copyFilesFromDir(path.join(bundleDir, 'macos'), ['.tar.gz']);
  433. }
  434. // Linux 平台
  435. if (platform.os === 'linux') {
  436. // 复制 deb
  437. copyFilesFromDir(path.join(bundleDir, 'deb'), ['.deb']);
  438. // 复制 AppImage
  439. copyFilesFromDir(path.join(bundleDir, 'appimage'), ['.AppImage']);
  440. // 复制 rpm
  441. copyFilesFromDir(path.join(bundleDir, 'rpm'), ['.rpm']);
  442. }
  443. if (copiedCount > 0) {
  444. log.success(`共复制 ${copiedCount} 个文件到 ${BUILT_DIR}`);
  445. } else {
  446. log.warn('没有找到可复制的构建产物');
  447. }
  448. }
  449. // 主函数
  450. async function main() {
  451. const options = parseArgs();
  452. if (options.help) {
  453. showHelp();
  454. process.exit(0);
  455. }
  456. console.log(`
  457. ${colors.bright}╔════════════════════════════════════════════╗
  458. ║ Claude AI Installer 构建脚本 ║
  459. ║ (Tauri 2.0) ║
  460. ╚════════════════════════════════════════════╝${colors.reset}
  461. `);
  462. const platform = options.target
  463. ? getPlatformFromTarget(options.target)
  464. : detectPlatform();
  465. const platformSuffix = getPlatformSuffix(options);
  466. log.info(`构建模式: ${options.debug ? '调试' : '发布'}`);
  467. log.info(`目标平台: ${platform.os} (${platformSuffix})`);
  468. if (options.target) {
  469. log.info(`目标架构: ${options.target}`);
  470. }
  471. log.info(`版本: ${getVersion()}`);
  472. log.info(`产品名称: ${getProductName()}`);
  473. const startTime = Date.now();
  474. try {
  475. // 清理之前的 portable 版本
  476. cleanPortable(options);
  477. // 构建 Tauri 应用
  478. buildTauri(options);
  479. // 重命名构建产物(仅发布模式)
  480. if (!options.debug) {
  481. renameArtifacts(options);
  482. }
  483. // 复制构建产物到 built 目录
  484. copyToBuiltDir(options);
  485. // 显示结果
  486. showResults(options);
  487. const duration = ((Date.now() - startTime) / 1000).toFixed(1);
  488. log.success(`构建完成! 耗时: ${duration}s`);
  489. } catch (error) {
  490. log.error(error.message);
  491. process.exit(1);
  492. }
  493. }
  494. main();