Previously, when using CPack to package a QML program, I noticed that the files installed by the CPack package differed significantly from those installed using the cmake install command. Below, I will explain and resolve this issue.

1. Problem code

Let’s take the previously written QML program as an example. To facilitate code improvements, the CMakeLists.txt is rewritten as follows, with dependency installation and packaging modules written separately into cmake/dependencies.cmake and cmake/packaging.cmake.

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

if(NOT $ENV{VCPKG_TARGET_TRIPLET} STREQUAL "")
  set(VCPKG_TARGET_TRIPLET $ENV{VCPKG_TARGET_TRIPLET})
  message(STATUS "VCPKG_TARGET_TRIPLET: ${VCPKG_TARGET_TRIPLET}")
  link_libraries("$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/lib")
  message(STATUS "Link libraries from: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/$<$<CONFIG:Debug>:Debug>/lib")
  include_directories("$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/include")
  message(STATUS "Include directories from: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/include")
  if(${VCPKG_TARGET_TRIPLET} MATCHES "static")
    set(CMAKE_TOOLCHAIN_FILE "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6/qt.toolchain.cmake")
    message(STATUS "Using static Qt6 toolchain file: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6/qt.toolchain.cmake")
    set(Qt6_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6")
    message(STATUS "Using static Qt6_DIR: $ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/share/Qt6")
  endif()
endif()

find_package(Qt6 6.5 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.5)
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")
    include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/dependencies.cmake)
endif()

include(${CMAKE_CURRENT_SOURCE_DIR}/cmake/packaging.cmake)

The cmake/dependencies.cmake module defines how to copy dependencies from the Qt installation path to the installation path.

# --- cmake/dependencies.cmake ---

set(deploy_tool_options_arg "")

if(APPLE)
  set(deploy_tool_options_arg --hardened-runtime)
elseif(WIN32)
  set(deploy_tool_options_arg --no-compiler-runtime --no-system-d3d-compiler --no-system-dxc-compiler)
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" "api-ms-*"
        POST_EXCLUDE_REGEXES "system32"
    )
    foreach(DEP_LIB ${RESOLVED_DEPS})
        file(INSTALL ${DEP_LIB} DESTINATION ${CMAKE_INSTALL_PREFIX}/bin)
    endforeach()
]])

install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/resources" DESTINATION .)

Note that after using the install(CODE) module, the install(DIRECTORY) is used to copy resource files. The target path here should be set to the relative path ., and you should not use an absolute path like ${CMAKE_INSTALL_PREFIX}, otherwise cpack will report an error. This is because when cpack calls the install module to copy installation files, absolute paths cannot be specified. Paradoxically, absolute paths inside install(CODE) do not cause errors during packaging, and inside install(CODE) you can only use ${CMAKE_INSTALL_PREFIX} to represent absolute paths; otherwise, running cmake install will result in incorrect paths.

The cmake/packaging.cmake module defines how to package the installation files.

# --- cmake/packaging.cmake ---

include(InstallRequiredSystemLibraries)

set(CPACK_PACKAGE_NAME "${PROJECT_NAME}-installer")
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_CREATE_DESKTOP_LINKS "bin/${EXECUTABLE_NAME}")

set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "A demo qt quick application with CMake packaging")
set(CPACK_PACKAGE_FILE_NAME "${CPACK_PACKAGE_NAME}-${CPACK_PACKAGE_VERSION}-${CMAKE_SYSTEM_NAME}")

set(CPACK_RESOURCE_FILE_LICENSE "${CMAKE_CURRENT_SOURCE_DIR}/resources/LICENSE" CACHE FILEPATH "License file")
set(CPACK_WIX_LICENSE_RTF "${CMAKE_CURRENT_SOURCE_DIR}/resources/LICENSE.rtf" CACHE FILEPATH "License file in RTF format")
set(CPACK_RESOURCE_FILE_README "${CMAKE_CURRENT_SOURCE_DIR}/resources/README.md" CACHE FILEPATH "Readme file")

set(CPACK_PACKAGE_INSTALL_DIRECTORY "${PROJECT_NAME}")
if(NOT WIN32)
    set(CPACK_PACKAGING_INSTALL_PREFIX "/opt")
endif()

include(CPack)

Here, as before, system dependencies are automatically searched through include(InstallRequiredSystemLibraries), but this only applies to system dependencies and does not automatically copy Qt dependencies. The Qt dependencies have already been copied once through the deployment script in cmake/dependencies.cmake using qt_generate_deploy_qml_app_script. If you switch to other libraries, such as wxWidgets or GTK, and no deployment script is defined, then it is very likely that the packaged program will only include the standalone executable and will not copy the dependency files for wxWidgets or GTK at all.

2. Example

By installing and packaging with the following commands, you can compare the differences between the CMake installation and CPack packaging scripts above. Here, we use the clang64 toolchain from msys2 to compile the output.

$Env:PATH += ";D:\conda\envs\cpp_env\NSIS"
cmake -B build .
cmake --build build --config Release
cmake --install build --config Release --prefix $PWD/install
cd build
cpack -G "NSIS"

Compare the differences between the output directories of CMake installation and CPack packaging by using the diff tool in msys2.

$ diff -r `cygpath -u 'D:\example\cpack\qml_sample\install'` `cygpath -u 'D:\example\cpack\qml_sampl
e\build\_CPack_Packages\win64\NSIS\sample-installer-0.1-Windows'`
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:double-conversion.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:freetype.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libb2-1.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libbrotlicommon.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libbrotlidec.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libbz2-1.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libc++.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:libcrypto-3-x64.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libdouble-conversion.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libfreetype-6.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libglib-2.0-0.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libgraphite2.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libharfbuzz-0.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libiconv-2.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libicudt77.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libicuin77.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libicuuc77.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libintl-8.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libmd4c.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libpcre2-16-0.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libpcre2-8-0.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:libpng16.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libpng16-16.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:libzstd.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:msvcp140.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:msvcp140_1.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:msvcp140_2.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:pcre2-16.dll
二进制文件 /d/example/cpack/qml_sample/install/bin/Qt6Core.dll 和 /d/example/cpack/qml_sample/build/
_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin/Qt6Core.dll 不同
二进制文件 /d/example/cpack/qml_sample/install/bin/Qt6Gui.dll 和 /d/example/cpack/qml_sample/build/_
CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin/Qt6Gui.dll 不同
二进制文件 /d/example/cpack/qml_sample/install/bin/Qt6Network.dll 和 /d/example/cpack/qml_sample/bui
ld/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin/Qt6Network.dll 不同
二进制文件 /d/example/cpack/qml_sample/install/bin/Qt6Qml.dll 和 /d/example/cpack/qml_sample/build/_
CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin/Qt6Qml.dll 不同
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:vcruntime140.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:vcruntime140_1.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:zlib.dll
只在 /d/example/cpack/qml_sample/install/bin 中存在:zlib1.dll
只在 /d/example/cpack/qml_sample/build/_CPack_Packages/win64/NSIS/sample-installer-0.1-Windows/bin
中存在:zstd.dll

It can be seen that the bin folder in the CPack packaging directory is missing many dependency libraries, including Clang’s runtime dependency libc.dll, which causes the program packaged by CPack to fail to start properly. In contrast, the CMake installation program does not have this issue and can start normally. This also shows that the CMake InstallRequiredSystemLibraries module does not automatically copy the dependencies of the MSYS2 toolchain, and InstallRequiredSystemLibraries gives priority to obtaining dependencies from the system PATH, which can result in different binary files for certain dependencies.

3. Improve

Here, comment out include(InstallRequiredSystemLibraries) in cmake/packaging.cmake, and keep everything else unchanged.

# --- cmake/packaging.cmake ---

# include(InstallRequiredSystemLibraries)
...

Improve cmake/dependencies.cmake by no longer using the install(CODE) block to copy dependencies, and instead use install(RUNTIME_DEPENDENCY_SET) and install(IMPORTED_RUNTIME_ARTIFACTS).

# --- cmake/dependencies.cmake ---
...
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()

install(RUNTIME_DEPENDENCY_SET qt_deps
  PRE_EXCLUDE_REGEXES "system32" "api-ms-*"
  POST_EXCLUDE_REGEXES "system32"
  DIRECTORIES ${QT_BIN_DIR}
)
install(IMPORTED_RUNTIME_ARTIFACTS ${EXECUTABLE_NAME}
  RUNTIME_DEPENDENCY_SET qt_deps
  DESTINATION ${CMAKE_INSTALL_BINDIR}
)
...

This approach avoids the problem of inconsistent variables between compile time and installation time. Using install(CODE), the installation phase cannot access variables input during the CMake configuration and compilation; parameters can only be passed through environment variables. CMake variables like ${PROJECT_NAME} will become undefined inside it. The method above avoids this problem. If interested, you could also rewrite vcpkg’s environment variable parameters as configuration-time input variables, but we won’t go into that here.

Regarding the installation path of dynamic library files, using the second method, that is, install(RUNTIME_DEPENDENCY_SET) and install(IMPORTED_RUNTIME_ARTIFACTS), the final installed dynamic library files are located in the lib folder of the installation directory on Linux (you may need to manually set the LD_LIBRARY_PATH environment variable to ensure the library search path, as with wxWidgets), while on Windows they are located in the bin folder. The first method explicitly specifies the path ${CMAKE_INSTALL_PREFIX}/bin, so this ambiguity does not occur.

4. Effect

The installation and packaging commands are the same as in 2. Example .

Comparing the directories of CMake installation and CPack packaging, there is no difference; both can start normally.

$ diff -r `cygpath -u 'D:\example\cpack\qml_sample\install'` `cygpath -u 'D:\example\cpack\qml_sampl
e\build\_CPack_Packages\win64\NSIS\sample-installer-0.1-Windows'`

65d5ae74dda5617b871783b97e3a6c93.png

5. Locate Search Path

There is another detail in the cmake/dependencies.cmake module mentioned above. When copying dependency libraries, it first searches for the qmake tool, then uses qmake to obtain the Qt installation path, and finally searches for installed binary dependencies in that path. This is the same way CMake searches for Qt libraries: it first locates the qmake path and then defines other variables. This approach can maximally ensure that the installed binary dependencies are not ‘polluted’ by the system PATH.

For other libraries, such as wxWidgets or gtk4, you can ensure accurate paths by locating wxrc or gtk4-demo.

However, there are exceptions. For example, vcpkg’s runtime and tool paths are separate, which needs to be considered individually.

# --- cmake/dependencies.cmake ---
...
if("$ENV{VCPKG_TARGET_TRIPLET}" STREQUAL "")
  find_program(GTK4DEMO gtk4-demo HINTS $ENV{PATH})
  if(NOT GTK4DEMO)
    message(FATAL_ERROR "gtk4-demo not found in PATH")
  else()
    message(STATUS "gtk4-demo found: ${GTK4DEMO}")
    cmake_path(GET GTK4DEMO PARENT_PATH GTK4_BIN_DIR)
    message(STATUS "gtk4-demo bin dir: ${GTK4_BIN_DIR}")
  endif()
else()
  set(GTK4_BIN_DIR "$ENV{VCPKG_ROOT}/installed/$ENV{VCPKG_TARGET_TRIPLET}/bin")
  if(NOT EXISTS ${GTK4_BIN_DIR})
    message(FATAL_ERROR "gtk4-demo bin dir not found: ${GTK4_BIN_DIR}")
  endif()
endif()
...

6. Search and install resource files

For resource files, such as when distributing a GTK4 program, in addition to copying binary dependencies, you also need to copy the application’s resource files1. This can be done using install(DIRECTORY) or install(FILES) to copy directories or files to the distribution directory. Note that the target path should use the relative path . instead of ${CMAKE_INSTALL_PREFIX}.

# --- cmake/dependencies.cmake ---
...
cmake_policy(SET CMP0177 NEW)

function(install_dir SEARCH_DIR DESTINATION_DIR SUB_DIR PATTERN)
  if(IS_DIRECTORY "${SEARCH_DIR}/${SUB_DIR}")
    file(GLOB DIRECTORIES LIST_DIRECTORIES true RELATIVE "${SEARCH_DIR}/${SUB_DIR}" "${SEARCH_DIR}/${SUB_DIR}/${PATTERN}")
    foreach(DIR ${DIRECTORIES})
      if(IS_DIRECTORY "${SEARCH_DIR}/${SUB_DIR}/${DIR}")
        install(DIRECTORY "${SEARCH_DIR}/${SUB_DIR}/${DIR}" DESTINATION "${DESTINATION_DIR}/${SUB_DIR}")
        message(STATUS "Install copy dir: ${SEARCH_DIR}/${SUB_DIR}/${DIR} -> ${DESTINATION_DIR}/${SUB_DIR}")
      endif()
    endforeach()
  endif()
endfunction()

cmake_path(GET GTK4_BIN_DIR PARENT_PATH GTK_SEARCH_DIRS)
install_dir(${GTK_SEARCH_DIRS} . "etc" "*gtk-4*")
install_dir(${GTK_SEARCH_DIRS} . "share" "*gtk-4*")
install_dir(${GTK_SEARCH_DIRS} . "share" "themes")
install_dir(${GTK_SEARCH_DIRS} . "share" "icons")
install_dir("${GTK_SEARCH_DIRS}/share" "./share" "glib-2.0" "schemas")

install(DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}/resources" DESTINATION .)
...

Here, set cmake_policy(SET CMP0177 NEW) to avoid errors caused by install() target paths being relative like . or .., but the CMake version must be >=3.31, otherwise it will report an error.