|
|
@@ -1,24 +1,25 @@
|
|
|
#!/usr/bin/env node
|
|
|
|
|
|
/**
|
|
|
- * 多平台构建脚本
|
|
|
- * Multi-platform build script for Claude AI Installer
|
|
|
+ * Tauri 构建脚本
|
|
|
+ * Build script for Claude AI Installer (Tauri)
|
|
|
*
|
|
|
* 用法 / Usage:
|
|
|
* node scripts/build.js [options]
|
|
|
*
|
|
|
* 选项 / Options:
|
|
|
- * --platform, -p 目标平台: win, mac, linux, all (默认: 当前平台)
|
|
|
- * --arch, -a 架构: x64, arm64, all (默认: 当前架构)
|
|
|
- * --publish, -P 发布模式: always, onTag, never (默认: never)
|
|
|
- * --skip-build 跳过前端构建,仅打包
|
|
|
- * --skip-typecheck 跳过 TypeScript 类型检查
|
|
|
+ * --debug, -d 调试模式构建
|
|
|
+ * --skip-frontend 跳过前端构建
|
|
|
* --help, -h 显示帮助信息
|
|
|
*/
|
|
|
|
|
|
-const { execSync } = require('child_process');
|
|
|
-const path = require('path');
|
|
|
-const fs = require('fs');
|
|
|
+import { execSync } from 'child_process';
|
|
|
+import path from 'path';
|
|
|
+import fs from 'fs';
|
|
|
+import { fileURLToPath } from 'url';
|
|
|
+
|
|
|
+const __filename = fileURLToPath(import.meta.url);
|
|
|
+const __dirname = path.dirname(__filename);
|
|
|
|
|
|
// 颜色输出
|
|
|
const colors = {
|
|
|
@@ -39,39 +40,31 @@ const log = {
|
|
|
step: (msg) => console.log(`\n${colors.cyan}${colors.bright}▶ ${msg}${colors.reset}`),
|
|
|
};
|
|
|
|
|
|
+// 项目路径
|
|
|
+const PROJECT_ROOT = path.resolve(__dirname, '..');
|
|
|
+const TAURI_DIR = path.join(PROJECT_ROOT, 'src-tauri');
|
|
|
+const RELEASE_DIR = path.join(PROJECT_ROOT, 'release');
|
|
|
+const LAUNCHER_DIR = path.join(PROJECT_ROOT, 'launcher');
|
|
|
+
|
|
|
// 解析命令行参数
|
|
|
function parseArgs() {
|
|
|
const args = process.argv.slice(2);
|
|
|
const options = {
|
|
|
- platform: process.platform === 'win32' ? 'win' : process.platform === 'darwin' ? 'mac' : 'linux',
|
|
|
- arch: process.arch,
|
|
|
- publish: 'never',
|
|
|
- skipBuild: false,
|
|
|
- skipTypecheck: false,
|
|
|
+ debug: false,
|
|
|
+ skipFrontend: false,
|
|
|
help: false,
|
|
|
};
|
|
|
|
|
|
for (let i = 0; i < args.length; i++) {
|
|
|
const arg = args[i];
|
|
|
switch (arg) {
|
|
|
- case '--platform':
|
|
|
- case '-p':
|
|
|
- options.platform = args[++i];
|
|
|
- break;
|
|
|
- case '--arch':
|
|
|
- case '-a':
|
|
|
- options.arch = args[++i];
|
|
|
+ case '--debug':
|
|
|
+ case '-d':
|
|
|
+ options.debug = true;
|
|
|
break;
|
|
|
- case '--publish':
|
|
|
- case '-P':
|
|
|
- options.publish = args[++i];
|
|
|
- break;
|
|
|
- case '--skip-build':
|
|
|
+ case '--skip-frontend':
|
|
|
case '-s':
|
|
|
- options.skipBuild = true;
|
|
|
- break;
|
|
|
- case '--skip-typecheck':
|
|
|
- options.skipTypecheck = true;
|
|
|
+ options.skipFrontend = true;
|
|
|
break;
|
|
|
case '--help':
|
|
|
case '-h':
|
|
|
@@ -86,42 +79,27 @@ function parseArgs() {
|
|
|
// 显示帮助信息
|
|
|
function showHelp() {
|
|
|
console.log(`
|
|
|
-${colors.bright}Claude AI Installer 构建脚本${colors.reset}
|
|
|
+${colors.bright}Claude AI Installer 构建脚本 (Tauri)${colors.reset}
|
|
|
|
|
|
${colors.cyan}用法:${colors.reset}
|
|
|
node scripts/build.js [options]
|
|
|
|
|
|
${colors.cyan}选项:${colors.reset}
|
|
|
- --platform, -p <platform> 目标平台
|
|
|
- win - Windows (NSIS 安装包)
|
|
|
- mac - macOS (DMG)
|
|
|
- linux - Linux (AppImage)
|
|
|
- all - 所有平台
|
|
|
- 默认: 当前平台
|
|
|
-
|
|
|
- --arch, -a <arch> 目标架构
|
|
|
- x64 - 64位 Intel/AMD
|
|
|
- arm64 - ARM64 (Apple Silicon, etc.)
|
|
|
- all - 所有架构
|
|
|
- 默认: 当前架构
|
|
|
-
|
|
|
- --publish, -P <mode> 发布模式
|
|
|
- always - 总是发布
|
|
|
- onTag - 仅在 Git tag 时发布
|
|
|
- never - 不发布 (默认)
|
|
|
-
|
|
|
- --skip-build, -s 跳过前端构建,仅执行打包
|
|
|
-
|
|
|
- --skip-typecheck 跳过 TypeScript 类型检查
|
|
|
-
|
|
|
- --help, -h 显示此帮助信息
|
|
|
+ --debug, -d 以调试模式构建
|
|
|
+ --skip-frontend, -s 跳过前端构建
|
|
|
+ --help, -h 显示此帮助信息
|
|
|
|
|
|
${colors.cyan}示例:${colors.reset}
|
|
|
- node scripts/build.js # 构建当前平台
|
|
|
- node scripts/build.js -p win # 构建 Windows 版本
|
|
|
- node scripts/build.js -p mac -a arm64 # 构建 macOS ARM64 版本
|
|
|
- node scripts/build.js -p all # 构建所有平台
|
|
|
- node scripts/build.js -p win -P always # 构建并发布 Windows 版本
|
|
|
+ node scripts/build.js # 完整发布构建
|
|
|
+ node scripts/build.js --debug # 调试构建
|
|
|
+ node scripts/build.js -s # 跳过前端,仅构建 Tauri
|
|
|
+
|
|
|
+${colors.cyan}输出:${colors.reset}
|
|
|
+ release/ # 最终发布目录
|
|
|
+ ├── *-nsis.exe # NSIS 安装程序
|
|
|
+ ├── *-portable.exe # 便携版
|
|
|
+ └── Claude-AI-Installer-x.x.x-portable/ # 带启动器的便携版
|
|
|
+ └── Claude-AI-Installer-x.x.x-portable.zip # 便携版压缩包
|
|
|
`);
|
|
|
}
|
|
|
|
|
|
@@ -131,7 +109,7 @@ function exec(command, options = {}) {
|
|
|
try {
|
|
|
execSync(command, {
|
|
|
stdio: 'inherit',
|
|
|
- cwd: path.resolve(__dirname, '..'),
|
|
|
+ cwd: PROJECT_ROOT,
|
|
|
...options,
|
|
|
});
|
|
|
return true;
|
|
|
@@ -141,137 +119,94 @@ function exec(command, options = {}) {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
-// 获取 electron-builder 平台参数
|
|
|
-function getPlatformArgs(platform, arch) {
|
|
|
- const platformMap = {
|
|
|
- win: '--win',
|
|
|
- mac: '--mac',
|
|
|
- linux: '--linux',
|
|
|
- };
|
|
|
-
|
|
|
- const archMap = {
|
|
|
- x64: '--x64',
|
|
|
- arm64: '--arm64',
|
|
|
- all: '--x64 --arm64',
|
|
|
- };
|
|
|
-
|
|
|
- let args = [];
|
|
|
-
|
|
|
- if (platform === 'all') {
|
|
|
- args.push('--win', '--mac', '--linux');
|
|
|
- } else {
|
|
|
- args.push(platformMap[platform] || platformMap.win);
|
|
|
- }
|
|
|
-
|
|
|
- if (arch && arch !== 'all') {
|
|
|
- args.push(archMap[arch] || '');
|
|
|
- } else if (arch === 'all') {
|
|
|
- args.push('--x64', '--arm64');
|
|
|
- }
|
|
|
+// 获取版本号
|
|
|
+function getVersion() {
|
|
|
+ const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json');
|
|
|
+ const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
|
|
|
+ return tauriConf.version;
|
|
|
+}
|
|
|
|
|
|
- return args.join(' ');
|
|
|
+// 获取产品名称
|
|
|
+function getProductName() {
|
|
|
+ const tauriConfPath = path.join(TAURI_DIR, 'tauri.conf.json');
|
|
|
+ const tauriConf = JSON.parse(fs.readFileSync(tauriConfPath, 'utf8'));
|
|
|
+ return tauriConf.productName || 'Claude AI Installer';
|
|
|
}
|
|
|
|
|
|
// 清理旧的构建文件
|
|
|
function cleanBuild() {
|
|
|
log.step('清理旧的构建文件...');
|
|
|
|
|
|
- const dirsToClean = ['dist', 'dist-electron'];
|
|
|
- const projectRoot = path.resolve(__dirname, '..');
|
|
|
-
|
|
|
- for (const dir of dirsToClean) {
|
|
|
- const fullPath = path.join(projectRoot, dir);
|
|
|
- if (fs.existsSync(fullPath)) {
|
|
|
- fs.rmSync(fullPath, { recursive: true, force: true });
|
|
|
- log.info(`已删除: ${dir}`);
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
- // 清空 release 目录内容,但保留目录本身
|
|
|
- const releaseDir = path.join(projectRoot, 'release');
|
|
|
- if (fs.existsSync(releaseDir)) {
|
|
|
- const files = fs.readdirSync(releaseDir);
|
|
|
- for (const file of files) {
|
|
|
- const filePath = path.join(releaseDir, file);
|
|
|
- fs.rmSync(filePath, { recursive: true, force: true });
|
|
|
- }
|
|
|
- log.info('已清空: release');
|
|
|
+ // 清空 release 目录
|
|
|
+ if (fs.existsSync(RELEASE_DIR)) {
|
|
|
+ fs.rmSync(RELEASE_DIR, { recursive: true, force: true });
|
|
|
}
|
|
|
+ fs.mkdirSync(RELEASE_DIR, { recursive: true });
|
|
|
|
|
|
log.success('清理完成');
|
|
|
}
|
|
|
|
|
|
-// 类型检查
|
|
|
-function typeCheck() {
|
|
|
- log.step('执行类型检查...');
|
|
|
- if (!exec('npx vue-tsc --noEmit')) {
|
|
|
- throw new Error('类型检查失败');
|
|
|
- }
|
|
|
- log.success('类型检查通过');
|
|
|
-}
|
|
|
+// 构建 Tauri 应用
|
|
|
+function buildTauri(options) {
|
|
|
+ log.step(`构建 Tauri 应用 (${options.debug ? '调试' : '发布'}模式)...`);
|
|
|
|
|
|
-// 构建前端
|
|
|
-function buildFrontend() {
|
|
|
- log.step('构建前端资源...');
|
|
|
- if (!exec('npx vite build')) {
|
|
|
- throw new Error('前端构建失败');
|
|
|
+ let command = 'npm run tauri build';
|
|
|
+ if (options.debug) {
|
|
|
+ command += ' -- --debug';
|
|
|
}
|
|
|
- log.success('前端构建完成');
|
|
|
-}
|
|
|
-
|
|
|
-// 打包 Electron 应用
|
|
|
-function packageApp(options) {
|
|
|
- log.step(`打包应用 (平台: ${options.platform}, 架构: ${options.arch})...`);
|
|
|
-
|
|
|
- const platformArgs = getPlatformArgs(options.platform, options.arch);
|
|
|
- const publishArg = options.publish !== 'never' ? `--publish ${options.publish}` : '';
|
|
|
-
|
|
|
- const command = `npx electron-builder ${platformArgs} ${publishArg}`.trim();
|
|
|
|
|
|
if (!exec(command)) {
|
|
|
- throw new Error('应用打包失败');
|
|
|
+ throw new Error('Tauri 构建失败');
|
|
|
}
|
|
|
|
|
|
- log.success('应用打包完成');
|
|
|
+ log.success('Tauri 构建完成');
|
|
|
}
|
|
|
|
|
|
-// 显示构建结果
|
|
|
-function showResults() {
|
|
|
- log.step('构建结果:');
|
|
|
-
|
|
|
- const releaseDir = path.resolve(__dirname, '..', 'release');
|
|
|
+// 获取 Tauri 生成的 exe 文件名(小写,空格转连字符)
|
|
|
+function getTauriExeName() {
|
|
|
+ const productName = getProductName();
|
|
|
+ // Tauri 将产品名称转换为小写并用连字符替换空格
|
|
|
+ return productName.toLowerCase().replace(/\s+/g, '-') + '.exe';
|
|
|
+}
|
|
|
|
|
|
- if (!fs.existsSync(releaseDir)) {
|
|
|
- log.warn('release 目录不存在');
|
|
|
- return;
|
|
|
- }
|
|
|
+// 复制构建产物到 release 目录
|
|
|
+function copyArtifacts(options) {
|
|
|
+ log.step('复制构建产物到 release 目录...');
|
|
|
|
|
|
- const files = fs.readdirSync(releaseDir);
|
|
|
- const packages = files.filter(f => {
|
|
|
- const ext = path.extname(f).toLowerCase();
|
|
|
- return ['.exe', '.dmg', '.appimage', '.deb', '.rpm', '.zip'].includes(ext);
|
|
|
- });
|
|
|
+ const mode = options.debug ? 'debug' : 'release';
|
|
|
+ const bundleDir = path.join(TAURI_DIR, 'target', mode, 'bundle');
|
|
|
+ const nsisDir = path.join(bundleDir, 'nsis');
|
|
|
|
|
|
- if (packages.length === 0) {
|
|
|
- log.warn('未找到打包文件');
|
|
|
- return;
|
|
|
+ // 复制 NSIS 安装程序
|
|
|
+ if (fs.existsSync(nsisDir)) {
|
|
|
+ const files = fs.readdirSync(nsisDir);
|
|
|
+ for (const file of files) {
|
|
|
+ if (file.endsWith('.exe')) {
|
|
|
+ const src = path.join(nsisDir, file);
|
|
|
+ const dest = path.join(RELEASE_DIR, file);
|
|
|
+ fs.copyFileSync(src, dest);
|
|
|
+ log.info(`复制: ${file}`);
|
|
|
+ }
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- console.log('');
|
|
|
- for (const pkg of packages) {
|
|
|
- const filePath = path.join(releaseDir, pkg);
|
|
|
- const stats = fs.statSync(filePath);
|
|
|
- const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
|
|
- console.log(` ${colors.green}•${colors.reset} ${pkg} (${sizeMB} MB)`);
|
|
|
+ // 复制独立 exe (portable)
|
|
|
+ const exeName = getTauriExeName();
|
|
|
+ const portableExe = path.join(TAURI_DIR, 'target', mode, exeName);
|
|
|
+ if (fs.existsSync(portableExe)) {
|
|
|
+ const version = getVersion();
|
|
|
+ const portableName = `Claude-AI-Installer_${version}_x64-portable.exe`;
|
|
|
+ const dest = path.join(RELEASE_DIR, portableName);
|
|
|
+ fs.copyFileSync(portableExe, dest);
|
|
|
+ log.info(`复制便携版: ${portableName}`);
|
|
|
+ } else {
|
|
|
+ log.warn(`未找到便携版 exe: ${portableExe}`);
|
|
|
}
|
|
|
- console.log('');
|
|
|
- log.info(`输出目录: ${releaseDir}`);
|
|
|
-}
|
|
|
|
|
|
-// ==================== Portable 版本构建 ====================
|
|
|
+ log.success('构建产物复制完成');
|
|
|
+}
|
|
|
|
|
|
-const LAUNCHER_DIR = path.resolve(__dirname, '..', 'launcher');
|
|
|
-const RELEASE_DIR = path.resolve(__dirname, '..', 'release');
|
|
|
+// ==================== Portable 版本构建(带启动器)====================
|
|
|
|
|
|
/**
|
|
|
* 查找 portable exe 文件
|
|
|
@@ -287,6 +222,13 @@ function findPortableExe() {
|
|
|
function buildLauncher() {
|
|
|
log.step('构建启动器...');
|
|
|
|
|
|
+ // 检查 launcher.js 是否存在
|
|
|
+ const launcherScript = path.join(LAUNCHER_DIR, 'launcher.js');
|
|
|
+ if (!fs.existsSync(launcherScript)) {
|
|
|
+ log.warn('launcher.js 不存在,跳过启动器构建');
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
// 检查是否安装了 pkg
|
|
|
try {
|
|
|
execSync('npx pkg --version', { stdio: 'ignore' });
|
|
|
@@ -295,13 +237,12 @@ function buildLauncher() {
|
|
|
execSync('npm install -g pkg', { stdio: 'inherit' });
|
|
|
}
|
|
|
|
|
|
- const launcherScript = path.join(LAUNCHER_DIR, 'launcher.js');
|
|
|
const outputPath = path.join(RELEASE_DIR, 'launcher.exe');
|
|
|
|
|
|
// 使用 pkg 打包
|
|
|
execSync(`npx pkg "${launcherScript}" --target node18-win-x64 --output "${outputPath}"`, {
|
|
|
stdio: 'inherit',
|
|
|
- cwd: path.resolve(__dirname, '..'),
|
|
|
+ cwd: PROJECT_ROOT,
|
|
|
});
|
|
|
|
|
|
log.success(`启动器构建完成: ${outputPath}`);
|
|
|
@@ -312,8 +253,7 @@ function buildLauncher() {
|
|
|
* 创建 Portable 目录结构
|
|
|
*/
|
|
|
function createPortableStructure(portableExe, launcherExe) {
|
|
|
- const packageJson = require(path.resolve(__dirname, '..', 'package.json'));
|
|
|
- const version = packageJson.version;
|
|
|
+ const version = getVersion();
|
|
|
const portableDirName = `Claude-AI-Installer-${version}-portable`;
|
|
|
const portableDir = path.join(RELEASE_DIR, portableDirName);
|
|
|
const appDir = path.join(portableDir, 'app');
|
|
|
@@ -394,19 +334,58 @@ function buildPortableVersion() {
|
|
|
// 查找 portable exe
|
|
|
const portableExe = findPortableExe();
|
|
|
if (!portableExe) {
|
|
|
- throw new Error('未找到 portable exe 文件');
|
|
|
+ log.warn('未找到 portable exe 文件,跳过带启动器的 Portable 版本构建');
|
|
|
+ return;
|
|
|
}
|
|
|
log.info(`找到 Portable 文件: ${portableExe}`);
|
|
|
|
|
|
// 构建启动器
|
|
|
const launcherExe = buildLauncher();
|
|
|
+ if (!launcherExe) {
|
|
|
+ log.warn('启动器构建失败,跳过带启动器的 Portable 版本构建');
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
// 创建 Portable 目录结构
|
|
|
createPortableStructure(portableExe, launcherExe);
|
|
|
|
|
|
+ // 清理临时的 launcher.exe
|
|
|
+ fs.unlinkSync(launcherExe);
|
|
|
+
|
|
|
log.success('Portable 版本构建完成');
|
|
|
}
|
|
|
|
|
|
+// 显示构建结果
|
|
|
+function showResults() {
|
|
|
+ log.step('构建结果:');
|
|
|
+
|
|
|
+ if (!fs.existsSync(RELEASE_DIR)) {
|
|
|
+ log.warn('release 目录不存在');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ const files = fs.readdirSync(RELEASE_DIR);
|
|
|
+ const packages = files.filter(f => {
|
|
|
+ const ext = path.extname(f).toLowerCase();
|
|
|
+ return ['.exe', '.zip'].includes(ext);
|
|
|
+ });
|
|
|
+
|
|
|
+ if (packages.length === 0) {
|
|
|
+ log.warn('未找到打包文件');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ console.log('');
|
|
|
+ for (const pkg of packages) {
|
|
|
+ const filePath = path.join(RELEASE_DIR, pkg);
|
|
|
+ const stats = fs.statSync(filePath);
|
|
|
+ const sizeMB = (stats.size / (1024 * 1024)).toFixed(2);
|
|
|
+ console.log(` ${colors.green}•${colors.reset} ${pkg} (${sizeMB} MB)`);
|
|
|
+ }
|
|
|
+ console.log('');
|
|
|
+ log.info(`输出目录: ${RELEASE_DIR}`);
|
|
|
+}
|
|
|
+
|
|
|
// 主函数
|
|
|
async function main() {
|
|
|
const options = parseArgs();
|
|
|
@@ -419,14 +398,12 @@ async function main() {
|
|
|
console.log(`
|
|
|
${colors.bright}╔════════════════════════════════════════════╗
|
|
|
║ Claude AI Installer 构建脚本 ║
|
|
|
+║ (Tauri 2.0) ║
|
|
|
╚════════════════════════════════════════════╝${colors.reset}
|
|
|
`);
|
|
|
|
|
|
- log.info(`目标平台: ${options.platform}`);
|
|
|
- log.info(`目标架构: ${options.arch}`);
|
|
|
- log.info(`发布模式: ${options.publish}`);
|
|
|
- log.info(`跳过构建: ${options.skipBuild}`);
|
|
|
- log.info(`跳过类型检查: ${options.skipTypecheck}`);
|
|
|
+ log.info(`构建模式: ${options.debug ? '调试' : '发布'}`);
|
|
|
+ log.info(`跳过前端: ${options.skipFrontend}`);
|
|
|
|
|
|
const startTime = Date.now();
|
|
|
|
|
|
@@ -434,26 +411,16 @@ ${colors.bright}╔════════════════════
|
|
|
// 清理旧的构建文件
|
|
|
cleanBuild();
|
|
|
|
|
|
- if (!options.skipBuild) {
|
|
|
- // 类型检查
|
|
|
- if (!options.skipTypecheck) {
|
|
|
- typeCheck();
|
|
|
- } else {
|
|
|
- log.warn('跳过类型检查');
|
|
|
- }
|
|
|
- // 构建前端
|
|
|
- buildFrontend();
|
|
|
- } else {
|
|
|
- log.warn('跳过前端构建');
|
|
|
- }
|
|
|
+ // 构建 Tauri 应用
|
|
|
+ buildTauri(options);
|
|
|
|
|
|
- packageApp(options);
|
|
|
+ // 复制构建产物
|
|
|
+ copyArtifacts(options);
|
|
|
|
|
|
- // Windows 平台自动构建带启动器的 Portable 版本
|
|
|
- if (options.platform === 'win' || options.platform === 'all') {
|
|
|
- buildPortableVersion();
|
|
|
- }
|
|
|
+ // 构建带启动器的 Portable 版本
|
|
|
+ buildPortableVersion();
|
|
|
|
|
|
+ // 显示结果
|
|
|
showResults();
|
|
|
|
|
|
const duration = ((Date.now() - startTime) / 1000).toFixed(1);
|