Files
qiming/qimingclaw/specs/agent-client/vendorsdoc/cargo-packager.md

14 KiB
Raw Blame History

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

核心依赖

[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

// 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

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),
}

使用示例

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(())
}

自动更新配置

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完整的打包配置

// 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(())
}

场景2CI/CD 集成

# .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自动更新配置

// 打包时生成更新配置

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资源文件处理

// 打包时的资源处理

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/ (其他资源)

打包命令

# 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