C++ Qt应用在macOS平台的编译与发布

在 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
brew install create-dmg

使用方法大致如下,具体参数可以根据实际情况来做调整:

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/"

因项目归属和协议限制,我无法在此公开完整代码,但文中所涉及的技术方案与实现细节,可随时与我进一步探讨。如有需要,欢迎通过其他渠道交流。