anti scroll

ブラウザと小説の新しい関係を模索する

Paket(.NETのパッケージマネージャー)とFAKE(F#のMake)について

調べてみたのですが、日本語の文献がほとんどなかったので、ここに記しておきます。

間違いなどありましたら、指摘していただけますと幸いです。

予備知識

PaketFAKEの説明の前に、dotnetの予備知識などを少々。

管理単位「ソリューション」と「プロジェクト」

プロジェクトは個々に別々のプログラムを指し、例えば「App」とか「Tests」とか、そういうもの。

そしてソリューションは、そのプロジェクトを集めたもの、ということらしいです。

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というものを提供する方式が一般的なようです。

その場合どうやって運用するかというと、

  1. ソリューションのルートディレクトリに.paketというディレクトリを作る。
  2. .paketの中に、公式が配布しているpaket.bootstrapper.exeというファイルをpaket.exeという名前で保存する(後述するMagic Mode)。
  3. で、mono .paket/paket.exe initとすると、Paketを実行するために必要なものが用意される。

Magic Modeについて

objectxさんに教わったのですが、.paketディレクトリにpaket.bootstrapper.exepaket.exeという名前で置いて、それをあたかも本物のpaket.exeのように使用するのはMagic Modeと呼ばれる使い方です。

Magic Modepaket.exe(実体はpaket.bootstrapper.exeで70kbyteぐらいしかない)は、初めて起動したときに本体のpaket.exe(8Mbyteもある)をネット越しにとってきて、dotnetの一時ディレクトリに展開します。

で、それ以降はpaket.exe(実際はpaket.bootstrapper.exe)に対するコマンドは、その本体に対して送られるようになる、ということみたいです。

本体ではないのに、本体のように使えるからMagic Modeということなんだと思います。

なんでそんな事をするかというと、

  1. 本体のpaket.exeは頻繁に更新されるし、サイズもでかいのでリポジトリに入れるのはしんどい。
  2. めったに更新されないpaket.bootstrapper.exepaket.exeとしてリポジトリに入れておき、それを経由してpaket.exe(本体)を実行する仕組みにすれば、paket.exe(本体)の更新に強くなる。

といった理由だと思います。

ちなみにPaketのドキュメントでpaket.exeと記述されているときは、それがMagic Modepaket.exe(つまり本当はpaket.bootstrapper.exe)のことを指すのか、本体のpaket.exeなのかを、文脈で判断する必要があって、ちょっとややこしいです。

Paketの依存性管理ファイル群

色々な管理ファイルがありますが、先に説明したソリューションプロジェクトという単位を知らないと混乱します。

paket.dependencies

これはソリューション単位の依存関係記述ファイルです。ソリューションはプロジェクトの集合でしたから、つまりは全プロジェクトを横断して必要なパッケージを記述するファイルになります。

例えばAppプロジェクトでJson関連、TestプロジェクトでNUnit関連が必要なら、その両方を記述します。

このファイルでの依存性の書き方はThe packet.dependencies fileを参照して下さい。

ちなみに自分の場合は、objectxさんに全ておまかせしてしまいました(笑)。

paket.references

これはプロジェクトの依存性記述ファイルです。そのプロジェクトで必要とする依存性だけを記述します。先のAppならJson。TestならNUnitとか。

先のpaket.dependenciesとは違い、単にパッケージ名を羅列するだけで良いみたいです。

例えばTypeNovelの場合、コンパイラ本体プログラム(プロジェクト名はTnc)のpaket.referencesはこんな感じです。

FSharp.Core
Argu

で、コンパイラが参照するTypeNovel用のライブラリ(プロジェクト名はLib)だとこうなっています。

FSharp.Core
FSharp.Data
FsLexYacc

paket.lock

これは後述するコマンドを利用して依存性を更新、インストールしたりすると、自動的に作られるファイルです。

paket.dependenciesに記述したすべてのパッケージが間接的に必要とするものまで含めて、すべての依存性がこのファイルに記述されます。

gitなどでは、依存性をはっきりと固定させるために、commit対象とすることが推奨されています(参考:Why should I commit the lock file?)。

.paket/Paket.Restore.targets

これもパッケージの更新やインストール処理の過程で、自動で更新されるファイルです。

このファイルは何に使うかと言うと、*.fsprojという各プロジェクトのルートディレクトリにあるプロジェクト構成ファイルにてImportするために使います。

例えば次のように取り込みます。

<?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>というタグがそれです。こうしておけば、ちゃんと使用しているパッケージへの参照が通ります。

パッケージの更新処理など

グローバルにすでに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 update App/paket.references
paket install

FAKEについて

FAKEはF#のMakeです。ドメイン固有言語で記述するということになっていますが、実体は普通のF#コードです。

概念についても通常のMakeと同じで、それぞれの生成物(Target)が何に依存し、何を実行して作られるのか、みたいなことを記述します。

例えば「exeをzipでまとめたやつ」みたいなターゲットは、もちろん「exe」に依存しています。だからそれをビルドしたかったら、まずは「exe」をビルドする処理が走るとか、そういうことです。

ソースをクローンした人のディレクトリに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.fsxfake.cmdfake.shが作成されます。

fake.cmdWindows用のビルドスクリプトで、fake.shMac/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しろ、ということです。

実行される順番を記述したもの、と考えてもいいかもしれませんね。

BuildRestoreに依存し、RestoreCleanに依存している、ということだから、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です。

最後に

なんていうか、知らない間にすごい洗練されていたんだなあと実感しました。