C#でビルドスクリプトが書けるCakeBuildを使ってみる

# 始めに

dotnetプロジェクトを作るにあたり、多くの人はIDEを用いて開発していると思う。しかし、以下のように、IDEでは面倒な作業はどうしても存在する。

* ビルドした後アーカイブにまとめる
* 複数の設定を一度にビルドあるいはテストを行う
    * PlatformとかConfigurationとかTargetFrameworkとか
* ビルド結果を受けて更に別の外部コマンドを実行する
    * VSのカスタムビルドステップだとプラットフォーム依存が発生する
* XML,JSONファイルの編集

また、CI/CD等を使用する場合に、シェルスクリプトの直接実行に頼ると、プラットフォームの違いを意識する部分が多くなり、ビルドスクリプトが複雑になりがちになる。
dotnetならMSBuildがあるじゃないかという意見もあるだろうが、MSBuildはスクリプトを書くのに向いていないツールなので、複雑なことをすると非常に辛くなる。

そこで、それらの問題を解決するために、[Cake Build](https://cakebuild.net/)というツールを紹介する。
F#erな人のために、[FAKE](https://fake.build/)なるツールもあり、[アドベントカレンダーで記事にもなっている](https://www.amusement-creators.info/post/articles/advent_calendar/2019/05_0/)ようなので、
興味のある人はこちらも見てみるといいかもしれない。

# 特徴

以下のような特徴を持つ

* C#のような構文でビルドスクリプトが記述可能
    * csharp scripting + cakebuild独自のプリプロセス構文
    * その他、基本的なルール等は[公式ドキュメント](https://cakebuild.net/docs/fundamentals/)を参照
* nugetで外部ライブラリが使用可能
* [拡張も豊富](https://cakebuild.net/dsl/)
    * 自分でももちろん書ける

## 必要なもの

以下のいずれかが必要

* dotnet-sdk 2.1以降
* .NET Framework(Windows)とpowershell
* mono

# 使い方

## CakeBuild実行ファイルの導入

まず、cakebuildの実行ファイルをプロジェクトに導入する。
インストールはいくつかの手段がある。

* [共通のコマンドラインオプション](https://cakebuild.net/docs/cli/usage)

### build.shまたはbuild.ps1の導入

公式推奨の方法で、shスクリプトで実行可能な`build.sh`、またはpowershellで実行可能な`build.ps1`が公式より提供されている。
やっていることとしては、CakeBuild実行ファイル本体のDLと、実行ファイル本体に引数を整理して渡す等。

利点としては、

* 公式推奨
* 枯れている

辺りだろうか。後述するdotnet toolとは別に衝突しないので、開発マシンはbuild.ps1、CI/CDではdotnet toolというように両方併用するのもいいかもしれない。

欠点としては、

* プラットフォーム依存がある(build.shかbuild.ps1か)
* build.shとbuild.ps1で微妙に指定できる書式が異なる

辺り。開発環境であればほとんど障害にならない程度ではある。

以下のURLから実行スクリプトをcurl等でDLする。やってることはあくまで本体のDLと実行ファイルへのラッパーで、サイズも大きくはないので、ソース管理に追加することを推奨する。

* Windows: https://cakebuild.net/download/bootstrapper/windows
* Linux: https://cakebuild.net/download/bootstrapper/linux
* macOS: https://cakebuild.net/download/bootstrapper/osx

なお、build.ps1については、monoとpowershell6以降があればlinuxでも動いたりする(macでは未検証)。

* [コマンドラインスイッチ詳細](https://cakebuild.net/docs/tutorials/extending-the-bootstrapper)

### dotnet global toolの導入

build.sh,build.ps1を導入する方法は、OSごとに実行スクリプトが異なることになるので、CI/CDで回すときには面倒な場合も多い。
そこで、[dotnet global tool](https://docs.microsoft.com/en-us/dotnet/core/tools/global-tools)を導入して実行するやり方がある。
なお、**実行にはdotnet-sdk 2.1以降が必要**。

1. `dotnet tool install -g Cake.Tool` を実行する
2. PATH環境変数に、`$HOME/.dotnet/tools`フォルダを追加する
3. `dotnet-cake --help` でCake.exeのヘルプが表示されればOK

### dotnet global toolをプロジェクト下のフォルダに展開

上記dotnet global toolの導入の応用で、プロジェクト配下のフォルダにdotnet global toolの実行ファイルを入れる。
**実行にはdotnet-sdk 2.1以降が必要**。

1. `dotnet tool install --tool-path [実行ファイルを配置するフォルダ] Cake.Tool`
2. [実行ファイルを配置するフォルダ]で指定したフォルダに、dotnet-cake(.exe)という実行ファイルがあればOK

### dotnet local toolの導入

[dotnet-sdk-3.0から新しく導入された機能](https://stu.dev/dotnet-core-3-local-tools/)で、dotnet global toolをプロジェクトローカルに導入する方法。
実行時コマンドが冗長になるが、以下の利点がある。

* OSの違いを意識しなくて済む
* PATH変数を汚さなくて済む
* CakeBuildバージョンがプロジェクト内部で固定される
* git clone等で別マシンに展開しても、`dotnet tool restore`コマンドで、コマンドの復元ができる

なお、**実行にはdotnet-sdk 3.0以降が必要**。

初回インストールは以下のように行うことができる

1. `dotnet new tool-manifest` を実行
    * `.config/dotnet-tools.json`というファイルが作成される
    * 作成されたファイルはソース管理に入れる
2. `dotnet tool install Cake.Tool` を実行
3. `dotnet tool run dotnet-cake --help`で、Cake.exeのヘルプが表示されればOK

2回目以降、例えばCI/CDで使う場合は、

1. `dotnet tool restore`

を実行することで、`dotnet tool run dotnet-cake`が可能になる。
なお、dotnet-sdk 3.1以降では`tool run`部分の省略ができるようになったようなので、

* dotnet tool run dotnet-cake
* dotnet dotnet-cake
* dotnet cake

の三通りの実行の仕方ができるようになった。

## 除外の設定

build.ps1やbuild.shを使う場合、実行時にアセンブリをダウンロードして展開する。
その時にデフォルトで格納領域に使われるのが`tools`フォルダなので、ソース管理ツールを使用している場合、必ず`tools/*`は除外パスに指定しておこう。
ただし、後述するが、補完は`tools/packages.config`が必要なので、`tools/packages.config`は除外から外しておこう。
まとめると、以下のような設定となる

```text:.gitignore
tools/*
!tools/packages.config
```

## 実行スクリプトの作成

cakebuildが実行できる状態になったら、実行スクリプトを記述する。
実行スクリプトは、通常`.cake`という拡張子で、更に`build.cake`がデフォルトで最初に解釈されるファイルになるので、最初は`build.cake`ファイルを作成する。
例として以下のように記述する。詳しい構文は、[公式ドキュメントを参照](https://cakebuild.net/docs/fundamentals/)

```csharp:build.cake
// パラメーター設定
// 第一引数が名前、第二引数がデフォルト値
string target = Argument("Target", "Default");
// タスクの定義
Task("Default")
  .Does(() =>
  {
    Information("Hello World");
  });
  .Finally(() =>
  {
    // タスク終了後に必ず実行されるブロック
    Information("Finally");
  });
Task("Task1")
  // タスク実行するかどうかを決定できる
  .WithCriteria(() => true)
  .Does(() =>
  {
    Information("Task1");
  });
// 依存関係がある場合は、IsDependentOnを後につなげる
Task("Task2")
  .IsDependentOn("Task1")
  .Does(() =>
  {
    Information("Task2");
  });
// IsDependentOnとは逆に、特定タスクの依存元として指定することも可能
Task("Task3")
  .IsDependeeOf("Task1")
  .Does(() =>
  {
    Information("Task3");
  });
// 指定されたターゲットの実行。必須
RunTarget(target);
```

### プリプロセス

CakeBuildでは、`#`で始まるプリプロセスディレクティブで外部スクリプトの読出しや、外部パッケージの使用ができる。
詳しくは[プリプロセッサディレクティブに関する公式ドキュメント](https://cakebuild.net/docs/fundamentals/preprocessor-directives)を参照。

以下、よく使うものについて記述する。

#### 外部スクリプトのロード

大規模なビルドに対応するため、cakebuildではスクリプトファイルの読出しもサポートしている。
cakeファイルに以下のような記述を追加する。

```
// 呼び出し側のcakeファイルからの相対パスで指定する
#load "relative/path/to/x.cake"
```

なお、ロードは書かれた順に上から実行されるので注意。
面白いところでは、nugetパッケージからの読出しもサポートしている。
以下のように記述する。

```
// nugetパッケージの中に含まれている*.cakeファイルが全て読み込まれる
#load "nuget:?package=[パッケージ名]"
```

#### 拡張パッケージを使う

cakebuildでは、nugetパッケージを使って拡張を実現している。
拡張を使用するには、以下のように記述する。

```
#addin "nuget:?package=[パッケージ名]"
```

具体的に何を指定できるかについては、[CakeBuild公式リファレンス](https://cakebuild.net/dsl/)が詳しい。
また、Jil等の通常のnugetパッケージをスクリプトの中で使う場合も、`#addin`ディレクティブを使用する。

### パラメーターセットアップ

チュートリアルにもあるように、実行時に引数を受け取る場合、
`var arg = Argument(argname, defaultValue);`をグローバルに追加するのが手軽と言えば手軽で、チュートリアルでも使われている。
しかし、規模が大きくなり、複数ファイルに分割する状態になった場合、グローバルに変数が存在するのは都合が悪いと言えば悪い。

そこで、`Setup<T>(Func<ICakeContext, T> f)`で、渡すパラメーターを初期化すれば、`Task("t1").Does<T>(p => Information($"{p}");`のようにして、パラメーターを外部から受け取ることができ、かつグローバルにインスタンスを置くこともなくなる。

コード例は以下

```csharp
// タスクに渡すパラメーターの型
class TaskParameters
{
    public string Configuration;
}
// Setup<T>()で型付パラメーターのセットアップが可能
Setup<TaskParameters>(ctx => new TaskParameters()
  { Configuration = ctx.Argument("Configuration", "Debug") }
);
// Setup<T>()後は、Does<T>()またはWithCriteria<T>()で引数を指定することができる
Task("Task4").Does<TaskParameters>((p) =>
  {
    Information($"task4: {p.Configuration}");
  });
```

## 実行

スクリプトを作成したら、cakebuildのコマンドを実行する。
build.ps1であれば、`./build.ps1 -Target [ターゲット]`のように実行する。
dotnet-cakeで実行する場合は、`dotnet-cake -Target=[ターゲット]`のように指定する。
build.ps1と若干仕様がことなるので注意。
読み込む.cakeファイルは、デフォルトではカレントディレクトリの`build.cake`が使われるが、ファイルを指定する場合は、
`dotnet-cake path/to/script.cake ....`のように指定する。


# よく使う機能

個人的に筆者が良く使っている機能について記述する

## ファイルまたはディレクトリパスを合成する

例えば`System.IO.Path.Combine`のようなもの。cakebuildではFilePathとDirectoryPathでクラスが分かれているので注意。
`FilePath.FromString(path)`または`DirectoryPath.FromString(path)`でインスタンスを作成し、`Combine`でつなげていく。
例:

```csharp
var dir1 = DirectoryPath.FromString("a/b"); // a/b
var dir2 = dir1.Combine("c"); // a/b/c
var file1 = dir2.CombineWithFilePath("d.txt"); // a/b/c/d.txt
```

また、フォルダ以下全てのファイルのリストを取得したい場合、[GetFiles](https://cakebuild.net/api/Cake.Common.IO/GlobbingAliases/91154142)を使用する。

```csharp
// aというフォルダの拡張子".txt"というファイルを全て列挙する
var files = GetFiles("a/**/*.txt");
```

## 外部コマンドを実行する

[Process](https://cakebuild.net/dsl/process/)モジュールを使用する。
基本的には[実行ファイルパスと引数](https://cakebuild.net/api/Cake.Common/ProcessAliases/BB668900)を使うことになるだろう。

```csharp:build.cake
Task("ExecuteProcess")
    .Does(() =>
    {
        // 引数付きのコマンドを実行する場合
        var args = new ProcessArgumentBuilder()
            .Append("/c")
            .Append("echo")
            .Append("abcdefg")
            ;
        var procsetting = new ProcessSettings()
        {
            Arguments = args
        };
        using(var proc = StartAndReturnProcess(FilePath.FromString("cmd.exe"), procsetting))
        {
            proc.WaitForExit();
            Information("proc exit code is {0}", proc.GetExitCode());
        }
    });
RunTarget("ExecuteProcess");
```

## dotnet-sdk付属の`dotnet`コマンドを実行する

[DotNetCore](https://cakebuild.net/dsl/dotnetcore/)モジュールを使用する。

大体`DotNetCore[コマンド]`という構成になっている。

```csharp:build.cake
Task("DotNetClean")
    .Does(() =>
    {
        // パスはcakeファイルからの相対パス
        DotNetCoreClean("./sampleproject/sampleproject.csproj");
    });
RunTarget("DotNetClean");
```

## MSBuildを実行する

[MSBuild](https://cakebuild.net/dsl/msbuild/)モジュールを使用する。オーバーライドがいくつかあるが、
結局MSBuildSettingsを渡す所に集約するので、今回はこれについて書く。

VS付属のものか、`msbuild`コマンドにパスが通っていれば、そちらを使用する。
dotnet-sdkに付属のmsbuild(`dotnet msbuild`)を使用したい場合は、[DotNetCoreMSBuild](https://cakebuild.net/api/Cake.Common.Tools.DotNetCore/DotNetCoreAliases/E4F74647)を使用する。

### MSBuildSettings

パラメーターは多いが、よく使うのは以下。

* Configuration: "Release","Debug"等のこと
* EnvironmentVariables: 追加の環境変数
* Properties: MSBuildに渡すプロパティ。値の型が`IList<string>`なことに注意
* [ToolVersion](https://cakebuild.net/api/Cake.Common.Tools.MSBuild/MSBuildToolVersion/): どのMSBuildを使用するか
* [BinaryLogger](https://cakebuild.net/api/Cake.Common.Tools.MSBuild/MSBuildBinaryLogSettings/): [MSBuildのバイナリログ](https://github.com/microsoft/msbuild/blob/master/documentation/wiki/Binary-Log.md)出力設定

## ZIPファイルを作成する

ビルド成果物等をzipに纏めたい場合は、[Cake.Compression](https://cakebuild.net/dsl/compression/)モジュールを使用する。とりあえず良く使うのは[フォルダごとアーカイブするパターン](https://cakebuild.net/api/Cake.Common.IO/ZipAliases/B6C83EAE)だろうか。
ただし、これで作成したアーカイブはルートディレクトリを取り除いた状態で作成されるので、うっかり展開するとカレントディレクトリにファイルをぶちまけてしまう可能性があるので要注意

```csharp
#addin nuget:?package=SharpZipLib
#addin nuget:?package=Cake.Compression

...
  // a/b/c.txt, a/d.txtというファイルがフォルダに存在する場合、
  // b/c.txt, d.txtというファイルがoutput.zipファイルに圧縮される
  Zip(DirectoryPath.FromString("a"), "output.zip");
...
```

# VSCodeの補完

以下の条件を満たすことにより、vscodeで補完を効かせることもできる。

* build.ps1またはbuild.shを使用している
* [Cake.Bakery.exe](https://github.com/cake-build/bakery)が存在する
    * tools/Cake.Bakeryが存在する
        * tools/packages.configのpackages/packageに`id="Cake.Bakery"`、`version="[nugetバージョン]"`を追加し、`build.ps1 -DryRun`または`build.sh --dryrun`を実行すれば取得できる
        * また、build.cake等に`#tool nuget:?package=Cake.Bakery&version=[nugetバージョン]`を記載し、`dotnet-cake build.cake --dryrun`でも`tools`フォルダに追加される
    * [omnisharp.jsonの"/cake/BakeryPath"](https://github.com/OmniSharp/omnisharp-roslyn/wiki/Configuration-Options#cake)でバイナリを指定できれば、packages.configへの記述は不要
    * Cake.BakeryをDL後はomnisharpを再起動する必要がある
* [Cake](https://www.nuget.org/packages/Cake/)がtools以下に存在する
    * `#tool`ディレクティブだと[明示的にエラーにされる](https://github.com/cake-build/cake/blob/630eb9a15f02e09351a6aa1e049924760348e24a/src/Cake.NuGet/Install/NuGetPackageInstaller.cs#L47-L53)ので、build.ps1等の実行が必要
* vscodeのC#拡張をインストールする
* linuxまたはmacの場合、monoがインストールされている

# 終りに

普段はIDEで開発してれば良いのでそれほど使う部分もないかもしれない。
また、やはりシェルスクリプト等に比べると冗長になる部分は否めないため、
単独のcsprojしかないような場合だと、割に合わない部分もあるかもしれない。
それでも、ある程度の規模でCI/CDを回す時などは、プラットフォーム間の差異をある程度吸収できるため、非常に役に立つツールだと思う。

機能的には基本的な部分しか書いていないため、気が向いたらまた別の記事を書きたいと思う。

## 参考リンク

* [公式ドキュメントトップ](https://cakebuild.net/docs/)
* [構文等について](https://cakebuild.net/docs/fundamentals/)
* [プリプロセスについて](https://cakebuild.net/docs/fundamentals/preprocessor-directives)