在 macOS 上分发 Qt 应用,开发者常被多架构编译、代码签名、公证流程所困扰。本文基于 Qt6.9.2 实战经验,系统梳理了从编译配置到最终分发的完整路径。你将了解如何通过 CMake 管理多架构构建,使用 macdeployqt 打包依赖,配置正确的 entitlements 文件,并完成 Apple 强制要求的签名与公证。更提供自动化脚本示例,助你告别重复操作,确保应用在 Intel 和 Apple Silicon(M 芯片) 设备上均能无缝运行。这份避坑指南,将带你高效跨越 macOS 分发的最后一道门槛。
不同架构 mac 有两种 CPU 芯片:Intel 芯片和 M 系列芯片。
Intel 芯片对应 x64 架构,M 芯片对应 arm64 架构,我们需要为不同的架构编译不同的软件和库。
在使用 vcpkg 安装依赖库时,这两种架构所对应的 triplet 分别为 x64-osx、arm64-osx:
1 2 3 ./vcpkg install --triplet "x64-osx" ./vcpkg install --triplet "arm64-osx"
使用lipo -info命令可以查看二进制支持的 CPU 架构信息。
Universal 版本 我们经常看到有些程序会提供 Universal 版本(统一版本),即该版本同时支持 Intel 芯片和 M 芯片。
我们可以先分别编译 arm64 和 x64 架构的库,然后借助lipomerge 工具来生成 Universal 版本,命令如下:
1 python3 ~/lipomerge.py arm64-osx x64-osx uni-osx
值得欣慰的是,Qt 官方提供的库默认就是 Universal 版本,不需要额外编译。
自此程序依赖库的编译工作已经完成,下面开始编译程序主体。<原文出自: jiangxueqiao.com,请尊重原创> 需要在 CMake 中指定程序的目标架构和所支持 OSX 的最低版本:
1 2 set (CMAKE_OSX_DEPLOYMENT_TARGET "12.0" )set (CMAKE_OSX_ARCHITECTURES "x86_64;arm64" )
并通过指定 MACOSX_BUNDLE 参数将可执行文件构建为 macOS 软件包(这一步是非必须,但在本教程中是按照该方案来讲解的):
1 2 3 4 5 6 7 add_executable ( SampleApp MACOSX_BUNDLE ${SOURCE_FILES} SampleClient.ui SampleClient.qrc )
使用 CMake 进行编译:
1 2 cmake -DCMAKE_PREFIX_PATH=~/vcpkg/installed/uni-osx .. make
如果提示找不到 Qt,可以在 CMake 脚本中显式将 Qt 的安装路径及 uni-osx 库的路径添加到 CMAKE_PREFIX_PATH 中:
1 2 list (APPEND CMAKE_PREFIX_PATH "/Users/imac/Qt/6.9.2/macos" )list (APPEND CMAKE_PREFIX_PATH "/Users/imac/vcpkg/vcpkg_installed/uni-osx" )
macdeployqt 在 macOS 上每次编译完 Qt 程序后,都需要使用 macdeployqt 命令来拷贝依赖的 Qt 库,否则即便完成了签名和公证,在用户机器上仍然无法启动,而在 Windows 上只需要拷贝一次,下次仅替换 exe 即可(大坑,浪费了我很多时间)。<原文出自: jiangxueqiao.com,请尊重原创> 可以将每次编译后都需要执行的 macdeployqt 命令添加到 CMake 脚本中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 find_program (MACDEPLOYQT_EXECUTABLE macdeployqt HINTS "${Qt6_DIR}/../../../bin" "${QT_INSTALL_PREFIX}/bin" ) if (MACDEPLOYQT_EXECUTABLE) add_custom_command (TARGET SampleApp POST_BUILD COMMAND ${CMAKE_COMMAND} -E echo "===== 开始执行MacDeployQt命令 =====" COMMAND ${MACDEPLOYQT_EXECUTABLE} "$<TARGET_BUNDLE_DIR:SampleApp>" -always-overwrite -verbose=1 -executable="$<TARGET_BUNDLE_DIR:SampleApp>/Contents/MacOS/SampleApp" VERBATIM ) else () message (FATAL_ERROR "macdeployqt not found! Qt libraries will not be bundled." ) endif ()
app 软件包 macOS 上的 app 软件包实际为一个按照特定结构组织的、名称以.app结尾的文件夹。
通常需要在 XXX.app\Contents\Info.plist 文件中定义了软件包的版本、图标、可执行文件等信息,包括注册的自定义 URL 协议也是在该文件中指定。
由于我们的软件包是 CMake 编译后自动生成的,因此每次都需要手动将 Info.plist 拷贝到软件包 Contents 目录。为了将流程自动化,方便后续进行自动签名,我们将 Info.plist 的生成和拷贝操作放入到 CMake 脚本中。<原文出自: jiangxueqiao.com,请尊重原创> 使用 Info.plist.in 模板生成 Info.plist:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 set (MACOSX_BUNDLE_BUNDLE_NAME "SampleApp" )set (MACOSX_BUNDLE_BUNDLE_VERSION "1.0.20" )set (MACOSX_BUNDLE_SHORT_VERSION_STRING "1.0" )set (MACOSX_BUNDLE_COPYRIGHT "Copyright 2025" )set (MACOSX_BUNDLE_GUI_IDENTIFIER "com.test.sampleapp" )set (MACOSX_BUNDLE_ICON_FILE "logo" )set (MACOSX_BUNDLE_EXECUTABLE_NAME "SampleApp" )set_target_properties (SampleApp PROPERTIES MACOSX_BUNDLE_BUNDLE_NAME "${MACOSX_BUNDLE_BUNDLE_NAME}" MACOSX_BUNDLE_BUNDLE_VERSION "${MACOSX_BUNDLE_BUNDLE_VERSION}" MACOSX_BUNDLE_SHORT_VERSION_STRING "${MACOSX_BUNDLE_SHORT_VERSION_STRING}" MACOSX_BUNDLE_COPYRIGHT "${MACOSX_BUNDLE_COPYRIGHT}" MACOSX_BUNDLE_GUI_IDENTIFIER "${MACOSX_BUNDLE_GUI_IDENTIFIER}" MACOSX_BUNDLE_ICON_FILE "${MACOSX_BUNDLE_ICON_FILE}" ) configure_file (Info.plist.in Info.plist)set_target_properties (SampleApp PROPERTIES MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_BINARY_DIR}/Info.plist" )
使用 add_custom_command 命令将 Info.plist 文件拷贝到软件包,并将软件图标拷贝 Contents/Resources 目录:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 if (MACDEPLOYQT_EXECUTABLE) add_custom_command (TARGET SampleApp POST_BUILD COMMAND ${CMAKE_COMMAND} -E echo "===== 开始执行MacDeployQt命令 =====" COMMAND ${MACDEPLOYQT_EXECUTABLE} "$<TARGET_BUNDLE_DIR:SampleApp>" -always-overwrite -verbose=1 -executable="$<TARGET_BUNDLE_DIR:SampleApp>/Contents/MacOS/SampleApp" COMMAND ${CMAKE_COMMAND} -E echo "===== 开始替换Info.plist =====" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_BINARY_DIR}/Info.plist" "$<TARGET_BUNDLE_DIR:SampleApp>/Contents/Info.plist" COMMAND ${CMAKE_COMMAND} -E echo "===== 开始拷贝logo.icns =====" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/Resources/logo.icns" "$<TARGET_BUNDLE_DIR:SampleApp>/Contents/Resources/logo.icns" VERBATIM ) else () message (FATAL_ERROR "macdeployqt not found! Qt libraries will not be bundled." ) endif ()
软件签名 在签名之前,需要从 Apple 开发者官网申请并下载开发者证书,并导入到钥匙串(在此省略具体步骤,读者自行解决)。
使用 codesign 工具对.app 文件夹进行签名:
1 2 3 4 codesign --force --sign "Developer ID Application: XXXX (XXXXX)" --options runtime --entitlements entitlements.plist --deep --timestamp SampleApp.app codesign -dvvv SampleApp.app
使用 codesign 命令对应用进行签名时,如果出现“The timestamp service is not available”错误,这通常表示您的电脑在签名过程中无法连接到 Apple 的时间戳服务器 time.apple.com。
我们在上述签名命令中指定了 entitlements 参数文件,该文件声明了应用可以访问哪些受保护的系统资源,具体内容如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 <?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd" > <plist version ="1.0" > <dict > <key > com.apple.security.app-sandbox</key > <false /> <key > com.apple.security.device.audio-output</key > <true /> <key > com.apple.security.device.usb</key > <true /> <key > com.apple.security.device.hid.device</key > <true /> <key > com.apple.security.cs.allow-unsigned-executable-memory</key > <true /> <key > com.apple.security.cs.disable-library-validation</key > <true /> <key > com.apple.security.network.client</key > <true /> <key > com.apple.security.network.server</key > <true /> </dict > </plist >
现在将 codesign 命令附加到 CMake 脚本中,实现自动化签名:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 if (MACDEPLOYQT_EXECUTABLE) add_custom_command (TARGET SampleApp POST_BUILD COMMAND ${CMAKE_COMMAND} -E echo "===== 开始执行MacDeployQt命令 =====" COMMAND ${MACDEPLOYQT_EXECUTABLE} "$<TARGET_BUNDLE_DIR:SampleApp>" -always-overwrite -verbose=1 -executable="$<TARGET_BUNDLE_DIR:SampleApp>/Contents/MacOS/SampleApp" COMMAND ${CMAKE_COMMAND} -E echo "===== 开始替换Info.plist =====" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_CURRENT_BINARY_DIR}/Info.plist" "$<TARGET_BUNDLE_DIR:SampleApp>/Contents/Info.plist" COMMAND ${CMAKE_COMMAND} -E echo "===== 开始拷贝logo.icns =====" COMMAND ${CMAKE_COMMAND} -E copy "${CMAKE_SOURCE_DIR}/Resources/logo.icns" "$<TARGET_BUNDLE_DIR:SampleApp>/Contents/Resources/logo.icns" COMMAND ${CMAKE_COMMAND} -E echo "===== 开始执行codesign命令 =====" COMMAND codesign --force --sign "065174DD4EF38F7DC8BFD2F7EC238C90AC18CC82" --entitlements "${CMAKE_SOURCE_DIR}/entitlements.plist" --deep --timestamp --options runtime "$<TARGET_BUNDLE_DIR:SampleApp>" COMMENT "Deploying Qt, restoring Info.plist, copy logo.icns and signing the bundle" VERBATIM ) else () message (FATAL_ERROR "macdeployqt not found! Qt libraries will not be bundled." ) endif ()
由于证书名称中含义空格,添加到 CMake 脚本中会出现语法错误,因此在上面的 CMake 脚本中,--sign使用的是证书的 SHA-1 哈希值(非必须,但这样解决起来更简单)。
使用如下命令查看电脑上可用证书的哈希值:
1 security find-identity -v -p codesigning
公证 公证大致分为下面三个步骤。
第一步:存储凭证
将你的开发者账号凭证安全地存储在钥匙串中,后面直接使用存储的名称即可,该操作仅需要操作一次。
在存储凭证之前需要生成专用密码,可以参考https://support.apple.com/zh-cn/102654 文档来生成专用密码。
自 2023 年 11 月 1 日起,Apple 推荐使用 notarytool,旧工具 altool 已不再被接受。
1 xcrun notarytool store-credentials "SampleApp_Notary" --apple-id "xxxxx@gmail.com" --team-id "85666CM666" --password "bxdt-888-888-888"
第二步:提交公证
将.app 文件夹压缩为.zip 文件,使用如下命令提交公证。
1 xcrun notarytool submit SampleApp.app --keychain-profile "SampleApp_Notary"
提交成功后,会返回类似如下的结果,记住下面结果中的 id,在后面查询的时候需要用到。
1 2 3 4 5 6 7 Conducting pre-submission checks for SampleApp.dmg and initiating connection to the Apple notary service... Submission ID received id: e9808593-40e1-3333-a9b7-10dfc2668ead Upload progress: 100.00% (47.2 MB of 47.2 MB) Successfully uploaded file id: e9808593-40e1-3333-a9b7-10dfc2668ead path: /Users/imac/SampleAppSrc/setup/macos/SampleApp.dmg
第三步:查询结果
1 xcrun notarytool info e9808593-40e1-3333-a9b7-10dfc2668ead --keychain-profile "SampleApp_Notary"
初次公证需要耗时 1 个小时左右,后面软件更新再次提交公证,就比较快了,只需要几分钟。
装订票据 所谓装订票据就是将公证结果装订到.app 文件夹中,确保在离线状态下也可以通过 Gatekeeper 安全验证。
1 xcrun stapler staple SampleApp.app
检查 上述签名、公证、装订完成之后,进行最后的检查,确保没有出错。
验证应用是否已成功装订公证票据:
1 xcrun stapler validate SampleApp.app
返回类似如下内容表示成功:
1 2 Processing: /Users/imac/SampleAppSrc/setup/macos/SampleApp.app The validate action worked!
模拟用户双击安装应用时 Gatekeeper 的完整安全检查流程:
1 spctl -a -v -t install SampleApp.app
返回类似如下内容表示通过检查:
1 2 SampleApp.dmg: accepted source=Notarized Developer ID
生成 DMG 最后来生成 DMG,DMG 不需要签名,也不需要公证。
使用 create-dmg 工具来生成 DMG,而不是每次手动生成,这样可以确保每次生成的 DMG 都是一样的。
create-dmg 是一个用来创建磁盘镜像文件(dmg)的 shell 脚本命令行工具,使用如下命令安装:
使用方法大致如下,具体参数可以根据实际情况来做调整:
1 2 3 4 5 6 7 8 9 10 11 12 create-dmg \ --volname "Sample App Installer" \ --volicon "logo.icns" \ --background "background.png" \ --window-pos 400 200 \ --window-size 480 600 \ --icon-size 100 \ --icon "SampleApp.app" 240 100 \ --hide-extension "SampleApp.app" \ --app-drop-link 240 400 \ "SampleApp.dmg" \ "SampleApp.app/"
因项目归属和协议限制,我无法在此公开完整代码,但文中所涉及的技术方案与实现细节,可随时与我进一步探讨。如有需要,欢迎通过其他渠道交流。