昨年のSATySFi Conf 2023で発表させてもらった,SATySFi 0.1.0に向けたエコシステム Saphe(セイフ)について,設計とプロトタイプ実装が固まってきたため,そのおおよその仕組みを共有しようと思います.発表の内容とも一部オーバーラップします.
前編である本稿は,Sapheがどのように使えるツールなのかをハンズオン的に記載します.後編はSapheの詳細な設計・実装について述べる予定です.→ 追記: 後編ができました.
本稿執筆時点でのSapheの実装は,以下のPRの c48c047
です(ちなみに本記事の初公開時は 44ff21b
でした):
目次
Sapheの役割
Saphe(SATySFi Package-Handling Ecosystem,/seɪf/ と発音)は,簡単に言えばSATySFiのためのパッケージマネージャとビルドシステムが複合したものです.具体的には,以下のようなことを円滑に行なうためのソフトウェアです:
- ユーザがコンフィグファイルによって指定した依存パッケージたちをパッケージレジストリと呼ばれる集積所から取得し,ユーザのマシンで使えるようにする.
- この過程で,依存制約を充足するように,各々のパッケージに対して適切なバージョンを選択する必要がある.同一パッケージの異なるバージョン間の互換性はバージョン番号によって判別できるようになっており,共存できる組み合わせがある場合はそのうちの1つを選ぶ依存解決を行なう.
- 文書組版の再現性を担保する.
- 主にこの目的のために,依存解決の結果はロックファイルに出力される.ロックファイルもバージョン管理に含めるのが望ましい.
- 取得したパッケージを,SATySFiで文書を組むのに使ったり,または更にそれらに依存するパッケージを実装するのに使う.
すなわち,SATySFiに於けるSapheは,Rustに対するCargoや,Haskellに対するCabalまたはStackに相当する機構です.
なぜ新しいエコシステムが必要だったか
SATySFi 0.0.xには,既にSatyrographosという便利なデファクトスタンダードのエコシステムがあります.そのため,Satyrographosを拡張した上でSATySFi 0.1.0に関しても継続して使うという選択肢もあります.では,Sapheを新たに設計・実装する必要性はどこから生じたのでしょうか? それは,SATySFi 0.0.xがしばらく使われるうちに顕在化してきた以下のような課題です:
- 依存関係ゆえに同一パッケージの複数のバージョンを共存させざるを得ない状況が生じることがあるが,こうした共存は現行のエコシステムではかなり回りくどい方法でしか実現できない.また,SATySFi 0.0.x本体の言語設計に於いても,同一パッケージの複数バージョンが共存処理するためには同一の名前のモジュールが共存できる必要があるが,実現可能ながらも筋の良い方法ではなく,制御が難しい.
- 任意の文字列がバージョン番号に指定でき,また後方互換性の判定もバージョンによって自動化されたりはしていないため,互換性の記述がパッケージ作成者の気づかいや技量に大きく依存している.
SATySFi処理系本体の問題点を差し引くと,これらの課題を生じる原因は実は共通しています: Satyrographosがパッケージ管理に関してOPAMというOCamlやCoq向けのパッケージマネージャに依存していることです.したがって,これらの課題を解消するには,以下のような改修が望ましそうです:
- SATySFi本体の言語設計を,同一パッケージの複数バージョン共存などにも堪えうる筋の良いものに修正する
- Satyrographosの設計・実装をOPAMに依存しないものにする
実際,前者がSATySFi 0.1.0を開発している目的の1つでもあるわけです.しかしながら,後者のSatyrographosの改修にはやや障壁がありました:
- Satyrographosの作者であるSakamotoさんが本業でかなり多忙になられており,大規模な改修の労力を捻出してもらうのは難しそうだった
- そもそも複数バージョン共存なども要件に含めるとSatyrographosとSATySFi 0.1.0の間での責務分担はかなり非自明であり,Sakamotoさんと相談しながら分担して開発を行なうにしても取っ掛かりが掴めなかった
- 一方で,私自身がSatyrographosの既存のコードベースに大幅に手を加えて0.0.x向けの機能を壊さないように0.1.0向けの機能を追加するのは労力的に割に合いそうにないと感じた
そこで,SATySFiを改築しつつエコシステムのプロトタイプを同時にゼロから構築していき,適宜それぞれの設計を修正することで責務分担の吟味を1人で反復するのが早かろうと考え,まずはそうしたプロトタイプであるSapheをつくることにした,という次第です.2024年1月の現時点でSapheはあくまでもプロトタイプ実装として位置づけており,将来的にも現状の形式をとり続けるか,または相談の上でSatyrographosに一部分として取り込んでもらうことになるかなどは未定ですが,実用に耐えるくらいには実際に動作するものになりました.
サブコマンド
現在のところ,以下の5つのサブコマンドがあります:
$ saphe init [document|library] <file-path>
:- プロジェクト(文書またはパッケージ)を初期化する.
$ saphe solve <file-path>
- パッケージの依存解決を行ない,ロックファイルを生成・更新する.
$ saphe update <file-path>
- パッケージレジストリの情報を最新のものに更新する.
$ saphe build <file-path>
- 文書を組む,またはライブラリを検査する.
$ saphe test <file-path>
- ライブラリのテストを実行する.
update
の解説は後編に譲りますが,他のコマンドは本稿の以下の章でハンズオン的に紹介します.
文書を組みたい場合のSapheの使い方
1. 初期化
カレントディレクトリ(以下 /path/to
とします)に foo.saty
という文書をつくって書き始めたいとします.この場合,まず以下を実行してファイルの雛形を生成します:
$ saphe init document foo.saty
created a package config '/path/to/foo.saphe.yaml'
created '/path/to/foo.saty'
以下のような2つのファイルが生成されました.
.
├── foo.saphe.yaml
└── foo.saty
foo.saty
が文書本体,foo.saphe.yaml
がコンフィグファイルです.文書本体 foo.saty
は以下のような内容で生成されています:
use package open StdJaReport
document (|
title = {The Title of Your Document},
author = {Your Name},
|) '<
+chapter{First Chapter}<
+p{
Hello, world!
}
>
>
従来のSATySFi 0.0.xに馴染んでいるユーザからすると,1行目の use package …
が目新しいほか,注意深く見るとレコード (| … |)
の内容がセミコロン ;
ではなくコンマ ,
で区切られていたりしますが,基本的にはほぼ見覚えのあるファイルですね.
コンフィグファイル foo.saphe.yaml
は以下のような内容です:
saphe: "^0.0.1"
satysfi: "^0.1.0"
name: "your-document"
authors:
- "Your Name"
registries:
- name: "default"
git:
url: "https://github.com/SATySFi/default-registry"
branch: "temp-dev-saphe"
contents:
document: {}
dependencies:
- used_as: "StdJaReport"
registered:
registry: "default"
name: "std-ja-report"
requirement: "^0.0.1"
CargoやCabalを使ったことがある人なら,なんとなく各部分が何を指定しているか想像できるかもしれません.saphe
には想定するSapheのバージョンを,satysfi
には想定するSATySFi本体のバージョンを,それぞれ指定します.Sapheはコンフィグファイルを読むときにまず saphe
を見て,自身がそのコンフィグファイルを適切に処理できる(とユーザが考えている)ことを確認します.^0.0.1
は「0.0.1
またはそれに後方互換なバージョン」を指す記述です.バージョン番号は基本的にはSemantic Versioningに準拠してつけますが,Sapheはバージョン番号のつけ方に追加で制約を要請します: メジャーバージョンが 0
の場合,マイナーバージョンを保ったままパッチバージョンを加算する更新は,後方互換なリリースであるとみなします(これはCargoが採用している方針と共通しています).
name
および authors
は現状必須のフィールドながら文書では特に実用上の用途がありません.後述のライブラリ作成では意味をもつフィールドですが,文書の場合はひとまず文書名と著者リストで埋めておいてください.
registries
はパッケージレジストリを指定する箇所です.基本的にはここをいじる機会は少なく,生成された記述をずっと使うのが典型的であろうと思います.社内ライブラリなど,一般には公開しないパッケージを集積するパッケージレジストリを社内ネットワークに用意したい場合は,ここに追記することになります.
dependencies
には依存パッケージの指定が入ります.典型的なユーザが最もいじる箇所はこの部分であろうと思います.初期値では「std-ja-report
パッケージの 0.0.1
またはそれに後方互換なバージョン」が依存パッケージとして指定されています.ここで書かれている used_as
がSapheでの特徴的な指定なのですが,これについては後述します.
2. パッケージの依存解決
さて,この文書を組むには,まずはパッケージレジストリのデータをもとに依存パッケージの依存関係を辿って必要なパッケージとそのバージョンを決定し,必要なものを取得する必要があります.これを行なうのが solve
サブコマンドです:
$ saphe solve foo.saty
package dependencies to solve:
- std-ja-report (^0.0.1) used as StdJaReport
package dependency solutions:
- annot 0.0.1
- code 0.0.1
- font-ipa-ex 0.0.1
- font-junicode 0.0.1
- font-latin-modern 0.0.1
- font-latin-modern-math 0.0.1
- footnote-scheme 0.0.1
- math 0.0.1
- std-ja-report 0.0.1
- stdlib 0.0.1
(中略.取得処理が並ぶ)
lock config written on '/path/to/foo.saphe.lock.yaml'.
依存パッケージは std-ja-report
ただ1つでしたが,std-ja-report
を介して間接依存するパッケージも全部かき集められて取得され,その結果がロックファイル foo.saphe.lock.yaml
に書き出されました.
.
├── foo.saphe.lock.yaml
├── foo.saphe.yaml
└── foo.saty
生成されたロックファイル foo.saphe.lock.yaml
は,ユーザが中身を読み書きすることは全くありませんが,処理の再現性のために,Gitなどによるバージョン管理には含めるのが望ましいものです.
3. 文書を組む
さて,いよいよ文書を組む準備ができたので,実際に文書を組みます.
$ saphe build foo.saty
deps file: '/path/to/foo.satysfi-deps.yaml'
---- ---- ---- ----
target file: '/path/to/foo.pdf'
dump file: '/path/to/foo.satysfi-aux' (will be created)
parsing '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/annot/annot.0.0.1/src/annot.satyh' ...
(中略)
parsing '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/stdlib/stdlib.0.0.1/src/hdecoset.satyh' ...
---- ---- ---- ----
type checking '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/stdlib/stdlib.0.0.1/src/option.satyg' ...
type check passed.
(中略)
type checking '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/std-ja-report/std-ja-report.0.0.1/src/std-ja-report.satyh' ...
type check passed.
parsing '/path/to/foo.saty' ...
---- ---- ---- ----
type checking '/path/to/foo.saty' ...
type check passed. (document)
preprocessing '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/stdlib/stdlib.0.0.1/src/option.satyg' ...
(中略)
preprocessing '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/std-ja-report/std-ja-report.0.0.1/src/std-ja-report.satyh' ...
evaluating '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/stdlib/stdlib.0.0.1/src/option.satyg' ...
(中略)
evaluating '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/std-ja-report/std-ja-report.0.0.1/src/std-ja-report.satyh' ...
---- ---- ---- ----
evaluating texts ...
modified version of stdjareport.
evaluation done.
---- ---- ---- ----
breaking contents into pages ...
(中略)
all cross references were solved.
---- ---- ---- ----
embedding fonts ...
---- ---- ---- ----
writing pages ...
---- ---- ---- ----
output written on '/path/to/foo.pdf'.
deps config written on '/path/to/foo.satysfi-deps.yaml'.
随分と色々うるさく標準出力に出ますが,最終的に無事 foo.pdf
が生成できています.開いて見てみましょう:
組めていますね.やったぜ.
実は,saphe build
はPDF以外にも若干ファイルを生成します.最終的には以下のようになっているはずです:
.
├── foo.pdf
├── foo.saphe.lock.yaml
├── foo.saphe.yaml
├── foo.saty
├── foo.satysfi-aux
└── foo.satysfi-deps.yaml
foo.satysfi-aux
はSATySFi本体が組版処理中に出力したファイルで,0.0.x時代からある相互参照のダンプファイルです(TeX/LaTeXでいうところの .aux
と等価なもの).もう一方の foo.satysfi-deps.yaml
は,SATySFi本体を起動する前にSapheがロックファイル foo.saphe.lock.yaml
をもとに生成し,依存パッケージの配置や読み込むべき順序などをSATySFi本体に伝えるためのファイルで,依存コンフィグファイルと呼んでいます.これら2つはバージョン管理から除外すべきファイルです.$saphe build
の動作を図にまとめると以下のような具合になります:
こうして $ saphe init
,$ saphe solve
,$ saphe build
の3コマンドで文書が組めるようになっているわけです.
used_as
とは何なのか
used_as
は,簡単に言えば「そのパッケージがどのようなモジュール名でプロジェクト中の use package …
で参照されるか」を指定する箇所です.
もう少し前提から紹介すると,SATySFi 0.1.xおよびSapheでは「各パッケージはメインモジュールと呼ばれるただ1つのモジュールだけを公開する」という仕組みがとられています.例えばパッケージ stdlib
は Stdlib
をメインモジュールとし,このモジュールの入れ子のメンバとして Stdlib.Option
,Stdlib.List
などのモジュールを提供するという具合です.
このメインモジュールの名前は,使う側がコンフィグファイル中で指定して替えられるようになっています.その指定を行なう箇所が used_as
だったというわけです.ためしに先ほどの例の used_as
を変更して処理してみましょう:
dependencies:
- - used_as: "StdJaReport"
+ - used_as: "FooBar"
registered:
registry: "default"
name: "std-ja-report"
requirement: "^0.0.1"
$ saphe solve foo.saty
(中略)
lock config written on '/path/to/foo.saphe.lock.yaml'.
$ saphe build foo.saty
(中略)
parsing '/path/to/foo.saty' ...
! [Error] at "foo.saty", line 1, characters 17-28:
dependency on unknown package 'StdJaReport'
StdJaReport
が知らないものとして弾かれました.文書の方も合わせて修正して処理してみましょう:
-use package open StdJaReport
+use package open FooBar
$ saphe build foo.saty
(中略)
output written on '/path/to/foo.pdf'.
処理できました.このように,used_as
で依存パッケージをどのように参照するかが指定できるようになっています.
とはいえ,現実的にはこの used_as
に指定するモジュール名としてはパッケージ名をcapitalizeしたものを書けば事足りることがほとんどです.上記の例でも std-ja-report
をcapitalizeした StdJaReport
を指定しているだけでしたし,FooBar
のような別名にするとわかりにくいため積極的に変える理由はありません.ではなぜこんな仕組みがあるのかというと,同一パッケージの異なるバージョンが共存する際にこのような名前の変更が必要となるためです.
例えば,base
パッケージの 1.x.y
と 2.x.y
を両方使わねばならない場面があるとしましょう.何もなければ両方使う必要が生じることはまずありませんが,例えば依存パッケージAとBがそれぞれ base
の 1.x.y
と 2.x.y
のみに依存していて,両方必要になることがありえます.このとき,used_as
を使うと両者を BaseOld
,Base
などと参照し分けることができます:
dependencies:
- used_as: "BaseOld"
registered:
registry: "default"
name: "base"
requirement: "^1.0.0"
- used_as: "Base"
registered:
registry: "default"
name: "base"
requirement: "^2.0.0"
この used_as
という機構によって,同一パッケージの複数の非互換なバージョンの共存が許容できる仕組みになっています.
ライブラリを実装したい場合のSapheの使い方
1. 初期化
ライブラリの実装を始めたいときは,以下のように初期化します:
$ saphe init library .
created a package config '/path/to/saphe.yaml'
created '/path/to/src/Calc.satyh'
created '/path/to/test/CalcTest.satyh'
以下のようなファイルができました.
.
├── saphe.yaml
├── src
│ └── Calc.satyh
└── test
└── CalcTest.satyh
コンフィグファイル saphe.yaml
は以下のような内容です:
saphe: "^0.0.1"
satysfi: "^0.1.0"
name: "your-library"
authors:
- "Your Name"
registries:
- name: "default"
git:
url: "https://github.com/SATySFi/default-registry"
branch: "temp-dev-saphe"
contents:
library:
main_module: "Calc"
source_directories:
- "./src"
test_directories:
- "./test"
dependencies:
- used_as: "Stdlib"
registered:
registry: "default"
name: "stdlib"
requirement: "^0.0.1"
test_dependencies:
- used_as: "Testing"
registered:
registry: "default"
name: "testing"
requirement: "^0.0.1"
contents
以外は文書の場合と同じです.contents
直下には library
というフィールドがあり,その下の dependencies
もやはり文書の場合と同様です.test_dependencies
は dependencies
とよく似た項目を書くことができ,ここにはテストでのみ使用する依存パッケージが列挙されます.典型的にはテストフレームワークを提供するパッケージが記述されることになるでしょう.
source_directories
はソースファイルを置くディレクトリを指定する項目で,ここに列挙されたディレクトリにあるファイルのうち適切な拡張子をもつものがライブラリに属するソースファイルとして認識されます.ここでは ./src
のみが指定されており,./src/Calc.satyh
のみがソースファイルです.同様に,test_directories
にはテストファイルを置くディレクトリを指定します.ここでは ./test
のみが書かれており,./test/CalcTest.satyh
のみがテストファイルです.
main_module
にはこのライブラリのメインモジュールを指定します.ここでは唯一のソースファイルである Calc.satyh
がメインモジュール Calc
を定義しており,これが指定されています.
初期化された Calc.satyh
の内容は以下になっています:
module Calc :> sig
val succ : int -> int
end = struct
val succ n = n + 1
end
テストは一旦置いておいて,まずはこのソースファイルを検査してみましょう.
2. パッケージの依存解決
文書の場合と同様に,まずはパッケージの依存解決をします:
$ saphe solve .
envelope config written on '/path/to/satysfi-envelope.yaml'.
package dependencies to solve:
- stdlib (^0.0.1) used as Stdlib
- testing (^0.0.1, test_only) used as Testing
package dependency solutions:
- stdlib 0.0.1
- testing 0.0.1
(中略.取得処理が走る)
lock config written on '/path/to/saphe.lock.yaml'.
.
├── saphe.lock.yaml
├── saphe.yaml
├── satysfi-envelope.yaml
├── src
│ └── Calc.satyh
└── test
└── CalcTest.satyh
ロックファイル saphe.lock.yaml
が生成されるのは文書の場合と同様ですが,これに加えてエンベロープファイル satysfi-envelope.yaml
も生成されました.エンベロープファイルはSATySFi 0.1.0処理系本体が読むためのファイルであり,簡単に言えばソースファイルやテストファイルの位置がコンフィグファイルから転記されているものです.ロックファイルと同様,ユーザが読み書きすることはありませんが,バージョン管理には含めるべきファイルです.
ちなみに,エンベロープとは(Sapheではなく)SATySFi処理系本体から見えるパッケージのような単位のことなのですが,これについては本稿の取り扱い範囲とせず後編に譲ります.実際,ユーザはエンベロープの存在を意識せずともSapheを介してSATySFi 0.1.0が使えるようになっています.
3. 型検査を走らせる
ライブラリの型検査はやはり build
サブコマンドで行なわれます:
$ saphe build .
deps file: '/path/to/satysfi-deps.yaml'
parsing '/path/to/src/Calc.satyh' ...
parsing '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/stdlib/stdlib.0.0.1/src/paper-size.satyh' ...
(中略)
type checking '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/stdlib/stdlib.0.0.1/src/stdlib.satyh' ...
type check passed.
---- ---- ---- ----
type checking '/path/to/src/Calc.satyh' ...
type check passed.
deps config written on '/path/to/satysfi-deps.yaml'.
通りました.やったぜ.
4. テストを走らせる
今度はユニットテストを実行してみましょう.まず,生成された CalcTest.satyh
は以下のようなファイルです:
use Calc
use package Testing
module CalcTest = struct
module IntTarget = struct
type t = int
val equal m n = (m == n)
val show = arabic
end
module IntEquality = Testing.Equality.Make IntTarget
#[test]
val succ-test =
IntEquality.assert-equal 43 (Calc.succ 42)
end
use …
は同一パッケージ内の他のファイルが定義するモジュールを参照する指定で,Haskellの import qualified …
と同様のものです.ここでは Calc.succ
をテスト対象として使うために Calc.satyh
が定義する Calc
を参照しています.
構文的にRustとよく似ていますが,#[test]
つきの val
はテスト項目として扱われます.ここでは,Calc.succ 42
の結果が 43
であることをテストする項目が書かれています.現状の Testing
の用法はファンクタを使用した若干ややこしいものですが,とりあえず IntEquality.assert-equal
が int
型の等価性をテストする函数であることだけ把握してもらえれば差し支えありません.
さて,実際にテストを走らせてみましょう:
$ saphe test .
deps file: '/path/to/satysfi-deps.yaml'
parsing '/path/to/src/Calc.satyh' ...
parsing '/path/to/test/CalcTest.satyh' ...
parsing '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/stdlib/stdlib.0.0.1/src/paper-size.satyh' ...
(中略)
parsing '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/testing/testing.0.0.1/src/testing.satyg' ...
---- ---- ---- ----
type checking '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/testing/testing.0.0.1/src/testing.satyg' ...
type check passed.
(中略)
---- ---- ---- ----
type checking '/path/to/src/Calc.satyh' ...
type check passed.
---- ---- ---- ----
type checking '/path/to/test/CalcTest.satyh' ...
type check passed.
preprocessing '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/testing/testing.0.0.1/src/testing.satyg' ...
(中略)
preprocessing '/path/to/test/CalcTest.satyh' ...
evaluating '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/testing/testing.0.0.1/src/testing.satyg' ...
(中略)
evaluating '~/.saphe/packages/6f2b80e9bb7c4e8af2104999fc25dbb3/stdlib/stdlib.0.0.1/src/stdlib.satyh' ...
evaluating '/path/to/src/Calc.satyh' ...
evaluating '/path/to/test/CalcTest.satyh' ...
OK: succ-test
---- ---- ---- ----
all tests have passed.
テストが通りました.やったぜ.ちなみに,失敗する場合はどうなるでしょうか? 実装を間違ったものに変えて実行してみましょう:
module Calc :> sig
val succ : int -> int
end = struct
- val succ n = n + 1
+ val succ n = n + 15
end
evaluating '/path/to/test/CalcTest.satyh' ...
! FAILED: succ-test
expected: 43, got: 57
---- ---- ---- ----
! some test has failed.
失敗が報告されました.これでスムーズにライブラリの開発ができそうですね.
フォントパッケージをつくりたい場合のSapheの使い方
ここまで触れてきませんでしたが,SATySFi 0.1.0が0.0.xから大きく変わっている点のひとつとして,フォントの読み込み方法があります.もともと0.0.xでは ~/.satysfi/dist/fonts/
といったディレクトリにフォントファイルを置き,~/.satysfi/dist/hash/fonts.satysfi-hash
などの設定ファイルにフォントを指し示すための名前文字列とフォントファイルの紐づけを記載するという方法を採っていました.これは仕組みとしては単純ですが,基本的にはOSのユーザ単位のグローバルな設定を書き換えねばならないため,以下のような難点がありました:
- 複数の文書・ライブラリ間で,使いたいフォント名が衝突するおそれがある.
- フォントに関して組版処理の再現性が担保しにくい.
- 最初にSATySFiをインストールするときに,デフォルトのフォントとその設定を記述したファイルをシステムにインストールせねばならず,自動で行なわれるにせよユーザが手動で行なうにせよ手順が煩雑化しやすく,移植性も下がりやすい.
Satyrographosはフォントをパッケージ単位に分割して取り扱う機能を提供し,上記の問題をうまく隠蔽してくれていましたが,今回パッケージの仕組みを改修し,処理系本体も或る程度パッケージの単位を認識するように修正したついでに,こうしたやや筋の悪かったフォントの取り扱いも刷新することにしました.その結果できたのがフォントパッケージという概念です.発想自体は従来のSatyrographosによるフォントの取り扱い方とよく似ていますが,フォントパッケージの下では従来の `Junicode-it`
といった名前文字列による指定はなくなり,FontJunicode.italic
といった識別子でのみフォントを指し示すようになります.
ファイルの配置とコンフィグファイルの概要
フォントパッケージを $ saphe init
などで初期化する機能が未実装なこともあり,ここではハンズオンではなく実例を追うだけにとどめます.フォントパッケージ font-junicode
を例にとって見てみます.まず,ファイルの配置は以下のような具合です:
.
├── saphe.yaml
└── fonts
├── Junicode.ttf
├── Junicode-Bold.ttf
├── Junicode-Italic.ttf
└── Junicode-BoldItalic.ttf
ライブラリと同様にコンフィグファイル saphe.yaml
がディレクトリ直下に配置されており,おおよそ以下のような内容が記述されています:
saphe: "^0.0.1"
satysfi: "^0.1.0"
name: "font-junicode"
authors: []
contents:
font:
main_module: "FontJunicode"
files:
- path: "./fonts/Junicode.ttf"
opentype_single:
name: "normal"
- path: "./fonts/Junicode-Bold.ttf"
opentype_single:
name: "bold"
- path: "./fonts/Junicode-Italic.ttf"
opentype_single:
name: "italic"
- path: "./fonts/Junicode-BoldItalic.ttf"
opentype_single:
name: "bold-italic"
やはり contents
以外は文書やライブラリと同じです.contents
直下には font
という項目があり,このフォントパッケージに属する各フォントファイルとそれに関する設定が files
に列挙されています.各要素は path
でまずフォントファイルへの相対パスを記述しており,そして内容の読み方を他の項目で指定しています.ここではいずれも opentype_single
という項目を記載し,単一のフォントをもつOpenTypeフォントファイルであることを指定しています.この opentype_single
の下には,name
という項目により「言語中でどのような識別子でそのフォントを扱ってほしいか」を指定します.例えば,通常のローマンのJunicodeは normal
,Junicode Bold Italicは bold-italic
という識別子でアクセスできる font
型の値になります.実際には,このフォントパッケージを依存パッケージとする文書などで used_as: FontJunicode
などとメインモジュールが指定され,そのメンバとして FontJunicode.normal
とか FontJunicode.bold-italic
といった方法でアクセスできるようになります.
(ちなみに,実は上記の main_module
という指定には特に意味がありません.ライブラリの場合と同様に,フォントパッケージを使う側が必ず used_as
でメインモジュール名を与えるがゆえに,フォントパッケージ側でメインモジュールの名前を指定する必要がないからです.正式にリリースする頃には廃止しているのではないかと思います.)
数式フォントとして使いたい場合は,name
の他に math: true
という指定をつけます(より正確には,math
という項目は省略可能で,デフォルト値が false
であるということです).例えば font-latin-modern-math
パッケージのコンフィグファイルの contents
は以下のような記述になっています:
contents:
font:
main_module: "FontLatinModernMath"
files:
- path: "./fonts/latinmodern-math.otf"
opentype_single:
name: "main"
math: true
該当ファイルがOpenType Collectionである場合は,opentype_single
ではなく opentype_collection
という項目を記載します.こちらはフォントファイルの要素となるフォントと同じ個数だけ name
と math
からなる要素が並んだリストで内容の扱い方を指定します(具体例はまだないため省略).
外部からフォントファイルを取得する場合の記述
実際には,フォントファイルが陽に path
で指定された箇所に置かれている必要はありません.フォントパッケージがエンドユーザの手元に取得されるタイミングで,エンドユーザの環境に於いてフォントファイルが特定のURLから取得されるように指定することができます.こうした仕組みにより再配布にはならない形でフォントの利用手段を提供でき,より幅広いライセンスに対応できたり,該当URLへのアクセス権限をもつ人だけが使えるフォントパッケージが作成できたりします.
これを実現するのが external_resources
という項目です.実際の現状の font-junicode
パッケージは,この external_resources
を利用してJunicodeの配布元からフォントファイルを取得する処理が書かれたものになっています:
saphe: "^0.0.1"
satysfi: "^0.1.0"
name: "font-junicode"
authors: []
external_resources:
- name: "junicode-1.002"
zip:
url: "https://downloads.sourceforge.net/project/junicode/junicode/junicode-1.002/junicode-1.002.zip"
checksum: "b1d6ed8796d8d1eacd733c0d568d939f"
extractions:
- from: "Junicode.ttf"
to: "./fonts/Junicode.ttf"
- from: "Junicode-Bold.ttf"
to: "./fonts/Junicode-Bold.ttf"
- from: "Junicode-Italic.ttf"
to: "./fonts/Junicode-Italic.ttf"
- from: "Junicode-BoldItalic.ttf"
to: "./fonts/Junicode-BoldItalic.ttf"
contents:
font:
(以降略)
external_resources
直下には外部から取得するファイルが列挙されたリストがあります.各要素は name
で一意に名前が振られており,形式・取得方法・展開方法などが関連づけられています.現在のところ,対応している形式は zip
で指定されるZIPファイルの取得のみです.zip
の直下には取得元URLである url
と内容確認用のMD5チェックサムである checksum
,そして取得したZIPファイルの中身を解凍してどう配置するかの指定 extractions
が記載されます.
ネットワークを介して任意のファイルを取得し配置する処理であるため,正式にリリースするにはセキュリティホールにならないように注意を払う必要がありますが,SATySFiでフォントを筋良く扱うには有望な仕組みであろうと考えています.
サブディレクトリを別のパッケージとして扱う
特定のサブディレクトリを,“実装が固定された別のパッケージ” と看なして扱うことができます.今のところ上位のディレクトリも指定できるので,実際にはサブディレクトリに限らない任意のディレクトリです.これは,例えば以下のような状況で便利です:
- 文書を書いているうちに特定部分だけ切り出してライブラリにしたくなってきたが,再利用性が高い形で切り分けられるかまだ自信がないし,別個のリポジトリを作成してそこに移植し修正を重ねるたびにレジストリに登録するという手間を払ってよいと言えるほど自信がないので,特定の実装を別のパッケージとして括り出しつつも手間のかからない方法が欲しい.
- フォントパッケージにはなっていないが手元にあるフォントファイルをSATySFiで使いたい.→サブディレクトリをそのフォント用のフォントパッケージにする方法によって手頃に実現できる.
指定はとても簡単で,以下のような記述を dependencies
に追加することで相対ディレクトリ ./your-local-package/
が依存パッケージのひとつとして使えるようになります:
dependencies:
- used_as: "StdJaReport"
registered:
registry: "default"
name: "std-ja-report"
requirement: "^0.0.1"
+ - used_as: "YourLocalPackage"
+ local:
+ path: "./your-local-package/"
まとめ
Sapheは以下のようなコマンドで簡単に文書やライブラリを記述することのできるエコシステムです.
- 文書を組む場合:
$ saphe init document foo.saty
で文書ファイルfoo.saty
とコンフィグファイルfoo.saphe.yaml
を初期化し,適宜書き換える.$ saphe solve foo.saty
で依存パッケージの解決を行ない,配置する.この結果ロックファイルfoo.saphe.lock.yaml
が書き出される.$ saphe build foo.saty
で文書を実際に組み,foo.pdf
を出力することができる.副産物として依存コンフィグファイルfoo.satysfi-deps.yaml
とダンプファイルfoo.satysfi-aux
が生成される.
- ライブラリを書く場合:
$ saphe init library .
でコンフィグファイルsaphe.yaml
,ソースファイルのディレクトリsrc/
,テストファイルのディレクトリtest/
を初期化し,適宜書き換える.$ saphe solve .
で依存パッケージの解決を行ない,配置する.この結果ロックファイルsaphe.lock.yaml
とエンベロープファイルsatysfi-envelope.yaml
が書き出される.$ saphe build .
でライブラリの実装を型検査できる.$ saphe test .
でユニットテストを走らせることができる.
後編は,Sapheの内部的な設計や実装について記載する予定です.