多彩编程 多彩编程MZPH · CODE BLOG
ARTICLE DETAIL

文章详情

深耕前端与后端开发技术的一线实战笔记与踩坑复盘。

C# 打包部署:.NET Framework 离线集成与版本检测实战

C# 打包部署:.NET Framework 离线集成与版本检测实战 做 C# 上位机或者企业内部管理系统的朋友大概率都经历过这种场面程序在开发机上点开就跑拷到现场那台工控机上双击一闪就没了或者弹个小窗提示“这台计算机中已经安装了 .NET Framework 4.8 或版本更高的更新”然后安装程序直接退出。C# 打包部署里真正麻烦的从来不是把 exe 塞进安装包而是目标机上那套 .NET Framework 到底在不在、版本够不够、没网的时候怎么补上去。这篇就把“把 .NET Framework 也打进安装包”这条路走一遍从版本定档、判断逻辑、Inno Setup 和 WiX 的具体写法到现场踩过的坑尽量说透。内容适合两类人一类是刚接手部署工作的 C# 开发者只会 F5 调试没打过安装包另一类是做过多轮部署、但每次都被框架版本问题磨掉半天的老手。前者可以照着流程一步步抄后者可以直接跳到判断逻辑和排查速查表那几节里面有几个关于注册表视图和 Win7 补丁的细节是我自己踩过之后才记住的。1. 先把问题想清楚为什么框架要跟着安装包一起走1.1 现场机器到底缺什么部署现场的真实情况和开发机完全不是一回事。开发机上装过 Visual Studio.NET Framework 4.x 全家桶早就顺带装齐了但客户那边的工控机、收银机、办公电脑可能是一台装了七八年的 Win7 SP1可能是一台刚重装过系统的 Win10 纯净版也可能是一台被 IT 部门统一管控、装软件要走审批的内网机器。前两种情况下机器上大概率只有一个残缺的框架环境甚至什么运行时都没有。.NET Framework 和 .NET Core / .NET 5 有一个本质区别4.x 是 Windows 的系统级组件装在%WINDIR%\Microsoft.NET\Framework和Framework64下面全机器共享版本之间是就地升级关系。这意味着你没法像 .NET 6 那样做一个自包含发布把运行时塞在自己程序目录里。你的 C# 程序在目标机上运行时用的永远是系统里那一份。所以“程序能不能跑起来”这件事主动权不完全在你手里。我遇到过最典型的一次客户现场二十多台机器其中三台是刚做的系统装完我们的安装包之后主程序起来就报Could not load file or assembly或者直接闪退事件查看器里写着找不到某个方法。查了半天那三台机器上只有 .NET Framework 4.5而我们的程序是按 4.7.2 编译的。安装包把文件都拷过去了但运行时缺位程序自然起不来。从那之后凡是我经手的项目安装包里必带框架离线包。1.2 三种分发方式的真实取舍把框架送到目标机网上常见的做法有三类实际效果差别很大。第一种是让程序自己去微软官网下载。这种方式包体最小安装包可能只有几 MB但前提是目标机能上外网。工控现场、涉密内网、企业统一出口管控的环境这条路基本走不通而且用户看到安装过程卡在那里等下载体验很差。我的经验是只要项目会交付到非开发人员手里就不要指望网络。第二种是把dotnetfx.exe这类在线引导包塞进安装包。文件很小大概一两 MB但它本质是个下载器运行时还是要联网拉几百 MB 的内容。这个方案经常被误认为“已经把框架打进去了”实际上只是把下载这一步藏起来而已用户端依旧会失败而且报错信息往往很含糊。第三种是把完整的离线安装包放进安装包安装时先判断、再静默安装、最后安装主程序。包体会大.NET Framework 4.8 的离线包本身就在 110MB 上下压缩后也压不了多少最终成品可能 130MB 到 150MB。但换来的是完全离线可用、装完就能跑。做企业内部工具和工控上位机我基本都选这条。提示不要为了体积去考虑“只装 Web 版引导包 让用户自己联网”这种折中方案。现场部署最怕的就是不确定性一次失败带来的沟通成本远高于多传 100MB 文件。1.3 一个容易忽略的前提4.x 是就地升级在动手之前必须搞明白 .NET Framework 4.x 的版本关系。4.5、4.6、4.7、4.8 共享同一个 CLR 主版本号 4它们是覆盖式安装的。一台机器上装了 4.8就相当于同时满足了 4.5、4.6、4.7 的所有要求不会出现“4.5 和 4.8 并存”的情况。这点和 3.5 及以下版本完全不同2.0、3.0、3.5 是另一套体系可以和 4.x 共存。这个特性带来了两个直接后果。一是判断逻辑要写成“大于等于”而不是“等于”。如果按等于判断一台装了 4.8.1 的机器会被你判定为“不满足”然后你去装 4.8安装程序会直接拒绝执行报出那句很多人见过的提示——这台计算机中已经安装了 .NET Framework 4.8 或版本更高的更新。二是不要在程序里写死某个具体版本号做校验用 Release 数值做下限比较才是稳的做法。顺便提一句版本支持状态.NET Framework 4.5.2、4.6、4.6.1 早就过了支持期4.6.2 之后的版本才值得作为目标。新项目直接定 4.8 是最省心的选择兼容性覆盖 Win7 SP1 到 Win11 全系。如果确实要兼容更老的环境那目标框架定 4.6.2但要在安装逻辑里做“已装更高版本就跳过”的处理。2. 动手前的准备版本定档、依赖梳理、工具选型2.1 目标框架版本怎么定档版本定档这件事看起来是个技术决策其实更多是客户环境决策。我的习惯是先问三个问题目标机最低是什么系统有没有可能还在用 Win7程序会用到哪些只在特定版本里出现的 API如果客户环境里确实有 Win7 SP1那 4.8 是可以上的但有个前提——那台 Win7 必须已经打了 SHA-2 代码签名支持的补丁。微软从 2019 年开始所有 4.8 相关的安装包签名都换成了 SHA-2而没打过补丁的 Win7 SP1 会直接报“时间戳签名和/或证书无法验证或已损坏”然后安装中断。这不是你的安装包写错了是系统太老。解决办法是先在目标机上装KB4474419SHA-2 支持和KB4490628服务栈更新再装 4.8。至于 .NET Framework 3.5它的分发方式和 4.x 完全不同后面单独用一节说因为 Win10 和 Win11 上的处理方式跟 Win7 是两回事。还有一个细节项目里的目标框架版本不要随便改。在 csproj 里TargetFrameworkVersion写的是 v4.8编译器就会按 4.8 的引用程序集编译生成的程序集元数据里会记录这个依赖。如果你改成 v4.6.2 但代码里用了 4.7 才有的 API编译直接失败这是好事反过来把它调低却忘了改 app.config 里的supportedRuntime运行时可能加载到别的版本上行为会很诡异。app.config 里这一段最好和 csproj 保持一致startup supportedRuntime versionv4.0 sku.NETFramework,Versionv4.8 / /startup2.2 依赖清单与离线安装包获取准备工作里最容易被跳过的一步是把依赖列全。除了 .NET Framework项目里可能还依赖 VC 运行库、SQL Server 的某个组件、某个第三方打印控件、某个串口驱动。这些东西任何一个缺失都会让现场表现为“装了但用不了”。我的做法是拿一台干净的虚拟机做验证。虚拟机装一个最接近客户环境的最低配置系统然后只用安装包去装装完把程序的主要功能走一遍。这一步花两小时能省掉后面反复沟通的两天。.NET Framework 的离线包要去微软官方下载页拿注意区分在线版和离线版文件名里通常带allos的才是完整离线版比如 4.8 的ndp48-x86-x64-allos-enu.exe中文版对应的是ndp48-x86-x64-allos-chs.exe。下载下来之后先双击装一遍确认能装成功再放进项目目录。这个动作很重要因为有些下载页给的是引导包文件名很像实际只有几 MB放进去之后现场才发现要联网就麻烦了。2.3 三套打包工具怎么选能实现“安装包里带框架”的工具主要有三套各有各的适用场景。Inno Setup 是我最常用的。免费、脚本化、控制粒度细特别适合需要精确控制安装顺序和判断逻辑的场景。它的[Code]段支持 Pascal 风格的脚本能在安装文件之前做前置检查、调用外部安装程序、根据返回码决定后续流程。缺点是界面偏朴素要好看得自己写皮肤。WiX Toolset 是微软官方路线用 XML 描述安装包配合 Burn 引导程序Bootstrapper可以做很专业的链式安装。适合需要 MSI 标准化交付、要走企业软件分发系统的项目。学习曲线陡一个括号写错就得查半天。Visual Studio 的 Installer Projects 扩展是最省事的图形界面点点点就能配前置条件。它的“已下载的前提条件”功能配合本地包目录也能实现离线安装。缺点是模板比较老遇到复杂逻辑就不好扩展而且每次 VS 大版本更新后经常要重新找安装入口。选择建议很简单内部工具、工控上位机、需要精细控制流程的用 Inno Setup要给企业 IT 部门交付标准 MSI 的用 WiX只是自己团队几十台机器用、不想折腾的用 VS Installer Projects。2.4 目录结构先规划好开始写脚本之前建议把工程目录先理清楚后面维护会轻松很多。我的习惯是这样Deploy/ redist/ # 第三方运行时原样存放 ndp48-x86-x64-allos-chs.exe vc_redist.x64.exe # 如果有 src/ # 主程序发布输出 MyApp.exe MyApp.exe.config *.dll installer/ setup.iss # Inno Setup 脚本 License.txt app.ico build.ps1 # 一键构建脚本把redist和src分开好处是每次发布只需要替换src里的内容框架包常年不动构建脚本也好写。用 PowerShell 或者批处理在 CI 里跑的时候这个结构能让清理和归档逻辑非常干净。3. 判断逻辑怎么知道目标机到底装没装3.1 注册表里的 Release 值就是标准答案判断 .NET Framework 4.x 版本靠的是注册表。位置在HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full里面有个Release的 DWORD 值数值越大版本越新。这个值从 4.5 开始引入是官方推荐也是唯一可靠的判断方式。不要去读Version字符串做字符串比较也不要用Environment.Version后者拿到的是 CLR 版本永远是 4.0.30319 这种跟你装的框架版本不是一回事。常用的 Release 数值我整理成一张表做下限比较的时候直接对照.NET Framework 版本Release 最小值说明4.5378389已过支持期4.5.1 / 4.5.2378758 / 379893已过支持期4.6393295已过支持期4.6.1394254已过支持期4.6.2394802建议作为最低目标4.7460798建议作为最低目标4.7.1461308可用4.7.2461808可用4.8528040推荐目标4.8.1533320Win11 22H2 起自带表里的数值是起始值实际机器上读到的可能是 528372、533325 这类数字只要是大于等于就说明满足了。有个坑要记住如果v4\Full这个子键不存在说明机器上连 4.5 都没有直接把 Release 当 0 处理不要抛异常。3.2 C# 里怎么写这段判断含 32/64 位视图坑在 C# 里写这段判断正确的做法是显式指定注册表视图。因为 64 位 Windows 上32 位进程访问HKLM\SOFTWARE时会被系统自动重定向到Wow6432Node虽然 .NET 的 NDP 键在两个视图下一般都能读到但在某些被精简过的系统镜像上两个视图的内容可能不一致。如果你的程序是 AnyCPU 编译、在 64 位系统上以 32 位方式运行就可能读到错误的结果。using System; using Microsoft.Win32; public static class DotNetFrameworkChecker { // .NET Framework 4.8 的起始 Release 值 private const int Net48Release 528040; /// summary /// 读取 v4\Full 的 Release 值未安装时返回 0。 /// 显式使用 Registry64 视图避免 32 位进程被重定向。 /// /summary public static int GetRelease() { try { using (var baseKey RegistryKey.OpenBaseKey( RegistryHive.LocalMachine, RegistryView.Registry64)) using (var ndpKey baseKey.OpenSubKey( SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full)) { if (ndpKey null) return 0; var value ndpKey.GetValue(Release); return value null ? 0 : Convert.ToInt32(value); } } catch { // 注册表不可读时保守返回 0让上层走安装流程 return 0; } } public static bool IsNet48OrLaterInstalled() { return GetRelease() Net48Release; } }这段代码可以直接放进启动器Launcher里。很多项目会做一个很小的引导 exe用最老的框架编译先检查框架版本不够就弹提示并调起安装包。这样做的好处是引导程序本身对运行时要求极低哪怕目标机只有一个 4.0 也能跑起来。注意OpenBaseKey指定Registry64之后在纯 32 位系统上访问不会失败系统会回落到默认视图所以这段代码在 32 位 Win7 上也是安全的。3.3 把判断逻辑搬进安装脚本安装脚本里的判断逻辑本质就是把上面那段 C# 换成 Pascal 写法。区别在于安装脚本运行在安装阶段机器上还没有你的程序集只能用脚本引擎自己的能力。判断逻辑要写成“够用就跳过不够就装”而不是“没装就装”。还有个细节值得单独说如果安装包里同时要处理 3.5 和 4.8判断 3.5 的时候要两个注册表视图都查一遍。因为 3.5 是个很老的组件它的NDP\v3.5键在 64 位系统上通常位于 32 位视图里只查 64 位视图会漏判导致明明装了还去装一遍浪费时间还可能报错。下面这段是我实际在用的写法[Code] const NET48_RELEASE 528040; function GetNet48Release(): Cardinal; var Release: Cardinal; begin Result : 0; // HKLM64 是 Inno Setup 提供的常量强制访问 64 位注册表视图 if RegQueryDWordValue(HKLM64, SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full, Release, Release) then Result : Release; end; function NeedsNet48(): Boolean; begin Result : GetNet48Release() NET48_RELEASE; end; function IsNetFx35Installed(): Boolean; var Install: Cardinal; begin // 3.5 的键可能落在任一视图下两边都查 Result : (RegQueryDWordValue(HKLM64, SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5, Install, Install) and (Install 1)) or (RegQueryDWordValue(HKLM32, SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5, Install, Install) and (Install 1)); end;4. Inno Setup 实战把 4.8 离线包塞进安装包4.1 主脚本骨架与关键参数Inno Setup 的脚本分成几个段[Setup]描述安装包本身[Files]描述要打包的文件[Code]放自定义逻辑。先看骨架部分几个参数值得特别说明。[Setup] AppNameMyApp AppVersion1.3.0 AppPublisherMyCompany DefaultDirName{autopf}\MyApp DefaultGroupNameMyApp DisableProgramGroupPageyes OutputDir..\release OutputBaseFilenameMyApp_Setup_v1.3.0 Compressionlzma2/max SolidCompressionyes PrivilegesRequiredadmin ArchitecturesInstallIn64BitModex64 ArchitecturesAllowedx64 MinVersion6.1sp1 WizardStylemodern SetupLoggingyes CloseApplicationsyes RestartApplicationsno UninstallDisplayIcon{app}\MyApp.exe LicenseFileLicense.txt SetupIconFileapp.ico [Files] Source: ..\src\*; DestDir: {app}; Flags: ignoreversion recursesubdirs createallsubdirs ; dontcopy 表示不进安装包的常规解包流程只在脚本里按需释放 Source: ..\redist\ndp48-x86-x64-allos-chs.exe; Flags: dontcopyCompressionlzma2/max是必须的框架离线包本身已经是 CAB 压缩过的用 lzma2 大概还能再挤出一点空间。PrivilegesRequiredadmin也很关键装框架必须提权如果安装包本身没申请管理员权限调用框架安装程序时会静默失败日志里只留一个看不出所以然的错误码。MinVersion6.1sp1限定了最低系统是 Win7 SP1比这更老的系统直接拒绝安装比装到一半再失败体验好得多。CloseApplicationsyes在升级安装时会自动关闭正在运行的程序避免文件占用导致的替换失败。源文件里的dontcopy是个容易忽略的点。加了它之后这个文件不会被放在常规的文件列表里而是在需要的时候由脚本主动释放到临时目录。这样做的原因是框架安装必须在主程序文件落地之前完成顺序上要可控。4.2 用 PrepareToInstall 做前置安装安装顺序这块PrepareToInstall是最合适的位置。它在 Inno Setup 创建完安装目录、准备复制文件之前调用如果返回非空字符串安装会中止并显示这个字符串作为错误信息。所有前置依赖都应该放在这里处理。function PrepareToInstall(var NeedsRestart: Boolean): String; var ResultCode: Integer; InstallerPath: String; begin Result : ; if not NeedsNet48() then Exit; // 已经满足要求直接进入正常安装流程 InstallerPath : ExpandConstant({tmp}\ndp48-x86-x64-allos-chs.exe); if not ExtractTemporaryFile(ndp48-x86-x64-allos-chs.exe) then begin Result : 释放 .NET Framework 4.8 安装包失败请确认安装包完整。; Exit; end; if not Exec(InstallerPath, /q /norestart, , SW_SHOW, ewWaitUntilTerminated, ResultCode) then begin Result : .NET Framework 4.8 安装程序无法启动可能被杀毒软件拦截。; Exit; end; case ResultCode of 0, 1641, 3010: begin // 3010 和 1641 都表示安装成功但需要重启 if ResultCode 0 then NeedsRestart : True; end; 1602: Result : 用户取消了 .NET Framework 4.8 的安装无法继续。; 1603: Result : .NET Framework 4.8 安装失败1603。 请查看 %TEMP% 目录下的 dd_netfx 开头的日志文件。; else Result : Format(.NET Framework 4.8 安装失败返回码 %d。, [ResultCode]); end; end;这段逻辑里有几个点是我改了三四版才定下来的。一是先判断再释放避免不必要的磁盘写入二是ExtractTemporaryFile必须在用之前调用否则{tmp}目录下根本没有文件三是NeedsRestart变量的处理框架装完返回 3010 的时候如果你不告诉 Inno Setup安装结束后它不会提示用户重启用户直接点开程序某些依赖会以奇怪的方式失败。4.3 静默参数与返回码处理.NET Framework 离线安装包的参数支持不算丰富常用的就这几个/q或/quiet完全静默没有任何界面/passive显示进度条但不接受交互适合想让用户看到进度又不想被打断的场景/norestart禁止安装程序自动重启机器这个参数在安装包里几乎是必须的否则用户装到一半机器突然重启体验非常糟糕/log 路径可以指定日志文件位置排查问题时很有用。返回码这块0 是成功3010 是成功但需要重启1641 是成功且已经主动发起了重启1602 是用户取消1603 是致命错误。1603 的成因特别杂可能是系统里旧版本 4.x 的残留、可能是某个补丁缺失、可能是权限不足、也可能是磁盘空间不够。碰到 1603 不要瞎猜直接去看日志路径通常在%TEMP%下文件名以dd_开头里面会明确写出哪一步失败了。注意如果你把框架安装放在[Run]段而不是[Code]里Inno Setup 不会因为返回码非零而中断安装也不会拿到返回码做判断。需要根据返回码决定后续流程的场景一律用[Code]里的Exec。4.4 收尾重启提示与日志框架装完之后还有两件收尾的事值得做。一是给用户一个明确的重启提示。如果NeedsRestart被设为 True可以在[Code]的CurStepChanged里判断ssDone阶段弹一个自定义消息框告诉用户“运行时组件已安装完成建议重启计算机后再使用本软件”。这个提示比让用户自己去发现程序起不来要友好得多。二是开启SetupLoggingyes之后安装过程会在%TEMP%下生成Setup Log *.txt里面记录了每一步的动作和框架安装程序的返回码。现场出问题时让客户把这个文件发过来比在电话里问十遍“你点了什么”管用得多。我现在的习惯是在安装包的“完成”页面上加一行文字写明日志文件的位置客户自己就能找到。5. WiX 与 VS 安装项目另外两条路怎么走5.1 WiX Burn 引导程序如果项目必须产出标准 MSIWiX 是绕不开的。它的核心思路是用 Burn 做一层引导程序Bundle在链Chain里按顺序排列需要安装的包。框架包作为一个ExePackage存在链的最前面DetectCondition用来判断是否需要安装。Bundle NameMyApp Setup Version1.3.0.0 ManufacturerMyCompany UpgradeCodePUT-YOUR-GUID-HERE BootstrapperApplicationRef IdWixStandardBootstrapperApplication.RtfLicense / Chain ExePackage IdNetFx48 SourceFileredist\ndp48-x86-x64-allos-chs.exe PerMachineyes Vitalyes Compressedyes InstallCommand/q /norestart RepairCommand/q /repair /norestart UninstallCommand/q /uninstall /norestart DetectConditionNETFRAMEWORK45 gt; 528040 / MsiPackage SourceFilesrc\MyApp.msi Vitalyes / /Chain /BundleNETFRAMEWORK45是 WiX 的 NetFx 扩展提供的变量值就是注册表里那个 Release 数值。用它做DetectCondition比自己写判断省事但要注意gt;里的在 XML 里必须转义成gt;这个写法我第一次写的时候漏了编译直接报错查了半天才发现是转义问题。Vitalyes表示这个包必须安装成功失败则整个引导程序中止。Compressedyes让它进引导程序的压缩包用户拿到的是一个可执行文件不用管旁边的框架包放哪这点比 Inno Setup 的脚本方式省心一些。5.2 VS Installer Projects 的本地前置条件用 Visual Studio 的 Installer Projects 扩展流程是图形化的在安装项目的属性页里找到“系统必备Prerequisites”勾选对应的 .NET Framework 版本然后在下面选“从与我的应用程序相同的位置下载系统必备组件”。关键在最后一步。选了这个选项之后构建时 VS 会去C:\Program Files (x86)\Microsoft SDKs\Windows\v10.0A\Bootstrapper\Packages\下面找对应的包目录目录里有个Product.xml描述安装命令还有各语言的package.xml描述文件名。默认情况下这些描述指向的是官网下载地址所以你要做两件事把下载好的离线安装包放进对应目录并改成描述里约定的文件名然后把package.xml里的PackageFile属性和Command段的文件名改成一致。举个例子4.8 的目录是DotNetFX48里面有en和zh-Hans两个子目录。把离线包拷进zh-Hans目录编辑该目录下的package.xml把Name属性改成你实际的文件名同时把Command PackageFile...里的名字也改掉。改完之后重新构建生成的setup.exe加Packages文件夹一起拷给客户就能离线安装了。这个方案的坑在于不同 VS 版本和不同扩展版本Packages 目录的路径和结构可能有差异而且升级 VS 之后自定义过的 package.xml 有可能被覆盖。所以我的建议是改完之后立刻把整个 Packages 目录备份一份放进项目仓库构建脚本里加一步自动复制别指望它一直在那里。5.3 .NET 3.5 的特殊处理3.5 在 Win10 和 Win11 上的处理方式和 4.x 完全不同。微软从 Win8 开始就把 3.5 的按需组件化系统里保留了安装源但默认不启用。你拿dotnetfx35.exe去装在 Win10 上大概率会失败或者要求联网。正确做法是用系统自带的部署命令配合本地的安装源DISM /Online /Enable-Feature /FeatureName:NetFx3 /All /LimitAccess /Source:%~dp0sxs/LimitAccess是关键参数它告诉系统不要去找 Windows 更新只用指定目录里的文件。sxs目录可以从同版本 Windows 的安装镜像里拿路径是sources\sxs拷出来大概六十多 MB。用DISM而不是图形界面的“启用或关闭 Windows 功能”好处是可以在安装脚本里静默执行也能拿到明确的错误码。如果项目确实同时需要 3.5 和 4.8安装脚本里的顺序建议是先装 4.8它是完整的独立安装包成功率最高再启用 3.5最后装主程序。3.5 启用失败不应该阻断整个安装因为很多程序的 3.5 依赖只在特定功能里用到把它设成非致命错误让用户先把主程序装上再单独排查更合理。6. 实测踩坑与排查速查表6.1 体积与压缩的现实预期很多人第一次打这种包都会被体积吓一跳。.NET Framework 4.8 的离线安装包在 110MB 左右用 lzma2/max 压缩之后大概能到 100MB 上下因为安装包内部的 CAB 已经压过了再压空间有限。加上你的程序本体最终成品在 120MB 到 150MB 之间是很正常的。如果你实在在意体积可以考虑按客户环境做两个版本一个带框架的完整版给现场和纯净系统用一个不带框架的轻量版给已经确认装好框架的内部机器用。两个版本共用同一份 Inno Setup 脚本用#define加编译参数控制[Files]段是否包含框架包构建时传不同参数即可。这样不会因为维护两份脚本而出错。还有一个现实问题超过一定体积之后通过某些企业内网的文件传输渠道分发会很慢有些邮件系统还会拦截大附件。这时候要考虑分卷打包Inno Setup 的DiskSpanning支持把输出切成多个固定大小的分片配合解压使用。具体用哪种渠道分发按客户的实际情况定我这里只是提醒一句打包前先确认一下分发通道的限制。6.2 高频报错对照表下面这张表是我这几年攒下来的基本覆盖了现场能碰到的绝大多数情况现象根本原因处理方式提示已安装 4.8 或更高版本安装中止目标机框架版本高于安装包提供的版本4.x 拒绝降级安装判断逻辑改成下限比较达标就跳过安装提示时间戳签名无法验证Win7 SP1 缺少 SHA-2 代码签名支持先安装 KB4474419 和 KB4490628框架安装返回 1603旧版本残留、补丁缺失、权限不足或磁盘空间不够读取 %TEMP% 下 dd_ 开头的日志定位安装完成但程序闪退目标机框架版本低于编译目标检查判断逻辑是否被跳过确认 Release 读取正确程序报找不到方法或程序集引用了高版本框架才有的 API统一 csproj、app.config 和安装判断的版本号框架装完必须重启才能用安装返回 3010未提示用户处理 NeedsRestart在完成页给出重启提示安装包被安全软件拦截自解压可执行文件未签名或触发启发式规则对安装包做代码签名并向客户 IT 报备32 位程序读不到框架版本注册表被重定向到 Wow6432Node显式使用 Registry64 视图读取看这张表能发现一个规律一大半的问题根源都在“判断逻辑没做对”而不是安装本身有多难。把判断这关做扎实后面的问题会少很多。6.3 权限、签名与杀软安装包申请管理员权限这件事看起来是常识但确实有项目栽在这上面。如果安装包自身没有requireAdministrator清单而用户又不是管理员账户调用框架安装程序时会静默失败。表现是安装包一路跑完、什么都没报但程序就是起不来。判断办法很简单看安装目录下有没有框架安装留下的痕迹或者直接问用户安装过程中有没有出现 UAC 提权弹窗。代码签名是另一个容易被忽略的点。没有签名的安装包在 Win10 和 Win11 上会触发 SmartScreen 拦截用户得点“更多信息”再点“仍要运行”多两步操作还会降低信任感。更麻烦的是有些企业级安全软件会把带自解压结构的安装包当成可疑文件处理直接隔离。给安装包和主程序 exe 都做上代码签名能显著减少这类问题。签名证书用标准的代码签名证书即可签名时记得带时间戳否则证书过期后已发布的包会失效。提示签名之后安装包的哈希值会变如果内部有校验逻辑或者自动化分发系统记录了哈希记得同步更新。6.4 几个容易被忽略的细节第一个是临时目录的空间。Inno Setup 的{tmp}指向当前用户的临时目录通常在系统盘。安装时要把 110MB 的框架包释放到这里如果系统盘只剩几十 MB 空间ExtractTemporaryFile会失败报出来的错误信息还不一定指向磁盘空间。稳妥的做法是在PrepareToInstall里先检查可用空间不够就提前给出明确提示。第二个是路径里的中文和空格。安装目录默认是{autopf}\MyApp在中文系统上会展开成“C:\Program Files (x86)\MyApp”本身没问题。但如果你把框架安装包放在带中文的路径下再用命令行调用某些老版本的安装程序会解析失败。所以释放到{tmp}再用{tmp}的路径调用是比直接用相对路径更稳的做法这也是我在示例里坚持用ExpandConstant({tmp}\...)的原因。第三个是升级安装的场景。用户已经装了旧版本新版本安装包跑起来框架判断会跳过因为已经装好了文件替换会由CloseApplications处理。但如果旧版本正在运行且用户没保存数据强制关闭会引起投诉。我的做法是在安装开始前弹一个确认框提示“安装过程需要关闭正在运行的 MyApp请先保存数据”让用户自己确认而不是无声无息地把人家窗口关掉。第四个是卸载时的处理。安装时装了框架卸载时不要试图把框架卸掉。因为这台机器上的其他软件可能也在用同一个框架卸掉会引发连锁问题。安装包只需要卸载自己的文件框架留在系统里就好。这一点在给客户做说明文档的时候最好写清楚避免客户质疑“为什么卸载不干净”。7. 部署现场的几个小技巧框架包放进安装包之后还有一个经常被忽略的问题你怎么知道客户装成功了我现在的做法是让程序在启动时做一次自检把当前框架的 Release 值、CLR 版本、程序版本、启动时间写进一个日志文件放在程序目录下的logs文件夹里保留最近十个文件。现场出问题时让客户把这个日志发过来第一眼就能看出框架版本对不对省掉大量来回确认。另一个技巧是给安装包准备一个“诊断模式”。在安装命令后面加一个自定义参数比如/diag脚本里判断到这个参数就跳过所有安装动作只输出一份环境检测报告系统版本、已安装的框架版本、可用磁盘空间、注册表读取结果。这个功能在远程支持的时候特别有用客户运行一条命令把结果发过来比你远程桌面连过去查半天效率高得多。最后说一个心态上的事。打包部署这件事技术难度不高但细节极多而且每个客户的机器环境都不一样。我的经验是把每次现场遇到的问题都记下来归到那张速查表里下次打包前扫一遍。做了几年之后你会发现翻来覆去就那么十几个坑踩过一遍就能记住。真正让部署变顺的不是某个高深的技巧而是把判断逻辑写扎实、把提示信息写清楚、把日志留全让出了问题的人能自己找到线索。
返回列表