chore: initialize qiming workspace repository
This commit is contained in:
516
qimingclaw/specs/agent-client/vendorsdoc/cargo-packager.md
Normal file
516
qimingclaw/specs/agent-client/vendorsdoc/cargo-packager.md
Normal file
@@ -0,0 +1,516 @@
|
||||
# cargo-packager
|
||||
|
||||
## 项目概述
|
||||
|
||||
应用打包工具,支持生成多种平台安装包。是从 Tauri 框架剥离出来的独立打包工具。
|
||||
|
||||
**本地路径**: `vendors/cargo-packager`
|
||||
|
||||
## 目录结构
|
||||
|
||||
```
|
||||
cargo-packager/
|
||||
├── Cargo.toml # workspace 配置
|
||||
├── packager/ # 打包核心
|
||||
│ └── src/
|
||||
│ ├── lib.rs
|
||||
│ ├── config.rs # 配置
|
||||
│ ├── package.rs # 打包逻辑
|
||||
│ └── utils.rs # 工具函数
|
||||
├── config/ # 配置定义
|
||||
│ └── src/
|
||||
│ └── lib.rs
|
||||
├── resource-resolver/ # 资源解析
|
||||
│ └── src/
|
||||
│ └── lib.rs
|
||||
├── updater/ # 自动更新
|
||||
│ └── src/
|
||||
│ └── lib.rs
|
||||
├── codesign/ # 代码签名
|
||||
│ └── src/
|
||||
│ └── lib.rs
|
||||
├── package/
|
||||
│ ├── deb/ # Debian 包
|
||||
│ ├── appimage/ # AppImage
|
||||
│ ├── dmg/ # macOS DMG
|
||||
│ ├── msi/ # Windows MSI
|
||||
│ └── nsis/ # Windows NSIS
|
||||
└── Cargo.toml
|
||||
```
|
||||
|
||||
## 核心依赖
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
toml = "0.7"
|
||||
clap = { version = "4.0", features = ["derive"] }
|
||||
schemars = "0.8"
|
||||
tracing = "0.1"
|
||||
tar = "0.4"
|
||||
zip = "0.6"
|
||||
napi = { version = "2.0", optional = true }
|
||||
dirs = "5.0"
|
||||
semver = "1.0"
|
||||
url = "2.4"
|
||||
thiserror = "1.0"
|
||||
```
|
||||
|
||||
## 核心 API
|
||||
|
||||
### Config
|
||||
|
||||
```rust
|
||||
// config/src/lib.rs
|
||||
|
||||
pub struct PackageConfig {
|
||||
pub product_name: String,
|
||||
pub version: String,
|
||||
pub description: Option<String>,
|
||||
pub homepage: Option<String>,
|
||||
pub default_author: Option<String>,
|
||||
pub icons: Vec<IconConfig>,
|
||||
pub resources: Vec<ResourceConfig>,
|
||||
pub bin: Vec<BinConfig>,
|
||||
pub i18n: Vec<I18nConfig>,
|
||||
}
|
||||
|
||||
pub struct Config {
|
||||
pub package: PackageConfig,
|
||||
pub formats: Vec<PackageFormat>,
|
||||
pub signing: Option<SigningConfig>,
|
||||
pub windows: Option<WindowsConfig>,
|
||||
pub macos: Option<MacosConfig>,
|
||||
pub linux: Option<LinuxConfig>,
|
||||
}
|
||||
```
|
||||
|
||||
### PackageFormat
|
||||
|
||||
```rust
|
||||
pub enum PackageFormat {
|
||||
/// Windows MSI
|
||||
Msi(WindowsMsiConfig),
|
||||
/// Windows NSIS
|
||||
Nsis(WindowsNsisConfig),
|
||||
/// macOS DMG
|
||||
Dmg(MacosDmgConfig),
|
||||
/// macOS Bundle
|
||||
Bundle(MacosBundleConfig),
|
||||
/// Linux Deb
|
||||
Deb(DebConfig),
|
||||
/// Linux AppImage
|
||||
AppImage(AppImageConfig),
|
||||
}
|
||||
```
|
||||
|
||||
### 使用示例
|
||||
|
||||
```rust
|
||||
use cargo_packager::{Config, PackageFormat};
|
||||
|
||||
#[tokio::main]
|
||||
async fn main() -> Result<()> {
|
||||
let config = Config {
|
||||
package: PackageConfig {
|
||||
product_name: "qiming-agent".to_string(),
|
||||
version: "1.0.0".to_string(),
|
||||
description: Some("Agent Client".to_string()),
|
||||
homepage: Some("https://example.com".to_string()),
|
||||
default_author: Some("Author <author@example.com>".to_string()),
|
||||
icons: vec![IconConfig::Path {
|
||||
path: "icons/icon.png".into(),
|
||||
}],
|
||||
resources: vec![ResourceConfig {
|
||||
patterns: vec!["assets/**/*".into()],
|
||||
target: None,
|
||||
}],
|
||||
bin: vec![],
|
||||
i18n: vec![],
|
||||
},
|
||||
formats: vec![
|
||||
PackageFormat::Msi(WindowsMsiConfig::default()),
|
||||
PackageFormat::Dmg(MacosDmgConfig::default()),
|
||||
PackageFormat::Deb(DebConfig::default()),
|
||||
PackageFormat::AppImage(AppImageConfig::default()),
|
||||
],
|
||||
signing: Some(SigningConfig {
|
||||
identity: None,
|
||||
digest: Some(DigestAlgorithm::Sha256),
|
||||
timestamp: Some(TimestampConfig::default()),
|
||||
..Default::default()
|
||||
}),
|
||||
windows: Some(WindowsConfig {
|
||||
signing_config: Some(SigningConfig::default()),
|
||||
..Default::default()
|
||||
}),
|
||||
macos: Some(MacosConfig {
|
||||
signing_config: Some(SigningConfig::default()),
|
||||
provider: None,
|
||||
}),
|
||||
linux: Some(LinuxConfig {
|
||||
package: LinuxPackageFormatConfig::default(),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
// 执行打包
|
||||
cargo_packager::package(&config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## 自动更新配置
|
||||
|
||||
```rust
|
||||
pub struct UpdaterConfig {
|
||||
/// 启用自动更新
|
||||
pub active: bool,
|
||||
/// 更新端点 URL 模板
|
||||
pub endpoints: Option<Vec<String>>,
|
||||
/// 公钥
|
||||
pub pubkey: String,
|
||||
/// 苹果开发者 ID
|
||||
pub apple_id: Option<String>,
|
||||
/// Windows 证书
|
||||
pub windows: Option<WindowsUpdaterConfig>,
|
||||
}
|
||||
|
||||
impl UpdaterConfig {
|
||||
/// 默认端点模板
|
||||
pub fn default_endpoints(&self, target: &str) -> Vec<String> {
|
||||
vec![
|
||||
format!("https://updates.example.com/update/{target}/{version}"),
|
||||
format!("https://updates.example.com/update/{target}/{version}-{target}.{extension}"),
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 与 agent-client 集成场景
|
||||
|
||||
### 场景1:完整的打包配置
|
||||
|
||||
```rust
|
||||
// build.rs 或打包脚本
|
||||
|
||||
use cargo_packager::{Config, PackageFormat, PackageConfig};
|
||||
use cargo_packager::config::{MacosDmgConfig, WindowsMsiConfig, DebConfig, AppImageConfig};
|
||||
|
||||
pub async fn build_packages() -> Result<()> {
|
||||
let version = env!("CARGO_PKG_VERSION");
|
||||
let app_name = "qiming-agent";
|
||||
|
||||
let config = Config {
|
||||
package: PackageConfig {
|
||||
product_name: app_name.to_string(),
|
||||
version: version.to_string(),
|
||||
description: Some("Cross-platform AI Agent Client".to_string()),
|
||||
homepage: Some("https://example.com".to_string()),
|
||||
default_author: Some("qiming <support@example.com>".to_string()),
|
||||
icons: vec![
|
||||
IconConfig::Path {
|
||||
path: "assets/icons/icon.png".into(),
|
||||
},
|
||||
IconConfig::Path {
|
||||
path: "assets/icons/icon.ico".into(),
|
||||
},
|
||||
IconConfig::Path {
|
||||
path: "assets/icons/icon.icns".into(),
|
||||
},
|
||||
],
|
||||
resources: vec![
|
||||
ResourceConfig {
|
||||
patterns: vec![
|
||||
"assets/**/*".into(),
|
||||
"locales/**/*".into(),
|
||||
],
|
||||
target: None,
|
||||
},
|
||||
],
|
||||
bin: vec![],
|
||||
i18n: vec![],
|
||||
},
|
||||
formats: vec![
|
||||
PackageFormat::Msi(WindowsMsiConfig {
|
||||
// MSI 配置
|
||||
database_path: Some("assets/wix/database.wxs".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
PackageFormat::Dmg(MacosDmgConfig {
|
||||
// DMG 配置
|
||||
background: Some("assets/dmg/background.png".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
PackageFormat::Deb(DebConfig {
|
||||
// Debian 包配置
|
||||
priority: Some("optional".to_string()),
|
||||
section: Some("utils".to_string()),
|
||||
..Default::default()
|
||||
}),
|
||||
PackageFormat::AppImage(AppImageConfig {
|
||||
// AppImage 配置
|
||||
..Default::default()
|
||||
}),
|
||||
],
|
||||
signing: Some(SigningConfig {
|
||||
identity: std::env::var("SIGNING_IDENTITY").ok(),
|
||||
digest: Some(DigestAlgorithm::Sha256),
|
||||
timestamp: Some(TimestampConfig {
|
||||
server: Some("http://timestamp.digicert.com".to_string()),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
windows: Some(WindowsConfig {
|
||||
signing_config: Some(SigningConfig {
|
||||
identity: std::env::var("WINDOWS_CERT").ok(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
macos: Some(MacosConfig {
|
||||
signing_config: Some(SigningConfig {
|
||||
identity: std::env::var("APPLE_SIGNING_ID").ok(),
|
||||
provider: std::env::var("APPLE_PROVIDER").ok(),
|
||||
}),
|
||||
..Default::default()
|
||||
}),
|
||||
linux: Some(LinuxConfig {
|
||||
package: LinuxPackageFormatConfig::default(),
|
||||
..Default::default()
|
||||
}),
|
||||
};
|
||||
|
||||
// 执行打包
|
||||
cargo_packager::package(&config).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### 场景2:CI/CD 集成
|
||||
|
||||
```yaml
|
||||
# .github/workflows/release.yml
|
||||
|
||||
name: Release
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [created]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- os: ubuntu-latest
|
||||
targets: deb,appimage
|
||||
- os: macos-latest
|
||||
targets: dmg
|
||||
- os: windows-latest
|
||||
targets: msi
|
||||
|
||||
runs-on: ${{ matrix.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
target: ${{ matrix.target == 'deb' && 'x86_64-unknown-linux-gnu' || 'x86_64-apple-darwin' }}
|
||||
|
||||
- name: Build
|
||||
run: cargo build --release
|
||||
|
||||
- name: Package
|
||||
run: |
|
||||
cargo run --package cargo-packager -- --package-format ${{ matrix.targets }}
|
||||
|
||||
- name: Sign macOS
|
||||
if: matrix.os == 'macos-latest'
|
||||
run: |
|
||||
echo "${{ secrets.APPLE_CERT }}" | base64 -d > certificate.p12
|
||||
security create-keychain -p password build.keychain
|
||||
security default-keychain -s build.keychain
|
||||
security unlock-keychain -p password build.keychain
|
||||
security import certificate.p12 -P "" -A -t cert -f pkcs12 -k build.keychain
|
||||
codesign --deep --force --sign "${{ secrets.APPLE_SIGNING_ID }}" target/release/qiming-agent
|
||||
|
||||
- name: Sign Windows
|
||||
if: matrix.os == 'windows-latest'
|
||||
run: |
|
||||
& "C:\Program Files (x86)\Windows Kits\10\bin\10.0.22621.0\x64\signtool.exe" sign /f certificate.pfx /p ${{ secrets.WINDOWS_CERT_PASSWORD }} /fd SHA256 /tr http://timestamp.digicert.com /td SHA256 /v target/release/qiming-agent-*.msi
|
||||
|
||||
- name: Upload Assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: |
|
||||
target/release/qiming-agent-*.msi
|
||||
target/release/qiming-agent-*.dmg
|
||||
target/release/qiming-agent-*.deb
|
||||
target/release/qiming-agent-*.AppImage
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
```
|
||||
|
||||
### 场景3:自动更新配置
|
||||
|
||||
```rust
|
||||
// 打包时生成更新配置
|
||||
|
||||
pub struct UpdaterConfig {
|
||||
/// 更新服务器地址
|
||||
pub update_url: String,
|
||||
|
||||
/// 公钥(用于签名验证)
|
||||
pub public_key: String,
|
||||
|
||||
/// 是否启用自动更新
|
||||
pub enable_auto_update: bool,
|
||||
}
|
||||
|
||||
impl UpdaterConfig {
|
||||
/// 生成更新配置模板
|
||||
pub fn generate_app_config(&self) -> String {
|
||||
format!(r#"
|
||||
[updater]
|
||||
enabled = {}
|
||||
url = "{}"
|
||||
"#, self.enable_auto_update, self.update_url)
|
||||
}
|
||||
|
||||
/// 获取更新端点
|
||||
pub fn get_update_endpoints(&self, version: &str, target: &str) -> Vec<String> {
|
||||
vec![
|
||||
format!("{}/{}/{}/qiming-agent_{}_{}.{}",
|
||||
self.update_url, version, target, version, target, self.extension(target)),
|
||||
]
|
||||
}
|
||||
|
||||
fn extension(target: &str) -> &str {
|
||||
match target {
|
||||
_ if target.contains("windows") => "msi",
|
||||
_ if target.contains("darwin") => "dmg",
|
||||
_ if target.contains("linux") => if target.contains("deb") { "deb" } else { "AppImage" },
|
||||
_ => "tar.gz",
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 场景4:资源文件处理
|
||||
|
||||
```rust
|
||||
// 打包时的资源处理
|
||||
|
||||
pub struct ResourceProcessor {
|
||||
// 资源目录
|
||||
resource_dir: PathBuf,
|
||||
// 输出目录
|
||||
output_dir: PathBuf,
|
||||
}
|
||||
|
||||
impl ResourceProcessor {
|
||||
pub fn new(resource_dir: &str, output_dir: &str) -> Self {
|
||||
Self {
|
||||
resource_dir: PathBuf::from(resource_dir),
|
||||
output_dir: PathBuf::from(output_dir),
|
||||
}
|
||||
}
|
||||
|
||||
/// 处理所有资源
|
||||
pub async fn process(&self) -> Result<Vec<ResourceFile>> {
|
||||
let mut resources = Vec::new();
|
||||
|
||||
// 1. 图标资源
|
||||
resources.extend(self.process_icons().await?);
|
||||
|
||||
// 2. 翻译文件
|
||||
resources.extend(self.process_locales().await?);
|
||||
|
||||
// 3. 配置文件
|
||||
resources.extend(self.process_configs().await?);
|
||||
|
||||
Ok(resources)
|
||||
}
|
||||
|
||||
async fn process_icons(&self) -> Result<Vec<ResourceFile>> {
|
||||
let icons_dir = self.resource_dir.join("icons");
|
||||
let mut icons = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(&icons_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().map(|e| e == "png" || e == "ico" || e == "icns").unwrap_or(false) {
|
||||
icons.push(ResourceFile {
|
||||
source: path.clone(),
|
||||
target: format!("icons/{}", path.file_name().unwrap()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(icons)
|
||||
}
|
||||
|
||||
async fn process_locales(&self) -> Result<Vec<ResourceFile>> {
|
||||
let locales_dir = self.resource_dir.join("locales");
|
||||
let mut locales = Vec::new();
|
||||
|
||||
for entry in fs::read_dir(&locales_dir)? {
|
||||
let entry = entry?;
|
||||
let path = entry.path();
|
||||
|
||||
if path.extension().map(|e| e == "json").unwrap_or(false) {
|
||||
locales.push(ResourceFile {
|
||||
source: path.clone(),
|
||||
target: format!("locales/{}", path.file_name().unwrap()),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(locales)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 在本项目中的使用
|
||||
|
||||
用于打包 agent-client 为各平台安装包:
|
||||
|
||||
```
|
||||
agent-client
|
||||
│
|
||||
├── cargo-packager
|
||||
│ │
|
||||
│ ├── Windows MSI/NSIS 安装包
|
||||
│ ├── macOS DMG 磁盘镜像
|
||||
│ ├── Linux Deb 包
|
||||
│ └── Linux AppImage
|
||||
│
|
||||
└── 资源文件
|
||||
├── icons/ (应用图标)
|
||||
├── locales/ (多语言)
|
||||
└── assets/ (其他资源)
|
||||
```
|
||||
|
||||
## 打包命令
|
||||
|
||||
```bash
|
||||
# CLI 打包
|
||||
cargo-packager --package-format msi,dmg,deb,appimage
|
||||
|
||||
# 代码调用
|
||||
cargo_packager::package(&config).await?;
|
||||
|
||||
# 单独平台打包
|
||||
cargo-packager --package-format msi # Windows
|
||||
cargo-packager --package-format dmg # macOS
|
||||
cargo-packager --package-format deb # Linux Deb
|
||||
cargo-packager --package-format appimage # Linux AppImage
|
||||
```
|
||||
Reference in New Issue
Block a user