tauri-build
主线位置:启动主线开始之前。启动主线的第一步是
generate_context!把Context编出来,而它要读的 ACL 清单和能力文件,是应用 crate 的 build script 先写进OUT_DIR的。本页讲这个 build script:build.rs里那一行tauri_build::build()从哪里拿输入、产出什么、怎么让 cargo 知道何时重跑。
flowchart TB tauribs["tauri 的 build.rs"] -->|"cargo:dev 和权限路径"| depenv["DEP_TAURI_* 环境变量"] plugbs["插件的 build.rs"] -->|links 元数据| depenv depenv --> trybuild["try_build"] conf["tauri.conf.json"] --> trybuild trybuild --> cfgs["cfg desktop mobile dev"] trybuild --> check["manifest::check"] trybuild --> aclb["acl::build"] caps["capabilities 目录"] --> aclb aclb --> outdir["OUT_DIR 两份 JSON"] aclb --> gen["gen/schemas"] outdir --> ctxmacro["generate_context!"]
本页的实验 crate 是 build-app:从模板 base 复制,build.rs 在调用 tauri_build::build() 之前先把所有 DEP_* 环境变量写进一个文件。另有 build-acl(AppManifest 加一个能力文件)和 build-mismatch(feature 和配置不一致)。依赖是 tauri =2.12.5、tauri-build =2.7.4,一律 --features tauri/custom-protocol 编译。平台:M2 Max、macOS 27、rustc 1.99.0。Windows、Linux、iOS、Android 分支只读了源码,没有实测。
两个 build script
tauri_build::build() 不是第一个跑的。tauri 本体的 Cargo.toml 里有一行 links = "Tauri",它自己的 build.rs 先跑,打印几条带自定义键的 cargo: 指令。cargo 对声明了 links 的包有一条规则:这些键值会以 DEP_<links>_<KEY> 的形式,作为环境变量交给直接依赖它、且有 build script 的包。应用 crate 依赖 tauri,所以应用的 build script 能读到它们。build-app 的 build.rs 记下来的是这些(路径中段用 ... 省略):
DEP_TAURI_CORE:APP__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-app-permission-files
DEP_TAURI_CORE:EVENT__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-event-permission-files
DEP_TAURI_CORE:IMAGE__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-image-permission-files
DEP_TAURI_CORE:MENU__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-menu-permission-files
DEP_TAURI_CORE:PATH__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-path-permission-files
DEP_TAURI_CORE:RESOURCES__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-resources-permission-files
DEP_TAURI_CORE:TRAY__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-tray-permission-files
DEP_TAURI_CORE:WEBVIEW__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-webview-permission-files
DEP_TAURI_CORE:WINDOW__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-window-permission-files
DEP_TAURI_CORE__CORE_PLUGIN___PERMISSION_FILES_PATH=.../build/tauri-fdd5acda78a81a21/out/tauri-core-permission-files
DEP_TAURI_DEV=false
DEP_TAURI_GLOBAL_API_SCRIPT_PATH=~/.cargo/registry/src/index.crates.io-1949cf8c6b5b557f/tauri-2.12.5/scripts/bundle.global.js
三类东西从这条通道过来:
- 是否开发模式。tauri 的 build.rs 看自己有没有开
custom-protocolfeature,println!("cargo:dev={dev}")。应用这边的is_dev读DEP_TAURI_DEV,读不到就 panic,提示「missingcargo:devinstruction, please update tauri to latest」。这里是false,因为编译时带了tauri/custom-protocol。 - 核心插件的权限文件。tauri 的 build.rs 里有一张
PLUGINS表,列出core:path、core:event、core:window、core:webview、core:app、core:image、core:resources、core:menu、core:tray九个核心插件的每个命令,以及它是否进默认权限集。define_permissions为每个命令生成一对allow-*/deny-*权限,写进 tauri 自己的OUT_DIR,再把「权限文件列表」的路径打印出去;core本身是一个只含默认集的清单。 - 全局 API 脚本。
withGlobalTauri时要注入的bundle.global.js的路径。
键名里的 core:app__CORE_PLUGIN__ 是 tauri-utils 的 define_permissions 拼出来的:包名以 tauri: 开头时打印 cargo:{plugin_name}__CORE_PLUGIN___PERMISSION_FILES_PATH=...,否则打印 cargo:PERMISSION_FILES_PATH=...。读的一侧 read_permissions 遍历所有 DEP_*_PERMISSION_FILES_PATH,去掉 __CORE_PLUGIN__ 和 TAURI_ 前缀,转小写、_ 换成 -,再去掉 tauri-plugin- 前缀,得到 core:app、core:window 这样的清单名。第三方插件走后一种写法:插件 crate 在自己的 Cargo.toml 里声明 links(仓库里的示例插件是 links = "tauri-plugin-sample"),应用就会收到 DEP_TAURI_PLUGIN_SAMPLE_PERMISSION_FILES_PATH,清单名是 sample。
问: 应用自己的 build script 也打印了 cargo:PERMISSION_FILES_PATH=...(见下一节的输出),谁读它?
答: 没有人。cargo:KEY=VALUE 只有在包声明了 links 时才会传给依赖它的包,应用 crate 没有 links,也没有包依赖它。这一行是共用代码的副产品:应用的权限文件和插件的权限文件走同一个 define_permissions,函数不知道调用者是谁,照样打印。对插件 crate(tauri-plugin 的构建辅助在 build/mod.rs 调同一个函数)来说,这一行就是上面那条通道的入口。
try_build 一步一步
build 只是用默认 Attributes 调 try_build,出错时把错误 println! 到标准输出、exit(1);错误以「unknown field」开头时再补一句「CLI 比 tauri-build 新」的提示。try_build 按顺序做这些事:
| 顺序 | 做什么 | 留下什么 |
|---|---|---|
| 1 | 声明 TAURI_CONFIG 变化时重跑 | cargo:rerun-if-env-changed=TAURI_CONFIG |
| 2 | 按 CARGO_CFG_TARGET_OS 定 desktop / mobile | cfg(desktop) 或 cfg(mobile) |
| 3 | 读 tauri.conf.json(及 tauri.macos.conf.json 这类平台文件,JSON Merge Patch 合并),再合并 TAURI_CONFIG 环境变量里的 JSON,反序列化成 Config | 每个配置文件一条 rerun-if-changed |
| 4 | 由 identifier 拼 Android 包名 | 两条 rustc-env(桌面上也照样打印) |
| 5 | is_dev() | cfg(dev)(开发模式才有) |
| 6 | manifest::check:核对 Cargo feature | 不一致就报错退出 |
| 7 | acl::build:权限、能力、schema | OUT_DIR 和 gen/schemas 里的文件 |
| 8 | save_global_api_scripts_paths | OUT_DIR/__global-api-script.js |
| 9 | 记下目标三元组 | rustc-env=TAURI_ENV_TARGET_TRIPLE,tauri-codegen 用它判断目标平台 |
| 10 | 把 bundle > externalBin、bundle > resources 复制到 target 目录 | cargo run 时程序旁边就有这些文件 |
| 11 | macOS:复制 bundle > macOS > frameworks、加 rpath;非开发模式设部署目标 | rustc-link-arg、rustc-env=MACOSX_DEPLOYMENT_TARGET |
| 12 | Windows:编译资源文件(图标、版本信息、应用清单),按需静态链接 VC 运行时 | 只读了源码 |
| 13 | 开了 codegen feature 且设置了 Attributes::codegen 时,在 build script 里生成 Context | 默认不开 |
第 2、5 步的 cfg 用 cfg_alias 发出:先 rustc-check-cfg 声明这个 cfg 名合法,条件成立时再 rustc-cfg。所以不成立的 mobile、dev 只有声明,没有设置,应用代码里写 #[cfg(dev)] 不会触发 unexpected_cfgs 警告。tauri 本体的 build.rs 给自己发了一套同名的 cfg,tauri 源码里的 #[cfg(desktop)]、#[cfg(dev)] 用的是那一套;这里发的是给应用代码用的。
第 10 步要知道 target 目录,而 cargo 不把它告诉 build script。target_dir_from_out_dir 从 OUT_DIR 往上找名为 build 的那一级,取它的父目录;注释写明稳定版的布局是 <target dir>/build/<pkg>-<hash>/out,新的 nightly 是 <target dir>/build/<pkg>/<hash>/out,所以不按固定层数截。
build-app 这次运行的全部输出(target-build/debug/build/build-app-*/output):
cargo:rerun-if-env-changed=TAURI_CONFIG
cargo:rustc-check-cfg=cfg(desktop)
cargo:rustc-cfg=desktop
cargo:rustc-check-cfg=cfg(mobile)
cargo:rerun-if-changed=~/exp-tauri/build-app/tauri.conf.json
cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_APP_NAME=base
cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_PREFIX=app_rustineverything_exp
cargo:rustc-check-cfg=cfg(dev)
cargo:PERMISSION_FILES_PATH=~/exp-tauri/target-build/debug/build/build-app-fff6b7aff4ec31ae/out/app-manifest/__app__-permission-files
cargo:rerun-if-changed=~/exp-tauri/build-app/capabilities
cargo:rerun-if-env-changed=REMOVE_UNUSED_COMMANDS
cargo:rustc-env=TAURI_ENV_TARGET_TRIPLE=aarch64-apple-darwin
cargo:rustc-env=MACOSX_DEPLOYMENT_TARGET=10.13
和目录里的描述对一下:没有 cargo:rustc-cfg=dev,因为不是开发模式;MACOSX_DEPLOYMENT_TARGET=10.13 是 bundle > macOS > minimumSystemVersion 的默认值,开发模式下不打印。macOS 上这个 build script 不碰 Info.plist,也不处理图标:开发模式下嵌入 Info.plist 和应用图标的是 generate_context!,见下一页;图标在 build script 里只有 Windows 分支用到(.ico 编进资源文件)。
核对 feature:「does not match the allowlist」
有几个 tauri 的 Cargo feature 和配置项一一对应。AppConfig::features 从配置推出「应该开哪些 feature」:
pub fn features(&self) -> Vec<&str> {
let mut features = Vec::new();
if self.tray_icon.is_some() {
features.push("tray-icon");
}
// TODO: Remove in v3
if self.macos_private_api {
features.push("macos-private-api");
}
if self.security.asset_protocol.enable {
features.push("protocol-asset");
}
if let PatternKind::Isolation { .. } = self.security.pattern {
features.push("isolation");
}
features.sort_unstable();
features
}
manifest::check 用 cargo_toml 解析应用的 Cargo.toml,找到 tauri 依赖(以及 [target.*] 下的同名依赖),只看 macos-private-api、protocol-asset、isolation 这几个由 CLI 管理的 feature(tray-icon 被排除在外),和上面的期望值做差集;tauri-build 依赖的 isolation feature 也按同样方式核对。差集不空就返回这个错误:
for dep in deps {
if let Err(error) = check_features(dep, &metadata) {
return Err(anyhow!("
The `{}` dependency features on the `Cargo.toml` file does not match the allowlist defined under `tauri.conf.json`.
Please run `tauri dev` or `tauri build` or {}.
", name, error));
}
}
build-mismatch 在 tauri 的 feature 里加了 protocol-asset,配置里没有 assetProtocol.enable:
error: failed to run custom build command for `build-mismatch v0.1.0 (~/exp-tauri/build-mismatch)`
Caused by:
process didn't exit successfully: `~/exp-tauri/target-build/debug/build/build-mismatch-03be60f9796511e2/build-script-build` (exit status: 1)
--- stdout
cargo:rerun-if-env-changed=TAURI_CONFIG
cargo:rustc-check-cfg=cfg(desktop)
cargo:rustc-cfg=desktop
cargo:rustc-check-cfg=cfg(mobile)
cargo:rerun-if-changed=~/exp-tauri/build-mismatch/tauri.conf.json
cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_APP_NAME=base
cargo:rustc-env=TAURI_ANDROID_PACKAGE_NAME_PREFIX=app_rustineverything_exp
cargo:rustc-check-cfg=cfg(dev)
The `tauri` dependency features on the `Cargo.toml` file does not match the allowlist defined under `tauri.conf.json`.
Please run `tauri dev` or `tauri build` or remove the `protocol-asset` feature.
错误出自应用 crate 的 build script,也就是 tauri-build 的 manifest.rs;因为 build() 用 println! 报错,它出现在 cargo 输出的 --- stdout 一段里,exit status: 1 也来自 build() 自己的 exit(1)。「allowlist」是 1.x 配置里的 allowlist 段,这个检查在 1.0 发布之前就加了(tauri-build 的 CHANGELOG:「Validate tauri dependency features under Cargo.toml matching tauri.conf.json's allowlist.」)。2.x 的配置里已经没有 allowlist,命令权限交给了 ACL,这个检查只剩下「配置用到的功能有没有编进来」这一层意思,提示文字没改。修法照提示:要么删 feature,要么在配置里打开对应项(反过来,配置开了而 feature 没开,提示是「add the ... feature」)。提示里的 tauri dev / tauri build 指 CLI 会按配置改写 Cargo.toml 里的这些 feature(tauri-cli 的 rewrite_manifest),所以用 CLI 构建的项目一般碰不到它。
收集权限
acl::build 把三个来源的权限合成一张「清单名 → Manifest」的表:
| 来源 | 怎么拿 | 清单名 |
|---|---|---|
| tauri 和插件 crate | read_plugins_manifests:上一节的 DEP_*_PERMISSION_FILES_PATH,外加插件用 GLOBAL_SCOPE_SCHEMA_PATH 传来的作用域 schema | core:window、fs 等 |
| 应用自己 | app_manifest_permissions:AppManifest::commands 列出的命令各生成一对权限,加上项目 permissions/ 目录下的 .toml / .json | __app-acl__ |
| 内联插件 | inline_plugins:Attributes::plugin 登记的插件,生成的文件放在 OUT_DIR/plugins/<name>,另读 permissions/<name>/ | 插件名 |
应用清单只在非空时加进表里(有默认集、权限集或权限之一)。这一点后面有用:表里有没有 __app-acl__,决定了运行时对 app 自己的命令查不查 ACL,见运行时检查。
build-acl 的 build.rs 换成了:
fn main() {
let attributes = tauri_build::Attributes::new()
.app_manifest(tauri_build::AppManifest::new().commands(&["greet", "report"]));
tauri_build::try_build(attributes).expect("failed to run tauri-build");
}
再加一个 capabilities/default.json:
{
"identifier": "default",
"windows": ["main"],
"permissions": ["core:default", "allow-greet", "allow-report"]
}
构建之后,项目目录里多出 permissions/autogenerated/greet.toml 和 report.toml。注意它们写在项目目录,不在 OUT_DIR:
# Automatically generated - DO NOT EDIT!
[[permission]]
identifier = "allow-greet"
description = "Enables the greet command without any pre-configured scope."
commands.allow = ["greet"]
[[permission]]
identifier = "deny-greet"
description = "Denies the greet command without any pre-configured scope."
commands.deny = ["greet"]
生成它的是 autogenerate_command_permissions,命令名里的 _ 换成 - 作标识符。随后按 permissions/**/* 收集时,这两个文件又被读进来,所以 build script 的输出里 cargo:PERMISSION_FILES_PATH= 出现两次,第二次写的列表覆盖第一次,内容是全集。表的键列出来是:
['__app-acl__', 'core', 'core:app', 'core:event', 'core:image', 'core:menu', 'core:path', 'core:resources', 'core:tray', 'core:webview', 'core:window']
读能力并校验
能力文件默认按 ./capabilities/**/* 找,扩展名是 json、toml(开 config-json5 时加 json5),parse_capabilities 遇到重复的 identifier 直接报错。真正参与解析的能力集由 get_capabilities 决定:配置里 app > security > capabilities 非空时,只取它列出的(可以是内联的能力对象,也可以是按 identifier 引用文件里的),否则取文件里的全部。
validate_capabilities 对当前目标平台适用的每个能力、每条权限查一遍:按 : 前缀找清单(没有前缀就是 __app-acl__),再看权限名或权限集名在不在里面;default 永远算存在。找不到就报错,并用 suggest_identifier 猜一个最像的。把 allow-greet 改成 allow-gret:
thread 'main' (2432453) panicked at build.rs:4:38:
failed to run tauri-build: Permission allow-gret not found (did you mean "allow-greet"?), expected one of allow-greet, allow-report, deny-greet, deny-report, core:default, core:app:default, core:app:allow-app-hide, core:app:allow-app-show, core:app:allow-bundle-type, core:app:allow-default-window-icon, core:app:allow-exit, ...
这里是 panic 而不是 exit status: 1,因为 build-acl 自己用 try_build(..).expect(..)。猜测的规则在源码注释和测试里写得很细:按 - 拆成段逐段比,段长不小于 4 时允许一处编辑距离,read 和 write、listen 和 unlisten 不算像;漏写 allow- 前缀也能猜出来。
这一步只回答「引用的权限存不存在」。哪个命令在哪个窗口、哪个来源下被允许,要等 generate_context! 调 Resolved::resolve 才算出来,规则见编译期解析。
写出的文件
校验通过后,acl::build 写这几样:
gen/schemas/acl-manifests.json:上面那张清单表,再复制一份到OUT_DIR/acl-manifests.json。gen/schemas/capabilities.json:从文件读到的能力(不含配置里内联的),复制到OUT_DIR/capabilities.json。generate_context!读它之后会再调一次get_capabilities合并配置。gen/schemas/<目标>-schema.json和desktop-schema.json:generate_capability_schema按清单生成的能力文件 JSON schema,编辑器补全用。macOS 上是macOS-schema.json。OUT_DIR/allowed-commands.json:只在设置了REMOVE_UNUSED_COMMANDS环境变量(build > removeUnusedCommands时由 CLI 设置)时由generate_allowed_commands写出,否则删掉旧的。generate_handler!读它来删掉没被任何能力允许的命令,见 #[command] 与 generate_handler!。
gen/schemas 是相对路径,相对的是 build script 的工作目录,也就是应用 crate 的目录。这些文件出现在源码树里,CLI 的应用模板在 .gitignore 里写了 /gen/schemas。
OUT_DIR 里有什么
build-app 构建完之后的 OUT_DIR:
./__global-api-script.js
./acl-manifests.json
./app-manifest/__app__-permission-files
./capabilities.json
./cb184881903692ec3fa7e723fd4ab370a605b0308df8b4a07814e0ac609ff474
./tauri-codegen-assets/79e7ed6655fff8f70f7ef80b436f3c9a011ec83ee047d6d4bf7b2ac87dd3f680.html
| 文件 | 谁写 | 内容 |
|---|---|---|
acl-manifests.json | build script | 清单表(67 KB,10 个核心清单) |
capabilities.json | build script | 文件里的能力,这里是 {} |
app-manifest/__app__-permission-files | build script | 应用权限文件的路径列表,这里是 [] |
__global-api-script.js | build script | 名字是 .js,内容是 JSON:全局 API 脚本的路径列表 |
cb18… | generate_context! | 32×32 图标解码后的 RGBA,4 096 字节 |
tauri-codegen-assets/79e7….html | generate_context! | brotli 压缩后的 index.html |
后两类是编译应用 crate 时由过程宏写的:cargo 给 rustc 设了同一个 OUT_DIR 环境变量,过程宏在 rustc 进程里读得到。它们为什么用哈希命名、修改时间为什么是 2000 年 1 月 1 日,见 generate_context!。
build script 和过程宏之间没有别的通道:generate_context! 用 OUT_DIR 拼出 acl-manifests.json 和 capabilities.json 的路径,文件不存在就当作空表。配置文件两边各读一遍,都会合并 TAURI_CONFIG。
rerun-if-changed 与每次都重编
build script 打印的 rerun-if-changed 决定 cargo 什么时候重跑它。build-app 的输出里有三条:配置文件、capabilities 目录(绝对路径),以及环境变量 TAURI_CONFIG、REMOVE_UNUSED_COMMANDS。permissions 目录只在存在时才被监视。
build-app 没有 capabilities 目录、配置里也没有 capabilities。刚 cargo build 完,什么都不改再 cargo build -v 一次,build-app 会是 Fresh 还是重新编译?能力目录的监视条件在 acl::build 里:
let capabilities_dir = std::env::current_dir()
.context("resolve current dir for capabilities watch path")?
.join("capabilities");
// For the same reason, skip the watch when the directory doesn't exist and the
// configuration lists capabilities in `app > security > capabilities`: that list
// only selects the files it references by identifier, so creating the directory
// alone doesn't change the selected capabilities.
if capabilities_dir.exists() || config.app.security.capabilities.is_empty() {
println!("cargo:rerun-if-changed={}", capabilities_dir.display());
}
cargo 把一个不存在的被监视路径当作「已变化」。目录不存在、配置也没列能力时,这里照样监视它,因为之后新建 capabilities/ 放进能力文件,确实会改变结果,build script 必须重跑。代价是在目录建出来之前,每次构建都重跑 build script、重编应用 crate。CLI 生成的项目自带 capabilities/default.json,碰不到这种情况;手写的最小项目(比如本卷的 base 模板)会碰到。建一个空的 capabilities 目录就够了。
在它之前,不管配置里有没有列能力,只要目录不存在就一直监视,所以在 app > security > capabilities 里内联能力、不建目录的应用,每次构建都会重跑 build script 并重编。#16326(首次发布于 tauri-build 2.7.4,也就是本卷用的版本)把条件改成现在的样子,CHANGELOG 原文:「Don't watch the capabilities directory when it doesn't exist and the configuration lists capabilities in app > security > capabilities. Cargo treats a missing watched path as changed, so these apps re-ran the build script and recompiled on every build.」配置里没列能力的情况保留了监视,原因就是上一段说的:那时目录里的文件就是全部能力的来源。同一处还有更早的一次修复 #15918(tauri-build 2.7.0),把这条路径改成绝对路径:cargo 按拥有 build script 的包解析相对路径,而能力文件的 glob 按进程工作目录解析,先 set_current_dir 再调用的人会监视一个不存在的目录。
permissions 那一条是相对路径(cargo:rerun-if-changed=permissions),而且只在目录存在时打印,所以不会触发这个问题。
动脑筋答案
会重新编译。-v 打印出原因:
Dirty build-app v0.1.0 (~/exp-tauri/build-app): the file `capabilities` is missing
Compiling build-app v0.1.0 (~/exp-tauri/build-app)
Running `~/exp-tauri/target-build/debug/build/build-app-a559251810c0d668/build-script-build`
Dirty 一行说的就是被监视的 capabilities 不存在;Running .../build-script-build 是 build script 重跑,随后应用 crate 重编(这个小应用每次约 0.8 秒)。之后 mkdir capabilities,第一次构建因为「capabilities has changed」再重编一次,第二次就是 Fresh:
Dirty build-app v0.1.0 (~/exp-tauri/build-app): the file `capabilities` has changed (1793193550.694107706s, 4m 45s after last build at 1793193265.892897776s)
Compiling build-app v0.1.0 (~/exp-tauri/build-app)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.74s
Fresh build-app v0.1.0 (~/exp-tauri/build-app)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.14s
小结
tauri_build::build()的输入有两路:配置文件(加TAURI_CONFIG),以及 tauri 和插件 crate 经links元数据传来的DEP_*环境变量(是否开发模式、核心插件和插件的权限文件路径、全局 API 脚本)。- 它发
desktop/mobile/dev三个 cfg 给应用代码用;dev跟着 tauri 的custom-protocolfeature 走。 - 「does not match the allowlist」出自
tauri-build的manifest.rs:只核对macos-private-api、protocol-asset、isolation这几个 feature 和配置是否一致。 - 权限来自三处:依赖 crate、应用(
__app-acl__,AppManifest生成的文件写在项目的permissions/autogenerated/)、内联插件;能力只校验「引用的权限存在」,解析留给generate_context!。 - 写进
OUT_DIR的是acl-manifests.json、capabilities.json、__global-api-script.js(以及可选的allowed-commands.json);schema 写进项目的gen/schemas。Info.plist和 macOS 图标不归它管。 - 项目里没有
capabilities目录、配置里也没列能力时,cargo 每次都认为 build script 的输入变了;建一个空目录就好。