返回博客

Tauri 2 打包 DMG 卡死:rw 镜像残留挂载的根因与 hdiutil 绕行方案

用 Tauri 2 给一个桌面应用打 macOS 包,.app 签名一切正常,构建日志卡在「Running bundle_dmg.sh」后吐出一行毫无信息量的报错就退出,dmg/ 目录里只剩 bundle_dmg.shicon.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。流程是:

  1. 创建 rw.NNNN.<productName>.dmg 临时读写镜像
  2. 挂载到 /Volumes/<productName>
  3. .app 复制进去
  4. 调用 hdiutil detach 弹出,最多重试 3 次(MAXIMUM_UNMOUNTING_ATTEMPTS=3,硬编码)
  5. 转换成只读压缩格式,输出最终 .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,不清理挂载。

坑:hdiutil detach 失败不自动清理,残留挂载使后续构建永久卡死

脚本在 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 Requirement
正解:hdiutil create -format UDZO 直接封装已签名的 .app

Tauri 的 bundle_dmg.sh 本质上就是这几条 hdiutil 命令的包装。create-dmg 脚本增加了背景图、按钮布局等美化步骤,但如果你只需要一个能用的 DMG,直接调 hdiutil create 稳定可靠,完全跳过 Spotlight 竞争问题。

最根本的规避:productName 用连字符替代空格

如果你能控制配置,把 productName: "My App LAN" 改成 "My-App-LAN" 是成本最低的规避手段。单词间无空格时,/Volumes/My-App-LAN 路径在 hdiutil 命令行处理中不会触发引号边界问题,脚本命中 EBUSY 的概率大幅降低。

一句话外卖

Tauri 的 bundle_dmg.shhdiutil 命令的脆弱封装,在 macOS 15 + 含空格 productName 组合下必然卡死;残留挂载不自动清理会让问题永久复现——认清本质后直接用 hdiutil create -format UDZO 绕行,三行命令搞定。