シングル Makefile で簡単 SATySFi サンドボックス環境を使おう!

:::note info
これは「[SATySFi Advent Caleandar 2022](https://adventar.org/calendars/7515)」の4日目の記事です。
（3日目は [zr_tex8r さん](https://qiita.com/zr_tex8r/items/d426a24ee9674f044a20)、5日目は [yozu さん](https://qiita.com/yasuo-ozu/items/4746bf8ad8eb63e4c4bc)です。）
:::

SATySFiの環境を整備するのは、ちょっと厄介です。例えば

- インストールしないといけないものが多い。例えば、opam, ocaml, satysfi, satyrographos, 各種パッケージ, ...
- インストール手順が煩雑。例えばopamリポジトリの設定、satyrographosの実行、...
- 各ドキュメント毎にパッケージを管理したい(仮想環境)のに、通常のパッケージ管理ではユーザー全体のパッケージデータベースに影響を与えてしまう
- satysfiやsatyrographos、各種パッケージのバージョンをドキュメント毎に変えるのが大変
- SATySFiパッケージとOCamlのパッケージが混ざるので管理しづらい

このような点を解決するMakefileを作成しました。

https://gist.github.com/yasuo-ozu/ef3134dbc80f657a9f476de4c80df113

```
$ wget https://gist.githubusercontent.com/yasuo-ozu/ef3134dbc80f657a9f476de4c80df113/raw/00f972c64113c7b94dbb6af14649e8398a230dab/Makefile
```

# 使い方

1. まずシステムに `opam` をインストールします。
1. ドキュメント用のディレクトリを作成し、そこに移動します。
1. Makefileをダウンロードして配置します。
1. `make` を実行します。 `Satyristes` ファイルが無い状態で実行するとサンプルの `*.saty` ファイルを作成しpdfを生成します。

例えば、 `satysfi_doc` という空ディレクトリの中で上の手順を行うと、以下のようなファイルが生成されます。

```bash
$ ls
.gitignore  Makefile  Satyristes  _build  _opam  satysfi-satysfi_doc.opam  satysfi_doc.pdf  satysfi_doc.saty  satysfi_doc.satysfi-aux
```

## ファイルの説明

### `Satyristes`

`satyrographos`が読み込むファイルで、`*.saty`ファイルのコンパイルに必要なSATySFiパッケージをリストアップします。同じフォルダに複数の `*.saty` ファイルが存在する場合、 `Satyristes` にも対応する `doc` エントリが存在する必要があります。
このファイルが存在しない場合、 `make` 実行時にサンプルのファイル構成が生成されます。

サンプル

```clojure
(lang 0.0.3)
(doc
  (name "satysfi_doc")
  (workingDirectory "./")
  (build (
    (make-with-env-var "satysfi_doc.pdf")
  ))
  (dependencies (dist))
  (autogen ($today))
)
```

### `satysfi-ドキュメント名.opam`

opamが読み込むファイルで、`ドキュメント名.saty` で使われるパッケージとそのバージョンをリストアップします。 `Satyristes`ファイルで指定されたパッケージはすべてこのファイルにも含まれる必要があります。
SATySFiのパッケージ管理では、パッケージをインストールするのはsatyrographosですが、ダウンロードやバージョンの解決のためにopamが使われるため、`Satyrographos`と`*.opam` の両方が必要になっています。

サンプル

```satysfi-satysfi_doc.opam
opam-version: "2.0"
depends: ["satysfi" { >= "0.0.5" & < "0.0.7" } "satysfi-dist" "satyrographos"]
```

### `ドキュメント名.saty`

メインのファイルです。

サンプル

```satysfi_doc.saty
@require: stdjabook
@require: $today
let-inline ctx \today = Today.datetime |> embed-string |> read-inline ctx
in
document (|
  title = {\SATySFi;};
  author = {};
  show-title = true;
  show-toc = false;
|) '<
	+p { Today is \today; }
>
```

### `ドキュメント名.d`

Makeが読み込むファイルで、`ドキュメント名.saty`内に画像ファイルが使われていた場合、`make` 実行時にそのパスが`ドキュメント名.d`ファイル内にリストアップされます。
そのため、実際にその画像が存在しない場合でも、`*.pdf` を生成する前に `make 画像ファイル名` が実行されるため、予め `Makefile` 内に適切なルールを書いておくことで`*.saty`ファイル内で使用している画像を別のソフトウェアで生成することができます。(make cleanで削除される)

### `_build`

satyrographosが作成する仮想環境用のディレクトリです。(make cleanで削除される)

### `_opam`

`satysfi`, `satyrographos` 等のバイナリや `satysfi-dist` 等のパッケージのファイルがインストールされる仮想環境です。(make cleanで削除される)

### `*.satysfi-aux`

SATySFi が生成する一時ファイルです。(make clean で削除される)

# 解説

## 仮想環境について

このMakefileでは、ユーザーの環境に対して影響を与えないため、ドキュメントのディレクトリに閉じた仮想環境を作成します。
ここでいう仮想環境には、以下の2つが存在します。

- opamの仮想環境 ... `opam switch`コマンドによって`_opam`以下に作成される。
- satyrographosの仮想環境 ... `satyrographos build`コマンドによって `_build` 以下に作成される。

パッケージをインストールするのになぜ2つも仮想環境が必要になるのが疑問に思うでしょうが、それはSATySFiのパッケージをインストールする際に以下のような2段階の処理が行われるからです。

- `opam install パッケージ名` コマンドでSATySFiのパッケージ(に `satysfi-` というプレフィクスをつけたもの)を `satyrographos-repo` というopamリポジトリからダウンロードしてインストールする。インストールされたファイルはopamレポジトリ(仮想環境の場合は`_opam`)で管理される。
- `satyrographos install`コマンドを実行すると opam リポジトリを探索してsatysfiパッケージを探し出し、必要なファイルを `dist` ディレクトリにインストールする。
- `satysfi` コマンドはコンパイル中 `@require:` を処理する際、 `dist` ディレクトリを参照する

そのため本Makefile では `opam switch` 及び `satyrographos build` を活用することで2つの仮想環境を管理しています。

## `make build` コマンドの動作

`make` (もしくは `make build`) を実行すると以下の処理が行われます。

- 存在しない場合、opamの仮想環境 `_opam` を作成する。
- `Satyristes`が存在しない場合、サンプルファイルを生成する。
- 仮想環境`_opam`内に `satysfi`, `satyrographos`をインストールする。(この際、`*.opam`内に書かれたバージョン制約を反映)
- `satyrographos build`コマンドを用いて、`Satyristes`内に記述されたドキュメントを全てコンパイルする

`satyrographos build`コマンドが参照する`Satyristes`は例えば次のようになっています。

```clojure
(lang 0.0.3)
(doc
  (name "satysfi_doc")
  (workingDirectory "./")
  (build (
    (make-with-env-var "satysfi_doc.pdf")
  ))
  (dependencies (dist))
  (autogen ($today))
)
```

ここでは `make-with-env-var` が使われており、これは`satysfi_doc.pdf`生成時に`make`コマンドを使用することを意味します。この場合 `satyrographos build`コマンドは以下のことをします。

- ドキュメント毎に仮想環境を作る。(`_build`)
- `Satyristes`内に指示された依存パッケージを`_opam`から探し出し、仮想環境にインストールする。
- `make satysfi_doc.pdf` を実行する。

ここで`make satysfi_doc.pdf` が実行された時に呼ばれる部分は以下のようになっています。

```Makefile
%.pdf:	%.saty $(SATYSFI_BIN) Makefile
	@if [ -n "$$SATYSFI_RUNTIME" ]; then \
		eval $$(opam env) && satysfi -C "$$SATYSFI_RUNTIME" --no-default-config "$<" -o "$@" ; \
	fi
```

`$SATYSFI_RUNTIME` には `satyrographos build` が作成した仮想環境のディレクトリが与えられます。これを `-C`オプションを界してsatysfiに与えています。
ここでpdfを作成するためにわざわざMakefileを呼び出している理由なのですが、それは`*.saty`が依存しているファイルをMakefile内で同時に生成することを想定しているからです。例えば、`satysfi_doc.saty`ファイルの中で`example.pdf`ファイルを使用している場合、最初の`make build`実行時に以下のファイルが自動的に生成されます。

```satysfi_doc.d
satysfi_doc.pdf:    example.pdf
```

このファイルは`satysfi_doc.pdf`を生成するためには`example.pdf`が必要であるという事実を述べており、`make satysfi_doc.pdf`を実行したときに読み込まれます。
`example.pdf` が存在しない場合、自動的に`make example.pdf`が実行され、Makefile が画像ファイルを生成しようとします。

例えば、`example.svg`から自動的に`example.pdf`を生成したい時は、`Makefile`に以下の内容を追記(もしくは`include.am`というファイルを作成)すればよいです。
(なお、この場合`example.pdf`は`make clean`で自動的に削除されないので注意してください。このような中間ファイルは一時ディレクトリを作成し、その中に生成させるのがおすすめです。)

```Makefile
%.jpg:   %.svg
    inkscape "--export-filename=$@" "$<"
```