macOS 使用 dmgbuild 打包 DMG
本文介绍如何使用 dmgbuild 替代脆弱的 appdmg,实现稳定可靠的 macOS DMG 自动化打包与 CI 集成。
在 macOS 平台的桌面应用分发中(无论是基于 Go、Tauri、Electron、Flutter 还是原生 Swift 开发),提供一个精致的 .dmg(Apple Disk Image)挂载镜像,通常是交付用户的首选标准方案。
一个体验优良的 DMG 镜像通常具备以下特征:
- 挂载后桌面显示定制的磁盘卷图标(Volume Icon);
- 打开后呈现固定尺寸的引导窗口与高保真背景图;
- 窗口内左侧放置应用程序主体(
.app),右侧放置系统/Applications目录的快捷替身(Symlink),引导用户直接拖拽完成安装; - 自动隐藏无关的系统临时文件夹与垃圾文件。
然而,在持续集成(CI/CD)流水线中自动化打包出这种具有精细布局的 DMG,却一直是个令不少开发者头疼的工程痛点。
为什么传统打包方案在 CI 上频繁翻车?
长期以来,社区中最常见的两种定制 DMG 方案分别是基于 Node.js 的 appdmg 以及基于 Shell 的 create-dmg。但在无界面的 CI/CD 环境(如 GitHub Actions、GitLab CI 的 macOS Runner)中,它们存在明显的致命缺陷:
1. 深度依赖 Finder GUI 与 AppleScript
appdmg 和 create-dmg 底层通常依赖 AppleScript 唤起 macOS 的 Finder 窗口,通过模拟界面操作来调整窗口大小、图标位置以及背景图,最后依赖 Finder 将这些视口状态异步持久化到 .DS_Store 二进制文件中。
但在无头(Headless)无物理显示器的 CI Runner 上:
- Finder 进程经常没有初始化图形渲染上下文;
.DS_Store的写入是异步且不可控的,常常发生构建脚本已经执行完毕、镜像已打包,但 Finder 还没来得及写盘;- 导致最终产出的 DMG 随机丢失背景图、图标位置重叠乱跳,产生极其隐蔽的偶发性构建失败。
2. 脆弱的预制 DS_Store 黑盒方案
为了规避 CI 上无法调用 Finder 的问题,部分项目选择在本地物理机上手动调整好一次布局,把生成的隐藏文件 .DS_Store 提取出来作为静态资源存入代码仓库,打包时直接覆盖注入。但这种方案不仅难以适应多分辨率窗口的调整,而且一旦需要更改应用图标尺寸或新增软链接,就必须重新在物理机上肉眼微调并重新提取,维护极其痛苦。
3. Node.js 原生扩展的编译包袱
appdmg 依赖底层 C++ 原生模块编译,每当 Node.js 主版本迭代或 macOS 系统架构(如 x86_64 与 arm64)切换时,经常在 CI 上因 node-gyp 编译失败而中断发布。
解决方案:纯代码构建的 dmgbuild
为了追求 100% 确定性和高可维护性,基于 Python 的 dmgbuild 成为了目前自动化构建 macOS DMG 的最佳选择。
核心工作原理
dmgbuild 之所以稳定,是因为它从根本上摆脱了对 Finder GUI 和 AppleScript 的依赖:
- 直接构造二进制元数据:借助纯 Python 库
ds_store与mac_alias,dmgbuild直接在内存中解析并按苹果二进制规范精确构造.DS_Store文件与替身别名数据,不唤起任何图形窗口。 - 纯声明式配置:通过一个简单的 Python 脚本(
dmgbuild-settings.py)声明窗口尺寸、图标坐标与背景路径,支持从命令行动态注入变量(如版本号、产物路径)。 - 原生工具压缩:布局写入完成后,调用 macOS 原生系统级工具
hdiutil将其转换并压缩为标准只读 DMG 磁盘镜像。
⚠️ 运行平台限制
虽然ds_store和mac_alias是跨平台的 Python 库,但镜像的格式转换与压缩最终依赖苹果专有的hdiutil命令行工具。因此,dmgbuild只能运行在 macOS 系统上(本地 macOS 或 GitHub Actionsmacos-latestrunner),在 Linux 或 Windows 上即使安装了 Python 依赖也无法生成最终的 DMG 文件。
项目工程结构设计
为了让本地开发和 CI 构建共用一套确定性的流程,推荐在项目中采用如下清晰的目录结构:
1 | |
如果你之前使用的是 appdmg,以下历史遗留文件可以直接安全清理:
scripts/dmg-config.json(不再需要)assets/dmg-ds-store(不再需要预制缓存)
核心配置:编写 dmgbuild-settings.py
dmgbuild 的配置文件本身是一个合法的 Python 脚本。我们可以在脚本中通过 os.environ 接收外部传入的自定义参数,实现高度动态化的流水线集成。
新建 scripts/dmgbuild-settings.py:
1 | |
关键配置项说明
window_rect:定义 DMG 双击打开时的默认窗口大小。建议设计背景图时与该尺寸严格保持一致(例如800 × 520)。contents:设置左侧.app与右侧/Applications软链接的坐标。微调该参数可以使图标恰好对准背景图中的引导箭头。format = "UDZO":macOS 最通用的只读压缩磁盘映像格式,兼顾压缩率与挂载性能。
自动化打包与校验脚本:package-dmg.sh
在实际工程中,绝不能“执行了打包命令就不管”。我们需要一份严谨的 Shell 脚本,完成前置资源强校验、构建调用,并在构建完成后自动挂载 DMG 验证 .DS_Store 是否真实生成,形成完整的质量卡点。
flowchart TD
A["执行 package-dmg.sh"] --> B{"前置资源强校验
(App/背景/图标/配置)"}
B --"资源缺失"--> ERR1["❌ 立即中断 (Fail-Fast)"]
B --"校验通过"--> C["调用 dmgbuild 注入 -D 动态参数"]
C --> D["内存构造 .DS_Store
+ 调用 hdiutil 压缩 UDZO"]
D --> E["hdiutil attach 静默挂载测试"]
E --> F{"根目录 .DS_Store 是否存在?"}
F --"否"--> ERR2["❌ 阻断: 视口元数据丢失"]
F --"是"--> G["安全卸载 (带锁盘重试机制)"]
G --> H["🎉 产出质量合格的 DMG 资产"]
新建 scripts/package-dmg.sh:
1 | |
赋予脚本可执行权限并运行:
1 | |
集成到 GitHub Actions CI/CD 流水线
通过将打包逻辑收敛在 scripts/package-dmg.sh 中,GitHub Actions 的工作流配置变得异常精简且易于维护。
以下是一个完整的 Release 发布流水线配置参考(.github/workflows/release.yml):
1 | |
常见问题与高频踩坑清单(Gotchas)
在落地实际业务打包时,以下几个高频细节非常值得关注:
1. 背景图在 Retina(视网膜)屏幕上模糊或尺寸不匹配
- 原因:DMG 的
window_rect = ((x, y), (w, h))使用的是逻辑点(Points),例如800 × 520。 - 最佳实践:制作背景图片时,推荐直接导出 2 倍物理像素 的高清图片(即
1600 × 1040,72 或 144 DPI 均可)。macOS 在高清屏下渲染挂载窗口时会自动进行高质量缩放,保证文字与引导箭头锐利清晰。
2. 挂载后图标与背景图箭头位置偏移
- 不同的 macOS 主版本(Sonoma / Sequoia 等)对于字体字阶和应用图标的边缘判定可能存在轻微差异。
- 建议初次配置时,在本地 macOS 机器运行脚本,生成后直接双击挂载打开,微调
dmgbuild-settings.py中contents数组里的rect坐标参数,直到视觉上完全居中对齐。
3. hdiutil detach: Resource temporarily unavailable
- 原因:在调用
hdiutil attach后,macOS 系统的Spotlight(索引服务)或mds进程可能会在后台尝试为刚挂载的临时磁盘建立索引,产生文件锁。 - 解决方式:挂载时务必加上
-nobrowse参数(防止 Finder 主动读取),卸载时如果失败,sleep 2后使用hdiutil detach -force强制卸载即可。
4. 卷图标格式必须为标准的 .icns
volume_icon必须是包含多种分辨率层级(16x16 至 512x512@2x)的标准苹果图标文件(.icns),直接传入.png格式会导致卷图标无法正常渲染。- 可使用 macOS 原生命令行工具
iconutil将包含各尺寸 png 的.iconset目录转换为标准.icns。
5. 关于 Gatekeeper、代码签名与公证(Notarization)
如果你需要公开发布应用供外部用户下载:
- 单纯生成 DMG 是不够的,macOS Gatekeeper 安全机制会在用户打开未签名未公证的 DMG 时拦截并提示“已损坏,无法打开”;
- 正确的签名顺序应当是:
- 先对应用程序内部二进制与
MyApp.app进行codesign深度签名; - 使用
dmgbuild将已签名的.app打包为.dmg; - 对产出的
.dmg整体进行codesign签名; - 使用苹果官方命令行
xcrun notarytool submit提交 DMG 进行公证; - 公证成功后,执行
xcrun stapler staple将票据装订到 DMG 上。
- 先对应用程序内部二进制与
方案选型综合对比
| 方案 | 稳定性 (CI) | 外部运行时依赖 | 配置方式 | 自定义外观能力 | 推荐指数 |
|---|---|---|---|---|---|
| dmgbuild | ⭐⭐⭐⭐⭐ (纯二进制写 DS_Store,无 GUI 依赖) | Python (macOS 自带或 pip) | Python 脚本 (支持动态参数注入) | 强 (背景、卷图标、坐标、软链接) | 首选推荐 |
| appdmg | ⭐⭐ (CI 频繁丢布局、易超时) | Node.js + C++ 编译依赖 | JSON 配置文件 + 缓存 DS_Store | 强 (但维护成本高) | 已不推荐 |
| create-dmg | ⭐⭐⭐ (依赖 AppleScript) | Bash + AppleScript | CLI 纯命令行传参 | 中等 (简单场景可用) | 备选 |
| 原生 hdiutil | ⭐⭐⭐⭐⭐ (系统原生) | 无 | 原生 Shell 命令 | 弱 (仅支持纯白/裸盘,无布局) | 仅适合极简分发 |
结语
从脆弱的 GUI 模拟与预制模板,转向代码化、确定性的声明式构建,是现代客户端工程交付的必然趋势。利用 dmgbuild 搭配参数化脚本与挂载自检,不仅彻底解决了 CI/CD 无头环境下 DMG 布局丢失的历史顽疾,也为团队沉淀了一套极低维护成本的 macOS 打包标准范式。
