SATySFi 0.1.0の新しいエコシステムSapheのプロトタイプについて(後編)

記事 SATySFi プログラミング言語 組版

前編に続いて,後編では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 は,簡単に言えばバージョン制約の解決を行なって結果をロックファイルに書き出し,必要なパッケージを取得・配置しているのでした(前編の図再掲):

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}\) とすると,バージョン制約の解決結果とは,いくつかのリリース(=特定のバージョンに固定されたパッケージ)の列挙であって,大雑把に言えば以下を満たしてほしいものです:

  1. \(\mathit{directDeps}\) に列挙されたパッケージが,いずれもそのバージョン制約を満たす1つのバージョンに固定されている.
  2. 解決結果に含まれる各リリースが依存するどのパッケージも,その制約を満たすバージョンのリリースで解決結果に含まれている.
  3. 循環依存が含まれていない.
  4. 上記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.1base.2 を共存させることが可能です:

複数バージョン共存のグラフ1

これにより,プロジェクト本体からは Base という名前で base.2 にアクセスでき,一方で easytable.2 からは Base という名前で base.1 に曖昧性なくアクセスできます.要するに,used_as によって,メインモジュールの名前が “グローバルに1つの頂点を特定するもの” ではなく “どの頂点からどの頂点をどんな名前で見ているか” のデータになっていることにより複数バージョンが共存できています.

では,もう少し難しい状況を考えましょう: 「先ほどと同様にプロジェクトで base.2 を使いたいが,easytable.2base.1 の型をシグネチャに公開しているなどして base.1 にも直接依存せねばならない」場合,どうなるでしょうか? 実はこうした状況も used_as で扱えます.グラフとしては以下のようになります:

複数バージョン共存のグラフ2

要するに,base.1used_asBase1 という名前を指定して 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本体を起動し,開発中のプロジェクト(ライブラリや文書)を処理するのでした(前編の図再掲):

saphe buildの動作の概略図

この過程で行なわれていることを以下の節で記述します.

エンベロープ: 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

パッケージ名.小文字始まりでケバブケースの文字列でなければならない.

VersionRequirementSemanticVersion

VersionRequirement ::= "^" SemanticVersion | …

SemanticVersion ::= Decimal "." Decimal "." Decimal | …

Decimal ::= "0" | ("1"-"9") ("0"-"9")*

バージョン指定.現状では ^ を先頭につけた「特定のバージョンまたはそれに後方互換な任意のバージョン」の指定のみが可能

RegistryRegistryNameRegistryRemote

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ファイルを取得する元のURL
  • checksum: ファイルのMD5チェックサム.取得したファイルが期待通りの内容であることを確かめるのに使う
  • extractions: zipファイルの中身をどこに展開するかの指定の列挙
Extraction ::= {
  from: RelativePath,
  to: RelativePath,
}
  • from: zipファイル中に格納されているファイルの相対パス
  • tofrom に指定されたファイルを,パッケージコンフィグファイルから見てどの相対パスに配置したいかの指定

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: プロジェクトがテスト時にのみ直接依存するロックの列挙

LockLockName

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リポジトリのURL
  • branch: 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: フォントファイルの指定

  1. エンベロープは「封筒」の意で,文書と縁語で洒落てるんじゃないかなと思って名づけたものです. ↩︎

  2. 依存コンフィグファイルを経由せずにコマンドラインで直接受け取る形でもよいのですが,コマンドラインで受け取るには幾分か複雑なデータであるため,一旦YAMLファイルに書き出されたものをSATySFi本体が読む形をとることにしました. ↩︎