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 行 |
assets | EmbeddedAssets,或宏参数 assets = .. | 三个文件 |
default_window_icon | 非 Windows:icons/icon.png(或 bundle > icon 里第一个 .png)解码成 RGBA;Windows:运行时从可执行文件的图标资源读 | 32×32 |
app_icon | 仅 macOS 开发模式:原始图标文件 | None |
package_info | productName / version,缺了用 env!("CARGO_PKG_*") | |
pattern | Brownfield,或 isolation 模式的资源和密钥 | Brownfield |
runtime_authority | ACL 清单表和解析结果 | 见下文 |
plugin_global_api_scripts | withGlobalTauri 时各插件全局脚本的内容 | None |
配置了托盘图标时还会多一句 context.set_tray_icon(..),开发模式下多一句 with_config_parent(..)。整段代码的 7 897 行里,约 7 580 行是 RuntimeAuthority::new 的第一个参数:九个核心插件每个命令一对 allow-* / deny-* 权限,全部写成 BTreeMap::insert。即使应用一个能力都没有,这张表也一样大。
为什么要包两层:外层 fn inner 的注释说,包成函数是为了让 rust-analyzer 快一点(#14457),资源作为参数传进去,是为了用户自己传 assets = .. 时,函数项不去捕获调用处的环境。里层的线程是为了栈。
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:
- 读文件,算出键:去掉目录前缀,补一个根,
dist/js/app.js变成/js/app.js(Windows 上把分隔符换成/)。 - 调
map回调改写内容。普通资源的回调是map_core_assets:只处理.html,而且只在配置了 CSP 时处理(见下一节)。 - 对改写后的内容算 blake3(tauri-codegen 里 vendor 了一份 blake3 参考实现),十六进制作文件名,保留扩展名,放在
OUT_DIR/tauri-codegen-assets/下。 - 文件不存在才压缩写入;写完把修改时间设成 2000-01-01。
- 返回
(键, (原路径, 压缩后路径))。
压缩质量按 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」。
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
生成的代码用 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 里只有宏展开时已经存在的文件。
问: 往 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 时不内嵌 |
| CSP | csp | devCsp,没有再用 csp |
app_icon | None | macOS:原始图标文件(优先 .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、原始图标,并记下配置目录的绝对路径。