始めに
以前CakeBuildについて全体的な記事を書いたが、Taskについてあまり掘り下げていなかったと思うので、この記事でもう少し詳しく書きたいと思う。
Taskとは
一言でいうと、CakeBuildにおける処理の一塊のこと。
CakeBuildではこのTaskを単位に成功、失敗判定を行う。
Taskの登録
まず初めにしなければならないことは、TaskをCakeBuildのコンテキストに登録することである。
これは、Task("TaskName");
とすることで、TaskName
というTaskを登録できる。
登録しただけでは何もしないので、Task("TaskName")
の後にメソッドチェーンの形でどんどん設定を追加していく。
Taskの名前に文字種制限は特にないので、グループ化したい場合は"."や"/"で区切ることもできる。
タスクのリストを取得したい場合、ICakeContext.Tasks
経由で取得が可能で、グローバルで暗黙的に使用可能。
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を実行するという設定を追加する。
Task("Task1");
Task("Task2")
.IsDependentOn("Task1");
上記のようにすれば、Task1
が成功した後、Task2
を実行する、という動作になる。
Task1
が失敗した場合、それ以降のTask(Task2
)は実行されない。
IsDependeeOf("TaskName")
IsDependentOn
とは逆の動作をする。つまり、指定したタスクの前に実行し、失敗すればそれ以後のタスクを行わなくなる、というような動作になる。
Task("Task1");
Task("Task2")
.IsDependeeOf("Task1");
上記のようにすれば、Task2
がTask1
の前に実行され、失敗すればそれ以降のタスクは行わないという動作になる。
使用場面としては、何か特定のタスクにフック的に動作させたい場合だろうか。
タスクが重複した場合
タスクが増えてくると、下記のように同じタスクが依存ツリーに重複して出てくる場合がある。
Task("Task1");
Task("Task2").IsDependentOn("Task1");
Task("Task3")
// Task1の依存が重複した
.IsDependentOn("Task1")
.IsDependentOn("Task2");
上記のようになった場合も、cakebuildの方で関係を整理してくれるので、実行はTask1
→Task2
→Task3
となり、Task1
は重複して実行されない。
また、下記のように循環依存になった場合は、実行時エラーとなる。
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)
となる。具体的には以下
Task("TaskName").Does(() => Information("Hello World"));
また、Does(Func<System.Threading.Tasks.Task> act)
という形も取れるため、async awaitも可能
Task("TaskName").Does(async () => await System.Threading.Tasks.Task.Delay(100));
なお、System.Threading.Tasks
名前空間はデフォルトでusingされてないので注意。
また、他のDoes
にも言えることだが、以下のように複数繋げることも可能。
複数繋げた場合、書かれた順番に実行がされる。
Task("TaskName")
.Does(() => Information("1"))
.Does(() => Information("2"))
;
型付の引数をとる形
また、自分で設定した型を取ることができる。その場合は以下のような手順となる。
- パラメーターとなる型を宣言
-
Setup<T>(Func<ISetupContext, T> act)
でパラメーターの実体を作る -
Does<T>(Action<T> act)
でパラメーターを受け取る
コードは以下のようになる。
// 型の宣言
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
を引数に取ることもできる。
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>
の個別の要素について、タスクが実行できる。
Task("TaskName").DoesForEach(GetFiles("*.txt"), fpath => Information($"{fpath}"));
条件によって実行する、しないを決定する
例えばあるフラグが渡された場合や、特定のファイルがある場合等、条件によってタスクを実行する/しないを決定したい場合がある。
そういう時はWithCriteria
を使用する。
これもDoes
同様、型付引数をとれるもの、ICakeContext
をとれるオーバーライドが存在する。
// "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
を使用する。
Task("Task1")
.Finally(() => Information("Finally"));
ただし、依存元のTaskが失敗した場合は実行されずに終わるので注意。
Task("Task1").Does(() => throw new Exception(""));
Task("Task2").IsDependentOn("Task1").Finally(() => Information("Finally"));
上記の場合、Task2
のFinally
が実行されずに終了する。
例外を捕捉する
独自に処理する
Error
を使えば、Task内で例外が起こった場合、ブロック内でキャッチして無視することもできる。
Task("TaskName").OnError(er => Warning($"{er}"));
ただし、これは同一Task内でのみ有効な設定なので、依存元のTaskの中でエラーが出た場合は、そのまま終了してしまうので注意
Task("Task1").Does(() => throw new Exception(""));
Task("Task2")
.IsDependentOn("Task1")
.OnError(er => Warning($"{er}"));
上記の場合、Task2
のOnErrorまでは来ないで、Task1の実行完了時点で止まる。
ログ出力のみして継続する
ログ出力のみして継続したいのであれば、ContinueOnError
を指定するだけで良い。
Task("TaskName").ContinueOnError();
独自に処理してそのままエラー終了する
エラーを受け取ってログ出力するだけで、そのまま終了させたい場合、ReportOnError
を使用する。
Task("TaskName").ReportOnError(er => Warning($"{er}"));
最後まで実行してからエラーとして処理する
エラーが起こった場合、同一タスク内のアクションは通常スキップされるが、そうしたくない場合は、DeferOnError
を使う。
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
を受け取ることができるので、これを元にカスタム出力を行うことができる。
ただし、エラーの場合は例外が送出されるため、結果が受け取れないことに注意
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は、大まかに
- グローバル処理
- RunTarget
- Setup
- TaskSetup
- Task
- TaskTeardown
- Teardown
- Setup
の順で処理が実行される。
Setupは全てのTaskが実行される前に、全体で一回だけ実行される処理で、主に型付きパラメーターの登録等を行う。
逆にTeardownは、全てのTaskが実行された後に一回だけ実行される処理となる。
TaskSetup/TaskTeardownは、各Taskが実行される前と後で実行される。
Teardown/TaskTeardownは、Setup及びTaskが失敗したとしても実行される。
コード例は以下のようになる。
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()
だけでそれなりに書けたりする。
後は気が向いたら自分なりのベストプラクティスなんか書ければいいかなと思っている。