WiX Toolset v3 实战:编写可共享的 Bundle 包清单(Package Manifest)完整指南

原创2026-10-03 16:39:40264 阅读
文章标签:开发工具构建工具

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)。注意其中还体现了两个高级技巧:

  1. 用内置变量做安装条件:<MsiPackage Name='data\wix64.msi' ... InstallCondition='VersionNT64' />——仅当运行在 64 位系统时(VersionNT64 变量被定义)才安装 64 位 MSI;
  2. 用 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 引擎消费:

  1. 编译(candle):Compiler.cs 的 ParseChainPackage 统一解析所有链式包元素,无论它挂在 <Chain> 还是 <PackageGroup> 下;包级公共属性与扩展属性在此被转换为 Burn 内部表结构;
  2. 绑定(light):Binder 按 SourceFile 收集载荷、按 DownloadUrl 生成远端载荷引用,并按需计算 InstallSize;
  3. 检测(detect):引擎启动后按类型分派检测(见前文 core.cpp),MSI/MSP 通过 Windows Installer API 探测,EXE/MSU 依赖作者写的 DetectCondition 表达式;
  4. 求值(evaluate):所有条件(DetectCondition、InstallCondition、搜索 Condition)统一由 Burn 表达式引擎求值,底层入口为 ConditionEvaluate(condition.cpp),并可通过 IBootstrapperEngine::EvaluateCondition 从托管/原生引导程序应用中按需调用(EngineForApplication.cpp);
  5. 计划与执行(plan/apply):依据检测结果与条件结果,Burn 生成安装/修复/卸载计划并执行,Vital 属性决定失败时是回滚整链还是继续(可配合 <Chain> 的 DisableRollback、DisableSystemRestore、ParallelCache 属性调整行为,见 wix.xsd 第 525-576 行)。

九、延伸阅读

登录后查看全文
wix3