使用WiX创建Windows应用安装包

参考:官方教程

WiX 工具集(简称 WiX)用于构建 Windows 安装程序,它是构建工具、运行时工具和库的集合,不只是制作基本的安装包,还可以安装IIS网站、创建SQL Server、在Windows防火墙中注册例外。

安装 Wix 工具集

  • 无Visual Studio:用命令行安装 Wix 工具集。

    bash 复制代码
    dotnet tool install --global wix
  • 有Visual Studio:安装 HeatWave for VS2022 扩展。安装后,重启 Visual Studio,你将看到可用的新项目模板。


添加MSI Package项目

添加 Wix 安装项目。在 解决方案资源管理器 中右键单击解决方案,然后选择" 添加新 > 项目

选择MSI Package,下一步,命名项目为<产品名称>.Installer


WiX项目介绍

HeatWave中的 MSI Package (WiX v4) 模板给出了4个文件:

  • Package.wxs
  • Folders.wxs
  • ExampleComponents.wxs
  • Package.en-us.wxl

文件扩展名

HeatWave为我们生成的文件有两个扩展:

  • .wxs : WiX源文件。
  • .wxl : WiX本地化文件。

Package.wxs文件

WiX源文件是XML文件,根元素就是WiX,命名空间是 http://wixtoolset.org/schemas/v4/wxs。所有WiX源文件的根元素和命名空间都是相同的。

Package 元素

Package元素表示MSI包,只能有一个Package元素。HeatWave模板填写了所有MSI包所必需的属性,例如:

xml 复制代码
<Package Id="TODO_Manufacturer.Installer" 
    Name="Installer" 
    Manufacturer="TODO Manufacturer" 
    Version="1.0.0.0">
  • Id 属性设置包的全球唯一ID,建议以公司名称作为前缀。在新开发项目中用此属性代替UpgradeCode,不再手动设置UpgradeCode
  • Name 属性设置包的名称。该名称是在Windows Installed apps 列表中显示的名称。
  • Manufacturer 属性设置软件的公司的名称。此字符串也显示在Windows Installed apps 列表中。
  • Version 属性设置包的版本。包版本是管理包升级的重要部分。

MajorUpgrade 元素

xml 复制代码
<MajorUpgrade
  DowngradeErrorMessage="!(loc.DowngradeError)" />

当你安装一个更高的版本时,之前的版本会首先被删除。

  • DowngradeErrorMessage 属性指定一个消息,当用户试图安装一个比已经安装的版本低的版本时显示。!(loc.DowngradeError) 语法是对本地化字符串的引用。
  • AllowSameVersionUpgrades 属性设置为no(默认)时,具有相同版本和UpgradeCode,但产品代码不同(产品代码每次构建时自动更新)的产品被视为两个不同产品,将被安装为两个应用。当设置为yes时,将被视为同一个产品,重复安装将视为升级。
  • IgnoreLanguage 属性设置为no(默认)时,不同语言的安装包将被安装为不同应用,设置为yes时,安装不同语言安装包将被视为升级。

Feature元素

Feature元素表示控制安装的内容。

xml 复制代码
<Feature Id="Main">
  <ComponentGroupRef Id="ExampleComponents" />
</Feature>

ComponentGroupRef 元素

ComponentGroupRef元素表示引用具有相同Id的ComponentGroup元素,等同于把ComponentGroup元素填充到Feature元素。

另一种引用方式是通过属性引用,例如ComponentGroup元素有一个Directory属性,它的值是所引用的Directory元素的ID,表示Directory元素指定的目录作为组件组中所有文件的父目录。

xml 复制代码
<ComponentGroup Id="ExampleComponents" Directory="INSTALLFOLDER">

Folders.wxs文件

Fragment元素

Fragment元素中的内容用于被引用。单个 .wxs 文件中可以包含多个 Fragment

StandardDirectory元素

xml 复制代码
<StandardDirectory Id="ProgramFiles6432Folder">

StandardDirectory 元素使示标准目录之一作为包目录的父目录,通过Id属性指定标准目录,可用的ID参考,其中有些是MSI系统文件夹属性ID,而有些不是,例如ProgramFiles6432Folder,它会根据安装包的位数变为标准MSI目录,例如,对于32位包,将解析为ProgramFilesFolder,对应路径C:\Program Files (x86),对于64位包,将解析为ProgramFiles64Folder,对应路径C:\Program Files

Directory 元素

xml 复制代码
<Directory Id="INSTALLFOLDER"
  Name="!(bind.Property.Manufacturer) !(bind.Property.ProductName)" />

Directory 元素将创建一个新目录:

  • Id属性指定被引用时的ID。
  • Name是目录的名称。
  • !(bind.Property.Manufacturer)表示引用Package 元素的Manufacturer属性。
  • !(bind.Property.ProductName)表示Package 元素的Name属性。

等效于:

xml 复制代码
<Directory Id="INSTALLFOLDER"
  Name="TODO Manufacturer WixTutorialPackage" />

ExampleComponents.wxs文件

File元素

表示一个文件。

xml 复制代码
<File Source="ExampleComponents.wxs" />
  • Source属性指定文件的名称。
  • 示例项目是把自身的ExampleComponents.wxs文件当作安装文件,实际需改成应用程序。

Package.en-us.wxl文件

这是一个本地化文件,可以添加多个本地化文件。

xml 复制代码
<WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="en-US">
  
</WixLocalization>
  • 由于本地化文件不是 WiX 源文件,因此它们使用不同的根元素和命名空间。
  • Culture 属性指定本地化使用的语言和区域。
xml 复制代码
<String Id="DowngradeError" 
	Value="A newer version of [ProductName] is already installed." />
  • 本地化文件主要包含一堆字符串。每个字符串都有一个 id 和一个值,该值是指定的区域性中本地化的字符串。

支持在任何硬编码字符串的地方引用本地化字符串,例如,在 Package.wxs 中使用 DowngradeError 字符串为 MajorUpgrade 元素提供错误消息:

xml 复制代码
<MajorUpgrade
  DowngradeErrorMessage="!(loc.DowngradeError)" />

实操

以下例子为MyApplication项目创建安装包:

对需要生成安装包的项目创建发布:

添加项目引用

在MSI Pachage项目中,添加对 MyApplication 项目的引用。右键单击 解决方案资源管理器 中的安装项目,然后选择" 添加 > 项目引用"。

项目现在应包括一个 ProjectReference 元素,如下所示:

xml 复制代码
<Project Sdk="WixToolset.Sdk/6.0.0">
  <ItemGroup>
    <ProjectReference Include="..\MyApplication\MyApplication.csproj" />
  </ItemGroup>
</Project>

添加项目引用起到以下作用:

  • 确保应用项目在包项目之前生成,以便在生成包时应用本身可用。
  • 为包项目提供某种方法来查找应用项目的输出。

修改 Package 文件

修改Package元素的Id属性Name属性和Manufacturer属性。

嵌入Cabinet 文件

MSI Package项目构建后生成三个文件:

  • cab1.cab
  • WixTutorialPackage.msi
  • WixTutorialPackage.wixpdb

.cab是一个Cabinet 文件,包含了被安装的程序,与.msi文件组成了.msi包。.wixpdb文件与.pdb 文件一样,是一个调试文件,不是实际.msi包输出的一部分,可以被忽略。

WiX 的默认设值是将 cabinet 文件保持在 .msi 文件的外部,改变设置可以将 cabinet 文件嵌入到 .msi 文件本身中。在 WiX 中,MediaTemplate 元素控制如何生成和嵌入cabinet文件。在Package元素中添加MediaTemplate 元素,并设置EmbedCab属性为yes,如下所示:

xml 复制代码
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
  <Package
	Id="EdgerockConcepts.WixTutorialPackage"
    Name="WixTutorialPackage"
    Manufacturer="Edgerock Concepts"
    Version="1.0.0.0">

    <MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" 
				  AllowSameVersionUpgrades="yes"
				  IgnoreLanguage="yes"/>

    <MediaTemplate EmbedCab="yes" />

    <Feature Id="Main">
      <ComponentGroupRef Id="AppComponents" />
    </Feature>
  </Package>
</Wix>

重命名 ExampleComponents

ExampleComponents是模板的示例,应该重命名为合适的名称。

以下位置需要重命名:

  • ExampleComponents.wxs重命名为AppComponents.wxs

  • 打开AppComponents.wxs文件,将ComponentGroup的ID修改为AppComponents

    xml 复制代码
    <ComponentGroup Id="AppComponents" Directory="INSTALLFOLDER">
  • 打开Package.wxs文件,更新 ComponentGroupRef 的 ID:

    xml 复制代码
    <Feature Id="Main">
      <ComponentGroupRef Id="AppComponents" />
    </Feature>

修改安装文件引用

打开AppComponents.wxs文件,修改内容如下:

xml 复制代码
<Component>
  <File Source="MyApplication.exe" />
</Component>

WiX为每个引用项目的输出目录添加到绑定路径,绑定路径是 WiX 搜索打包的文件的路径。因此只需填写应用程序文件名,而不用填详细路径。

虽然不需要,但也可以指定绝对路径或相对路径,并且可以使用WiX预处理器变量引用具有相同名称的MSBuild 属性。例如以下示例中的$(Configuration)根据不同的解决方案配置获得路径:

xml 复制代码
<Component>
  <File Source="..\MyApplication\bin\$(Configuration)\MyApplication.exe" />
</Component>

Configuration是一个预处理器变量

File元素只添加单个文件。通常应用程序包含多个文件,添加多个文件需使用Files元素,如下:

xml 复制代码
<ComponentGroup Id="AppComponents" Directory="INSTALLFOLDER">
  <?define PublishDir = "$(MyApplication.ProjectDir)\bin\Release\net8.0\publish" ?>
  <Files Include="$(PublishDir)\**" />
</ComponentGroup>

如果要保留某个文件卸载时不删除,比如配置文件,通过给Component元素添加Permanent="yes",表明此Component卸载时不被删除:

xml 复制代码
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
	<Fragment>
		<ComponentGroup Id="AppComponents" Directory="INSTALLFOLDER">
			<?define PublishDir = "$(MyApplication.ProjectDir)\bin\Release\net8.0\publish" ?>
			<Files Include="$(PublishDir)\**">
				<Exclude Files="$(PublishDir)\appsettings.json"/>
			</Files>
			<Component Permanent="yes">
				<File Source="$(PublishDir)\appsettings.json" />
			</Component>
		</ComponentGroup>
	</Fragment>
</Wix>

安装为服务

添加NuGet包:

  • WixToolset.Util.wixext

修改AppComponents.wxs文件。示例:

xml 复制代码
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"
     xmlns:util="http://wixtoolset.org/schemas/v4/wxs/util">
	<Fragment>
		<ComponentGroup Id="AppComponents" Directory="INSTALLFOLDER">
			<?define PublishDir = "$(MyApplication.ProjectDir)\bin\Release\net8.0\publish" ?>
			<Files Include="$(PublishDir)\**">
				<Exclude Files="$(PublishDir)\appsettings.json"/>
				<Exclude Files="$(PublishDir)\MyApplication.exe"/>
			</Files>
			<Component Permanent="yes">
				<File Source="$(PublishDir)\appsettings.json" />
			</Component>
			<Component>
				<File Source="$(PublishDir)\MyApplication.exe" KeyPath="yes"/>
				<!-- Tell WiX to install the Service -->
				<ServiceInstall Id="ServiceInstaller"
								Type="ownProcess"
								Name="!(bind.Property.ProductName)"
								DisplayName="!(bind.Property.ProductName)"
								Description="A service."
								Start="auto"
								ErrorControl="normal" >
					<util:ServiceConfig FirstFailureActionType="restart"
										SecondFailureActionType="restart"
										ThirdFailureActionType="restart"
										RestartServiceDelayInSeconds ="10" />
				</ServiceInstall>

				<!-- Tell WiX to start the Service -->
				<ServiceControl Id="StartService"
								Start="install"
								Stop="both"
								Remove="uninstall"
								Name="!(bind.Property.ProductName)"
								Wait="true" />
			</Component>
		</ComponentGroup>
	</Fragment>
</Wix>

缺少官方教程,以上是个人尝试出的可行方法:

  • 使用Exclude排除掉主程序文件,以便在后面的Component指定主程序文件。
  • 主程序文件使用KeyPath="yes",以便自动生成GUID
ServiceInstall

主要配置参数:

  • Startrequired

    确定何时启动服务。该属性的值必须是下列值之一:

    • auto:服务将在系统启动期间启动。
    • demand:当服务控制管理器调用 StartService 函数时,服务将启动。
    • disabled:服务不能再启动。
util:ServiceConfig

此元素是Util扩展中的元素,需要添加NuGet包:WixToolset.Util.wixext,并添加命名空间:http://wixtoolset.org/schemas/v4/wxs/util

  • FirstFailureActionType:第一次失败时的操作

  • SecondFailureActionType:第二次失败时的操作

  • ThirdFailureActionType:后续失败时的操作

    可取以下值:

    • none:无操作(默认值)
    • reboot:重新启动计算机
    • restart:重新启动服务
    • runCommand:运行一个程序
  • RestartServiceDelayInSeconds:如果三个*ActionType属性中的任何一个是"restart",则指定在执行此操作之前等待的秒数。

修改安装路径

如果需要修改安装路径,例如建立两层文件夹,修改Folders.wxs,示例如下:

xml 复制代码
<Wix xmlns="http://wixtoolset.org/schemas/v4/wxs">
  <Fragment>
    <StandardDirectory Id="ProgramFiles6432Folder">
      <Directory Id="ROOTDIRECTORY" Name="!(bind.Property.Manufacturer)">
      	<Directory Id="INSTALLFOLDER" Name="!(bind.Property.ProductName)" />
      </Directory>
    </StandardDirectory>
  </Fragment>
</Wix>

生成安装包

先对被安装的项目执行发布。

然后对安装包项目执行生成,将生成一个msi文件。


进阶

阻止包安装

假如要检测安装包是否在 Windows 的 Server 版本上运行,如果是,则显示一条消息并退出安装程序。Windows Installer 可以为我们做以上事。

属性

MSI 具有许多*内置属性* ,这些属性允许我们检测运行包的 Windows 版本,以及一个名为 launch conditions 的工具,用于在不满足用户指定的条件时阻止包。

MsiNTProductType 属性具有以下值:

Value Meaning
1 Windows 2000 Professional and later
2 Windows 2000 domain controller and later
3 Windows 2000 Server and later

所以如果MsiNTProductType 属性不是1,则表明在WIndows Server上运行。

条件和表达式

Windows Installer的表达式语法类似Basic,相等运算符是=,不等运算符是<>。但WiX 语言是用 XML 表示的,<> 字符是特殊的,仅用于将元素的名称括起来,不能在其他地方使用它们,因此<>要改成使用&lt;&gt;。对于MsiNTProductType <> 1,要写成MsiNTProductType &lt;&gt; 1

编写启动条件

启动条件由Launch元素表示,该元素通常作为Package 元素的子元素。

xml 复制代码
<Launch Condition="" Message="" />

如果Condition属性中的条件表达式为false,则表明不满足启动条件,将弹出一个消息框,显示Message属性的内容。

例如:

xml 复制代码
<Launch Condition="MsiNTProductType = 1" Message="Launch Condition Failed Message Goes Here" />

MsiNTProductType不为1,将弹出如下消息框: