之前用CPack打包QML程序的时候发现,CPack打包程序安装的文件和使用cmake install命令安装的文件存在明显的不同,下面解释并解决这个问题。
1. 问题代码
还是以之前编写的QML程序为例,为了方便改进代码将CMakeLists.txt改写如下,依赖安装和打包模块分别写入cmake/dependencies.cmake和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)
cmake/dependencies.cmake模块定义了如何从qt安装路径中复制依赖带安装路径。
# --- 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 .)
注意在install(CODE)模块之后使用了install(DIRECTORY)复制资源文件,这里的目标路径要设置为相对路径.,不能用绝对路径比如${CMAKE_INSTALL_PREFIX},否则cpack打包的时候会报错。因为cpack调用install模块复制安装文件的时候不能指定绝对路径,但吊诡的是install(CODE)里面的绝对路径打包的时候居然不报错,而且install(CODE)里面也只能使用${CMAKE_INSTALL_PREFIX}表示绝对路径,否则运行cmake install安装的时候路径又会出错。
cmake/packaging.cmake模块定义了如何打包安装文件。
# --- 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)
这里和之前一样,通过include(InstallRequiredSystemLibraries)自动搜索系统依赖,但是也仅限于系统依赖,不会自动复制qt的依赖。qt的依赖在cmake/dependencies.cmake里面已经通过qt_generate_deploy_qml_app_script发布脚本自动复制了一遍。如果换成其他库,比如wxWidgets或者gtk,没有定义发布脚本的话,那么很大可能打包的程序就只有单独的可执行文件,根本不会复制wxWidgets或者gtk的依赖文件。
2. 问题示例
通过以下命令安装和打包,可以比较一下上面的脚本CMake安装和CPack打包效果的区别,这里用msys2的clang64工具链编译输出。
$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"
比较CMake安装和CPack打包两个输出目录的差异,使用msys2运行diff工具检查。
$ 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
可以看到CPack打包目录下的bin文件夹少了很多依赖库,包括clang的运行时依赖libc++.dll,导致CPack打包的程序不能正常启动。而CMake安装程序则没有这个问题,可以正常启动。从这里也能看到,CMakeInstallRequiredSystemLibraries模块不会自动复制msys2工具链的依赖库,而且InstallRequiredSystemLibraries更优先从系统PATH中获取依赖,造成某些依赖库的文件二进制不同。
3. 改进代码
这里将cmake/packaging.cmake里面的include(InstallRequiredSystemLibraries)注释掉,其他保持不变。
# --- cmake/packaging.cmake ---
# include(InstallRequiredSystemLibraries)
...
改进cmake/dependencies.cmake,不再使用install(CODE)代码块复制依赖,改用install(RUNTIME_DEPENDENCY_SET)和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}
)
...
这种写法避免了编译期和安装期变量不一致的问题。使用install(CODE)安装期无法获取cmake配置编译期间输入的变量,只能通过环境变量传入参数,类似${PROJECT_NAME}的CMAKE变量在其内部都会失效变成未定义。上面的写法则避免了这个问题,感兴趣的话还可以把vcpkg的环境变量参数改写成配置期的输入变量,这里不做过多展开了。
关于动态链接库文件的安装路径,使用第二种写法也就是install(RUNTIME_DEPENDENCY_SET)+install(IMPORTED_RUNTIME_ARTIFACTS)的写法,最后安装的动态链接库文件,在linux平台上位于安装目录的lib文件夹中(可能需要手动指定LD_LIBRARY_PATH环境变量确保库文件搜索路径,比如wxWidgets),而Windows平台下则是位于bin文件夹中。而第一种写法已经指定了${CMAKE_INSTALL_PREFIX}/bin路径,不会出现这种歧义。
4. 改进效果
安装和打包命令同2. 问题示例 。
比较CMake安装和CPack打包目录的差异,没有任何区别,都能正常启动。
$ 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'`
5. 定位搜索路径
上面的cmake/dependencies.cmake模块还有个细节,复制依赖库的时候先搜索qmake工具,再通过qmake工具获取Qt的安装路径,然后从该安装路径中搜索安装二进制依赖库。这和CMake搜索Qt库的方式也是一样的,先定位qmake的路径再定义其他变量。这种方式可以最大程度确保安装的二进制依赖库不受系统PATH的"污染"。
对于其他库,比如wxWidget或者gtk4,可以通过定位wxrc或gtk4-demo来确保路径准确。
不过也存在例外,比如vcpkg的运行时路径和工具路径是分开的,需要单独考虑。
# --- 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. 搜索安装资源文件
对于资源文件,比如gtk4程序发布除了复制二进制依赖之外还需要复制应用资源文件的1,可以通过install(DIRECTORY)或install(FILES)的方式拷贝发布目录或文件,目标路径注意需要用相对路径.替代${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 .)
...
这里设置cmake_policy(SET CMP0177 NEW),避免出现因install()目标路径为相对路径.和..导致的错误,但是CMake版本必须>=3.31,否则会报错。