最近在发布Qt应用时遇到了一些困难,Windows还好,在Linux上面发布遇到了不少的麻烦(实际Linux应该简单才对),经过在网络搜索发现帖子不少,但都比较片面,现把Qt应用程序在Linux&Windows打包部署总结如下。
核心
应用部署的核心是加载库,一个Qt应用程序至少包含以下库:
Windows
Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll
Linux
libQt5Core.so.5、libQt5Gui.so、libQt5Widgets.so
与其他应用程序一样,Qt应用程序也依赖于操作系统来加载这些库文件,这意味着它们必须放置在操作系统可以找到它们的位置。在Windows上,这意味着在.exe文件同一目录中或搜索路径中指定的目录(例如C:\ Windows \ System32)。将DLL复制到未安装Qt的另一台PC上时,我们通常不想弄乱其他计算机上的搜索路径,而是将DLL与.exe文件放在一起,这可能是最好的解决方案。
在Linux上,将.so文件放在可执行文件同一目录中不会像Windows中那样自动加载它们。但大多数Linux系统都预先安装好了Qt,例如在Ubuntu上,通常在/ usr / lib / i686-linux-gnu或/ usr / lib / x86_64-linux-gnu中安装了Qt,因此我们的应用仍然可以运行(但是请注意版本问题,Ubuntu提供的Qt可能太旧了,我们的应用无法启动)。
还有一点在部署Qt应用比较容易出错的是Qt的插件机制,除了上面提到的几个Qt核心库之外,程序想要运行还必须提供一些插件,例如:platforms、sqldrivers、imageformats,这些是插件目录,名字是Qt定的(当然可以修改,但是不建议修改)。其中最重要的是platforms插件目录,里面提供一些平台插件,所有Qt应用程序都需要这些库,对于Windows默认的是qwindows.dll,对于Linux,则为libqxcb.so。这是Qt的QPA(Qt Platform Abstraction层)它负责许多特定于OS的事情,例如将调用转换setWindowState(Qt::WindowMaximized)为Windows / Linux特定的系统调用。
插件设置
上面提到了一些Qt插件,这些插件Qt是如何加载的呢?默认Qt会在可执行文件所在的目录查找并加载这些插件。但是为了我们的程序目录更加简洁,可以自定义插件目录。
设置环境变量QT_PLUGIN_PATH
:
使用环境变量可能是比较容易的选择,例如:
export QT_PLUGIN_PATH=plugins
Windows使用
set
设置上面环境变量,QT将在plugins\platforms
寻找qwindows.dll
。当然,也可以通过这种方式设置绝对路径。而且,如果想获得大量的插件目录,则可以附加以;
分隔的其他路径。(在Linux中是:
)。
在代码中指定插件路径:
#include "mainwindow.h"
#include <QApplication>
int main(int argc, char *argv[])
{
QApplication::addLibraryPath("plugins");//设置插件目录
QApplication a(argc, argv);
MainWindow w;
w.show();
return a.exec();
}
可以在代码中指定插件路径,但是由于Qt在构造QApplication时会加载插件,因此设置此项的机会就是在调用QApplication的构造函数之前进行。上述代码和设置环境变量效果一样。
创建一个qt.conf
文件:
这是一种设置插件路径比较的流行方法。
[Paths]
Plugins=plugins
此文件与应用可执行文件位于同一目录中,Qt将读取该文件并将plugins= path
路径添加到其插件目录列表中。上面的例子实现效果和之前的一样。
请注意,如果您使用的是Windows:该
Plugins=
设置中的反斜杠不起作用,应该使用Linux风格的正斜杠。
插件问题排查:
最后,如果仍然遇到插件加载问题,可以通过将环境变量QT_DEBUG_PLUGINS
设置为非零值来打开插件加载过程的log
export QT_DEBUG_PLUGINS=1
然后从Terminal/CMD窗口启动应用程序。对于Linux,在终端中会看到类似QFactoryLoader :: QFactoryLoader()checking...
这样的行,但是在Windows Qt中,输出会将输出路由到OutputDebugString()API,因此CMD窗口中将不会显示任何内容。使用Visual Studio或Qt Creator可以看到输出,如果在非开发PC上遇到插件问题,这没有太大帮助。另一种选择是下载一个实用程序,并在启动应用程序之前将其打开。
Windows部署
首先使用Release模式构建应用得到可执行文件myapp.exe
。使用windeployqt.exe
工具可以自动拷贝Qt库和插件到应用程序目录,但是这个工具会拷贝多于程序需要的好多东西。通常都需要手动删除一些程序不需要的库。
Windows部署Qt应用程序通常至少包含以下文件:
platforms目录内包含qwindows.dll
,这样简单的应用程序是可以运行的,并且可以打包分发。
这是一个最基础的应用,通常我们的应用会更复杂,比如需要插件sqldrivers、imageformats、translations。比如:
所有插件都放在一个目录会显得很乱,现在我们创建一个qt.conf
,内容如下:
[Paths]
Plugins=plugins
然后我们新建目录plugins,把所有插件移动到plugins。使我们的应用程序目录变得的更加简洁:
注意:plugins是Qt默认就会查找的目录,所以没有qt.conf也无所谓,但是如果想要指定一个特殊的插件目录名称,则必须使用qt.conf,如:
third
。
Linux部署
几乎所有的Linux发行版都会预装Qt,所以如果我们的应用是为特定发行版编译的,我们几乎是不需要进行任何配置,直接打包分发可执行文件即可(如果你是这种情况,就不需要看下面内容了)。
但是如果目标Linux没有安装Qt或者版本比较老,那我们的应用程序就可能不会运行了。这也是我遇到的问题,我的应用使用Qt5.12.5
开发并编译,但目前非常主流的Ubuntu 18.04 lts
最高只可使用Qt5.9.5
。
那就没办法了么?是不是可以升级Linux系统的Qt安装呢?网上有相关文章,需要破坏系统基础配置,对于只想运行一个应用程序,就需要破坏系统环境配置,这是得不偿失的。能不能像Windows那样拷贝Qt5.12.5
的动态库到其他安装了低版本的Qt,甚至没有安装Qt的Linux上面运行呢,答案是肯定的。下面内容就是说明如何完全体部署Qt应用到Linux。
修改连接库(不推荐)
就是把系统Qt库链接到新版本的Qt库,网上有类似方法,这种方式非常不推荐,原因是Linux发行版在开发时对所有库进行了预设,强行修改可能会造成一个应用程序好用了,十个应用程序崩溃了的情况。
带动态库一起打包发布(推荐)
首先同样使用Release模式构建应用,得到可执行文件myapp
。GitHub上面有一个linuxdeployqt工具可以帮助构建.AppImage
格式的Linux可执行程序,但是它的宗旨是构建可在所有平台运行的Qt程序,因此就会有同样的问题,如果我们编译环境过于新,也就无法使用这个工具了。
自己动手,丰衣足食
网上好多文章使用ldd
命令来查看我们的应用依赖了哪些库,并且还有脚本可以自动复制这些库到应用目录,这通常是不可行的。因为ldd
列表出来的大部分库并不是我们需要的,有些甚至是我们构建系统独有的库,并且我们并不能很好的确定该删除哪些库,还有就是ldd
只能检查我们的应用程序直接依赖的哪些库,并不能查出依赖库的间接依赖,这些只有Qt自己知道,比如:libqxcb.so依赖libQt5DBus.so.5。
链接库
把.so
文件放在可执行文件旁边并不会像Windows加载DLL那样自动被系统加载,Linux有几个可以配置加载动态库的方法,我这里使用设置环境变量LD_LIBRARY_PATH
的方法,其他方法通常都需要root
权限,并且破坏了系统原有运行模式,所以对于部署一个独立运行的应用程序使用LD_LIBRARY_PATH
是最好的方法。
以下是我的应用程序在Linux下的目录,这是一个最基础应用程序的目录结构,你几乎不能删除其中任何文件。
./bin 可执行文件目录
./bin/qt.conf qt特殊配置文件
./bin/myapp 可执行文件
./plugins 插件目录
./plugins/platforms
./plugins/platforms/libqxcb.so
./lib 动态库加载目录
./lib/libQt5Gui.so.5
./lib/libQt5Widgets.so.5
./lib/libQt5XcbQpa.so.5
./lib/libQt5DBus.so.5
./lib/libicudata.so.56
./lib/libicui18n.so.56
./lib/libQt5Core.so.5
./lib/libicuuc.so.56
./myapp.sh 应用启动脚本
启动脚本
因为我们的程序包含了所需要的库,所以需要让操作系统来加载我们自己的库,基于此我们需要使用脚本的方式启动我们的应用程序,来代替直接运行可执行文件。使用LD_LIBRARY_PATH
来指定动态库加载目录,这里需要指定到了lib
目录。
myapp.sh
内容如下:
#!/bin/sh
appname=`basename $0 | sed s,\.sh$,,`
dirname=`dirname $0`
tmp="${dirname#?}"
if [ "${dirname%$tmp}" != "/" ]; then
dirname=$PWD/$dirname
fi
LD_LIBRARY_PATH=$dirname/lib
export LD_LIBRARY_PATH
$dirname/bin/$appname "$@"
如果你是用以上脚本来启动应用程序,只需要修改脚本名称即可。
bin目录
bin
目录存放可执行文件和qt.conf
qt.conf
内容如下:
[Paths]
Prefix = ../
Plugins = plugins
Prefix
指定程序工作目录,相对于可执行文件路径。默认为.
当前目录,这里我为了是目录更加整洁,指定为..
上级目录。
Plugins
指定插件加载目录,相对于Prefix目录。上面这种指定方式,Qt就会在我的plugins
目录下寻找插件了,例如plugins/platforms/libqxcb.so
。
打包发布
以上目录结构和各种依赖即是一个Qt发布Linux环境的最小集了,可以使用以上目录针对各种不同Linux发行版进行构建打包,或者直接压缩进行分发。这样应用就可以在装有低版本Qt甚至没有安装Qt的环境上面运行了。
你还可以编写自己的
.desktop
文件使用myapp.sh
作为应用启动命令。