在使用 .NET 6 開發的 Worker Service 編譯為 Windows 服務後,如何提供客戶方便的安裝方式是一個重要的課題。

原本透過 NSIS 打包成 EXE 安裝檔,但客戶希望使用 MSI 進行大量派送。因此,研究了使用 WiX Toolset 的方法,這是一個理想的選擇,因為它可以透過 XML 進行編輯。

WiX Toolset: https://wixtoolset.org

WiX Toolset 是一個開源的工具集,用於創建 Windows 安裝程式。最新的 WiX 版本 4 尚未正式發布,因此建議使用穩定的 3.11 版本。可以透過官方網站的 GitHub Release 下載最新版本的安裝檔。此外,WiX v3 還提供 Visual Studio 2022 擴充功能,方便在 Visual Studio 2022 環境下建立 WiX 專案。

image

使用 WiX Toolset 建立 MSI 安裝檔

在 Visual Studio 中使用 WiX v3 的專案建立後,會生成一個名為 Product.wxs 的檔案,這是 MSI 安裝程式的主要 XML 結構。

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="*" Name="SetupProject1" Language="1033" Version="1.0.0.0" Manufacturer="" UpgradeCode="7ab0bd7e-538d-407a-bca5-a497bba02c97">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

		<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
		<MediaTemplate />

		<Feature Id="ProductFeature" Title="SetupProject1" Level="1">
			<ComponentGroupRef Id="ProductComponents" />
		</Feature>
	</Product>

	<Fragment>
		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLFOLDER" Name="SetupProject1" />
			</Directory>
		</Directory>
	</Fragment>

	<Fragment>
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
			<!-- TODO: Remove the comments around this Component element and the ComponentRef below in order to add resources to this installer. -->
			<!-- <Component Id="ProductComponent"> -->
				<!-- TODO: Insert files, registry keys, and other resources here. -->
			<!-- </Component> -->
		</ComponentGroup>
	</Fragment>
</Wix>

註解的地方是要放置檔案的位子
以下是一些重要的欄位:

  • Product – Id: 安裝完成後的 Product Code,若輸入 *,則每次安裝完成後都會隨機產生 GUID。
  • Product – Version: MSI 安裝完成後的版本號,未來更新時會參考此版本號。
  • Product – UpgradeCode: 用於 MSI 更新,若 Code 相同則進行更新,否則視為不同軟體。

在輸入重要欄位的資訊後,可以使用 File 元件加入需要安裝的檔案:

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="*" Name="Main Service" Language="1033" Version="0.1.2" Manufacturer="Technology lnc" UpgradeCode="6fa4caeb-d239-49c2-bdd8-ae8c0e84d9dd">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

		<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
		<MediaTemplate EmbedCab="yes" />

		<Feature Id="ProductFeature" Title="GDMS" Level="1">
			<ComponentGroupRef Id="ProductComponents" />
		</Feature>

	</Product>

	<Fragment>
		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLFOLDER" Name="Main Service" />
			</Directory>
		</Directory>
	</Fragment>

	<Fragment Id="FileFragment">
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
			<Component Guid="*">
						<File Source="C:\temp\main.exe" />
			</Component>

			<Component Guid="*">
				<File Source="C:\temp\service.dll" />
			</Component>

		</ComponentGroup>
	</Fragment>
</Wix>

這樣加入了兩個檔案分別為 main.exeservice.dll
INSTALLFOLDER 則表示安裝的路徑。

安裝 Windows 服務

因為需要安裝成為服務,除了安裝檔案之外,還需加入安裝服務的控制項。使用 ServiceInstall 加入 Windows 服務,並用 ServiceControl 控制服務。這樣簡單的 Windows 服務封裝的 MSI 檔就完成了。

<?xml version="1.0" encoding="UTF-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
	<Product Id="*" Name="Main Service" Language="1033" Version="0.1.2" Manufacturer="Technology lnc" UpgradeCode="6fa4caeb-d239-49c2-bdd8-ae8c0e84d9dd">
		<Package InstallerVersion="200" Compressed="yes" InstallScope="perMachine" />

		<MajorUpgrade DowngradeErrorMessage="A newer version of [ProductName] is already installed." />
		<MediaTemplate EmbedCab="yes" />

		<Feature Id="ProductFeature" Title="GDMS" Level="1">
			<ComponentGroupRef Id="ProductComponents" />
		</Feature>

	</Product>

	<Fragment>
		<Directory Id="TARGETDIR" Name="SourceDir">
			<Directory Id="ProgramFilesFolder">
				<Directory Id="INSTALLFOLDER" Name="Main Service" />
			</Directory>
		</Directory>
	</Fragment>

	<Fragment Id="FileFragment">
		<ComponentGroup Id="ProductComponents" Directory="INSTALLFOLDER">
			<Component Guid="*">
				<ServiceInstall Id="MainServiceInstall" Name="MainService"
                  DisplayName="Main Service"
                  Description="Main Service"
                  ErrorControl="normal"
                  Type="ownProcess"
                  Vital="yes"
                  Start="auto"
                  Account="LOCALSYSTEM"
                  Interactive="no" />
				<ServiceControl Id="MainService" Name="MainService" Start="install" Stop="both" Remove="uninstall" Wait="yes"/>
				<File Source="C:\temp\main.exe" />
			</Component>

			<Component Guid="*">
				<File Source="C:\temp\service.dll" />
			</Component>

		</ComponentGroup>
	</Fragment>
</Wix>

專案建置完成後,即可在目錄底下產生 MSI 安裝程式。

此外,目錄底下會有一個 wixpdb 檔案,可以將此檔案一同壓縮進 MSI 程式。

在專案屬性頁面中的 Build 分頁,勾選 “Suppress output of the wixpdb files” 選項,編譯時 wixpdb 檔案就會被包進 MSI 安裝程式。

Ubuntu 下封裝 MSI

若需要在 Ubuntu 環境下封裝 MSI,可以使用 wixl 工具。安裝方式如下:

sudo apt install wixl

將 wxs 檔案放置於指定目錄,然後使用 wixl 指令進行封裝:

wixl -v product.wxs -o /var/output/product.msi

詳細命令參數可參考 Ubuntu manpages

值得注意的是,wixl 和 WiX Toolset 雖然都使用 XML 進行封裝,但因為是不同的程式與版本,有些 XML 物件的支援度不一樣。這可能會導致一些問題,例如有些 Tag 在 wixl 需要 Id,而 WiX Toolset 不需要,甚至有些 Tag wixl 直接不支援。這些問題需要逐步測試解決。

分類於:

標籤:

, , , , ,