序文

GNU Guixでは、簡単にパッケージを作り、自分でそれを利用することができます。 GNU Guixでは、パッケージ定義群は「チャンネル」という名前がついています。 各々が自分だけのチャンネルを作り、それを公開するなりローカルに保持するなりし、Guixにそれを教えてあげることで、 そのチャンネルを経由してパッケージを利用することができるようになります。

この記事では、おそらく最も簡単であるフォントのパッケージを実際に作り、チャンネルを経由して配信するところまでを扱います。

前提

GNU Guix自体はインストールされていて、使えるようになっていることを前提とします。

チャンネルを用意する

チャンネルは、(試したことないですが)ローカルに用意することもできますし、GitHubやGitLabのレポジトリホスティングサービスを使うこともできます。 チャンネルにしたいレポジトリかディレクトリ直下に、以下のような内容のファイル .guix-channel をおいてください。 <your-channel-URL> は、ローカルの場合は file:///path/to/channel/ 、 GitHubやGitLabレポジトリの場合は https://gitlab.com/username/channel の形式のURLに置き換えてください。 また、レポジトリを利用する場合はコミットしてプッシュするのを忘れないでくださいね。

(channel
 (version 0)
 (url "<your-channel-URL>"))

これはチャンネルの宣言です。依存関係がある場合や、チャンネルをサブディレクトリに置きたい場合には、 ここで一緒に宣言します。詳しくは公式のドキュメント(英語)をご覧ください。

次に、チャンネルの位置をGuixに教えてあげます。 ~/.config/guix/channels.scm に以下を書いてください。 <your-channel-URL> は先程と同様に置き換えてください。また、 your-channel-name は任意の名前です。 好きな名前を付けて愛着を湧かせましょう。

(cons* (channel
        (name 'your-channel-name)
        (url "<your-channel-URL>")
        ;; もしレポジトリのデフォルトブランチが `main' の場合は以下を追記
        ;; (branch "main")
        )
       %default-channels)

これで登録は完了です。以下のコマンドを実行し、今登録したチャンネルからパッケージ定義を取り寄せていることを確認しましょう。

guix pull

もし成功していれば、以下のような表示が得られます。

Updating channel 'your-channel-name' from Git repository at '<your-channel-URL>'...
Updating channel 'guix' from Git repository at 'https://git.savannah.gnu.org/git/guix.git'...
Building from these channels:
  guix      https://git.savannah.gnu.org/git/guix.git	a4d52f0
  your-channel-name    https://github.com/ROCKTAKEY/roquix	0000000
(後略)

これで準備は完了です。

パッケージを定義する

さて、パッケージを定義していきましょう。 まず、先程のディレクトリの下に your-channel-name 、及び your-channel-name/packages ディレクトリを作成してください。 これは単なる慣習です。基本的には your-channel-name/packages/ 以下にパッケージ定義を置いていきます。

今回は、Cicaというフォントのパッケージを作成していきます。 先にコードを貼っておきます。 your-channel-name/packages/fonts.scm に以下を書いてください。

(define-module (your-channel-name packages fonts)
  #:use-module (guix packages)
  #:use-module ((guix licenses) #:prefix license:)
  #:use-module (guix download)
  #:use-module (guix build-system font))

(define-public font-cica
  (package
    (name "font-cica")
    (version "5.0.3")
    (source (origin
              (method url-fetch)
              (uri (string-append
                    "https://github.com/miiton/Cica/releases/download/"
                    "v"
                    version
                    "/Cica_v"
                    version
                    ".zip"))
              (sha256
               (base32
                "0vshn2cd70mnbavsw9cbagcasa95wiv9qdj4wkzxn7gxygqvrlfb"))))
    (build-system font-build-system)
    (home-page "https://github.com/miiton/Cica")
    (synopsis "Japanese monospaced font for programming")
    (description
     "Cica is a Japanese monospaced font for programming.
Hack + DejaVu Sans Mono is used for ASCII, and Rounded Mgen+ for the other.
In addition, Nerd Fonts, Noto Emoji, Icons for Devs, and some adjustment forked from
the Ricty generator are converted and adjusted.")
    (license license:silofl1.1)))

このコードはSchemeという言語で書かれています。 Lispに慣れていないと括弧に圧倒されるかもしれませんが、基本的には (関数名 引数1 引数2...) が関数呼び出しとなっています(このような塊をS式と呼びます)。 ただし、評価されないS式も多々出てきます(S式を関数呼び出しとして解釈して実行することを評価と呼びます)。 それは上位の関数的なもの(マクロと呼び、関数と全く同様な呼び出し記法で呼び出す)が各引数を評価するかどうか自由に決めることができるからですが、 理解できなくても構いません。ひとつずつ解説していくので安心してください。

モジュール

まずは、冒頭にある define-module の式をご覧ください。

(define-module (your-channel-name packages fonts)
  #:use-module (guix packages)
  #:use-module ((guix licenses) #:prefix license:)
  #:use-module (guix download)
  #:use-module (guix build-system font))

これは名前の通り、「モジュールを定義する」部分になります。実はパッケージは、Schemeのモジュールという機構によって定義されます。 ここではそれを定義することでこのファイルをモジュールとし、Guixがモジュールとしてファイルを読み込むことができるようにします。

第一引数である (your-channel-name packages fonts) は、ちょうどディレクトリの構造及びファイル名を反映していなければなりません。 第二引数と第三引数、及びそれ以降の引数の組は #:use-module (guix packages) のような形をしています。 これは、このモジュールがなんのモジュールに依存しているかを表しています。 依存が足りなかったら後でエラーが出て、「○○を use-module してなくない?」と教えてくれるので、 厳密にどのパッケージがどこにあるか覚えていなくても問題にならないことが多いです。 ちなみに #:prefix license: の行では、「このモジュールでエクスポートされた変数は license: という接頭辞を付けてアクセスする」という指定が付いています。

パッケージ定義

さて、いよいよパッケージ定義です。外側から順番にみていきましょう。

(define-public font-cica
  ...)

define-public は、変数を定義する define の亜種です。 こちらも変数を定義しますが、それに加えて「モジュールの外からも見えるようにする(つまり公開状態(public)にする)」という機能があります。 先程も言ったように、Guixはモジュールを経由してパッケージ定義を得ます。そのため、パッケージ定義は公開されている必要があります。 第一引数は変数の名前で、第二引数がその値です。

さて、以下のようなものが値として与えられていました。

(package
 (name "font-cica")
 (version "5.0.3")
 (source ...)
 (build-system font-build-system)
 (home-page ...)
 (synopsis ...)
 (description ...)
 (license ...))

package というのは、 package というオブジェクトを生成する、所謂コンストラクタにあたるものです (たぶん。違ったらすみません。普段使っているLispの方言と色々違っていてまだ完全にSchemeを習得できていません)。 各引数は (name "font-cica") のようになっています。 (フィールド名 値) の形で各フィールドの値を与えています。 名前(フィールド名)付きの引数(値)を複数渡せるようになっていると考えるとわかりやすいかもしれません。ちなみに順不同です。

この (フィールド名 値) のS式は評価されない(評価されたらフィールド名が関数名と解釈されて評価後の値が package の引数となってしまうため、 package コンストラクタがフィールド名を知る術がなくなる)ため、 package コンストラクタはマクロだろうと推測できます(これも定かではない)。 なお、 の部分は評価されます。

少し話が逸れましたが、ここまで来ればあと少しです。 package コンストラクタに渡す引数をひとつひとつ見ていきましょう。

nameフィールド、versionフィールド

まずは nameversion です。

(package
  (name "font-cica")
  (version "5.0.3")
 ...)

name は、文字通りパッケージの名前です。 この値は、コマンドラインからパッケージをインストールする場合に使います(例: guix install font-cica)。 バージョン違いなど、パッケージとなる対象が同じ場合、名前は他のパッケージと被っても構いません。 複数の定義がある場合はデフォルトで最も新しいものがインストールされます。 バージョンを指定してインストールすることも可能(例: guix install gcc-toolchain@10)なため、 バージョン違いで名前を被せることはむしろ有用です。

version はバージョンを表す文字列です。 おそらく形式に決まりはないですが、先頭に"v"を付けずに"5.0.3"のようにする場合が多いです。

home-pageフィールド、synopsisフィールド、descriptionフィールド

一番説明が面倒な sourcebuild-system を一旦飛ばして、 home-pagesynopsisdescription に移ります。

(package
  ...
  (home-page "https://github.com/miiton/Cica")
  (synopsis "Japanese monospaced font for programming")
  (description
   "Cica is a Japanese monospaced font for programming.
Hack + DejaVu Sans Mono is used for ASCII, and Rounded Mgen+ for the other.
In addition, Nerd Fonts, Noto Emoji, Icons for Devs, and some adjustment forked from
the Ricty generator are converted and adjusted.")
  ...)

この3つは割と見たまんまで、 home-page にはパッケージの対象のホームページを、 synopsis にはパッケージの1行の概要を、 description はパッケージの説明を文字列で与えます。なお、 descriptiontexinfoの記法を使えます。

licenseフィールド

次は license です。

(package
  ...
  (license license:silofl1.1))

ここにはライセンスを指定します。ライセンスを表す定数は (guix licenses) モジュールで定義されています。 利用できるパッケージは公式レポジトリで見ることができます。MITライセンスの名前は expat なので注意。 モジュール定義の際に #:prefix license: を指定したため、 silofl1.1 の代わりに license:silofl1.1 となっています。

sourceフィールド

次は先程飛ばした source です。

(package
  ...
  (source (origin
            (method url-fetch)
            (uri (string-append
                  "https://github.com/miiton/Cica/releases/download/"
                  "v"
                  version
                  "/Cica_v"
                  version
                  ".zip"))
            (sha256
             (base32
              "0vshn2cd70mnbavsw9cbagcasa95wiv9qdj4wkzxn7gxygqvrlfb"))))
  ...)

source は少し複雑ですが、基本的にはほとんどイディオムで、普段は専らコピペしています。 source フィールドには origin オブジェクトを渡しています。 origin オブジェクトはその場で生成して渡しています。 origin オブジェクトのコンストラクタの部分を抜き出してみます。

(origin
  (method url-fetch)
  (uri (string-append
        "https://github.com/miiton/Cica/releases/download/"
        "v"
        version
        "/Cica_v"
        version
        ".zip"))
  (sha256
   (base32
    "0vshn2cd70mnbavsw9cbagcasa95wiv9qdj4wkzxn7gxygqvrlfb")))

フィールドは methodurisha256 の3つあることがわかります。

method としてここで与えているのは url-fetch です。 他には git-fetch を使うことが多いと思います。 その場合、 uri フィールドではgitレポジトリの情報を与えなければならないため、以下のような形になります。

(uri (git-reference
      (url "https://example.com/your/git/repo")
      ;; コミットハッシュの代わりにタグを使うこともできます。
      ;; (commit (string-append "v" version))
      (commit "0000000")))

uri として与えているのは文字列です。 string-append は単に文字列を結合する関数で、引数に文字列を与えてそれを呼び出しています。 マクロの力(たぶん、もしかしたらScheme自体の特徴かも)により、 version という名前で先程 (version "5.0.3") として渡した version フィールドの値 "5.0.3" にアクセスできるため、 呼び出した結果は以下のようになるはずです。

;; S式
(string-append
          "https://github.com/miiton/Cica/releases/download/"
          "v"
          version
          "/Cica_v"
          version
          ".zip")

;; 評価後の値
"https://github.com/miiton/Cica/releases/download/v5.0.3/Cica_v5.0.3.zip"

sha256 には、 謎の文字列を base32 に渡したものが与えられています。 これはいわゆる「ハッシュ値」というもので、与えられたディレクトリの内容から一意に定まります。 URIから得られたファイルがパッケージを定義した人と一致していない場合、偽物を掴まされたことになりますが、 ハッシュ値を比較することで偽物であることを感知できます。 これにより、偽物を掴まされたことに気付かずにそのパッケージを使用してウイルスやマルウェアに侵されることを防ぎます。 この方法ではパッケージを定義した人がマルウェアを配信している場合に感知することはできないので、信頼できるチャンネルだけを登録しましょう。

このハッシュ値は、 guix download <URL> と入れることで得ることができます。 対象がgitレポジトリの場合、そのレポジトリをクローンして guix hash -rx /path/to/repo とすることで得ることができます。 後者の場合、異なるコミットやタグにチェックアウトしている場合は異なるハッシュ値が返るので注意してください。 また、裏技として、適当なハッシュ値を入れてパッケージをビルドすると「ハッシュ値が間違ってるよ」と言いながら実際のハッシュ値を教えてくれます。

build-systemフィールド

最後は build-system です。

(package
  ...
  (build-system font-build-system)
  ...)

build-system では、「パッケージをどのようにビルド・インストールするか」を与えます。 フォント専用の font-build-system の場合は既にあるファイルを所定のディレクトリに配置する作業が主です。 フォント以外、例えばソースコードからビルドするような場合には、ビルド方法を指定する必要があります。 ビルド方法によって色々定義されています。詳細は公式マニュアルにあります。 明日の記事で詳しく触れる予定です。

パッケージをビルドしてみる

さて、これでパッケージの定義が終わりました。実際に使ってみましょう。 まずはパッケージをあなたのチャンネルに配信しましょう、と言いたいところですが、 実際に動くかわからないコードをいきなりプッシュするのは怖いですよね。 guix コマンドは -L オプションでチャンネルとなる(つまりロード対象となる)ディレクトリを渡すことで、 一時的にそのチャンネルを登録したかのように振舞います。 今定義したパッケージ font-cica をビルドすべく、以下を実行してみましょう。

guix build -L /path/to/your/channel/ font-cica

正常にビルドが成功すれば、大量のログの末尾に /gnu/store/p6v2bm5s9diamkanr9z6c7r63ikzibb5-font-cica-5.0.3 のような表示が出ていると思います (ただしハッシュ値は違う可能性があります)。

次に、それをインストールした環境に入ってみます。

guix shell -L /path/to/your/channel/ font-cica

guix shell は、指定したパッケージが *追加で*インストールされた環境に一時的に入ります (元々入っていたパッケージは引き継がれた上で、新しい環境に入ります もしまっさらな環境に指定パッケージだけが入った環境へ一時的に移りたい場合は、 --pure オプションを付ければよいです)。 shellのプロンプトに [env] が追加されていたら正常に移行できています。

とはいっても、フォントをインストールした場合はなにかを実行できるわけではなく、 このような環境に入ったところでよくわからないので、一旦 exit してください。 実行ファイルがビルドされるようなパッケージであれば、ここで実際に動くか試すことが可能です。

フォントが入っているか確かめるために、以下のコマンドを実行してまた新しい環境に移動してみましょう。

guix shell --container -L /path/to/your/channel/ font-cica fontconfig grep

これは先程定義した font-cica に加えて、 fontconfiggrep をインストールした環境へ一時的に移行するコマンドです。 今回は --container オプションを付けています。これにより、指定したパッケージだけをインストールした(つまり今の環境にあるパッケージは含まない)、 ファイルシステムを含めて完全に独立した環境へ一時的に移行するオプションです。 --pure オプションの場合も指定したパッケージだけをインストールした環境へと一時的に移行するという点では同じですが、 ファイルシステムが独立していないという点が異なります。 これは本筋と全く関係のない話なので無視して構いませんが、ここで --pure ではなく --container を利用している理由は、 fontconfig がキャッシュをユーザーディレクトリに残すからか、 ファイルシステムが共有の --pure では元の環境にインストールされているフォントが漏れ出してしまうからです。

さて、新しい環境で以下を実行してみましょう。

fc-list | grep Cica

これは fc-list で得られたフォントの一覧を、 grep コマンドで “Cica” を含むものだけに絞りこんでいます。 ここでなんらかの出力が得られれば、成功です。無事フォントをインストールできていることが確認できます。 終わったら exit を打って環境を戻しておきましょう。

念のため、なにもない環境でなんの出力も出ないかどうかを確認してみましょう。

guix shell --container font-cica fontconfig grep
fc-list | grep Cica

ここでなにも出ないことが確認できれば、 font-cica パッケージを入れたことによってフォントがインストールできたことの証明になります。

ちなみに余談ですが、 guix shell である環境に一時的に移行してあるコマンドを実行してすぐ戻ってくる、という操作は簡略化できます。 具体的には、以下のように末尾に -- 実行したいコマンド とすればよいです。

guix shell --container font-cica fontconfig grep -- fc-list | grep Cica

ただし、上記の場合パイプ | の後ろの grep は一時環境から戻ってきた後に実行されているため、 現環境に grep がインストールされている必要があることに注意してください。つまり以下のように解釈されています。

(guix shell --container font-cica fontconfig grep -- fc-list) | grep Cica

私もこれに対する解法は持ち合わせていません。 パイプや &&guix shell に渡す方法があれば教えていただけると幸いです。

パッケージをチャンネルに配信する

これでパッケージの動作確認が終わりました。最後に lintstyle をかけておきましょう。

1
2
guix style -L /path/to/your/channel/ font-cica
guix lint -L /path/to/your/channel/ font-cica

lintについては必要以上に厳しい場合があるので、脆弱性など重要そうなものでなければ無視しても構いません。 特に上流のレポジトリに対して「tagが打たれてない」とか「GitHubでReleaseが作られてない」とか言われますが、大抵の場合どうしようもないです。 一行の長さなどは従っておきましょう。

Gitレポジトリのホスティングサービスを使っている人は、変更をコミットしてプッシュしてください。 ローカルの人はそのままでよいです。

そうしたら、また以下のコマンドを打ってください。

guix pull

これによって、新しいパッケージ定義がGNU Guixに読み込まれました。 パッケージ定義が読み込まれているか確認するために、以下のコマンドを打ってみましょう。

guix show font-cica

これで今登録したパッケージの詳細が表示されれば成功です。もしそのままインストールしたければ

guix install font-cica

とすればよいですし、取り除きたければ

guix remove font-cica

とできます。また、一つ前の状態に戻したい(たとえば入れたパッケージがやっぱりいらなかったので入れなかったことにする、など)場合は、

guix package --roll-back

とすればよいです。

最後に

この記事では、フォントという比較的簡単な対象に対してパッケージ定義を行いました。 この「簡単」という言葉に驚くかもしれませんが、そもそもパッケージ定義自体に加えてその周辺知識についても一緒に説明しているため、 フォントのパッケージ定義自体は簡単でも、記事としてはかなり密度が高くなっています。 実際に font-cica に書いた定義自体も30行程度と短いですし、フォントのパッケージ定義自体よりもその周辺知識に頭を使ったと考えてください。 難しく感じるのも無理はありません。もしなにか質問などあれば、コメントなりメールなりで受け付けます。 また、Guixの日本語コミュニティであるGuix-jpもあります(ちなみに今回作ったパッケージと同じものがGuix-jp公式チャンネルにあります)。 そちらには私以上のエキスパートの人がいますので、是非そちらもご興味あれば覗いてみてください。

次回の記事

次回の記事は「Guixのパッケージをつくってみよう-その2: ビルドが必要なパッケージを作る」です。