Read OSS

Tauri 工具链:CLI、构建流水线与跨平台打包

中级

前置知识

  • 第 1 篇:架构与 Crate 全景图
  • 第 2 篇:应用生命周期与 Builder 模式
  • 熟悉 build.rs 脚本和 Cargo 构建流程
  • 了解平台特定打包的基本概念

Tauri 工具链:CLI、构建流水线与跨平台打包

前几篇文章我们聚焦于 Tauri 应用运行时的内部机制。但一个项目究竟如何从源代码变成可分发的二进制文件?这背后涉及一个统筹全局的 CLI、一条带有编译期代码生成的多阶段构建流水线,以及一个能产出 DMG、MSI、DEB 等格式的打包器。让我们沿着这条路径逐步探索。

CLI 架构与命令结构

Tauri CLI 位于 crates/tauri-cli,以两种形式分发:Cargo 二进制包(cargo install tauri-cli)和 npm 包(@tauri-apps/cli,通过 NAPI 封装了底层的 Rust 二进制)。

入口文件 main.rs 处理了一个微妙的问题:当作为 Cargo 子命令调用(cargo tauri dev)时,二进制文件收到的参数是 cargo-tauri tauri dev——tauri 出现了两次。主函数通过检查二进制名称是否为 cargo-tauri 并查看第一个参数来识别这种情况:

Some("cargo-tauri") => {
    if args.peek().and_then(|s| s.to_str()) == Some("tauri") {
        args.next(); // remove the extra cargo subcommand
        Some("cargo tauri".into())
    } else {
        Some("cargo-tauri".into())
    }
}

命令结构通过 Clap 的 derive API 在 lib.rs 中定义:

graph TD
    CLI["cargo tauri"] --> init["init"]
    CLI --> dev["dev"]
    CLI --> build["build"]
    CLI --> bundle["bundle"]
    CLI --> android["android"]
    CLI --> ios["ios"]
    CLI --> add["add"]
    CLI --> remove["remove"]
    CLI --> plugin["plugin"]
    CLI --> icon["icon"]
    CLI --> signer["signer"]
    CLI --> info["info"]
    CLI --> migrate["migrate"]
    CLI --> permission["permission"]
    CLI --> capability["capability"]
    CLI --> inspect["inspect"]

devbuild 是两条核心工作流。addremove 用于管理插件依赖。permissioncapability 则负责管理 ACL 文件——这也从侧面反映出安全系统在开发工作流中的核心地位。

配置解析

Tauri 支持三种配置格式:JSON(tauri.conf.json)、JSON5(tauri.conf.json5,需启用 config-json5 feature)和 TOML(Tauri.toml,需启用 config-toml feature)。

Config 结构体 包含所有应用配置项:

pub struct Config {
    pub schema: Option<String>,
    pub product_name: Option<String>,
    pub main_binary_name: Option<String>,
    pub version: Option<String>,
    pub identifier: String,
    pub app: AppConfig,
    pub build: BuildConfig,
    pub bundle: BundleConfig,
    pub plugins: PluginConfig,
}
flowchart LR
    FILE["tauri.conf.json<br/>tauri.conf.json5<br/>Tauri.toml"] --> PARSE["Config::parse()"]
    ENV["TAURI_CONFIG env var<br/>(JSON string)"] --> MERGE["deep merge"]
    PLATFORM["Platform-specific<br/>tauri.macos.conf.json"] --> MERGE
    PARSE --> MERGE
    MERGE --> CONFIG["Final Config struct"]
    CONFIG --> CODEGEN["tauri-codegen"]
    CONFIG --> CLI["CLI commands"]

TAURI_CONFIG 环境变量是 CLI 向 Cargo 构建过程传递配置覆盖项的机制。CLI 读取配置后,会合并平台特定的覆盖配置(如 tauri.macos.conf.json),将最终结果序列化为 JSON,并设置为 TAURI_CONFIG。在编译阶段,tauri-codegen 中的 get_config() 会读取该变量,并与文件中的配置进行深度合并。

提示: TAURI_CONFIG 环境变量接受任意合法的 JSON 字符串,非常适合用于 CI/CD 场景中的配置覆盖。例如,TAURI_CONFIG='{"identifier":"com.example.staging"}' cargo tauri build 可以在不修改任何配置文件的情况下覆盖 identifier。

build.rs 流水线:tauri-build

用户的 build.rs 调用 tauri-build,由其统筹编译期的初始化工作:

sequenceDiagram
    participant BS as build.rs
    participant TB as tauri-build
    participant ACL as ACL Resolution
    participant CG as tauri-codegen

    BS->>TB: tauri_build::build()
    TB->>ACL: Resolve capabilities & permissions
    ACL-->>TB: Resolved ACL struct
    TB->>TB: Generate Windows manifest (.rc)
    TB->>TB: Copy resources & external binaries
    TB->>TB: Set cargo:rerun-if-changed directives
    TB->>CG: Generate Context (if codegen feature)
    CG-->>TB: tauri-build-context.rs

ACL 解析步骤(位于 crates/tauri-build/src/acl.rs)尤为关键——它从每个依赖的 $OUT_DIR 中发现所有插件清单,将其与应用的 capability 文件合并,解析完整的权限树,并将结果序列化写入,供 generate_context!(或 codegen feature)在编译时嵌入。

在 Windows 上,tauri-build 还会为应用图标和版本信息生成资源文件(.rc),并处理 Visual C++ 运行时的链接策略(静态链接或动态链接)。

编译期代码生成:资源与 Context

代码生成流水线将配置和前端资源转换为 Rust 代码形式的 Context 结构体。如第 2 篇所述,这一过程由 generate_context!(过程宏)或带 codegen feature 的 tauri-build 触发。

InvokeInitializationScript 模板值得重点关注:

#[derive(Template)]
#[default_template("../scripts/ipc-protocol.js")]
pub(crate) struct InvokeInitializationScript<'a> {
    pub(crate) process_ipc_message_fn: &'a str,
    pub(crate) os_name: &'a str,
    pub(crate) fetch_channel_data_command: &'a str,
    pub(crate) invoke_key: &'a str,
}

这段代码会生成在每个 webview 中初始化 window.__TAURI_INTERNALS__ 所需的 JavaScript。其中 process_ipc_message_fn 来自 PROCESS_IPC_MESSAGE_FN,本质上是内联自 scripts/process-ipc-message-fn.js 的 JavaScript 代码;invoke key 则是 Builder::new() 中生成的随机令牌。

打包器:跨平台打包

tauri-bundler crate 负责将编译好的二进制文件包装成各平台的安装包。CLI 的 build 命令以 release 模式编译 Rust 项目,然后为每个配置的目标平台调用打包器:

flowchart TD
    BINARY["Compiled binary"] --> BUNDLER["tauri-bundler"]
    BUNDLER --> MAC{"macOS?"}
    BUNDLER --> WIN{"Windows?"}
    BUNDLER --> LIN{"Linux?"}

    MAC -->|"Yes"| APP[".app bundle"]
    MAC -->|"Yes"| DMG["DMG installer"]
    WIN -->|"Yes"| MSI["MSI installer"]
    WIN -->|"Yes"| NSIS["NSIS installer"]
    LIN -->|"Yes"| DEB["DEB package"]
    LIN -->|"Yes"| RPM["RPM package"]
    LIN -->|"Yes"| APPIMAGE["AppImage"]

每个打包器从配置中读取 BundleConfig,获取应用图标、标识符、文件关联、版权信息和外部签名命令等设置。打包器还负责处理需要包含在安装包中的 sidecar 二进制文件和资源文件。

macOS 打包器会创建 .app bundle 的目录结构,根据配置生成 Info.plist,并可选地调用 tauri-macos-sign 进行代码签名和公证。Windows 打包器同时支持 MSI(通过 WiX)和 NSIS,并提供机器级和用户级安装选项。Linux 打包器则生成带有正确依赖关系的 Debian 包、RPM spec 文件以及自包含的 AppImage 文件。

开发模式:tauri dev

tauri dev 命令提供了热重载的开发体验。它会启动前端开发服务器(Vite、webpack 等),然后以 dev cargo profile 编译并运行 Rust 后端。

其核心机制是 PROXY_DEV_SERVER 常量

pub(crate) const PROXY_DEV_SERVER: bool = cfg!(all(dev, mobile));

在桌面端的开发模式下,webview 直接从开发服务器 URL(如 http://localhost:1420)加载内容。tauri:// 协议处理器 仍然注册,但主要作为回退使用。

移动端的情况则有所不同。移动端 webview 无法使用 localhost,因此 PROXY_DEV_SERVERtruetauri:// 协议会将所有请求代理到开发服务器。这样既保证了移动端 webview 获得安全上下文(某些 Web API 所必需),又能从开发服务器加载最新内容。

提示: tauri dev 命令会监听 Rust 源文件的变更并自动重新编译。但由于前端资源由开发服务器提供(而非嵌入二进制),前端的修改会立即生效,无需重新编译 Rust。这种分离机制正是 Tauri 开发体验高效的关键所在。

在本系列的最后一篇文章中,我们将深入最底层——Tauri 与平台之间的运行时抽象、用于线程安全通信的 dispatcher 模式,以及同一套代码库如何同时覆盖桌面和移动端。