#!/usr/bin/env node /** * 生成 Tauri 更新器的 latest.json 文件 * * 使用方法: * node scripts/generate-latest-json.js [options] * * 选项: * --base-url 更新文件的基 URL(必需) * --version 版本号(可选,默认从 tauri.conf.json 读取) * --notes 更新说明(可选) * --target 构建类型(可选,默认 release,可选值:debug、release、fast-release) * --rename 在生成 latest.json 之前,将文件名中的 "CS工具箱" 改为 "CS_Toolbox" */ const fs = require('fs'); const path = require('path'); // 解析命令行参数 const args = process.argv.slice(2); const getArg = (name, defaultValue = null) => { const index = args.indexOf(name); if (index !== -1 && args[index + 1]) { return args[index + 1]; } return defaultValue; }; // 读取配置 const tauriConfigPath = path.join(__dirname, '../src-tauri/tauri.conf.json'); const tauriConfig = JSON.parse(fs.readFileSync(tauriConfigPath, 'utf-8')); const version = getArg('--version', tauriConfig.version); const baseUrl = getArg('--base-url'); const notes = getArg('--notes', ''); const target = getArg('--target', 'release'); const shouldRename = args.includes('--rename'); // 验证 target 参数 const validTargets = ['debug', 'release', 'fast-release']; if (!validTargets.includes(target)) { console.error(`错误: --target 必须是以下值之一: ${validTargets.join(', ')}`); process.exit(1); } if (!baseUrl) { console.error('错误: 必须提供 --base-url 参数'); console.error('示例: node scripts/generate-latest-json.js --base-url https://your-server.com/releases'); process.exit(1); } // 如果需要重命名,先执行重命名脚本 if (shouldRename) { console.log('\n执行文件重命名...'); const { execSync } = require('child_process'); try { execSync(`node scripts/rename-build-artifacts.js --target ${target}`, { stdio: 'inherit', cwd: path.join(__dirname, '..') }); console.log('✓ 文件重命名完成\n'); } catch (err) { console.error('✗ 文件重命名失败:', err.message); process.exit(1); } } // 确保 baseUrl 不以 / 结尾 const cleanBaseUrl = baseUrl.replace(/\/$/, ''); // 检测是否是 GitHub releases URL const githubReleasesMatch = cleanBaseUrl.match(/^https?:\/\/github\.com\/([^\/]+)\/([^\/]+)\/releases$/i); let githubOwner = null; let githubRepo = null; if (githubReleasesMatch) { githubOwner = githubReleasesMatch[1]; githubRepo = githubReleasesMatch[2]; console.log(`检测到 GitHub releases: ${githubOwner}/${githubRepo}`); } // 生成文件 URL function generateFileUrl(filename) { // 如果使用了 --rename,文件名应该已经是 CS_Toolbox 了 // 但为了兼容,我们检查一下是否需要替换 let finalFilename = filename; if (shouldRename && filename.includes('CS工具箱')) { finalFilename = filename.replace(/CS工具箱/g, 'CS_Toolbox'); } if (githubOwner && githubRepo) { // GitHub releases 下载 URL 格式: https://github.com/{owner}/{repo}/releases/download/{tag}/{filename} const tag = version.startsWith('v') ? version : `v${version}`; return `https://github.com/${githubOwner}/${githubRepo}/releases/download/${tag}/${finalFilename}`; } // 普通 URL return `${cleanBaseUrl}/${finalFilename}`; } // 根据 target 确定构建产物目录 const bundleDir = path.join(__dirname, '../src-tauri/target', target, 'bundle'); console.log(`使用构建类型: ${target}`); console.log(`构建产物目录: ${bundleDir}`); const platforms = { 'windows-x86_64': { dir: path.join(bundleDir, 'nsis'), extensions: ['.exe'], pattern: /windows|win/i }, 'darwin-x86_64': { dir: path.join(bundleDir, 'macos'), extensions: ['.dmg', '.app.tar.gz'], pattern: /darwin|macos|mac/i }, 'darwin-aarch64': { dir: path.join(bundleDir, 'macos'), extensions: ['.dmg', '.app.tar.gz'], pattern: /darwin|macos|mac|aarch64|arm64/i }, 'linux-x86_64': { dir: path.join(bundleDir, 'appimage'), extensions: ['.AppImage', '.deb', '.rpm'], pattern: /linux/i } }; // 递归查找文件 function findFilesRecursive(dir, version, extensions, maxDepth = 2, currentDepth = 0) { if (!fs.existsSync(dir) || currentDepth > maxDepth) { return null; } try { const items = fs.readdirSync(dir); const versionPattern = version.replace(/\./g, '\\.').replace(/-/g, '[-.]'); const regex = new RegExp(versionPattern, 'i'); // 先尝试精确匹配版本 for (const item of items) { const itemPath = path.join(dir, item); const stat = fs.statSync(itemPath); if (stat.isFile()) { if (!regex.test(item)) continue; const ext = path.extname(item); if (extensions.includes(ext)) { const sigPath = itemPath + '.sig'; return { file: item, url: generateFileUrl(item), signature: fs.existsSync(sigPath) ? fs.readFileSync(sigPath, 'utf-8').trim() : null }; } } else if (stat.isDirectory() && currentDepth < maxDepth) { const result = findFilesRecursive(itemPath, version, extensions, maxDepth, currentDepth + 1); if (result) { return result; } } } // 如果精确匹配失败,尝试查找最新版本的文件(仅在同一目录下) if (currentDepth === 0) { const matchingFiles = []; for (const item of items) { const itemPath = path.join(dir, item); const stat = fs.statSync(itemPath); if (stat.isFile()) { const ext = path.extname(item); if (extensions.includes(ext)) { matchingFiles.push({ file: item, path: itemPath, mtime: stat.mtime }); } } } if (matchingFiles.length > 0) { // 按修改时间排序,选择最新的文件 matchingFiles.sort((a, b) => b.mtime - a.mtime); const latest = matchingFiles[0]; const sigPath = latest.path + '.sig'; return { file: latest.file, url: generateFileUrl(latest.file), signature: fs.existsSync(sigPath) ? fs.readFileSync(sigPath, 'utf-8').trim() : null }; } } } catch (err) { // 忽略错误,继续查找 } return null; } // 查找文件(兼容旧接口) function findFiles(dir, version, extensions) { return findFilesRecursive(dir, version, extensions); } // 生成 platforms 对象 const platformsData = {}; for (const [platform, config] of Object.entries(platforms)) { console.log(`\n查找平台 ${platform}:`); console.log(` 目录: ${config.dir}`); console.log(` 扩展名: ${config.extensions.join(', ')}`); // 检查目录是否存在 if (!fs.existsSync(config.dir)) { console.warn(` 警告: 目录不存在: ${config.dir}`); continue; } const result = findFiles(config.dir, version, config.extensions); if (result) { // 如果使用了 --rename,文件名应该已经是 CS_Toolbox 了 // 但如果仍然包含 CS工具箱,说明重命名可能失败了,尝试手动处理 let fileName = result.file; if (shouldRename && fileName.includes('CS工具箱')) { console.warn(` 警告: 文件 ${fileName} 仍包含中文,尝试查找重命名后的文件...`); const renamedFile = fileName.replace(/CS工具箱/g, 'CS_Toolbox'); const renamedPath = path.join(config.dir, renamedFile); if (fs.existsSync(renamedPath)) { fileName = renamedFile; result.file = renamedFile; result.url = generateFileUrl(renamedFile); // 检查重命名后的签名文件 const renamedSigPath = renamedPath + '.sig'; if (fs.existsSync(renamedSigPath)) { result.signature = fs.readFileSync(renamedSigPath, 'utf-8').trim(); } } } console.log(` ✓ 找到文件: ${fileName}`); platformsData[platform] = { url: result.url, signature: result.signature }; } else { console.warn(` ✗ 未找到匹配版本 ${version} 的文件`); // 列出目录中的所有文件以便调试 try { const files = fs.readdirSync(config.dir); const exeFiles = files.filter(f => { const ext = path.extname(f); return config.extensions.includes(ext); }); if (exeFiles.length > 0) { console.log(` 目录中的文件: ${exeFiles.join(', ')}`); } } catch (err) { // 忽略错误 } } } // 如果没有找到任何平台文件,尝试查找所有文件 if (Object.keys(platformsData).length === 0) { console.warn('\n警告: 未找到任何构建产物,将生成空的 platforms 对象'); console.warn('请确保已经构建了应用: npm run build'); console.warn(`构建产物应该在: ${bundleDir}`); } // 生成 latest.json const latestJson = { version: version, notes: notes || `版本 ${version} 更新`, pub_date: new Date().toISOString(), platforms: Object.keys(platformsData).length > 0 ? platformsData : undefined }; // 如果只有一个平台,也可以使用简化的格式 if (Object.keys(platformsData).length === 1) { const platform = Object.keys(platformsData)[0]; latestJson.download_url = platformsData[platform].url; latestJson.signature = platformsData[platform].signature; } // 输出文件到对应的 bundle 目录 const outputDir = bundleDir; if (!fs.existsSync(outputDir)) { fs.mkdirSync(outputDir, { recursive: true }); } // 确保 nsis 子目录存在 const nsisOutputDir = path.join(outputDir, "nsis"); if (!fs.existsSync(nsisOutputDir)) { fs.mkdirSync(nsisOutputDir, { recursive: true }); } const outputPath = path.join(nsisOutputDir, 'latest.json'); fs.writeFileSync(outputPath, JSON.stringify(latestJson, null, 2), 'utf-8'); console.log('✓ 成功生成 latest.json'); console.log(` 构建类型: ${target}`); console.log(` 文件位置: ${outputPath}`); console.log(` 版本: ${version}`); console.log(` 平台数量: ${Object.keys(platformsData).length}`); if (Object.keys(platformsData).length > 0) { console.log(` 平台: ${Object.keys(platformsData).join(', ')}`); // 显示签名信息 for (const [platform, data] of Object.entries(platformsData)) { if (data.signature) { console.log(` ${platform}: 已找到签名文件`); } else { console.log(` ${platform}: 未找到签名文件`); } } } console.log('\n文件内容:'); console.log(JSON.stringify(latestJson, null, 2));