跨平台桌面 App 的隐形炸弹:Default 值悄悄伪装成另一个平台
用 Tauri 2 做了一个桌面 App,Mac 上跑了好几个月,稳稳的。发了 Windows 版本给用户,装上、启动,某个核心功能就卡在"检测中..."一动不动。重装、重启、换机器——还是卡。
cargo build --target x86_64-pc-windows-msvc 没报错,installer 装得上,App 也打得开——偏偏只有这一个运行时功能挂了。
这类问题的根因几乎总是同一个:那段功能从来没有被真正为 Windows 适配过。
现象
- Mac 用户:功能正常,用了好几个月
- Windows 用户:安装成功,App 启动,但功能卡死/报错
- 编译链路:
cargo build --target windows绿色通过 - grep 代码:出现
chrome-mac-arm64、Application Support、.app、.dylib等字符串,但没有cfg(target_os = "windows")/process.platform === 'win32'分支 - CI pipeline:只有 mac runner,Windows build 靠本地手动或自建 builder,从未进过 PR check
根因
问题核心是:"编译通过" ≠ "运行时路径可达"。
Rust 的 cfg(target_os) 和 JS 的 process.platform 都是主动声明的,不是自动推导的。如果你在 Rust 里写:
const ASSET_URL: &str = option_env!("ASSET_URL").unwrap_or(
"http://host/oss/asset-mac-arm64.tar.gz"
);这段代码在 Windows 上编译完全合法——"http://host/oss/asset-mac-arm64.tar.gz" 只是个字符串,编译器不会检查它在运行时是否可达、是否指向正确平台。Windows App 启动后真正去 fetch 这个 URL,才发现文件不存在。
更危险的是,这种问题几乎不会是单点,而是4-6 个独立 layer 全部同时缺失:
| Layer | mac 正常 | Windows 实际 |
|-------|---------|-------------|
| Rust const default URL | 指向 mac tarball,用上了 | 同一个 default URL 被 Windows 用 → 下载到 mac 包 |
| Build script env 注入 | 跑了,注入了正确 URL | Windows build script 根本没注入 → Rust 走 mac default |
| Server 端文件 | mac tarball 上传了 | win tarball 从来没上传过 → 404 |
| JS path detector | 找 chrome-mac-*/.app,命中 | 在 Windows 上找 .app → 永远找不到 → 误判"未安装" |
| 文件扩展名 | .app .dylib | Windows 需要 .exe .dll → 可执行文件找不到 |
每次修一个 layer,重新构建(30 分钟+),发版,Windows 装新包测试——然后发现撞上了下一个 layer 的问题。一个功能要来回跑五趟。
正确策略是:先用 grep 把所有缺失 layer 一次性盘点完,再一次性全补。
正解
第一步:盘点全部缺失,再动手
# 1. Rust 的 const / option_env default 值
grep -rn "option_env!\|unwrap_or" src-tauri/src | grep -iE "url|path|exe|app"
# 2. build script 是否为 windows 注入 env
grep -rn "ASSET_URL\|BUNDLE_URL\|PLATFORM" scripts/release-*.mjs
# 3. JS/TS 端有没有 platform 分支
grep -rn "process\.platform\|win32\|windows" src/ desktop/
# 4. server 上实际有什么文件
ssh myserver "ls /path/to/asset-storage/"
# 5. 文件名约定里的 mac-arm64 / win-x64
grep -rn "mac-arm64\|chrome-mac\|\.app\|\.dylib" src/ scripts/每条 grep 出 mac 专属字符串、没有对应 windows 分支的,就是一个需要补的 layer。
第二步:六个 layer 对称补齐
Layer 1 · Rust default 值加 cfg 分支:
#[cfg(target_os = "windows")]
const ASSET_URL: &str = match option_env!("ASSET_URL") {
Some(u) => u,
None => "http://host/oss/asset-win-x64.tar.gz",
};
#[cfg(not(target_os = "windows"))]
const ASSET_URL: &str = match option_env!("ASSET_URL") {
Some(u) => u,
None => "http://host/oss/asset-mac-arm64.tar.gz",
};Layer 2 · Windows release script 注入 env:
// release-windows.mjs 必须和 release-mac.mjs 对称
process.env.ASSET_URL = `http://host/oss/asset-${version}-win-x64.tar.gz`Layer 3 · JS path detector 加 platform 分支:
if (process.platform === 'win32') {
return fs.existsSync(path.join(dir, 'chrome-win', 'chrome.exe'))
}
// mac: chrome-mac-*/.app
return globSync(path.join(dir, 'chrome-mac-*', '*.app')).length > 0Layer 4 · Server 端确保双平台文件都在:
发版 script 里加 ensure 步骤,检查 mac 和 windows tarball 都已上传,缺哪个就在对应平台的 builder 上重新 package + push(mac builder 上没法装 Windows 版的 playwright chromium,反之亦然)。
Layer 5 · 文件名 SSOT:
定一个统一约定 <asset>-<version>-{mac-arm64|win-x64}.tar.gz,所有 layer 引用同一格式,不要让每个地方各写一遍。
把 5 个 grep 命令跑完,列出所有缺失 layer,再统一修,一次重新发版——Windows 上从"卡死"直接跑通,不用来回折腾多次构建。
排查
修完后逐项验证:
curl -I http://host/path/asset-win-x64.tar.gz返回 200(server 端文件真存在)- 用
strings看编译产物里是否含win-x64URL(确认 env 注入生效) - 功能日志里加一行
console.log('platform:', process.platform)确认走 win32 分支 - Windows 上装新 installer,runtime 功能不再 hang
一句话外卖
cargo build --target windows通过只验证语法,不验证运行时路径;default 值"看着合理"但其实是 mac 格式是最隐蔽的陷阱——跨平台坑往往不是单点,而是多个 layer 同时缺失,要一次性盘点完再修。