前編に続いて,後編ではSapheが裏側でどのように動いているかの設計・実装について概説します.特に,SapheとSATySFi本体とがどのように責務を分担しているかが重要な内容です.単にSapheを使いたい人は本稿で扱う内容を把握しておく必要はなく,前編のみで十分です.
目次
パッケージレジストリが保持するデータの構造
パッケージレジストリは,基本的には各パッケージがどんなバージョンを提供していてそれぞれのバージョンがどのような実装かを保持しています.つまり,基本的には以下のようなマップを保持しているとみてよいもので,根幹はわりと単純な機構です:
\[ \mathit{pkgName} \mapsto (\mathit{semver} \mapsto \mathit{release})\]
しかし,パッケージレジストリは単にこれを単一のコンフィグファイルに記録するだけで済ませては不都合になる,以下のようないくつかの事情があります:
- 利用が進むと,記載されているパッケージの数は莫大になり,保守に困るほどコンフィグファイルが巨大になってしまうかもしれない.例えば,エディタで読み込むのに支障が出るかもしれない.
- コンフィグファイルをGitHubなどで管理する場合,パッケージを登録するPRがコンフリクトしやすくなる.記載順序をパッケージの名前順でソートするとしても,その順番で近いものがなおコンフリクトする.
- エコシステムはうまくいけば長い寿命が予期されるソフトウェアであり,要件が変わってコンフィグの形式を非互換に変えたくなることも十分ありうるが,単一のファイルだと形式の変更に際して既存の記述の前方互換性が失われてしまい,古いバージョンのエコシステムからの利用ができなくなる.
そこで,上記の問題を緩和するために,新しいバージョンごとに \(\mathit{release}\) 相当の内容を1つのファイルに記述する形式にします.これをリリースコンフィグファイル (release config file) と呼び,ファイル名は 〈PkgName〉.〈Semver〉.saphe-release.yaml
という形とします.内容はほぼパッケージコンフィグファイルと同じようなもので,以下のような具合です:
saphe: "^0.0.1"
satysfi: "^0.1.0"
name: "std-ja"
version: "0.0.1"
source:
tar_gzip:
url: "https://gfngfn.github.io/temp/std-ja.0.0.1.tar.gz"
checksum: "c705d4e239c05cd980b51a5401090510"
dependencies:
- used_as: "Stdlib"
name: "stdlib"
requirement: "^0.0.1"
- used_as: "Math"
name: "math"
requirement: "^0.0.1"
- used_as: "Annot"
name: "annot"
requirement: "^0.0.1"
- used_as: "Code"
name: "code"
requirement: "^0.0.1"
- used_as: "FontJunicode"
name: "font-junicode"
requirement: "^0.0.1"
- used_as: "FontLatinModern"
name: "font-latin-modern"
requirement: "^0.0.1"
- used_as: "FontIpaEx"
name: "font-ipa-ex"
requirement: "^0.0.1"
- used_as: "FontLatinModernMath"
name: "font-latin-modern-math"
requirement: "^0.0.1"
このリリースコンフィグが以下のような形で配置されて1つのパッケージレジストリを成しています:
./
├── saphe-registry.yaml
└── packages/
├── 〈PkgName_1〉/
│ ├── 〈PkgName_1〉〈Semver_1_1〉.saphe-release.yaml
│ ├── 〈PkgName_1〉〈Semver_1_2〉.saphe-release.yaml
│ ...
├── 〈PkgName_2〉/
│ ├── 〈PkgName_2〉〈Semver_2_1〉.saphe-release.yaml
│ ├── 〈PkgName_2〉〈Semver_2_2〉.saphe-release.yaml
... ...
根元のディレクトリにはレジストリコンフィグファイル saphe-registry.yaml
が置かれますが,これは将来的にレジストリの形式を変えたくなった場合に互換性を判定するのに使うためのもので,今のところそれ以外の本質的情報は記載されていません.
saphe solve
がやっていること
saphe solve
は,簡単に言えばバージョン制約の解決を行なって結果をロックファイルに書き出し,必要なパッケージを取得・配置しているのでした(前編の図再掲):
この章ではその制約解決がどのような定式化であるかを記載します.
基礎的な定式化
簡単のため,まずは以下の場合に限定して説明します:
- 使用するパッケージレジストリは1つだけ
- 1つのパッケージに対して高々1つのバージョンが選択される
制約解決に於いて,パッケージレジストリ \(\mathit{reg}\) は以下のように看なせます:
\[\begin{align*} &\mathit{reg} \in (\mathrm{PkgName} \stackrel{\mathrm{fin}}{\rightharpoonup} (\mathrm{Semver}\stackrel{\mathrm{fin}}{\rightharpoonup} \mathrm{Deps})) \\ &\mathit{reg} = \{\mathit{pkgName}_i \mapsto \{\mathit{semver}_{i,j} \mapsto \mathit{deps}_{i,j}\}_{j = 1}^{\mathit{numReleases}_i}\}_{i = 1}^{\mathit{numPkgs}} \end{align*}\]
ここで \(A \stackrel{\mathrm{fin}}{\rightharpoonup} B\) は \(A\) から \(B\) への有限部分写像全体を表し,また \(\mathit{deps}_{i,j} \in \mathrm{Deps}\) は各リリースが依存するパッケージとそのバージョン制約を列挙したものであって以下のような形をしています:
\[\begin{align*} &\mathit{deps}_{i,j} \in \mathrm{Deps} := (\mathrm{PkgName} \stackrel{\mathrm{fin}}{\rightharpoonup} \mathrm{Constraint}) \\ &\mathit{deps}_{i,j} = \{\mathit{depPkgName}_{i,j,k} \mapsto \mathit{constraint}_{i,j,k}\}_{k = 1}^{\mathit{numDeps}_{i,j}} \end{align*}\]
開発中のプロジェクトが直接依存するパッケージの列挙を \(\mathit{directDeps} \in \mathrm{Deps}\) とすると,バージョン制約の解決結果とは,いくつかのリリース(=特定のバージョンに固定されたパッケージ)の列挙であって,大雑把に言えば以下を満たしてほしいものです:
- \(\mathit{directDeps}\) に列挙されたパッケージが,いずれもそのバージョン制約を満たす1つのバージョンに固定されている.
- 解決結果に含まれる各リリースが依存するどのパッケージも,その制約を満たすバージョンのリリースで解決結果に含まれている.
- 循環依存が含まれていない.
- 上記1–3を満たしつつ,不要な依存パッケージを含んでいない.
これをよりフォーマルに述べると,バージョン制約の解決結果とは,以下を満たすような \(\mathit{solut} \in \mathrm{Solut} := (\mathrm{PkgName} \stackrel{\mathrm{fin}}{\rightharpoonup} \mathrm{Semver})\) のうち包含関係に関して極小なものです:
\[\begin{align*} (1)\quad &\forall (\mathit{depPkgName} \mapsto \mathit{constraint}) \in \mathit{directDeps}.\\ &\mathit{constraint} \vDash \mathit{solut}(\mathit{depPkgName}) \\ (2)\quad &\forall (\mathit{pkgName} \mapsto \mathit{semver}) \in \mathit{solut}.\\ &\forall (\mathit{depPkgName} \mapsto \mathit{constraint}) \in \mathit{all}(\mathit{pkgName})(\mathit{semver}).\\ &\mathit{constraint} \vDash \mathit{solut}(\mathit{depPkgName}) \\ (3)\quad &\forall \mathit{pkgName} \forall \mathit{pkgName}^{\prime} \in \mathop{\mathrm{dom}}(\mathit{solut}).\\ &\mathit{pkgName} \triangleright_{D(\mathit{reg}, \mathit{solut})}^{\ast} \mathit{pkgName}^{\prime} \land \mathit{pkgName}^{\prime} \triangleright_{D(\mathit{reg}, \mathit{solut})}^{\ast} \mathit{pkgName} \Rightarrow\\ &\mathit{pkgName} = \mathit{pkgName}^{\prime} \end{align*}\]
ただし,\((\vDash) \subseteq \mathrm{Constraint} \times \mathrm{Semver}\) は \(\mathit{constraint} \vDash \mathit{semver}\) でバージョン \(\mathit{semver}\) がバージョン制約 \(\mathit{constraint}\) を満たすことを表す二項関係です.また,\(D(\mathit{reg}, \mathit{solut}) \in (\mathrm{PkgName} \stackrel{\mathrm{fin}}{\rightharpoonup} \mathrm{Deps})\) は解消結果の各リリースの依存を列挙したものであり,以下のように定義されます:
\[\begin{align*} & D(\mathit{reg}, \mathit{solut}) := \\&\quad \{\mathit{pkgName} \mapsto \mathit{reg}(\mathit{pkgName})(\mathit{semver}) \mid (\mathit{pkgName} \mapsto \mathit{semver}) \in \mathit{solut}\} \end{align*}\]
そして,\(d \in (\mathrm{PkgName} \stackrel{\mathrm{fin}}{\rightharpoonup} \mathrm{Deps})\) に対して \(\triangleright_{d}^{\ast}\) は \(\triangleright_{d}\) の反射推移閉包であり,その \(\mathit{pkgName} \triangleright_{d} \mathit{pkgName}^{\prime}\) は \(d\) に於いてパッケージ \(\mathit{pkgName}\) がパッケージ \(\mathit{pkgName}^{\prime}\) に直接依存することを表します:
\[\mathit{pkgName} \triangleright_d \mathit{pkgName}^{\prime} \mathrel{\mathord{:}\mathord{\Longleftrightarrow}} \mathit{pkgName}' \in \mathop{\mathrm{dom}}(d(\mathit{pkgName}))\]
つまり,\((3)\) は(自身を含む)間接依存を判定する二項関係 \(\triangleright_{D(\mathit{reg}, \mathit{solut})}\) が半順序であることを要請しています.
ロックファイルに出力される内容
バージョン制約解決は,基本的には得られた上記のような解決結果 \(\mathit{solut}\) をロックファイルへと書き出して終了なのですが,\(\mathit{solut}\) だけだとその後のビルドに際して毎回パッケージレジストリ \(\mathit{reg}\) を参照しなければ依存パッケージの読み込み順序が決定できません.この \(\mathit{reg}\) は利用が進めば巨大になるものなので,ビルドごとに逐一 \(\mathit{reg}\) を参照していては著しく効率が悪くなるおそれがあります.また,登録されている依存関係に間違いが見つかったりするとレジストリ管理者やパッケージ作成者が \(\mathit{reg}\) の内容を一部修正したくなることもありそうなため,これを可能にするためにも \(\mathit{reg}\) 中の既存のリリース情報が将来に亘って変更されないことは仮定しない方がよいでしょう.これらの理由から,\(\mathit{reg}\) のうちビルドに必要な情報だけを抜き出してロックファイルに書き出すということを行ないます.具体的には上記の \(D(\mathit{reg}, \mathit{solut})\) と \(\mathop{\mathrm{dom}}(\mathit{directDeps})\) にあたる内容も \(\mathit{solut}\) に加えて書き出します.
この結果,ロックファイルに書き出す内容は,直観的には開発中のプロジェクトやパッケージの間の依存関係を記述した有向非巡回グラフになっています.すなわち,開発中のプロジェクトを \(\mathrm{project}\) と表すとして,この有向非巡回グラフ \((V, E)\) は,頂点集合が
\[V := \{\mathrm{project}\} \uplus \{(\mathit{pkgName}, \mathit{semver}) \mid (\mathit{pkgName} \mapsto \mathit{semver}) \in \mathit{solut}\}\]
であり,\(u\) から \(v\) への依存関係が(\(D(\mathit{reg}, \mathit{solut})\) と \(\mathop{\mathrm{dom}}(\mathit{directDeps})\) に基づいて張られた)有向辺 \((u, v) \in E\) になっています.図示すると以下のような具合です:
複数レジストリの場合
これは定式化上はそれほど難しくありません.というのも,前節の議論の \(\mathrm{PkgName}\) を \(\mathrm{RegId} \times \mathrm{PkgName}\) というレジストリ識別用のIDを付加したものに置き換えてしまえば基本的には事足りるためです.ただし,実装上は以下の点でそれなりに面倒です:
- 新たに管理の仕組みを設けずに全世界で一意的にパッケージレジストリを識別するためのIDは,正規化したレジストリのURLくらいしかない
- ちなみに,これに関連して理想的にはパッケージレジストリ側も正規化に関して同値なものを除いて一意なURLでしか自身を公開しないようにする必要がある.
- 取得したファイルをマシンに配置するために,パッケージレジストリのURLをローカルのファイルシステムで使える名前にマップする必要があったりする
- ユーザはレジストリの識別用の値を直接書くのではなく,URLなどにより指定する
- レジストリの識別用の値からはレジストリのURLなどがそのままでは復元できない
このあたりの話は「パッケージマネージャを自作するときに考えること」で詳しめに扱っています.
同一パッケージの非互換な複数バージョンの共存を許す場合
こちらはなかなか大変です.すでに前編で紹介しましたが,Sapheでは同一パッケージの非互換な複数バージョンが共存できるようにするために used_as
というリネーミング用の指定があります.これにより,パッケージへの依存は used_as
に指定された名前が紐づき,以下のような形態に一般化されます:
\[\mathrm{Deps} := (\mathrm{RegId} \times \mathrm{PkgName} \stackrel{\mathrm{fin}}{\rightharpoonup} (\mathrm{ModuleName} \stackrel{\mathrm{fin}}{\rightharpoonup} \mathrm{Constraint}))\]
同様に,解決結果も以下のように一般化されます:
\[\mathrm{Solut} := (\mathrm{RegId} \times \mathrm{PkgName} \stackrel{\mathrm{fin}}{\rightharpoonup} (\mathrm{ModuleName} \stackrel{\mathrm{fin}}{\rightharpoonup} \mathrm{Semver}))\]
フォーマルに定式化を書いていると大変なので省略しますが,直観としてはロックファイルに書き出される有向非巡回グラフは各有向辺に used_as
のモジュール名のラベルがつくように拡張されます.すなわち,出力されるグラフは以下のような \((V, E, w)\) です:
- 頂点集合は \(V \subseteq_{\mathrm{fin}} (\mathrm{PkgName} \times \mathrm{Semver}) \uplus \{\mathrm{project}\}\) の形で,同一パッケージの頂点が一般には複数あるが,同一パッケージに由来する任意の2つの頂点 \((\mathit{pkgName}, \mathit{semver}_1), (\mathit{pkgName}, \mathit{semver}_2) \in V\) に対し,\(\mathit{semver}_1\) と \(\mathit{semver}_2\) は互換性がないバージョンである.
- 各有向辺 \(e = (u, v) \in E\) には,\(u\) が \(v\) を
used_as
でどのように参照しているかのラベル \(w(e) \in \mathrm{ModuleName}\) がついている.
先ほどのグラフを同様にして図示すると以下のような具合です:
この例だと同一パッケージの複数バージョンが共存していないのでラベルがついた以外に何も変わりませんが,実際にこのラベルの存在によって複数バージョンが無理なく共存できるようになっています.以下でもう少し見てみましょう.
まず,「開発中のプロジェクトが easytable.2
に依存しており,その easytable.2
は必ず base.1
に依存するのだが,プロジェクトでは base.2
を使いたい」という状況を考えます.これは以下のような形で base.1
と base.2
を共存させることが可能です:
これにより,プロジェクト本体からは Base
という名前で base.2
にアクセスでき,一方で easytable.2
からは Base
という名前で base.1
に曖昧性なくアクセスできます.要するに,used_as
によって,メインモジュールの名前が “グローバルに1つの頂点を特定するもの” ではなく “どの頂点からどの頂点をどんな名前で見ているか” のデータになっていることにより複数バージョンが共存できています.
では,もう少し難しい状況を考えましょう: 「先ほどと同様にプロジェクトで base.2
を使いたいが,easytable.2
が base.1
の型をシグネチャに公開しているなどして base.1
にも直接依存せねばならない」場合,どうなるでしょうか? 実はこうした状況も used_as
で扱えます.グラフとしては以下のようになります:
要するに,base.1
は used_as
に Base1
という名前を指定して base.2
を指す Base
と区別することで呼び分けるということができます.したがって,プロジェクトのパッケージコンフィグファイルには以下のような記述を含めるとよいことになります:
dependencies:
- used_as: "Table"
registered:
registry: "default"
name: "easytable"
constraint: "^2.1.0"
- used_as: "Base"
registered:
registry: "default"
name: "base"
constraint: "^2.0.0"
- used_as: "Base1"
registered:
registry: "default"
name: "base"
constraint: "^1.0.0"
取得したファイルは手元でどこに配置されるか
Sapheがパッケージレジストリや各パッケージが指定するURLから取得したファイルは,ストアルート (store root) と呼ぶディレクトリの下に配置されます.ストアルートは,Unix系OSに於いては $HOME/.saphe/
,(満足に動く保証は全くありませんが)Windowsに於いては %userprofile%\.saphe\
です.いずれもいわゆるホームディレクトリを指す環境変数に依存しており,この環境変数が設定されていない場合Sapheは異常終了します.ユーザがこのストアルート以下のファイルを手動でいじったり,新たにファイルを配置したりといった変更を行なうことは推奨されません.
ストアルート以下は,下記のような構造でファイルが保持されています:
$HOME/.saphe/
├── saphe-store-root.yaml
├── registries/
│ ├── 〈RegistryHashValue_1〉/
│ ├── 〈RegistryHashValue_2〉/
│ ...
│
├── cache/
│ └── locks/
│ ├── 〈RegistryHashValue_1〉/
│ │ ├── 〈PkgName_1〉.〈SemVer_1_1〉.tar.gz
│ │ ├── 〈PkgName_1〉.〈SemVer_1_1〉/
│ │ ├── 〈PkgName_1〉.〈SemVer_1_2〉.tar.gz
│ │ ...
│ │ ├── 〈PkgName_2〉.〈SemVer_2_1〉.tar.gz
│ │ ...
│ │
│ ├── 〈RegistryHashValue_2〉/
│ ...
│
└── packages/
├── 〈RegistryHashValue_1〉/
│ ├── 〈PkgName_1〉/
│ │ ├── 〈PkgName_1〉.〈SemVer_1_1〉/
│ │ ├── 〈PkgName_1〉.〈SemVer_1_2〉/
│ │ ...
│ │
│ ├── 〈PkgName_2〉/
│ │ ├── 〈PkgName_2〉.〈SemVer_2_1〉/
│ │ ...
│ ...
│
├── 〈RegistryHashValue_2〉/
...
まずストアルート直下にある saphe-store-root.yaml
がストアルートコンフィグファイル (store root config file)で,ここには取得されているレジストリのURLなどが記載されています.
registries/
は実際に取得されている各レジストリがすぐ下のサブディレクトリに並んでいます.これの各内容はすでに上で述べたとおりです.
cache/locks/
には,取得した各実装のtarball 〈PkgName_i〉.〈SemVer_i〉.tar.gz
が一時的に置かれています.また,その実装のパッケージコンフィグに external_resources
の指定がある場合は,その指定に応じて取得した外部ファイルを一時的に置くディレクトリ 〈PkgName_i〉.〈SemVer_i〉/
もつくられています.
そして,取得された実装のtarballが解凍されて配置される先が packages/
以下です.ビルドなどで実際に使われるソースファイルは,この packages/
以下にあるものです.
saphe update
がやっていること
saphe update
は,手元に取得しているパッケージレジストリの情報を最新のものに更新するためのコマンドです.これは単に各パッケージレジストリからデータを取得して $HOME/.saphe/registries/〈RegistryHashValue_i〉
のデータを更新するだけで,Gitリポジトリのパッケージレジストリであればほぼ git pull
をしているだけです.
saphe build
がやっていること
saphe build
は,すでに書き出されているロックファイルを参照し,必要なパッケージを読むための情報を渡してSATySFi本体を起動し,開発中のプロジェクト(ライブラリや文書)を処理するのでした(前編の図再掲):
この過程で行なわれていることを以下の節で記述します.
エンベロープ: SATySFi本体からはパッケージがどう見えるか
まず前提として,Sapheから起動されたSATySFi本体がパッケージをどのように扱うかをこの節で共有しておきます.
SATySFi本体は,“リリースの単位” としてのパッケージを認識しません.一方で,“モジュールが集まったひとかたまり” を認識する機能はあり,これをエンベロープ (envelope) と呼んでいます1.1つのエンベロープは,典型的には以下のような構造をしています:
./
├── satysfi-envelope.yaml
├── src/
│ ├── 〈MainModuleFile〉.satyh
│ ├── 〈SubModuleFile_1〉.satyh
│ ├── 〈SubModuleFile_2〉.satyh
│ ...
└── test/
├── 〈TestFile_1〉.satyh
├── 〈TestFile_2〉.satyh
...
まず,必ず直下のディレクトリにエンベロープコンフィグファイル (envelope config file) satysfi-envelope.yaml
があります.これには以下のような内容が書かれています:
library:
main_module: 〈MainModuleName〉
source_directories:
- ./src
test_directories:
- ./test
source_directories
にソースファイルを格納しているサブディレクトリが,test_directories
にテストファイルを格納しているサブディレクトリが,それぞれ列挙されています.また,ソースファイルのうちどれがメインモジュール (main module) であるかが main_module
に指定されています.このように,エンベロープとは基本的には “リリースに関する情報のないパッケージのようなもの” と理解して差し支えない構造物です.
SATySFi本体は,起動時に開発中のプロジェクトが依存するエンベロープの置かれているパス一覧を受け取り,それを見に行ってプロジェクトの処理に使います.より正確には,各エンベロープの間にどのような依存関係があってどのような名前でメインモジュールを参照しているかなどのデータも受け取って使います.このデータの受け渡しは依存コンフィグファイル (dependency config file) satysfi-deps.yaml
によって行なわれます2.
依存コンフィグファイルの生成
SATySFi本体が読むための依存コンフィグファイルは,Sapheがロックファイルをもとに生成します.前節で述べた通り,依存コンフィグファイルの内容は各エンベロープの配置されているパスとそれらの間の依存関係です.Sapheは配置済みの各パッケージをエンベロープと看なし,ロックファイルからリリースに関する情報を除去して依存コンフィグファイルを生成し,SATySFi本体に渡します.依存コンフィグファイルの生成は,模式的に描くと以下のようになります:
要するにロックファイルの構造がほとんどそのまま引き継がれています.ロックファイルとの違いは,各頂点は “単なるエンベロープ” を指していて,それがどのパッケージ由来のものかという情報が残っていない点です.例えば,上記の図で言うと \(E_2\) と \(E_3\) とが同じパッケージ由来であるという情報はすでに忘れられています.このようにして,SATySFi本体からは同一パッケージ由来の2つのエンベロープは互いに全く関係のないものに見えています.
ちなみに,「ロックファイルを一旦書き出してから依存コンフィグファイルに変換するよりも,最初から saphe solve
の時点で依存コンフィグファイルのみを生成すればよいのではないか?」と疑問に思う方もいるかもしれません.それは大変もっともな着眼点なのですが,やはりロックファイルが必要である理由があります.それは,再現ビルドなど,ロックファイルからデータを読み出す用途が今後生じうると考えているためです.要するに,各パッケージのどのバージョンが使われているのかをSaphe自身が読み出す必要性が十分生じうると考えているからです.依存コンフィグファイルからこれらの情報を読み出すこともありえますが,依存コンフィグファイルの形式はSATySFi本体側が規定しているものであり,Sapheの都合は知らないので,仮に読み出せるとしても “リバースエンジニアリング” になってしまい責務分担上不自然です.これゆえに,ロックファイルと依存コンフィグファイルは別々にあるべきと考え,別々になっています.
なお,前編からの繰り返しになりますが,ロックファイルはバージョン管理に含めるべきものである一方で依存コンフィグファイルはバージョン管理に含めるものではありません.
Future Work
Sapheもプロトタイプとしてはわりと形になってきましたが,今後はさらに以下のような変更を施すことを考えています:
- 修正:
- 安全性:
- コンフィグファイルから読み取るパッケージ名・モジュール名などの文字列を適切にバリデートする
- セキュリティ上の懸念から,
external_resouces
によって取得したファイルを配置できる場所を制限する - エラーが生じて実行を中断するとき,可能な限り状態の一貫性を回復してから処理を終えられるようにする
- ファイル配置:
- 中間生成物は
target/
ディレクトリ以下に生成したりできるようにする
- 中間生成物は
- 依存解決処理:
- 非互換なバージョンがなるべく共存しない依存解決を優先する
- 安全性:
- 機能追加:
saphe init
時の.gitignore
の生成saphe cache clear
で取得済みのファイルを消せるようにするsaphe build
が実行されたときに,必要であれば自動でsaphe solve
をその前に実行する- 複数の
saphe
プロセスが同時に起動しても破綻しないように何らかの排他制御をする
Sapheが規定する各種形式
この章ではSapheが扱う各種ファイルの形式の,現状のフォーマルな定義を記載します.
?
とdefault
のついているフィールドは,省略可能であることを表します.…
は他のパターンを追加で扱えるようにする可能性が高いことを表します.++
はフィールド名が重複しない2つのオブジェクトの結合を表します.
パッケージコンフィグ
PackageConfig
PackageConfig ::= {
saphe: VersionRequirement,
satysfi: VersionRequirement,
?name: (PackageName | null) default = null,
authors: List(String),
?registries: List(Registry) default = [],
?external_resources: List(ExternalResource) default = [],
contents: PackageContents,
?dependencies: List(Dependency) default = [],
?test_dependencies: List(Dependency) default = [],
}
saphe
: このパッケージコンフィグを処理するSapheに期待するバージョンsatysfi
: 文書やパッケージを処理してほしいSATySFi本体のバージョンname
: パッケージ名.人間が読むためのもので,処理上はバリデーションに使いうる以外特に指定する意味がないauthors
: このパッケージまたは文書の作者の名前を列挙したものregistries
: 使用するレジストリの列挙.典型的にはsaphe init
で生成されるregistry: "default"
のものだけが記述されるexternal_resources
: インストール時に外部から取得するファイルの指定.主としてフォントパッケージでの使用を想定しているcontents
: このパッケージまたは文書の内容dependencies
: 依存パッケージの列挙test_dependencies
: テスト用の依存パッケージの列挙
PackageContents ::=
{ document: Document }
| { library: Library }
| { font: Font }
| …
パッケージまたは文書の内容.文書の場合は document
を,ライブラリの場合は library
を,フォントパッケージの場合は font
を,それぞれ記載する.形式は今後増えるかもしれない.
Document ::= {}
今のところ特に指定項目なし.将来的にはビルド用オプションなどを指定できるようにするかもしれない.
Library ::= {
main_module: UppercaseIdentifier,
source_directories: List(RelativePath),
?test_directories: List(RelativePath) default = [],
?markdown_conversion: (MarkdownConversion | null) default = null,
}
RelativePath ::= String
main_module
: メインモジュールの指定source_directories
: ソースファイルが置かれているディレクトリへの相対パスの列挙.典型的には[ "./src" ]
test_directories
: テストファイルが置かれているディレクトリへの相対パスの列挙.典型的には[ "./test" ]
markdown_conversion
: Markdown入力を受けつけるクラスファイルに於ける指定で,Markdown中の各マークアップをどのようなSATySFiコマンドに変換するかを指定したもの
Font ::= {
main_module: UppercaseIdentifier,
files: List(FontFile),
}
main_module
: メインモジュールの指定だが,使われない.フォントパッケージを使う側がused_as
でメインモジュール名を指定するため.files
: フォントファイルに関する指定の列挙
PackageName
PackageName ::= LowercaseIdentifier
パッケージ名.小文字始まりでケバブケースの文字列でなければならない.
VersionRequirement
,SemanticVersion
VersionRequirement ::= "^" SemanticVersion | …
SemanticVersion ::= Decimal "." Decimal "." Decimal | …
Decimal ::= "0" | ("1"-"9") ("0"-"9")*
バージョン指定.現状では ^
を先頭につけた「特定のバージョンまたはそれに後方互換な任意のバージョン」の指定のみが可能
Registry
,RegistryName
,RegistryRemote
Registry ::= {
name: RegistryName,
} ++ RegistryRemote
RegistryName ::= String
RegistryRemote ::= { git: GitRegistry } | …
GitRegistry ::= {
url: String,
branch: String,
}
パッケージレジストリの指定,現状ではGitリポジトリのみが指定可能.ここで name
に指定したレジストリ名はコンフィグファイル内でのみ通用し,依存パッケージの記述の registry:
で使われる
Dependency
Dependency ::= {
used_as: UppercaseIdentifier,
} ++ DependencySpec
DependencySpec ::=
{ registered: RegisteredDependencySpec }
| { local: LocalDependencySpec }
| …
RegisteredDependencySpec ::= {
registry: RegistryName,
name: PackageName,
requirement: VersionRequirement,
}
LocalDependencySpec ::= {
path: RelativePath,
}
依存パッケージの指定.現状ではパッケージレジストリに登録されているものかサブディレクトリが指定できる.
ExternalResource
ExternalResource ::= {
name: ExternalResourceName,
} ++ ({zip : ZipExternalResource} | …)
ExternalResourceName ::= (! '/')*
外部から取得するファイルに関するデータ.
name
: 取得を一意に識別する名前.一時ファイルの名前などに使う
ZipExternalResource ::= {
url: String,
checksum: String,
extractions: List(Extraction),
}
url
: zipファイルを取得する元のURLchecksum
: ファイルのMD5チェックサム.取得したファイルが期待通りの内容であることを確かめるのに使うextractions
: zipファイルの中身をどこに展開するかの指定の列挙
Extraction ::= {
from: RelativePath,
to: RelativePath,
}
from
: zipファイル中に格納されているファイルの相対パスto
:from
に指定されたファイルを,パッケージコンフィグファイルから見てどの相対パスに配置したいかの指定
FontFile
FontFile ::= {
path: String,
} ++ FontFileContents
FontFileContents ::=
{ opentype_single: FontSpec }
| { opentype_collection: List(FontSpec) }
フォントファイルの相対パスと,そのファイルに格納されている内容の指定.単一のフォントを格納しているOpenTypeファイルである場合は opentype_single
を,OpenType Collectionである場合は opentype_collection
を,それぞれ指定する.ここでいうところの「OpenTypeファイル」とはOpen Font Formatに適合するフォントファイルすべてを指す広義の意味であり,狭義であるCFFアウトラインをもつファイルのみに限らない.
FontSpec ::= {
name: LowercaseIdentifier,
?math: Boolean default = false,
}
name
: 該当フォントにプログラムから見てどんな識別子でアクセスできるようにするかの指定math
: 数式フォントとして使うかの指定
MarkdownConversion
MarkdownConversion ::= {
document: LowercaseIdentifier,
paragraph: BlockCommand,
hr: BlockCommand,
h1: BlockCommand,
h2: BlockCommand,
h3: BlockCommand,
h4: BlockCommand,
h5: BlockCommand,
h6: BlockCommand,
ul: BlockCommand,
ol: BlockCommand,
code_block: BlockCommand,
blockquote: BlockCommand,
emph: InlineCommand,
strong: InlineCommand,
?hard_break: (InlineCommand | null) default = null,
code: InlineCommand,
link: InlineCommand,
img: InlineCommand,
}
BlockCommand ::= "+" (UppercaseIdentifier ".")* LowercaseIdentifier
InlineCommand ::= "\" (UppercaseIdentifier ".")* LowercaseIdentifier
クラスパッケージでのみ用いる,Markdown形式の入力をどのように変換するかの指定.
ロックファイル
LockConfig
LockConfig ::= {
saphe: VersionRequirement,
?locks: List(Lock) default = [],
?dependencies: List(LockDependency),
?test_dependencies: List(LockDependency),
}
ecosystem
: ロックファイルを読むSapheに期待するバージョンlocks
: 使用するロック(バージョンが固定されたパッケージ)の列挙dependencies
: プロジェクトが直接依存するロックの列挙test_dependencies
: プロジェクトがテスト時にのみ直接依存するロックの列挙
Lock
,LockName
Lock ::= {
name: LockName,
?dependencies: LockDependency default = [],
?test_only: Boolean default = false,
} ++ LockContents
LockName ::= String
1つのロックに関するデータ.
name
: ロックを識別するための名前.任意の文字列が使える(/
などを含んでいてもよい).dependencies
: このロックが依存する他のロックの列挙test_only
: プロジェクトのテスト時にのみ使われるロックであるか否か
LockContents
LockContents ::= { registered: RegisteredLock } | …
RegisteredLock ::= {
registry_hash_value: RegistryHashValue,
package_name: PackageName,
version: SemanticVersion,
}
registry_hash_value
: レジストリを識別するための値package_name
: パッケージ名version
: パッケージのバージョン
RegistryHashValue
RegistryHashValue = ("0"-"9" | "a" - "z" | "-")+
レジストリを識別するための値.具体的にはURLなどのデータから生成されたハッシュ値
LockDependency
LockDependency ::= {
name: LockName,
used_as: UppercaseIdentifier,
}
ロックへの依存の記述.
name
: ロックの名前used_as
: そのロックのメインモジュールをどんな名前で参照するか
ストアルートコンフィグ
StoreRootConfig ::= {
saphe: VersionRequirement,
registries: List(StoredRegistry),
}
ストアルートコンフィグファイルの内容.
saphe
: Sapheのバージョンregistries
: 保持しているレジストリの一覧
StoredRegistry ::= {
hash_value: RegistryHashValue,
} ++ StoredRemoteRegistrySpec
StoredRemoteRegistrySpec ::= { git: StoredGitRegistrySpec } | …
保持しているレジストリのデータ.
hash_value
: 保持しているレジストリを識別するための値
StoredGitRegistrySpec ::= {
url: String,
branch: String,
}
Gitによって管理されているレジストリのデータ.
url
: レジストリとして使われているGitリポジトリのURLbranch
: branchの指定
レジストリコンフィグ
RegistryConfig ::= { registry_format: String }
registry_format
: レジストリの形式のバージョン.現在のところ"1"
のみが妥当な値.
リリースコンフィグ
ReleaseConfig ::= {
saphe: VersionRequirement,
satysfi: VersionRequirement,
name: PackageName,
authors: List(String),
contents: PackageContents,
?dependencies: List(Dependency) default = [],
}
ほぼ PackageConfig
と同じ.
SATySFiが規定する各種形式
依存コンフィグ
DepsConfig ::= {
envelopes: List(EnvelopeSpec),
dependencies: List(EnvelopeDependency),
test_dependencies: List(EnvelopeDependency),
}
依存コンフィグファイルの内容.
envelopes
: 文書やライブラリに必要なエンベロープの一覧dependencies:
: 文書やライブラリが直接依存するエンベロープの列挙test_dependencies
: ライブラリのテストが直接依存するエンベロープの列挙
EnvelopeSpec ::= {
name: EnvelopeName,
path: String,
dependencies: List(EnvelopeDependency),
test_only: Boolean,
}
1つのエンベロープに関するデータ.
name
: エンベロープ名path
: 該当エンベロープのエンベロープコンフィグを指す相対パスdependencies
: 該当エンベロープが依存する他のエンベロープの列挙test_only
: テストでのみ使用するエンベロープであるか否か
EnvelopeDependency ::= {
name: EnvelopeName,
used_as: UppercaseIdentifier,
}
他のエンベロープへの依存の記述.
name
: 依存するエンベロープの名前used_as
: そのエンベロープのメインモジュールをどのような名前で参照するか
エンベロープコンフィグ
EnvelopeConfig ::=
{ library: LibraryEnvelope}
| { font: FontEnvelope }
| …
エンベロープコンフィグファイルの内容.基本的にはユーザが書いたパッケージコンフィグの一部が転記されたもの.
LibraryEnvelope
LibraryEnvelope ::= {
main_module: UpperIdentifier,
source_directories: List(String),
test_directories: List(String),
?markdown_conversion: (MarkdownConversion | null) default = null,
}
main_module
: メインモジュールとなるモジュールの指定source_directories
: ソースファイルが置かれているディレクトリの相対パスの列挙.ここに指定されたディレクトリで該当する拡張子のファイルはすべてソースファイルとみなされる.サブディレクトリは明示的に書かれていない限り含まないtest_directories
: テストファイルが置かれているディレクトリの相対パスの列挙markdown_conversion
: Markdown入力の変換方法
FontEnvelope
FontEnvelope ::= {
main_module: UpperIdentifier,
files: List(FontFile),
}
main_module
: メインモジュール名だが,現在のところは特に必要のないデータfiles
: フォントファイルの指定