調べてみたのですが、日本語の文献がほとんどなかったので、ここに記しておきます。
間違いなどありましたら、指摘していただけますと幸いです。
予備知識
Paket
とFAKE
の説明の前に、dotnetの予備知識などを少々。
管理単位「ソリューション」と「プロジェクト」
プロジェクトは個々に別々のプログラムを指します。例えばアプリケーション本体とかテストプログラムとか。
ソリューションは、プロジェクトをまとめた単位です。
monoアプリケーションとその実行について
dotnetでは色々な*.exe
を必要とすることが多いのですけど、*.exe
の形式になってるのは大抵monoのアプリケーションです。
そういうプログラムは(少なくともMacでは)単体での実行ができないので(Windowsではどうか知らない)、次のようにmonoコマンド
を経由する必要があります。
mono some_app.exe
Paketのインストール
ちょっと紛らわしいですが、Packetではなく、Paketです(間の「c」がいらない)。
グローバルにインストールするなら
dotnet tool install --global Paket --version 5.215.0
とします。
しかし通常はソースをcloneした人が個別にそんなことをしなくてもいいように、paket.bootstrapper.exe
というものを提供する方式が一般的なようです。
その場合どうやって運用するかというと、
- ソリューションのルートディレクトリに
.paket
というディレクトリを作る。 .paket
の中に、公式が配布しているpaket.bootstrapper.exe
というファイルをpaket.exe
という名前で保存する(後述するMagic Mode
)。- で、
mono .paket/paket.exe init
とすると、Paket
を実行するために必要なものが用意される。
Magic Modeについて
objectxさんに教わったのですが、.paket
ディレクトリにpaket.bootstrapper.exe
をpaket.exe
という名前で置いて、それをあたかも本物のpaket.exe
のように使用するのはMagic Mode
と呼ばれる使い方です。
Magic Mode
のpaket.exe
(実体はpaket.bootstrapper.exe
で70kbyteぐらいしかない)は、初めて起動したときに本体のpaket.exe
(8Mbyteもある)をネット越しにとってきて、dotnetの一時ディレクトリに展開します。
で、それ以降はpaket.exe
(実際はpaket.bootstrapper.exe
)に対するコマンドは、その本体に対して送られるようになる、ということみたいです。
本体ではないのに、本体のように使えるからMagic Mode
ということなんだと思います。
なんでそんな事をするかというと、
- 本体の
paket.exe
は頻繁に更新されるし、サイズもでかいのでリポジトリに入れるのはしんどい。 - めったに更新されない
paket.bootstrapper.exe
をpaket.exe
としてリポジトリに入れておき、それを経由してpaket.exe
(本体)を実行する仕組みにすれば、paket.exe
(本体)の更新に強くなる。
といった理由だと思います。
ちなみにPaketのドキュメントでpaket.exe
と記述されているときは、それがMagic Mode
のpaket.exe
(つまり本当はpaket.bootstrapper.exe
)のことを指すのか、本体のpaket.exe
なのかを、文脈で判断する必要があって、ちょっとややこしいです。
Paketの依存性管理ファイル群
色々な管理ファイルがありますが、先に説明したソリューションとプロジェクトという単位を知らないと混乱します。
paket.dependencies
paket.dependenciesはソリューション単位の依存関係を記述するファイルです。ソリューションはプロジェクトの集合でしたから、つまりは全プロジェクトを横断して必要なパッケージを記述するファイルになります。
このファイルでの依存性の書き方はThe packet.dependencies fileを参照して下さい。
ちなみに自分の場合は、objectxさんに全ておまかせしてしまいました(笑)。
paket.references
paket.referencesはプロジェクト単位の依存性を記述するファイルです。当該プロジェクトで必要とする依存性だけを記述します。
先のpaket.dependencies
とは違い、単にパッケージ名を羅列するだけで良いみたいです。
例えばTypeNovelの場合、コンパイラ本体のプログラム(プロジェクト名はTnc
)のpaket.references
はこんな感じです。
FSharp.Core Argu
で、コンパイラが参照するTypeNovel用のライブラリ(プロジェクト名はLib
)だとこうなっています。
FSharp.Core FSharp.Data FsLexYacc
paket.lock
これは後述するコマンド(packet updateやpacket restoreなど)を利用して依存性を更新すると、自動的に作られるファイルです。
paket.dependencies
に記述したすべてのパッケージが間接的に必要とするものまで含めて、すべての依存性がこのファイルに記述されます。
gitなどでは、依存性をはっきりと固定させるために、commitの対象にすることが推奨されています(参考:Why should I commit the lock file?)。
.paket/Paket.Restore.targets
paket.lock
と同様に、パッケージの更新やインストールによって、自動で更新されるファイルです。
.paket/Paket.Restore.targets
は、*.fsproj
(各プロジェクトのルートディレクトリにあるプロジェクト構成ファイル)にて、次のように取り込まれます。
<?xml version="1.0" encoding="utf-8"?> <Project Sdk="Microsoft.NET.Sdk"> <PropertyGroup> <OutputType>Exe</OutputType> <TargetFramework>netcoreapp2.2</TargetFramework> </PropertyGroup> <ItemGroup> <Compile Include="Program.fs" /> </ItemGroup> <ItemGroup> <ProjectReference Include="..\Lib\Lib.fsproj" /> </ItemGroup> <Import Project="..\.paket\Paket.Restore.targets" /> </Project>
最終行の<Import Project="..\.paket\Paket.Restore.targets" />
というタグがそれです。
このタグによって、使用しているパッケージへの参照が通ります。
パッケージの更新処理など
グローバルにすでにpaketがインストールされてるならpaket [コマンド名]
でいいのですが、そうじゃない場合は、先に説明したMagic Mode
での実行、つまりmono .paket/paket.exe [コマンド名]
として下さい。以降は記述を簡潔にするため、グローバルにインストールされている体裁で説明します。
色々とコマンドがあるのですが、ようするにpaket.lock
というファイルの扱いがそれぞれ異なるからコマンド名が分かれている、と考えればわかりやすいです。
paket outdated
新しいバージョンが用意されているパッケージの一覧を表示してくれます。
一覧するだけなので、paket.lock
ファイルには影響がありません。
paket install
paket.lock
の内容に従い依存関係を調べ、必要なパッケージをインストールしてくれます。
ただしpaket.dependencies
に変更があった場合は、先にpaket.lock
の更新処理が入ります。
paket update
現在の依存関係を更新して、必要ならば新しいパッケージをインストールします。
paket update
とするとすべての依存関係、paket update [パッケージID]
とすると、指定したパッケージだけが更新されます。
このコマンドを走らせると、まず最初にpaket.lock
ファイルが削除されて、一から作り直されます。
で、この新しいpaket.lock
を元に(必要ならば)新しいパッケージがインストールされます。
先のpaket install
とは違い、paket.lock
を作り直してからインストールすることに注意して下さい。
paket restore
このコマンドを走らせると、paket.lock
ファイルが更新されます。ただし更新されるだけで、インストールまではされません。
つまりpaket update
= paket restore
+ paket install
です。
ちなみになにも引数を与えないとpaket.lock
に対しての更新になりますが、
次のように--references-file
オプションで各プロジェクトのpaket.references
を指定することもできます。
paket restore --references-file App/paket.references paket install
FAKEについて
FAKE
はF#のMakeです。ドメイン固有言語で記述するということになっていますが、実体は普通のF#コードです。
概念についても通常のMakeと同じで、それぞれの生成物(Target
)が何に依存し、何を実行して作られるのか、みたいなことを記述します。
ちなみにfake-cli
がインストールされてなかったら、勝手にダウンロードして実行、みたいなことまでしてくれるみたいです。
インストール
グローバルにインストールするなら
dotnet tool install fake-cli -g
そして、ディレクトリを指定するなら
dotnet tool install fake-cli --tool-path /path/to/tool
とやります。
ちなみにdotnet tool install
のグローバルインストール先は、初期設定では$HOME/.dotnet/tools/
です。
テンプレート作成
Fakefileをその都度つくるのはしんどいので、テンプレートから雛形を作ることができます。
まずはテンプレート一覧みたいなのを次のコマンドで取得します。
dotnet new -i "fake-template::*"
で、テンプレートの作成は次のようにします。
dotnet new fake
すると、カレントディレクトリに.fake
ディレクトリ、build.fsx
、fake.cmd
、fake.sh
が作成されます。
fake.cmd
はWindows用のビルドスクリプトで、fake.sh
はMac/Linux用です。
で、もっぱらMake処理を書く先はbuild.fsx
で、中身は普通のF#コードです。こんな感じになっています。
#load ".fake/build.fsx/intellisense.fsx" open Fake.Core open Fake.DotNet open Fake.IO open Fake.IO.FileSystemOperators open Fake.IO.Globbing.Operators open Fake.Core.TargetOperators Target.create "Clean" (fun _ -> !! "src/**/bin" ++ "src/**/obj" |> Shell.cleanDirs ) Target.create "Build" (fun _ -> !! "src/**/*.*proj" |> Seq.iter (DotNet.build id) ) Target.create "All" ignore "Clean" ==> "Build" ==> "All" Target.runOrDefault "All"
Targetの作り方
Target.create
関数を使います。
Target.create "Build" (fun _ -> Trace.log "--- Building the app ---" DotNet.build (Path.Combine ("src", "demo")) )
https://gist.github.com/KarandikarMihir/7776c887cd73364c5cfc279f8c270934#file-build-fsx
DotNet.build
はFAKEが提供するビルド用のモジュールで、ようするにdotnet build
というコマンドラインの処理をしてくれます。
依存関係の書き方
open Fake.Core.TargetOperators "Clean" ==> "Restore" ==> "Build"
https://gist.github.com/KarandikarMihir/1b6903ef77655ed65bd2f6c6c1cd618f#file-build-fsx
上の==>
とかは、Fake.Core.TargetOperators
からインポートされたものです。
で、上のコードの意味するところはBuild
したかったらRestore
しろ、Restore
したかったらClean
しろ、ということです。
実行される順番を記述したもの、と考えてもいいかもしれませんね。
Build
はRestore
に依存し、Restore
はClean
に依存している、ということだから、makeで書くとこんな感じでしょうか。
build: restore @echo "do build" restore: clean @echo "do restore" clean: @echo "do clean"
FakeのOperators
Fakeには記述を読みやすくするための二項演算子がたくさん定義されています。
見ておいたほうがよさそうなのは、
です。
例えばGlobbingOperator
は検索するファイルの候補のリストを検索して作成するものなんですが、こんなイメージ。
!! x // [x] ++ y // [x; y] -- y // [x]
!!
は引数をIncludeのただ一つの候補として初期化。
++
はそのIncludeの候補に追加。
--
は削除です。
次にTargetOperators
は、Makeの依存関係を書くための二項演算子を定義しているのですが、
"Clean" ?=> "Build"
というのは、Build
の前にClean
を実行するけど、Clean
が実行できなくてもBuild
を実行しちゃってもいいよ、という緩やかな依存関係を意味します。一方で、
"Clean" ==> "All"
は、All
の処理をするまえにClean
は絶対に実行しなくちゃ駄目だよ、という厳密な依存関係です。
ビルドの仕方
winならfake.cmd build
で、Linxu/Macならfake.sh build
です。
最後に
なんていうか、知らない間にすごい洗練されていたんだなあと実感しました。