WiXSharpのカスタムアクションに、NuGetのライブラリを追加する方法

# 概要

WiXSharpというインストーラ作成用のライブラリの話です。[以前の記事](https://qiita.com/spc_ksudoh/items/afab902b6dea7fd4f6ed)でも少し触れましたが、なかなか癖が強く、すぐにやれそうなことも意外と工夫が必要だったりします。

その中で、「NuGetでライブラリを追加したい」という、よく必要になる上に簡単にやれそうな作業で意外と工夫が必要だったので、今回はそのやり方を書きます。

# 最初に結論まとめ

 * ビルド先に出力されたdllの相対パスを、msi定義部分のDefaultRefAssembliesに手動で追記すればOKです
 * ちょっと工夫すると、自動でも追記できます
 * デフォルトのビルド先だとソリューション構成ごとにパスが変わってしまうので、ビルド後イベントで固定のパスにコピーしておくとやりやすいです

# 説明

## 問題の内容

「WiXSharp Managed Setup - Custom WPF UI (WiX4)」のプロジェクトを作成しました。

NuGetでライブラリを追加することで、カスタムアクションの実装に使うことができます。JSONを扱いたかったので、System.Text.Jsonを入れてみました。しかし、「カスタムアクションに実装したが、動かしてみるとDLLがロードできない」という厄介な動きになりました。こういった具合です。

```
System.IO.FileNotFoundException: ファイルまたはアセンブリ 'System.Text.Json, Version=8.0.0.1, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51'、またはその依存関係の 1 つが読み込めませんでした。指定されたファイルが見つかりません。
```

動きを観察してみると、WiXSharpによってビルドされたexeがmsiに埋め込まれ、msi実行時に一時フォルダへそのexeを展開し、実行しているようです。

するとこういうことになるので、DLLがロードできないのも道理です。

```plantuml
@startuml

component "VisualStudio(NuGet)" as vs

component "ビルド出力先" as output {
    component "System.Text.Json.dll" as json
    component "カスタムアクションが入ったexe" as exe
}

component "msi" as msi

component "インストール中の一時フォルダ" as temp {
    component "カスタムアクションが入ったexe" as exe2
}

vs --> json : 出力
vs --> exe : 出力

exe --> msi : 埋め込み

msi --> exe2 : 出力・実行


@enduml
```

このexeを実行する一時フォルダへ、NuGetのDLLも出力させる必要があります。

## 解決方法

### DefaultRefAssembliesの追加

WiXSharpでは、ManagedProjectクラスのインスタンスを作成して、そこにmsiの色々な設定をしていると思います。そこで、必要なDLLをDefaultRefAssembliesプロパティに追加します。すると、対象をmsiに埋め込んで、実行時に一時フォルダへ展開してくれます。

```csharp

var project = new ManagedProject(ProductDefine.ProductName //省略

//省略

project.DefaultRefAssemblies = new List<string>
{
    //System.Text.Jsonの場合
    "System.Text.Json.dll",
    "Microsoft.Bcl.AsyncInterfaces.dll",
    "System.Buffers.dll",
    "System.Memory.dll",
    "System.Numerics.Vectors.dll",
    "System.Runtime.CompilerServices.Unsafe.dll",
    "System.Text.Encodings.Web.dll",
    "System.Threading.Tasks.Extensions.dll",
    "System.ValueTuple.dll",
};

```

NuGetと連携して自動でこれを作ってくれたりすると助かるんですが、どうやら手動での定義が必要なようです。

ただしこれだけだと、プロジェクトファイルからの相対パスなので、ファイルが見つからずにエラーとなります。ファイルが見つかるように、正しい相対パスで書く必要があります。次のような感じです。（以降、System.Text.Json.dll以外は省略します）

```csharp
project.DefaultRefAssemblies = new List<string>
{
    "bin\Release\net481\win-x86\System.Text.Json.dll",
};
```

これで一応動きますが・・・しかしこれではパスにビルド構成が入ってしまうので、Debugビルドに変更しただけで失敗します。ちょっと不便です。

### ビルド後イベントの追加

なのでもう一工夫して、ビルド後イベントで次のようにDLLを別フォルダにコピーすると良いです。

```bat
xcopy "$(TargetDir)*.dll" "$(ProjectDir)DependentAssembly\" /Y
```

そうすると、次のように参照できます。

```csharp
project.DefaultRefAssemblies = new List<string>
{
    "DependentAssembly\System.Text.Json.dll",
};
```

これで、カスタムアクション内でNuGetで取得したDLLを使うことができます。

### DefaultRefAssembliesの追加（応用）

WiXSharpのmsi生成処理は、ビルド完了後に動的に実行されます。（ビルド後イベントに元々入っている処理で実行されています）

そのため、ハードコーディングで1つずつ定義しなくても、ビルドで出力されたDLLを列挙して取り込むという手が使えます。ただし、WiXSharpが元々使っているDLLは除外する必要があります。すると、DefaultRefAssembliesを追加する部分で、次のようなコードを書けば実現できます。

```csharp

var dependAsmDir = Path.Combine(Environment.CurrentDirectory, "DependentAssembly");
project.DefaultRefAssemblies = Directory.GetFiles(dependAsmDir, "*.dll")
    .Where(ValidateFileIsNotWiXSharpDefaultAsm)
    .ToList();

bool ValidateFileIsNotWiXSharpDefaultAsm(string filePath)
{
    //元々組み込まれているDLL。これを組み込むと二重に組み込むことになって無駄なので、対象から除外する。ネイティブDLLを渡すとDefaultRefAssembliesがエラーを出すので、それを回避する狙いもある。
    var defaultAsm = new[]
    {
        "Caliburn.Micro.Core.dll",
        "Caliburn.Micro.Platform.Core.dll",
        "Caliburn.Micro.Platform.dll",
        "mbanative.dll",
        "Microsoft.Xaml.Behaviors.dll",
        "WixSharp.dll",
        "WixSharp.Msi.dll",
        "WixSharp.UI.dll",
        "WixSharp.UI.WPF.dll",
        "WixToolset.Dtf.WindowsInstaller.dll",
        "WixToolset.Mba.Core.dll",
    };
    var fileName = Path.GetFileName(filePath);
    return defaultAsm.All(d => !d.Equals(fileName, StringComparison.OrdinalIgnoreCase));
}

```

## 解決方法まとめ

つまり、次の2つで解決可能です。

1，ビルド後イベントの最初に次の処理を追加

```bat
xcopy "$(TargetDir)*.dll" "$(ProjectDir)DependentAssembly\" /Y
```

2，WiXSharp向けコード（Program.cs）で、次のようにDefaultRefAssembliesを追加


```csharp

var project = new ManagedProject(ProductDefine.ProductName //省略

//省略

var dependAsmDir = Path.Combine(Environment.CurrentDirectory, "DependentAssembly");
project.DefaultRefAssemblies = Directory.GetFiles(dependAsmDir, "*.dll")
    .Where(ValidateFileIsNotWiXSharpDefaultAsm)
    .ToList();

bool ValidateFileIsNotWiXSharpDefaultAsm(string filePath)
{
    //元々組み込まれているDLL。これを組み込むと二重に組み込むことになって無駄なので、対象から除外する。ネイティブDLLを渡すとDefaultRefAssembliesがエラーを出すので、それを回避する狙いもある。
    var defaultAsm = new[]
    {
        "Caliburn.Micro.Core.dll",
        "Caliburn.Micro.Platform.Core.dll",
        "Caliburn.Micro.Platform.dll",
        "mbanative.dll",
        "Microsoft.Xaml.Behaviors.dll",
        "WixSharp.dll",
        "WixSharp.Msi.dll",
        "WixSharp.UI.dll",
        "WixSharp.UI.WPF.dll",
        "WixToolset.Dtf.WindowsInstaller.dll",
        "WixToolset.Mba.Core.dll",
    };
    var fileName = Path.GetFileName(filePath);
    return defaultAsm.All(d => !d.Equals(fileName, StringComparison.OrdinalIgnoreCase));
}

```




# まとめ

C#の開発では、NuGetで取得したDLLを使うというのはもはや息をするように自然にやっていることだと思いますが、WiXSharpでは意外に一工夫必要でした。[以前の記事](https://qiita.com/spc_ksudoh/items/afab902b6dea7fd4f6ed)でも書いたように、条件が合えばとても便利なライブラリなので、こういう所で挫折せずに上手く使っていけると良いと思います。