WiX Toolset v3 实战:编写可共享的 Bundle 包清单(Package Manifest)完整指南
WiX Toolset v3 实战:编写可共享的 Bundle 包清单(Package Manifest)完整指南
Burn(WiX Toolset 的引导引擎)允许你将多个独立的安装包(MSI、EXE、MSP、MSU)串成一个统一的安装链。要让任意一个包被 Bundle 消费,必须先为它编写一段“包清单(Package Manifest)”作者定义。本文以仓库文档 authoring_bundle_package_manifest.html.md 为核心,系统讲解包清单的两种组织方式、四种包类型、核心属性、内置变量与搜索条件的使用,并深入仓库源码与真实构建脚本验证其底层原理。读完本文,你将能独立编写可共享、带安装条件、可被多 Bundle 复用的包定义。
一、什么是包清单:包定义的两种放置位置
在 WiX 的 Bundle 作者定义中,每个包(Package)都需要一份描述自身的定义,这份定义就是“包清单”。它既可以直接写在 <Chain> 元素内部,也可以放进**<Fragment> 中的 <PackageGroup> 里**,再由 Bundle 通过 <Chain> 内的 <PackageGroupRef> 引用。后一种方式的优势在于:同一份包定义可以在不同的 Bundle 之间共享,避免重复作者和维护。
从 XSD 层面看,<Chain> 与 <PackageGroup> 的子元素集合完全一致(均允许 MsiPackage、MspPackage、MsuPackage、ExePackage、RollbackBoundary、PackageGroupRef),这一约束定义在 src/tools/wix/Xsd/wix.xsd 中(Chain 见第 525 行附近,PackageGroup 见第 1017 行附近,PackageGroupRef 见第 1040 行附近)。
两种方式的最小骨架如下:
<?xml version="1.0"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Bundle>
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense" />
<!-- 方式一:包定义直接放在 Chain 下 -->
<Chain>
<ExePackage SourceFile="path\to\MyPackage.exe"
DownloadUrl="http://example.com/?mypackage.exe"
InstallCommand="/q /ACTION=Install"
RepairCommand="/q ACTION=Repair /hideconsole" />
</Chain>
</Bundle>
</Wix>
<?xml version="1.0"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Bundle>
<BootstrapperApplicationRef Id="WixStandardBootstrapperApplication.RtfLicense" />
<!-- 方式二:引用共享的包定义 -->
<Chain>
<PackageGroupRef Id="MyPackage" />
</Chain>
</Bundle>
</Wix>
这两种写法在编译器中的处理路径是统一的:无论是 Compiler.cs 中解析 <Chain> 时(ParseMsiPackageElement、ParseMspPackageElement、ParseMsuPackageElement、ParseExePackageElement,见第 21236-21249 行),还是解析 <PackageGroup> 时(第 22065-22078 行),最终都汇入同一个 ParseChainPackage 方法(第 21462 行)。也就是说,包定义放在哪里只是作者层面的组织问题,语义完全一致。完整的 Chain 用法示例可参考同目录文档 bundle_author_chain.html.md。
二、WiX 支持的四种链式包类型
WiX schema 支持以下四种链式包,每种对应一个顶层元素,声明位置均位于 src/tools/wix/Xsd/wix.xsd:
| 元素 | 说明 | XSD 位置 |
|---|---|---|
<MsiPackage> |
描述单个要安装的 MSI 包 | wix.xsd 第 578 行 |
<ExePackage> |
描述单个要安装的 EXE 包 | wix.xsd 第 752 行 |
<MspPackage> |
描述单个 MSP(Windows Installer 补丁)包 | wix.xsd 第 652 行 |
<MsuPackage> |
描述单个 MSU(Windows Update 独立安装包)包 | wix.xsd 第 704 行 |
这四种类型共享一个名为 ChainPackageCommonAttributes 的属性组(wix.xsd 第 863 行),因此多数公共属性(SourceFile、Id、InstallCondition、Vital、Permanent 等)对所有包类型通用;而每类包又有自己的专有属性与子元素(如 ExePackage 的 InstallCommand、MsiPackage 的 MsiProperty 子元素、MspPackage 的 Slipstream、MsuPackage 的 KB)。
在 Burn 引擎的检测(detect)阶段,引擎会按包类型分派到各自的检测实现,见 src/burn/engine/core.cpp 第 288-322 行的 switch (pPackage->type) 分支:ExeEngineDetectPackage(exeengine.cpp)、MsiEngineDetectPackage(msiengine.cpp)、MspEngineDetectPackage(mspengine.cpp)、MsuEngineDetectPackage(msuengine.cpp)。这也解释了为什么 DetectCondition 只在 ExePackage 与 MsuPackage 上出现——Windows 本身不提供“检测某个 EXE/MSU 是否已安装”的机制,必须由作者用条件表达式告知引擎。
三、实战一:在可共享 Fragment 中编写 ExePackage
原文档给出了一个最典型的可共享包定义:把一个 EXE 包封装进 <Fragment> 的 <PackageGroup> 中,使任何 Bundle 都能通过 PackageGroupRef 复用:
<?xml version="1.0"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<PackageGroup Id="MyPackage">
<ExePackage
SourceFile="[sources]\packages\shared\MyPackage.exe"
DetectCondition="ExeDetectedVariable"
DownloadUrl="http://example.com/?mypackage.exe"
InstallCommand="/q /ACTION=Install"
RepairCommand="/q ACTION=Repair /hideconsole"
UninstallCommand="/q ACTION=Uninstall /hideconsole" />
</PackageGroup>
</Fragment>
</Wix>
逐项解读这些属性的真实语义(依据 wix.xsd 中的 ChainPackageCommonAttributes 与 ExePackage 声明):
SourceFile:要加入 Bundle 的包文件位置。SourceFile与Name至少指定其一;若不指定Name,默认取SourceFile的文件名。示例中的[sources]是预处理器变量(通常在.wixproj或.wxi中定义,指向共享源文件目录)。DetectCondition:判断该包是否已存在于目标系统的条件表达式,可使用内置变量和搜索返回的变量。由于 Windows 不提供检测 EXE 是否存在的方法,这个条件是 Burn 决定“安装时是否跳过本包”的依据:如果条件为假或省略,且 Bundle 正在执行安装,Burn 就会执行该包。参考 wix.xsd 第 780-790 行的注解。DownloadUrl:包的下载 URL。该属性支持三个占位符:{0}替换为包 Id、{1}替换为 Payload Id、{2}替换为 Payload 文件名(见 wix.xsd 第 882-893 行)。这意味着一个包既可以随 Bundle 分发,也可以在 Bundle 中只放下载元数据、按需从远端获取。InstallCommand/RepairCommand/UninstallCommand:安装、修复、卸载时传递给 EXE 的命令行参数。若InstallCommand缺省,则不带任何参数启动可执行文件;RepairCommand若设置为空字符串表示“支持修复但无需特殊参数”,若省略该属性则代表“该包不支持修复”;若不想让包被卸载,可结合Permanent="yes"阻止卸载(见 wix.xsd 第 791-809 行)。
仓库中就有大量真实的生产级 ExePackage 作者范例,例如 src/ext/NetFxExtension/wixlib/NetFx452.wxs 中的 .NET Framework 4.5.2 可再发行包定义,它在上述属性之外还使用了 PerMachine="yes"(要求提升权限执行)、Vital="yes"(失败则终止整个链)、Permanent="yes"(不可卸载)、Protocol="netfx4"(支持扩展进度/错误报告协议)、LogPathVariable="NetFx452FullLog"(把日志路径写入变量)、Compressed="no"(作为外部载荷),并通过 RemotePayload 声明远端文件的哈希与证书信息。这些属性组合展示了包清单在真实场景中的完整写法。
四、实战二:PackageGroupRef 共享机制的仓库实证
“共享包定义”并非纸上谈兵——本仓库自带的 WiX Toolset 安装引导程序就是典型用例。看 src/Setup/Bundle/Bundle.wxs:
<Bundle Name='!(loc.ShortProduct) v$(var.WixVersion)' ...>
...
<Chain>
<PackageGroupRef Id='NetFx462Redist' />
<PackageGroupRef Id='WixPackages' />
</Chain>
</Bundle>
其中 WixPackages 组定义在 src/Setup/Bundle/PackageGroups.wxs 的 <Fragment> 中,包含多个 MsiPackage(core.msi、wix64.msi、msdk.msi 以及按 VS 版本条件编译的 nsdk*.msi)。注意其中还体现了两个高级技巧:
- 用内置变量做安装条件:
<MsiPackage Name='data\wix64.msi' ... InstallCondition='VersionNT64' />——仅当运行在 64 位系统时(VersionNT64变量被定义)才安装 64 位 MSI; - 用
MsiProperty把 Burn 变量传给 MSI:<MsiProperty Name='INSTALLFOLDER' Value='[InstallFolder]' />,把 Bundle 侧计算的安装目录变量注入 MSI 的 INSTALLFOLDER 属性(MsiProperty的定义见 wix.xsd 第 1063 行,注意 Burn 保留ACTION、ALLUSERS、REBOOT、REINSTALL、REINSTALLMODE等属性不允许作者覆盖)。
此外,PackageGroups.wxs 中还用 util:RegistrySearch + util:DirectorySearch 查找上一次安装目录并回填 InstallFolder,这正是下一节“搜索定义变量”的仓库内实例。测试侧同样可见:test/data/Burn/DependencyTests/BundleA.wxs 等依赖测试 Bundle 都用 ExePackage 构建链,验证了包清单在实际 Burn 测试中的广泛使用。
五、实战三:用内置变量构造 InstallCondition
包清单的另一大主题是安装条件(InstallCondition)。原文档演示了如何给上面的 ExePackage 追加一个“仅在 x86 的 Windows XP 及以上安装”的条件:
<?xml version="1.0"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
<Fragment>
<PackageGroup Id="MyPackage">
<ExePackage
SourceFile="[sources]\packages\shared\MyPackage.exe"
DetectCondition="ExeDetectedVariable"
DownloadUrl="http://example.com/?mypackage.exe"
InstallCommand="/q /ACTION=Install"
RepairCommand="/q ACTION=Repair /hideconsole"
UninstallCommand="/q ACTION=Uninstall /hideconsole"
InstallCondition="NOT VersionNT64 AND VersionNT >= v5.1" />
</PackageGroup>
</Fragment>
</Wix>
InstallCondition 的语义(wix.xsd 第 920-924 行):仅在条件求值为真时安装该包;若条件为假且 Bundle 正在执行安装/修复/修改,该包反而会被卸载。这一点在规划升级/降级逻辑时必须格外注意。
条件表达式可用的变量来自 Burn 的“内置变量”体系,完整清单见仓库文档 bundle_built_in_variables.html.md。按用途分类,最常用的有:
- 系统版本类:
VersionNT(操作系统版本,形式为v#.#.#.#的版本变量)、VersionNT64(64 位系统的版本,32 位系统下未定义)、ServicePackLevel、NTProductType、TerminalServer、RebootPending、NativeMachine(v3.14 起,仅 Windows 10 TH2 及以上设置)、ProcessorArchitecture; - 路径类:
WindowsFolder、SystemFolder、System64Folder、ProgramFilesFolder、ProgramFiles64Folder、CommonFilesFolder、AppDataFolder、LocalAppDataFolder、CommonAppDataFolder、TempFolder、DesktopFolder、StartMenuFolder等(对应 CSIDL 明确定位文件夹); - Bundle 自身类:
WixBundleName、WixBundleManufacturer、WixBundleVersion、WixBundleTag、WixBundleProviderKey、WixBundleInstalled、WixBundleElevated、WixBundleUILevel、WixBundleOriginalSource、WixBundleOriginalSourceFolder、WixBundleLog_[PackageId](包日志路径变量)等; - 环境与用户类:
ComputerName、LogonUser、Privileged(进程能否提升运行)、SystemLanguageID、UserLanguageID、UserUILanguageID、InstallerName、InstallerVersion、VersionMsi。
其中 VersionNT 与其他 MSI 属性中的同名属性有本质区别:Burn 中它是版本类型变量(BURN_VARIANT_TYPE_VERSION),支持最多四段版本号 <a href="https://link.gitcode.com/i/11df765ceaaa21d410767b7398ece1a7" target="_blank">Major].[Minor].[Build].[Revision],因此条件写作 VersionNT >= v5.1 而非整数比较。这一点在引擎实现中有直接体现:src/burn/engine/variable.cpp 中版本变量以 MAKEQWORDVERSION 打包成 64 位四段值存储,并以 '%hu.%hu.%hu.%hu' 的格式输出(见 [variable.cpp 第 1597-1598 行、第 1664-1665 行);而条件的实际求值发生在 Burn 引擎的 ConditionEvaluate(condition.cpp),Bootstrapper 应用也可通过 IBootstrapperEngine::EvaluateCondition 主动求值任意表达式(EngineForApplication.cpp)。
提示:想针对特定 Windows 主版本/次版本编写条件时,可查阅操作系统版本对照表;示例
NOT VersionNT64 AND VersionNT >= v5.1即表示“非 64 位系统且版本不低于 5.1(Windows XP)”。
六、实战四:通过搜索定义自定义变量并驱动条件
仅靠内置变量往往不够——你还需要探测目标机器上的自定义状态(如某注册表键是否存在、某文件是否存在)。Burn 的解决方案是搜索(Search):把搜索结果的“值”存入变量,再用变量构造条件。搜索全部定义在 WiXUtilExtension 中,支持的搜索类型见仓库文档 bundle_define_searches.html.md:
| 搜索元素 | 用途 | XSD 位置(util.xsd) |
|---|---|---|
util:RegistrySearch |
搜索注册表 | 第 1012 行 |
util:FileSearch |
搜索文件 | 第 319 行 |
util:DirectorySearch |
搜索目录 | 第 170 行 |
util:ComponentSearch |
搜索 MSI 组件 | 第 110 行 |
util:ProductSearch |
搜索已安装的 MSI 产品 | 第 828 行 |
所有搜索元素共享 SearchCommonAttributes 属性组(util.xsd 第 1478-1499 行):
Variable(必填):存放搜索结果的变量名;Id:搜索的标识,用于排序和依赖;After:指定本搜索在其他某个搜索(按其 Id)之后执行——搜索之间可以互相依赖;Condition:本搜索的执行条件,若求值为假则整个搜索不执行。
原文档中的依赖式搜索示例(第二个搜索的注册表路径引用了第一个搜索的结果变量 [UniqueId]):
<?xml version="1.0"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Fragment>
<util:RegistrySearch Id="Path"
Variable="UniqueId"
Root="HKLM"
Key="Software\MyCompany\MyProduct\Unique Id\Product"
Result="Value" />
<util:RegistrySearch
Variable="patchLevel"
Root="HKLM"
Key="Software\MyCompany\MyProduct\[UniqueId]\Setup\PatchLevel"
Result="Exists"
After="Path" />
</Fragment>
</Wix>
要点说明:
Root的合法值为HKLM、HKCU、HKCR、HKU(util.xsd 第 1022-1050 行);Result="Value"把注册表值原文存入变量,Result="Exists"只返回是否存在;Format="raw"与Format="compatible"可控制 REG_DWORD 等值是否按 Windows Installer 格式(#1)返回(第 1061-1078 行);- 由于搜索属于 UtilExtension,必须在
<Wix>根元素上声明xmlns:util="http://schemas.microsoft.com/wix/UtilExtension",并在编译时引用 WixUtilExtension。
把搜索与包定义组合在一个 Fragment 中,就得到一份“完全自洽、可共享”的包清单——这也是原文档的收尾示例:
<?xml version="1.0"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi"
xmlns:util="http://schemas.microsoft.com/wix/UtilExtension">
<Fragment>
<util:RegistrySearch Id="Path"
Variable="UniqueId"
Root="HKLM"
Key="Software\MyCompany\MyProduct\Unique Id\Product"
Result="Value" />
<util:RegistrySearch
Variable="patchLevel"
Root="HKLM"
Key="Software\MyCompany\MyProduct\[UniqueId]\Setup\PatchLevel"
Result="Exists"
After="Path" />
<PackageGroup Id="MyPackage">
<ExePackage
SourceFile="[sources]\packages\shared\MyPackage.exe"
DownloadURL="http://mywebdomain.com/?mypackage.exe"
InstallCommand="/q /ACTION=Install"
RepairCommand="/q ACTION=Repair /hideconsole"
UninstallCommand="/q ACTION=Uninstall /hideconsole"
InstallCondition="x86 = 1 AND OSVersion >= v5.0.5121.0 AND patchLevel = 0" />
</PackageGroup>
</Fragment>
</Wix>
这里的 InstallCondition 同时使用了搜索变量(patchLevel = 0,表示“未打过该补丁”)与自定义变量(x86、OSVersion,通常由 util:RegistrySearch 等其他搜索产生),展示了“搜索 → 变量 → 条件”的完整数据流。
七、公共属性速查:ChainPackageCommonAttributes
无论是哪种包类型,以下通用属性都可用(定义于 wix.xsd 第 863-1016 行的 ChainPackageCommonAttributes 属性组):
| 属性 | 默认值 | 说明 |
|---|---|---|
SourceFile / Name |
互推 | 包来源文件 / 链内目标文件名,至少指定其一,Name 不允许 .. 目录 |
Id |
Name 清理后的标识 | 包标识,用于排序与交叉引用 |
After |
前一个兄弟包 | 指定本包在哪个包之后安装,注意避免成环 |
InstallCondition |
空 | 安装前置条件,假则跳过/卸载 |
InstallSize |
自动计算 | 安装后占用字节数,默认由 Binder 扫描(MSI 的 File 表、EXE 的 Payloads)得出 |
DownloadUrl |
无 | 下载地址,支持 {0} 包 Id、{1} Payload Id、{2} 文件名占位 |
Cache |
yes | 是否缓存包(还可设为 always) |
CacheId |
无 | 缓存时使用的标识 |
DisplayName / Description |
从包元数据提取 | 写入引导程序应用数据清单的显示信息 |
LogPathVariable / RollbackLogPathVariable |
WixBundleLog_[Id] / WixBundleRollbackLog_[Id] |
存放日志路径的变量名(MSU 默认不产生日志) |
Permanent |
no | 是否允许被卸载(设为 yes 则不可卸载) |
Vital |
yes | 失败时是否终止整条链并回滚 |
Compressed |
跟随 Bundle | 载荷嵌入容器还是作为外部载荷 |
SuppressSignatureVerification |
见说明 | 是否跳过签名校验,默认用哈希校验;显式设为 no 且包带 Authenticode 签名时才改用签名(v3.9 起默认值变更,见 wix.xsd 第 993-1004 行的历史说明) |
各类型专有属性:
ExePackage:DetectCondition(检测存在性)、InstallCommand/RepairCommand/UninstallCommand、PerMachine(默认 no,需提升执行时设为 yes)、Protocol(none/burn/netfx4等通信协议,用于扩展进度与错误报告);MsiPackage:DisplayInternalUI(默认 no,统一由引导程序接管 UI)、EnableFeatureSelection(是否允许控制 MSI 功能选择)、ForcePerMachine(强制按每机器安装)、Visible(是否在“程序和功能”中显示该 MSI,默认 no);子元素MsiProperty(把引擎表达式写入 MSI 属性)与SlipstreamMsp(安装本 MSI 时顺带安装补丁);MspPackage:DisplayInternalUI、PerMachine、Slipstream(是否自动为链中所有目标 MSI 顺带打补丁,默认 no);MsuPackage:DetectCondition、KB(知识库编号,指定后才能在 Windows 7 及以上卸载 MSU,否则 MSU 通常只能由Permanent控制是否卸载)。
八、包清单的编译与引擎执行链路
了解作者语法后,再看包清单如何被 WiX 工具链与 Burn 引擎消费:
- 编译(candle):
Compiler.cs的ParseChainPackage统一解析所有链式包元素,无论它挂在<Chain>还是<PackageGroup>下;包级公共属性与扩展属性在此被转换为 Burn 内部表结构; - 绑定(light):Binder 按
SourceFile收集载荷、按DownloadUrl生成远端载荷引用,并按需计算InstallSize; - 检测(detect):引擎启动后按类型分派检测(见前文 core.cpp),MSI/MSP 通过 Windows Installer API 探测,EXE/MSU 依赖作者写的
DetectCondition表达式; - 求值(evaluate):所有条件(
DetectCondition、InstallCondition、搜索Condition)统一由 Burn 表达式引擎求值,底层入口为ConditionEvaluate(condition.cpp),并可通过IBootstrapperEngine::EvaluateCondition从托管/原生引导程序应用中按需调用(EngineForApplication.cpp); - 计划与执行(plan/apply):依据检测结果与条件结果,Burn 生成安装/修复/卸载计划并执行,
Vital属性决定失败时是回滚整链还是继续(可配合<Chain>的DisableRollback、DisableSystemRestore、ParallelCache属性调整行为,见 wix.xsd 第 525-576 行)。
九、延伸阅读
- 包清单的上下文与骨架:authoring_bundle_skeleton.html.md
- 包如何串入 Bundle 的两种写法:bundle_author_chain.html.md
- 内置变量完整清单与逐条说明:bundle_built_in_variables.html.md
- 搜索与变量定义详解:bundle_define_searches.html.md
- 引导程序应用作者指南:authoring_bundle_application.html.md
- XSD 权威定义:wix.xsd(Bundle/Chain/Package 元素)与 util.xsd(搜索元素)
- 仓库内真实示例:src/Setup/Bundle/Bundle.wxs、src/Setup/Bundle/PackageGroups.wxs、src/ext/NetFxExtension/wixlib/NetFx452.wxs