30、CPack实战案例二:构建一个带Qt界面的桌面应用并打包为NSIS安装包和DMG镜像

说实话,Qt 桌面应用打包这件事,我踩过的坑比写代码的时间还多。早些年我还在用 Qt4 的时候,给客户发一个 exe 过去,对方打开就报“缺少 DLL”,那场面别提多尴尬了。后来我学乖了,老老实实用 CPack 配合 NSIS 和 DMG 来做安装包。今天我就把这个流程完整走一遍,你跟着做就行。

30.1 项目结构设计

我个人习惯把 Qt 项目拆成三层:源码、资源、打包脚本。这样后期维护起来特别清爽。来看一个典型的目录结构:

MyQtApp/
├── CMakeLists.txt
├── src/
│   ├── main.cpp
│   ├── mainwindow.h
│   └── mainwindow.cpp
├── resources/
│   ├── app.qrc
│   └── icons/
│       └── app.ico
└── cmake/
    └── packaging.cmake

嗯,这里要注意:packaging.cmake 文件我单独拎出来了,因为打包逻辑和构建逻辑混在一起的话,后面改起来很痛苦。你想想看,如果哪天要换安装包格式,你只需要改这一个文件就行。

30.2 核心 CMakeLists.txt 配置

先看主 CMakeLists.txt。这里我用了 find_package 来定位 Qt,然后设置 CMAKE_INSTALL_PREFIX。这个前缀路径决定了安装包里的目录结构。

cmake_minimum_required(VERSION 3.16)
project(MyQtApp VERSION 1.0.0 LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

# 找 Qt
find_package(Qt5 COMPONENTS Widgets REQUIRED)

# 源文件
set(SOURCES
    src/main.cpp
    src/mainwindow.cpp
    src/mainwindow.h
)

# 资源文件
set(RESOURCES
    resources/app.qrc
)

# 生成可执行文件
add_executable(${PROJECT_NAME} ${SOURCES} ${RESOURCES})

# 链接 Qt
target_link_libraries(${PROJECT_NAME} PRIVATE Qt5::Widgets)

# 设置安装规则
install(TARGETS ${PROJECT_NAME}
    RUNTIME DESTINATION bin
)

# 引入打包配置
include(cmake/packaging.cmake)

这里有个小细节:RUNTIME DESTINATION bin 是给 Windows 用的,Linux 和 macOS 下会忽略。我刚开始做跨平台打包时,总以为要写三个不同的 install 规则,后来发现 CMake 自己会处理。

30.3 打包配置:packaging.cmake

这个文件是核心。我把它拆成两部分:通用设置和平台特定设置。

30.3.1 通用设置

# 设置 CPack 基本信息
set(CPACK_PACKAGE_NAME "MyQtApp")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_VENDOR "MyCompany")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A Qt Desktop Application")
set(CPACK_PACKAGE_CONTACT "support@mycompany.com")

# 设置输出目录
set(CPACK_OUTPUT_FILE_PREFIX "${CMAKE_BINARY_DIR}/packages")

30.3.2 Windows NSIS 配置

NSIS 打包,说白了就是生成一个 exe 安装向导。我最头疼的是 Qt 的 DLL 依赖,少一个就崩。后来我用了 windeployqt 工具来自动收集 DLL。

if(WIN32)
    # 启用 NSIS 生成器
    set(CPACK_GENERATOR "NSIS")

    # 安装后自动运行 windeployqt
    install(CODE "
        execute_process(
            COMMAND windeployqt --release
                \"${CMAKE_INSTALL_PREFIX}/bin/${PROJECT_NAME}.exe\"
            WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/bin\"
        )
    ")

    # NSIS 特定设置
    set(CPACK_NSIS_DISPLAY_NAME "MyQtApp")
    set(CPACK_NSIS_INSTALL_ROOT "$PROGRAMFILES64")
    set(CPACK_NSIS_CREATE_ICONS_EXTRA
        "CreateShortCut '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\MyQtApp.lnk' '$INSTDIR\\\\bin\\\\${PROJECT_NAME}.exe'"
    )
    set(CPACK_NSIS_DELETE_ICONS_EXTRA
        "Delete '$SMPROGRAMS\\\\$STARTMENU_FOLDER\\\\MyQtApp.lnk'"
    )
endif()

注意: 我曾经在 NSIS 打包时忘记设置 CPACK_NSIS_INSTALL_ROOT,结果安装包默认装到了 32 位目录下。64 位应用一定要指定 $PROGRAMFILES64

30.3.3 macOS DMG 配置

macOS 打包相对简单,但有个坑:DMG 镜像的卷标不能有中文,否则挂载时会乱码。我吃过这个亏,后来老老实实用英文卷标。

if(APPLE)
    # 启用 DMG 生成器
    set(CPACK_GENERATOR "DragNDrop")

    # 安装后自动运行 macdeployqt
    install(CODE "
        execute_process(
            COMMAND macdeployqt
                \"${CMAKE_INSTALL_PREFIX}/bin/${PROJECT_NAME}.app\"
            WORKING_DIRECTORY \"${CMAKE_INSTALL_PREFIX}/bin\"
        )
    ")

    # DMG 特定设置
    set(CPACK_DMG_VOLUME_NAME "MyQtApp")
    set(CPACK_DMG_FORMAT "UDBZ")
    set(CPACK_DMG_DS_STORE "${CMAKE_SOURCE_DIR}/resources/DS_Store")
endif()

小技巧: CPACK_DMG_DS_STORE 可以指定一个预制的 .DS_Store 文件,这样生成的 DMG 打开后窗口大小和图标位置都是你想要的。我一般先在 Finder 里手动调整好,然后把隐藏的 .DS_Store 复制出来。

30.4 构建与打包命令

构建命令其实很简单,但顺序不能乱。我习惯用 cmake --build 而不是直接 make,因为这样更通用。

# 配置
mkdir build && cd build
cmake .. -DCMAKE_INSTALL_PREFIX=./install

# 构建
cmake --build . --config Release

# 安装到临时目录
cmake --install . --config Release

# 打包
cpack --config CPackConfig.cmake

打包完成后,在 build/packages 目录下会生成 MyQtApp-1.0.0-win64.exeMyQtApp-1.0.0-Darwin.dmg

30.5 核心流程 SVG 图

下面这张图展示了从源码到安装包的完整流程。我特意把 windeployqtmacdeployqt 标红了,因为这是最容易出问题的环节。

Qt 桌面应用 CPack 打包流程 CMakeLists.txt 源码 + 资源 cmake .. 生成构建系统 cmake --build 编译生成 exe/app cmake --install 安装到临时目录 windeployqt / macdeployqt 自动收集 DLL / Framework cpack 生成 NSIS / DMG 安装包 .exe / .dmg Windows: NSIS macOS: DragNDrop

30.6 避坑指南

我总结几个实战中容易翻车的地方:

  • Qt 版本不匹配: 我曾经用 Qt 5.15 编译,但系统里装的是 Qt 5.12 的 windeployqt,结果打出来的包缺了 Qt5Core.dll。一定要确保 windeployqt 和编译用的 Qt 版本一致。
  • NSIS 中文乱码: 安装向导里的中文文本如果编码不对,会显示成乱码。我建议在 CPackConfig.cmake 里显式设置 CPACK_NSIS_ENCODING UTF-8
  • DMG 卷标长度: macOS 的 DMG 卷标不能超过 27 个字符,否则挂载时会截断。我一般控制在 20 个字符以内。
  • 权限问题: macOS 下如果 macdeployqt 报权限错误,记得给可执行文件加上执行权限:chmod +x

核心要点: Qt 桌面应用打包的关键在于依赖收集。无论是 Windows 的 DLL 还是 macOS 的 Framework,少一个应用就起不来。CPack 本身不处理这些,你需要借助 windeployqtmacdeployqt 来完成。我建议在 CI/CD 流程中加上这一步的自动化验证,比如打包后启动应用看看能不能正常显示窗口。

30.7 验证打包结果

打包完成后,我习惯做三件事来验证:

  1. 检查文件完整性:dumpbin /dependents(Windows)或 otool -L(macOS)查看可执行文件的依赖是否都在。
  2. 在干净环境安装: 找一台没有 Qt 的机器或虚拟机,安装后运行,看是否报错。
  3. 检查安装路径: 确保安装目录没有硬编码的绝对路径,否则换台机器就崩了。

嗯,基本上做到这三点,你的安装包就稳了。我当年第一次用 CPack 打包 Qt 应用时,就是因为没做第二步验证,结果发给客户后对方说打不开,排查了半天发现是少了 platforms/qwindows.dll。从那以后,我每次打包完都会在干净环境里跑一遍。


公众号:蓝海资料掘金营,微信deep3321