做Windows桌面软件的人早晚都会面对同一个问题代码写完了exe也编出来了怎么把它变成用户可以放心双击、一路Next就装好的安装包程序我见过太多项目死磕代码结果发布环节随便用RAR自解压糊弄或者直接扔一个绿色文件夹让用户跑后续卸载全靠手动删口碑全毁在安装体验上。我的方案一直很固定用Inno Setup。这工具开源免费、脚本灵活、打出来的安装包体积小还能顺手搞定快捷方式、注册表、静默安装这些事。这篇文章把我在实际项目里用Inno Setup打包exe文件的整套过程包括脚本写法、参数逻辑、踩过的坑一次性拆开讲清楚。1. 为什么偏偏选Inno Setup同类工具对比与选型思路市面上能做安装包的工具不少但真正到实操环节每个工具的脾气差别很大。Inno Setup能成为我长期主力不是因为它功能最多而是因为它在“够用”和“可控”之间平衡得最好。先看最典型的几个场景。以Python写工具的人为例代码写好之后通常用PyInstaller把整个解释器和依赖打成exe但同事或者客户要的不是一个裸exe而是一个带安装向导、能选目录、能给开始菜单和桌面创建快捷方式的安装程序。Qt开发者也是一样windeployqt把Qt运行库收集完还是要一个安装包把这一堆DLL、插件、QML文件整整齐齐地装到目标机器上。甚至有些驱动类工具辛辛苦苦从驱动exe里把inf文件提取出来最终也要靠一个安装包把它们统一部署进去。这些场景的共同点是原始产物是exe文件但最终交付物应该是一个安装包程序。Inno Setup正好卡在这个需求上。再对比其它工具。NSIS是另一款经典开源方案脚本系统和Inno Setup完全不同它的语法更像汇编写起来繁琐学习成本高而且要在UI精致程度和默认模板上追赶Inno Setup得额外花不少功夫。InstallShield是老牌商业软件功能确实全但授权费用不低工程文件也重很多项目根本用不到那一堆高级功能。Advanced Installer界面友好可惜免费版限制太多稍微需要些高级设置就逼你升级付费版。相比之下Inno Setup靠一份文本格式的.iss脚本就能把整个安装逻辑表达清楚脚本还能直接放进Git里管理团队成员谁改动一目了然这是其它工具难替代的优势。再说说Inno Setup的脚本语言。它的脚本基于Pascal语法但不要求你会完整的Pascal编程很多配置就是“参数名值”的形式理解门槛比NSIS低得多。遇到真正需要定制逻辑的场景比如检测系统版本、读注册表决定安装路径、在安装过程中执行一段判断Inno Setup又能随时用[Code]段写函数。这种“平时用配置复杂场景用代码”的设计让我这种不喜欢被框架束缚的人用起来非常舒服。基于这套思路我在新项目里基本固定了流程不管是PyInstaller出来的exe、Qt编译的exe还是从驱动包里提取出来的inf文件统一交给Inno Setup收口。不是因为它多高大上而是它足够稳定踩过的坑少出了问题也查得到。1.1 哪些场景最适合用Inno Setup收尾先说Python转exe这条线。很多教程推荐用pyinstaller -F生成单文件exe然后直接发给别人用。我在实际项目里吃过亏单文件exe每次启动都要把运行库释放到临时目录首启速度慢不说杀毒软件盯着这类自释放行为盯得非常紧误报率明显比目录模式高。所以我的发布流程是先用PyInstaller以目录模式打出dist目录确认程序能跑再让Inno Setup把整个dist目录原样装进安装包。这样一来用户拿到的安装包是“安装后启动”的正常体验不是每次双击都解压一次的临时体验。再说Qt程序。Qt的发布天然依赖一大堆DLL很多人直接把一个几百兆的安装包塞给用户其实只需要在编译产物目录里执行一遍windeployqtQt运行库会被自动收集到exe旁边这一下体积就能从几百兆降到几十兆。但即便如此直接给人zip包依然不专业。用Inno Setup把exe、DLL、plugins、resources全部封装成安装包再在[Run]段里静默装好VC运行库才算是真正完整的交付。还有一类场景容易被忽略硬件设备相关的工具。很多驱动的原始发布形式是自解压exe运维想要离线部署时第一步是先把里面的inf文件提取出来第二步就是把inf连同sys、cat一起打成自己的安装包。这种需求用Inno Setup也能做关键是要保持原始的驱动文件目录结构不能只挑一个inf文件塞进去。1.2 为什么不用绿色版或自解压代替安装包我见过不少小团队用WinRAR做自解压美其名曰“绿色软件”。但有个实际问题是自解压默认没有安装/卸载的概念文件释放到哪里、释放之后执行什么命令全看解压配置写得对不对用户想卸载时根本找不到入口系统注册表里还可能残留垃圾。Inno Setup本身就带卸载器安装时自动生成“添加或删除程序”条目卸载时能把注册表项、快捷方式、安装的文件一起清干净。做产品发布的时候安装和卸载都是产品体验的一部分这一点不要含糊。2. 打包前的准备工作把exe和依赖整理干净很多新手上来就打开Inno Setup写脚本结果要么装完之后程序缺DLL跑不起来要么安装包体积大到离谱。安装包做得好不好其实在打包之前就已经决定了。必须先把目标exe和它周围的依赖清理干净再让Inno Setup接手。2.1 Python转exe之后先做目录瘦身和自测用PyInstaller生成发布目录时我建议的命令大概是这样的pyinstaller -D -w --clean --add-data config.ini;. --add-data assets;assets -i app.ico main.py几个关键参数解释一下-D是目录模式生成一个完整的dist文件夹-w隐藏控制台窗口适合界面程序--add-data把配置文件、资源目录带到exe旁边-i设置图标。目录模式生成的dist\你的应用名\下面除了exe之外还有_internal目录里面是Python运行库和依赖包。打包完成后千万别急着做安装包先把dist目录里的exe直接双击试一遍确认资源路径都对。很多程序以源码方式运行时找相对路径没问题一旦用PyInstaller打包工作目录变了配置文件和模型文件就找不到这种问题到了安装包里更难排查。我在处理完dist目录之后还会顺手删掉一些不必要的文件比如README、示例数据、无用的语言包。安装包体积直接影响用户下载耐心能省则省。2.2 Qt程序用windeployqt收集运行库Qt程序编译完之后直接拷贝exe到别的机器大概率会报“缺少Qt5Core.dll”所以发布之前要让windeployqt把运行库收集到exe所在目录。Windows下我习惯这样操作cd /d D:\build\release windeployqt MyApp.exe --release --no-translations执行完之后exe旁边会自动出现平台插件目录platforms核心文件是qwindows.dll、一堆Qt的DLL以及styles、imageformats等插件目录。这一步做完再把整个release目录拖到Inno Setup的[Files]段里打包。这里有个细节值得注意Qt如果是用MSVC工具链编译的目标机器上往往还需要对应的VC运行库。所以在prepare阶段就要把vc_redist.x64.exe单独下载好放在一个固定目录稍后在Inno Setup的[Run]段里静默安装它。这个问题在32位环境上尤其容易漏因为很多精简版Windows是没有VC运行库的。2.3 从exe形式的驱动文件里提取inf文件这个需求最初来自一个运维朋友厂商只给了驱动打包exe但公司想把它做进统一的软件分发系统需要先拿到inf文件。很多驱动exe本质是个自解压包只是套了一层安装引导里面装的就是inf、sys和cat文件。我常用的办法是直接用7-Zip打开exe。右键选择“打开压缩包”能看到内部文件列表找到.inf、.sys、.cat之后把它们全部解压到一个干净目录。有些驱动exe不支持7-Zip直接解那就运行一次让它正常安装或解压然后马上去看C:\Windows\Temp、%TEMP%或者安装程序的日志目录往往能找到它释放出来的原始文件。还有一种更简洁的办法有些驱动exe自带/extract或-e参数能只解压不安装具体参数见各厂商说明。提取出来的inf文件如果只是单独一个往往装不上。因为inf文件里会通过Include、Needs引用其它inf或者要求sys、cat必须放在相同目录所以完整地把整个解压目录的结构保留下来是后面Inno Setup打包成安装包的前提。这是我踩过Realtek网卡驱动坑之后总结出来的目录不完整即使安装成功也会在设备管理器里显示黄色感叹号。3. Inno Setup脚本核心原理解读Inno Setup的核心是一个文本格式的.iss脚本文件。所有安装流程、文件选择、界面文案、运行时机全都在这个文本里定义。把脚本写明白比点一万次向导都有用。3.1 一个最小可用脚本的逐行拆解先看一个最简单的脚本[Setup] AppId{{D6A8D95E-0018-4E2B-B9C1-3F7B0E5A3E21} AppName数据导出助手 AppVersion1.0.0 AppPublisherYourName DefaultDirName{autopf}\DataExportTool DefaultGroupName数据导出助手 OutputDir.\output OutputBaseFilenameDataExportTool_Setup_1.0.0 Compressionlzma2 SolidCompressionyes WizardStylemodern PrivilegesRequiredadmin [Files] Source: dist\DataExportTool\*; DestDir: {app}; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: {group}\数据导出助手; Filename: {app}\DataExportTool.exe Name: {autodesktop}\数据导出助手; Filename: {app}\DataExportTool.exe; Tasks: desktopicon [Tasks] Name: desktopicon; Description: 创建桌面快捷方式; GroupDescription: 附加图标; Flags: unchecked [Run] Filename: {app}\DataExportTool.exe; Description: 立即运行程序; Flags: nowait postinstall skipifsilent每段都说说。[Setup]是整个安装包的全局配置AppId、版本号、安装目录、输出文件名都在这里定义。[Files]决定哪些文件要被复制到哪里是安装包的主体。[Tasks]定义用户可选的附加操作比如要不要桌面快捷方式。[Icons]根据Task的勾选情况创建快捷方式。[Run]定义安装完成后执行什么程序。这段脚本已经能覆盖七八成常规需求。把脚本保存成setup.iss然后编译Output目录里就会多一个DataExportTool_Setup_1.0.0.exe这就是用户最终要运行的安装包程序。3.2 几个关键参数背后的逻辑AppId是最容易被忽略但最重要的一项。它在Inno Setup中相当于安装包程序的身份证卸载、升级、判断是否已安装全都依赖它。同一个产品哪怕版本升级到2.0AppId也必须保持不变否则系统会认为是两个完全不同的程序旧版本卸载信息残留新版本又另立门户。DefaultDirName是默认安装目录。{autopf}这个宏会根据PrivilegesRequired自动解析成实际路径如果要求管理员权限就是C:\Program Files下的目录如果是普通用户安装就会落到C:\Users\用户名\AppData\Local\Programs。我不建议硬编码C:\Program Files因为系统盘的盘符和用户目录结构都可能变化用宏去适配才是正路。PrivilegesRequiredadmin让安装过程申请管理员权限适用于写注册表、装运行库、往Program Files写文件的场景。如果只是一个普通用户级小工具完全可以改成lowest避免安装包一出来就弹UAC降低用户心理门槛。Compressionlzma2和SolidCompressionyes组合的压缩率最高代价是编译时间和安装时间略长。对于几十MB到几百MB的交付物这点时间完全可以接受。3.3 从脚本到安装包的编译流程Inno Setup本身就是一个带图形界面的IDE。安装后新建一个空脚本把上面的内容贴进去保存。右侧或者菜单里找到“Compile”点下去下方会输出编译日志。没有报错时安装包就生成在Output目录里。生成的安装包程序可以直接在测试机上跑一遍完整流程安装、启动、卸载确认卸载后没有残留目录和注册表项再对外发布。脚本还有一个好处文件是纯文本天然适配版本管理。团队里有人改过脚本Git记录一目了然出问题了可以回滚。4. 实战案例从Python和Qt程序到安装包全流程理论讲再多不如完整走一遍流程。这里用两个真实项目场景做演示一个是Python写的桌面小工具一个是Qt写的业务系统。4.1 案例一Python桌面工具打包假设项目结构是这样的PyInstaller已经把程序打包到了dist\DataExportTool\目录里面有一个DataExportTool.exe、一个_internal目录和一些配表。我的打包脚本如下[Setup] AppId{{D6A8D95E-0018-4E2B-B9C1-3F7B0E5A3E22} AppName数据导出助手 AppVersion1.2.0 AppPublisherYourName DefaultDirName{autopf}\DataExportTool DefaultGroupName数据导出助手 OutputDir.\output OutputBaseFilenameDataExportTool_Setup_1.2.0 Compressionlzma2 SolidCompressionyes WizardStylemodern PrivilegesRequiredadmin ArchitecturesInstallIn64BitModex64compatible ArchitecturesAllowedx64compatible [Languages] Name: chinesesimp; MessagesFile: compiler:Default.isl,compiler:Languages\ChineseSimplified.isl [Tasks] Name: desktopicon; Description: 创建桌面快捷方式; GroupDescription: 附加图标; Flags: unchecked [Files] Source: dist\DataExportTool\*; DestDir: {app}; Flags: ignoreversion recursesubdirs createallsubdirs [Icons] Name: {group}\数据导出助手; Filename: {app}\DataExportTool.exe Name: {autodesktop}\数据导出助手; Filename: {app}\DataExportTool.exe; Tasks: desktopicon [Registry] Root: HKCU; Subkey: Software\DataExportTool; ValueType: string; ValueName: InstallPath; ValueData: {app}; Flags: uninsdeletekey [Run] Filename: {app}\DataExportTool.exe; Description: 立即运行程序; Flags: nowait postinstall skipifsilent这次加了[Registry]段把安装路径写进注册表同时声明了uninsdeletekey卸载时自动删除注册表项。这个习惯值得形成程序里凡是写了注册表的地方安装脚本里都要考虑卸载清理。ArchitecturesInstallIn64BitModex64compatible这个设置是为64位系统准备的。如果目标机器只有64位Windows加不加这行影响不大但如果要兼容32位系统就不能乱用x64相关参数。实际项目里要看PyInstaller生成的是不是64位exe如果是建议直接声明64位避免被装到32位兼容目录下反而给用户造成困扰。编译完成后拿安装包到一个干净的虚拟机里测试。重点看三步安装过程中能不能正常选择目录、安装完成后exe能不能直接启动、控制面板里卸载后安装目录是否连根拔起。这三步过关发布包才算合格。4.2 案例二Qt程序打包同样的一套逻辑换到Qt项目上变化主要在准备工作。先用windeployqt把运行库收集好目录结构完整了再写Inno Setup脚本[Setup] AppId{{D6A8D95E-0018-4E2B-B9C1-3F7B0E5A3E23} AppName智能看板系统 AppVersion2.0.1 AppPublisherYourName DefaultDirName{autopf}\Kanban DefaultGroupName智能看板系统 OutputDir.\output OutputBaseFilenameKanban_Setup_2.0.1 Compressionlzma2 SolidCompressionyes WizardStylemodern PrivilegesRequiredadmin [Files] Source: build\release\*; DestDir: {app}; Flags: ignoreversion recursesubdirs createallsubdirs Source: redist\vc_redist.x64.exe; DestDir: {tmp}; Flags: deleteafterinstall [Run] Filename: {tmp}\vc_redist.x64.exe; Parameters: /install /quiet /norestart; StatusMsg: 正在安装 VC 运行库请稍候...; Flags: skipifdoesntexist Filename: {app}\Kanban.exe; Description: 运行智能看板系统; Flags: nowait postinstall skipifsilent这里有一个很实用的模式Source: redist\vc_redist.x64.exe; DestDir: {tmp}; Flags: deleteafterinstall。意思是不把VC运行库装进用户的应用目录而是先把安装程序放进临时目录在[Run]节里以静默方式执行装完自动删掉临时文件。skipifdoesntexist标志则确保只有这个文件真的存在时才会去执行。还要说明一个容易被坑的地方Qt在MSVC和MinGW下编译出来的发布目录结构不完全一样。MSVC版本依赖运行时库必须带vc_redistMinGW版本则要特别注意libgcc_s-*.dll、libstdc-6.dll这些运行库是否被正确拷贝。windeployqt通常能覆盖大部分库文件但遇到第三方DLL比如OpenSSL、FFmpeg一定要在打包前用Dependencies工具扫描一遍看exe到底加载了哪些动态库再决定发布目录里还缺什么。4.3 给安装包程序加上必要的运行时机控制[Run]段是最容易被人忽视又最实用的部分。除了安装结束后启动程序它还能在特定时机做更多事。比如安装完成时静默执行一条配置命令[Run] Filename: {cmd}; Parameters: /c setx KANBAN_HOME {app}; Flags: runhidden通过runhidden标志隐藏命令行窗口把应用目录写入系统环境变量方便后续脚本调用。又比如卸载前需要停掉正在运行的进程可以用[UninstallRun]段调用taskkill。这些都是工程里真实会遇到的场景不是单纯的“安装完启动一下”就够了。5. 常见问题与排查技巧实录做安装包做得多了自然积累了一堆踩坑经验。下面几个问题是我在各类项目里反复遇到的整理成速查形式方便对号入座。5.1 安装包程序被Windows Defender或第三方杀软误报这是PyInstaller和Inno Setup组合最容易遇到的问题。新生成的安装包没有任何信誉积累杀软对这种“一个exe释放文件再执行其它exe”的行为天然敏感。我自己踩过最严重的一次是给一个小财务工具做的安装包刚发出去没两天客户说杀毒软件直接隔离了。处理思路分几步走。第一代码签名是降低误报最有效的手段。有条件就做没条件至少要在发布页提供文件的SHA256校验值引导用户下载后先比对哈希再运行。第二PyInstaller项目务必使用最新版本旧版本被扫描引擎标记的特征明显更多。第三不要给exe加UPX壳。很多教程为了压缩体积让PyInstaller加UPX结果反而提高误报概率得不偿失。第四如果真的被误报了正规做法是去Windows Defender安全中心“提交文件进行误报分析”等微软处理。这里要额外说明一句一切发布行为都要走正规软件的正常分发渠道不要去研究什么绕过查杀的路子那是把自己往坑里带。5.2 安装完成后程序无法启动这个问题最典型的现场是双击exe完全没反应或者弹窗提示“找不到xxx.dll”。原因往往是两种安装包少了文件或者动态库加载路径不对。排查时不要直接去猜我习惯分三步。第一步在开发机的原始发布目录里运行exe如果原始目录运行正常说明问题出在安装包少了文件如果原始目录也不行问题在前面的打包环境。第二步用Dependencies工具打开exe看它依赖的动态库是否都在安装目录里。第三步如果程序启动秒退而且没有报错就去看Windows事件查看器里应用程序日志里面通常会写明缺失的模块名。把这个排查流程固定下来基本能覆盖大多数启动失败场景。还有一个容易忽视的点是路径问题。程序里如果用绝对路径读配置文件或者试图往安装目录写数据而安装目录在Program Files下手权限受限就会表现成“安装成功但运行失败”。这类问题最好根治程序数据一律写到%APPDATA%安装包只负责放程序本体。5.3 静默安装和卸载的参数细节企业批量部署经常需要静默安装。Inno Setup安装包程序支持命令行参数Kanban_Setup_2.0.1.exe /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /SP- /DIRD:\Kanban /TASKSdesktopicon /LOGC:\install.log/VERYSILENT让安装过程完全不弹界面进度条都不显示/SUPPRESSMSGBOXES抑制所有消息框/DIR指定安装目录/TASKS动态控制是否创建桌面快捷方式/LOG写出安装日志出问题时排查非常有用。关于静默模式有两个细节要注意。一是/DIR的参数值如果包含空格必须加英文双引号包起来。二是静默安装不会展示[Run]段里那些带postinstall和nowait标志的启动项要是你指望安装完自动启动一次程序做初始化配置得改用/MERGETASKS或者写成独立命令。另外卸载器的静默卸载参数通常形如C:\Program Files\Kanban\unins000.exe /SILENT这个在自动化运维脚本里也常用到。5.4 版本升级时AppId和旧版本的纠缠产品迭代之后安装包同名不同版本是常态。为了规避坑我把AppId的维护原则定为只要产品不变AppId一律不动。AppId变了新版安装包在升级时不会把旧版本视为同一个产品卸载器记录就会分裂结果就是用户机器上出现两个安装记录旧目录删不干净。另外升级安装包时有一个容易忽视的界面行为新版本默认会沿用上一次的安装目录这个逻辑在大多数场景下是对的但如果你这次发布改了产品名旧路径还带着老名字就会很奇怪。可以在[Setup]里用UsePreviousAppDirno让安装程序在升级时忽略上一次记录的目录重新弹出目录选择页。同理DisableProgramGroupPage这类参数也值得按产品实际情况调整不要让安装向导出现一个毫无意义的选择步骤。最后再分享一个长期实践下来的体会安装包不是一个“写完代码顺带做一下”的东西它和源代码一样需要进版本库、需要写变更记录、需要放到干净环境里完整测一遍安装和卸载。Inno Setup的脚本本身就是工程的一部分维护好脚本比依赖一个黑盒工具或花钱买商业套件更省心。
阅读完成 · 觉得有帮助?