anti scroll

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

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

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

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

予備知識

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

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

  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

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.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です。

最後に

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