NUKE Build ノススメ

# .NET Build tool あれこれ
前振りが面倒な人は[NUKE の使い方](#nuke-の使い方)からどうぞ!

.NET は昔からビルドツールをあまり必要としていません。実際には MSBuild を暗黙的に使っているのですが、意識して使うことはまれなのではないでしょうか?

自分は CI を多用するようになってからビルドツールを利用するようになりました。

* テストの前にテスト用のデータベースを作成したうえでマイグレーションをする
* コード検査の結果を成型する
* リリースタグに合わせてバージョン番号を埋め込む

などなど。

最初のうちはコマンドを並べていただけだったのですが、コミットしてプッシュして、CI ジョブはいまた失敗! を繰り返すうちにいやになってきました。

コマンドがたくさんあると手元でテストするのも大変です。手元でテストしたコマンドを CI に設定するときにコピペミスなんかやってすごくいらいらします。


## あれこれ
### シェルスクリプト
個人的にはこれが一番好みです。コマンドを並べるのの延長線上ですし。

ですが、開発は Windows で、CI は Linux でとなっているとやりにくいんですね。

* Windows だと、書いたシェルスクリプトがそう簡単に試せない
* Windows で Git リポジトリにシェルスクリプトを +x パーミッションどうやって与えるの?
* 複雑な処理になると、やっぱしんどい


### PowerShell
開発が Windows で CI も Windows の時はシェルスクリプトが使えないので、PowerShell を使いました。

.NET のクラスライブラリが使えるのはとてもいいです。シェルスクリプトだと、XML をパースして書き換えるとか、接続文字列を書き換えるとかちょっとしんどいんですね。

* PowerShell になじめない (おっさんだから? 新しいプログラム言語は覚えられるから違うと思うけど...)
* Linux で動かない (わけじゃないけど、シェルスクリプトほど手軽ではない)
* インスタンスメソッドと関数で挙動が違う
* バックグラウンドでジョブを実行するだけでなんでこんなにくろうするんや...


### Cake
Windows でも Linux でも動くビルドツールということで、紹介してもらった [Cake](https://cakebuild.net/) も使いました。

検索すると PHP の方が出てくるので、`Cake Build` で検索するのをお勧めします。

Cake はほぼ C# でビルドスクリプトが書けます。Cake Script は (おそらく) C# のコードに変換され、ビルドされ、.NET 上で実行されます。

```csharp
var target = Argument("target", "Build");

Task("Build")
    .Does(() =>
    {
        DotNetCoreBuild(".");
    });

RunTask(target);
```

公式・非公式を合わせると、様々なアドインが用意されていまして、かなり便利です。

でも、この Cake Script、Visual Studio や Rider のコード補完などの支援を得られないんですね。IDE の支援に頼っている身としてはかなり苦しい。

足りないアドインを Cake Script じゃなくて、アセンブリとして作れば IDE の支援が得られるのですが、そのアセンブリの管理がめんどくさいという!


### NUKE
ひょんなことから発見したのが [NUKE](ahttps://nuke.build/) です。

使ってみてびっくり。Visual Studio のソリューションに `_build` って名前のプロジェクトと `Build.cs` が作られて、そのプロジェクトを実行するという強引さ!

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/103982/0efaecc8-760a-4f56-d65f-ade915029834.png)

普通の C# プロジェクトのなので、IDE のコード補完などの支援がバリバリ効きます!

シェルスクリプトほどお手軽ではありませんが、C# のコードを書く延長で書けるのはいいですね!


## NUKE の使い方
### セットアップ
セットアップの前に、Visual Studio でプロジェクトを作りましょう!

コマンドでやりたい人はこんな感じで。

```
$ mkdir app
$ cd app
$ dotnet new sln
$ dotnet new console --output ConsoleApp
$ dotnet sln add ConsoleApp
```

NUKE は .NET Tool 版がおすすめです。インストールしちゃいます。

```
$ dotnet new tool-manifest
$ dotnet tool install Nuke.GlobalTool
```

続いてセットアップです。いろいろと聞かれますが、とりあえずデフォルトでよいでしょう。ただ、`GitVersion` は `No` を選んでください。

```
$ dotnet nuke :setup:

NUKE Global Tool version 5.0.2 (Windows,.NETCoreApp,Version=v2.1)
Could not find root directory. Falling back to working directory.
How should the build project be named?
≫
￢  _build

...

Do you use GitVersion?
￢  No, custom versioning
Creating directory 'C:\Users\masakura\tmp\app\.\build'...
Creating directory 'C:\Users\masakura\tmp\app\source'...
Creating directory 'C:\Users\masakura\tmp\app\tests'...
```

ビルドスクリプトのプロジェクトができましたので、ビルドします。

```
$ dotnet nuke build

...

═══════════════════════════════════════
Target             Status      Duration
───────────────────────────────────────
Restore            Executed        0:01
Compile            Executed        0:01
───────────────────────────────────────
Total                              0:03
═══════════════════════════════════════

Build succeeded on 2021/02/18 19:36:15. ＼（＾ᴗ＾）／
```

ビルドできました!

ところで、横長の顔文字って開発者って日本人なんでしょうか?


### ビルドのカスタマイズ
ビルドスクリプトのカスタマイズは `build` プロジェクトの `Build.cs` ファイルを修正します。

```csharp:build/Build.cs
class Build: NukeBuild
{
    // ...

    Target Restore => _ => _
        .Executes(() =>
        {
            DotNetRestore(s => s
                .SetProjectFile(Solution));
        });

    Target Compile => _ => _
        .DependsOn(Restore)
        .Executes(() =>
        {
            DotNetBuild(s => s
                .SetProjectFile(Solution)
                .SetConfiguration(Configuration)
                .EnableNoRestore());
        });
}
```

`=> _ => _` なんて、??? ってな書き方を要求されますが、そういうもんだと思ってください。([FAQ](https://nuke.build/docs/getting-started/faq.html#that-----looks-weird-can-it-be-changed) にも書かれているぐらい...)

`dotnet nuke Restore` や `dotnet nuke Compile` でそれぞれのターゲットを実行できます。詳しくは [公式ドキュメント](https://nuke.build/docs/authoring-builds/fundamentals.html)を読んでいただければ。

`DotNetBuild()` などの関数がタスクになります。タスクの一覧は [API Reference](https://nuke.build/api/Nuke.Common/Nuke.Common.ControlFlow.html) にあるクラスのうち、`Tasks` で終わるものです。Cake と比べると少なめです。

### タスクを利用する
[ReShaper Command Line Tools](https://www.jetbrains.com/resharper/features/command-line.html) を利用して、コード検査をしてみます。

```csharp:build/Build.cs
class Build : NukeBuild
{
    // ...

    Target Lint => _ => _
        .DependsOn(Compile)
        .Executes(() =>
        {
            ReSharperInspectCode(s => s
                .SetTargetPath("app.sln")
                .SetOutput("inspection-report.xml"));
        });
}
```

そして、`dotnet nuke Lint` を実行します。と、エラーが出ます。

```
$ dotnet nuke Lint

...

Assertion failed: Could not find package 'JetBrains.ReSharper.GlobalTools' using:
 - Project assets file 'C:\Users\masakura\tmp\app\build\obj\project.assets.json'
 - NuGet packages config 'C:\Users\masakura\tmp\app\build\_build.csproj'
Run 'nuke :fix' to add missing references.

...
```

`dotnet nuke :fix` をしろと仰せなので、実行します。

```
$ dotnet nuke :fix

NUKE Global Tool version 5.0.2 (Windows,.NETCoreApp,Version=v2.1)
During last execution a package was found to be missing.
Which version of 'JetBrains.ReSharper.GlobalTools' should be added?
¬  2020.3.2 (latest release)
> "C:\Program Files\dotnet\dotnet.exe" --info
Added 'JetBrains.ReSharper.GlobalTools' to 'C:\Users\masakura\tmp\app\build\_build.csproj'.
```

.NET Tools 版の ReSharper Command Line Tools を `_build` プロジェクトに取り込んでくれます。

もう一度 `dotnet nuke Lint` とすると今度は成功します。

```
$ dotnet nuke Lint

Build succeeded on 2021/02/18 20:18:43. ＼（＾ᴗ＾）／
```

残念ですけど、あくまで ReSharper Command Line Tools を実行するタスクなので、コードに問題があってもビルドは失敗しません...

が、レポートファイルを読み込んで問題があった時は `Fail()` を呼び出すようターゲットを書き換えればよいでしょう。


### パラメーター
パラメーターは以下のように書きます。

```csharp:build/Build.cs
class Build: NukeBUild
{
    [Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
    readonly Configuration Configuration = IsLocalBuild ? Configuration.Debug : Configuration.Release;

    // ...
}
```

`dotnet nuke --configuration Release` とすると、`Configuration` 変数に `Release` が入ります。

`Compile` ターゲットで、`.SetConfiguration(Configuration)` としていますので、リリースビルドになるわけです。

何気に `IsLocalBuild` プロパティが便利でして。これ、開発者のパソコンでビルドされたかどうかを検出してくれます。つまり、CI でビルドされているときはリリースビルドになるんですね。＼（＾ᴗ＾）／


### パス
少し面白いなあと思ったのが、パス。ディレクトリとかファイルとか。

```csharp:build/Build.cs
class Build : NukeBuild
{
    // ...

    AbsolutePath SourceDirectory => RootDirectory / "source";
    AbsolutePath TestsDirectory => RootDirectory / "tests";
    AbsolutePath OutputDirectory => RootDirectory / "output";

    // ...
}
```

おペーレーターオーバーロードで `/` を上書きしてそれっぽく書けるようになっています。


### 外部ツールを呼び出す
`Lint` ターゲットで出力したレポートを Codeclimate 形式の JSON ファイルに変換します。

```csharp:build/Build.cs
class Build : NukeBuild
{
    // ...

    [PackageExecutable("resharper-to-codeclimate", "resharper-to-codeclimate.dll")]
    readonly Tool ReSharperToCodeClimate;

    Target Lint => _ => _
        .DependsOn(Compile)
        .Executes(() =>
        {
            ReSharperInspectCode(s => s
                .SetTargetPath("app.sln")
                .SetOutput("inspection-report.xml"));
            ReSharperToCodeClimate("inspection-report.xml inspection-report.json");
        });
}
```

`PackageExecutable` で NuGet にあるパッケージを実行することができます。ツールを追加したときは `dotnet :fix` を忘れないでください。

```
$ dotnet nuke Lint

Build succeeded on 2021/02/18 20:31:53. ＼（＾ᴗ＾）／
```

`inspection-report.json` ファイルができていると思います。＼（＾ᴗ＾）／

ほかにも、実行ファイルを呼び出すこともできます。詳しくは [Lightweight Integration](https://nuke.build/docs/authoring-builds/cli-tools.html) をご覧ください。

余談ですが、GitLab は Codeclimate 形式のレポートを Merge Request (GitHub の Pull Request) に表示する機能があります。興味がある方は [Code Quality](https://docs.gitlab.com/ce/user/project/merge_requests/code_quality.html) をどうぞ。何気に便利です。


## 使ってみて
シェルスクリプトなどと比べると、起動に少し時間がかかるのが残念ですが、とても書きやすいので満足しています。複雑な処理を書く時も C# のコードそのままですし、何といっても IDE のコード補完やコード検査にコード整形ががっつりききます。

ばりばりビルドスクリプトを書きたい人はおすすめです。

タスクが少ないのはあれですが、サクッと C# でコード書いちゃえばいいだけなので、あまり気になりませんし。

[GitVersion](https://gitversion.net/) との連携も予想外に便利でした。またの機会に開設しようかと思います。