Nixによる再現性のあるTypst執筆環境
この記事はTypst Advent Calendar 2024 の7日目の記事です。
再現性を担保してPDFを作りたい
Typstに限らず組版作業をしていると、再現性の問題にぶつかることがあります。
古い資料を再利用しようとしたら、コンパイルできない ... こういう問題を解決するお話です。
コンパイルができなくなる原因は以下の2つがあります。
組版ソフトウェアが外部のソフトウェアに依存している:
TexLive で コードハイライトを行うために pygments が必要
高度なグラフを描画するために graphvizが必要 など
組版ソフトウェアのバージョンが異なるためにコンパイルできない
macOSでは新しいTexLiveの環境でコンパイルしていたけど
サーバーにのこってる古いTexLive環境でビルドに失敗してしまう
組版ソフトウェアが外部のフォントや画像などに依存している:
TexLive で うっかりヒラギノフォントをつかった資料をつくってしまい
Linux 環境でフォントがなくコンパイルができなくなるなど。
ひとつめの課題について、Typstは素晴しいことに wasm 処理系を搭載しているので、外部ソフトウェアもwasmとして同梱してしまえば、上記の悩みに悩まされることは少なくなりました。しかし、まだ環境依存な部分があります。フォントです。
Typstはフォントの管理をプラットフォームに依存しており、Typstのパッケージの範囲外になります。これは興味深い設計で、TexLiveがフォントをパッケージとして同梱しているのとは対照的です。そこで、私は 再現性を売りにしているパッケージマネージャである Nix をつかうことを提案します。Nix は以下の利点があります。
Nix は汎用パッケージマネージャなので外部ソフトウェアの依存関係も記述できる
Nix はロックファイルがあるので、組版ソフトウェアのバージョンを固定できる
Nix はフォントもパッケージされているのでフォントごとインストールできる
Typst にある --ignore-system-fonts と組み合わせると、システムから完全に独立したビルド環境が作れます。以下にその方法を記します。
Nix Flakeで管理するため プロジェクトルートに flake.nix というパッケージ管理用のファイルを書きます。Nix が参照するレジストリを指定します。ここでは、最新の typstを使える unstable ブランチを指定しています。unstable で大丈夫なのか? と思うかもしれませんが、 Nix はロックファイルを生成するので一度ロックファイルを生成してしまえばバージョンが固定されるので、諸々に振り回されることはありません。
code:nix
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable";
flake-parts.url = "github:hercules-ci/flake-parts";
};
Nixのパッケージ管理は関数を基本としており、上記で指定したレジストリを 引数として、パッケージ (プロジェクト) のレシピを出力とします。Nix の関数記法は 引数: ボディという形をしています。以下の場合、引数部は inputs@{nixpkgs, flake-parts} です。 @ は特殊な記法であり、 仮引数@構造化束縛 という形になっています。
code:nix
outputs = inputs@{ nixpkgs, flake-parts, ... }:
では実際のNix によるプロジェクトの定義を書いていきます。プロジェクトを簡単に書くためのフレームワークである flake-parts を採用しています。flake-parts の lib にある mkFlake という関数に二つの引数を渡します。ひとつめは パッケージが受けとる引数情報 {inputs} です。
ふたつめに処理を書きます。systems には、Nix が対応するプラットフォームの情報; ここでは全てのプラットフォームに対応していると宣言します。perSystem に、プラットフォームごとの処理をする関数を渡します。
ML言語風に let 構文で変数を定義していきましょう。
inputfile は入力用のtypstファイル
outputfile は出力されるpdfファイル
code:nix
flake-parts.lib.mkFlake { inherit inputs; } {
systems = nixpkgs.lib.platforms.all;
perSystem = { pkgs, ... }: let
inputfile = "main.typ";
outputfile = "main.pdf";
以下にフォントも定義します。 with はシンタクスシュガーで with zzz; [ aaa bbb ] を [zzz.aaa zzz.bbb] に展開します。ここでは typst.app にならって Notoフォントを指定しています。
そして、これらのフォントパッケージから フォントのpathを生成します。nix のパッケージ (pkgs.noto-fonts-cjk-sansなど)は特殊で、それ自体がパッケージ定義のオブジェクトであると同時に文字列化されるときに 自動で パッケージがインストールされるディレクトリのパスに変換されます。つまり、以下のコードでは 自動で /path/to/cjk-sans:/path/to/cjk-serif のようなパスが得られます。
code:nix
fonts = with pkgs; [
noto-fonts-cjk-sans
noto-fonts-cjk-serif
];
font-path = builtins.concatStringsSep ":" fonts;
これらから PDFを実際にコンパイルするスクリプトを作ります。
${pkgs.typst}/bin/typst は typst というパッケージがインストールされる
ディレクトリにある bin/typst を指定しています。
--font-path ${font-path} で typst が参照するべきフォントを指定します。
--ignore-system-fonts で外部のフォントを使わないことを保証します。
この typst-compile というシェルスクリプトを実行すると実際に main.typ から main.pdf が生成されます。
code:nix
typst-compile = pkgs.writeShellScriptBin "compile" ''
${pkgs.typst}/bin/typst compile --font-path ${font-path} --ignore-system-fonts ${inputfile} ${outputfile}
'';
ついでにどのフォントが使えるのかを確認するシェルスクリプトも書きましょう。
code:nix
typst-fonts = pkgs.writeShellScriptBin "fonts" ''
${pkgs.typst}/bin/typst fonts --font-path ${font-path} --ignore-system-fonts
'';
さて、ここまでが let 節で定義した変数になります。この変数をつかっていよいよパッケージを書きます。ML言語の慣例にならって let を in で閉じたあとに実行可能なアプリを定義します。ここでは compile と fonts というアプリを登録しました。
code:nix
in
{
apps = {
compile = {
type = "app";
program = typst-compile;
};
fonts = {
type = "app";
program = typst-fonts;
};
};
};
これらのアプリは以下のように使えます。nix run が実行コマンドであり、レジストリ#アプリ名 を実行します。ここではカレントディレクトリ . にある compile と fonts を実行しています。
code:bash
nix run .#compile # ちゃんと PDFが出力される
DejaVu Sans Mono
Libertinus Serif
New Computer Modern
New Computer Modern Math
Noto Sans CJK HK
Noto Sans CJK JP
Noto Sans CJK KR
Noto Sans CJK SC
Noto Sans CJK TC
Noto Sans Mono CJK HK
Noto Sans Mono CJK JP
Noto Sans Mono CJK KR
Noto Sans Mono CJK SC
Noto Sans Mono CJK TC
Noto Serif CJK HK
Noto Serif CJK JP
Noto Serif CJK KR
Noto Serif CJK SC
Noto Serif CJK TC
さて以上で Nix で Tyspt コンパイルする基本的な環境がつくれました。Typstのバージョンのみならずフォントもすべて管理できる nix を使えば、過去の資料をコンパイルする必要にせまられても 当時の環境をすぐに再現できますね!