始めに
dotnetプロジェクトを作るにあたり、多くの人はIDEを用いて開発していると思う。しかし、以下のように、IDEでは面倒な作業はどうしても存在する。
- ビルドした後アーカイブにまとめる
- 複数の設定を一度にビルドあるいはテストを行う
- PlatformとかConfigurationとかTargetFrameworkとか
- ビルド結果を受けて更に別の外部コマンドを実行する
- VSのカスタムビルドステップだとプラットフォーム依存が発生する
- XML,JSONファイルの編集
また、CI/CD等を使用する場合に、シェルスクリプトの直接実行に頼ると、プラットフォームの違いを意識する部分が多くなり、ビルドスクリプトが複雑になりがちになる。
dotnetならMSBuildがあるじゃないかという意見もあるだろうが、MSBuildはスクリプトを書くのに向いていないツールなので、複雑なことをすると非常に辛くなる。
そこで、それらの問題を解決するために、Cake Buildというツールを紹介する。
F#erな人のために、FAKEなるツールもあり、アドベントカレンダーで記事にもなっているようなので、
興味のある人はこちらも見てみるといいかもしれない。
特徴
以下のような特徴を持つ
- C#のような構文でビルドスクリプトが記述可能
- csharp scripting + cakebuild独自のプリプロセス構文
- その他、基本的なルール等は公式ドキュメントを参照
- nugetで外部ライブラリが使用可能
-
拡張も豊富
- 自分でももちろん書ける
必要なもの
以下のいずれかが必要
- dotnet-sdk 2.1以降
- .NET Framework(Windows)とpowershell
- mono
使い方
CakeBuild実行ファイルの導入
まず、cakebuildの実行ファイルをプロジェクトに導入する。
インストールはいくつかの手段がある。
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では未検証)。
dotnet global toolの導入
build.sh,build.ps1を導入する方法は、OSごとに実行スクリプトが異なることになるので、CI/CDで回すときには面倒な場合も多い。
そこで、dotnet global toolを導入して実行するやり方がある。
なお、実行にはdotnet-sdk 2.1以降が必要。
-
dotnet tool install -g Cake.Tool
を実行する - PATH環境変数に、
$HOME/.dotnet/tools
フォルダを追加する -
dotnet-cake --help
でCake.exeのヘルプが表示されればOK
dotnet global toolをプロジェクト下のフォルダに展開
上記dotnet global toolの導入の応用で、プロジェクト配下のフォルダにdotnet global toolの実行ファイルを入れる。
実行にはdotnet-sdk 2.1以降が必要。
dotnet tool install --tool-path [実行ファイルを配置するフォルダ] Cake.Tool
- [実行ファイルを配置するフォルダ]で指定したフォルダに、dotnet-cake(.exe)という実行ファイルがあればOK
dotnet local toolの導入
dotnet-sdk-3.0から新しく導入された機能で、dotnet global toolをプロジェクトローカルに導入する方法。
実行時コマンドが冗長になるが、以下の利点がある。
- OSの違いを意識しなくて済む
- PATH変数を汚さなくて済む
- CakeBuildバージョンがプロジェクト内部で固定される
- git clone等で別マシンに展開しても、
dotnet tool restore
コマンドで、コマンドの復元ができる
なお、実行にはdotnet-sdk 3.0以降が必要。
初回インストールは以下のように行うことができる
-
dotnet new tool-manifest
を実行-
.config/dotnet-tools.json
というファイルが作成される - 作成されたファイルはソース管理に入れる
-
-
dotnet tool install Cake.Tool
を実行 -
dotnet tool run dotnet-cake --help
で、Cake.exeのヘルプが表示されればOK
2回目以降、例えばCI/CDで使う場合は、
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
は除外から外しておこう。
まとめると、以下のような設定となる
tools/*
!tools/packages.config
実行スクリプトの作成
cakebuildが実行できる状態になったら、実行スクリプトを記述する。
実行スクリプトは、通常.cake
という拡張子で、更にbuild.cake
がデフォルトで最初に解釈されるファイルになるので、最初は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では、#
で始まるプリプロセスディレクティブで外部スクリプトの読出しや、外部パッケージの使用ができる。
詳しくはプリプロセッサディレクティブに関する公式ドキュメントを参照。
以下、よく使うものについて記述する。
外部スクリプトのロード
大規模なビルドに対応するため、cakebuildではスクリプトファイルの読出しもサポートしている。
cakeファイルに以下のような記述を追加する。
// 呼び出し側のcakeファイルからの相対パスで指定する
#load "relative/path/to/x.cake"
なお、ロードは書かれた順に上から実行されるので注意。
面白いところでは、nugetパッケージからの読出しもサポートしている。
以下のように記述する。
// nugetパッケージの中に含まれている*.cakeファイルが全て読み込まれる
#load "nuget:?package=[パッケージ名]"
拡張パッケージを使う
cakebuildでは、nugetパッケージを使って拡張を実現している。
拡張を使用するには、以下のように記述する。
#addin "nuget:?package=[パッケージ名]"
具体的に何を指定できるかについては、CakeBuild公式リファレンスが詳しい。
また、Jil等の通常のnugetパッケージをスクリプトの中で使う場合も、#addin
ディレクティブを使用する。
パラメーターセットアップ
チュートリアルにもあるように、実行時に引数を受け取る場合、
var arg = Argument(argname, defaultValue);
をグローバルに追加するのが手軽と言えば手軽で、チュートリアルでも使われている。
しかし、規模が大きくなり、複数ファイルに分割する状態になった場合、グローバルに変数が存在するのは都合が悪いと言えば悪い。
そこで、Setup<T>(Func<ICakeContext, T> f)
で、渡すパラメーターを初期化すれば、Task("t1").Does<T>(p => Information($"{p}");
のようにして、パラメーターを外部から受け取ることができ、かつグローバルにインスタンスを置くこともなくなる。
コード例は以下
// タスクに渡すパラメーターの型
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
でつなげていく。
例:
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を使用する。
// aというフォルダの拡張子".txt"というファイルを全て列挙する
var files = GetFiles("a/**/*.txt");
外部コマンドを実行する
Processモジュールを使用する。
基本的には実行ファイルパスと引数を使うことになるだろう。
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モジュールを使用する。
大体DotNetCore[コマンド]
という構成になっている。
Task("DotNetClean")
.Does(() =>
{
// パスはcakeファイルからの相対パス
DotNetCoreClean("./sampleproject/sampleproject.csproj");
});
RunTarget("DotNetClean");
MSBuildを実行する
MSBuildモジュールを使用する。オーバーライドがいくつかあるが、
結局MSBuildSettingsを渡す所に集約するので、今回はこれについて書く。
VS付属のものか、msbuild
コマンドにパスが通っていれば、そちらを使用する。
dotnet-sdkに付属のmsbuild(dotnet msbuild
)を使用したい場合は、DotNetCoreMSBuildを使用する。
MSBuildSettings
パラメーターは多いが、よく使うのは以下。
- Configuration: "Release","Debug"等のこと
- EnvironmentVariables: 追加の環境変数
- Properties: MSBuildに渡すプロパティ。値の型が
IList<string>
なことに注意 - ToolVersion: どのMSBuildを使用するか
- BinaryLogger: MSBuildのバイナリログ出力設定
ZIPファイルを作成する
ビルド成果物等をzipに纏めたい場合は、Cake.Compressionモジュールを使用する。とりあえず良く使うのはフォルダごとアーカイブするパターンだろうか。
ただし、これで作成したアーカイブはルートディレクトリを取り除いた状態で作成されるので、うっかり展開するとカレントディレクトリにファイルをぶちまけてしまう可能性があるので要注意
#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が存在する
- 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
フォルダに追加される
- tools/packages.configのpackages/packageに
- omnisharp.jsonの"/cake/BakeryPath"でバイナリを指定できれば、packages.configへの記述は不要
- Cake.BakeryをDL後はomnisharpを再起動する必要がある
- tools/Cake.Bakeryが存在する
-
Cakeがtools以下に存在する
-
#tool
ディレクティブだと明示的にエラーにされるので、build.ps1等の実行が必要
-
- vscodeのC#拡張をインストールする
- linuxまたはmacの場合、monoがインストールされている
終りに
普段はIDEで開発してれば良いのでそれほど使う部分もないかもしれない。
また、やはりシェルスクリプト等に比べると冗長になる部分は否めないため、
単独のcsprojしかないような場合だと、割に合わない部分もあるかもしれない。
それでも、ある程度の規模でCI/CDを回す時などは、プラットフォーム間の差異をある程度吸収できるため、非常に役に立つツールだと思う。
機能的には基本的な部分しか書いていないため、気が向いたらまた別の記事を書きたいと思う。