[fix] update setup
This commit is contained in:
93
scripts/README.md
Normal file
93
scripts/README.md
Normal file
@@ -0,0 +1,93 @@
|
||||
# 生成 latest.json 脚本使用说明
|
||||
|
||||
## 概述
|
||||
|
||||
`generate-latest-json.js` 脚本用于生成 Tauri 更新器所需的 `latest.json` 文件。该文件包含版本信息、更新说明、下载链接和签名信息。
|
||||
|
||||
## 使用方法
|
||||
|
||||
### 基本用法
|
||||
|
||||
```bash
|
||||
npm run generate-latest -- --base-url https://your-server.com/releases
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```bash
|
||||
npm run generate-latest -- --base-url https://your-server.com/releases --version 0.0.6-beta.6 --notes "修复了已知问题"
|
||||
```
|
||||
|
||||
### 参数说明
|
||||
|
||||
- `--base-url` (必需): 更新文件的基 URL,例如 `https://your-server.com/releases`
|
||||
- `--version` (可选): 版本号,如果不提供,将从 `tauri.conf.json` 读取
|
||||
- `--notes` (可选): 更新说明,支持 Markdown 格式
|
||||
|
||||
## 工作流程
|
||||
|
||||
1. **构建应用**: 首先确保已经构建了应用
|
||||
```bash
|
||||
npm run build
|
||||
```
|
||||
|
||||
2. **生成 latest.json**: 运行脚本生成 `latest.json` 文件
|
||||
```bash
|
||||
npm run generate-latest -- --base-url https://your-server.com/releases
|
||||
```
|
||||
|
||||
3. **上传文件**: 将以下文件上传到服务器:
|
||||
- 安装包文件(如 `.exe`, `.dmg`, `.AppImage`)
|
||||
- 签名文件(`.sig`)
|
||||
- `latest.json` 文件
|
||||
|
||||
## 文件结构
|
||||
|
||||
生成的 `latest.json` 文件格式:
|
||||
|
||||
```json
|
||||
{
|
||||
"version": "0.0.6-beta.6",
|
||||
"notes": "版本 0.0.6-beta.6 更新",
|
||||
"pub_date": "2025-01-15T10:00:00.000Z",
|
||||
"platforms": {
|
||||
"windows-x86_64": {
|
||||
"url": "https://your-server.com/releases/CS工具箱_0.0.6-beta.6_x64-setup.exe",
|
||||
"signature": "签名内容..."
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **构建产物**: 脚本会自动查找构建产物目录中的文件,确保已经完成构建
|
||||
2. **签名文件**: 如果存在 `.sig` 文件,脚本会自动读取并包含在 `latest.json` 中
|
||||
3. **版本匹配**: 脚本会根据版本号匹配文件,确保文件名包含版本号
|
||||
4. **平台支持**: 脚本支持 Windows、macOS 和 Linux 平台
|
||||
|
||||
## 故障排查
|
||||
|
||||
### 未找到构建产物
|
||||
|
||||
如果脚本提示"未找到任何构建产物":
|
||||
1. 确保已经运行 `npm run build` 完成构建
|
||||
2. 检查 `src-tauri/target/release/bundle` 目录是否存在
|
||||
3. 确认构建产物文件名包含版本号
|
||||
|
||||
### 签名文件缺失
|
||||
|
||||
如果签名文件缺失:
|
||||
1. 确保设置了 `TAURI_SIGNING_PRIVATE_KEY_PASSWORD` 环境变量
|
||||
2. 确保 `tauri.conf.json` 中 `createUpdaterArtifacts` 设置为 `true`
|
||||
3. 重新构建应用
|
||||
|
||||
### 上传到服务器
|
||||
|
||||
将以下文件上传到服务器:
|
||||
- 安装包文件
|
||||
- 签名文件(`.sig`)
|
||||
- `latest.json` 文件
|
||||
|
||||
确保服务器上的 URL 路径与 `--base-url` 参数匹配。
|
||||
|
||||
275
scripts/generate-latest-json.js
Normal file
275
scripts/generate-latest-json.js
Normal file
@@ -0,0 +1,275 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* 生成 Tauri 更新器的 latest.json 文件
|
||||
*
|
||||
* 使用方法:
|
||||
* node scripts/generate-latest-json.js [options]
|
||||
*
|
||||
* 选项:
|
||||
* --base-url <url> 更新文件的基 URL(必需)
|
||||
* --version <version> 版本号(可选,默认从 tauri.conf.json 读取)
|
||||
* --notes <notes> 更新说明(可选)
|
||||
* --target <target> 构建类型(可选,默认 release,可选值:debug、release、fast-release)
|
||||
*/
|
||||
|
||||
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');
|
||||
|
||||
// 验证 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);
|
||||
}
|
||||
|
||||
// 确保 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) {
|
||||
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}/${filename}`;
|
||||
}
|
||||
// 普通 URL
|
||||
return `${cleanBaseUrl}/${filename}`;
|
||||
}
|
||||
|
||||
// 根据 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) {
|
||||
console.log(` ✓ 找到文件: ${result.file}`);
|
||||
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));
|
||||
|
||||
Reference in New Issue
Block a user