ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

用Qt Installer Framework制作Windows离线安装包:从零到实战

用Qt Installer Framework制作Windows离线安装包:从零到实战 把Qt程序发给别人用最怕听到什么十有八九是这句“我双击了但是启动报错少了个Qt6Core.dll。”这种事发生一次我就决定不再用绿色版文件夹交付项目了。后来我换成了Qt官方出的 Qt Installer Framework在win10上做一个离线安装包客户拿到一个exe双击就进入熟悉的安装向导装完有桌面图标有开始菜单控制面板能卸载感觉立刻“正式”了不少。这篇文章就从这个场景出发讲讲我用 Qt Installer Framework 4.6.1 做 win10 离线安装包 的最基础流程。我尽量不用太深的术语把目录结构、配置文件、打包命令和踩过的坑一次说清楚适合刚接触QIFW的Qt开发者也适合那些已经用CMake或qmake编译出exe、但还没想好怎么交付给同事或客户的人。1. 先回答一个基础问题为什么我不用“绿色版文件夹”交付1.1 绿包的三个老问题网上经常有人推荐“把整个release文件夹压缩发过去”也就是所谓的绿色版。自己开发时这样确实方便但要交付给别人的时候麻烦就来了。第一缺运行库。Qt程序不是一个exe单独就能跑的它依赖Qt6Core.dll、Qt6Gui.dll、Qt6Widgets.dll还依赖platforms/qwindows.dll这些插件。windeployqt可以帮你把这些库拷全但如果你只是在开发机上直接压缩文件夹很可能漏掉某些插件目标机器上就会报“找不到Qt平台插件”或“无法定位程序输入点”之类的错。这个问题极其常见而且对不懂技术的人来说简直是灾难他根本不知道该怎么办。第二没有卸载入口。绿色版如果用户不想要了只能手动删文件夹。删不干净还好说如果安装到了C盘Program Files目录下普通用户根本没有权限乱删到时候还会怪你的软件“装上了就卸不掉”。第三升级困难。绿包发下一个版本用户要手动覆盖文件配置文件、数据库文件、旧版本残留很容易互相干扰。我见过不少项目因为绿包覆盖升级造成配置错乱最后连日志都找不回来。所以但凡你是认真要交付一个桌面软件安装器几乎是必须的。它帮你做了文件拷贝、目录选择、权限提升、快捷方式创建、卸载清理这么多事不是“多个文件而已”。1.2 Qt Installer Framework在打包方案里的位置当时我摆在面前的选择有这么几类NSIS、Inno Setup、Qt Installer Framework以及CMake里的CPack。NSIS很老牌脚本灵活但写起来反人类而且界面完全要靠插件美化离Qt的“那种感觉”很远。Inno Setup上手快Pascal脚本也简单做普通Windows安装器完全够用但它不是为组件化、在线更新设计的。CPack用CMake的话可以直接生成NSIS或Inno的安装器胜在集成但遇到自定义安装流程还是绕不开上面的脚本。Qt Installer FrameworkQt官方出品最大的特点是天生为Qt程序服务支持组件化安装、模块依赖关系、在线/离线更新、维护工具也就是安装后那个MaintenanceTool。如果你还会做Qt Widgets/QML程序QIFW的整套逻辑你会觉得很熟悉。当然QIFW不是没有缺点。它的自定义界面能力比较弱默认向导样式偏Linux风格想在Windows上做出特别现代的安装界面需要改很多代码。但对我们大多数人来说“官方、稳定、组件化、支持离线”这几个点已经足够了。尤其Qt Installer Framework 4.6.1这个版本配合Qt 6环境已经相当成熟不用再跟自己较劲选哪个脚本语言。2. 准备QIFW 4.6.1下载、目录、Win10下的环境细节2.1 从Qt官方渠道拿到工具拿到QIFW最正规的方式是打开Qt在线安装器在“Developer and Designer Tools”分类下找到“Qt Installer Framework 4.6.1”勾选后安装。这里要提醒一件事很多人会把“Qt离线安装包”和“Qt Installer Framework”弄混。网上搜“qt离线安装包下载5.14”出来的是老版本Qt库的离线安装文件那是给编译环境用的而QIFW是一个帮你生成安装器的工具包它的产物才是最终给别人用的离线安装包。两个东西完全不是一个概念。如果你不想走在线安装器Qt官方其实也分发了独立的QIFW压缩包解压就能用。我建议直接装进一个专门的目录比如D:\Qt\Tools\QtInstallerFramework\4.6.1后面所有命令都对着这个路径来避免和环境变量乱七八糟的冲突。2.2 bin目录里那几样工具分别干什么装完之后打开bin目录你会发现有好几个exe。对新手来说先认识四个足够工具作用我实际使用的频率binarycreator.exe根据config和packages打包生成安装器exe每次打包都用installerbase.exe安装器运行时的底层程序通常由binarycreator调用一般不用直接碰archivegen.exe把一个或多个文件压缩成7z格式的资源包组件较多时用repogen.exe根据packages生成一个可发布的软件仓库目录做在线更新时用第一次接触的人看到这么多工具确实会头大但记住一句话就够了最基础的离线安装包你只动binarycreator一个工具其他都是为“更复杂的分发方式”准备的。第4章我会专门讲它们为什么存在。2.3 建议先把环境变量和目录结构规划好我推荐在环境变量PATH里加上D:\Qt\Tools\QtInstallerFramework\4.6.1\bin这样命令行里直接敲binarycreator就行。不加也没问题每次写全路径就好但打包命令会显得很长而且容易打着打着找不到目录。Windows 10下面还有个小细节如果你把QIFW装在C盘且目录权限管得严第一次跑binarycreator可能会触发UAC弹窗或者被Defender/SmartScreen拦一下。这是工具本身没问题的状态下也会遇到的情况不用慌确认是Qt官方文件就行。但如果你是用在线安装器装的QIFW它会装在用户目录或指定目录一般不会碰权限问题。我自己的习惯是单独建一个D:\myinstaller工作目录里面只放打包相关的config目录和packages目录编译好的程序最终也是拷到这里来。这样项目源码、构建产物、安装器配置三者分开后面排查问题会比较清爽。3. 跑通最简离线安装器config.xml、package.xml与binarycreator3.1 先搭一个干净的目录骨架QIFW的输入目录结构是固定的我开始接触时也抄错过。最基本的骨架是这样D:\myinstaller\ ├─ config\ │ └─ config.xml └─ packages\ └─ com.example.myapp\ ├─ meta\ │ ├─ package.xml │ └─ installscript.qs └─ data\ └─ ... 这里放你程序的真实文件我习惯把com.example.myapp作为包名它类似Java包名的规则目的是全局唯一。如果你只做内部软件叫myapp也能跑但一旦以后要做在线更新、多组件依赖包名的唯一性就很重要了。data目录里放的是要安装到本机的文件meta目录里放的是描述和控制安装流程的元数据。记住这个区分后面所有逻辑都想得通。3.2 config.xml安装器的“门面”配置config.xml描述的是整个安装器而不是某一个组件。我这份是一个能直接跑的最小示例?xml version1.0 encodingUTF-8? Installer NameMyApp/Name Version1.0.0/Version TitleMyApp 安装程序/Title PublisherMyCompany/Publisher ProductUrlhttps://example.com/ProductUrl InstallerApplicationIconicon.ico/InstallerApplicationIcon InstallerWindowIconicon.ico/InstallerWindowIcon StartMenuDirMyApp/StartMenuDir TargetDirApplicationsDir/MyApp/TargetDir RequireAdmintrue/RequireAdmin MaintenanceToolNameMaintenanceTool.exe/MaintenanceToolName /Installer逐个说下重点Name和Version是安装器识别自己的关键信息控制面板卸载时会用到版本号的格式最好严格点比如“1.0.0”否则后续维护工具做更新判断会出问题。Title就是安装向导窗口标题上显示的文字建议用中文或本地化文案。Publisher是发布者名称如果以后做在线更新它会出现在维护工具的界面尽量固定不要乱改。TargetDir是默认安装目录。ApplicationsDir在Windows上通常指向“C:\Program Files”所以我会配合RequireAdmintrue/RequireAdmin。如果你想让普通用户装到自己的目录也可以换成HomeDir/MyApp。MaintenanceToolName是安装完成后生成的维护工具名字如果你不想让用户到处乱点起名“Uninstall.exe”或“UpdateTool.exe”都行。很多教程里会写一个RemoteRepositories小节这是在线安装器的配置。对于纯离线包下面的写法很常见RemoteRepositories Repository Url/Url /Repository /RemoteRepositories它的意思是“保留仓库节点的结构但里面没有地址”。这么做之后安装器就不会尝试联网找更新源。后文我会结合离线/在线差异继续讲这里你先记下离线包要么不写RemoteRepositories要么就把Url留空。3.3 package.xml组件的元数据怎么声明再来写packages/com.example.myapp/meta/package.xml。这个文件描述的是“包内组件”的元数据?xml version1.0 encodingUTF-8? Package DisplayNameMyApp 主程序/DisplayName DescriptionMyApp的核心组件必须安装/Description Version1.0.0/Version ReleaseDate2025-01-01/ReleaseDate Defaulttrue/Default ForcedInstallationtrue/ForcedInstallation Licenses License name许可协议 filelicense.txt/ /Licenses /Package关键字段Defaulttrue表示默认勾选安装。ForcedInstallationtrue表示这个组件强制安装用户在安装向导里不能取消勾选。像主程序这种组件我一般都会设成强制安装避免用户勾掉之后整个软件没法用。Licenses指向的是meta目录下的license.txt。如果配置了许可协议安装向导会多出一个“协议确认”页面用户必须同意才能继续。对正式交付来说这一步最好加上。如果我有多个组件比如主程序、命令行工具、示例项目就分别建不同目录并在各自的package.xml里做依赖声明比如用Requires标签指定依赖另一个包。这个基础篇先不展开但你要知道组件化是QIFW相对其他脚本工具的核心优势。3.4 用binarycreator一次生成exe目录和配置文件就位后生成安装包就一条命令cd /d D:\myinstaller D:\Qt\Tools\QtInstallerFramework\4.6.1\bin\binarycreator.exe -c config\config.xml -p packages -o MyAppInstaller.exe --offline-only参数含义-c指定config目录下的config.xml。-p指定packages目录binarycreator会扫描这个目录下所有包。-o指定输出的安装器文件名。--offline-only是强制生成离线安装器的参数。实际上当你没配置远程仓库时生成结果已经是离线的但加上这个参数等于给一条保险也让后来维护代码的人一眼知道初衷。如果生成过程中报错最可能是XML文件写错了。我通常会在命令行追个--verbose输出会详细很多。第一次跑通的人看到当前目录多出一个几MB到几十MB的exe就算是入门了。直接双击运行你就能看到一个带“下一步、同意协议、选择目录、安装、完成”的标准安装向导。4. 离线和在线差异本地仓库、Updates.xml与那几条配置4.1 离线包的“离线”到底体现在哪里很多人刚接触QIFW时会有个疑问我明明没有写任何在线逻辑为什么安装器还要在config.xml里处理仓库这得说清楚QIFW的底层模型。QIFW本质上相当于把安装内容拆成了一个个“包”每个包是一些7z压缩资源。当安装器运行时它会从某一个仓库中读取这些资源列表。在线安装器把仓库放在远程HTTP服务器上资源按需下载离线安装器则把仓库直接内嵌在exe里安装时从exe内部解包。所以“离线包”的意思是所有安装数据都被封进了一个exe安装过程中不访问网络。对很多内网办公环境、客户现场、甚至拿着U盘去演示的场合来说这是最省心的一种交付形态。判断一个安装器是否真的离线我常用的方法是在安装时开着任务管理器或防火墙监控看有没有对外网络请求。如果你照着前面config.xml里的空仓库配置做出来安装过程完全是静默的。4.2 config.xml里的仓库配置怎么影响安装器行为时间轴回到配置那一步。如果你在config.xml里写了RemoteRepositories Repository Urlhttps://update.example.com/repo/Url /Repository /RemoteRepositories安装器就会认为当前不是纯离线模式。用户安装时向导会尝试访问这个地址获取组件列表和资源索引。如果网络不通或者地址失效安装器会卡住或报错这就是很多人“明明给了exe但双击后一直转圈”的原因。所以做离线包的正确思路很明确RemoteRepositories不写或留空binarycreator加--offline-only运维和交付同事不要把exe当成万能钥匙离线包就是离线包别指望它能静默拉取升级内容。4.3 archivegen和repogen在什么场景下才需要碰如果你需要把所有组件的源文件交给一个集中仓库让用户可以从服务器安装/升级那就需要repogenD:\Qt\Tools\QtInstallerFramework\4.6.1\bin\repogen.exe -p packages D:\repo这样会在D:\repo下生成一个目录里面有各个组件的7z资源和Updates.xml索引文件。之后再用binarycreator做在线安装器时把这个目录用IIS或Nginx发布出去配置好URL就能在线安装。而archivegen更底层它负责把单个文件目录打包成7zD:\Qt\Tools\QtInstallerFramework\4.6.1\bin\archivegen.exe myapp.7z D:\build\release\*大多数基础场景下archivegen不用你手动调用binarycreator内部已经帮你处理了。但理解这个层级关系很重要不然你看到“仓库”“索引”“7z包”这些概念会一头雾水。其实一句话总结archivegen打包单个资源repogen组织成一个仓库binarycreator把仓库编译成安装器exe。5. 把真实Qt程序塞进离线包windeployqt与快捷方式脚本5.1 先用windeployqt补齐运行依赖很多人在这一步卡住是因为把自己编译的MyApp.exe丢进data目录就算完结果装完运行还是提示缺DLL。原因很简单你的开发机上装了Qt系统环境里能找到Qt6Core.dll但目标客户机器上不一定有。所以正确做法是先对你的Release版exe运行windeployqt。以Qt 6.6 MinGW为例cd /d D:\build\release D:\Qt\6.6.0\mingw_64\bin\windeployqt.exe MyApp.exe --release --no-translations执行完release目录下会多出大量文件比如Qt6Core.dll、Qt6Gui.dll、Qt6Widgets.dllplatforms/qwindows.dllstyles/qmodernwindowsstyle.dllimageformats/qjpeg.dll等图片格式插件tls/、networkinformation/等网络插件如果你的程序用了网络功能这些文件一个都不能少。它们的相对位置也很讲究比如platforms必须放在“exe所在目录/platforms”下Qt是通过相对路径找插件的。5.2 按“安装后的目录结构”组织data目录现在你就要把这些依赖文件和exe一起放到packages/com.example.myapp/data里。这里有个重要原则data目录内的相对路径就是安装后的相对路径。举个例子如果我希望安装后C:\Program Files\MyApp\platforms\qwindows.dll那么你在data目录下就应该同样有platforms\qwindows.dll。不要把整个release文件夹再套一层比如data\release\MyApp.exe那样安装完用户还得进release目录才能启动非常别扭。我自己习惯的做法在packages/com.example.myapp/data下先建一个顶层目录MyApp还是直接摊开放取决于软件的安装结构。如果data目录下只有一个exe直接摊开放没问题如果程序本身有很多子目录和插件就按照项目的部署结构摆放。5.3 用installscript.qs创建桌面和开始菜单快捷方式光把文件装进去还不够正式软件装完总得有个桌面图标。这个功能要靠meta目录下的installscript.qs脚本实现。名字可以随便起但要和package.xml里的Script标签对应Scriptinstallscript.qs/Script然后在installscript.qs里写function Component() {} Component.prototype.createOperations function() { component.createOperations(); if (systemInfo.productType windows) { component.addOperation(CreateShortcut, TargetDir/MyApp.exe, DesktopDir/MyApp.lnk, workingDirectoryTargetDir, iconPathTargetDir/MyApp.exe, iconId0); } };几个变量解释一下TargetDir是最终安装目录也就是用户在安装向导里选的那个目录DesktopDir是当前用户桌面路径QIFW会在安装时解析成实际路径CreateShortcut是QIFW提供的内置操作第一个参数是程序路径第二个参数是快捷方式路径后面可以带工作目录、图标路径等可选项。如果你还想在开始菜单里放一个卸载入口QIFW默认会生成维护工具并在开始菜单里建一个MaintenanceTool.lnk。这个不用你手动加只要config.xml里配置了StartMenuDir安装器会自动生成。5.4 安装日志、卸载入口与注册表信息安装器不是“把文件复制过去”就完了。它在Windows上安装完成后会在注册表里写入卸载信息控制面板的“程序和功能”里能看到“MyApp”用户点卸载时会启动维护工具。卸载时维护工具会根据安装记录删除文件、快捷方式、注册的组件信息。这个机制很关键为什么要把它放进安装器而不是绿包因为你每次用QIFW生成安装器后相当于维护了一个“安装档案”。我遇到过一次用户手动删除程序目录结果控制面板里残留“MyApp”再点卸载就报错。这种问题听起来像系统脏了但其实是因为破坏了安装器自己的记账逻辑。调试阶段想快速看安装日志的话安装时可以用命令行加--verbose运行安装器日志会输出到临时目录Windows下一般是%TEMP%下的install.log。排查“装到一半闪退”“某个步骤失败”时这个日志比任何SQLite数据库都有用。6. Windows 10实测过程中最容易踩的5个坑6.1 目标目录带中文或空格QIFW自身对中文路径的兼容还行但它生成的快捷方式脚本、以及一些外部命令在目标目录含中文或空格时经常出幺蛾子。比如安装到D:\软件发布\我的程序桌面快捷方式可能打不开或者CreateShortcut时传参错误。我自己在Windows 10中文用户名下就踩过默认HomeDir展开后是C:\Users\张三后面拼接安装路径时一旦处理不好整个安装器都能崩。我的建议是正式交付软件的默认安装目录尽量用纯英文路径如果用户自定义安装目录非要选中文路径至少你在脚本里要处理引号并且用TargetDir变量而不是手拼字符串。6.2 SmartScreen“未知发布者”拦截新生成的安装器没有代码签名Windows 10会弹出“Windows已保护你的电脑”首次运行需要点“更多信息-仍要运行”。这个提示对客户体验非常致命很多不懂电脑的人看到这个弹窗就以为有病毒直接放弃安装了。这个问题的正解是做代码签名用证书对安装器exe签名后SmartScreen就不会再拦截。个人开发者没有证书时至少要在交付说明里提前告诉客户程序没有签名Windows会有提示选择“仍要运行”即可。这不丢人但千万别假装没有这回事。6.3 缺管理员权限导致写入失败如果你在config.xml里用了ApplicationsDir作为默认安装目录但没有设置RequireAdmintrue/RequireAdmin安装器会在写到C:\Program Files时因为没有权限而失败或者只能写入到虚拟化的重定向目录用户根本找不到程序装哪去了。这个坑隐蔽在程序开发机上的用户是管理员你怎么装都能成功换到普通用户机器上就废了。我现在只要目标目录是Program Files就一定会配RequireAdmintrue/RequireAdmin安装器运行时会主动请求UAC提权并把安装行为提升到管理员级别。6.4 杀毒软件把生成的安装器当风险程序Qt Installer Framework生成的安装器是自解压结构内部还有7z压缩数据一些杀毒软件甚至Defender在没有足够信誉信息的情况下会把全新的未签名安装器判定为“风险程序”或行为可疑。如果你的客户内部装有更严格的EDR这个概率还会增加。怎么处理第一优先级是代码签名第二是保证每次生成都在干净的可信环境里避免安装器被二次打包或加壳第三是给测试机临时加白名单但这对正式客户没有意义。我不建议为了绕过杀毒去做免杀、加壳、篡改QIFW内部组件那只会让误报更严重而且会让客户觉得你心虚。6.5 4.6.x脚本接口变化让人照老教程写不对QIFW 4.6.1的脚本API和很多老版本不太一样。网上搜到的旧教程里可能会出现installer.installerBaseDirectory、旧的component.addOperation(CreateDesktopEntry)等写法。在4.6.x里某些接口已经调整windows快捷方式更推荐用CreateShortcut。如果你照抄老代码发现没有效果不一定是写错了可能是接口变了。我的办法是遇到问题先打开QIFW自带的example目录Qt官方在安装目录下带了完整示例比如examples\tutorial里面是最标准的写法。在新版本里踩坑与其看网上零散笔记不如以官方example为基准再改。7. 收尾这个系列接下来我会写的方向这一篇主要把工具链、目录结构、离线exe生成和Win10下的基础避坑讲完了。按照“最基础使用”这个标题我更希望你先把上面第3章的例子完整跑一遍哪怕什么都不理解先看到一个能安装卸载的安装器。等这一步通了后面很多东西都是在这个基础上的迭代。我个人在实际项目中的体会是QIFW虽然看起来配置文件多、概念多但它最大的价值是“组件化”和“维护工具”这两套机制——一旦你的软件分成主程序、插件、文档多个模块或者需要在用户机器上保持一个长期可靠的卸载/升级档案前面这些学习成本就完全值得。下一篇我大概率会聊聊怎么在离线包里做多组件依赖与自定义安装页面以及如何用repogen把同一个包升级成“可增量更新”的在线仓库。如果你也在研究Qt Installer Framework欢迎把遇到的问题留在这篇文章后面我看到了会挑典型的坑继续写。
RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进