十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

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

Visual Studio Installer Projects实战:从零构建MSI安装包

Visual Studio Installer Projects实战:从零构建MSI安装包 1. 方案预评估Installer Projects到底解决什么问题做Windows桌面开发的迟早都会遇到一个灵魂拷问项目编译出来了bin目录下几十上百个文件有exe、dll、config、依赖的外部资源总不能让人家客户自己去挨个拷贝吧我见过不少团队项目开发期一切正常一到交付就整出幺蛾子——要么发给对方的压缩包漏了几个dll跑到客户机器上双击报错要么忘了带运行库对方电脑装了半天.NET环境最崩溃的是产品迭代了几轮安装包版本号、卸载逻辑、桌面快捷方式全是乱的客户卸载旧版本时弹一堆乱七八糟的提示。这些问题的根源只有一个没有用正规的安装包工程去管理交付物。Microsoft Visual Studio Installer Projects也就是大家常说的VS安装项目模板是微软官方提供的Visual Studio拓展专门用来生成Windows InstallerMSI格式的安装程序。它做出来的安装包有几个很实在的优点第一它是和Visual Studio深度集成的主项目的输出、依赖、项目引用、文件内容都是可视化管理的不用像NSIS、Inno Setup那样去写脚本配置适合大多数.NET平台的开发团队直接上手。第二生成的MSI格式天然支持Windows Installer服务这意味着标准化的安装、修复、卸载流程是系统级保证的不需要自己去做文件覆盖、注册表清理这种容易出事故的逻辑。第三它可以自动检测目标机器上的运行环境比如.NET Framework版本是否满足要求不满足就直接引导用户去装这块在InstallShield或者其他脚本方案里还得自己实现或者购买付费组件。当然它也有自己的短板东西相对古老界面不算现代功能上对于复杂的自定义操作比如安装时调用一段自定义逻辑、根据用户选择动态修改安装内容会比较吃力。但就“把一套.NET程序打包成能被普通用户正常安装、卸载、升级的MSI安装包”这件事来说它是最不需要费脑子的选择没有之一。这篇文章我会把整个流程走一遍从安装插件开始到创建项目、配置安装路径、设置快捷方式、检测运行库再到最终的编译验证所有细节都会摊开讲清楚包括我实际踩过的坑。如果你之前只是用过InstallShield或者一直手动拷贝分发文件建议从头到尾看一遍这里面的思路能帮你省掉不少交付环节的麻烦。2. 环境准备与项目前期配置2.1 安装Installer Projects扩展Installer Projects在Visual Studio里不是默认自带的模板第一次使用之前要先装扩展。以现在的Visual Studio 2022为例直接打开“扩展”——“管理扩展”在联机搜索框输入“Installer Projects”找到那个微软官方发布的“Microsoft Visual Studio Installer Projects”扩展下载安装后重启IDE即可。这里有个很关键的版本匹配问题扩展的版本要和VS版本对应上。VS 2019、VS 2022使用的扩展版本号是不同的虽然大体上都叫Microsoft Visual Studio Installer Projects但安装包的内部依赖有差异装错版本会出现模板不显示、甚至IDE直接报加载失败的情况。保险的做法是直接从VS的“管理扩展”里搜索安装而不要从第三方网站下载未经签名的插件包。注意公司内网或离线环境的开发者可以在微软官方的扩展市场页面手动下载VSIX安装包后缀是.vsix双击后Visual Studio会弹出安装界面同样需要关闭VS实例才能安装成功。安装完成后在“新建项目”对话框里搜索“Setup”就可以看到两个模板Setup Project和Setup Wizard。前者是直接创建一个空的安装项目所有内容手动配置后者是一个向导会引导你一步步选择项目输出、添加文件、配置快捷方式等。向导相对适合新手快速了解流程但实际项目里我用得更多的还是Setup Project因为自定义空间更大而且配置项分布在各节点里改起来更直观。2.2 为什么选择MSI/Setup项目而不是其他打包工具在进入实操前先花点时间说清楚方案选择的问题因为这个问题我几乎每次都会被问到。市面上的安装包方案并不少。NSIS和Inno Setup都是非常优秀的脚本式打包方案灵活度高、生成的安装包体积小尤其Inno Setup的安装包是做绿色软件分发的首选之一。但如果你的项目是一套标准的.NET应用有多个项目引用、有资源文件、有运行时依赖用脚本方案意味着你得自己去梳理组件清单、文件复制规则、卸载清理逻辑而这些恰恰是Setup Project模板里默认就替你考虑好的。用PlantUML或者图形化工具画个架构图你会清晰看到差异脚本方案是“用户自己启动一个可执行程序按照脚本来执行文件释放和注册表写入”而MSI方案是“用户启动WiX引擎Windows Installer服务自己解析MSI内部的数据库按事务机制执行整个部署”。后者多了一层系统级的事务保障中途安装失败、磁盘空间不足、权限不足这些异常情况下MSI可以自动回滚到之前的状态不会出现装到一半留下一堆垃圾文件的情况。当然InstallShield Limited Edition也是WinForms开发者经常接触到的另一条路线它曾经随VS一起提供。但说实话LimEd版本的配置界面比Installer Projects还老旧而且很多功能是付费版才开放的个人开发者和中小团队用起来有点憋屈。相比之下Installer Projects虽然功能朴素但该有的面面俱到学习成本也低得多——你不需要去理解Windows Installer的底层打包规范直接在VS里拖拖拽拽就能完成80%的需求。2.3 万事开头难确定安装包的目标框架和CPU架构这个环节非常容易踩坑但我先说结论在创建安装项目之前先把你的主项目目标框架和平台目标统一确定下来。什么叫“统一”举个例子你的主项目如果是基于.NET Framework 4.7.2的WinForms程序平台目标是x64那安装项目的“TargetPlatform”也要对应设置为x64下面安装的检测逻辑也要以x64为基准来配置。如果你在主项目的“平台目标”里选了“Any CPU”而安装项目选了x64那装到32位系统上虽然能装但运行时可能提示主程序兼容性问题反过来也有情况你主项目是Any CPU安装项目设成x86那装出来的程序集就是32位宿主托管内存超过2GB就会出OutOfMemory这是很多大内存应用容易漏掉的一个细节。最稳妥的做法从开发的第一天就用统一的配置比如统一用Release x64不仅意味着主程序的编译平台是x64安装项目、启动条件、注册表重定向、文件系统节点这三者之间的上下文也要一致。3. 创建安装项目的核心步骤实录3.1 从空项目开始添加项目输出而不是手动复制文件当你已经安装了扩展也规划好了目标框架接下来就可以正式创建安装项目了。我习惯的路径是在解决方案上右键——添加——新建项目选择“Setup Project”命名比如“MyAppSetup”确定后解决方案里就多出一个带四种编辑器节点文件系统、注册表、文件类型、用户界面的项目。这个项目本身不包含任何源文件它的所有内容都通过“添加”的方式从其他项目引用进来。在“文件系统”节点上右键——添加——项目输出对话框里可以选择你的主项目下拉框里会列出主项目的输出组比如“主输出”编译出来的exe/dll、“内容文件”、“源文件”、“调试符号”等。这里最关键的是勾选“主输出”和“内容文件”。实操心得不要手动一个个去添加exe、dll到安装项目里那样虽然最终也能生成安装包但版本更新时容易漏文件。直接引用项目输出的好处是每次重新编译主项目安装项目自动拿到的都是最新编译产物构建顺序也由编译器自动处理。我见过不少人第一次用这个功能时只添加主输出不加内容文件结果资源文件没进去程序装完直接报找不到图片、打不开配置文件。添加完之后展开“应用程序文件夹”你会看到主输出已经在这里生成了一个快捷方式样式的对象。注意这个对象集成的是项目输出的逻辑而不是一个静态文件路径它会在每次Build时被替代成实际的产品文件。这个细节是Setup Project最讨喜的地方也是和NSIS方案拉开差距的地方。3.2 配置安装目录、公司名和版本序列选中安装项目的根节点F4打开属性面板一长串属性里有几个是需要实际花心思填的ProductName最终生成安装包时显示的产品名称。注意这个名称同时会写进注册表的Add/Remove Programs或“程序和功能”列表里。Manufacture公司名同样是卸载列表里的组织字段也是默认安装目录的组成部分之一。Version安装包的版本号格式是x.x.x默认写的1.0.0。需要重点强调的是这个Version是在你进行“升级发布”时判断新旧的依据原则上只能增不能减一旦某个版本已经发布出去后面的版本号必须严格大于这个数。InstallAllUsers决定安装包是面向当前用户安装还是面向所有用户安装。默认是False也就是只装到当前用户目录不需要UAC提权如果你希望装到Program Files里把它改为True安装时机就会自动提示提权。绝大多数企业软件会改成True这样不会因为登录用户不同导致权限不一致。RemovePreviousVersions升级覆盖旧版本的关键开关。如果你发布了v1.0现在要更新到v1.1且这个值为True那么用户直接运行新安装包旧版本就会被自动卸载后再安装新版本。如果不打开这个开关你看到的就是安装包提示“另一个版本已安装”用户只能先手动卸载再安装体验极差。我遇到的比较常见的问题是把Manufacture填成了项目名称导致安装目录变成了C:\Program Files\MyProject...看着不专业更常见的是Version没有跟着产品迭代走导致客户端装了新包却识别不到更新。这个属性看似简单实际维护起来一定要有纪律性每一次发版前同步更新Version和ProductCode自动生成的不要手动改Release Notes里也保持一致。3.3 文件系统规划目录结构与必要的快捷方式安装包的“文件系统”节点是整个打包逻辑的骨架。默认情况下只有三个目标文件夹“应用程序文件夹”对应实际安装时的根目录比如C:\Program Files\YourCompany\YourProduct。“用户的‘程序’菜单”对应开始菜单的Programs目录。“用户桌面”对应桌面快捷方式。这三者的关系类似源目录和目标目录的映射左侧是“逻辑目录”右侧是“加入安装包的内容”。你一靠“添加项目输出”进来的文件其实已经挂在“应用程序文件夹”下了快捷方式对象也是挂在子节点下生成的。接下来的工作分两类一类是调整文件摆放。有些产品希望在根目录下按子文件夹归类比如把配置文件放在Config子目录里、把日志组件放在Log目录下或者把VC运行时库拆到Redist目录。这时你需要在“应用程序文件夹”上右键新建目录然后把文件拖到对应的目录节点里。注意拖动前要确认目标目录的“属性”里的“DefaultLocation”正确否则安装完会发现实际文件跑到了意外路径。另一类是创建快捷方式。在项目输出对象上右键创建主输出的快捷方式然后把这个快捷方式拖到“用户桌面”和“用户的‘程序’菜单”对应的目录下。创建完成后可以选中快捷方式查看属性修改它的Icon和DisplayName。这里的Icon选的是主项目exe里的图标资源如果没有内置图标资源安装后在桌面上就会显示成空白图标这是很多新手容易忽略的隐性坑。实操心得微软的MSI默认不会为每个用户的桌面都创建快捷方式如果InstallAllUsers为True且用户是通过层叠账户登陆的场景有些户桌面没有图标。这不是Setup Project的bug而是Windows Installer的权限模型本身如此。真要在多用户场景下保证所有用户都有快捷方式要么接受安装时默认创建的公共桌面快捷方式要么在自定义操作里配合PowerShell脚本处理。3.4 安装启动条件让程序在用户机器上乖乖跑起来安装包不光要把文件放对位置还要保证目标机器具备运行条件。这一块在“启动条件”编辑器里配置。最常见的条件是检测.NET Framework。如果你的主项目目标框架是.NET Framework 4.7.2那需要右键“启动条件”——添加“.NET Framework启动条件”。如果目标机器没有安装对应版本安装程序会提示并弹出下载链接引导用户去安装运行库。这个提示页面是MSI标准行为不需要额外写代码。第二种常见条件是检测系统版本。有些发布方限制只允许在Windows 10及以上版本安装这时候需要在“启动条件”里添加一个注册表搜索或操作系统的版本对比条件写起来稍微繁琐但实际场景中银行/政企项目里使用频率很高旧机器上跑新程序兼容性没法保证与其等到用户反馈各种诡异BUG不如在安装阶段就做好门槛拦截。注意事项启动条件的检测结果在安装包Uninstall的时候也会跟着跑一遍如果条件太严格可能导致卸载也失败。典型例子是某个版本升级后启动条件要求变为64位系统老用户在三十二位系统上卸载旧版本时因为条件不满足Windows Installer会拒绝卸载。这是个很隐蔽的历史包袱遇到后只能靠手动清理注册表。4. 界面配置、扩展功能与自更新方案4.1 定制安装界面脱离那种一眼假的原生UISetup Project默认提供的是老式的Windows Installer界面左侧蓝色横幅右侧字体居中偏上整体感觉停留在Windows XP时代。这个UI虽然丑但胜在稳定不依赖额外的UI渲染组件在各个Windows版本上都不会因为系统主题变化而错乱。如果你觉得这个界面无法满足商业软件的形象要求Installer Projects提供了一个可行的替代在“用户界面”编辑器中可以右键安装文件夹下各个对话框选择“添加对话框”然后调整显示顺序。但真正允许深度定制的地方微乎其微你无法修改对话框的布局只能在每个对话框右侧的“属性”里修改标题、横幅位图、提示文本。位图建议做成一张特定尺寸的BMP通常是500x320或类似的比例否则它会默认拉伸导致图像变形那种拉伸效果一眼看着就很廉价。如果对UI要求特别高也有两条路线一是用WiX Toolset的Burn引导程序来包一层现代化的安装引导二是直接提供一个前置的引导exe比如用.NET WinForms写在引导界面里完成产品介绍和许可协议勾选然后拉起真正的MSI安装过程。第二种方案成本更低而且能完全自定义品牌风格我几个对外发布的商业项目都是用这个思路改造的。4.2 文件关联、注册表与卸载信息管理Setup Project的四个编辑器里“注册表”和“文件类型”两个节点平时用得不多但偶尔需要用到时就特别关键。文件类型编辑器可以为安装包注册文件关联。比如你需要把.abc格式的文件默认关联到你的程序来打开右键根节点添加文件类型设置扩展名、ProgID、图标和打开方式。注意这里的“打开方式”不能直接选主输出exe只能选择这个安装项目里已经加入的文件引用这个细节如果没注意写完之后编译都会报错。注册表编辑器则直接映射目标机器上的注册表。在这个编辑器里写入的键值会跟随安装进程写入并在卸载时自动清理。需要特别小心的是64位系统下的注册表重定向——如果你安装项目TargetPlatform选的是x86那么安装时写入的HKLM\Software会被重定向到Wow6432Node节点下。如果你在代码里用RegistryKey读取的是64位视角会读到空值。这块不只是在安装包配置也牵连到代码的注册表读取逻辑建议统一从Environment.Is64BitProcess来判断是否打开WOW64视角。4.3 升级与Web安装场景自更新功能是桌面软件的安身立命之本。Installer Projects提供的最原始的升级方式就是前面提到的“RemovePreviousVersions Version递增”。这套机制足以支撑中小型产品的版本迭代但对大型产品来说远远不够因为你无法控制老客户从哪个版本升级也无法在升级过程中保留用户配置。我对升级配置的建议是先把产品的用户数据目录和安装目录分离。这就牵扯到安装目录的Location布局尽量把可执行文件放在Program Files把用户的可写配置数据库路径、日志路径、许可证文件放在ProgramData或者用户的AppData。否则升级版本一旦要卸载旧包会连带把配置也卸掉数据全没了这是所有人在没做配置分离前都会踩的大坑。Web安装场景也就是在网页里提供安装包下载直接触发安装的流量在这个领域里其实没有一个由Installer Projects原生支持的机制因为它生成的MSI本质上就是一个文件你可以放在任何静态资源服务器上下载。但如果你要做的是增量安装、按需下载功能模块那建议直接在代码里结合ClickOnce发布通道去做或者用Burn/BA引导加HTTP下载链的组合。5. 构建编译与安装包产物校验5.1 Debug/Release交叉正规禁止使用Debug配置发布在开始构建之前还有一个必须强调的规范化操作发布安装包时主项目和安装项目的配置都应该切换到Release模式。Debug模式编出来的程序集不仅包含大量调试信息通常也没有做代码优化性能和体积都不适合对外分发。更重要的是Debug模式可能输出额外的pdb、vshost等多个美术文件即便你做了项目输出引用也会被一并纳入安装包打包出来既臃肿又暴露了一些不必要的信息。实际操作中在解决方案配置管理器中把主项目配置改为Release同时把Setup项目的配置也改为Release然后编译整个解决方案。编译完成后Setup项目会在输出目录下生成两个文件一个是.msi安装包另一个是setup.exe引导程序。5.2 为什么有setup.exe和.msi两个文件这套双文件的组成常常被问到实际上它俩分工明确。MSI文件是真正的安装本体是Windows Installer可以直接执行的对象。如果你在命令行里执行msiexec /i 你的包.msi也能完成整个安装流程。setup.exe是一个引导器它的作用是检测目标机器是否已经具备Windows Installer运行环境某些精简版系统会阉割掉MSI引擎不具备的话会先装引擎然后才拉起MSI安装。它还有一点额外的能力比如根据当前系统架构自动从bootstrapper里选择64位还是32位的安装逻辑。所以对外发布时不要只发布MSI文件两个一起打包发布。用户双击setup.exe获得的体验最稳定。如果你图省事只发MSI遇到一台阉割过Windows Installer的机器装的时候就直接报错并且没有任何引导修复能力。实操心得如果公司内部有软件分发系统比如SCCM通常只需要集成MSI文件还可以配合msiexec参数做静默安装msiexec /i xxx.msi /qn。Setup.exe反而因为要先过一层引导在某些分发系统里没那么好用。你可以根据分发场景选择发布一个还是两个但默认都建议统一发布双文件。5.3 安装包产物验证三板斧构建完成之后不要急着发出去先做一个快速自查清单干净环境测试找一台没有装过.NET SDK、没有开发工具的裸机或虚拟机把安装包装一遍确认程序能正常启动。条件允许的话最好Win10/Win11各测一次。卸载链路测试安装完执行卸载然后去Program Files目录和ProgramData目录确认没有残留文件、注册表相关键值是否清理干净。升级测试如果这是v1.1及以后的包拿一台装了旧版本v1.0的机器直接覆盖安装确认数据保留、快捷方式正常、版本信息已经更新到新版本。这个三步验证我每次发布都会走一遍不夸张地说大部分发布事故都逃不出这几类问题。6. 常见问题与坑点修复实录6.1 构建报错“Unable to find vjsclnc.dll”和类似文件缺失这个错误通常出现在安装项目和主项目之间项目引用没有建立好时。出现这个问题的根本原因是你添加项目输出时对话框选择的是文件而不是项目输出或者在引用主项目时主项目路径存在变动导致引用失效。处理方式检查安装项目下“项目输出”节点的属性确认SourcePath指向正确的主项目输出路径。如果引用的主项目被删除或者改名重新添加。删除安装项目里的无效项目输出节点重新添加。6.2 安装时提示“另一个版本已安装”无法覆盖这个现象100%是RemovePreviousVersions或者Version设置有问题。如果是RemovePreviousVersions为False安装器会认为你已经在机器上安装了同一个产品并拒绝覆盖。解决方式是把RemovePreviousVersions改为True。 如果是Version没有递增安装器也会报同样错误因为MSI的ProductCode和PackageCode虽然不同但同一ProductCode下的UpgradeCode默认是自动生成的和版本号组合无法互相覆盖时会直接拒绝安装。注意有些老项目在发布初期是直接给客户手动覆盖文件的后期想接入MSI升级时因为旧版本根本没有记录在Windows Installer里此时无论你怎么设置RemovePreviousVersions都没有用旧版本不会被识别。这种只能接受“先卸载旧包再装新包”的过渡方案并在下一次迭代强制全部走安装包更新。6.3 x64系统下安装到了Program Files (x86)原因多半是安装项目的TargetPlatform被设置成了x86而主项目输出的是x64目标。MSI在安装到64位系统时如果标记为x86包会自动重定向目录到x86路径。解决方法是到安装项目属性里把TargetPlatform改为x64然后重新构建。注意修改后注册表、文件系统的重定向逻辑会一并变化需要重新测试一遍全部功能。还有另一个可能导致这个问题的点你的主项目平台目标尽管是x64但安装项目本身没有跟随。IDE里这种组合很容易因为“配置管理器”搞混默认新建Setup项目时它的平台始终跟随解决方案的Active solution platform如果solution是x86就会出问题。6.4 Prerequisite必备组件下载失败或进度条卡住Installer Projects默认在官方构建时会把.NET Framework等必备组件标记为“从供应商网站下载”。在目标机器能联网的情况下这没问题但如果目标机器内网隔离安装时就会卡在下组件。解决办法安装项目属性中有“Prerequisites”按钮点击后可以选择“从与应用程序相同的位置下载必备组件”。你先把这些组件文件下载好放到安装项目目录下的某个文件夹发布时一并打包即可。这块对需要给客户离线部署的场景是刚需中的刚需晚些时候再踩就晚了。6.5 自定义操作Custom Action导致“Installation ended because the custom action failed”自定义操作是Installer Projects里比较进阶的用法但也是绝大多数安装失败的头号元凶。它本质上允许你在安装的不同阶段去调用你自己写的一段程序通常是exe或dll来完成额外的安装逻辑比如创建数据库、写配置文件、启动Windows服务等。问题多数出现在安装权限上——MSI在InstallAllUsersTrue时安装进程是以System权限运行的。这时候如果你的自定义操作程序还尝试访问当前用户比如写HKCU注册表就会失败因为System账户没有当前用户的注册表加载区。更坑的是MSI自定义操作失败后整次安装会被回滚你根本看不到自己写的日志日志还没写完就被清理了。我的建议是能不用自定义操作就不用尽量把需要在安装时干的活挪到程序第一次启动时去处理lazy initialization。这样安装进度稳定用户也不需要在装到99%时看着进度条卡住不知所措。如果实在必须用比如注册Windows服务保证自定义操作程序自身不依赖用户上下文并支持静默模式运行。7. 工程化落地把安装包构建接入团队流程最后从工程流程的角度聊聊怎么把安装包这块管起来。很多人觉得“打包嘛就是发版前点一下编译”但正经团队里这往往是最容易出幺蛾子的环节之一。首先安装项目文件.vdproj默认是单向转换的它适合跟随主项目一起纳入版本控制。团队协作中我一般要求每个成员在本地构建用Debug就行但所有正式发布的安装包统一在CI服务器上由Release配置生成。这样可以避免“我这机器上能装他那机器上装不了”的诡异现象因为CI环境的依赖库和生成路径是完全一致的。其次安装包产物的版本信息要和主程序版本联动起来。我在实践中会在CI脚本里先构建主项目然后读取生成的主exe的程序集版本号用脚本更新vdproj里的Version字段再调用devenv进行最终构建。这样能确保安装包版本和程序集版本永远保持一致不会出现安装包写的是1.2.0程序里面实际是1.1.5这种低级错误。实操心得vdproj是个文本格式的工程文件理论上可以用脚本来修改其中的Version但有风险因为里面有些GUID字段是生成时根据内容算出来的。我建议只修改version相关的字段不要动ProductCode、PackageCode这些唯一标识否则会引发前面提到的覆盖安装失效问题。另外每次正式发版我会把生成的MSI和setup.exe统一放到一个带日期和版本号的发布服务器目录里同时记录一份构建说明包括.NET版本、平台目标、包含的组件版本等信息。这个习惯看起来不起眼但在线上问题回溯时价值巨大——经常遇到客户报“装完闪退”这种问题一看构建说明就知道是哪一批次构建受影响而不是回消息让人家先重装一遍。如果团队里有人想进一步自动化Visual Studio命令行工具devenv.exe /build可以命令行方式构建安装项目PowerShell脚本调用起来没有任何障碍。接入Azure DevOps或GitLab CI基本就是加一个编译任务步骤的事难度不高但能明显提升交付效率。打包这件事表面上只是生成两个文件实际上承载的是软件交付责任的最后一公里。一个可靠的安装包能让客户从拿到包到用上软件的耗时缩短到分钟级也能让售后团队免去大量“排环境”的加班时间。把这块做好值得下点功夫。
返回列表