CakeBuildのTaskについて

# 始めに

以前[CakeBuild](https://cakebuild.net)について[全体的な記事](https://qiita.com/skitoy4321/items/e76b178dd7fca1fbbd8f)を書いたが、Taskについてあまり掘り下げていなかったと思うので、この記事でもう少し詳しく書きたいと思う。

# Taskとは

一言でいうと、CakeBuildにおける処理の一塊のこと。
CakeBuildではこのTaskを単位に成功、失敗判定を行う。

## Taskの登録

まず初めにしなければならないことは、TaskをCakeBuildのコンテキストに登録することである。
これは、`Task("TaskName");`とすることで、`TaskName`というTaskを登録できる。
登録しただけでは何もしないので、`Task("TaskName")`の後にメソッドチェーンの形でどんどん設定を追加していく。

Taskの名前に文字種制限は特にないので、グループ化したい場合は"."や"/"で区切ることもできる。

タスクのリストを取得したい場合、`ICakeContext.Tasks`経由で取得が可能で、グローバルで暗黙的に使用可能。

```csharp
Task("Task1/a");
Task("Task1/b");
Task("Task2/c");

// "Task1"から始まるタスクのみ実行する
foreach(var t in Tasks.Where(x => x.Name.StartsWith("Task1"))
{
    RunTarget(t.TaskName);
}
```

`cake --showtree`で、依存関係等も含めてタスクが一覧表示される。

## Taskの依存関係

例えば`Task1`を実行した後に`Task2`を実行したい等、タスク間で依存関係を作りたい場合は、`IsDependentOn`と`IsDependeeOf`の二つの方法がある。

### `IsDependentOn("TaskName")`

指定した名前のTaskが成功した後に、Taskを実行するという設定を追加する。

```csharp
Task("Task1");
Task("Task2")
  .IsDependentOn("Task1");
```

上記のようにすれば、`Task1`が成功した後、`Task2`を実行する、という動作になる。
`Task1`が失敗した場合、それ以降のTask(`Task2`)は実行されない。

### `IsDependeeOf("TaskName")`

`IsDependentOn`とは逆の動作をする。つまり、指定したタスクの前に実行し、失敗すればそれ以後のタスクを行わなくなる、というような動作になる。

```csharp
Task("Task1");
Task("Task2")
  .IsDependeeOf("Task1");
```

上記のようにすれば、`Task2`が`Task1`の前に実行され、失敗すればそれ以降のタスクは行わないという動作になる。
使用場面としては、何か特定のタスクにフック的に動作させたい場合だろうか。

### タスクが重複した場合

タスクが増えてくると、下記のように同じタスクが依存ツリーに重複して出てくる場合がある。

```csharp
Task("Task1");
Task("Task2").IsDependentOn("Task1");
Task("Task3")
  // Task1の依存が重複した
  .IsDependentOn("Task1")
  .IsDependentOn("Task2");
```

上記のようになった場合も、cakebuildの方で関係を整理してくれるので、実行は`Task1`→`Task2`→`Task3`となり、`Task1`は重複して実行されない。

また、下記のように循環依存になった場合は、実行時エラーとなる。

```csharp
Task("Cyclic/1").IsDependentOn("Cyclic/3");
Task("Cyclic/2").IsDependentOn("Cyclic/1");
Task("Cyclic/3").IsDependentOn("Cyclic/2");
```

## Taskの実行内容の登録

Taskに実際の動作を登録するには、`Does`を`Task("TaskName")`の後に続ける。
この時、指定できる形は以下のようになる。

### 最も単純な形

最も単純な形は、`Does(Action act)`となる。具体的には以下

```csharp
Task("TaskName").Does(() => Information("Hello World"));
```

また、`Does(Func<System.Threading.Tasks.Task> act)`という形も取れるため、async awaitも可能

```csharp
Task("TaskName").Does(async () => await System.Threading.Tasks.Task.Delay(100));
```

なお、`System.Threading.Tasks`名前空間はデフォルトでusingされてないので注意。

また、他の`Does`にも言えることだが、以下のように複数繋げることも可能。
複数繋げた場合、書かれた順番に実行がされる。

```csharp
Task("TaskName")
  .Does(() => Information("1"))
  .Does(() => Information("2"))
  ;
```

### 型付の引数をとる形

また、自分で設定した型を取ることができる。その場合は以下のような手順となる。

1. パラメーターとなる型を宣言
2. `Setup<T>(Func<ISetupContext, T> act)`でパラメーターの実体を作る
3. `Does<T>(Action<T> act)`でパラメーターを受け取る

コードは以下のようになる。

```csharp
// 型の宣言
class MyClass
{
    public string X;
}
Setup<MyClass>(ctx => new MyClass(){ X = ctx.Argument("X", "") });
Task("TaskName").Does<MyClass>(x => Information($"{x.X}");
```

なお、`Setup`と`Task`を書く順番は、前後しても問題ないが、同じ型で二回`Setup<T>()`すると実行時にエラーになる。

### `ICakeContext`をとる形

スクリプト全体の状態を保持する`ICakeContext`を引数に取ることもできる。

```csharp
Task("TaskName").Does(
  ctx => Information("Hello {0}", ctx.Argument("X", "default")));
Task("TaskName").Does<MyClass>(
  (ctx, x) => Information("Hello {0}, {1}", ctx.Argument("X", "default"), x.X));
```

### コレクションの個別要素を引数にとる形

`DoesForEach`を使用することにより、`IEnumerable<T>`の個別の要素について、タスクが実行できる。

```csharp
Task("TaskName").DoesForEach(GetFiles("*.txt"), fpath => Information($"{fpath}"));
```

## 条件によって実行する、しないを決定する

例えばあるフラグが渡された場合や、特定のファイルがある場合等、条件によってタスクを実行する/しないを決定したい場合がある。
そういう時は`WithCriteria`を使用する。
これも`Does`同様、型付引数をとれるもの、`ICakeContext`をとれるオーバーライドが存在する。

```csharp
// "IsRelease"が引数に来た場合に実行されるようにする
Task("Task1")
  .WithCriteria(() => Argument<bool>("IsRelease", false))
  .Does(() => Information("release task"));
Task("Task2")
  .IsDependentOn("Task1")
  .Does(() => Information("release task"));
```

上記の通りにすれば、`dotnet cake -IsRelease -Target=Task2`とされた場合のみ`Task1`が実行される。

ただし、**`RunTarget`で指定したターゲットそのものがCriteriaがfalseになってスキップされると、cakebuildはタスク全体を失敗とみなすので注意が必要。**

よって、`WithCriteria`を使うTaskは、直接Targetに指定しない方が良い。

## 必ず最後に実行される処理を入れる

try - finallyのように、エラー如何に関わらずTaskの最後に実行したい処理がある場合、`Finally`を使用する。

```csharp
Task("Task1")
  .Finally(() => Information("Finally"));
```

ただし、依存元のTaskが失敗した場合は実行されずに終わるので注意。

```csharp
Task("Task1").Does(() => throw new Exception(""));
Task("Task2").IsDependentOn("Task1").Finally(() => Information("Finally"));
```

上記の場合、`Task2`の`Finally`が実行されずに終了する。

## 例外を捕捉する

### 独自に処理する

`Error`を使えば、Task内で例外が起こった場合、ブロック内でキャッチして無視することもできる。

```csharp
Task("TaskName").OnError(er => Warning($"{er}"));
```

ただし、これは同一Task内でのみ有効な設定なので、依存元のTaskの中でエラーが出た場合は、そのまま終了してしまうので注意

```csharp
Task("Task1").Does(() => throw new Exception(""));
Task("Task2")
  .IsDependentOn("Task1")
  .OnError(er => Warning($"{er}"));
```

上記の場合、`Task2`のOnErrorまでは来ないで、Task1の実行完了時点で止まる。

### ログ出力のみして継続する

ログ出力のみして継続したいのであれば、`ContinueOnError`を指定するだけで良い。

```csharp
Task("TaskName").ContinueOnError();
```

### 独自に処理してそのままエラー終了する

エラーを受け取ってログ出力するだけで、そのまま終了させたい場合、`ReportOnError`を使用する。

```csharp
Task("TaskName").ReportOnError(er => Warning($"{er}"));
```

### 最後まで実行してからエラーとして処理する

エラーが起こった場合、同一タスク内のアクションは通常スキップされるが、そうしたくない場合は、`DeferOnError`を使う。

```csharp
Task("TaskName")
  .Does(() => throw new Exception(""))
  .Does(() => Information("1")
  .Does(() => Information("2")
  .DeferOnError();
```

上記のようにすると、`.Does(() => Information("1")`と`.Does(() => Information("2")`の部分が実行されてからエラーとして処理される。`ReportOnError()`または`OnError()`と併用することも可能。

## Taskの実行

`RunTarget("TaskName")`とすることで、コンテキストに登録されたタスクを実行する。
固定で引数を指定することもできるが、大抵の場合引数で分けたいはずなので、`RunTarget(Argument("Target", "Default"))`のような形になる。
この時、コンソールにログは出るが、更に実行結果をXMLに出力したいなどの特殊な処理をしたい場合、戻り値として`CakeReport`を受け取ることができるので、これを元にカスタム出力を行うことができる。
ただし、**エラーの場合は例外が送出されるため、結果が受け取れないことに注意**

```csharp
var report = RunTarget("TaskName");
foreach(var entry in report)
{
    // Task,Setup,Teardownのどれか
    Information($"{entry.Category}");
    // 所要時間
    Information($"{entry.Duration}");
    // Executed, Delegated, Skippedのどれか
    Information($"{entry.ExecutionStatus}");
    // タスクの名前
    Information($"{entry.TaskName}");
}
```

## SetupとTeardown

CakeBuildは、大まかに

1. グローバル処理
2. RunTarget
    1. Setup
        1. TaskSetup
        2. Task
        3. TaskTeardown
    2. Teardown

の順で処理が実行される。

Setupは全てのTaskが実行される前に、全体で一回だけ実行される処理で、主に型付きパラメーターの登録等を行う。
逆にTeardownは、全てのTaskが実行された後に一回だけ実行される処理となる。
TaskSetup/TaskTeardownは、各Taskが実行される前と後で実行される。
Teardown/TaskTeardownは、Setup及びTaskが失敗したとしても実行される。

コード例は以下のようになる。

```csharp
Task("Default");

Setup(ctx => Information($"global setup"));
TaskSetup(ctx => Information($"task setup: {ctx.Task.Name}"));
TaskTeardown(ctx => Information($"task teardown: {ctx.Task.Name}, {ctx.Successful}, {ctx.Skipped}, {ctx.ThrownException}"));
Teardown(ctx => Information($"global teardown {ctx.Successful}, {ctx.ThrownException}"));

RunTarget(Argument("Target", "Default"));
```

なお、**全てのSetup,Teardown,TaskSetup,TaskTeardownの登録は、RunTargetの前に行う必要がある**ので注意。

# 終りに

これで一通りのTaskに関する記述は出来たと思う。色々書いたけど、まあ`Task()`、`IsDependentOf()`、`Does()`だけでそれなりに書けたりする。
後は気が向いたら自分なりのベストプラクティスなんか書ければいいかなと思っている。