事实上,CMake除了构建管理之外,还可以进行测试和打包安装程序。其中打包功能由CPack模块1支持,可以针对不同平台打包生成适用的安装程序。

1. Qt程序CMake配置

以之前编写的QML程序为例,编写CMakeLists.txt配置文件如下。末尾引入的packaging.cmake文件定义了打包配置相关信息。

cmake_minimum_required(VERSION 3.16)
set(PROJECT_NAME sample)
project(${PROJECT_NAME} VERSION 0.1 LANGUAGES CXX)
set(EXECUTABLE_NAME ${PROJECT_NAME}_app)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

find_package(Qt6 REQUIRED COMPONENTS Quick)

if(Qt6_FOUND)
    message(STATUS "Qt6 found: ${Qt6_DIR}")
else()
    message(FATAL_ERROR "Qt6 not found!")
endif()

qt_standard_project_setup(REQUIRES 6.8)
qt_add_executable(${EXECUTABLE_NAME}
    main.cpp
)
qt_add_qml_module(${EXECUTABLE_NAME}
    URI ${PROJECT_NAME}
    VERSION 1.0
    QML_FILES
    Main.qml
)

# set static linking for MSVC
if(MSVC AND (${VCPKG_TARGET_TRIPLET} MATCHES "static"))
    message(STATUS "Configuring for MSVC static linking")
    set_property(TARGET ${EXECUTABLE_NAME} PROPERTY
        MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>:Debug>")
endif()

set_target_properties(${EXECUTABLE_NAME} PROPERTIES

    # MACOSX_BUNDLE_GUI_IDENTIFIER com.example.${EXECUTABLE_NAME}
    MACOSX_BUNDLE_BUNDLE_VERSION ${PROJECT_VERSION}
    MACOSX_BUNDLE_SHORT_VERSION_STRING ${PROJECT_VERSION_MAJOR}.${PROJECT_VERSION_MINOR}
    MACOSX_BUNDLE TRUE
    WIN32_EXECUTABLE TRUE
)

target_link_libraries(${EXECUTABLE_NAME}
    PRIVATE Qt6::Quick
)

include(GNUInstallDirs)
install(TARGETS ${EXECUTABLE_NAME}
    BUNDLE DESTINATION .
    LIBRARY DESTINATION ${CMAKE_INSTALL_LIBDIR}
    RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}
)

if(MSVC AND (${VCPKG_TARGET_TRIPLET} MATCHES "static"))
    message(STATUS "Linking static MSVC runtime, skipping install of MSVC runtime DLLs")
else()
    message(STATUS "Configuring install of runtime dependencies")
    set(deploy_tool_options_arg "")

    if(APPLE)
        set(deploy_tool_options_arg --hardened-runtime)
    elseif(WIN32)
        set(deploy_tool_options_arg --no-compiler-runtime)
    endif()

    qt_generate_deploy_qml_app_script(
        TARGET ${EXECUTABLE_NAME}
        OUTPUT_SCRIPT deploy_script
        MACOS_BUNDLE_POST_BUILD
        NO_UNSUPPORTED_PLATFORM_ERROR
        DEPLOY_USER_QML_MODULES_ON_UNSUPPORTED_PLATFORM
        DEPLOY_TOOL_OPTIONS ${deploy_tool_options_arg}
    )

    install(SCRIPT ${deploy_script})

    install(CODE [[
    if("$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
      find_program(QMAKE qmake6 HINTS $ENV{PATH})
      if(NOT QMAKE)
        message(FATAL_ERROR "qmake not found in PATH")
      else()
        message(STATUS "qmake found: ${QMAKE}")
        cmake_path(GET QMAKE PARENT_PATH QT_BIN_DIR)
        message(STATUS "qmake bin dir: ${QT_BIN_DIR}")
      endif()
    else()
      set(QT_BIN_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/bin")
      if(NOT EXISTS ${QT_BIN_DIR})
        message(FATAL_ERROR "qmake bin dir not found: ${QT_BIN_DIR}")
      endif()
    endif()
    file(GET_RUNTIME_DEPENDENCIES
        RESOLVED_DEPENDENCIES_VAR RESOLVED_DEPS
        UNRESOLVED_DEPENDENCIES_VAR UNRESOLVED_DEPS
        # path to the executable files
        EXECUTABLES $<TARGET_FILE:sample_app>
        # directories to search for library files
        DIRECTORIES ${QT_BIN_DIR}
        PRE_EXCLUDE_REGEXES "system32"
        PRE_EXCLUDE_REGEXES "api-ms-*"
        POST_EXCLUDE_REGEXES "system32"
    )
    foreach(DEP_LIB ${RESOLVED_DEPS})
        file(INSTALL ${DEP_LIB} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
    endforeach()
    ]])
endif()

include(${CMAKE_CURRENT_SOURCE_DIR}/packaging.cmake)

install定义了要安装什么内容之后,在cmake配置文件末尾增加打包相关内容2

# --- packaging.cmake ---

include(InstallRequiredSystemLibraries)
set(CPACK_PACKAGE_NAME "qml_sample_install")
set(CPACK_PACKAGE_VERSION ${PROJECT_VERSION})
set(CPACK_PACKAGE_CONTACT "author_email@example.com")
set(CPACK_PACKAGE_VENDOR "author_name")
set(CPACK_PACKAGE_EXECUTABLES "${EXECUTABLE_NAME}" "${EXECUTABLE_NAME}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A demo qt quick application with CMake packaging")
set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE" CACHE FILEPATH "License file")
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/README.md" CACHE FILEPATH "Readme file")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}")
set(CPACK_CREATE_DESKTOP_LINKS "bin/${EXECUTABLE_NAME}")
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_NAME}")

include(CPack)

2. NSIS 打包

CPack本身不提供打包安装程序,需要借助其他程序打包成可执行的安装包。Windows平台下比较常用的有NSIS3和Inno Setup4等。

首先运行cmake命令配置构建脚本并编译。

cmake -B build .
cmake --build build --config Release

进入构建目录,运行CPack程序进行打包。-G "NSIS"表示打包生成器指定为NSIS5,注意要将NSIS安装目录添加到PATH里面。

$Env:PATH += ";D:\conda\envs\cpp_env\NSIS"
cd build
cpack -G "NSIS"

完成后在build/_CPack_Packages/win64/NSIS路径下可以找到生成的exe安装包。

f4fb6534a8293fdd745b34ffe095af87.png
a453db88b335dcbc591de463e93f5ee5.png

用NSIS打包只适合Windows平台,而且打包的安装程序可以通过7z、WinRAR等压缩程序直接打开,安全性不高。

3. Inno Setup打包

CPack的生成器选择"INNOSETUP"6,同样需要先安装Inno Setup并将安装路径添加到PATH。

$Env:PATH += ";D:\opt\Inno Setup 6"
cd build
cpack -G "INNOSETUP"

打包完成后在build/_CPack_Packages/win64/INNOSETUP路径下生成安装文件。

b316b68c1086c67f5a773fa6eb2c2555.png
11d8d0810489b44cf2b2398f185467b1.png

Inno Setup同样只适合Windows平台,相比NSIS安全性有了一些提升,但还是可以通过专用工具解包。

4. WiX打包

WiX7是专门为Windows开发的打包工具,用于生成.msi格式的安装包。打包工具可以从WiX3发布页面 下载。

WiX的许可证文件只支持rtf格式文件,需要在CMake配置文件中定义CPACK_WIX_LICENSE_RTF来指定许可证文件,否则构建安装包时会报错。

# --- packaging.cmake ---

...
set(CPACK_WIX_LICENSE_RTF "${CMAKE_CURRENT_SOURCE_DIR}/LICENSE.rtf" CACHE FILEPATH "License file in RTF format")
...

生成器选择"WIX"8

$Env:PATH += ";D:\opt\wix314-binaries"
cd build
cpack -G "WIX"

打包完成后在build/_CPack_Packages/win64/WIX路径下查看生成的文件。

8d17cb4321aea99723a954818bb90dcb.png
d3fda1b6f8e18983ade49c6ff8444b3c.png

使用这种方式打包的程序安装之后不会在安装目录留下卸载程序,需要通过控制面板-程序设置-应用界面卸载。

5. Qt Installer Framework打包

Qt Installer Framework9是Qt官方专门为Qt开发的打包安装程序,适合所有受Qt支持的平台。Qt Installer Framework可以从Qt官方发布网站下载10

CPack打包命令,生成器选择"IFW"11

$Env:PATH += ";D:\opt\Qt\QtIFW-4.8.1\bin"
cd build
cpack -G "IFW"

Windows下打包完成后在build/_CPack_Packages/win64/IFW路径下生成安装文件。

bf1367ede5af04ff1ced896b297cb147.png
e84a7cfbc8a5be12b59f321449c83f6a.png

Linux下打包完成后直接在build目录下就能找到安装文件,默认添加.run后缀名。

259e4d9e069c1ef7810529078f115cc7.png
6cd355e959f2302c0e50713aa0c9fc47.png

Qt Installer Framework打包的优点是支持跨平台,而且不同平台的安装程序操作界面一致性较好,缺点是无法生成开始菜单启动项和桌面快捷方式,需要自己针对不同安装平台单独编写安装脚本。

6. 打包DEB文件

CPack支持打包Linux二进制文件。指定DEB生成器12用于生成DEB包,适合Ubuntu、Debian等系统。

cpack -G "DEB"

打包完成后直接在build目录下生成DEB包。

82619641499051f90b9a27de91f6a6e2.png

使用dpkg工具直接安装,也可以通过apt工具安装。

dpkg -i path-to-package.deb

7. 打包RPM文件

指定DEB生成器13用于生成RPM包,适合Fedora、Red Hat、SUSE等系统。

cpack -G "RPM"

build目录下可以找到打包生成的RPM包。

c85cf8fb75a085bfc72a994e3f69043c.png

使用rpm工具安装即可,也可以通过dnf、zypper等包管理工具安装。

rpm -ivh path-to-package.rpm

8. 总结

Windows下发布的安装包,推荐使用Inno Setup或者WiX打包。Linux则建议分别生成DEB包和RPM包以适应不同发行版要求。

如果不考虑打包之后的文件体积,可以使用Qt Installer Framework打包,跨平台而且一致性更好,只是需要自己编写脚本生成桌面图标和开始菜单启动项。