build.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435
  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. * --skip-frontend 跳过前端构建
  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 RELEASE_DIR = path.join(PROJECT_ROOT, 'release');
  41. const LAUNCHER_DIR = path.join(PROJECT_ROOT, 'launcher');
  42. // 解析命令行参数
  43. function parseArgs() {
  44. const args = process.argv.slice(2);
  45. const options = {
  46. debug: false,
  47. skipFrontend: false,
  48. help: false,
  49. };
  50. for (let i = 0; i < args.length; i++) {
  51. const arg = args[i];
  52. switch (arg) {
  53. case '--debug':
  54. case '-d':
  55. options.debug = true;
  56. break;
  57. case '--skip-frontend':
  58. case '-s':
  59. options.skipFrontend = true;
  60. break;
  61. case '--help':
  62. case '-h':
  63. options.help = true;
  64. break;
  65. }
  66. }
  67. return options;
  68. }
  69. // 显示帮助信息
  70. function showHelp() {
  71. console.log(`
  72. ${colors.bright}Claude AI Installer 构建脚本 (Tauri)${colors.reset}
  73. ${colors.cyan}用法:${colors.reset}
  74. node scripts/build.js [options]
  75. ${colors.cyan}选项:${colors.reset}
  76. --debug, -d 以调试模式构建
  77. --skip-frontend, -s 跳过前端构建
  78. --help, -h 显示此帮助信息
  79. ${colors.cyan}示例:${colors.reset}
  80. node scripts/build.js # 完整发布构建
  81. node scripts/build.js --debug # 调试构建
  82. node scripts/build.js -s # 跳过前端,仅构建 Tauri
  83. ${colors.cyan}输出:${colors.reset}
  84. release/ # 最终发布目录
  85. ├── *-nsis.exe # NSIS 安装程序
  86. ├── *-portable.exe # 便携版
  87. └── Claude-AI-Installer-x.x.x-portable/ # 带启动器的便携版
  88. └── Claude-AI-Installer-x.x.x-portable.zip # 便携版压缩包
  89. `);
  90. }
  91. // 执行命令
  92. function exec(command, options = {}) {
  93. log.info(`执行: ${command}`);
  94. try {
  95. execSync(command, {
  96. stdio: 'inherit',
  97. cwd: PROJECT_ROOT,
  98. ...options,
  99. });
  100. return true;
  101. } catch (error) {
  102. log.error(`命令执行失败: ${command}`);
  103. return false;
  104. }
  105. }
  106. // 获取版本号
  107. function getVersion() {
  108. const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json');
  109. const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
  110. return tauriConf.version;
  111. }
  112. // 获取产品名称
  113. function getProductName() {
  114. const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json');
  115. const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
  116. return tauriConf.productName || 'Claude AI Installer';
  117. }
  118. // 清理旧的构建文件
  119. function cleanBuild() {
  120. log.step('清理旧的构建文件...');
  121. // 清空 release 目录
  122. if (fs.existsSync(RELEASE_DIR)) {
  123. fs.rmSync(RELEASE_DIR, { recursive: true, force: true });
  124. }
  125. fs.mkdirSync(RELEASE_DIR, { recursive: true });
  126. log.success('清理完成');
  127. }
  128. // 构建 Tauri 应用
  129. function buildTauri(options) {
  130. log.step(`构建 Tauri 应用 (${options.debug ? '调试' : '发布'}模式)...`);
  131. let command = 'npm run tauri build';
  132. if (options.debug) {
  133. command += ' -- --debug';
  134. }
  135. if (!exec(command)) {
  136. throw new Error('Tauri 构建失败');
  137. }
  138. log.success('Tauri 构建完成');
  139. }
  140. // 获取 Tauri 生成的 exe 文件名(小写,空格转连字符)
  141. function getTauriExeName() {
  142. const productName = getProductName();
  143. // Tauri 将产品名称转换为小写并用连字符替换空格
  144. return productName.toLowerCase().replace(/\s+/g, '-') + '.exe';
  145. }
  146. // 复制构建产物到 release 目录
  147. function copyArtifacts(options) {
  148. log.step('复制构建产物到 release 目录...');
  149. const mode = options.debug ? 'debug' : 'release';
  150. const bundleDir = path.join(TAURI_DIR, 'target', mode, 'bundle');
  151. const nsisDir = path.join(bundleDir, 'nsis');
  152. // 复制 NSIS 安装程序
  153. if (fs.existsSync(nsisDir)) {
  154. const files = fs.readdirSync(nsisDir);
  155. for (const file of files) {
  156. if (file.endsWith('.exe')) {
  157. const src = path.join(nsisDir, file);
  158. const dest = path.join(RELEASE_DIR, file);
  159. fs.copyFileSync(src, dest);
  160. log.info(`复制: ${file}`);
  161. }
  162. }
  163. }
  164. // 复制独立 exe (portable)
  165. const exeName = getTauriExeName();
  166. const portableExe = path.join(TAURI_DIR, 'target', mode, exeName);
  167. if (fs.existsSync(portableExe)) {
  168. const version = getVersion();
  169. const portableName = `Claude-AI-Installer_${version}_x64-portable.exe`;
  170. const dest = path.join(RELEASE_DIR, portableName);
  171. fs.copyFileSync(portableExe, dest);
  172. log.info(`复制便携版: ${portableName}`);
  173. } else {
  174. log.warn(`未找到便携版 exe: ${portableExe}`);
  175. }
  176. log.success('构建产物复制完成');
  177. }
  178. // ==================== Portable 版本构建(带启动器)====================
  179. /**
  180. * 查找 portable exe 文件
  181. */
  182. function findPortableExe() {
  183. const files = fs.readdirSync(RELEASE_DIR);
  184. return files.find(f => f.includes('portable') && f.endsWith('.exe'));
  185. }
  186. /**
  187. * 使用 pkg 打包启动器
  188. */
  189. function buildLauncher() {
  190. log.step('构建启动器...');
  191. // 检查 launcher.js 是否存在
  192. const launcherScript = path.join(LAUNCHER_DIR, 'launcher.js');
  193. if (!fs.existsSync(launcherScript)) {
  194. log.warn('launcher.js 不存在,跳过启动器构建');
  195. return null;
  196. }
  197. // 检查是否安装了 pkg
  198. try {
  199. execSync('npx pkg --version', { stdio: 'ignore' });
  200. } catch {
  201. log.info('安装 pkg...');
  202. execSync('npm install -g pkg', { stdio: 'inherit' });
  203. }
  204. const outputPath = path.join(RELEASE_DIR, 'launcher.exe');
  205. // 使用 pkg 打包
  206. execSync(`npx pkg "${launcherScript}" --target node18-win-x64 --output "${outputPath}"`, {
  207. stdio: 'inherit',
  208. cwd: PROJECT_ROOT,
  209. });
  210. log.success(`启动器构建完成: ${outputPath}`);
  211. return outputPath;
  212. }
  213. /**
  214. * 创建 Portable 目录结构
  215. */
  216. function createPortableStructure(portableExe, launcherExe) {
  217. const version = getVersion();
  218. const portableDirName = `Claude-AI-Installer-${version}-portable`;
  219. const portableDir = path.join(RELEASE_DIR, portableDirName);
  220. const appDir = path.join(portableDir, 'app');
  221. const updateDir = path.join(portableDir, 'update');
  222. log.step('创建 Portable 目录结构...');
  223. // 清理旧目录
  224. if (fs.existsSync(portableDir)) {
  225. fs.rmSync(portableDir, { recursive: true });
  226. }
  227. // 创建目录
  228. fs.mkdirSync(appDir, { recursive: true });
  229. fs.mkdirSync(updateDir, { recursive: true });
  230. // 复制启动器
  231. const launcherDest = path.join(portableDir, 'Claude-AI-Installer.exe');
  232. fs.copyFileSync(launcherExe, launcherDest);
  233. log.info(`复制启动器: ${launcherDest}`);
  234. // 复制主程序到 app 目录
  235. const portableExePath = path.join(RELEASE_DIR, portableExe);
  236. const appExeName = `Claude-AI-Installer-${version}.exe`;
  237. const appExeDest = path.join(appDir, appExeName);
  238. fs.copyFileSync(portableExePath, appExeDest);
  239. log.info(`复制主程序: ${appExeDest}`);
  240. // 创建 .gitkeep 文件保持 update 目录
  241. fs.writeFileSync(path.join(updateDir, '.gitkeep'), '');
  242. // 创建 README
  243. const readme = `Claude AI Installer - Portable 版本
  244. 使用方法:
  245. 1. 双击 Claude-AI-Installer.exe 启动程序
  246. 2. 程序会自动检查更新
  247. 3. 更新下载后,重启程序即可完成更新
  248. 目录说明:
  249. - Claude-AI-Installer.exe: 启动器
  250. - app/: 主程序目录
  251. - update/: 更新文件临时目录
  252. 版本: ${version}
  253. `;
  254. fs.writeFileSync(path.join(portableDir, 'README.txt'), readme, 'utf8');
  255. log.success(`Portable 目录创建完成: ${portableDir}`);
  256. // 创建 zip 包
  257. createPortableZip(portableDir, portableDirName);
  258. return portableDir;
  259. }
  260. /**
  261. * 创建 ZIP 压缩包
  262. */
  263. function createPortableZip(sourceDir, name) {
  264. const zipPath = path.join(RELEASE_DIR, `${name}.zip`);
  265. log.step('创建 ZIP 压缩包...');
  266. // 使用 PowerShell 创建 zip
  267. const psCommand = `Compress-Archive -Path "${sourceDir}\\*" -DestinationPath "${zipPath}" -Force`;
  268. execSync(`powershell -Command "${psCommand}"`, { stdio: 'inherit' });
  269. log.success(`ZIP 压缩包创建完成: ${zipPath}`);
  270. }
  271. /**
  272. * 构建 Portable 版本(带启动器)
  273. */
  274. function buildPortableVersion() {
  275. log.step('构建 Portable 版本(带启动器)...');
  276. // 查找 portable exe
  277. const portableExe = findPortableExe();
  278. if (!portableExe) {
  279. log.warn('未找到 portable exe 文件,跳过带启动器的 Portable 版本构建');
  280. return;
  281. }
  282. log.info(`找到 Portable 文件: ${portableExe}`);
  283. // 构建启动器
  284. const launcherExe = buildLauncher();
  285. if (!launcherExe) {
  286. log.warn('启动器构建失败,跳过带启动器的 Portable 版本构建');
  287. return;
  288. }
  289. // 创建 Portable 目录结构
  290. createPortableStructure(portableExe, launcherExe);
  291. // 清理临时的 launcher.exe
  292. fs.unlinkSync(launcherExe);
  293. log.success('Portable 版本构建完成');
  294. }
  295. // 显示构建结果
  296. function showResults() {
  297. log.step('构建结果:');
  298. if (!fs.existsSync(RELEASE_DIR)) {
  299. log.warn('release 目录不存在');
  300. return;
  301. }
  302. const files = fs.readdirSync(RELEASE_DIR);
  303. const packages = files.filter(f => {
  304. const ext = path.extname(f).toLowerCase();
  305. return ['.exe', '.zip'].includes(ext);
  306. });
  307. if (packages.length === 0) {
  308. log.warn('未找到打包文件');
  309. return;
  310. }
  311. console.log('');
  312. for (const pkg of packages) {
  313. const filePath = path.join(RELEASE_DIR, pkg);
  314. const stats = fs.statSync(filePath);
  315. const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
  316. console.log(` ${colors.green}•${colors.reset} ${pkg} (${sizeMB} MB)`);
  317. }
  318. console.log('');
  319. log.info(`输出目录: ${RELEASE_DIR}`);
  320. }
  321. // 主函数
  322. async function main() {
  323. const options = parseArgs();
  324. if (options.help) {
  325. showHelp();
  326. process.exit(0);
  327. }
  328. console.log(`
  329. ${colors.bright}╔════════════════════════════════════════════╗
  330. ║ Claude AI Installer 构建脚本 ║
  331. ║ (Tauri 2.0) ║
  332. ╚════════════════════════════════════════════╝${colors.reset}
  333. `);
  334. log.info(`构建模式: ${options.debug ? '调试' : '发布'}`);
  335. log.info(`跳过前端: ${options.skipFrontend}`);
  336. const startTime = Date.now();
  337. try {
  338. // 清理旧的构建文件
  339. cleanBuild();
  340. // 构建 Tauri 应用
  341. buildTauri(options);
  342. // 复制构建产物
  343. copyArtifacts(options);
  344. // 构建带启动器的 Portable 版本
  345. buildPortableVersion();
  346. // 显示结果
  347. showResults();
  348. const duration = ((Date.now() - startTime) / 1000).toFixed(1);
  349. log.success(`构建完成! 耗时: ${duration}s`);
  350. } catch (error) {
  351. log.error(error.message);
  352. process.exit(1);
  353. }
  354. }
  355. main();