1. 构建Qt安装程序
1.1. Qt应用程序结构
还有一些说明文档。如:ReadMe、License等
在 Windows 中,要解决可执行文件过大的问题,可以使用像 UPX 这样的加壳工具。在对可执行文件压缩之后,会在启动时将其解压缩到 RAM 中。
而在 Linux 中,可以通过 strip 命令来去除目标文件中的调试信息、符号信息,以减小程序的大小。但要注意一点,strip 只能用于可执行文件和动态库(.so),不能用于静态库(.a)。
1.2. 不同操作系统常用的打包工具
1.2.1. 多平台 GUI 安装程序 跨平台安装工具
-
Qt Installer Framework https://doc.qt.io/qtinstallerframework/ 简称 Qt IFW,由 Qt 官方提供,以前仅用于 Qt 本身,但现在已经发布了,用于创建通用的安装程序。 -
InstallBuilder http://installbuilder.bitrock.com/ 是一个功能强大、易于使用的跨平台安装程序创建工具。它一直都在积极维护,但仅供商业使用。 -
InstallJammer https://sourceforge.net/projects/installjammer/ 是一个跨平台 GUI 安装程序和生成器,虽然是开源的,但 2011 年之后就不再维护了,比较遗憾!
1.2.2. windows
-
NSIS https://nsis.sourceforge.io/Main_Page 如果你喜欢开源软件,则一定知道 NSIS(全称:Nullsoft Scriptable Install System)。它是一个专业的工具,可用于创建从非常简单到非常复杂的安装程序。虽然它很小,但功能却很丰富,非常适合 Internet 分发。 正如其名,NSIS 是基于脚本的,它能够让我们创建处理任何情况所需的复杂逻辑。幸运的是,它包含了许多插件和预定义脚本,以帮助初学者入门。 -
Inno Setup http://www.jrsoftware.org/isinfo.php 是一个免费的安装制作软件,小巧、简便、精美是其最大特点,支持 pascal 脚本,能快速制作出标准 Windows 2000 风格的安装界面,足以完成一般安装任务。 该软件用 Delphi 写成,其官网同时也提供源程序免费下载。它虽不能与 Installshield 这类“恐龙级”的安装制作软件相比,但也算是当之无愧的后起之秀。 -
Advanced Installer https://www.advancedinstaller.com/ 有一个免费版本,但也有其他几个版本,这些版本的价格取决于安装程序的复杂程度。如果你正在寻找更专业的东西,其中还包括一些支持选项,那么 Advanced Installer 是一个不错的选择。 用它创建 MSI 文件包非常方便,只需添加文件、修改名称、添加按钮即可,无需任何脚本方面的知识,并且生成的安装文件保证符合 Windows 最佳操作建议。 -
InstallShield https://www.flexerasoftware.com/install/products/installshield.html 是一款“恐龙级”的安装包制作工具,是 Flexera Software 的当家产品。这不仅是因为它拥有 20 多年的研发历史,而且它也是全球著名软件公司的“皇家御用”打包软件,比如 Adobe、Corel、Autodesk 等公司。 然而这款软件过于专业,并不像 NSIS、Inno Setup 等那样容易入门,所以想学习必须下很大功夫。这也是全球领先的 Windows 安装开发解决方案,现在已经成为 Windows Installer 和 InstallScript 安装方面的行业标准。 -
WIX Toolset https://wixtoolset.org/ 是一个免费的打包工具,通常要与 Visual Studio(2012 或更高版本)一起使用。之所以最后提到它,是因为它需要经过大量的学习。虽然可用它创建一些非常复杂的安装程序,但要编写大量的代码并经常使用命令行。
1.2.3. Linux
对于 Linux 系统来说,要查看可执行文件依赖的库以及缺少的函数符号,可以使用 ldd -r 指令。
-
构建源码包 对于开源项目来说,这是最简单的方法。 但一定要记得,在使用 tar 命令对目录树进行压缩之前,需要先运行 make distclean 来清理构建环境。 -
创建本地分发包 这要考虑系统的发行版,而主流的包管理系统有两个:rpm 和 deb。rpm 格式起源于 RedHat,但也被其他发行版使用,例如:CentOS、SuSE 和 Fedora。deb 格式是由 Debian 项目开发的,适用于 Debian/Ubuntu 及其衍生版。 rpm 可参考 RPM HOWTO(http://www.tldp.org/HOWTO/RPM-HOWTO/),尤其是关于构建的部分 - Building RPMs。 deb 可参考Debian 新维护者手册(https://www.debian.org/doc/manuals/maint-guide/)。 -
创建独立的应用程序包 若要将 Qt 程序作为一个独立的包部署到 Linux 中,需要将其以及所需的组件捆绑在一起,像 Qt 库、Qt 插件(尤其是 platforms 插件)。 推荐一个 Linux 部署工具 - linuxdeployqt(https://github.com/probonopd/linuxdeployqt),它能够自动执行上述的流程,并提供 AppImage(https://appimage.org/)。
Tips:和 Windows 不同的是,Linux 下可执行文件及其依赖库放在同一目录一般是无法正常运行的。常用方式是写一个 Shell 脚本,并用 LD_LIBRARY_PATH 指定依赖库所在目录,然后通过运行这个脚本来间接地启动程序。
1.3. 静态库与动态库的区别
2. 程序打包过程
@startuml
:1.构建realease版本应用程序;
:2.用打包工具添加依赖项;
:3.添加其他帮助文档到目录结构;
:4.测试程序打包是否成功;
@enduml
过程说明: 过程2中. 常用的打包工具
- windeployqt:由 Qt 官方提供,旨在自动化创建可部署文件夹的过程,该文件夹包含了应用程序所需的 Qt 相关依赖项(库、插件和翻译等)。
- Dependency Walker:用于查找程序所需的依赖库,类似的工具还有 Process Explore。
Tips: windeployqt 并没有考虑程序依赖的第三方库(例如:OpenCV)。此外,如果使用了 MSVC 编译器,它也不会将相应的 C/C++ 运行时库拷贝进去。这时,需用Dependency Walker检查程序所链接的库文件。
对于特定的编译器,其依赖的库文件如下:
VC++ 14.0 (2015) | MinGW |
---|
C 运行时:vccorlib140.dll | libwinpthread-1.dll | vcruntime140.dll | libgcc_s_dw2-1.dll | C++ 运行时:msvcp140.dll | libstdc+±6.dll |
3. QtIFW 安装配置
Qt Installer Framework 简称 Qt IFW,是由 Qt 官方提供的安装程序制作框架。 Qt IFW 下载页
3.1. 下载安装对应版本,选择安装路径
bin:提供了一些基本的工具,比如打包要用的 binarycreator。 doc:包含了相应的帮助文档,有助于更好的掌握 Qt IFW。 examples:有各种各样的示例,方便我们学习研究。 Licenses:许可协议。
3.2. 配置环境变量
3.3. 在QtCreatoer添加帮助文档
3.4. 构建examples.pro可查看各个例子安装效果
4. QtIFW 创建安装程序
Qt IFW 创建安装程序有一定的步骤:
4.1. 创建一个新的文件夹,里面必须包含两个文件夹config/和packages/
4.2. config 创建配置文件config.xml
Name: 被添加到页面和简介文本中的应用程序名称 Version: 程序版本号 Title: 标题栏上的安装程序名称 Publisher: 软件的发布者(如 Windows 控制面板中所示) StartMenuDir: Windows 开始菜单中产品的默认程序组名称 TargetDir: 程序安装的目标路径
其他参数可查看:https://doc.qt.io/qtinstallerframework/ifw-globalconfig.html
4.3. 创建包的配置信息
QtIFW框架中安装程序只需要处理一个组件,提供安装程序时安装组件可选,如:com.qtobject.ifw。 在 com.qtobject.ifw/meta 中配置package.xml。
DisplayName: 组件名称 Description: 组件描述信息 Version: 组件版本号 ReleaseDate: 组件发布日期 Default: 如果在安装程序中预先选择了组件,则为 true。 Script: js文件名,如本例installscriqt.qs,用于在加载时执行一些安装操作。
其他参数可查看: https://doc.qt.io/qtinstallerframework/ifw-component-description.html#package-information-file-syntax
安装组件效果如下:
4.4. 打包软件生成安装程序
-
在com.qtobject.ifw/data中放入需打包的文件(.exe、.dll 等)。 -
要生成安装程序,需要借助 QtIFW 提供的 binarycreator.exe 参考链接 -
打开 CMD或 PowerShell,并进入包目录 ,然后输入 binarycreator -c config\config.xml -p packages MySoftwareInstaller.exe -v
QtIFW 在生成安装程序时,会用自带的 archivegen 工具将这些文件压缩成 7zip 格式;然后在安装时,再从压缩包中将它们提取出来。 好处:保证程序的安全性,压缩程序的大小。
4.5. 测试安装
最好使用一台没有qt开发环境的电脑测试安装程序。除了在windows系统上运行,有条件的话也可以尝试在不同的系统中生成安装程序,测试运行。
5. QtIFW 创建在线安装程序
上一节简单介绍QtIFW离线安装程序,接下来介绍QtIFW如何在线安装程序。主要区别是在于存储库的创建和配置过程。
5.1. 配置存储库
- 在config.xml配置存储库信息。这些信息由RemoteRepositories元素指定,它可以包含若干个Repository子元素。Repository每一个都包含以下信息:
Url: 存储库的地址,指向列出可用组件列表的 Updates.xml 文件。如上图:本地Url也可以,注意格式前面要加file:/// Enabled: 若为 0,则禁用存储库。若为 1,则启用存储库。 Username: 对于需要身份验证的存储库,用来表示用户名。 Password: 对于需要身份验证的存储库,用来表示密码。 DisplayName: 设置要显示的字符串,而非 Url。
Tips:密码是以纯文本形式保存的,因此不建议在这里输入。此处如果未设置身份认证信息,将会在运行时通过对话框获取,用户可以在运行时处理这些设置。
- 生成安装程序
使用binarycreator -c config\config.xml -p packages MySoftwareInstaller.exe -v 命令生成安装程序
5.2. 创建在线存储库
- 在package.xml中修改信息,注意版本号一定要增加
版本号从1.0.0增加到2.0.0
- 利用 repogen 工具命令repogen -p packages repository,将包转换为安装程序能在运行时获取的文件结构。此时会生成一个名叫repository 的存储库。里面包含组件包的完整副本和Updates.xml的一些额外生成的元数据(如:SHA安全校验码)
5.3. 查看在线安装程序
-
在安装程序的根目录选中maintenancetool.exe(Qt 中的维护工具,用于添加/更新/删除组件)可以在线更新组件,如下图可以看见组件版本号从1.0.0增加到2.0.0,选中即可更新版本。 -
在设置中的资料档案库可以看见之前配置存储库的用户和密码,选中该存储库可以用下方的按钮“条件测试”检测存储库是否可用。
6. QtIFW 覆盖安装
QtIFW不支持离线升级,程序新版本目录不能直接覆盖旧版本目录安装,要先卸载。QtIFW 提供覆盖安装的方法简化这一流程。
6.1. 自动卸载旧版本
- 编写卸载脚本。参考链接
function Controller()
{
gui.clickButton(buttons.NextButton);
gui.clickButton(buttons.NextButton);
installer.uninstallationFinished.connect(this, this.uninstallationFinished);
}
Controller.prototype.uninstallationFinished = function()
{
gui.clickButton(buttons.NextButton);
}
Controller.prototype.FinishedPageCallback = function()
{
gui.clickButton(buttons.FinishButton);
}
然后用之前的安装程序测试;执行 maintenancetool.exe --script=uninstallscript.qs 命令卸载旧版本。
- 将卸载脚本uninstallscript.qs放入 data 目录中(例如:data/script/uninstallscript.qs),最终由 Qt IFW 打包进安装程序。当需要进行覆盖安装时,maintenancetool 工具就可以很容易的找到它。
6.2. 覆盖安装
- 既然是覆盖安装,必然少不了对安装位置的检测,一旦发现程序已安装,往往需要加一些友好性的提示信息(例如:显示的“检测到程序已安装,继续将会被覆盖。”)。
要完成这一步,则需要为安装程序添加自定义 UI。首先,要在 meta 目录下添加一个 targetwidget.ui 界面文件(QtDesigner可以编写ui),然后,还需要在 package.xml 文件中用 UserInterfaces元素标记它:
<?xml version="1.0" encoding="UTF-8"?>
<Package>
<Name>MySoftware</Name>
<Version>1.0.0</Version>
<Title>MySoftware Installer</Title>
<Publisher>Twy</Publisher>
<StartMenuDir>MySoftware</StartMenuDir>
<TargetDir>@HomeDir@/MySoftware</TargetDir>
<UserInterfaces>
<UserInterface>targetwidget.ui</UserInterface>
</UserInterfaces>
</Package>
- 修改安装脚本
将交互部分添加到安装脚本 installscript.qs 中。注意程序各文件的路径。
var targetDirectoryPage = null;
function Component()
{
installer.gainAdminRights();
component.loaded.connect(this, this.installerLoaded);
}
var Dir = new function () {
this.toNativeSparator = function (path) {
if (installer.value("os") == "win")
return path.replace(/\//g, '\\');
return path;
}
};
Component.prototype.createOperations = function()
{
component.createOperations();
component.addOperation("CreateShortcut",
"@TargetDir@/bin/MySoftware.exe",
"@DesktopDir@/MySoftware.lnk",
"workingDirectory=@TargetDir@");
component.addOperation("CreateShortcut",
"@TargetDir@/bin/MySoftware.exe",
"@StartMenuDir@/MySoftware.lnk",
"workingDirectory=@TargetDir@");
}
Component.prototype.installerLoaded = function()
{
installer.setDefaultPageVisible(QInstaller.TargetDirectory, false);
installer.addWizardPage(component, "TargetWidget", QInstaller.TargetDirectory);
targetDirectoryPage = gui.pageWidgetByObjectName("DynamicTargetWidget");
targetDirectoryPage.windowTitle = "选择安装目录";
targetDirectoryPage.description.setText("请选择程序的安装位置:");
targetDirectoryPage.targetDirectory.textChanged.connect(this, this.targetDirectoryChanged);
targetDirectoryPage.targetDirectory.setText(Dir.toNativeSparator(installer.value("TargetDir")));
targetDirectoryPage.targetChooser.released.connect(this, this.targetChooserClicked);
gui.pageById(QInstaller.ComponentSelection).entered.connect(this, this.componentSelectionPageEntered);
}
Component.prototype.targetChooserClicked = function()
{
var dir = QFileDialog.getExistingDirectory("", targetDirectoryPage.targetDirectory.text);
if (dir != "") {
targetDirectoryPage.targetDirectory.setText(Dir.toNativeSparator(dir));
}
}
Component.prototype.targetDirectoryChanged = function()
{
var dir = targetDirectoryPage.targetDirectory.text;
if (installer.fileExists(dir) && installer.fileExists(dir + "/bin/MySoftware.exe")) {
targetDirectoryPage.warning.setText("<p style=\"color: red\">检测到程序已安装,继续将会被覆盖。</p>");
} else {
targetDirectoryPage.warning.setText("");
}
installer.setValue("TargetDir", dir);
}
Component.prototype.componentSelectionPageEntered = function()
{
var dir = installer.value("TargetDir");
if (installer.fileExists(dir) && installer.fileExists(dir + "/maintenancetool.exe")) {
installer.execute(dir + "/maintenancetool.exe", "--script=" + dir + "/script/uninstallscript.qs");
}
}
7. QtIFW 实现自动升级
要实现自动更新,必须要有一个在线存储库respoitory。在 QtIFW 中,如果要实现这样的功能,可以利用 maintenancetool.exe及其两个重要选项:
--checkupdates:检测更新,并返回一个 XML。
--updater:以更新模式启动应用程序。
7.1. 检测更新
为了获取更新相关的信息,我们新建一个 bat 脚本,并通过以下命令将输出重定向到 checkUpdate.txt 文件中:
@echo off
maintenancetool --checkupdates > checkUpdate.txt
当有可用的更新时,maintenancetool 会返回一个 XML,通过bat 脚本将输出信息重定向到 checkUpdate.txt 文件中:其中包含了新版本的名称、大小、以及版本号等内容。倘若没有任何更新,将不会返回任何内容。
7.2. 以更新模式启动maintenancetool
一旦检测到有新版本存在,只需要以更新模式启动 maintenancetool 就行maintenancetool --updater,这样以来,默认就会选择【Update components】选项:剩下的具体要更新哪些组件,就交由用户选择。
7.3. 具体实现
为了启动外部程序,先简单介绍下 QProcess 类,它有两种启动方式:
一体式:start(),外部程序启动后,将随主程序的退出而退出。
分离式:startDetached(),外部程序启动后,当主程序退出时并不退出,而是继续运行。
参考 https://github.com/Skycoder42/QtAutoUpdater。自写Demo,
//在使用 --checkupdates 检测更新时,并不会运行 GUI,而是仅输出更新信息
//以下内容放入main.cpp中的一个函数中
QString program("../maintenancetool.exe");
QStringList checkArgs;
checkArgs << "--checkupdates";
// 检测更新
QProcess process;
process.start(program, checkArgs);
// 等待检测完成
if (!process.waitForFinished()) {
qDebug() << "Error checking for updates.";
return;
}
// 读取输出内容
QByteArray data = process.readAllStandardOutput();
// 没有输出意味着没有可用的更新
if (data.isEmpty()) {
qDebug() << "No updates available.";
return;
}
// 倘若需要特定的更新信息,应该解析输出的 XML。
//当检测到有可用的更新之后,以更新模式启动 maintenancetool
// 以分离式启动
QStringList updaterArgs;
updaterArgs << "--updater";
bool success = QProcess::startDetached(program, updaterArgs);
if (!success) {
qDebug() << "Program startup failed.";
return;
}
//需要注意的是,这里需要以分离式启动,因为程序需要关闭以进行更新。
//在启动成功之后,最后记得关闭程序:
// 关闭程序
qApp->closeAllWindows();
8. 制作一款精美的QtIFW安装程序
主要是界面的UI美化。参考样式可在github上搜索“Qt Frameless"。 在config.xml中添加QSS文件美化UI用StyleSheet等元素标记。
<WizardStyle>Classic</WizardStyle>
<StyleSheet>style.qss</StyleSheet>
<TitleColor>#b1b1b1</TitleColor>
9. 参考资料
- github地址(含examples) https://github.com/Waleon/QtIFW.git
- 公众号:高效程序员 教程链接
-
|