文章目录
- 概要
- 整体架构流程
- 技术名词解释
- 技术细节
- 1. 界面设计
- 2. 递归枚举文件
- 3. 运行windeployqt
- 4. 运行ldd并拷贝文件
- 5. 驱动流程
- 小结
- 完整工程链接
概要
在windows下,动态链接库一直是发布Qt程序最为头痛的问题。在msys2环境下,尤其如此。msys2的windeployqt工具无法递归的发布所有依赖库到目标文件夹,导致需要手工的拷贝很多依赖项,非常繁琐。一种讨巧的方法是一股脑拷贝所有dll:
但这个方法易漏掉文件。主要原因是有些dll是延迟加载的。比如QtSQL模块postgresql插件,其实依赖着libpq.dll,而 libpq.dll自身也有一堆依赖。只有在主程序里创建了QPSQL类型的连接,才会短期加载这个dll。如果无法在潜在依赖项完全占用的状态下执行删除,则很可能会删除原本有用的文件。
一旦出现上述情况,就只能通过ldd递归的分析并找到所有依赖。这个工作非常考研耐心。本着爱造工具的猿思维,通过本文开发一个Qt工具,在msys2开发模式下,帮助程序员快速生成一个绿色版的完整发布包。
整体架构流程
我们开发一个工具叫做“msys2qtdeployplus”,也就是帮助msys2发布qt的增强工具。它会遵循如下流程,完成发布:
- 开发者把待发布的二进制文件拷贝到一个文件夹下,叫做"target_foler"。
- 一些复杂项目,可能依赖很多子文件夹下的其他包,这些文件夹定义为“extra_folders”,用分号分割.
- 工具首先递归枚举上述两类文件夹下的所有可执行文件、dll,对每个文件以target_foler为目的文件夹,执行
windeployqt --dir target_foler driver:/path/to/file.exe
- 工具多次递归枚举上述文件夹内的所有可执行文件、dll,对每个文件执行 ldd,并捕获其输出。
- 对每组ldd输出依赖,工具分析那些位于msys2系统环境下的文件,并拷贝到target_foler。
- 如果本轮结束后,发生了新的拷贝动作,说明有新的依赖被发布到target_foler。此时,要转到3继续下一轮枚举。
- 结束。
经过这样的方法,等于是递归的把所有dll、exe的依赖都找齐了。
技术名词解释
- msys2:是一个独立的软件包管理系统,它提供了一个类似于Linux的shell环境和丰富的软件包库。Qt则是一个跨平台的C++图形用户界面应用程序开发框架,广泛用于开发GUI程序和开发工具。下面将详细介绍使用msys2环境搭配Qt的优势:
(1) 软件包管理pacman工具:msys2提供了pacman命令行工具,可以方便地安装、升级和管理软件包。该工具是滚动更新,由一系列强大的自动化编译机器人维护,始终向Git最新社区进度看齐。通过pacman可以轻松安装Qt及其IDE Qt Creator,以及其他开发所需工具。不但如此,大量Linux下的GNU软件库都能直接调用,显著强化了windows下的编程体验。
(2) 优化的性能表现:使用MinGW-w64编译器,可以在Windows平台上获得接近原生的性能。
(3) Qt5支持静态链接库:msys2支持安装Qt的静态库版本,这对于创建不需要额外依赖的独立可执行文件非常有用。(Qt6暂时缺少完善的静态支持)
(4) 跨平台一致性开发体验:在Windows上模拟类Unix环境,使得开发者在本地就能享受到接近目标Unix平台的开发体验。
技术细节
1. 界面设计
界面采用Qt原生界面,较为简单:
这个界面上,
- TargetFolder是发布的文件夹
- Extra Folders是存放相关其他二进制依赖的文件夹
- MSYS2指定本地msys2的安装文件夹。
- PATH的两个控件
– 第一个用于微调一些第三方依赖,以绕过msys2(如使用了第三方的libfftw)。这些路径会追加到PATH的最前边。
– 第二个用于为某些二进制提供完整的依赖位置,比如有些dll没有第三方库,ldd会崩溃。
点击:run开始执行,执行的进度在左侧,外部程序的输出在右侧显示。
2. 递归枚举文件
采用一个简单的递归枚举函数枚举所有文件夹下的exe\dll
void DlgQtDeplus::enumAllExes(QString folder,QFileInfoList * pLst)
{
QDir dir_target(folder);
QStringList lstExecTypes;
lstExecTypes << "*.dll";
lstExecTypes << "*.exe";
QFileInfoList lstExec = dir_target.entryInfoList(lstExecTypes);
pLst->append(lstExec);
lstExecTypes.clear();
lstExecTypes<<"*";
lstExecTypes<<"*.*";
lstExec = dir_target.entryInfoList(lstExecTypes);
foreach(QFileInfo info, lstExec)
{
if (info.isDir())
{
if (!info.fileName().startsWith("."))
{
enumAllExes(info.absoluteFilePath(),pLst);
}
}
}
}
3. 运行windeployqt
使用QProcess可以方便的运行windeployqt
int DlgQtDeplus::run_deployqt()
{
//Enum all exe in target folder
QFileInfoList lstExec;
enumAllExes(ui->lineEdit_targetFolder->text(),&lstExec);
foreach(QFileInfo info, lstExec)
{
QProcess * call_process = new QProcess(this);
call_process->setProgram("windeployqt.exe");
QStringList args;
args<<"--dir";
args<<ui->lineEdit_targetFolder->text();
args<<info.absoluteFilePath();
call_process->setArguments(args);
call_process->start();
call_process->waitForStarted();
call_process->waitForFinished();
call_process->deleteLater();
}
return 0;
}
4. 运行ldd并拷贝文件
通过分析ldd的输出,可以拷贝msys2的文件到target_folder, 并返回本轮成功拷贝的文件个数。
int DlgQtDeplus::run_ldd()
{
static QRegularExpression exp("[\\ \\n\\r\\=\\>)()]");
QFileInfoList lstExec;
enumAllExes(ui->lineEdit_targetFolder->text(),&lstExec);
int cp = 0;
QFileInfo infod(ui->lineEdit_targetFolder->text());
QString pathM2 = ui->lineEdit_msys2->text();
foreach(QFileInfo info, lstExec)
{
ui->progressBar_bar->setValue(c*1000/lstExec.size());
QProcess * call_process = new QProcess(this);
call_process->setProgram("ldd.exe");
QStringList args;
args<<info.absoluteFilePath();
call_process->setArguments(args);
call_process->start();
call_process->waitForStarted();
call_process->waitForFinished();
if (call_process->bytesAvailable())
{
QString str = QString::fromUtf8(call_process->readAllStandardOutput());
QStringList lstDeps = str.split(exp);
foreach(QString dep, lstDeps)
{
if (dep.startsWith("/ucrt64/")||dep.startsWith("/msys64/"))
{
QFileInfo info(pathM2+dep.trimmed());
QString tar(infod.absoluteFilePath()+"/"+info.fileName());
QFile file(pathM2+dep.trimmed());
if (file.copy(tar))
++cp;
}
}
}
call_process->deleteLater();
}
return cp;
}
5. 驱动流程
在按钮响应函数中,调用上述两个过程,完成功能实现。
void DlgQtDeplus::on_pushButton_run_clicked()
{
run_deployqt();
while ((!stopcmd) &&run_ldd())
{
QCoreApplication::processEvents();
}
}
小结
这个工具需要在与待发布的可执行文件相一致的QtCreator环境里执行。 一致的执行环境是指类似msys64,ucrt64这样的环境。代码里目前只支持这两种,当然想支持更多,只要添加一下判断语句即可。执行效果:
可以看到,在sqldrivers下的所有数据库驱动的依赖项也被发布了。imageformats的各种依赖也都存在了。如果不拷贝完整,可能有的图标格式就显示不出来,还有些SQL数据库就连不上。
总之,通过windeployqt可以拷贝Qt直接依赖的所有DLL、文件夹结构到目标文件夹;使用ldd可以递归拷贝上述所有二进制素材的依赖树,从而完成功能。
完整工程链接
请参考 gitcode.com 或者 gitcode.net.
PS. 啥时候上面两个网站先得挂一个。