#!/usr/bin/env node import { execSync, spawn } from 'child_process'; import { platform } from 'os'; import { existsSync, readFileSync, readdirSync, rmSync, writeFileSync } from 'fs'; import { join } from 'path'; import { APPLE_CERTIFICATE, APPLE_CERTIFICATE_PASSWORD, APPLE_ID, APPLE_PASSWORD, APPLE_SIGNING_IDENTITY, APPLE_TEAM_ID, TAURI_SIGNING_PRIVATE_KEY, TAURI_SIGNING_PRIVATE_KEY_PASSWORD, } from './utils/constant.ts'; // ==================== 解析命令行参数 ==================== // 从命令行参数中获取 --features 的值 const args: string[] = process.argv.slice(2); const featuresIndex: number = args.findIndex((arg: string) => arg === '--features'); const buildFeature: string = featuresIndex !== -1 && args[featuresIndex + 1] ? (args[featuresIndex + 1] as string) : 'production'; // 根据 feature 设置对应的环境模式 const modeMap: Record = { production: 'production', development: 'development', prod_150_8080: 'prod_150_8080', }; const buildMode: string = (buildFeature in modeMap ? modeMap[buildFeature as keyof typeof modeMap] : modeMap.production) || 'production'; console.log('\x1b[36m%s\x1b[0m', `构建配置: --features ${buildFeature}, --mode ${buildMode}`); // ==================== updater签名环境变量配置 ==================== // 私匙 process.env.TAURI_SIGNING_PRIVATE_KEY = TAURI_SIGNING_PRIVATE_KEY; // 私匙对应的密码,没有就不配置 process.env.TAURI_SIGNING_PRIVATE_KEY_PASSWORD = TAURI_SIGNING_PRIVATE_KEY_PASSWORD; // Mac 公证 if (platform() === 'darwin') { /** 签名证书在钥匙串中的名称(签名标识) */ process.env.APPLE_SIGNING_IDENTITY = APPLE_SIGNING_IDENTITY; /** 从钥匙串导出的 .p12 证书的 base64 字符串(适用于 CI 或没有本地证书时) */ process.env.APPLE_CERTIFICATE = APPLE_CERTIFICATE; /** .p12 证书的密码。 */ process.env.APPLE_CERTIFICATE_PASSWORD = APPLE_CERTIFICATE_PASSWORD; /** 你的 Apple 账号邮箱(用于公证) */ process.env.APPLE_ID = APPLE_ID; /** Apple 账号的 App 专用密码(用于公证) */ process.env.APPLE_PASSWORD = APPLE_PASSWORD; /** 你的 Apple 开发者团队 ID(用于公证) */ process.env.APPLE_TEAM_ID = APPLE_TEAM_ID; } // 运行版本更新脚本,同步 tauri.conf.json 和 package.json 的版本号 console.log('\x1b[36m%s\x1b[0m', '正在同步版本号...'); try { const updateVersionScriptPath = join('.', 'vite', 'scripts', 'update-version.ts'); execSync(`node "${updateVersionScriptPath}"`, { stdio: 'inherit' }); console.log('\x1b[32m%s\x1b[0m', '版本号同步完成'); } catch (error: any) { console.error('\x1b[31m%s\x1b[0m', '版本号同步失败:', error.message); process.exit(1); } // 读取更新后的 package.json 获取版本号 interface PackageJson { version: string; [key: string]: any; } const packageJsonPath = join('package.json'); let packageJson: PackageJson = {} as PackageJson; try { packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf8')); } catch (error: any) { console.error('\x1b[31m%s\x1b[0m', '读取 package.json 失败:', error.message); process.exit(1); } // 获取当前版本号 const newVersion: string = packageJson.version; // 在构建前删除 src-tauri/target/release/bundle 目录内容 const bundleDir = join('src-tauri', 'target', 'release', 'bundle'); if (existsSync(bundleDir)) { console.log('\x1b[33m%s\x1b[0m', `正在删除 ${bundleDir} 目录内容...`); rmSync(bundleDir, { recursive: true, force: true }); } // 先执行 Vite 默认配置构建 console.log('\x1b[36m%s\x1b[0m', '正在构建 index.html入口...'); const viteBuild = spawn('vite', ['build', '--mode', buildMode], { stdio: 'inherit', shell: true, env: { ...process.env }, }); viteBuild.on('close', (code) => { if (code !== 0) { console.error('\x1b[31m%s\x1b[0m', `Vite 打包 index.html失败,退出码: ${code}`); process.exit(code); } console.log('\x1b[36m%s\x1b[0m', '正在构建 其他.html入口...'); const viteSimplifiedBuild = spawn('vite', ['build', '--config', 'vite.simplified.config.ts', '--mode', buildMode], { stdio: 'inherit', shell: true, env: { ...process.env }, }); viteSimplifiedBuild.on('close', (simplifiedCode) => { if (simplifiedCode !== 0) { console.error('\x1b[31m%s\x1b[0m', `Vite 打包 其他.html入口构建失败,退出码: ${simplifiedCode}`); process.exit(simplifiedCode); } console.log('\x1b[36m%s\x1b[0m', 'Vite 构建完成,开始执行 Tauri 构建...'); const tauriBuild = spawn('tauri', ['build', '--features', buildFeature], { stdio: 'inherit', shell: true, env: process.env, // 传递所有环境变量 }); tauriBuild.on('close', (tauriCode) => { if (tauriCode === 0) { // 写入 latest.json 到 bundle 目录 const latestJsonPath = join(bundleDir, 'latest.json'); // 为不同平台生成相应的 latest.json if (platform() === 'win32') { const latestJson = getLatestJson('windows', newVersion, 'nsis', 'exe'); writeFileSync(latestJsonPath, JSON.stringify(latestJson, null, 2)); console.log('\x1b[32m%s\x1b[0m', `已生成 latest.json 文件: ${latestJsonPath}`); } else if (platform() === 'darwin') { const latestJson = getLatestJson('macos', newVersion, 'macos', 'app.tar.gz'); writeFileSync(latestJsonPath, JSON.stringify(latestJson, null, 2)); console.log('\x1b[32m%s\x1b[0m', `已生成 latest.json 文件: ${latestJsonPath}`); } else if (platform() === 'linux') { // 为 AppImage 生成 latest.json const appImageLatest = getLatestJson('linux', newVersion, 'appimage', 'AppImage'); writeFileSync(join(bundleDir, 'appimage', 'latest.json'), JSON.stringify(appImageLatest, null, 2)); console.log('\x1b[32m%s\x1b[0m', `已生成 appimage-latest.json 文件`); // 为 deb 生成 latest.json const debLatest = getLatestJson('linux', newVersion, 'deb', 'deb'); writeFileSync(join(bundleDir, 'deb', 'latest.json'), JSON.stringify(debLatest, null, 2)); console.log('\x1b[32m%s\x1b[0m', `已生成 deb-latest.json 文件`); // 为 rpm 生成 latest.json const rpmLatest = getLatestJson('linux', newVersion, 'rpm', 'rpm'); writeFileSync(join(bundleDir, 'rpm', 'latest.json'), JSON.stringify(rpmLatest, null, 2)); console.log('\x1b[32m%s\x1b[0m', `已生成 rpm-latest.json 文件`); } // Tauri 构建成功后执行自动部署 console.log('\x1b[36m%s\x1b[0m', '开始执行自动部署...'); try { const autoDeployScriptPath = join('.', 'vite', 'scripts', 'auto-deply.ts'); execSync(`npx tsx "${autoDeployScriptPath}"`, { stdio: 'inherit' }); console.log('\x1b[32m%s\x1b[0m', '自动部署执行完成'); } catch (error: any) { console.error('\x1b[31m%s\x1b[0m', '自动部署执行失败:', error.message); // 注意:这里不退出进程,因为构建本身是成功的 } } else { console.error('\x1b[31m%s\x1b[0m', `Tauri 构建失败,退出码: ${tauriCode}`); process.exit(tauriCode); } }); }); }); /** * 得到最新版本信息 * @param ostype - 平台类型,如 'windows'、'macos'、'linux' * @param version - 新版本号 * @param catalog - 包的目录名 * @param extension - 包的扩展名 */ interface LatestJson { version: string; pub_date: string; url: string; signature: string; notes: string; downloadLink?: string; } /** * 获取最新版本信息 * @param ostype - 平台类型,如 'windows'、'macos'、'linux' * @param version - 新版本号 * @param catalog - 包的目录名 * @param extension - 包的扩展名 */ function getLatestJson(ostype: string, version: string, catalog: string, extension: string): LatestJson { const pubDate = new Date().toISOString(); const latestJson: LatestJson = { version, pub_date: pubDate, url: '', signature: '', notes: '1. 修复BUG \n 2. 优化部分功能', }; const baseUrl = `https://file.qyzhjy.com/app/zpkt_desktop_app/${ostype}`; try { if (ostype === 'windows') { const nsisDir = join(bundleDir, catalog); const files = readdirSync(nsisDir); const exeFile = files.find((file) => file.endsWith('exe')); latestJson.downloadLink = `${baseUrl}/${catalog}/${exeFile}`; } else if (ostype === 'macos') { const dmgDir = join(bundleDir, 'dmg'); const files = readdirSync(dmgDir); const dmgFile = files.find((file) => file.endsWith('dmg')); latestJson.downloadLink = `${baseUrl}/dmg/${dmgFile}`; } else if (ostype === 'linux') { const appImageDir = join(bundleDir, catalog); const files = readdirSync(appImageDir); const appImageFile = files.find((file) => file.endsWith(extension)); latestJson.downloadLink = `${baseUrl}/${catalog}/${appImageFile}`; } // 查找 nsis 目录下的 exe 文件 const nsisDir = join(bundleDir, catalog); if (existsSync(nsisDir)) { const files = readdirSync(nsisDir); const exeFile = files.find((file) => file.endsWith(extension)); const sigFile = files.find((file) => file.endsWith(`${extension}.sig`)); if (exeFile) { // 设置 Windows 平台的 URL latestJson.url = `${baseUrl}/${catalog}/${exeFile}`; // 如果存在对应的 .sig 文件,读取签名内容 if (sigFile) { const sigFilePath = join(nsisDir, sigFile); latestJson.signature = readFileSync(sigFilePath, 'utf8').trim(); } } } return latestJson; } catch (error: any) { console.warn('\x1b[33m%s\x1b[0m', '警告: 无法读取 Windows 安装包信息:', error.message); return latestJson; } }