wixsharpでインストーラーを作る

# はじめに

Windows向けインストーラーの形式として、MSIというものが存在する。
インストーラー作成ツールとしてメジャーなものには、VSのインストールプロジェクト、あるいはInstallShieldなどがあるが、
このMSIを基盤としている([NSIS](https://nsis.sourceforge.io/Main_Page)や[Inno Setup](https://jrsoftware.org/isinfo.php)は別の仕組み)。

このMSIを作るための手法として、[Wix Toolset](http://wixtoolset.org/)というものが存在する。
XMLで宣言的にコンポーネント等を設定して、MSIを作るというものだ。

しかしこのwix、再利用がしにくい部分がある、記述が冗長になりやすい、補完が難しい等、作りこんでいくと多少苦しくなる時がある。
そのようなwixをC#で記述することによって、IDE(vsやvscode)の補完や、処理の再利用などをしやすくした [WixSharp](https://github.com/oleg-shilo/wixsharp) なるものが存在する。

今回は、そのWixSharpをvscodeで記述するための最初のステップや、注意するべき点について記述したいと思う。

なお、WixSharpを使うに当たりMSIとWixの関連領域に踏み込むことは不可避なので、これらも並行して学んでいくと良いと思う。

# WixSharpとは

WixSharpとは、C#でMSIパッケージの生成をできるようにしたライブラリで、ほとんどの機能はWixのラッパーとなる。
また、Wixに備わっているDTF(Deployment Tool Foundation)を利用することによって、winformsやWPFでのUI記述や、.NETによるカスタムアクションの定義等を容易に行う事が可能になる。
もともとCS-Scriptという、Roslyn以前にC#をスクリプトで使おうというプロジェクトの一つの活用事例として作られたものであるが、実際はCS-Scriptなしでも単体のライブラリとして存在している。

今回は執筆時点での最新版である1.15.0をベースに書いていく。

# 前提条件

本記事の中で必要なものは以下の通り

* Windowsマシン
    * そもそもMSIがWindows専用の機構であるため
* .NET Framework SDK
    * WixSharpは.NET Framework用のライブラリしか提供してないため
* Windows 10 SDK
    * インストールコンポーネントの中の"MSI Tools"というものがあるので、それを有効にする
* Wix v3
    * https://wixtoolset.org/releases/ より、最新安定版をDLする
    * `*-binaries.zip`と`*.exe`があるが、片方はバイナリのみ、片方はインストーラーとなり、今回はどちらでも構わない
* Visual Studio Code + C#拡張
    * エディタとして本記事で使う
* Visual Studio + .NET開発 サポート
    * 同じくC#拡張のため
    * 最低限Visual Studio Build Toolでも可

# 使用するWixの準備

WixSharpはその性質上、Wixの実行バイナリ(candle.exe等)がどうしても必要になる。そのため、Wixの実行バイナリがあるフォルダがどこにあるかを知る必要がある。
定義するための手法は以下のようなものがあり、いずれも設定されたディレクトリ直下にcandle.exe等があることが期待される。
上から順に優先される。

1. MSI生成時に`WixSharp.Compiler.WixLocation`に設定する
2. コマンドライン引数で`/WIXBIN:`をプレフィックスに持つものを探し、その後ろのパスを使用する
    * MSBuildで使う用
3. 環境変数の`WIXSHARP_WIXDIR`を取得
    * エラーメッセージ等でも示されているので、環境変数で設定する場合はこの方法が一番いいかも
4. 環境変数の`WixLocation`を取得
5. 環境変数の`WIX`を取得し、その後ろに`bin`フォルダを追加する
    * WIXをMSI経由でインストールした場合、環境変数`WIX`は自動的に設定される
6. `%ProgramFiles%`の配下にあるWixインストールディレクトリを検索し、あればそこに`bin`を追加する
7. `..\..\Wix_bin\bin`、`..\..\..\..\Wix_bin\bin`、`..\..\..\..\..\Wix_bin\bin`を検索し、あれば採用する
    * サンプル用なので実際には期待しない方が良い

MSIからWixをインストールした場合、大抵の場合は`WIX`環境変数の部分で引っかかると思う

# MSI生成プロジェクトの準備

## 新規プロジェクトの生成

さて、Wix実行ファイルの場所特定ができたら、次はMSIを生成するプロジェクトの作成である。
といっても、生成するのは普通のC#のコンソールプロジェクトで、`dotnet new console`辺りで作ればいいだけである。
ただし、WixSharpのライブラリは.NET Framework製なので、後でTargetFrameworkを.NET Framework系のものに変えておこう(`net461`とか`net35`とか)

## nugetパッケージの追加

プロジェクトの生成が完了したら、WixSharpのnugetパッケージを追加することになる。
WixSharpには以下のように複数のnugetパッケージが存在するため、自分のプロジェクトに合わせて参照するものを変えること。

* WixSharp.bin
    * ライブラリ部分のパッケージ
* WixSharp
    * WixSharp.bin + MSBuild時に更新が入った場合、自動的にプログラムが実行されるようになる→ビルドするだけでMSIができるようになる
* WixSharp.Lab
    * WixSharp.binに比べて実験的な機能が含まれている
    * WixSharp.binと共存可
    * 1.15.0時点では画面生成用のクラスが含まれている(WixSharp.Controls)

# 実装

プロジェクトの準備ができたら、ソースを編集していく。
おおまかには以下のような流れとなる

1. `WixSharp.Project`インスタンスの生成
2. ProductCode、UpgradeCode、言語設定等の主要属性の設定
3. 以下を再帰的に行う
    1. `WixSharp.Dir`の生成
        * インストールルート以外は、全てディレクトリ単体の名前を指定する
    2. 生成したディレクトリ内に`WixSharp.File`オブジェクト等をnewして追加
        * ファイルの他、レジストリやサービス登録等もここで追加する
    3. 内部に更にディレクトリがある場合、`WixSharp.Dir`を生成して繰り返す
4. カスタムアクションがあればそれを定義して、Projectインスタンスに追加する

この他にも、MSIのネイティブUIを定義できたり、Windows FormsやWPFを使って画面を作成することもできるが、長くなるのでこの記事では取り上げない。

## 主に決めるべき主要属性

wixsharpでは、wixに関するプロパティが細かい所まで設定できるが、その中でも最初に考慮した方が良いものを挙げていく。
下で紹介されている他にも、成果物出力先決定などに使われるOutDir等の設定があるが、あまり重要ではないため省略する。
全て見たい場合は、[WixSharpのコードを見ること](https://github.com/oleg-shilo/wixsharp/blob/v1.15.0.0/Source/src/WixSharp/Project.cs)

### UpgradeCode

MSIでいう所の [UpgradeCodeプロパティ](https://docs.microsoft.com/en-us/windows/win32/msi/upgradecode)を設定する。
**よほどのことが無い限り、MSIのUpgradeCodeは変わらないはずなので、とりあえず設定しておく方がいい。**

### ProductCode

MSIでいう所の [ProductCodeプロパティ](https://docs.microsoft.com/en-us/windows/win32/msi/productcode) を設定する。
ProductCodeプロパティはメジャー/マイナーアップグレードの主要な判断材料となるため、よく考えて設定する、しないを決定すること。
また、WixSharpにおいては、**各コンポーネント(ファイル等)のGUIDはProductCodeを基点に一意に決定されるため、値の取り扱いについては十分注意すること**。

### GUID

これはWixSharp独自のプロパティで、設定すると後述するProductCodeやUpgradeCodeが手動設定されていない場合の基点となる。
WixSharpがUpgradeCode、ProductCodeがどのように決定するかは以下の図を参照。

![UpgradeCode決定手順](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103253/ccd0d532-313e-e3c2-eb32-9be999759f92.png)

![ProductCodeの決定の仕方](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103253/ef761397-ac1b-7ec3-d3c7-e67b2bbc4692.png)

### MajorUpgrade

メジャーアップグレードで、ダウングレードを許すか、どのタイミングで既存プロダクトの削除を行うかを設定する。
メジャーアップグレードをしたいならば必ず設定しておくこと。
それぞれのフィールドの意味は以下で、[WixのMajorUpgradeエレメントに対応する](https://wixtoolset.org/documentation/manual/v3/xsd/wix/majorupgrade.html)

* `bool? AllowDowngrades`(default=false)
    * 古いバージョンへのダウングレードを検知したとき、インストールをブロックするかどうか
* `bool? AllowSameVersionUpgrades`(default=true)
    * バージョン番号が同じ場合でも、メジャーアップグレードを許可するか
* `bool? Disallow`(default=false)
    * メジャーアップグレードを禁止する
* `string DisallowUpgradeErrorMessage`
    * メジャーアップグレードを検知したときに、エラー画面とともに表示されるメッセージ
* `bool? IgnoreRemoveFailure`(default=false)
    * メジャーアップグレードの時にアンインストールを実行して失敗したとき、アップグレードを続行するかどうか
    * 続行したときに何が起こるかは個別の事情が絡むため網羅的にいいにくいので、実際どうなるかを確かめてから設定すること
* `bool? MigrateFeatures`(default=true)
    * Featuresを継承するかどうか
* `string RemoveFeatures`
    * 指定したFeatureがインストールされている時、それらを再インストールしない
* `WixSharp.UpgradeSchedule? Schedule`(default=afterInstallValidate)
    * メジャーアップグレードの中で、アンインストールをどのタイミングで行うか設定する
    * 大体はデフォルトのままで問題ないが、**アップグレードが途中で失敗して中断した場合、元バージョンもろとも消え去ってしまう**ため、可能性がある場合は注意すること

### MajorUpgradeStrategy

メジャーアップグレードをどのバージョンからどのバージョンまでで有効にするか等を決定する。
[Wixでいう所の、Upgradeエレメントに対応する](https://wixtoolset.org/documentation/manual/v3/xsd/wix/upgrade.html)

### Version

ここでバージョンを設定すると、ProductVersionプロパティが設定され、作成するMSIパッケージのバージョンになる。
ただし、MSIのルールとして、`[major].[minor].[build]`(各構成要素は全て整数)のように形式が決まっているので注意。
詳しくは [MSIのドキュメントを参照](https://docs.microsoft.com/en-us/windows/win32/msi/productversion)

[MSIにおいてバージョン番号は、メジャー/マイナー/スモールアップデートに関わる所なので気を付けること。](https://docs.microsoft.com/en-us/windows/win32/msi/major-upgrades)

### UI

Wixが標準で用意しているUIのどれを使うかを選択する。使えるのは以下で、`WixSharp.WUI`というenumクラスの値を指定する。
ここで設定すると、`UIRef`にIDを指定したものが追加される。
それぞれのダイアログの解説については、Wixのものをそのまま使っているだけなので、[wixtoolsetのWixUIダイアログの解説](https://wixtoolset.org//documentation/manual/v3/wixui/wixui_dialog_library.html)を参照。

ここではテンプレートから選ぶような感じになるが、アプリ独自要素(シリアルとか)をインストールUIで設定させたい場合もあるだろう。そのような時はカスタムダイアログを作ることになると思うが、これが書くとまた長いので、[公式のサンプルソースへのリンクを紹介するだけに留めたい](https://github.com/oleg-shilo/wixsharp/tree/master/Source/src/WixSharp.Samples/Wix%23%20Samples/Custom_UI)。サンプルでは、WinFormsやWPFのダイアログをUIシーケンスの中に挟み込んでいる。

### Properties

MSIのユーザー定義プロパティを追加する。
`AddProperties`メソッドで追加が可能。
なお、**ここで定義できる値は、全てインストール実行前に値が決定されるもの**となることに注意。
具体的に言うと、例えばあるプロパティの値に`[PROPERTY_A]\ABC`のような、他のプロパティに依存した値を付けても、実際に使う時にはプロパティ指定部分が空になったり、意図した値が反映されない場合がある(インストールディレクトリの指定等)。
そのような場合は、後述するカスタムアクションの一種である`WixSharp.SetPropertyAction`を使用する必要がある。このアクションを使う時も、アクションの実行順に注意しないと、意図した結果にならない場合があることに注意。

### SourceBaseDir

ファイル等のコンポーネントを指定する際、ソースに相対パスを指定した時の基点となるフォルダを指定する。
設定しないときは、カレントディレクトリが使われる。

## コンポーネントの追加

プロジェクトの属性が決まったら、各種コンポーネント(ファイル、レジストリ等)を追加していく。
基本的にインストールルートとなる`WixSharp.Dir`を作成し、そこにどんどんコンポーネントや配下のフォルダを作成していく形になる。
作成の時はLinq to XMLのように、コンストラクタで追加していってもいいし、インスタンスを作成した後追加していってもいい。

ソースでいうと

```csharp
// `WixSharp.Project`に対する便利拡張メソッドを使用するため
// using WixSharp.CommonTasks;
var project = new WixSharp.Project("ProjectName");
#region "projectオブジェクトへのプロパティ設定等"
#endregion
// コンストラクタ時に追加
var dir = new Dir(new WixSharp.Id("DirID"), "%ProgramFiles%Hoge", 
    new WixSharp.File(new WixSharp.Id("File1"), "path/to/source1")
    );
// 後から追加
// ここで設定するIdはWixSharp上のIDで、MSIのコンポーネントに設定するGUIDのIDは別途設定されることに注意
dir.Add(new WixSharp.File(new WixSharp.Id("File2"), "path/to/source2"));
// プロジェクトインスタンスに追加
project.Add(dir); 
```

のような感じになる。レジストリを追加するときは、`WixSharp.File`の代わりに`WixSharp.RegKey`を使う。
なお、インストールはしないが、カスタムアクションなどで使用するファイルがある場合は、`WixSharp.Binary`を使って追加する。これはDirに所属している必要は無い。

### Feature

例えばWindows SDKをインストールする時、インストールするコンポーネントを選択する画面が出てくると思うが、
あれを実現しているのがFeatureという機能となる。
特に指定しない限り、WixSharpは全てのコンポーネントを同じFeatureに属するものとして処理するが、
例えばインストール時にコンポーネントを選択させたい場合は、このFeatureを使うことになる。
基本的には、`WixSharp.Feature`オブジェクトを最初に生成しておき、各`WixSharp.File`等のコンポーネントを生成する時、`Feature`フィールドがあると思うので、そこに設定すればいい。
サンプルコードは[WixSharpのリポジトリにあるので](https://github.com/oleg-shilo/wixsharp/blob/master/Source/src/WixSharp.Samples/Wix%23%20Samples/Features/setup.cs)、そこを参照すること

## カスタムアクションの追加

コンポーネントが追加し終わったら、カスタムアクションを追加していく。
カスタムアクションは全て`WixSharp.Action`から派生しており、共通属性として以下のものを持つ

* `string Name`
    * カスタムアクションの名前
    * 他から参照する時に使われるので、少なくともプロジェクトの中ではユニークであること
* `WixSharp.Conditon Condition`
    * カスタムアクションを実行するかしないか
    * 文法は [MSIのもの](https://docs.microsoft.com/en-us/windows/win32/msi/conditional-statement-syntax) がそのまま使えるが、代表的なものは、`WixSharp.Condition`の静的フィールドとして定義されている
* `WixSharp.Execute Execute`
    * カスタムアクション実行のコンテキストを指定する(MSI由来)
    * 各設定の詳細は[Wix公式ドキュメント](https://wixtoolset.org/documentation/manual/v3/xsd/wix/customaction.html)にある
    * だいたい"immediate"か"deferred"で、デフォルトは"immediate"
    * なんでこんな設定があるかという事については書くと長いので、[MSI公式ドキュメント](https://docs.microsoft.com/en-us/windows/win32/msi/deferred-execution-custom-actions)を参照
    * [deferredアクションにした場合、一部のAPIについて制約が付くことに注意(特にプロパティ周り)](https://docs.microsoft.com/en-us/windows/win32/msi/obtaining-context-information-for-deferred-execution-custom-actions)
* `bool? Impersonate`
    * ユーザー権限(UAC無し)で実行するか(=true)、システム権限(UAC有)で実行するか(=false)で、デフォルトはtrue
    * falseで実行したいときは、更にExecuteを"deferred"にする必要がある
* `WixSharp.Return Return`
    * カスタムアクションの実行結果をどう処理するか(戻り値が0かどうかで成功/失敗を判断)
    * 通常は`check`(デフォルト)か`ignore`
* `WixSharp.Sequence`
    * インストールシーケンスのうち、どのシーケンスに所属するかを指定する
    * 大体`InstallExecuteSequence`(インストール開始後)か、`InstallUISequence`(MSIのUI画面)になる
    * 代表的なものは`WixSharp.Sequence`に静的フィールドとして存在する
* `WixSharp.Step`、`WixSharp.When`
    * カスタムアクションをどの時点(Step)の前後どちらで(When)実行するかを指定する(相対指定)
    * 代表的なものは`WixSharp.Step`に静的フィールドが存在するが、自分のカスタムアクションのIDを指定することも可
* `WixSharp.SequenceNumber`
    * 相対指定ではなく、絶対的な数値の順番で指定する
    * `Step`、`When`設定とは排他
    * 何を指定すべきかは、MSIの仕組みを知っていないと厳しいため、上級者向け

カスタムアクションの種類は、WixSharpにおいては以下のものが公式として存在する。

* BinaryFileAction
    * MSIに組み込んだ"Binary"ファイルを取り出して、実行可能ファイルとして実行する
* ManagedAction
    * DTF(Deployment Tools Foundation)というwix提供の機能を利用して、C#の静的メソッドをカスタムアクションとして実行する
    * 作り方は後述
* ElevatedManagedAction
    * 上記ManagedActionを管理者権限で実行する
    * 実際は、上記ManagedActionに`execute=deferred`と`impersonate=no`が設定されたものになる
* InstalledFileAction
    * インストールする(されているファイル)を、IDをキーにして実行する
* PathFileAction
    * システムに存在するEXEを指定して実行する
* SetPropertyAction
    * **インストーラー実行時**にプロパティを設定する
    * インストール中に値が変わるプロパティを基に更に他のプロパティを設定したい場合に使う
    * カスタムアクション実行時点のプロパティ値を使用するため、実行順に注意すること

他にWixとしては、ネイティブDLLの中の関数を実行するということも可能だが、WixSharpではデフォルトでやり方が用意されているわけではない。

### ManagedActionの作り方

C#でManagedActionを使う場合、いくつかの手順を踏む必要がある。
まず最初に、Microsoft.Deployment.WindowsInstaller.dllを、カスタムアクションを仕込むプロジェクトへ参照追加する必要がある。
カスタムアクションを入れるプロジェクトは、現在wixsharpを使用しているプロジェクトでもいいし、また別途プロジェクトを用意しても構わない。
これは、Wixをインストールするか、zipアーカイブを展開すると、candle.exe等があるフォルダに同梱されている。
これを直接Reference等で参照する。

次に、実行するメソッドを定義するわけだが、

* 所属するクラスがpublic
* 型が`public static ActionResult [メソッド名](Microsoft.Deployment.WindowsInstaller.Session)`であること
* 属性に`Microsoft.Deployment.WindowsInstaller.CustomAction`があること

を満たせばOK。例として下記。

```csharp:CustomAction.cs
using Microsoft.Deployment.WindowsInstaller;
public class CustomActions
{
  // WixSharp側で参照するための名前を設定する
  // デフォルトはメソッド名がそのまま使われる
  [CustomAction("Method1")]
  public static ActionResult Method1(Session session)
  {
    // 例外等エラーが起きてcatchしないと、制御が効かずにそのまま終わる可能性があるので注意
    try
    {
      // MSIのログに出力
      session.Log("in customaction");
      // MSIプロパティを設定する
      // Execute=immediateの時に使えるが、他の場合は無視される
      session["A_PROPERTY"] = "ABCDE";
      // MSIプロパティを取得する
      // Execute=immediateの時に使えるが、他の時はnullになる
      string propb = session["B_PROPERTY"];
      // CustomActionDataを取得する
      // Execute=deferredの時に、ここに値が入り、そうでないときはnull
      string caData = session.CustomActionData;
      
      return ActionResult.Success;
    }
    catch(Exception e)
    {
      session.Log("error: {0}", e);
      return ActionResult.Failure;
    }
  }
}
```

上記を満たしたメソッドを用意した上で(仮に`CustomActions.Method1`とする)、ManagedActionを以下のように追加する。

```csharp
project.AddAction(new WixSharp.ManagedAction("Method1")
{
    // カスタムアクションメソッドが入っているアセンブリを指定する
    // %this%は、このプロジェクトのアセンブリを意味する特別な文字列(デフォルトは%this%)
    ActionAssembly = "%this%",
    Condition = Condition.NOT_Installed,
    Execute = Execute.immediate,
    Impersonate = true,
    // ActionAssemblyに、DTFとシステムアセンブリ以外の依存がある場合、ここにもれなく記述する
    RefAssemblies = new string[],
    Return = Return.check,
    Step = Step.AppSearch,
    // Execute=deferredの時、ここにプロパティ名をカンマ区切りで入れる必要がある
    // こうすると、CustomActionDataに"A_PROPERTY=[A_PROPERTYの値];B_PROPERTY=[B_PROPERTYの値]"のように、値が格納される
    UsesProperties = "A_PROPERTY,B_PROPERTY",
});
```

注意点として、**ManagedActionを使用すると、その時点でMSIのインストールに.NET Framework依存ができてしまうため、ManagedActionが使用しているフレームワークバージョンは確実に満たせるように、開発要件を定めておくこと**

## MSIの生成

さて、全てのプロジェクト設定が終わったら、最後に`WixSharp.Project.BuildMsi(string path = null)`を実行する。
出力されるMSIファイルは、指定が無ければカレントディレクトリの"[プロダクト名].msi"という名前で出力される。
また、同時に生成されたwxsファイルが、"wix/[プロジェクト名].g.wxs"として出力される。
こちらは、生成結果の確認等に使うと良いだろう。

また、MSIを作らずにwxsの確認だけ行いたい場合は、`WixSharp.Project.BuildWxs(string path = null)`を実行する。
こちらはWixのツールを実行しないので、BuildMsiでエラーが出た時にwxsの中身を確認したい場合に使うと良いと思う。

# 終わりに

ここまでWixSharpの入り口として記事を書いたが、MSIネイティブやWindows Formsを画面に使えたり、あるいはMSIを実行するためのブートストラッパーの作成ができるなど、ここでは紹介していない機能が色々あるので、機会があればまたその辺りの記事を書いてみたい。

# 参考リンク

* [WixSharpのソース](https://github.com/oleg-shilo/wixsharp/tree/v1.15.0.0/)
* [WixSharpのサンプルソース集](https://github.com/oleg-shilo/wixsharp/tree/v1.15.0.0/Source/src/WixSharp.Samples)
* [MSIのメジャーアップグレードについて](https://docs.microsoft.com/en-us/windows/win32/msi/major-upgrades)
* [MSIX](https://docs.microsoft.com/ja-jp/windows/msix/overview)
    * 次世代Windowsパッケージシステム
    * [Wixでも有料サポートで作成は可能](https://www.firegiant.com/products/wix-expansion-pack/msix/)だが、WixSharpでは使えない
* [Wix Toolsetのサイト](https://wixtoolset.org/)
* [Wixのリファレンス(v3)](https://wixtoolset.org/documentation/manual/v3/)
* [NSIS](https://nsis.sourceforge.io/Main_Page)
* [Inno Setup](https://jrsoftware.org/isinfo.php)
* Orca
    * MS謹製のMSI閲覧、更新用ツール
    * Windows SDKの中にあるが、標準ではインストールされず、中にOrca.msiが含まれているので、これをインストールする
* [lessmsi](https://github.com/activescott/lessmsi)
    * MSIの中身の展開や、プロパティの閲覧等をコマンドラインから行える
    * orcaの起動が面倒なときなどに有用