ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

C#上位机离线交付:内嵌.NET Framework 4.8与静默安装

C#上位机离线交付:内嵌.NET Framework 4.8与静默安装 在客户的车间里交付一套 C# 上位机最尴尬的不是程序有 Bug而是你双击图标之后屏幕上弹出一句需要安装 .NET Framework 4.8 或更高版本。内网隔离、没有外网、现场只有一台不能随便联网的工控机这时候你翻遍 U 盘才发现自己只拷了程序目录没拷运行时。这个场景我遇到过不止一次后来就彻底改成了把所有东西都打进一个安装包C# 程序、依赖库、配置文件、以及 .NET Framework 离线安装包本身插上 U 盘双击一个 exe从零到能跑全程不需要外网。这篇文章讲的就是这套交付方式怎么落地。它不是那种打开 Visual Studio右键项目选择发布的入门介绍而是把打包部署里最容易被忽略的一环——运行时怎么跟着走——拆开讲透。涉及的核心内容包括.NET Framework 为什么不能像 .NET 8 那样自包含、怎么判断目标机器到底缺哪个版本、Inno Setup 和 WiX Burn 两条主流路线的完整脚本、以及静默安装返回 3010 之后到底该怎么办。适合正在做桌面软件交付、工控上位机、内部工具分发的开发者也适合被用户装不上框架这个问题折磨过的运维同学。1. 把运行时塞进安装包之前先认清 .NET Framework 的身份1.1 它不是 DLL是一套写进系统的地基很多人第一次做这件事时的直觉是把 .NET Framework 相关的 dll 拷到程序目录旁边不就完了这个思路在 .NET Core 之后的时代是对的但在 .NET Framework 上行不通。原因在于 .NET Framework 的运行时CLR并不是普通的托管程序集它包含大量非托管组件需要注册 COM 组件、写注册表、把mscoree.dll挂到系统加载链上并且会被多个进程共享。换句话说它是操作系统级别的共享组件而不是你的程序私有的一份拷贝。微软给 .NET Framework 的定位一直是系统组件所以它的安装方式只有一种通过官方安装程序或 Windows 功能装到系统里全机器共享。你在Program Files里看不到它因为它藏在C:\Windows\Microsoft.NET\Framework64\v4.0.30319这种系统目录下。这个特性直接决定了本文所有方案的形态你没办法随身携带框架只能随身携带框架的安装程序。安装包里放的是一个 100MB 左右的 exe安装时由它自己去完成系统级部署。这一点想通了后面所有设计的取舍就都好理解了。顺带说一句如果你现在还有选择权用 .NET 8 发布成 self-contained 单文件确实能彻底绕开这个问题dotnet publish -r win-x64 --self-contained出来就是一个自包含目录。但存量项目、必须用 WinForms/WPF 某些老控件库、或者依赖只支持 .NET Framework 的第三方 SDK 的场景短期内还是得走老路。1.2 目标机器上的三种状态对应三种处理策略在做检测逻辑之前我习惯先把目标机器分成三类因为不同类别的处理方式完全不同机器状态典型特征应该采取的策略完全没有框架干净的 Windows 7 SP1 或重装过的系统必须内嵌离线包静默安装装了旧版本有 4.5.2 或 4.6.1低于程序要求升级安装覆盖到 4.8已有 4.8 或更高Windows 10 1903 之后基本自带跳过安装直接复制程序文件第一类和第二类合起来是绝大多数问题来源。Windows 10 从 1903 版本开始内置 .NET Framework 4.8Windows 11 22H2 之后自带 4.8.1所以新机器上基本不会缺——但工控行业大量还在跑 Windows 7 SP1 和 Windows 10 早期版本这些机器就是重灾区。判断策略的关键点在于别写没装就装这种粗逻辑要写低于某个 Release 值就装。因为 4.8 的安装程序在一个已经装了 4.8 的机器上跑会直接弹窗告诉你这台计算机中已经安装了 .NET Framework 4.8 或版本更高的更新然后退出。用户看到这个弹窗会以为装错了体验非常差。1.3 必须接受的代价体积、时间和签名把框架打进去不是没有成本的。.NET Framework 4.8 的离线安装包英文版ndp48-x86-x64-allos-enu.exe大约 110MB中文版体积相近。Inno Setup 用 lzma2 压缩后整个安装包大概在 55MB 到 75MB 之间——注意压缩率并不高因为微软的这个 exe 内部本来就是 CAB 压缩过的自解压包你压不出太多水分。安装时长也要提前有个心理预期。在一台机械硬盘的老机器上装 .NET Framework 4.8 单独就要 2 到 5 分钟加上文件复制整个安装过程可能接近 8 分钟。如果你在工控现场交付最好在安装界面上放一行明确的提示文字告诉用户正在安装运行时组件请勿关闭否则八成会有人在第三分钟的时候以为卡死了然后强杀进程——那样留下的就是一堆半成品状态。还有一个容易被忽视的点是数字签名。你打包出来的整体安装包最好用代码签名证书签一下。原因不是炫耀而是当你把一个 110MB 的微软 exe 包进自己的安装包里整个文件的哈希就变了SmartScreen 会把它当成未知发布者。签名之后这个问题会缓和很多。2. 先量准尺寸你的程序到底依赖哪个版本2.1 从编译输出里读出真实的 TFM 和 supportedRuntime判断目标框架最可靠的地方不是你的记忆而是编译产物本身。C# 桌面项目的.csproj里会写TargetFrameworkVersionv4.8/TargetFrameworkVersion这个值就是项目的最低运行要求。但更值得看的是编译输出的App.config它会生成类似这样的内容configuration startup supportedRuntime versionv4.0 sku.NETFramework,Versionv4.8 / /startup /configurationsku里的版本号就是运行时加载器会去核对的目标版本。如果目标机器上装的框架低于它程序启动时会直接报错连Main函数都进不去。我一般会养成一个习惯在打包脚本里读一遍这个文件把版本号提取出来而不是靠人肉记忆。因为项目做久了中途改过目标框架却忘了同步安装包里的运行时版本这种事发生频率高得惊人。2.2 用反编译工具确认第三方库的最低框架要求你自己的项目是 4.8不代表第三方库也是。常见的情况是主项目 4.8但引用的某个老 SDK 是 4.0 编译的反过来更麻烦——某个库标称支持 4.5实际用了 4.6.2 才有的 API。用 ILSpy 或 dotPeek 打开对应的 dll看程序集的TargetFrameworkAttribute这是最直接的办法。命令行下也可以用 PowerShell 快速批量扫一遍Get-ChildItem .\bin\Release -Filter *.dll -Recurse | ForEach-Object { try { $asm [Reflection.Assembly]::ReflectionOnlyLoadFrom($_.FullName) $attr $asm.GetCustomAttributesData() | Where-Object { $_.AttributeType.Name -eq TargetFrameworkAttribute } [PSCustomObject]{ File $_.Name Framework if ($attr) { $attr[0].ConstructorArguments[0].Value } else { 未知 } } } catch { [PSCustomObject]{ File $_.Name; Framework 非托管或加载失败 } } } | Format-Table -AutoSize扫出来的结果里取最高版本那就是你安装包真正需要满足的下限。这个动作花不了五分钟但能避免你把框架装完了程序还是跑不起来的尴尬。2.3 Release 值对照表代码里判断版本的正确姿势写检测逻辑时不能靠读版本号字符串因为 .NET Framework 的注册表里没存4.8这种字符串。正确的做法是读HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full下的Release这个 DWORD 值它是个数字越大版本越新。常用的对照关系如下这张表我建议直接贴到你的代码注释里框架版本最低 Release 值Win10/11其他系统上的值4.53783893783894.5.23798933798934.63932953932974.6.23948023948064.74607984608054.7.24618084618144.85280405280494.8.1533320533325判断逻辑就是一句简单的不等式Release 528040就认为满足 4.8 的要求。用最低值做阈值是因为同一版本在不同 Windows 版本上的 Release 值不同但都大于等于表中的最小值用最小值做判断永远不会漏。有一个坑必须提醒在 64 位系统上32 位的安装程序读HKLM会被重定向到WOW6432Node。.NET Framework 的 NDP 键在两个视图里通常都有但为了万无一失我在 Inno Setup 里会显式判断系统位数再决定读哪个视图代码在后面第 4 节给出。2.4 3.5 和 4.0 这类老框架情况完全不一样如果你的程序依赖 .NET Framework 3.5比如用了某些只支持 2.0 运行时模型的老组件处理方式跟 4.x 是两码事。Windows 8 之后3.5 变成了按需功能Feature on Demand不能再像以前那样直接跑安装程序。在 Windows 10/11 上启用 3.5 的常规做法是用 DISM 从系统安装镜像的sources\sxs目录里取源文件dism /online /enable-feature /featurename:NetFx3 /All /Source:D:\sources\sxs /LimitAccess这意味着如果你的目标机器里有 Windows 10/11 需要装 3.5你的安装包里除了 3.5 的离线包还得考虑带上 sxs 源目录——那个体积又是几百 MB 起步。更现实的做法是尽量避免依赖 3.5把项目升级到 4.x。因为 4.0 本身已经在较新的系统上不再被支持继续留在 3.5 只会让交付越来越难。3.5 的检测方式也简单读这个键HKLM\SOFTWARE\Microsoft\NET Framework Setup\NDP\v3.5看Install这个 DWORD 是不是 1。注意这个键在 64 位系统上只存在于 32 位视图读的时候要用HKLM32。3. 四条打包路线怎么选我为什么最后落在 Inno Setup3.1 Visual Studio Installer Projects原生但引导能力弱如果你在 Visual Studio 里装了 Microsoft Visual Studio Installer Projects 扩展就能新建一个 Setup Project。它支持在 Prerequisites 里勾选 .NET Framework 4.8然后选择从与我的应用程序相同的位置下载系统必备组件再把离线包放进指定目录。这条路线的优点是官方出品心里踏实缺点是生成的是 MSI setup.exe 的组合界面非常朴素安装过程中的自定义能力很差——你几乎没法在安装前弹一个友好的版本检测提示也做不了复杂的条件分支。而且它需要在每台开发机上装扩展CI 服务器上配置起来也麻烦。小项目做一次交付可以批量维护不推荐。3.2 Inno Setup脚本可控、单文件输出中小项目的性价比之王Inno Setup 是我这些年最常用的方案原因有三条。第一它是脚本驱动的。整个安装行为写在一个.iss文本文件里可以进版本管理可以做 diff可以在 CI 里参数化替换版本号。这比在图形界面里点来点去可靠太多。第二它的[Code]段是完整的 Pascal 脚本能写条件判断、能调外部进程、能读注册表、能自定义安装流程。第 4 节那个安装前先检查框架版本低了就静默装装完再确认一遍的完整逻辑就是靠它实现的。第三输出是单一 exe用户拿到就是一个文件不用管旁边有没有别的目录。它唯一的门槛是你要接受 Pascal 语法。但其实用到的就那几个函数看一遍例子就会了。3.3 WiX Burn企业级链式安装的正规军WiX Toolset 的学习曲线陡得多但如果你需要管理一个多个前置条件 多个 MSI的复杂安装链它是唯一正经的答案。它的 Burn 引擎专门用来做 bundle可以在一个 exe 里串起任意多个安装包每个包有自己的检测条件和安装命令失败还能回滚。企业内部分发、需要走软件资产管理系统、或者安装过程要做成无人值守 日志上报的场景WiX 更合适。代价是调试起来比较痛苦XML 写错一个属性可能只是静默不生效没有任何报错。3.4 商用工具花钱买时间Advanced Installer、InstallShield 这类工具把上面所有事情都图形化了包括前置条件检测、离线包内嵌、多语言界面、自动更新。如果你所在团队对安装包的交付质量有硬要求又不想在脚本上花时间买个授权是划算的。它们的核心能力和 WiX 是同一层的只是包了一层好用的壳。3.5 选型对照表维度Setup ProjectInno SetupWiX Burn商用工具上手成本低中高低内嵌离线运行时支持支持支持支持自定义安装逻辑弱强强强单文件输出是是是是CI 友好度一般高高中适合场景一次性小工具中小项目主力复杂企业分发有预算的团队我的实际选择是中小项目一律 Inno Setup需要串多个 MSI 的复杂场景才上 WiX。下面两节把两条路线都给出可运行的样例。4. Inno Setup 落地从离线包准备到静默安装全流程4.1 离线安装包从哪来、放哪、怎么校验离线包建议从官方下载中心获取选 Offline installer 那个版本文件名形如ndp48-x86-x64-allos-chs.exe中文或ndp48-x86-x64-allos-enu.exe英文。微软允许把 .NET Framework 可再发行组件随你的应用程序一起分发所以放进安装包没有授权问题。下载完之后一定要校验哈希因为这是个 100MB 级别的文件网络传输损坏的概率并不低而且损坏的表现是安装到一半失败非常难排查。校验完之后把它放进项目的一个prereq目录MyProject/ ├─ installer/ │ └─ setup.iss ├─ prereq/ │ └─ ndp48-x86-x64-allos-chs.exe └─ dist/ - 程序发布输出prereq目录要不要进 Git我的做法是不进因为 100MB 的二进制文件会让仓库迅速膨胀。改成写一个fetch-prereq.ps1脚本构建时自动下载并校验哈希这样既保证了可复现又不污染仓库。4.2 检测不装重复不装错版本检测逻辑的核心就是读 Release 值但要注意视图问题。下面这段 Inno Setup 代码把 64 位和 32 位两个视图都试一遍哪个读得到用哪个const NET48_RELEASE 528040; function GetNetRelease(): Cardinal; var Value: Cardinal; begin Result : 0; if IsWin64 then begin if RegQueryDWordValue(HKLM64, SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full, Release, Value) then Result : Value; end; if Result 0 then begin if RegQueryDWordValue(HKLM32, SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full, Release, Value) then Result : Value; end; end;注意HKLM64和HKLM32这两个常量需要 Inno Setup 6.0 及以上版本。如果你还在用 5.x得换回HKLM加InstallIn64BitMode的写法会比较别扭建议直接升级。用一个独立的函数封装检测的好处是它可以在安装流程里被调用两次一次在开始安装前判断要不要装框架一次在装完之后验证是否真的装上了。第二次验证非常重要因为静默安装是有可能失败的如果不验证就往下走你会在用户那边得到一个安装成功但程序打不开的诡异现象。4.3 用 PrepareToInstall 而不是 [Run]这一行决定了失败时会不会留烂摊子这是我最想强调的一个实操点。很多教程会告诉你把 .NET Framework 的安装命令写到[Run]段里用Flags: runhidden waituntilterminated。这样确实能跑起来但顺序是错的——[Run]段是在文件复制完成之后执行的也就是说框架装没装上你的程序文件已经先落到目标目录里去了。如果框架安装失败比如用户点了取消或者系统版本不支持结果就是程序装了一半快捷方式已经建好了用户双击打开报错然后来找你。卸载重装也未必干净。正确的做法是把框架安装放到PrepareToInstall里。这个函数在 Inno Setup 开始复制文件之前被调用返回空字符串表示继续安装返回非空字符串则中止安装并把这个字符串作为错误信息展示给用户。这样一来框架装不上你的程序文件就一个都不会被复制系统保持原样。function PrepareToInstall(var NeedsRestart: Boolean): String; var ResultCode: Integer; ExePath: String; begin Result : ; if GetNetRelease() NET48_RELEASE then Exit; // 已经满足要求直接放行 ExePath : ExpandConstant({tmp}\ndp48-x86-x64-allos-chs.exe); if not FileExists(ExePath) then begin Result : 未找到 .NET Framework 4.8 离线安装包安装无法继续。; Exit; end; if not Exec(ExePath, /q /norestart, , SW_HIDE, ewWaitUntilTerminated, ResultCode) then begin Result : 无法启动 .NET Framework 4.8 安装程序。; Exit; end; case ResultCode of 0: ; // 成功 1641, 3010: NeedsRestart : True; // 成功但需要重启 1602: Result : 已取消 .NET Framework 4.8 的安装。; 5100: Result : 当前操作系统不支持 .NET Framework 4.8请先升级系统。; else Result : Format(.NET Framework 4.8 安装失败退出码 %d。, [ResultCode]); end; if (Result ) and (GetNetRelease() NET48_RELEASE) then Result : 安装程序执行完毕但仍未检测到 .NET Framework 4.8请手动安装后重试。; end;那段case里的退出码是实测中最常遇到的几个3010和1641都表示成功了但要重启这两个绝不能当成失败处理。5100是系统版本不支持这种情况只能让用户升级操作系统。把这几行写进代码比事后去猜为什么安装到一半停了要省事太多。4.4 完整的 setup.iss 骨架把上面这些拼起来一份能直接用的脚本大概长这样。我把关键项都加了注释[Setup] AppId{{9A2B7C4D-1111-4222-8333-ABCDEF012345} AppNameMyHmi AppVersion1.0.0 AppPublisherMyCompany DefaultDirName{autopf}\MyHmi DefaultGroupNameMyHmi UninstallDisplayIcon{app}\MyHmi.exe OutputDirOutput OutputBaseFilenameMyHmi_Setup_1.0.0 Compressionlzma2/max SolidCompressionyes PrivilegesRequiredadmin WizardStylemodern ArchitecturesInstallIn64BitModex64compatible MinVersion6.1sp1 [Files] Source: ..\dist\*; DestDir: {app}; \ Flags: ignoreversion recursesubdirs createallsubdirs Source: ..\prereq\ndp48-x86-x64-allos-chs.exe; DestDir: {tmp}; \ Flags: deleteafterinstall [Icons] Name: {group}\MyHmi; Filename: {app}\MyHmi.exe Name: {autodesktop}\MyHmi; Filename: {app}\MyHmi.exe; Tasks: desktopicon [Tasks] Name: desktopicon; Description: 创建桌面快捷方式; \ GroupDescription: 附加任务 [Run] Filename: {app}\MyHmi.exe; Description: 立即运行 MyHmi; \ Flags: nowait postinstall skipifsilent几个细节值得单独说一下。PrivilegesRequiredadmin是必须的因为装 .NET Framework 需要管理员权限。{tmp}目录用来临时存放离线包配合deleteafterinstall标志安装结束后 Inno 会自动清理不会在用户机器上留一个 110MB 的垃圾文件。MinVersion6.1sp1是给 .NET Framework 4.8 划的系统下限比这个还老的系统连框架都装不上不如直接在启动时就拦住。SolidCompressionyes会让编译时间明显变长我实测过一次大概三到五分钟但压缩率会有几个百分点的提升。如果你在 CI 上跑可以考虑关掉它换取构建速度。4.5 体积与单文件输出的取舍打包完之后你会看到一个 60MB 到 80MB 的 exe。这个体积在 2024 年其实不算什么但如果你的用户是通过某种带宽受限的内网渠道获取安装包可能就得考虑取舍了。第一种取舍是分成两个包主程序包几 MB和运行时包70MB。主程序包在检测到框架已满足时完全不下载运行时。这需要你的分发渠道支持按需拉取复杂度上去了。第二种取舍是只打包框架的 Web 安装器ndp48-web.exe只有 1MB 多代价是目标机器必须能上外网。这个方法在办公室环境很好用在隔离的工业现场就是废的。第三种思路是改用 4.6.2 或者 4.7.2 的离线包如果你只是图个框架版本别太老低版本的离线包体积会小一些但功能上不会有区别。我的经验是先搞清楚交付渠道再决定体积策略。内网 U 盘交付就老老实实打全量离线包别折腾有内部分发平台且带宽稳定就上双包方案。5. WiX Burn需要串多个前置条件时的组织方式5.1 Bundle / Chain / ExePackage 这三件套的关系WiX 的 Burn 模型其实很直观。一个Bundle就是最终生成的那个 exeChain是它要按顺序执行的一串安装包ExePackage和MsiPackage是链条上的具体节点。每个节点都可以带自己的检测条件和安装参数。一个最小可用的 bundle 大概是这样这里是 WiX v4 的 schemav3 主要是命名空间和BootstrapperApplicationRef的写法不同Wix xmlnshttp://wixtoolset.org/schemas/v4/wxs xmlns:balhttp://wixtoolset.org/schemas/v4/wxs/bal xmlns:utilhttp://wixtoolset.org/schemas/v4/wxs/util Bundle NameMyHmi Version1.0.0.0 ManufacturerMyCompany UpgradeCodePUT-YOUR-GUID-HERE BootstrapperApplication bal:WixStandardBootstrapperApplication ThemertfLicense / /BootstrapperApplication Chain ExePackage IdNetFx48 SourceFileprereq\ndp48-x86-x64-allos-chs.exe InstallCommand/q /norestart PerMachineyes Vitalyes DetectConditionNetFx48Release gt; 528040 / MsiPackage SourceFileMyHmi.msi Vitalyes / /Chain /Bundle Fragment util:RegistrySearch IdNetFx48Search VariableNetFx48Release RootHKLM KeySOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full ValueRelease Resultvalue Bitnessalways64 / /Fragment /WixVitalyes表示这个包必须成功失败就整个中止——框架包必须设成这个。DetectCondition引用的是上面RegistrySearch输出的变量读注册表拿到 Release 值跟 528040 比。Bitnessalways64是让搜索强制走 64 位视图。5.2 DetectCondition 写错的典型症状这个属性写错的表现特别有迷惑性安装过程看起来一切正常但框架包被跳过了。原因是检测条件返回真Burn 就认为已经装好了不用装。常见的写错方式有三种。第一种是忘记加Resultvalue默认行为只判断键存不存在而NDP\v4\Full这个键在很老的框架上也可能存在于是就被误判成已满足。第二种是Bitness没设32 位的 bootstrapper 去读WOW6432Node下的键读到一个空值或者旧值。第三种是 XML 里的符号没转义成gt;这个错误编译期会报反而是最好发现的。调试这类问题有个小技巧在ExePackage上加LogPathVariable或者干脆临时把InstallCommand改成带/passive的可见模式看看框架安装窗口到底有没有弹出来。弹出来了说明检测条件没生效没弹说明条件写对了。5.3 一个 Bundle 里同时处理 3.5 和 4.8如果程序确实需要两个框架比如某个第三方报表控件依赖 3.5主程序是 4.8在 Chain 里加两个ExePackage就行顺序按依赖关系排。但要注意 3.5 在 Windows 10/11 上不能用 exe 装得用 DISM这在 Burn 里要写成带参数的ExePackage指向dism.exeExePackage IdNetFx35 SourceFileC:\Windows\System32\dism.exe InstallCommand/online /enable-feature /featurename:NetFx3 /All /LimitAccess /Source:[SourceDir]sxs DetectConditionNetFx35Installed Vitalno /Vitalno是因为 3.5 装不上时程序或许还能降级运行不至于整个安装失败。但这种能跑但功能不全的状态最难排查我倾向于还是设成Vitalyes宁可装不上让用户明确知道也别留下一堆说不清的问题。6. 实测踩到的坑以及它们为什么难查6.1 静默安装返回 3010 之后系统处在什么状态3010 的含义是安装成功但需要重启才能完成。.NET Framework 安装到一半需要重启的情况并不罕见尤其是从很老的版本升级上来的时候。危险的地方在于返回 3010 之后注册表里的 Release 值可能已经更新了。也就是说如果你只是简单地检查 Release 值会以为一切正常然后继续装程序。但实际的运行时文件可能还没完成替换程序启动后会报一个非常难懂的异常。我的处理方式是如果返回 3010 或 1641除了设置NeedsRestart还要在安装完成页上加一句明确的提示引导用户重启。Inno Setup 里可以在[Setup]段声明RestartIfNeededByRunno然后用[Code]里的NeedRestart()配合自己的提示逻辑控制得更细。6.2 杀软误报单文件打包的副作用把一堆 exe 打成一个 exe你其实做了一次自解压。这个行为模式和一些恶意软件是重合的所以国内几个主流杀软对 Inno Setup 生成的包有一定概率报可疑行为。缓解办法有几个用带签名证书的SignTool对最终 exe 签名在 Inno 的[Setup]段里加上AppPublisherURL、AppSupportURL这些元信息如果预算允许在发布前把包提交到各家的白名单申诉渠道。这些都是体力活但一次做过之后后续版本只要签名证书不变一般不会再被拦。6.3 老系统上的 SHA-2 依赖这个坑专门针对 Windows 7 SP1。微软从 2019 年开始用 SHA-2 签名而 Windows 7 SP1 默认只认 SHA-1。所以在一个没打过补丁的 Win7 SP1 上你运行 .NET Framework 4.8 的安装包它会直接报签名验证失败。解决办法是先装KB4474419和KB4490628这两个补丁。但这两个补丁的安装本身又需要其他前置补丁形成一条依赖链。我最后的处理方式是把 Win7 的支持范围收窄到已打全补丁的机器在安装程序启动时检测系统版本如果是 Win7 SP1 且缺少 SHA-2 支持直接弹一个说明文档的链接让现场 IT 先把系统补丁打齐。这比在安装包里塞一整套补丁链现实得多。6.4 {tmp} 目录的残留问题用{tmp}存放离线包是很自然的做法但有个细节deleteafterinstall这个标志是在安装流程正常结束时才清理的。如果用户在安装中途点了取消或者安装失败中止这个文件就留在临时目录里了。大部分情况下这不是大问题因为临时目录迟早会被系统清理。但如果用户连续安装多次都失败你的 110MB 文件就会在磁盘上留好几份。稳妥一点的做法是在[Code]里加一个清理函数在DeinitializeSetup的时候主动删一次procedure DeinitializeSetup(); var Target: String; begin Target : ExpandConstant({tmp}\ndp48-x86-x64-allos-chs.exe); if FileExists(Target) then DeleteFile(Target); end;6.5 卸载时该不该动框架明确一个原则卸载你的程序时不要卸载 .NET Framework。理由很实在。第一目标机器上可能有其他程序也在用这个框架你卸掉就把别人搞挂了。第二卸载框架需要重启用户体验很差。第三框架本身占的那点磁盘空间在今天的硬盘容量面前不值一提。在 Inno Setup 里只要你不主动写[UninstallRun]去调用框架的卸载命令它就不会动。这是默认行为但值得在团队里形成共识免得有人觉得装了什么就得卸载什么而多写几行代码。7. 装完之后怎么验收出问题怎么定位7.1 一份可以直接照着做的验收清单每次发布新版本之前我都会在一台干净的虚拟机里跑一遍下面这套流程。虚拟机最好是快照状态验完就回滚保证每次都是全新机器。检查项操作期望结果无框架环境全新 Win7 SP1 快照运行安装包自动静默装框架全程无人工干预旧版本升级装 4.6.2 后运行安装包升级到 4.8不弹已安装更高版本已有 4.8Win10 1903 上运行跳过框架安装耗时明显缩短中途取消在框架安装阶段关掉安装程序程序目录未被创建无残留快捷方式卸载重装卸载后再装一次无重复项无报错中文路径安装到含中文和空格的目录程序能正常启动第三项和第六项是最容易被忽略的。已有 4.8 的场景如果检测逻辑写错会浪费时间在无谓的框架安装上中文路径出问题多数是因为安装脚本里某个地方没加引号路径一有空格就断开了。7.2 用日志定位失败到底在哪一环Inno Setup 支持/LOG文件名参数生成安装日志日志里会记录每一步的动作和返回码。给现场运维配一个带日志的快捷方式比在电话里问你当时看到什么提示了高效太多。MyHmi_Setup_1.0.0.exe /LOG%TEMP%\MyHmi_install.log /SILENT如果失败发生在框架安装这一环还可以让 .NET 的安装程序自己输出日志。它的命令行参数里支持/log加上一个路径ndp48-x86-x64-allos-chs.exe /q /norestart /log %TEMP%\netfx48.log这个日志会详细记录框架安装过程中每一个组件的状态能精确定位到是哪个组件失败了。我遇到过一次是目标机器上的 Windows Update 服务被组策略禁用导致的失败日志里写得很清楚但在安装界面上只显示一个笼统的错误码。7.3 给现场运维做一个小诊断工具装在程序目录里的第三方库版本更换、系统环境变化都可能让一个原本好用的程序突然打不开。与其每次远程排查不如在主程序里加一个命令行参数输出一份环境诊断报告using Microsoft.Win32; using System; using System.Reflection; static void DumpEnv() { Console.WriteLine($OS: {Environment.OSVersion}); Console.WriteLine($64-bit OS: {Environment.Is64BitOperatingSystem}); Console.WriteLine($CLR: {Environment.Version}); Console.WriteLine($Exe: {Assembly.GetEntryAssembly()?.Location}); using var baseKey RegistryKey.OpenBaseKey( RegistryHive.LocalMachine, RegistryView.Registry64); using var ndp baseKey.OpenSubKey( SOFTWARE\Microsoft\NET Framework Setup\NDP\v4\Full); var release ndp?.GetValue(Release); Console.WriteLine($NET Release: {release ?? not found}); }现场运维只要把这段输出截图发过来我基本就能判断问题出在哪一层。这个函数不到二十行但在实际支持中省下的沟通成本非常高。我甚至把它做成了快捷方式的一个隐藏入口右键属性里能看到完整的启动参数。有一点要注意这个诊断函数本身依赖 CLR 能启动。如果程序连 CLR 都加载不起来它自然是不会执行的。所以更稳妥的做法是把同样的检测逻辑用 PowerShell 写一份放在安装目录里作为check-env.ps1它不依赖 .NET Framework 的任何版本。如果交付的目标机器数量不多还有一种更省事的办法在安装程序的完成页上放一个复制诊断信息的按钮用[Code]里的Clipboard.SetText把检测结果直接放进剪贴板让用户粘贴给你。这个小功能我加过之后支持效率提升得比预想中明显。
返回列表