编写工具调用windeployqt+ldd为msys2 Qt应用程序生成完整发布包

概要

在windows下,动态链接库一直是发布Qt程序最为头痛的问题。在msys2环境下,尤其如此。msys2的windeployqt工具无法递归的发布所有依赖库到目标文件夹,导致需要手工的拷贝很多依赖项,非常繁琐。一种讨巧的方法是一股脑拷贝所有dll:

拷贝所有dll到文件夹
运行程序
删除所有dll
保留下来删不掉的就是必须库

但这个方法易漏掉文件。主要原因是有些dll是延迟加载的。比如QtSQL模块postgresql插件,其实依赖着libpq.dll,而 libpq.dll自身也有一堆依赖。只有在主程序里创建了QPSQL类型的连接,才会短期加载这个dll。如果无法在潜在依赖项完全占用的状态下执行删除,则很可能会删除原本有用的文件。

一旦出现上述情况,就只能通过ldd递归的分析并找到所有依赖。这个工作非常考研耐心。本着爱造工具的猿思维,通过本文开发一个Qt工具,在msys2开发模式下,帮助程序员快速生成一个绿色版的完整发布包。

整体架构流程

我们开发一个工具叫做“msys2qtdeployplus”,也就是帮助msys2发布qt的增强工具。它会遵循如下流程,完成发布:

  1. 开发者把待发布的二进制文件拷贝到一个文件夹下,叫做"target_foler"。
  2. 一些复杂项目,可能依赖很多子文件夹下的其他包,这些文件夹定义为“extra_folders”,用分号分割.
  3. 工具首先递归枚举上述两类文件夹下的所有可执行文件、dll,对每个文件以target_foler为目的文件夹,执行
windeployqt --dir target_foler driver:/path/to/file.exe
  1. 工具多次递归枚举上述文件夹内的所有可执行文件、dll,对每个文件执行 ldd,并捕获其输出。
  2. 对每组ldd输出依赖,工具分析那些位于msys2系统环境下的文件,并拷贝到target_foler。
  3. 如果本轮结束后,发生了新的拷贝动作,说明有新的依赖被发布到target_foler。此时,要转到3继续下一轮枚举。
  4. 结束。

经过这样的方法,等于是递归的把所有dll、exe的依赖都找齐了。

技术名词解释

  1. 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原生界面,较为简单:

GUI这个界面上,

  • 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这样的环境。代码里目前只支持这两种,当然想支持更多,只要添加一下判断语句即可。执行效果:
deploy可以看到,在sqldrivers下的所有数据库驱动的依赖项也被发布了。imageformats的各种依赖也都存在了。如果不拷贝完整,可能有的图标格式就显示不出来,还有些SQL数据库就连不上。

总之,通过windeployqt可以拷贝Qt直接依赖的所有DLL、文件夹结构到目标文件夹;使用ldd可以递归拷贝上述所有二进制素材的依赖树,从而完成功能。

完整工程链接

请参考 gitcode.com 或者 gitcode.net.

PS. 啥时候上面两个网站先得挂一个。

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-17 14:26:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-17 14:26:03       101 阅读
  3. 在Django里面运行非项目文件

    2024-06-17 14:26:03       82 阅读
  4. Python语言-面向对象

    2024-06-17 14:26:03       91 阅读

热门阅读

  1. 学习笔记——交通安全分析06

    2024-06-17 14:26:03       29 阅读
  2. PHP框架详解 - symfony框架

    2024-06-17 14:26:03       32 阅读
  3. Web前端三大主流框架介绍

    2024-06-17 14:26:03       25 阅读
  4. Android 放大镜代码

    2024-06-17 14:26:03       38 阅读
  5. ThreadLocal 详讲

    2024-06-17 14:26:03       22 阅读
  6. FileUtils类中常用方法的介绍

    2024-06-17 14:26:03       27 阅读
  7. HIVE及SparkSQL优化经验

    2024-06-17 14:26:03       34 阅读
  8. Docker Desktop Installer For Windows 国内下载地址

    2024-06-17 14:26:03       55 阅读