使用Wix Toolset和AutoUpdater.NET实现Windows桌面应用程序的打包与自动更新功能。
Wix Toolset是用来打包Windows Installer的工具集,通过编译源代码,然后链接以创建可执行文件。WiX命令行构建工具适用于任何自动化构建系统,MSBuild还支持常见的CI/CD构建系统,如GitHub Actions。
使用WiX Bundle可以创建安装包来安装先决条件,例如.NET Framework和其他运行时环境以及自己的msi文件。WiX Bundle将他们组合成一个可下载的exe文件。
注: 以下使用的Wix Toolset版本为5.0,与低版本的语法有所区别。在Wix语法中,[]内容为库中预定义的全局变量(安装期间可使用),$()为用户自己定义的全局变量,!(bind.)为绑定的属性,!(wix.)为Wix编译时的变量(安装期间不可使用),!(loc.)为本地化之后的内容。
Wix Package
Wix Package的基本功能是将应用程序打包成msi文件。此外,还支持桌面快捷方式、菜单快捷方式、卸载快捷方式添加,开机自启动,引导程序本地化等。
注: 需预先使用NuGet引入依赖包WixToolset.UI、WixToolset.Util
快捷方式添加
Folders.wxs
1 2 3 4 5 6 7 8
| <Wix xmlns="http://wixtoolset.org/schemas/v4/wxs"> <Fragment> <StandardDirectory Id="DesktopFolder" /> <StandardDirectory Id="ProgramMenuFolder" /> </Fragment> </Wix>
|
Package.wxs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| ...
<Component Id="DesktopShortcutComponent" Guid="{your-guid}"> <Shortcut Id="DesktopShortcut" Name="$(var.ProductName)" Target="[INSTALLFOLDER]{your-product-name}.exe" Icon="{your-icon-name}" Directory="DesktopFolder" /> <RegistryValue Root="HKCU" Key="Software\{your-company-name}\{your-product-name}" Name="Installed" Type="integer" Value="1" KeyPath="yes" /> </Component>
<Component Id="StartMenuShortcutComponent" Guid="{your-guid}"> <Shortcut Id="StartMenuShortcut" Name="$(var.ProductName)" Target="[INSTALLFOLDER]{your-product-name}.exe" Icon="{your-icon-name}" Directory="ProgramMenuFolder" /> <RegistryValue Root="HKCU" Key="Software\{your-company-name}\{your-product-name}" Name="Installed" Type="integer" Value="1" KeyPath="yes" /> </Component>
<Component Id="UninstallShortcutComponent" Guid="{your-guid}"> <Shortcut Id="UninstallProduct" Name="Uninstall" Target="[SystemFolder]msiexec.exe" Arguments="/x [ProductCode]" Icon="UninstallIcon" Description="Uninstall {your-product-name}." /> <RegistryValue Id="RegUninstallShortcut" Root="HKCU" Key="Software\{your-company-name}\{your-product-name}" Name="UninstallShortcut" Type="string" Value="" KeyPath="yes" /> </Component> ...
|
快捷方式图标设置
Package.wxs
1 2 3 4 5 6 7 8
| ...
<Icon Id="{your-icon-name}" SourceFile="$(var.{your-product-name}.ProjectDir)Resources\icon.ico" />
<Icon Id="UninstallIcon" SourceFile="$(var.{your-product-name}.ProjectDir)Resources\uninstall.ico" />
<Property Id="ARPPRODUCTICON" Value="{your-icon-name}" /> ...
|
开机自启动
Package.wxs
1 2 3 4 5 6 7 8
| ... <Component Id="Register"> <RegistryKey ForceCreateOnInstall="yes" Id="AutoStartKey" Root="HKLM" Key="SOFTWARE\Microsoft\Windows\CurrentVersion\Run"> <RegistryValue Id="AutoStartKeyValue" Name="$(var.ProductName)" KeyPath="yes" Type="string" Value="[INSTALLFOLDER]{your-product-name}.exe"> </RegistryValue> </RegistryKey> </Component> ...
|
卸载或更改前自动关闭程序进程
Package.wxs
1 2 3 4 5 6 7 8 9 10 11 12
| ... <Property Id="TASKKILL"> <DirectorySearch Id="SysDir" Path="[SystemFolder]" Depth="1"> <FileSearch Id="taskkillExe" Name="taskkill.exe" /> </DirectorySearch> </Property>
<util:CloseApplication Id="CloseApp" CloseMessage="yes" Target="{your-product-name}.exe" RebootPrompt="no" PromptToContinue="yes" Description="!(loc.CloseBeforeUninstall)"/>
<CustomAction Id="WixCloseApplications" Property="TASKKILL" Execute="immediate" Impersonate="yes" Return="ignore" ExeCommand="/F /FI "IMAGENAME eq {your-product-name}.exe""/> ...
|
Package.zh-cn.wxl
1 2 3 4
| <WixLocalization xmlns="http://wixtoolset.org/schemas/v4/wxl" Culture="zh-CN"> ... <String Id="CloseBeforeUninstall" Value="请关闭应用程序后再进行卸载!"></String> </WixLocalization>
|
x86和x64平台区分可执行文件源
Package.wxs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| ... <Package Name="$(var.ProductName)" Manufacturer="$(var.Manufacturer)" Version="!(bind.FileVersion.ExeFile_m2)" UpgradeCode="$(var.UpgradeCode)"> <MajorUpgrade DowngradeErrorMessage="!(loc.DowngradeError)" />
<MediaTemplate EmbedCab="yes" />
<Files Include="$({your-product-name}.TargetDir)\**"> <Exclude Files="$({your-product-name}.TargetDir)\{your-product-name}.exe"></Exclude> <Exclude Files="$({your-product-name}.TargetDir)\{your-product-name}_x86.exe"></Exclude> </Files>
<File Id="ExeFile_m2" Name="{your-product-name}.exe" Source="$({your-product-name}.TargetDir){your-product-name}_x86.exe" Condition="NOT VersionNT64"></File> <File Id="ExeFile_m1" Name="{your-product-name}.exe" Source="$({your-product-name}.TargetDir){your-product-name}.exe" Condition="VersionNT64"></File> </Package> ...
|
Wix Bundle
Wix Bundle主要用于设置系统必备组件,如各种依赖环境,可以根据目标平台区分依赖的安装包版本。此外还可引入主题文件,定制个性化安装界面。
注: 需预先使用NuGet引入依赖包WixToolset.BootstrapperApplications、WixToolset.Netfx、WixToolset.Util
设置系统必备组件
以下以.NET Core 6.0运行时和WebEdge WebView2运行时为例
Bundle.wxs
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97
| ... <Chain> <PackageGroupRef Id="DESKTOPNETCORERUNTIME6_t"/> <PackageGroupRef Id="WV_bootstrapper"/> <MsiPackage Id="MainPackage" Compressed="yes" SourceFile= "$({your-product-name}.TargetDir)\zh-CN\{your-product-name}.msi" Visible="no"> <MsiProperty Name="INSTALLFOLDER" Value="[InstallFolder]" /> </MsiPackage> </Chain> ...
<Fragment> <netfx:DotNetCoreSearch RuntimeType="desktop" Platform="x86" MajorVersion="6" Variable="DESKTOPNETCORERUNTIME6_x86"/> <netfx:DotNetCoreSearch RuntimeType="desktop" Platform="x64" MajorVersion="6" Variable="DESKTOPNETCORERUNTIME6_x64"/>
<WixVariable Id="DesktopNetCoreRuntime_6029_Redist_DetectCondition_x86" Value="DESKTOPNETCORERUNTIME6_x86" Overridable="yes" /> <WixVariable Id="DesktopNetCoreRuntime_6029_Redist_DetectCondition_x64" Value="DESKTOPNETCORERUNTIME6_x64" Overridable="yes" /> <PackageGroup Id="DESKTOPNETCORERUNTIME6_t"> <ExePackage Id="NetCoreRuntime6_x86" Cache="remove" Permanent="yes" PerMachine="yes" DetectCondition="!(wix.DesktopNetCoreRuntime_6029_Redist_DetectCondition_x86)" InstallCondition="NOT VersionNT64" InstallArguments="/install /quiet"> <ExePackagePayload Name="DESKTOPNETCORERUNTIME6_x86" Size="{file-size}" DownloadUrl="{dowload-url}" Hash="{file-hash}"/> </ExePackage>
<ExePackage Id="NetCoreRuntime6_x64" Cache="remove" Permanent="yes" PerMachine="yes" DetectCondition="!(wix.DesktopNetCoreRuntime_6029_Redist_DetectCondition_x64)" InstallCondition="VersionNT64" InstallArguments="/install /quiet"> <ExePackagePayload Name="DESKTOPNETCORERUNTIME6_x64" Size="{file-size}" DownloadUrl="{dowload-url}" Hash="{file-hash}"/> </ExePackage> </PackageGroup> </Fragment> ...
<Fragment> <util:RegistrySearch Root="HKLM" Key="Software\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Value="pv" Variable="WVRTInstalled_x86"/> <util:RegistrySearch Root="HKLM" Key="Software\WOW6432Node\Microsoft\EdgeUpdate\Clients\{F3017226-FE2A-4295-8BDF-00C3A9A7E4C5}" Value="pv" Variable="WVRTInstalled_x64"/> <WixVariable Id="WV_DetectCondition_x86" Value="WVRTInstalled_x86" Overridable="yes" /> <WixVariable Id="WV_DetectCondition_x64" Value="WVRTInstalled_x64" Overridable="yes" />
<PackageGroup Id="WV_bootstrapper"> <ExePackage Id="WV_bootstrapper_x86" PerMachine="yes" DetectCondition="!(wix.WV_DetectCondition_x86)" InstallCondition="NOT VersionNT64" Vital="yes" Permanent="yes" Cache="remove" CacheId="1"> <ExePackagePayload Name="WV_bootstrapper_x86" DownloadUrl="{dowload-url}" Hash="{file-hash}" Size="{file-size}" /> </ExePackage> <ExePackage Id="WV_bootstrapper_x64" PerMachine="yes" DetectCondition="!(wix.WV_DetectCondition_x64)" InstallCondition="VersionNT64" Vital="yes" Permanent="yes" Cache="remove" CacheId="2"> <ExePackagePayload Name="WV_bootstrapper_x64" DownloadUrl="{dowload-url}" Hash="{file-hash}" Size="{file-size}" /> </ExePackage> </PackageGroup> </Fragment> ...
|
定制个性化安装界面
注意: CustomTheme.xml定义了安装窗口、页面、按钮等各种UI元素以及样式、响应事件,CustomLocalize.wxl定义了个性化文本内容,具体可参照Wix Toolset源码
Bundle.wxs
1 2 3 4 5 6 7 8 9 10 11
| ... <Bundle Name="$(var.ProductName)" Manufacturer="$(var.Manufacturer)" Version="!(bind.packageVersion.MainPackage)" UpgradeCode="$(var.UpgradeCode)" IconSourceFile="$(var.{your-product-name}.ProjectDir)Resources\{your-company-name}.ico"> <BootstrapperApplication> <bal:WixStandardBootstrapperApplication LicenseUrl="" Theme="rtfLargeLicense" ShowVersion="true" LogoFile ="$(var.{your-product-name}.ProjectDir)Resources\{your-company-name}.ico" ThemeFile="CustomTheme.xml" LocalizationFile="CustomLocalize.wxl"/> </BootstrapperApplication> </Bundle> ...
|
AutoUpdater.NET
AutoUpdater.NET是一个类库,可让.NET开发人员轻松地将自动更新功能添加到桌面应用程序项目中。AutoUpdater.NET会从服务器下载包含更新信息的XML文件。它使用该XML文件获取软件最新版本的信息。如果软件的最新版本大于用户PC上安装的软件的当前版本,AutoUpdater.NET就会向用户显示更新对话框。如果用户按下更新按钮更新软件,它就会从XML文件中提供的URL下载更新文件(安装程序),并执行刚刚下载的安装程序文件。此后,安装程序的工作就是执行更新。如果您提供的是zip文件URL而不是安装程序,AutoUpdater.NET将把zip文件的内容解压缩到应用程序目录。
配置XML
1 2 3 4 5 6 7
| <?xml version="1.0" encoding="UTF-8"?> <item> <version>{latest-version}</version> <url>{your-download-url}</url> <changelog>{your-changelog-url}</changelog> <mandatory>false</mandatory> </item>
|
启动更新检查
1
| AutoUpdater.Start("{your-xml-url}");
|
手动处理更新
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
| AutoUpdater.CheckForUpdateEvent += AutoUpdaterOnCheckForUpdateEvent;
private async void AutoUpdaterOnCheckForUpdateEvent(UpdateInfoEventArgs args) { if (args.IsUpdateAvailable) { using var httpClient = new HttpClient(); try { string log = await httpClient.GetStringAsync(args.ChangelogURL);
var textContent = $"当前版本{args.InstalledVersion}, 最新版本{args.CurrentVersion}\n" + "有新版本可用,是否立即更新?\n" + $"{log}";
MessageBoxResult result = MessageBox.Show(textContent, "更新提示", MessageBoxButton.YesNo, MessageBoxImage.Information);
if (result == MessageBoxResult.Yes) { Close(); AutoUpdater.DownloadUpdate(args); } } catch (HttpRequestException e) { Console.WriteLine(e.Message); } } }
|
参考文档