Tauri 2 打包 DMG 卡死:rw 镜像残留挂载的根因与 hdiutil 绕行方案
用 Tauri 2 给一个桌面应用打 macOS 包,.app 签名一切正常,构建日志卡在「Running bundle_dmg.sh」后吐出一行毫无信息量的报错就退出,dmg/ 目录里只剩 bundle_dmg.sh 和 icon.icns,根本没有 .dmg。反复重跑,次次如此。
这是 macOS 14 Sonoma / 15 Sequoia + productName 含空格时,Tauri 2 内嵌的 bundle_dmg.sh 会稳定触发的一个坑。
现象
构建日志:
Bundling MyApp_1.0.0_aarch64.dmg (...)
Running bundle_dmg.sh
failed to bundle project error running bundle_dmg.sh: `failed to run
/Users/.../target/release/bundle/dmg/bundle_dmg.sh`
没有 hdiutil 错误码,没有行号,什么都没有。
手工检查挂载状态,问题立刻浮出水面:
hdiutil info | grep "MyApp"
# image-path : .../bundle/macos/rw.5432.MyApp_1.0.0_aarch64.dmg
# /dev/disk4 GUID_partition_scheme
# /dev/disk4s1 Apple_HFS /Volumes/My App上一次失败的临时读写镜像还挂着,占着 /dev/disk4。重新构建时,脚本再创一个新的 rw 镜像,又遇到同样的 detach 问题,形成稳定复现。
根因
bundle_dmg.sh 是 Tauri 内嵌的 create-dmg 冻结 fork。流程是:
- 创建
rw.NNNN.<productName>.dmg临时读写镜像 - 挂载到
/Volumes/<productName> - 把
.app复制进去 - 调用
hdiutil detach弹出,最多重试 3 次(MAXIMUM_UNMOUNTING_ATTEMPTS=3,硬编码) - 转换成只读压缩格式,输出最终
.dmg
在 macOS 15 上,步骤 4 概率性返回 exit code 16(EBUSY):Spotlight 的 mds_stores 和 Quick Look 的 mdworker_shared 会在新卷挂载后立刻打开 FD 探查 .app,导致卷始终处于"资源繁忙"状态,3 次重试全部失败。
productName 含空格会额外放大这个问题:/Volumes/My App(含空格)在 hdiutil 命令行引号处理的某些边缘情况下行为异常,且 set -e 一旦捕获非零退出码就直接 abort,不清理挂载。
脚本在 set -e 下 abort,不会 umount,也不会删 rw 镜像文件。每次重跑都遇到同一堵墙。Tauri 的 stderr 捕获层把 hdiutil 的具体报错全部吞掉,日志毫无线索,极难诊断。
排查确认
先用以下命令确认是否命中这个 bug(三条全中才是):
# 1. 是否有残留挂载
hdiutil info | grep "MyApp"
# 2. 是否有残留 rw 镜像文件
ls target/aarch64-apple-darwin/release/bundle/macos/ | grep "^rw\."
# 3. dmg 目录里有无最终产物
ls target/aarch64-apple-darwin/release/bundle/dmg/
# 预期:只有 bundle_dmg.sh + icon.icns,没有 .dmg正解
第一步:强制清理残留挂载
# 强制卸载所有相关磁盘设备
hdiutil info \
| sed -n '/image-path.*MyApp/,/^=====/p' \
| grep -E '^/dev/disk[0-9]+\b' \
| awk '{print $1}' \
| sort -u \
| while read d; do hdiutil detach "$d" -force; done
# 删除 rw 镜像文件
rm -f target/aarch64-apple-darwin/release/bundle/macos/rw.*.dmg
# 确认干净
hdiutil info | grep "MyApp" || echo "(clean)"第二步:绕过 bundle_dmg.sh,直接用 hdiutil 打包
.app 已经被 Tauri 构建并签名,只差最后封装这一步。直接调原生工具:
APP="target/aarch64-apple-darwin/release/bundle/macos/MyApp.app"
OUT="target/aarch64-apple-darwin/release/bundle/dmg/MyApp_1.0.0_aarch64.dmg"
hdiutil create \
-volname "My App" \
-srcfolder "$APP" \
-ov \
-format UDZO \
"$OUT"-format UDZO 是压缩只读格式,与 Tauri 正常输出完全一致。
第三步:给 DMG 容器补签名
.app 内部已签,但 DMG 容器本身需要单独签:
codesign --sign "Developer ID Application: Your Name (TEAMID)" \
--timestamp "$OUT"
codesign --verify --verbose=2 "$OUT"
# 期望输出:valid on disk / satisfies its Designated RequirementTauri 的 bundle_dmg.sh 本质上就是这几条 hdiutil 命令的包装。create-dmg 脚本增加了背景图、按钮布局等美化步骤,但如果你只需要一个能用的 DMG,直接调 hdiutil create 稳定可靠,完全跳过 Spotlight 竞争问题。
如果你能控制配置,把 productName: "My App LAN" 改成 "My-App-LAN" 是成本最低的规避手段。单词间无空格时,/Volumes/My-App-LAN 路径在 hdiutil 命令行处理中不会触发引号边界问题,脚本命中 EBUSY 的概率大幅降低。
一句话外卖
Tauri 的
bundle_dmg.sh是hdiutil命令的脆弱封装,在 macOS 15 + 含空格 productName 组合下必然卡死;残留挂载不自动清理会让问题永久复现——认清本质后直接用hdiutil create -format UDZO绕行,三行命令搞定。