不管是在Linux环境下,还是Windows环境下,使用Qt开发的程序,都需要依赖Qt的动态连接库才能运行。
所以,为了使用我们程序的用户的便利,我们给用Qt开发的程序做安装包的时候,把这些依赖的动态链接库都一起打包,会给我们的用户极大的方便。
但是,一个程序的运行需要什么动态库,一个一个地手动找出来,实在是太累了。做为程序员,我们都会通过自动化的工具,来实现这个操作。
通用思路
首先最通用的方法是,我们可以把编译出来的程序,使用二进制分析工具,进行分析,看它都依赖了哪些动态链接库。
在不同的操作系统下,有不同的实现。
GNU/Linux环境
对于Linux下的程序,即elf文件,可以使用ldd来命令,来解析出来程序需要的.so文件。
比如,我们可以通过如下命令,来获取/usr/bin/konsole程序的依赖:
ldd /usr/bin/konsole
执行以后,将得到如下输出:
linux-vdso.so.1 (0x00007f501757d000)
libkonsoleapp.so.24.05.0 => /lib64/libkonsoleapp.so.24.05.0 (0x00007f50174e9000)
libkonsoleprivate.so.24.05.0 => /lib64/libkonsoleprivate.so.24.05.0 (0x00007f5017200000)
libKF6DBusAddons.so.6 => /lib64/libKF6DBusAddons.so.6 (0x00007f50174c8000)
……
libunistring.so.5 => /lib64/libunistring.so.5 (0x00007f500dca0000)
libevent-2.1.so.7 => /lib64/libevent-2.1.so.7 (0x00007f500dc49000)
libsasl2.so.3 => /lib64/libsasl2.so.3 (0x00007f500dc2b000)
libcrypt.so.2 => /lib64/libcrypt.so.2 (0x00007f500dbf1000)
为了方便,我们可以写一个简单的shell脚本来自动化完成这个操作。
如:
ldd /usr/bin/konsole | awk -F=\> '{print $2}' | awk '{print $1}' | xargs
就可以把程序的依赖列表整理成一个复制文件的列表。
Windows环境
在Windows环境下,如果是Cygwin等类Linux环境,仍然可以使用ldd命令。
但如果没有ldd命令,可以使用dumpbin命令。这个命令的/dependents参数,可以输出二进制可执行程序依赖的库文件。
如:
dumpbin /dependents c:\windows\system32\regedt32.exe
就可以得到如下的输出:
Microsoft (R) COFF/PE Dumper Version 14.40.33811.0
Copyright (C) Microsoft Corporation. All rights reserved.
Dump of file c:\Windows\System32\regedt32.exe
File Type: EXECUTABLE IMAGE
Image has the following dependencies:
KERNEL32.dll
msvcrt.dll
SHELL32.dll
api-ms-win-core-shlwapi-legacy-l1-1-0.dll
Summary
……
Qt自带的工具打包
除了以上这种查看二进制依赖的通用方法,Qt的开发工具之中,也提供了部署Qt程序的命令,可以用来自动化地安装部署程序的依赖项。
Qt开发库在Windows下的相应工具名为windeployqt。
我们可以在编译好程序以后,使用windeploqt自动地把程序的所有依赖自动部署。而且,这个命令不仅会部署动态链接库,程序依赖到的其它文件、插件等,也会按照正确的目录,自动进行部署。
执行windeployqt --help可以查看windeployqt支持的参数:
The simplest way to use windeployqt is to add the bin directory of your Qt
installation (e.g. <QT_DIR\bin>) to the PATH variable and then run:
windeployqt <path-to-app-binary>
If your application uses Qt Quick, run:
windeployqt --qmldir <path-to-app-qml-files> <path-to-app-binary>
Options:
-?, -h, --help Displays help on commandline options.
--help-all Displays help, including generic Qt
options.
-v, --version Displays version information.
--dir <directory> Use directory instead of binary
directory.
--qmake <path> Use specified qmake instead of qmake
from PATH. Deprecated, use qtpaths
instead.
--qtpaths <path> Use specified qtpaths.exe instead of
qtpaths.exe from PATH.
--libdir <path> Copy libraries to path.
--plugindir <path> Copy plugins to path.
--translationdir <path> Copy translations to path.
--qml-deploy-dir <path> Copy qml files to path.
--debug Assume debug binaries.
--release Assume release binaries.
--pdb Deploy .pdb files (MSVC).
--force Force updating files.
--dry-run Simulation mode. Behave normally, but
do not copy/update any files.
--no-patchqt Do not patch the Qt6Core library.
--ignore-library-errors Ignore errors when libraries cannot be
found.
--no-plugins Skip plugin deployment.
--include-soft-plugins Include in the deployment all relevant
plugins by taking into account all soft
dependencies.
--skip-plugin-types <plugin types> A comma-separated list of plugin types
that are not deployed
(qmltooling,generic).
--add-plugin-types <plugin types> A comma-separated list of plugin types
that will be added to deployment
(imageformats,iconengines)
--include-plugins <plugins> A comma-separated list of individual
plugins that will be added to
deployment (scene2d,qjpeg)
--exclude-plugins <plugins> A comma-separated list of individual
plugins that will not be deployed
(qsvg,qpdf)
--no-libraries Skip library deployment.
--qmldir <directory> Scan for QML-imports starting from
directory.
--qmlimport <directory> Add the given path to the QML module
search locations.
--no-quick-import Skip deployment of Qt Quick imports.
--translations <languages> A comma-separated list of languages to
deploy (de,fi).
--no-translations Skip deployment of translations.
--no-system-d3d-compiler Skip deployment of the system D3D
compiler.
--no-system-dxc-compiler Skip deployment of the system DXC
(dxcompiler.dll, dxil.dll).
--compiler-runtime Deploy compiler runtime (Desktop
only).
--no-compiler-runtime Do not deploy compiler runtime
(Desktop only).
--json Print to stdout in JSON format.
--no-opengl-sw Do not deploy the software rasterizer
library.
--no-ffmpeg Do not deploy the FFmpeg libraries.
--list <option> Print only the names of the files
copied.
Available options:
source: absolute path of the source
files
target: absolute path of the target
files
relative: paths of the target files,
relative
to the target directory
mapping: outputs the source and the
relative
target, suitable for use
within an
Appx mapping file
--verbose <level> Verbose level (0-2).
Qt libraries can be added by passing their name (-xml) or removed by passing
the name prepended by --no- (--no-xml). Available libraries:
concurrent core core5compat dbus designer designercomponentsInternal
devicediscoverysupportInternal entrypointInternal exampleiconsInternal
fbsupportInternal freetypeInternal gui harfbuzzInternal qthelp jpegInternal
labsanimation labsfolderlistmodel labsqmlmodels labssettings labssharedimage
……
Qt plugins can be included or excluded individually or by type.
To deploy or block plugins individually, use the --include-plugins
and --exclude-plugins options (--include-plugins qjpeg,qsvgicon)
You can also use the --skip-plugin-types or --add-plugin-types to
achieve similar results with entire plugin groups, like imageformats, e.g.
(--add-plugin-types imageformats,iconengines). Exclusion always takes
precedence over inclusion, and types take precedence over specific plugins.
For example, including qjpeg, but skipping imageformats, will NOT deploy qjpeg.
Detected available plugins:
platforms:
qdirect2d
qminimal
Arguments:
[files] Binaries or directory containing the
binary.
这其中,–dir参数,可以指定我们部署的目录路径。
比如,我们在c:\build\hello目录下,使用Qt开发编译出了一个world.exe程序,就可以通过如下命令,把world.exe的运行环境部署到c:\build\hello目录中去。
windeployqt.exe c:\build\hello\world.exe --dir c:\build\hello
使用CMake检测依赖库
如果我们使用的CMake做的构建工具,那么还可以在安装的过程中,使用FILE命令来检测程序的依赖库。
CMake的FILE支持很多参数,其中get_runtime_dependencies参数,是专门用来检测程序的运行依赖的。
需要注意的是,使用这个功能,需要满足两点:
- CMake需要在3.16版本以上。
- 需要使用CMP0087这个Policy的新规则,即需要在CMakeLists.txt里面加上
因为我们构建的是安装脚本,所以把获取动态链接库的这个过程,写在INSTALL命令里面。这样当执行安装的时候,将自动通过FILE的get_runtime_dependencies参数来获取依赖,自动安装。
FILE这个函数的原型为:
file(GET_RUNTIME_DEPENDENCIES [...])
New in version 3.16.
Recursively get the list of libraries depended on by the given files:
file(GET_RUNTIME_DEPENDENCIES
[RESOLVED_DEPENDENCIES_VAR <deps_var>]
[UNRESOLVED_DEPENDENCIES_VAR <unresolved_deps_var>]
[CONFLICTING_DEPENDENCIES_PREFIX <conflicting_deps_prefix>]
[EXECUTABLES <executable_files>...]
[LIBRARIES <library_files>...]
[MODULES <module_files>...]
[DIRECTORIES <directories>...]
[BUNDLE_EXECUTABLE <bundle_executable_file>]
[PRE_INCLUDE_REGEXES <regexes>...]
[PRE_EXCLUDE_REGEXES <regexes>...]
[POST_INCLUDE_REGEXES <regexes>...]
[POST_EXCLUDE_REGEXES <regexes>...]
[POST_INCLUDE_FILES <files>...]
[POST_EXCLUDE_FILES <files>...]
)
其中,
-
RESOLVED_DEPENDENCIES_VAR <deps_var>
是找到的依赖的库的列表。 -
UNRESOLVED_DEPENDENCIES_VAR <unresolved_deps_var>
是没有找到的依赖的库的列表。 -
EXECUTABLES <executable_files>
是可执行文件,在开启了CMP0087之后,可以使用$<TARGET_FILE:target> 来指代目标编译出来的程序路径。 -
LIBRARIES <library_files>
-
DIRECTORIES 是除了系统目录以外,其它的dll查找路径,可以是一个列表。
-
PRE_EXCLUDE_REGEXES 是执行前排除掉的依赖库的匹配规则
-
POST_EXCLUDE_REGEXES 是执行后排除的依赖库的匹配规则。
因为很多Windows自带的dll不用再重复部署,这两个参数可以很方便我们排除掉它们。下面是一个示例框架,简单地演示了构建一个程序,再通过FILE命令来自动安装依赖的过程:
CMAKE_MINIMAL_REQUIRED (VERSION 3.16)
CMAKE_POLICY (SET CMP0087 NEW)
ADD_EXECUTE(helloworld hellworld.c)
INSTALL(TARGET helloworld DESTINATION ".")
INSTALL(CODE [[
LIST(APPEND pre_exclude_regexes "api-ms-.*")
LIST(APPEND pre_exclude_regexes "ext-ms-.*")
LIST(APPEND pre_exclude_regexes "ieshims.dll")
LIST(APPEND pre_exclude_regexes "emclient.dll")
LIST(APPEND pre_exclude_regexes "devicelockhelpers.dll")
LIST(APPEND post_exclude_regexes ".*WINDOWS[\\/]system32.*")
FILE(GET_RUNTIME_DEPENDENCIES
EXECUTABLES $<TARGET_FILE:helloworld>
RESOLVED_DEPENDENCIES_VAR _r_deps
UNRESOLVED_DEPENDENCIES_VAR _u_deps
PRE_EXCLUDE_REGEXES ${pre_exclude_regexes}
POST_EXCLUDE_REGEXES ${post_exclude_regexes}
)
LIST(LENGTH _u_deps _u_length)
IF("${_u_length}" GREATER 0)
MESSAGE(WARNING "Unresolved dependencies detected!")
FOREACH(_no ${_u_deps})
MESSAGE(INFO ${_no})
ENDFOREACH()
ENDIF()
FOREACH(_file ${_r_deps})
MESSAGE("install " ${_file})
FILE(INSTALL FILES "${_file}"
DESTINATION "."
)
ENDFOREACH()
]])