我们的可执行程序之所以可以运行是因为我们本地的环境可以找到可执行程序所依赖的库,但是如果我们想要发行,让所有的用户都可以安装后直接使用的话就需要进行打包,而打包的方式有很多,这里介绍一下cpack的方式(以下的代码展示并不能满足直接拷贝使用,需要根据你当前本地环境做修改).
最终客户端效果: 在win10下可以直接双击安装,支持桌面快捷方式. Linux下支持在Ubuntu18.04下双击安装同时在Ubuntu22.04上可以直接使用.
环境工具介绍
Windows10: cmake&&cpack 3.27 + conan2.16 + Qt5.15.2(windeployqt) + visual studio2022 + NSIS3.11 +python3.11
Linux(Ubuntu): cmake&&cpack 3.27 + conan2.16.1 + Qt5.15.11(linuxdeployqt) + g++7.5 + dpkg1.19 + python3.11
打包流程
梳理大致思路: 首先在conan中将所需的动态库准备好(查找拷贝), 然后在cmake中设置cpack和NSIS/DEB方法,同时针对于项目中所需的qt文件可以直接使用对应的deployqt工具进行最小限度的自动化查找拷贝.但是deployqt工具并不能很好的拷贝下来你所需的文件,你可以在运行时候的弹窗中锁定缺少的动态库,然后进行conan拷贝操作,这其实就可以避免拷贝不需要的库文件.
conan基本介绍
conna工具可以将我们项目中所需要的库进行管理,可以通过conafile.py的文件中设置def requirements(self):内部设置所需要的库和对应的版本,然后在执行connan install的时候就会自动进行下载,而在在好的库文件会放在文件路径:~/.conan2(与你的conan版本有关)下. 因此我们也可以通过conna提供的方法直接查找管理的库文件所在路径(以此搜集所需的库文件).同时还有一点就是conan中可以直接调用cmake的构建编译操作. conanfile.py中def定义实现的每个函数, 其实在执行conan xxx的相关命令时都会调用对应的方法.如果先要打包的话通常会在conanfile.py文件中声明def package(self): 的方法,此时对应的conna指令就是conan export-pkg . -pr:b=release -pr:h=release -of .build (这里的-of指定的目录与你conan install --output-folder的目录对应), 如果你不写的话默认就是build目录 :
conanfile.py中进行文件拷贝的方式(使用系统库 && conan管理的库):
if self.options.use_system_boost:
boost_pkg = self.conf.get("user.build:system_boost_prefix")
lib_dir = os.path.join(boost_pkg, "lib")
else:
boost_pkg = self.dependencies["boost"].package_folder
if self.settings.os == "Windows":
lib_dir = os.path.join(boost_pkg, "bin")
else:
lib_dir = os.path.join(boost_pkg, "lib")
这里需要注意conna在windows和linux上包管理方式上是有差异的, Windows下的库文件是放在对应路径下的bin目录下,而Linux是放在对应路径的lib目录下的.
cpack基本介绍
cpack是cmake的一个打包工具,但你下载cmake之后其实内部就已经有了cpack了,而你可以通过cpack -G xxx选择你想要的打包生成器,例如你想要生成NSIS的安装包就使用cpack -G NSIS指令,但是还差一个重要的参数选项:指定 CPackConfig.cmake
文件配置.其对应的生成时机就是你在使用cmake进行项目构建的过程,不过你需要在cmake中设置一些cpack的选项:
include(InstallRequiredSystemLibraries) // 编译器运行时所以来的库进行打包
set(CPACK_PACKAGE_NAME "ReviewClient")
set(CPACK_PACKAGE_VENDOR "666")
set(CPACK_PACKAGE_VERSION "${PROJECT_VERSION}")
set(CPACK_PACKAGE_DESCRIPTION_SUMMARY "Review Client Application")
set(CPACK_PACKAGE_INSTALL_DIRECTORY "ReviewClient")
set(CPACK_COMPONENTS_ALL runtime) // 设置cpack打包时包含的组件
include(CPack) //必须在所有install()、CPACK 相关变量之后调用,结束标识
同时必须知道cpack是根据你cmake中install()的内容决定你具体要打包哪些文件的(此时执行完cpack只是会在当前目录下生成_CPack_Packages的目录文件和一个安装包,只有在你双击安装包进行本地安装的时候才会进行相关文件的本地拷贝).所以说你其实可以通过cmake --install进行验证你的安装包文件的目录结构,同时cmake --install也是可以设置组件的,cpack生成安装包是打包拷贝哪些文件其实是通过判断组件是否相等从而进行install的.
install(
DIRECTORY ${CMAKE_BINARY_DIR}/../install/bin/
DESTINATION . # 安装到包的根目录
COMPONENT runtime
)
// 这种写法其实就是因为在conanfile中的 def package(self):将本地所需要的依赖文件已经拷贝到了${CMAKE_BINARY_DIR}/../install/bin/目录下
如上,设置install()中的 component组件为runtime, 与CPACK_COMPONENTS_ALL的值相等,所以也会拷贝进最终的安装包目录下, 同时另一点是可以实现过滤install不相关或重复的文件, 主要是针对项目中其他的submodule中cmake文件中的install()的干扰.同时也可以进行分组拷贝:
set(CPACK_COMPONENTS_ALL runtime devel docs)
还有就是这里的DESTINATION目标目录设置是一个需要格外注意的点, 例如在linux下你设置的最终目录是/xxx的话可能会设计到权限的问题,所以你如果使用cmake --install验证打包的话就会出现没有权限, 需要sudo提权,但是伺候也如果你在conan中有执行copy到这个路径的逻辑的话就会出现权限不够的问题,所以你此时就可以也可以继续提权copy的操作,不过这是最不推荐的做法, 可以直接去掉cmake install的操作,让cpack直接进行打包,而不用执行cmake --installl 因为cmake --install的操作其实就是一个打包前的验证.
windeployqt&&linuxdeployqt
PS C:\Users\aoi\Project\review-client> windeployqt --help-all
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 ICU, ANGLE, etc. are not in the bin directory, they need to be in the PATH
variable. 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 Qt specific options.
-v, --version Displays version information.
--dir <directory> Use directory instead of binary directory.
--libdir <path> Copy libraries to path.
--plugindir <path> Copy plugins 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 Qt5Core library.
--ignore-library-errors Ignore errors when libraries cannot be found.
--no-plugins Skip plugin deployment.
--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.
--compiler-runtime Deploy compiler runtime (Desktop only).
--no-virtualkeyboard Disable deployment of the Virtual Keyboard.
--no-compiler-runtime Do not deploy compiler runtime (Desktop only).
--webkit2 Deployment of WebKit2 (web process).
--no-webkit2 Skip deployment of WebKit2.
--json Print to stdout in JSON format.
--angle Force deployment of ANGLE.
--no-angle Disable deployment of ANGLE.
--no-opengl-sw Do not deploy the software rasterizer library.
--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).
linuxdeployqt (commit ), build <local dev build> built on 2023-11-20 06:51:56 UTC
Usage: linuxdeployqt <app-binary|desktop file> [options]
Options:
-always-overwrite : Copy files even if the target file exists.
-appimage : Create an AppImage (implies -bundle-non-qt-libs).
-bundle-non-qt-libs : Also bundle non-core, non-Qt libraries.
-exclude-libs=<list> : List of libraries which should be excluded,
separated by comma.
-ignore-glob=<glob> : Glob pattern relative to appdir to ignore when
searching for libraries.
-executable=<path> : Let the given executable use the deployed libraries
too
-extra-plugins=<list> : List of extra plugins which should be deployed,
separated by comma.
-no-copy-copyright-files : Skip deployment of copyright files.
-no-plugins : Skip plugin deployment.
-no-strip : Don't run 'strip' on the binaries.
-no-translations : Skip deployment of translations.
-qmake=<path> : The qmake executable to use.
-qmldir=<path> : Scan for QML imports in the given path.
-qmlimport=<path> : Add the given path to QML module search locations.
-show-exclude-libs : Print exclude libraries list.
-verbose=<0-3> : 0 = no output, 1 = error/warning (default),
2 = normal, 3 = debug.
-updateinformation=<update string> : Embed update information STRING; if zsyncmake is installed, generate zsync file
-qtlibinfix=<infix> : Adapt the .so search if your Qt distribution has infix.
-version : Print version statement and exit.linuxdeployqt takes an application as input and makes it
self-contained by copying in the Qt libraries and plugins that
the application uses.By default it deploys the Qt instance that qmake on the $PATH points to.
The '-qmake' option can be used to point to the qmake executable
to be used instead.Plugins related to a Qt library are copied in with the library.
See the "Deploying Applications on Linux" topic in the
documentation for more information about deployment on Linux.
其实对比这两个系统下的deployqt的用法其实就可以看出,其实他们的用法上是有差异的:
1.查找deployqt工具和可执行程序路径
2.查找对应的qml目录文件
3.命令执行
if not self.options.use_system_qt:
if self.settings.os == "Windows":
windeployqt = os.path.join(self.dependencies["qt"].package_folder,
"bin", "windeployqt.exe")
exe_path = os.path.join(install_dir, "bin", "app.exe")
elif self.settings.os == "Linux":
linuxdeployqt = os.path.join(self.dependencies["qt"].package_folder,
"bin", "linuxdeployqt")
exe_path = os.path.join(install_dir, "bin", "app")
else:
if self.settings.os == "Windows":
windeployqt = self.conf.get("user.build:system_qt_prefix") + "/lib/windeployqt"
exe_path = os.path.join(install_dir, "bin", "app.exe")
elif self.settings.os == "Linux":
linuxdeployqt = self.conf.get("user.build:system_qt_prefix") + "/lib/linuxdeployqt"
exe_path = os.path.join(install_dir, "bin", "app")
current_dir = os.path.dirname(os.path.abspath(__file__))
self.output.warning("current_dir: %s" % current_dir)
res_qml = os.path.abspath(os.path.join(current_dir, "res", "qml"))
if self.settings.os == "Windows":
if os.path.exists(windeployqt) and os.path.exists(exe_path):
with chdir(self, os.path.dirname(exe_path)):
command = [
windeployqt,
"--qmldir",
res_qml,
"--release",
exe_path
]
self.run(" ".join(command))
else:
error_str = f"windeployqt ({windeployqt}) 或可执行文件 ({exe_path}) 未找到"
self.output.error(error_str)
raise Exception(error_str)
else:
if os.path.exists(linuxdeployqt) and os.path.exists(exe_path):
with chdir(self, os.path.dirname(exe_path)):
command = [
linuxdeployqt,
exe_path,
"-qmldir=" + res_qml
]
self.run(" ".join(command))
else:
error_str = f"linuxdeployqt ({linuxdeployqt}) 或可执行文件 ({exe_path}) 未找到"
self.output.error(error_str)
raise Exception(error_str)
而且windeployqt个linuxdeployqt命令执行完之后所生成的目录结构也是不同的
windows:
Linux:
可以看出windows下将对应qt所以来的库个插件等内容都拷贝到可执行程序的同级目录下,而linuxdeployqt将这些内容是按照不同的目录结构拷贝的.而且这两个工具的拷贝并不完善,所以还是无法避免针对一些缺失的文件进行手动拷贝.
需要注意的点是:windeployqt由于讲库文件拷贝到可执行程序下了,所以就不需要设置rpath,而linuxdeployqt在调用的过程中同时会成生一个qt.conf的文件:
[Paths]
Prefix = ../
Plugins = plugins
Imports = qml
Qml2Imports = qml
其实这里的prefix其实就相当于是设置了rpath==$ORIGIN/../lib, 所以如果你将其他以来的库文件拷贝到可执行程序的上一级目录下的话其实也是可以不用设置环境变量的.
Windows下的NSIS配置
if(WIN32)
set(CPACK_GENERATOR "NSIS")
set (CPACK_NSIS_MODIFY_PATH "ON")
set(CPACK_NSIS_DISPLAY_NAME "ReviewClient")
set(CPACK_NSIS_PACKAGE_NAME "ReviewClient")
set(CPACK_NSIS_MUI_ICON "${CMAKE_SOURCE_DIR}/res/images/leichen_repair.ico")
set(CPACK_NSIS_MUI_UNIICON "${CMAKE_SOURCE_DIR}/res/images/xxxx.ico")
# 开始菜单快捷方式
set(CPACK_NSIS_MENU_LINKS "app.exe" "ReviewClient")
# 桌面快捷方式
set(CPACK_NSIS_CREATE_DESKTOP_LINKS ON)
set(CPACK_NSIS_CREATE_ICONS_EXTRA "CreateShortCut '$DESKTOP\\\\ReviewClient.lnk' '$INSTDIR\\\\app.exe'")
这里其实就是一些设计图标和快捷方式的设置,可以直接通过NSIS工具生成,不过需要下载NSIS的工具,然后配置好环境变量.
但是对c++ && Qt的程序,需要应用软件图标需要在cmake的add_executable后设置:
target_sources(MyApp PRIVATE ${CMAKE_SOURCE_DIR}/res/appicon.rc)
appicon.rc文件内容:
1 ICON "images/xxx.ico"
这样构建出的 exe 就会携带你定义的 xxx.ico 图标,在 Windows 文件管理器里显示你自己的图标.
Linux下的DEB配置
elseif(UNIX AND NOT APPLE) # 包含Ubuntu
# Ubuntu DEB配置
set(CPACK_GENERATOR "DEB")
# 自动生成包名+分类信息
set(CPACK_DEBIAN_FILE_NAME "${CPACK_PACKAGE_NAME}_${CPACK_PACKAGE_VERSION}_${CMAKE_SYSTEM_PROCESSOR}.deb")
set(CPACK_DEBIAN_PACKAGE_SECTION "utils")
# 设置安装路径(默认/usr)
set(CPACK_SET_DESTDIR ON)
set(CPACK_INSTALL_PREFIX "/opt/xxx/reviewclient")
# 快捷方式(先通过.in的文件生成.desktop的文件)
set(EXEC_PATH "${CPACK_INSTALL_PREFIX}/bin/app")
configure_file(
"${CMAKE_SOURCE_DIR}/config/ReviewClient.desktop.in"
"${CMAKE_BINARY_DIR}/ReviewClient.desktop"
@ONLY
)
install(
FILES "${CMAKE_BINARY_DIR}/ReviewClient.desktop"
DESTINATION "/usr/share/applications"
PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ
)
# 安装多种尺寸的图标
foreach(size 16 22 24 32 36 48 64 72 128 256)
install(
FILES "${CMAKE_SOURCE_DIR}/res/images/image_${size}x${size}.png"
DESTINATION "/usr/share/icons/hicolor/${size}x${size}/apps"
RENAME "aaa.png"
PERMISSIONS OWNER_READ OWNER_WRITE GROUP_READ WORLD_READ
)
endforeach()
endif()
因为Linux下的快捷方式其实就是一个.desktop 的文件, 所以需要先编写.in的文件:
[Desktop Entry]
Version=1.0
Type=Application
Name=ReviewClient
Comment=Review Client Application
Exec=@EXEC_PATH@
Icon=leichen_repair
Terminal=false
Categories=Utility;
StartupWMClass=app //
这文件中的Exec变量其实可以直接通过cmake中的
set(EXEC_PATH "${CPACK_INSTALL_PREFIX}/bin/app")
直接设置. 同时这个.in文件的关键点就是StartupWMClass=app //这个值的设置,这个的含义:指定窗口的 WM_CLASS 属性,用于将桌面启动器菜单的图标与实际打开的程序窗口正确关联。通俗点就是程序安装运行之后的任务栏图标显示. 而这个值需要先在命令行中输入: xprop | grep WM_CLASS命令,然后运行我们的程序,同时点解程序界面的任意位置,然后命令行就会输出类似如下的内容:WM_CLASS(STRING) = "app", "App" 分别是对应的资源名称和资源类. 所以将StartupWMClass设置成资源名称就行了.
libssl动态库变更问题
其实在进行Linux下打包的过程中遇到的一个棘手的问题其实就是在Ubuntu18.04和Ubuntu22.04版本下的libssl库其实是不兼容的. 因为Ubuntu22.04下默认是用的libssl3.x的版本,而Ubuntu18.04用的确实1.x的版本,但是高版本下的libssl库并不是很好的兼容低版本,存在接口变更的问题:
这其实就是在Ubuntu18.04下打包好后,用Ubuntu22.04进行安装后,运行时出现的问题,其实也就是因为我们的项目用的是libssl1.x的版本,而libssl3.x中是没有SSL_get_peer_certificate这个接口导致的链接错误.
其实这里起初的解决方式的直接将项目的libssl库在conanfile文件中设置成静态的,但是子模块中用到了libssl是没有进行特殊设置的,所以默认情况下是使用的动态链接,所以这即使在主目录的conanfile文件里面强制设置成静态的也是没有意义的.所以如果要使用静态链接的方式修改的话其实工作量是比较大的,毕竟你的子模块大概率是开源获取的.
所以最终还是选择用动态链接的方式进行, 因为可执行程序在Ubuntu22.04中动态链接的libssl库的版本是3.x的, 其中缺少1.x的符号接口,所以打包的时候将对应1.x的动态库拷贝进去,那么在链接的时候根据接口查找的情况也会动态加载libssl1.x的版本的. 所以最后在ldd查询链接库的时候其实就可以看出是链接了libssl的两个版本的动态库文件的.所以这种用法其实是根据你个人选择的,同时链接两个版本的同一个库肯定是不好的,但是是否可以正常运行需要直接实践得知,所以这个需要自行进行斟酌选择.