Rust in Everything
基于 tauri 2.12.5

generate_context!

主线位置:启动主线第一步。tauri::Builder::default().run(tauri::generate_context!()) 里的这个宏在编译期执行,产出一个构造 Context 的表达式:配置、内嵌的前端资源、CSP 哈希、图标、包信息、isolation 模式、ACL 解析结果都在里面。运行时 Builder::build 拿到的就是这个值,见 Builder 与 App。

flowchart TB
  macro["generate_context!"] --> items["ContextItems 解析参数"]
  items --> getcfg["get_config 读配置"]
  getcfg --> codegen["context_codegen"]
  codegen --> assets["EmbeddedAssets 压缩资源"]
  codegen --> icon["CachedIcon 图标"]
  codegen --> acl["Resolved::resolve"]
  codegen --> pattern["Pattern"]
  outdir["OUT_DIR"] -->|"acl-manifests.json"| acl
  assets -->|"写压缩文件"| outdir
  assets --> tokens["Context::new 表达式"]
  icon --> tokens
  acl --> tokens
  pattern --> tokens

宏分两层:tauri-macros 的 generate_context 解析参数、读配置,真正生成代码的是 tauri-codegen 的 context_codegen。拆成两个 crate,是因为 tauri-build 开 codegen feature 时也要在 build script 里调 context_codegen,而 build script 不能依赖一个 proc-macro crate 的内部函数。

本页的实验 crate 是 build-ctx:从模板 base 复制,dist/ 里放一个带内联脚本、<style>、外链脚本的 index.html,一个 js/app.js,一个 style.css;配置里设了 CSP。main 不建窗口,只构造 Context 再把内嵌资源读出来打印。另用 build-acl(有能力文件)的展开对照 ACL 部分。平台:M2 Max、macOS 27、rustc 1.99.0,cargo-expand 1.0.104。没特别说明时是 debug 构建、带 --features tauri/custom-protocol。

宏的入口

ContextItems 接受几种写法:第一个参数可以是配置文件路径(相对路径按 CARGO_MANIFEST_DIR 解析,默认 tauri.conf.json),之后可以跟一个路径表示 tauri 的根路径(默认 ::tauri),以及 capabilities = [..](额外的能力文件)、assets = ..(用自己的 Assets 实现代替内嵌资源)、test = true 三个键值。

generate_context 决定是不是开发模式的方式只有一行:dev: cfg!(not(feature = "custom-protocol"))。这是 tauri-macros 自己的 feature,由 tauri/custom-protocol 打开,所以和上一页里 tauri 的 build.rs 打印的 cargo:dev= 同源。get_config 再读一遍配置文件,合并 TAURI_CONFIG,反序列化成 Config 时临时把工作目录切到配置文件所在目录。任何一步出错,宏输出 compile_error!(错误文本),错误出现在 generate_context!() 那一行上。

展开的骨架

build-ctx 的展开有 7 897 行(cargo expand 会把 include_bytes! 也展开成字节串,实际的宏输出里是 include_bytes!("<路径>"))。去掉字面量,骨架是这样:

    let ctx: tauri::Context<tauri::Wry> = {
        fn inner<R: ::tauri::Runtime, A: ::tauri::Assets<R> + 'static>(
            assets: A,
        ) -> ::tauri::Context<R> {
            let thread = ::std::thread::Builder::new()
                .name(String::from("generated tauri context creation"))
                .stack_size(8 * 1024 * 1024)
                .spawn(move || {
                    #[allow(unused_mut, clippy::let_and_return)]
                    let mut context = ::tauri::Context::new(
                        ::tauri::utils::config::Config { /* ... 约 155 行 */ },
                        ::std::boxed::Box::new(assets),
                        ::std::option::Option::Some(
                            ::tauri::image::Image::new(b"\xc0@ \xff...", 32u32, 32u32),
                        ),
                        ::std::option::Option::None,
                        ::tauri::PackageInfo {
                            name: "build-ctx".to_string(),
                            version: "0.1.0".to_string().parse().unwrap(),
                            authors: "",
                            description: "",
                            crate_name: "build-ctx",
                        },
                        ::tauri::Pattern::Brownfield,
                        ::tauri::ipc::RuntimeAuthority::new(
                            { /* ... 清单表,约 7 580 行 */ },
                            ::tauri::utils::acl::resolved::Resolved {
                                has_app_acl: false,
                                allowed_commands: ::std::collections::BTreeMap::new(),
                                denied_commands: ::std::collections::BTreeMap::new(),
                                command_scope: ::std::collections::BTreeMap::new(),
                                global_scope: ::std::collections::BTreeMap::new(),
                            },
                        ),
                        ::std::option::Option::None,
                    );
                    context
                })
                .expect("unable to create thread with 8MiB stack");
            match thread.join() {
                Ok(context) => context,
                Err(_) => {
                    // ... 打印 "the generated Tauri `Context` panicked during creation"
                    ::std::process::exit(101);
                }
            }
        }
        inner({
            #[allow(unused_imports)]
            use ::tauri::utils::assets::{CspHash, EmbeddedAssets, phf, phf::phf_map};
            EmbeddedAssets::new(/* ... 见下文 */)
        })
    };

Context::new 的八个参数依次是:

参数来源build-ctx 里的值
config整个 Config 写成结构体字面量约 155 行
assetsEmbeddedAssets,或宏参数 assets = ..三个文件
default_window_icon非 Windows:icons/icon.png(或 bundle > icon 里第一个 .png)解码成 RGBA;Windows:运行时从可执行文件的图标资源读32×32
app_icon仅 macOS 开发模式:原始图标文件None
package_infoproductName / version,缺了用 env!("CARGO_PKG_*")
patternBrownfield,或 isolation 模式的资源和密钥Brownfield
runtime_authorityACL 清单表和解析结果见下文
plugin_global_api_scriptswithGlobalTauri 时各插件全局脚本的内容None

配置了托盘图标时还会多一句 context.set_tray_icon(..),开发模式下多一句 with_config_parent(..)。整段代码的 7 897 行里,约 7 580 行是 RuntimeAuthority::new 的第一个参数:九个核心插件每个命令一对 allow-* / deny-* 权限,全部写成 BTreeMap::insert。即使应用一个能力都没有,这张表也一样大。

为什么要包两层:外层 fn inner 的注释说,包成函数是为了让 rust-analyzer 快一点(#14457),资源作为参数传进去,是为了用户自己传 assets = .. 时,函数项不去捕获调用处的环境。里层的线程是为了栈。

📝NOTE
冷知识:Context 为什么在另一个线程里构造

Context::new 的参数是一个巨大的表达式,debug 构建里这些临时值全压在栈上。#10734(首次发布于 tauri-codegen 2.0.0-rc.7,是 2.0 的一个 rc 版本)之前,它直接在调用 generate_context!() 的线程上求值,也就是主线程。提交说明写的是:这是 #9882 的权宜之计,Windows 主线程的栈只有 1 MiB,其他平台是 8 MiB;真正的修法是减小生成代码的栈占用,「but with lots a plugins, there are lots of ACL configurations」。CHANGELOG 只有一句「Generate context in a separate thread to prevent a stack overflow.」所以现在每个 tauri 应用启动时都会先起一个名为 generated tauri context creation、栈大小 8 MiB 的线程,构造完就 join;线程 panic 时打印一行再 exit(101)。

上一节那张 7 580 行的表,就是提交说明里「lots of ACL configurations」的样子。

内嵌资源

资源从哪里来,由 context_codegen 开头的分支决定:

  let assets = if let Some(assets) = assets {
    quote!(#assets)
  } else if dev && config.build.dev_url.is_some() {
    let assets = EmbeddedAssets::default();
    quote!(#assets)
  } else {
    let assets = match &config.build.frontend_dist {
      Some(url) => match url {
        FrontendDist::Url(_url) => Default::default(),
        FrontendDist::Directory(path) => {
          let assets_path = config_parent.join(path);
          if !assets_path.exists() {
            panic!(
              "The `frontendDist` configuration is set to `{path:?}` but this path doesn't exist"
            )
          }
          EmbeddedAssets::new(assets_path, &options, map_core_assets(&options))?
        }
        // ...

开发模式并且配了 devUrl 才不内嵌;开发模式但没有 devUrl 时,照样内嵌 frontendDist。frontendDist 是 URL 时也不内嵌,窗口直接加载那个地址。

EmbeddedAssets::new 用 WalkDir 走一遍目录(跟随符号链接、只要文件),每个文件交给 compress_file:

  1. 读文件,算出键:去掉目录前缀,补一个根,dist/js/app.js 变成 /js/app.js(Windows 上把分隔符换成 /)。
  2. 调 map 回调改写内容。普通资源的回调是 map_core_assets:只处理 .html,而且只在配置了 CSP 时处理(见下一节)。
  3. 对改写后的内容算 blake3(tauri-codegen 里 vendor 了一份 blake3 参考实现),十六进制作文件名,保留扩展名,放在 OUT_DIR/tauri-codegen-assets/ 下。
  4. 文件不存在才压缩写入;写完把修改时间设成 2000-01-01。
  5. 返回 (键, (原路径, 压缩后路径))。

压缩质量按 cfg!(debug_assertions) 选:

  /// Use highest compression level for release, the fastest one for everything else
  #[cfg(feature = "compression")]
  fn compression_settings() -> BrotliEncoderParams {
    let mut settings = BrotliEncoderParams::default();

    // the following compression levels are hand-picked and are not min-maxed.
    // they have a good balance of runtime vs size for the respective profile goals.
    // see the "brotli" section of this comment https://github.com/tauri-apps/tauri/issues/3571#issuecomment-1054847558
    if cfg!(debug_assertions) {
      settings.quality = 2
    } else {
      settings.quality = 9
    }

    settings
  }

compression 是 tauri 的默认 feature;关掉它,文件原样写入,运行时原样返回。代码生成在 ToTokens for EmbeddedAssets:

    for (key, (input, output)) in &self.assets {
      let key: &str = key.as_ref();
      let input = input.display().to_string();
      let output = output.display().to_string();

      // add original asset as a compiler dependency, rely on dead code elimination to clean it up
      assets.append_all(quote!(#key => {
        const _: &[u8] = include_bytes!(#input);
        include_bytes!(#output)
      },));
    }

每个资源 include_bytes! 两次:压缩后的文件是真正的值;原文件放进一个丢弃的 const _,只为让 rustc 把它记进 dep-info,原文件一改,cargo 就知道要重编这个 crate。三个资源最后进了一张编译期构造的 phf::Map(完美哈希表),cargo expand 看到的是:

            EmbeddedAssets::new(
                phf::Map {
                    key: 16287231350648472473u64,
                    disps: &[(0u32, 0u32)],
                    entries: &[
                        (
                            "/js/app.js",
                            {
                                const _: &[u8] = b"console.log(\"app.js\");\n";
                                b"\x0b\x0b\x80console.log(\"app.js\");\n\x03"
                            },
                        ),
                        // "/style.css"、"/index.html" 同样
                    ],
                },
                &[
                    CspHash::Script(
                        "'sha256-77VSFaGE074r8N204WnaH95CtkK8u9BWRHWFeKCJgLg='",
                    ),
                ],
                phf::Map {
                    key: 16287231350648472473u64,
                    disps: &[(0u32, 0u32)],
                    entries: &[
                        (
                            "/index.html",
                            &[
                                CspHash::Script(
                                    "'sha256-LULgW+fc+QZHesIcL3pnGPtd1KA8V7qPdxWjJmIfTFQ='",
                                ),
                            ],
                        ),
                    ],
                },
            )

运行时 EmbeddedAssets::get 每次调用都现场 brotli 解压一次,返回 Cow::Owned;不缓存解压结果。tauri:// 协议怎么用它返回页面,见 tauri:// 协议。build-ctx 的 main:

use tauri::utils::assets::AssetKey;

fn main() {
  let ctx: tauri::Context<tauri::Wry> = tauri::generate_context!();
  let assets = ctx.assets();
  println!("csp config: {:?}", ctx.config().app.security.csp.as_ref().map(|c| c.to_string()));
  for (key, compressed) in assets.iter() {
    let raw = assets.get(&AssetKey::from(key.as_ref())).unwrap();
    println!("asset {key}: embedded {} bytes, decompressed {} bytes", compressed.len(), raw.len());
  }
  for hash in assets.csp_hashes(&AssetKey::from("index.html")) {
    println!("csp hash for /index.html: {} {}", hash.directive(), hash.hash());
  }
  let html = assets.get(&AssetKey::from("index.html")).unwrap();
  println!("--- /index.html as embedded ---\n{}", String::from_utf8_lossy(&html));
}

输出的前几行:

csp config: Some("default-src 'self'; script-src 'self'")
asset /js/app.js: embedded 27 bytes, decompressed 23 bytes
asset /style.css: embedded 29 bytes, decompressed 25 bytes
asset /index.html: embedded 277 bytes, decompressed 418 bytes

几十字节的小文件压缩后反而大了 4 字节,这是 brotli 帧头的固定开销;EmbeddedAssets::get 里的注释也提到「with the exception of extremely small files」。

❗IMPORTANT
动脑筋:同一个 build-ctx,改用 cargo build --release 编译,/index.html 内嵌的是多少字节?解压后呢?

压缩发生在编译期,而且结果按内容哈希缓存,这就有了下面两个问题:重编时会不会重复压缩,以及 cargo 怎么判断要不要重编。

文件名和修改时间

文件名是内容的 blake3,所以「文件已经存在」就等于「内容相同的压缩结果已经在了」,compress_file 直接跳过。改一个资源,生成一个新文件名,旧文件留在 OUT_DIR 里不删。图标、macOS 开发模式的 Info.plist 走的是 Cached:同样以 blake3 命名,写到 OUT_DIR 根下,生成的代码是 include_bytes!(concat!(env!("OUT_DIR"), "/<哈希>"))。上一页 OUT_DIR 里那个 4 096 字节的 cb18… 就是图标的 RGBA。

这些文件的修改时间都是 2000 年 1 月 1 日,由 backdate 设置:

-rw-r--r--  1 user staff  277 Jan  1  2000 1a1985071d161ec5a4c0d4a51c0150da434639b38e05a5f8242ca99f7c2dc9e8.html
-rw-r--r--  1 user staff   27 Jan  1  2000 1f58fe441bc3c5feb9dd4b087f01e94f663199528c7a653ffe0d718e12f69ddf.js
-rw-r--r--  1 user staff   29 Jan  1  2000 8a77dda5536a6f06cf98cdabdc9fee5a5252468b1c89940395830bdee9eb7bf6.css
📝NOTE
冷知识:backdate 是本卷所用版本才加的

生成的代码用 include_bytes! 引用这些文件,cargo 就像对待源文件一样检查它们的修改时间;而它们是过程宏在编译过程中写出来的,修改时间必然晚于「这次编译开始的时刻」,下一次 cargo build 就会认为它们变了,再编一遍。#16324(首次发布于 tauri-codegen 2.7.4,随 tauri 2.12.5 发布)把修改时间改成固定的 2000-01-01,CHANGELOG 原文:「Set the modification time of the asset, icon and macOS Info.plist files generate_context! writes to OUT_DIR to 2000-01-01 UTC, so the first cargo build without changes after a build that created them no longer recompiles the app crate.」源码注释补充了两点:文件名已经是内容哈希,不需要靠时间区分版本;选 2000 年而不是 Unix 纪元,是因为 FAT 和 exFAT 存不下 1970 年。

原文件靠那个 const _: &[u8] = include_bytes!(原路径) 进入 dep-info。反过来,dep-info 里只有宏展开时已经存在的文件。

📝NOTE
没有蠢问题

问: 往 dist/ 里新加一个文件,cargo build 会把它嵌进去吗?

答: 不一定。cargo 只看 dep-info 里记下的文件,目录本身不在里面,新文件不会让 crate 变脏。build-ctx(已经建了 capabilities 目录,排除上一页说的每次重编)加一个 dist/new.txt 再构建,cargo 报 Fresh build-ctx,程序列出的资源里没有 /new.txt;接着改一下 style.css,cargo 报「the file dist/style.css has changed」,重编之后 /new.txt 才出现。前端构建工具通常每次都重写 index.html,一般碰不到;手工往 dist 里放文件时要 touch 一个已有文件,或者 cargo clean -p <应用 crate>。

CSP 哈希和 nonce 占位符

配置里没有 CSP(开发模式看 devCsp,没有再看 csp)时,这一节什么都不发生:HTML 原样压缩,哈希表为空。build-ctx 配了 "csp": "default-src 'self'; script-src 'self'",于是 options.with_csp(),两个地方开始干活。

一是 CspHashes::add_if_applicable:每个 .js / .mjs 文件,把 \r\n 和单独的 \r 规范成 \n 后算 sha256,记进 scripts。这一组哈希最后成为 global_hashes,每个 HTML 的 CSP 都会带上。

二是 map_core_assets 对每个 .html:

  move |key, path, input, csp_hashes| {
    if path.extension() == Some(OsStr::new("html")) {
      #[allow(clippy::collapsible_if)]
      if csp {
        let document = parse_doc(String::from_utf8_lossy(input).into_owned());

        inject_nonce_token(&document, &dangerous_disable_asset_csp_modification);

        if dangerous_disable_asset_csp_modification.can_modify("script-src") {
          inject_script_hashes(&document, key, csp_hashes);
        }

        *input = serialize_doc(&document);
      }
    }
    Ok(())
  }

inject_nonce_token 给 script[src^='http'](外链到 http(s) 的脚本)加 nonce="__TAURI_SCRIPT_NONCE__",给每个 <style> 加 nonce="__TAURI_STYLE_NONCE__",已经有 nonce 的跳过。inject_script_hashes 对 script:not(:empty),也就是有内容的内联脚本,算 sha256,按这个 HTML 的键存进 inline_scripts。dangerousDisableAssetCspModification 可以按指令关掉其中一部分。

build-ctx 的源文件和输出:

<!doctype html>
<html>
<head>
<style>body { color: #333; }</style>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.example.com/lib.js"></script>
<script type="module" src="js/app.js"></script>
</head>
<body><p id="out">loading</p>
<script>document.getElementById('out').textContent = 'inline ran';</script>
<script></script>
</body>
</html>
csp hash for /index.html: script-src 'sha256-77VSFaGE074r8N204WnaH95CtkK8u9BWRHWFeKCJgLg='
csp hash for /index.html: script-src 'sha256-LULgW+fc+QZHesIcL3pnGPtd1KA8V7qPdxWjJmIfTFQ='
--- /index.html as embedded ---
<!DOCTYPE html><html><head>
<style nonce="__TAURI_STYLE_NONCE__">body { color: #333; }</style>
<link rel="stylesheet" href="style.css">
<script src="https://cdn.example.com/lib.js" nonce="__TAURI_SCRIPT_NONCE__"></script>
<script type="module" src="js/app.js"></script>
</head>
<body><p id="out">loading</p>
<script>document.getElementById('out').textContent = 'inline ran';</script>
<script></script>


</body></html>

第一个哈希是 js/app.js(全局),第二个是那段内联脚本(只属于 /index.html),空的 <script></script> 没有哈希。用 Python 单独算一遍,两个值一致:

77VSFaGE074r8N204WnaH95CtkK8u9BWRHWFeKCJgLg= b'console.log("app.js");\n'
LULgW+fc+QZHesIcL3pnGPtd1KA8V7qPdxWjJmIfTFQ= b"document.getElementById('out').textContent = 'inline ran';"

外部 style.css 和 <style> 都不算哈希;<style> 靠 nonce 放行。HTML 经过解析再序列化:<!doctype html> 变成了大写,<html> 后的换行没了,末尾多出空行,再加上两个 nonce 属性,解压后的 418 字节比 359 字节的源文件长。

占位符在编译期写进 HTML,真正的 nonce 是运行时生成的:返回页面时,manager/mod.rs 的 replace_csp_nonce 把每个占位符换成一个新的随机 u64,再把这些 nonce 和上面的哈希拼进 CSP。这一段见 CSP、原型冻结与隔离模式。isolation 模式下,隔离应用的 HTML 走另一个回调 map_isolation,额外注入隔离脚本、内联 iframe 的样式哈希,也在那一页讲。

图标、包信息和 Info.plist

CachedIcon::new_png 用 png crate 解码,要求颜色类型是 RGBA,否则在宏展开时 panic「icon ... is not RGBA」;文件不存在时 panic「failed to open icon ...」,所以没有 icons/icon.png 的项目在 generate_context!() 这一行编译失败。.ico 取面积最大的那一项。解码出的 RGBA 写进 OUT_DIR,生成 Image::new(include_bytes!(..), 宽, 高),运行时不再解码。

PackageInfo 的 authors、description、crate_name 永远用 env!("CARGO_PKG_*"),在应用 crate 编译时求值;name 和 version 优先取配置,配置里的版本号在宏展开时就用 semver 校验一遍。

macOS 的 Info.plist 只在开发模式(且不是 test = true)时嵌入:读项目里的 Info.plist(没有就从空字典开始),写入 CFBundleName、CFBundleShortVersionString、CFBundleVersion,序列化成 XML,用 embed_plist 放进 __TEXT,__info_plist 段。发布模式下这一段代码不生成。

ACL:读 build script 留下的两份文件

  let acl_file_path = out_dir.join(ACL_MANIFESTS_FILE_NAME);
  let acl: BTreeMap<String, Manifest> = if acl_file_path.exists() {
    let acl_file =
      std::fs::read_to_string(acl_file_path).expect("failed to read plugin manifest map");
    serde_json::from_str(&acl_file).expect("failed to parse plugin manifest map")
  } else {
    Default::default()
  };

  let capabilities_file_path = out_dir.join(CAPABILITIES_FILE_NAME);
  let capabilities_from_files = if capabilities_file_path.exists() {
    let capabilities_json =
      std::fs::read_to_string(&capabilities_file_path).expect("failed to read capabilities");
    serde_json::from_str(&capabilities_json).expect("failed to parse capabilities")
  } else {
    Default::default()
  };
  let capabilities = get_capabilities(
    &config,
    capabilities_from_files,
    additional_capabilities.as_deref(),
  )
  .unwrap();

  let resolved = Resolved::resolve(&acl, capabilities, target).expect("failed to resolve ACL");

acl-manifests.json 和 capabilities.json 是 tauri-build 写的;get_capabilities 在这里第二次合并配置里的能力,再加上宏参数 capabilities = [..] 指定的文件。Resolved::resolve 把能力展开成「命令 → 允许它的窗口、webview、来源和作用域」的表,规则见编译期解析。build-acl 的能力允许了 core:default 和两个应用命令,展开里的 Resolved 开头是:

                                ::tauri::utils::acl::resolved::Resolved {
                                    has_app_acl: true,
                                    allowed_commands: {
                                        let mut map = ::std::collections::BTreeMap::new();
                                        map.insert(
                                            "greet".into(),
                                            ::alloc::boxed::box_assume_init_into_vec_unsafe(
                                                ::alloc::intrinsics::write_box_via_move(
                                                    ::alloc::boxed::Box::new_uninit(),
                                                    [
                                                        ::tauri::utils::acl::resolved::ResolvedCommand::new(
                                                            ::tauri::utils::acl::ExecutionContext::Local,
                                                            ::tauri::utils::acl::resolved::ResolvedCommandReference::new(
                                                                "default".into(),
                                                                "allow-greet".into(),
                                                            ),
                                                            ::alloc::boxed::box_assume_init_into_vec_unsafe(
                                                                ::alloc::intrinsics::write_box_via_move(
                                                                    ::alloc::boxed::Box::new_uninit(),
                                                                    ["main".parse().unwrap()],
                                                                ),
                                                            ),
                                                            ::alloc::vec::Vec::new(),
                                                            ::core::option::Option::None,
                                                        ),
                                                    ],
                                                ),
                                            ),
                                        );
                                        map.insert(
                                            "plugin:app|bundle_type".into(),
                                            // ...

这两样最后交给 #root::runtime_authority!(清单表, 解析结果)。tauri 里这个宏有两个版本,按 tauri 自己的编译条件选:开了 dynamic-acl feature(默认开)或 debug_assertions 时,展开成 RuntimeAuthority::new(acl, resolved),清单表留在二进制里,运行时可以 add_capability,debug 构建的拒绝信息也要用它列出相关权限;两者都没有时,第一个参数被丢掉,只留 Resolved。代码生成这边不知道 tauri 开了什么 feature,所以总是两样都生成,由宏来取舍。

开发模式多出来的东西

项发布模式(custom-protocol)开发模式
前端资源内嵌 frontendDist有 devUrl 时不内嵌
CSPcspdevCsp,没有再用 csp
app_iconNonemacOS:原始图标文件(优先 .icns)
Info.plist不嵌入macOS:嵌入 __TEXT,__info_plist 段
配置目录不记录with_config_parent(<绝对路径>)

build-ctx 不带 custom-protocol 再编一次(它没有 devUrl,所以资源照样内嵌),OUT_DIR 里多了两个文件:

./2234847902d18d3b2159ee903406a3132e5385168e5fc47f6b7161b4c712c0c2
./777c27f18cece1ed27655053c5bcdfbffdb16896c91a172ca4f076e2cf47188c

file 认出前者是「XML 1.0 document text」(Info.plist),后者是「PNG image data, 32 x 32, 8-bit/color RGBA」(app_icon,原样的 PNG)。展开里多出 embed_info_plist! 生成的 #[link_section = "__TEXT,__info_plist,regular,no_dead_strip"] 静态量,以及 context.with_config_parent("~/exp-tauri/build-ctx")。这个绝对路径编进了二进制:开发模式下配了 devUrl 时资源没有内嵌,AssetResolver::get_for_scheme 在 #[cfg(dev)] 下改为直接读磁盘上的 frontendDist(注释原文「fall back to read from frontendDist directly」),用它拼出目录的位置。

动脑筋答案

202 字节,解压后还是 418 字节:

csp config: Some("default-src 'self'; script-src 'self'")
asset /js/app.js: embedded 27 bytes, decompressed 23 bytes
asset /style.css: embedded 29 bytes, decompressed 25 bytes
asset /index.html: embedded 202 bytes, decompressed 418 bytes

两个 OUT_DIR 里的文件对照(debug 在上,release 在下):

.../debug/build/build-ctx-1f9eb0306ef5f8ac/out/tauri-codegen-assets/:
total 24
-rw-r--r--  1 user staff  277 Jan  1  2000 1a1985071d161ec5a4c0d4a51c0150da434639b38e05a5f8242ca99f7c2dc9e8.html
-rw-r--r--  1 user staff   27 Jan  1  2000 1f58fe441bc3c5feb9dd4b087f01e94f663199528c7a653ffe0d718e12f69ddf.js
-rw-r--r--  1 user staff   29 Jan  1  2000 8a77dda5536a6f06cf98cdabdc9fee5a5252468b1c89940395830bdee9eb7bf6.css

.../release/build/build-ctx-bb5d06e22fba0ef9/out/tauri-codegen-assets/:
total 24
-rw-r--r--  1 user staff  202 Jan  1  2000 1a1985071d161ec5a4c0d4a51c0150da434639b38e05a5f8242ca99f7c2dc9e8.html
-rw-r--r--  1 user staff   27 Jan  1  2000 1f58fe441bc3c5feb9dd4b087f01e94f663199528c7a653ffe0d718e12f69ddf.js
-rw-r--r--  1 user staff   29 Jan  1  2000 8a77dda5536a6f06cf98cdabdc9fee5a5252468b1c89940395830bdee9eb7bf6.css
  • index.html 从 277 字节降到 202 字节:compression_settings 里的 cfg!(debug_assertions) 是在过程宏所在的 tauri-codegen 编译时求值的,cargo build --release 下它是 false,质量从 2 变成 9。
  • 两边的文件名一样:名字是改写后、压缩前内容的 blake3,和压缩质量无关。
  • 两个小文件在两种质量下都是原文加 4 字节,没有可压缩的东西。

小结

  • generate_context! 产出一个函数调用:在名为 generated tauri context creation、栈 8 MiB 的线程里求值 Context::new(..),起因是 Windows 主线程 1 MiB 的栈放不下这个表达式。
  • 资源:frontendDist 的每个文件按改写后内容的 blake3 命名,brotli 压缩(debug 质量 2、release 质量 9)写进 OUT_DIR/tauri-codegen-assets/,再 include_bytes! 进一张 phf::Map;原文件另 include_bytes! 一次只为登记依赖。运行时每次 get 都解压。
  • 只有开发模式且配了 devUrl 才不内嵌;往 dist 里新加文件不会触发重编。
  • 配置了 CSP 才算哈希:.js / .mjs 文件进全局哈希,有内容的内联脚本按 HTML 分开存;外链 http 脚本和 <style> 只加 nonce 占位符,由运行时替换。
  • ACL 在这里解析:读 tauri-build 写的两份 JSON,合并配置和宏参数里的能力,Resolved::resolve 后交给 runtime_authority!;默认 feature 下完整的清单表也编进二进制。
  • 开发模式多嵌入 macOS 的 Info.plist、原始图标,并记下配置目录的绝对路径。

💬 关联讨论

发起讨论
加载中...