1. 为什么需要DMG打包与公证?
在MacOS生态中,DMG(Disk Image)是最常见的软件分发格式之一。它本质上是一个虚拟磁盘镜像文件,用户双击后会自动挂载为一个虚拟磁盘,开发者可以将应用程序、说明文档等一起打包进去。相比直接分发APP文件,DMG具有以下优势:
- 专业形象:规范的DMG文件通常包含精心设计的背景图、应用程序快捷方式拖拽提示等,给用户更专业的体验
- 完整性保障:DMG作为容器可以包含多个文件(如主程序、帮助文档、许可协议等),避免用户遗漏必要组件
- 安装引导:通过自定义背景图可以直观指导用户将APP拖拽到Applications文件夹
- 压缩支持:DMG支持压缩存储,减小分发文件体积
而公证(Notarization)则是苹果在macOS 10.14.5后引入的安全机制。未经公证的应用程序在首次运行时会出现安全警告,甚至被系统直接阻止运行。公证过程实际上是苹果对应用程序进行恶意代码扫描和验证的过程,通过后会给应用程序添加数字票据,使用户可以更安全地安装和使用。
重要提示:从macOS 10.15 Catalina开始,所有包含可执行代码的分发包(包括DMG)都必须经过公证才能在默认安全设置下正常运行。
2. 环境准备与工具链配置
2.1 基础环境要求
在开始打包前,需要确保开发环境满足以下条件:
- macOS系统:建议使用较新版本(至少10.15以上)
- Xcode命令行工具:这是公证流程的必要组件
xcode-select --install - 开发者账号:有效的Apple Developer账号(个人或公司)
- App专用密码:为公证流程生成的特殊密码(非Apple ID密码)
2.2 关键工具介绍
- hdiutil:macOS内置的磁盘工具,用于创建和管理DMG文件
- codesign:苹果的代码签名工具,用于对应用程序进行签名
- altool:Xcode自带的公证上传工具(位于/usr/bin/altool)
- stapler:用于将公证结果"钉"到应用程序上的工具
2.3 开发者证书配置
- 登录 Apple Developer网站
- 在"Certificates, Identifiers & Profiles"中创建Developer ID Application证书
- 下载并安装证书到钥匙串
- 记下证书的完整名称(通常类似"Developer ID Application: Your Name (XXXXXXXXXX)")
3. 完整的DMG打包流程
3.1 应用程序准备
在打包前,确保应用程序已经完成以下步骤:
- 清理构建产物:删除所有调试符号和临时文件
- 代码签名:使用Developer ID证书对应用程序进行签名
codesign --deep --force --verify --verbose --sign "Developer ID Application: Your Name (XXXXXXXXXX)" YourApp.app - 验证签名:检查签名是否成功
codesign -dv --verbose=4 YourApp.app spctl -a -t exec -vv YourApp.app
3.2 创建DMG文件
以下是创建专业DMG的完整脚本:
#!/bin/bash APP_NAME="YourApp" VERSION="1.0" DMG_NAME="${APP_NAME}_${VERSION}.dmg" TEMP_DIR="temp_dmg" VOLUME_NAME="${APP_NAME} ${VERSION}" APP_PATH="./build/${APP_NAME}.app" DMG_BACKGROUND_IMG="background.png" # 创建临时目录 mkdir -p "${TEMP_DIR}" # 复制应用程序和资源 cp -R "${APP_PATH}" "${TEMP_DIR}" cp README.txt "${TEMP_DIR}" cp LICENSE.txt "${TEMP_DIR}" # 创建软链接到Applications ln -s /Applications "${TEMP_DIR}/Applications" # 计算DMG大小(增加20%余量) APP_SIZE=$(du -sm "${TEMP_DIR}" | cut -f1) DMG_SIZE=$((APP_SIZE + APP_SIZE/5)) # 创建空白DMG hdiutil create -srcfolder "${TEMP_DIR}" \ -volname "${VOLUME_NAME}" \ -fs HFS+ \ -fsargs "-c c=64,a=16,e=16" \ -format UDRW \ -size ${DMG_SIZE}m \ "${DMG_NAME}.temp" # 挂载DMG DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "${DMG_NAME}.temp.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}') MOUNT_POINT="/Volumes/${VOLUME_NAME}" # 设置DMG布局 echo ' tell application "Finder" tell disk "'"${VOLUME_NAME}"'" open set current view of container window to icon view set toolbar visible of container window to false set statusbar visible of container window to false set the bounds of container window to {400, 100, 1000, 500} set viewOptions to the icon view options of container window set arrangement of viewOptions to not arranged set icon size of viewOptions to 72 set background picture of viewOptions to file ".background:'"${DMG_BACKGROUND_IMG}"'" set position of item "'"${APP_NAME}.app"'" of container window to {150, 100} set position of item "Applications" of container window to {450, 100} set position of item "README.txt" of container window to {150, 250} set position of item "LICENSE.txt" of container window to {450, 250} update without registering applications delay 5 close end tell end tell ' | osascript # 设置权限 chmod -Rf go-w "${MOUNT_POINT}" # 卸载DMG hdiutil detach "${DEVICE}" # 压缩DMG hdiutil convert "${DMG_NAME}.temp.dmg" \ -format UDZO \ -imagekey zlib-level=9 \ -o "${DMG_NAME}" # 清理临时文件 rm -f "${DMG_NAME}.temp.dmg" rm -rf "${TEMP_DIR}"3.3 DMG签名
创建好的DMG也需要进行签名:
codesign --sign "Developer ID Application: Your Name (XXXXXXXXXX)" --timestamp "${DMG_NAME}"4. 公证流程详解
4.1 公证前的检查
在提交公证前,建议先进行以下检查:
- 检查签名有效性:
codesign -vvv --deep --strict "${DMG_NAME}" - 验证Gatekeeper兼容性:
spctl -a -t install --context context:primary-signature -v "${DMG_NAME}"
4.2 提交公证
使用altool提交公证请求:
xcrun altool --notarize-app \ --primary-bundle-id "com.yourcompany.yourapp" \ --username "your_apple_id@email.com" \ --password "@keychain:AC_PASSWORD" \ --file "${DMG_NAME}"注意:这里的密码是App专用密码,可以通过Apple ID账户页面生成。建议将密码存储在钥匙串中,通过"@keychain:AC_PASSWORD"引用,而不是直接写在脚本中。
4.3 检查公证状态
公证通常需要5-30分钟完成,可以通过以下命令检查状态:
xcrun altool --notarization-info <RequestUUID> \ --username "your_apple_id@email.com" \ --password "@keychain:AC_PASSWORD"4.4 钉书公证结果
公证通过后,需要将公证结果"钉"到DMG文件上:
xcrun stapler staple "${DMG_NAME}"验证钉书是否成功:
xcrun stapler validate "${DMG_NAME}"5. 自动化脚本整合
将上述流程整合为一个完整的自动化脚本:
#!/bin/bash # 配置变量 APP_NAME="YourApp" VERSION="1.0" BUNDLE_ID="com.yourcompany.yourapp" DEVELOPER_ID="Developer ID Application: Your Name (XXXXXXXXXX)" APPLE_ID="your_apple_id@email.com" DMG_BACKGROUND_IMG="background.png" # 清理旧文件 rm -rf "${APP_NAME}_${VERSION}.dmg" rm -rf "temp_dmg" # 1. 代码签名 echo "正在签名应用程序..." codesign --deep --force --verify --verbose --sign "${DEVELOPER_ID}" \ --options runtime \ --timestamp \ "./build/${APP_NAME}.app" # 2. 验证签名 echo "验证签名..." codesign -dv --verbose=4 "./build/${APP_NAME}.app" spctl -a -t exec -vv "./build/${APP_NAME}.app" # 3. 创建DMG echo "创建DMG文件..." mkdir -p "temp_dmg" cp -R "./build/${APP_NAME}.app" "temp_dmg" cp README.txt "temp_dmg" cp LICENSE.txt "temp_dmg" ln -s /Applications "temp_dmg/Applications" APP_SIZE=$(du -sm "temp_dmg" | cut -f1) DMG_SIZE=$((APP_SIZE + APP_SIZE/5)) hdiutil create -srcfolder "temp_dmg" \ -volname "${APP_NAME} ${VERSION}" \ -fs HFS+ \ -fsargs "-c c=64,a=16,e=16" \ -format UDRW \ -size ${DMG_SIZE}m \ "${APP_NAME}_${VERSION}.temp.dmg" DEVICE=$(hdiutil attach -readwrite -noverify -noautoopen "${APP_NAME}_${VERSION}.temp.dmg" | egrep '^/dev/' | sed 1q | awk '{print $1}') MOUNT_POINT="/Volumes/${APP_NAME} ${VERSION}" echo ' tell application "Finder" tell disk "'"${APP_NAME} ${VERSION}"'" open set current view of container window to icon view set toolbar visible of container window to false set statusbar visible of container window to false set the bounds of container window to {400, 100, 1000, 500} set viewOptions to the icon view options of container window set arrangement of viewOptions to not arranged set icon size of viewOptions to 72 set background picture of viewOptions to file ".background:'"${DMG_BACKGROUND_IMG}"'" set position of item "'"${APP_NAME}.app"'" of container window to {150, 100} set position of item "Applications" of container window to {450, 100} set position of item "README.txt" of container window to {150, 250} set position of item "LICENSE.txt" of container window to {450, 250} update without registering applications delay 5 close end tell end tell ' | osascript chmod -Rf go-w "${MOUNT_POINT}" hdiutil detach "${DEVICE}" hdiutil convert "${APP_NAME}_${VERSION}.temp.dmg" \ -format UDZO \ -imagekey zlib-level=9 \ -o "${APP_NAME}_${VERSION}.dmg" rm -f "${APP_NAME}_${VERSION}.temp.dmg" rm -rf "temp_dmg" # 4. 签名DMG echo "签名DMG文件..." codesign --sign "${DEVELOPER_ID}" --timestamp "${APP_NAME}_${VERSION}.dmg" # 5. 提交公证 echo "提交公证..." NOTARIZE_OUTPUT=$(xcrun altool --notarize-app \ --primary-bundle-id "${BUNDLE_ID}" \ --username "${APPLE_ID}" \ --password "@keychain:AC_PASSWORD" \ --file "${APP_NAME}_${VERSION}.dmg" 2>&1) REQUEST_UUID=$(echo "${NOTARIZE_OUTPUT}" | grep "RequestUUID" | awk '{print $3}') if [ -z "${REQUEST_UUID}" ]; then echo "公证提交失败:" echo "${NOTARIZE_OUTPUT}" exit 1 fi echo "公证已提交,RequestUUID: ${REQUEST_UUID}" echo "等待公证结果..." # 6. 检查公证状态 while true; do NOTARIZATION_INFO=$(xcrun altool --notarization-info "${REQUEST_UUID}" \ --username "${APPLE_ID}" \ --password "@keychain:AC_PASSWORD" 2>&1) STATUS=$(echo "${NOTARIZATION_INFO}" | grep "Status:" | awk -F ': ' '{print $2}') if [ "${STATUS}" = "success" ]; then echo "公证成功" break elif [ "${STATUS}" = "in progress" ]; then echo "公证仍在处理中,等待30秒后重试..." sleep 30 else echo "公证失败:" echo "${NOTARIZATION_INFO}" exit 1 fi done # 7. 钉书公证结果 echo "钉书公证结果..." xcrun stapler staple "${APP_NAME}_${VERSION}.dmg" xcrun stapler validate "${APP_NAME}_${VERSION}.dmg" echo "DMG打包及公证完成: ${APP_NAME}_${VERSION}.dmg"6. 常见问题与解决方案
6.1 公证失败常见原因
签名无效或不完整
- 解决方案:确保使用
--deep参数签名,并验证所有嵌套组件都已签名
codesign --deep --verify --verbose=4 YourApp.app- 解决方案:确保使用
时间戳服务不可用
- 解决方案:检查网络连接,确保能访问Apple的时间戳服务器
ping timestamp.apple.com证书问题
- 解决方案:确认钥匙串中的证书有效且未被撤销
security find-identity -v -p codesigning公证日志分析公证失败时,会返回日志URL,可以使用以下命令查看详细错误:
xcrun altool --notarization-info <RequestUUID> \ --username "your_apple_id@email.com" \ --password "@keychain:AC_PASSWORD"
6.2 DMG创建常见问题
背景图片不显示
- 确保图片已复制到
.background隐藏文件夹 - 图片格式建议使用PNG,尺寸与DMG窗口大小匹配
- 确保图片已复制到
DMG挂载后图标位置错乱
- 在osascript脚本中添加
delay让Finder有足够时间刷新 - 确保所有路径和文件名正确无误
- 在osascript脚本中添加
DMG文件过大
- 使用
UDZO格式并设置zlib-level=9获得最佳压缩比 - 清理应用程序中的调试符号和不需要的资源文件
- 使用
6.3 性能优化技巧
- 并行处理:将DMG创建和应用程序构建过程并行化
- 增量构建:仅重新构建变更的部分,减少完整构建时间
- 缓存公证结果:对于未修改的版本,可以直接复用之前的公证结果
- 使用
--options runtime:启用Hardened Runtime,这是公证的要求之一
7. 进阶技巧与最佳实践
7.1 自动化构建集成
可以将上述脚本集成到CI/CD流程中,例如GitHub Actions配置示例:
name: Build and Notarize on: release: types: [created] jobs: build: runs-on: macos-latest steps: - uses: actions/checkout@v2 - name: Build Application run: | # 你的构建命令 xcodebuild -project YourApp.xcodeproj -scheme YourApp -configuration Release - name: Package and Notarize env: APPLE_ID: ${{ secrets.APPLE_ID }} APPLE_ID_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} run: | chmod +x build_and_notarize.sh ./build_and_notarize.sh7.2 多架构支持
随着Apple Silicon的普及,建议构建通用二进制:
# 在Xcode构建命令中添加ARCHS参数 xcodebuild -project YourApp.xcodeproj \ -scheme YourApp \ -configuration Release \ ARCHS="arm64 x86_64" \ ONLY_ACTIVE_ARCH=NO7.3 版本管理策略
- 语义化版本:遵循主版本号.次版本号.修订号的格式
- DMG命名规范:建议包含应用名称、版本号和架构信息,如
YourApp_1.0.0_universal.dmg - 版本元数据:在Info.plist中正确设置
CFBundleShortVersionString和CFBundleVersion
7.4 安全增强措施
- 启用Hardened Runtime:在签名时添加
--options runtime标志 - 设置权限限制:确保应用程序只请求必要的权限
- 沙盒配置:根据应用需求配置合适的沙盒权限
- 库验证:启用库验证以防止代码注入
codesign --sign "${DEVELOPER_ID}" \ --options runtime \ --timestamp \ --library-validation \ YourApp.app
在实际项目中,我发现将整个流程脚本化可以显著提高发布效率,特别是在频繁迭代的开发周期中。一个完整的建议是创建一个Makefile来管理不同的构建目标,例如:
.PHONY: all clean sign package notarize all: package clean: rm -rf build rm -rf *.dmg rm -rf temp_dmg build: xcodebuild -project YourApp.xcodeproj \ -scheme YourApp \ -configuration Release \ ARCHS="arm64 x86_64" \ ONLY_ACTIVE_ARCH=NO \ CONFIGURATION_BUILD_DIR=build sign: build codesign --deep --force --verify --verbose \ --sign "Developer ID Application: Your Name (XXXXXXXXXX)" \ --options runtime \ --timestamp \ build/YourApp.app package: sign ./create_dmg.sh notarize: package xcrun altool --notarize-app \ --primary-bundle-id "com.yourcompany.yourapp" \ --username "your_apple_id@email.com" \ --password "@keychain:AC_PASSWORD" \ --file YourApp_1.0.0.dmg这种模块化的设计使得构建流程更加清晰,也便于团队协作和维护。对于大型项目,还可以考虑将配置信息提取到单独的配置文件中,实现脚本与配置的分离。