WixSharpで作成するインストーラに.NET5 Runtimeをバンドルする方法と、バンドルインストーラを日本語化する方法

# はじめに

## 注意

今回の記事は[前回](https://qiita.com/nao-a/items/31fd03fe5f8867420766)の記事の続きになります。
用意しているコードはすべて[前回](https://qiita.com/nao-a/items/31fd03fe5f8867420766)の続きからとなりますのでご注意ください。

## あらためて、はじめに

前回では、.NET5で作成したアプリケーションをWixSharp (Wix) を使用してインストーラにする、ということを実施しました。
しかしながら、.NET5はまだ広く一般ユーザに浸透しているとは言い難く、.NET5のランタイムをユーザにインストールしていただくということが必要になってくるかと思います。
Wixでは `Bundle` という方法で、依存するランタイムなどを一緒にインストールさせることが可能となります。
本記事では、その `Bundle` の使い方を紹介するとともに、 `Bundle` で作成したインストーラを同時に日本語化するところまでを実施します。

### あれ、でも日本語化って前回で。。。

前回までの記事をご覧になられていたそこのあなた、「日本語化はすでに前回実施したはずでは」と思われたはずです。

それはその通りですが、今回は「複数のインストーラを実行するインストーラを作成する」ということを実施することになりますので、ですから、別途日本語化作業が必要となるわけです。

### サンプルコード

また、この記事で紹介しているコードは以下のリポジトリに配置しております。

https://github.com/cocoalix/WindowsServiceTestWithDotNet5

## 実行環境

- VisualStudio2019
- Windows10Pro 20H2 (Build: 19042.867)
- .NET5 SDK 5.0.1

# 参考

https://github.com/oleg-shilo/wixsharp

## 本件を実施するにあたっていろいろと質問した経緯

みなさま、その節は本当にありがとうございました。この場を借りて御礼を申し上げます。。。

https://social.msdn.microsoft.com/Forums/ja-JP/356c4dca-2da9-4da1-b38f-a35a8a7973cf/wixsharp-or-wixtoolset-12398-bootstrapper-burn?forum=windowsgeneraldevelopmentissuesja

https://github.com/oleg-shilo/wixsharp/issues/985

# 実装

## ランタイムのバンドル

### 事前準備

今回も事前準備が必要です。

1. バンドルさせたいインストーラのDL
1. バンドルさせたいインストーラのDL元URLの確保
1. バンドルさせたいインストーラからWix用のmetadataの抽出
1. 実際にバンドル対象のインストーラからインストールを行い、レジストリがどうなるかを調べる

#### 1. バンドルさせたいインストーラのDL

今回は `.NET5 Desktop Runtime` のインストーラを、インストール時に実行させたい(=バンドルさせたい)ので、`.NET5 Desktop Runtime` をDLします

特にこだわりがないのであれば、アーキテクチャは64bit版(=x64)で良いです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507143/cbf7acf4-851a-a7fd-0e04-ba1d9e8d969a.png)

#### 2. バンドルさせたいインストーラのDL元URLの確保

DL画面で、現在取得したインストーラのダウンロードURLを取得できますので、ここで取得しておきましょう

画面中央部の `Direct link` となっている個所です

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507143/6536d3b0-1ad0-9c4c-6099-a6c3fbacfdfb.png)

#### 3. バンドルさせたいインストーラからWix用のmetadataの抽出

次に、metadataの抽出です

以下のコマンドを実行することで、Wix用のmetadataをXML形式で抽出することができます

**注意: 以下、 `heat` コマンドが出てきますが、こちらは [Wix Toolset](https://wixtoolset.org/) をインストールしておかないと使用することはできませんので、あらかじめインストールをお願い致します**

```powershell
PS> heat payload <バンドルしたいexeのバイナリのパス> -out .\remote.xml
```

`-out` で指定したファイルにmetadataが出力されていますので、それを利用しましょう。
例えばですが、今回の場合は.NET5 Version5.0.5のWindows Desktop Runtimeを使用しましたが、このような形で出力されます

```xml
<?xml version="1.0" encoding="utf-8"?>
<Wix xmlns="http://schemas.microsoft.com/wix/2006/wi">
    <Fragment>
        <RemotePayload CertificatePublicKey="F49F9B33E25E33CCA0BFB15A62B7C29FFAB3880B" CertificateThumbprint="ABDCA79AF9DD48A0EA702AD45260B3C03093FB4B" Description="Microsoft Windows Desktop Runtime - 5.0.5 (x64)" Hash="9CE1E77FE51E3F104D1DF7670F83F12B8ABF82F6" ProductName="Microsoft Windows Desktop Runtime - 5.0.5 (x64)" Size="54977944" Version="5.0.5.29917" />
    </Fragment>
</Wix>
```

#### 4. 実際にバンドル対象のインストーラからインストールを行い、レジストリがどうなるかを調べる

インストールをしましょう、まずはそこからです。

そして、レジストリエディタを見ましょう。

おおむね、.NET5の場合は以下のキーに存在しています。

```
コンピューター\HKEY_LOCAL_MACHINE\SOFTWARE\dotnet\Setup\InstalledVersions\x64\sharedhost
```

レジストリがADの設定等で見られないと嘆いているそこのあなた！

Powershellを使うという手もあります

```powershell
PS> Get-ChildItem -LiteralPath 'HKLM:\SOFTWARE\dotnet\Setup\InstalledVersions\x64' -Recurse
```

Powershellの場合、このように結果が出力されます

```
    Hive: HKEY_LOCAL_MACHINE\SOFTWARE\dotnet\Setup\InstalledVersions\x64


Name                           Property
----                           --------
sharedhost                     Version : 5.0.5
```

このVersion情報が後々必要となってきますので、ご注意ください

### ランタイムのバンドルのコード実装

以下の通りです、順を追って説明します

```csharp
// :
// これより上は前回の内容となりますのでそちらをご覧ください
// :

// インストール先を Program Files (x86) から Program Files に変更する
project.Platform = Platform.x64;
// インストール時に管理者権限を要求
project.InstallPrivileges = InstallPrivileges.elevated;
// インストール先をPC全体に変更
project.InstallScope = InstallScope.perMachine;


var projectMsi = project.BuildMsi();

var bootstrapper = new Bundle(
    "ウィンドウズサービステスト_バンドルインストーラ",
    new ExePackage()
    {
        Id = "DotNet5DesktopRuntime",
        Name = "dotnet5-windowsdesktop-runtime-5.0-win-x64.exe",
        Vital = true,
        Permanent = false,
        DownloadUrl = @"https://download.visualstudio.microsoft.com/download/pr/c1ef0b3f-9663-4fc5-85eb-4a9cadacdb87/52b890f91e6bd4350d29d2482038df1c/windowsdesktop-runtime-5.0.5-win-x64.exe
",
        InstallCommand = "/install /quiet",
        RepairCommand = "/repair /quiet",
        UninstallCommand = "/uninstall /quiet",
        LogPathVariable = "dotnet5desktopruntime.log",
        Compressed = true,

        // RemotePayloadは以下のコマンドで取得可能
        // heat payload <バンドルしたいexeのバイナリのパス> -out .\remote.xml
        RemotePayloads = new[]
        {
            new RemotePayload()
            {
                ProductName = "Microsoft Windows Desktop Runtime - 5.0.5 (x64)",
                Description = "Microsoft Windows Desktop Runtime - 5.0.5 (x64)",
                Hash="9CE1E77FE51E3F104D1DF7670F83F12B8ABF82F6",
                Size = 54977944,
                Version = "5.0.5.29917".ToRawVersion(),
                CertificatePublicKey = "F49F9B33E25E33CCA0BFB15A62B7C29FFAB3880B",
                CertificateThumbprint = "ABDCA79AF9DD48A0EA702AD45260B3C03093FB4B"
            }
        },
        new MsiPackage(projectMsi)
    }
);

// ランタイムバンドルインストーラのバージョン
bootstrapper.Version = new Version("1.0.0.0");

// ランタイムバンドルインストーラのアップグレードコード (変更厳禁)
bootstrapper.UpgradeCode = new Guid("XXXXX");

// ランタイムバンドルインストーラのアプリケーションスタイルの定義
bootstrapper.Application = new LicenseBootstrapperApplication();

// 表示する利用規約の指定
bootstrapper.Application.LicensePath = @".\Eula.rtf";

// インストール時のOption非表示
bootstrapper.Application.SuppressOptionsUI = true;
// アンインストール時の修復を非表示
bootstrapper.Application.SuppressRepair = true;

// 一次領域を使用するか
bootstrapper.PreserveTempFiles = true;

// Wixの必須パラメータの定義？は行わない
bootstrapper.SuppressWixMbaPrereqVars = false;

// インストーラ名の定義
bootstrapper.OutFileName = "MyAwesomeSetup";

// ランタイムバンドルインストーラの作成
bootstrapper.Build();
```

#### `var projectMsi = project.BuildMsi();`

前回作成した.msiのインストーラのパスを `projectMsi` で受けます

後の `Bundle` のインスタンス作成時に指定します

#### `var bootstrapper = new Bundle("ウィンドウズサービステスト_バンドルインストーラ", ... `

バンドルするインストーラのバンドルインスタンスを作成します

第一引数ではインストーラの名前(多くは製品名でしょう)を入力し、
第二引数以降では、バンドルするインストーラの情報を入力します
**第二引数以降で、必ずそれより前に作成したアプリ単体のインストーラを含めることを忘れないように注意しましょう**

#### `Id = "DotNet5DesktopRuntime"`

バンドルのIDです。たぶん、なんでもいいです。たぶん。わかりやすく名付けしておきましょう。

#### `Name = "dotnet5-windowsdesktop-runtime-5.0-win-x64.exe"`

バンドルの名前です。たぶん、なんでもいいです。たぶん。わかりやすく名付けしておきましょう。

#### `Vital = true,`

これちょっと重要です。

`true` で、「このバンドルのインストールが失敗した場合、後続のインストーラも失敗する」という設定になります

失敗させたくない場合は `false` にしましょう

#### `Permanent = false`

インストール元のパッケージのアンインストール時に、一緒にアンインストールさせるかどうかを決定します

`false` で一緒にアンインストール **させない** です

#### `DownloadUrl = "https://download.visualstudio.microsoft.com/download/pr/c1ef0b3f-9663-4fc5-85eb-4a9cadacdb87/52b890f91e6bd4350d29d2482038df1c/windowsdesktop-runtime-5.0.5-win-x64.exe"`

これも重要ですが、ここで指定する値はすでに取得しています

ここでは、事前準備で取得したパッケージのDL元のURLを指定すればそれでよいです

#### `InstallCommand` / `RepairCommand` / `UninstallCommand`

それぞれインストール時、修復時、アンインストール時に実行するコマンドを指定することができます

多くのインストーラは `/install` `/uninstall` のようなコマンドライン引数をサポートしていますので、それを指定してやればよいです

なお、.NET5 WindowsDesktopRuntimeの場合、 `/quiet` でインストール時に .NET5 WindowsRuntimeDesktop のインストーラ画面を表示させない、というオプションを持っています

#### LogPathVariable = "dotnet5desktopruntime.log",

このインストーラ(.NET5Runtimeのインストーラ)がミスった時にログを出すことができますが、そのログのファイル名を指定できます

#### `Compressed = true`

圧縮オプションです

#### `RemotePayloads = new[]`

こいつがめちゃくちゃ重要です

ここでは以下のように設定していますが、ここで指定する内容は **事前準備の「3. バンドルさせたいインストーラからWix用のmetadataの抽出」で取得したmetadataです**

取得したXMLをよく読んで、しっかり指定してあげましょう

```csharp
            new RemotePayload()
            {
                ProductName = "Microsoft Windows Desktop Runtime - 5.0.5 (x64)",
                Description = "Microsoft Windows Desktop Runtime - 5.0.5 (x64)",
                Hash="9CE1E77FE51E3F104D1DF7670F83F12B8ABF82F6",
                Size = 54977944,
                Version = "5.0.5.29917".ToRawVersion(),
                CertificatePublicKey = "F49F9B33E25E33CCA0BFB15A62B7C29FFAB3880B",
                CertificateThumbprint = "ABDCA79AF9DD48A0EA702AD45260B3C03093FB4B"
            }
```

### 作成したインストーラを実行してみる

このようにインストーラが表示されていればバッチリです！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507143/d206401f-f9e4-502e-35ad-eccbc28998a8.png)


## 日本語化

いよいよ日本語化ですが、残念ながらここでも事前準備は必要です

仕事は準備7割とかって言いますものね、仕方ありませんので根気強くやっていきましょう

### 事前準備

以下のプロセスが必要です

1. インストーラを日本語にするための `.wxl` ファイルの作成
1. `.wxl` をVisualStudioで管理するようにする

面倒ですが、やっていきましょう

#### 1. インストーラを日本語にするための `.wxl` ファイルの作成

##### そもそも

`.wxl` ファイルとは、ローカライゼーション対応のためのファイルで、XML形式で書かれます。
日本語化に際して、必ずこれが必要となります

##### 用意

`.wxl` ファイルの作成とはいいましたが、以下をそのまま持ってくるのもきっとありでしょう。
少なくとも私は、どのような値を定義すればよいのかがわかりませんでしたので。。。
(こういうところをWiXの公式で救ってくれると非常にありがたいんですけどね。。。)

https://github.com/dotnet/aspnetcore/blob/52eff90fbcfca39b7eb58baad597df6a99a542b0/src/Installers/Windows/WindowsHostingBundle/1041/thm.wxl

これを利用する際に一つ注意点なのですが、一部タイトル部分がこのままだときちんと表示されないので、次のように変えることをおススメします

```xml
  <String Id="Title">[WixBundleName]</String>
  <String Id="SubTitle">[WixBundleName]</String>
```

#### 2. `.wxl` をVisualStudioで管理するようにする

このようにしてください

今回用意した `.wxl` ファイルは `thm.wxl` としてプロジェクトの直下に保存しています
そのプロパティとして、以下を定義しています

- ビルドアクション: コンテンツ
- 出力ディレクトリにコピー: 常にコピーする

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507143/8cc5137d-0d02-e0f7-7679-58472e9e40fa.png)


### 実装

ようやく実装ですが、書く内容は以下のみで大丈夫です

```diff_c_sharp
// ランタイムバンドルインストーラのバージョン
bootstrapper.Version = new Version("1.0.0.0");

// ランタイムバンドルインストーラのアップグレードコード (変更厳禁)
bootstrapper.UpgradeCode = new Guid("bf3b1aeb-12c5-4401-ad23-6a49f905bd55");

// ランタイムバンドルインストーラのアプリケーションスタイルの定義
bootstrapper.Application = new LicenseBootstrapperApplication();
bootstrapper.Application.LicensePath = @".\Eula.rtf";

+// ローカライゼーションファイルの指定
+bootstrapper.Application.LocalizationFile = "thm.wxl";

// インストール時のOption非表示
bootstrapper.Application.SuppressOptionsUI = true;
// アンインストール時の修復を非表示
bootstrapper.Application.SuppressRepair = true;

// 一次領域を使用するか
bootstrapper.PreserveTempFiles = true;
// Wixの必須パラメータの定義？は行わない
bootstrapper.SuppressWixMbaPrereqVars = false;

// インストーラ名の定義
bootstrapper.OutFileName = "ウィンドウズサービステスト_バンドルインストーラ";

// ランタイムバンドルインストーラの作成
bootstrapper.Build();
```

#### 実行

このように日本語で表示されていれば、それでOKです

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/507143/945f3128-4270-bd2f-9793-17c595741a9a.png)

### 問題点

今回紹介した日本語化対応は、「インストーラをマルチロケール(i18n的なもの)に対応することが、そのままだとできない」というところが問題点となるかと思います。

つまり、今回紹介した方法とは、あくまで、「日本語のインストーラしか要らないから、日本語化されたインストーラをWixSharpで作りたい」という時に使用できる対応方法になります。

お気を付けください。

## インストーラがうまく動かない！でもエラーが表示されない！そんな時は……

エクスプローラで `%Temp%` を参照してみてください。何かしらログが吐かれていると思います。このログを基に、調査することも可能です。

# おわりに

このようにして、インストーラに.NET5のランタイムはバンドルできますし、日本語化もばっちり対応できるかと思います

あぁ、次はインストールコンディションだ。。。

同じ開発者の助けになる記事になることを望みます。

また、何か誤っている個所がありましたらご指摘ください。
ここまで読んでくださり、ありがとうございました。