C#でビルドタスクが書けるnukebuildについて

# はじめに

以前[CakeBuild](https://qiita.com/skitoy4321/items/e76b178dd7fca1fbbd8f)について記事を書いたが、同種のフレームワークとして[nukebuild](https://nuke.build/)というものを試したので、とりあえず入門的な記事を書く。

なお、本当の名前は"nuke"だが、同音異義語が世に溢れているため、ここでは"nukebuild"と呼称する([dotnet nukeも既に存在してるし](https://www.dnnsoftware.com/))。

# これは何か?

CakeBuildのように、主にビルド用途に最適化された、指定したタスクを実行してくれるもの。
nukebuildの本体はdotnetのライブラリであり、最終的にタスクを実行するのは普通のコンソールアプリである。
つまり、特殊なプラグイン等を必要とせずに、**既存のdotnet系に対する言語サポート(デバッグ、補完、nugetパッケージ)が受けられるということになる。**
ただし、タスクの実行部分は専用のプラグインが存在する。

# 最初の実行

まずは何もないディレクトリに移動し、下記を実行してみる

1. `dotnet tool install -g Nuke.GlobalTool`
    * "nuke"というコマンドが使えるようになる(使えない場合は、PATHに`$HOME/.dotnet/tools`を追加する)
2. `nuke`
    * 下記のような質問を聞かれるので、答えていく
        1. `Could not find .nuke file. Do you want to setup a build? [y/n]`
            * ひな形を作るかの確認。`y`を入力してエンターして次へ
        2. Where should the build project be located?
            * どこにビルドタスクを記述するプロジェクトを作成するか。デフォルトでは`build`以下に作られる
        3. Which NUKE version should be used?
            * 使用するnukebuildライブラリのバージョンを決める
            * 特に何もなければ安定最新版を選ぶ
        4. Which solution should be the default?
            * タスク内で参照できるソリューションファイルがどこにあるか
            * 存在しなければ"None"で問題ない
3. `nuke`
    * タスクが実行される
    * windowsで文字化けする場合は、ターミナルのコードページを"65001"に変更して実行する(出力にユニコード文字を使用しているため)

ひな形を作るかどうかは、カレントディレクトリに`.nuke`と`build.ps1`または`build.sh`いうファイルがあるかどうかで確認しているので、あれば質問は聞かれず、タスクを実行しようとする。

## [Nuke.GlobalTool](https://www.nuget.org/packages/Nuke.GlobalTool/0.24.11)

nukebuildの実行を補助する [dotnet global tool](https://docs.microsoft.com/ja-jp/dotnet/core/tools/global-tools)
機能としては、

* テンプレートプロジェクトとブートストラッパーの作成
* タスクの実行を行う
    * カレントディレクトリと上位ディレクトリを順番に見てbuild.ps1を探し、それを実行している

実は**ひな形さえ作れば以後は無くても良い**

## 構成ファイル

ひな形作成で作られるファイルには以下のようなものがある。

* .nuke
    * nukebuildを実行する時のルートディレクトリに置く
    * nukebuildはこのファイルがある場所をルートディレクトリと判断する
* build.ps1,build.sh
    * タスクを実行するためのブートストラッパー
    * build.ps1はpowershell用、build.shはbash用となる
    * dotnet sdkが無い場合はダウンロードを行って、そこからタスクを実行しようとする
    * **ひな形作成時に、タスクプロジェクトのcsprojへのパスがハードコードされるので、構成を変えるときは注意**
    * 動作的には以下の事をしている
        1. dotnet sdkが無ければDLして`.tmp`に展開
        2. ハードコードされたcsprojをビルド
        3. ビルドされたプロジェクトを実行
* build/*
    * タスクプロジェクト(デフォルトだとこのパス)
    * 中身は[Nuke.Common](https://www.nuget.org/packages/Nuke.Common/)を参照している
    * `Nuke.Common.NukeBuild`を継承したクラス`Build`が作られているので、このクラス内にタスクを記述していく
    * 一つのクラスにメンバーを追加していくことになるので、大規模になるならばpartialクラスにした方が良い

# タスクの記述

タスクはタスクプロジェクトのBuildクラス内に、[`Nuke.Common.Target`](https://www.nuke.build/api/Nuke.Common/Nuke.Common.Target.html)のインスタンスを追加していって記述する。
このTargetは定義としては `public delegate ITargetDefinition Target(ITargetDefinition definition)`というデリゲートなので、以下のようにTargetのインスタンスを記述する。

```csharp:Build_MyTask.cs
// 大元のBuildクラスをpartialクラスにしている前提
using Nuke.Common;
partial class Build
{
    // Executesの中の処理を実行する
    // Executesは必須ではなく、何もしないタスクを作ることも可能
    Target MyTask => _ => _
        .Executes(() => Logger.Info("MyTask"));
    // 分解すると、以下のような意味になる
    Target MyTask2
    {
        get
        {
            Target ret = (ITargetDefinition t) =>
            { return t.Executes(() => Logger.Info("MyTask")); };
            return ret;
        }
    }
}
```

## タスクの実行

タスクを記述した後、ブートストラッパー、あるいはビルドプロジェクトを実行すれば、タスクが実行される。デフォルトでは、Main関数に記述されているFunc<Target>で指定したタスクが実行される。

ヘルプを表示させたい場合は、コマンドライン引数に`--help`を指定すればOK
ターゲットを指定する場合は、`--target [TargetA] [TargetB]`のように指定する。
また、そのほかにも独自の引数を指定することができるが、それは後述する

## タスクの依存関係

例えば"Build"の前には"Restore"を成功させてほしい、"Publish"は"Build"が実行されていなくてもいいけど、必ず後に実行したい、等、タスクの実行順序と依存関係を制御したい場合は、Targetの定義時にITargetDefinitionで以下の操作を行う

### `ITargetDefinition DependsOn(params Target[] t)`

指定したタスクを依存しているタスクとして登録する。こうすると、"t"が事前に実行されるようになり、かつ成功しない限り、そのタスクは実行されなくなる。

```csharp:DependsOn.cs
Target TaskA => _ => _.Executes(() => Logger.Info("TaskA"));
Target TaskB => _ => _.DependsOn(TaskA).Executes(() => Logger.Info("TaskB"));
```

この状態でTaskBを実行しようとすると、かならずその前にTaskAが実行されるようになる。

### `ITargetDefinition DependentFor(params Target[] t)`

`DependsOn()`とは逆に、指定したタスクが自分に依存しているという登録を行う。

```csharp:DependentFor.cs
Target TaskA => _ => _.Executes(() => Logger.Info("TaskA"));
Target TaskB => _ => _.DependentFor(TaskA).Executes(() => Logger.Info("TaskB"));
```

この状態でTaskBを実行してもTaskAは実行されないが、TaskAを実行しようとすると、必ずその前にTaskBが実行されるようになる

### `ITargetDefinition Before(params Target[] t)`

指定したタスクに対して、依存関係は作成されないが、必ず引数指定したタスクよりも前に実行されることが保証されるようになる

```csharp:Before.cs
Target TaskA => _ => _.Executes(() => Logger.Info("TaskA"));
Target TaskB => _ => _.Before(TaskA).Executes(() => Logger.Info("TaskB"));
```

この状態でTaskAを単独で実行してもTaskBは実行されないが、TaskAとTaskBを同時に実行しようとすると、必ずTaskBが先に実行されるようになる

### `ITargetDefinition After(params Target[] t)`

`Before()`とは逆に、指定したタスクに対して、依存関係は作成されないが、必ず引数指定したタスクよりも後に実行されることが保証されるようになる

```csharp:Before.cs
Target TaskA => _ => _.Executes(() => Logger.Info("TaskA"));
Target TaskB => _ => _.After(TaskB).Executes(() => Logger.Info("TaskB"));
```

この状態でTaskAを単独で実行してもTaskBは実行されないが、TaskAとTaskBを同時に実行しようとすると、必ずTaskAが先に実行されるようになる

## 必要条件の記述

あるタスクを実行する時、条件を満たしていないとエラーとして扱いたい場合、`ITargetDefinition Requires(params Expression<Func<bool>>[] conditions)`が使える。
引数に指定した条件式はタスク実行前に評価され、falseならばエラーとして処理される

```csharp:Requires.cs
[Parameter("Hoge")]
readonly string Hoge;
Target TaskA => _ => _.Requires(() => !string.IsNullOrEmpty(Hoge));
```

上記でパラメーター無しでTaskAを実行しようとすると、Hogeが空なのでエラーで終わる。

## 特定の条件の時のみの実行

ある条件を満たした時のみタスクを実行したい場合、`ITargetDefinition OnlyWhenStatic(params Expression<Func<bool>>[] conditions)`または`ITargetDefinition OnlyWhenStatic(params Expression<Func<bool>>[] conditions)`が使用できる。

見た目はほぼ同じだが、OnlyWhenDynamicは、該当タスク実行直前に評価され、OnlyWhenStaticは、全てのタスク実行の前に一回だけ評価される。

```csharp:OnlyWhen.cs
[Parameter("Hoge")]
string Hoge;
Target TaskA => _ => _.OnlyWhenDynamic(() => !string.IsNullOrEmpty(Hoge));
Target TaskB => _ => _.OnlyWhenStatic(() => !string.IsNullOrEmpty(Hoge));
```

上記でパラメーター無しでTaskBを実行しようとすると、TaskBの実行はスキップされる。
TaskAの場合は、TaskAより前に実行される他のタスクで値が設定された場合、実行はスキップされない。

## イベントのフック

初期化やクリーンアップ等、ターゲット実行の前や後に処理を割り込ませたい場合は、イベントのフック機能を使う。
基本的にBuildクラスの中で各種イベントをオーバーライドすればOK。
実際よく使うのは、OnBuildInitialized(コマンドライン引数の解釈、各種タスクのセットアップが完了した後に一回実行される)、OnBuildFinished(全ての処理の後に実行される)だと思う。

```csharp:Event.cs
protected override void OnBuildInitialized()
{
    Logger.Info("on build initialized");
}
protected override void OnBuildFinished()
{
    Logger.Info("on build finished");
}
```

## パラメーターの追加

コマンドライン引数にはデフォルトで指定できるもの(`--target`,`--help`等)があるが、さらに追加で指定することも可能。
やり方としては、Buildクラスのメンバーに、`Nuke.Common.ParameterAttribute`を持ったプロパティまたはフィールドを追加すること。
ひな形にも記述があるが、以下のようにする。

```csharp:Parameter.cs
[Parameter("--helpで表示される文章の記述")]
readonly string MyParameter1 = "デフォルト値"; // デフォルト値が無ければdefault(T)が使われる
```

サポートされる型は、デフォルトではstring、プリミティブ型と、その配列型、nullable型をカバーしている。独自型のサポートは後述。
パラメーターの名前は、基本的にプロパティ/フィールド名をもとに決定されるが、CamelCaseを名前に指定した場合は、kebab-caseに自動的に変換される。
つまり、上記の場合は、パラメーター名が`--my-parameter1`となる。
なお、大文字小文字は区別しない。

配列を渡したい場合は、受け取るパラメーター型を配列型にして、該当引数の後に複数の値を渡せばOK。
nukebuildでは、次のパラメーター指定が始まるまで、以降の引数全てをパラメーターの引数と解釈するようになっている。

例えば `[Parameter("")]readonly int[] MyParameter1 = new int[0];`のように指定して、`--my-parameter1 1 2 3`のように指定すると、MyParameter1には1,2,3の値が入る

### 独自パラメーター型の追加

パラメーターに独自の型をサポートしたい場合、その型を`System.ComponentModel.TypeConverter`を使って、stringから型変換可能にしておく必要がある。

独自型の宣言では以下のように実装する

```csharp:TypeConverter.cs
using System.ComponentModel;
using System;
// この属性を追加して、型変換の時は指定のTypeConverterを使うようにする
[TypeConverter(typeof(MyParameterType.MyTypeConverter))]
class MyParameterType
{
    // System.ComponentModel.TypeConverterを継承したクラスを作成する
    class MyTypeConverter : TypeConverter
    {
        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
        {
            // string型の変換を受け付けられるようにしておく
            if (sourceType == typeof(string))
            {
                return true;
            }
            else
            {
                return false;
            }
        }
        public override object ConvertFrom(ITypeDescriptorContext context, System.Globalization.CultureInfo culture, object value)
        {
            // 実際の変換処理
            var str = (string)value;
            var ret = new MyParameterType();
            ret.Hoge = str;
            return ret;
        }
    }
    public string Hoge;
}
```

後は、パラメーターの型に指定した値を入れるだけでOK。

## ファイルパスの構築

ファイルパスの構築は、[AbolutePath](https://nuke.build/api/Nuke.Common/Nuke.Common.IO.AbsolutePath.html?q=AbsolutePath)または[RelativePath](https://nuke.build/api/Nuke.Common/Nuke.Common.IO.RelativePath.html?q=RelativePath)を使用する。
どちらも'/'をオーバーロードしているため、'/'で繋げてパス構築が可能。
文字列に出力する際に、プラットフォーム固有の区切り文字へ変更してくれる。

```csharp
// 使用する際はnewではなくstringからのキャストを使用する
var rootpath = (AbsolutePath)"c:/";
var path_a = rootpath / "a"; // c:\a
var path_b = path_a / "b"; // c:\a\b
```

デフォルトでは、`AbsolutePath RootDirectory`というプロパティがBuildクラスで使用できるので、各種タスクの起点をここに設定するといいと思う。

# よく使う機能

## [FileSystemTasks](https://nuke.build/api/Nuke.Common/Nuke.Common.IO.FileSystemTasks.html)

ファイルコピー、作成、移動のユーティリティ。
なお、ファイル検索に関しては[PathConstruction](https://nuke.build/api/Nuke.Common/Nuke.Common.IO.PathConstruction.html)のGlobFiles等を使用する。

## [ProcessTasks](https://nuke.build/api/Nuke.Common/Nuke.Common.Tooling.ProcessTasks.html)

プロセス起動等のユーティリティ。
`ToolSettings`は直接インスタンス化できないので、基本的には引数が沢山あるバージョンを使うことになる。
**実行時のワーキングディレクトリはRootDirectoryと一致しない場合があるので、workingDirectoryの指定はしておいた方が良い。**

## [MSBuildTasks](https://nuke.build/api/Nuke.Common/Nuke.Common.Tools.MSBuild.MSBuildTasks.html)

MSBuildを実行するための補助を行う。ここで使用するMSBuildは、VisualStudioあるいはmonoが持つものであり、dotnet sdkとは異なるものになることに注意。
実行パターンは複数あるが、最もよく使うのは`MSBuild(Configure<MSBuildSettings> configure)`だろうか。
`Configure<MSBuildSettings>`とは、要するに`Func<MSBuildSettings, MSBuildSettings>`のようなものである。
とりあえず`SetProjectFile(string)`は必須で、さらに使用するVSのバージョンを限定したい場合は、`SetMSBuildVersion()`、ビルドアーキテクチャを指定したい場合は、`SetTargetPlatform()`を実行することになる。

## [CompressionTasks](https://nuke.build/api/Nuke.Common/Nuke.Common.IO.CompressionTasks.html)

zip,tar,gz,bz2等、圧縮・伸長等を行うユーティリティ。
xzや7zに関してはサポートしていないので、別途処理を用意する必要がある。

# 終わりに

とりあえずここまで書けば、基本的なタスクというのは書けると思う。
公式ドキュメントを参考にしながら記述すること。ここで紹介していない便利機能もあるので、
一回は見てみると良い。

本当はcakebuildとの比較を書きたかったが、この記事が長くなってしまったので宿題とする。

## 参考リンク

* [公式サイト](https://nuke.build)
* [ドキュメント](https://nuke.build/docs/getting-started/philosophy.html)
* [APIリファレンス](https://nuke.build/api/Nuke.Common/Nuke.Common.ControlFlow.html)