🌃

一般構築魔法(Nix)のVimへの応用について

2024/02/23に公開

始めに

Vimにはdark powerがよく似合う。

いわゆるShougowareが真っ先に思い浮かぶのがVimmerの性ではあるが、ここでは闇夜より出でしNixの話をしたい。

Nixとは宣言的、再現性、信頼性におもきをおいたパッケージマネージャである。
プログラムから設定ファイル、データに至るまで、あらゆるものを一元的に管理することができ、それはVim環境においても例外ではない。
Vim本体から始まりプラグイン、Language Serverやその他外部プログラムまで、全てNixで管理できるのだ。

そんなNixがなぜdark powerなのか、少し起源をたどってみよう。
Nixの起源については諸説あるが、ここではギリシア神話にそれを求めることとする。
古代ギリシアの詩人、ヘシオドスの神統記によると、世界にはまずChaos(混沌)があった。そして次にGaia(大地)、Tartarus(奈落)、Eros(愛)があった。
Chaosはその後Erebus(幽冥)とNyx(夜)を生み出す。地下世界の闇と地上世界の闇である。
その後NyxはErebusとの間にHemera(昼)とAether(高天)を授かる。地上と天上に光が誕生した瞬間である。
神統記ではNyxがHemeraという自身と対となる存在を生み出した過程は描かれないが、ここで大きなブレイクスルーが起きていることは想像に難くない。
Nyxは混沌の闇夜のなかで生き抜くべく「\overset{\scriptsize \text{ニックス}} {\small \text{世界を創造する魔法}}」を開発していたのである。混沌から生まれた夜の神であるNyxが世界を創造する魔法を生み出し、世界に光をもたらしたのは必然と言えよう。
さらにNyxは16柱の神々を矢継ぎ早にデプロイし、世界を創造する魔法の強力な特徴の一つである宣言的な性質を自ら実証したと言われている。
そんな原初世界に革新を起こしたNyxであったが、系譜としてはGaiaを祖とする一族に圧倒されることとなる。これは世界を創造する魔法が稀代のHaskellerであったNyxにしか扱うことのできない代物であったからであろう。誰もが扱うことのできる魔法でなければ普及することはないというのは何も驚くべきことではない。
また余談ではあるが、Gaiaの系譜の神々は時折奈落に幽閉される描写があるが、Nyxのそれにはそうした描写が見られない。これは世界を創造する魔法にdependency hellを防ぐ術が組み込んであったことの左証に他ならない。
このように強力でしかし難解な「\overset{\scriptsize \text{ニックス}} {\small \text{世界を創造する魔法}}」であったが、人類は数千年の研究を経て神話として語り継がれてきたこの技術をコンピュータ上で再現できるようになった。
それが今日広く知られている「\overset{\scriptsize \text{ニックス}} {\small \text{一般構築魔法}}」である。つまりNixの出自自体が既にdark powerなのである。

そして今、そのNixがvim-jpのコミュニティでは空前絶後のブームとなっている。体感では8秒に一人入門しているといっても過言ではない。
Zennなどの媒体においてもこの1,2年ほどの間に数多くのNixのタグがついた投稿を目にした方も多いだろう。
それだけではない。Redditなどを見ても明らかなように、日本だけでなく北米を始めとする全世界にNixの波が到来しているのだ。
Nixの何が人を惹きつけるのかを説明するには話が脱線しすぎるため、他の記事や、vim-jp slackの#tech-nix、DiscordのNix日本語コミュニティといったコミュニティに解説を譲る。

前置きが長くなったが、これから神話より伝わる闇の技術Nixを使って、Vimの環境を制する方法を紹介しよう。

なお、以降の文章ではVim/Neovimを合わせてVimと呼ぶ。注釈がない限りどちらにも共通する話題だと考えてほしい。

この記事の目的

この記事はまずVimのユーザーに向けて、Nixを使った環境の例をいくつか挙げながら紹介する。続いて主にプラグインやその他パッケージの開発者に向けて、Nixを使った再現性の高い最小限の環境を提供する方法を紹介する。これらを通じて、Nixをどのように実生活に取り込んでいけるか想像できることを願っている。

一つ注意として、この記事を読んですぐにNixを使いこなせるようになるとは思わないでいただきたい。ここではNixについての説明は敢えて最小限にしている。これはNixを使いこなすための指南書ではなく、Nixを使って何ができるかを紹介するためのものである。書いてある以上のことをやろうとすると必ずどこかで躓くだろう。
それでもこの文書によってNixが普及した世界がどのようなものであるのか、片鱗だけでも感じていただければ幸いである。

Nixのインストール

もしまだNixをインストールしていない場合は、Derminate Systemsが提供するDeterminate Nix Installerを使ってインストールすることを推奨する。
Nixが合わないと感じ、アンインストールすることを選択した場合でも、すべてのリソースを削除してくれるアンインストーラが付属しているため安心して試してほしい。
Nixのインストール自体はこの記事の対象外のため、詳しい解説は上記ガイドやそのほかの記事に譲ることとする。

NixでVimを管理する

Nixに触れたばかりの人は何でもNixで管理しなければならないという使命感を燃やしてしまう傾向にある。そのような情熱はとても大切なものではあるが、Nixはあまりに複雑でいとも簡単にあなたの心を折るに違いない。
そのため気楽にNixと向かい合ってもらうため、簡単なステップから紹介する。管理対象を広げていく形で進めるが気に入ったものだけ実践すればよいし、もちろん気に入らなければやる必要もない。

Vim本体のみをNixで管理する

Nixの紹介記事では決まってNixOSやHome Managerなどを持ち出して不必要に話を大きくしてしまうきらいがある。
あなたはNixを試したいのであって環境全体をNixで管理することに興味がないかもしれない。Vimの設定をしたいだけなのにHome Managerのお作法を学ぶのは面倒だろう。
ドリルを欲しがる顧客には穴を提供するのではなく、DIYセットを提供するのでもなく、ドリルを提供すべきなのだ。

以下のコマンドはnixpkgsでパッケージングされたVim/Neovimをインストールすることなく実行するコマンドである。

nix run nixpkgs#vim    # for vim
nix run nixpkgs#neovim # for neovim

すでにvimrcやinit.luaを持っているのなら、上記コマンドで既存の設定ファイルを読み込んだVimが起動するはずだ。ただしプレーンなVimが起動するため、ビルド時にパッチを当てたりコンパイルオプションを変更していたりするといつもの環境とは異なるものが立ち上がるかもしれない。そして私が使用するプラグインのいくつかはNeovimのHEADを要求するため、上記コマンドではうまく起動させることはできない。

Nixはnixpkgsに存在するパッケージしか扱えないのだろうか。そんなことはない。
公式のリポジトリ外のパッケージであっても同じ操作体系で実行することができる。そしてNeovimは自身のリポジトリにおいてNixのパッケージを提供している。
つまりNixを使うとgccやcmake、その他全ての依存パッケージの準備を必要とすることなくNeovimのHEADビルドができるのである。
試しに次のコマンドを実行してみてほしい。

nix run "github:neovim/neovim?dir=conrib"

文字通り最新のNeovimが動作しているのが確認できるだろう。

NixでインストールするVimが期待通りに動作することが確認できたら実際にインストールしてみよう。
Home Managerなどを利用している場合はhome.packagesvim/neovimを追加するか、programs.neovim.enable = trueのような設定を追加する。
それらを利用していない場合はnix profileコマンドを使って以下のようにインストールするとよい。

nix profile nixpkgs#vim

which vimと入力して/nix/store/...から始まるパスが表示されれば成功だ。

これでdark powerなVim本体が手に入った。一般構築魔法を習得したことについて胸を張っていい。

設定ファイルやプラグインの管理もNixに委譲する

一つ前のステップでNixを使ってVimを管理できるようになった。しかしこれだけではあまりNixを使う妙味を感じられていないだろう。なぜならこれまで使っていたOS付属のパッケージマネージャーや自前のビルドスクリプトからNixに変更しただけだからだ。

そこでさらに、プラグインを例にNixのdark powerを見ていくことにしよう。
このステップではmarkdown-preview.nvimを例に話を進める。
しかしこれはただの例示であってなにもこのプラグイン自体にしか使えない方法を紹介するものではない。

このプラグインはNode.jsに依存しているため、使用するにはそれをインストールする必要がある。既にNode.jsがグローバル環境に存在するならばプラグインのインストール自体はそれほど難しくはないかもしれない。
しかしグローバルなものに依存しているといつか痛い目を見ることになるだろう。また、このプラグイン一つだけの面倒を見るだけであればそれほど大変ではないが、大抵こういったものは複数存在することが世の常である。それらが各々異なる言語ランタイムで動作するものであれば目も当てられたものではないということは言うまでもない。

そしてmarkdown-preview.nvimは依存パッケージをインストールするため、プラグインを追加する際にmkdp#util#install()を呼ぶかyarn installを実行する必要がある。
こうしてユーザーの管理下にない制御不能なパッケージが増殖していくのだ。
https://github.com/iamcco/markdown-preview.nvim?tab=readme-ov-file#installation--usage

ここでNixの出番である。
nixpkgsで配布されるVimは実は単なるバイナリではなく、各種設定ファイルや環境変数を読み込むbashによるwrapperが被せられている。また、Vim標準のpackages機能にも対応しているため、パッケージマネージャーを使わないシンプルな構成であれば全てNixのみで完結させることも可能である。

そしてnixpkgsに収載されたプラグインは既にnode_modulesやその他外部のプログラムへの依存関係が解決された上でパッケージングされている。利用者は以下のようにpackages.*.startに必要なプラグインを列挙するだけでよい。また、始めに述べたようにNixではdependency hellは起こり得ないため安心して追加してほしい。

$XDG_CONFIG_HOME/nixpkgs/config.nix
{
  packageOverrides =
    pkgs: with pkgs; {
      myNeovim = neovim.override {
        configure = {
          packages.myVimPackage = with pkgs.vimPlugins; {
            start = [ markdown-preview-nvim ];
          };
        };
      };
    };
}

試しに実際に上記のコードを$XDG_CONFIG_HOME/nixpkgs/config.nixに追加するとmyNeovimというパッケージ名でインストールが可能となる。そしてnix build --impure nixpkgs#myNeovimを実行すると./result/bin/nvimというシンボリックリンクが得られる。これを使ってmarkdownファイルを開くと、:MarkdownPreviewが実行できることが確認できるだろう。

この./result/bin/nvimが何をやっているのかを知るには実際にコードを読んでみると良い。このファイルはbashによるneovimのラッパーになっているため、catコマンドなどで中身を見ることができる。最終行を見るとinit.luaを読み込んだりpackpathやrtpをよしなにセットしているようである。
これらの中身は見てもらえばわかるように、packageOverridesで設定したものとなっている。興味があればさらに/nix/storeから始まるファイルやディレクトリにアクセスしてみると面白いかもしれない。

./result/bin/nvim
#! /nix/store/cjbyb45nxiqidj95c4k1mh65azn1x896-bash-5.2-p21/bin/bash -e
export NVIM_SYSTEM_RPLUGIN_MANIFEST='/nix/store/dsh2q1yxav6fgr7dcw97frb0r0iwvflj-neovim-0.9.5/rplugin.vim'
export GEM_HOME='/nix/store/53qjf5bhwn5i59krh5pjn6xj61zs074q-neovim-ruby-env/lib/ruby/gems/3.1.0'
PATH=${PATH:+':'$PATH':'}
if [[ $PATH != *':''/nix/store/53qjf5bhwn5i59krh5pjn6xj61zs074q-neovim-ruby-env/bin'':'* ]]; then
    PATH=$PATH'/nix/store/53qjf5bhwn5i59krh5pjn6xj61zs074q-neovim-ruby-env/bin'
fi
PATH=${PATH#':'}
PATH=${PATH%':'}
export PATH
LUA_PATH=${LUA_PATH:+';'$LUA_PATH';'}
LUA_PATH=${LUA_PATH/';''/nix/store/asy0yg5bhi8ws7aahh83m20yhp69xpl3-luajit-2.1.1693350652-env/share/lua/5.1/?/init.lua'';'/';'}
LUA_PATH='/nix/store/asy0yg5bhi8ws7aahh83m20yhp69xpl3-luajit-2.1.1693350652-env/share/lua/5.1/?/init.lua'$LUA_PATH
LUA_PATH=${LUA_PATH#';'}
LUA_PATH=${LUA_PATH%';'}
export LUA_PATH
LUA_PATH=${LUA_PATH:+';'$LUA_PATH';'}
LUA_PATH=${LUA_PATH/';''/nix/store/asy0yg5bhi8ws7aahh83m20yhp69xpl3-luajit-2.1.1693350652-env/share/lua/5.1/?.lua'';'/';'}
LUA_PATH='/nix/store/asy0yg5bhi8ws7aahh83m20yhp69xpl3-luajit-2.1.1693350652-env/share/lua/5.1/?.lua'$LUA_PATH
LUA_PATH=${LUA_PATH#';'}
LUA_PATH=${LUA_PATH%';'}
export LUA_PATH
LUA_CPATH=${LUA_CPATH:+';'$LUA_CPATH';'}
LUA_CPATH=${LUA_CPATH/';''/nix/store/asy0yg5bhi8ws7aahh83m20yhp69xpl3-luajit-2.1.1693350652-env/lib/lua/5.1/?.so'';'/';'}
LUA_CPATH='/nix/store/asy0yg5bhi8ws7aahh83m20yhp69xpl3-luajit-2.1.1693350652-env/lib/lua/5.1/?.so'$LUA_CPATH
LUA_CPATH=${LUA_CPATH#';'}
LUA_CPATH=${LUA_CPATH%';'}
export LUA_CPATH
exec -a "$0" "/nix/store/0mrn8874pisnpizlkkvcchkly3dkrhif-neovim-unwrapped-0.9.5/bin/nvim"  -u /nix/store/h39ncvdv1sr0n8qk7wfr0crk5kakhlx1-init.lua --cmd "lua vim.g.loaded_node_provider=0;vim.g.loaded_perl_provider=0;vim.g.loaded_python_provider=0;vim.g.python3_host_prog='/nix/store/dsh2q1yxav6fgr7dcw97frb0r0iwvflj-neovim-0.9.5/bin/nvim-python3';vim.g.ruby_host_prog='/nix/store/dsh2q1yxav6fgr7dcw97frb0r0iwvflj-neovim-0.9.5/bin/nvim-ruby'" --cmd "set packpath^=/nix/store/xx44lq349fmr7cy15fnwnycayhy33d5w-vim-pack-dir" --cmd "set rtp^=/nix/store/xx44lq349fmr7cy15fnwnycayhy33d5w-vim-pack-dir" "$@" 

気に入った設定ができたら忘れずにnix profile install --impure nixpkgs#myNeovimでインストールしておこう。

さらに詳しく知りたい方は多少古い情報もあるが、NixOS Wikiの以下のページを参照するとよい。
https://nixos.wiki/wiki/Vim

一点だけ注意として、wikiに出てくるvim_configurable.customizeというのはvim-full.cusomizeに置き換えられている。
2024年2月現在ではまだvim_configurableにエイリアスが貼られているが、将来的に完全に置き換わるため後者を使用することを推奨する。

lazy.nvimとNixを使った私の環境

ここまでVim自体と標準のpackages機能を用いてプラグインをNixで制御する方法を紹介してきた。プラグインマネージャーを使わないミニマルな運用をしている方であればすぐに実践に移れるかもしれないが、私を含めほとんどの方はそうではないだろう。
さらに現代ではLanguage Serverを筆頭として、外部のプログラムの補助なしには開発することもままならない。Neovimであればmason.nvimなどを使ってこれらをインストールすることが一般的だが、これもNixで一元管理できるとレイヤーが減り嬉しいはずだ。

また、私は怠惰な人間なので、プラグインの導入はできるだけコピペで済ませたい。勝手に遅延ロードされているともっと嬉しい。
ここではそうしたプラグインマネージャーと外部の依存プログラム、Nixを協調させる一例として、私の設定ファイルの一部を紹介する。

このステップはこれまでのものとは違いnixpkgsによって公式に提供された方法ではない。あくまでも現時点における私のセットアップであり、ベストプラクティスではないことに留意してほしい。

以下はlazy.nvimに従った私のプラグイン定義の一例である。
https://github.com/natsukium/dotfiles/blob/8b65470ab0cef80760832e2586345dfd57490d05/nix/applications/nvim/lua/plugins/misc.lua#L257-L266

lazy.nvimには以下のようにowner/repoをテーブルに追加すると勝手にGitHubからクローンしてくる機能がついている。

{
    "iamcco/markdown-preview.nvim",
}

しかし私はNixでパッケージングされているものを使いたいため、プラグインの存在するディレクトリを直接指定したい。lazy.nvimの場合、dirというキーにパスを渡せばよいが、Nixでインストールしたものは既にご存知の通り/nix/store/...と容易に指定できるパスにはなっていない。
そこで一旦以下のように設定しておくこととする。

markdown.lua
{
    name = "markdown-preview.nvim",
    dir = "@markdown_preview_nvim@",
}

Nixを使っているとこのようにスクリプト中にパッケージのパスを埋め込みたいということが頻繁に発生する。ということはそれにこたえる仕組みが存在するということだ。
実際nixpkgsにはsubstituteAllという便利関数が存在しており、これはファイル中の@@で囲まれた変数を置換する。
つまり上記の設定は次のように置換されるのである。

markdown.lua
{
    name = "markdown-preview.nvim",
    dir = "/nix/store/g6qlysarjpi72plww1570mifba93lq2k-vimplugin-markdown-preview.nvim-2022-05-13",
}

実際に動作を確認しておこう。置換前のmarkdown.luaをカレントディレクトリに用意し、REPLで次のように実行してみるとよい。

$ nix repl '<nixpkgs>'
nix-repl> builtins.readFile (
            pkgs.substituteAll {
              src = ./markdown.lua;
              markdown_preview_nvim = pkgs.vimPlugins.markdown-preview-nvim;
            }
          )
"{\n    name = \"markdown-preview.nvim\",\n    dir = \"/nix/store/g6qlysarjpi72plww1570mifba93lq2k-vimplugin-markdown-preview.nvim-2022-05-13\",\n}\n"

これでnixpkgsで配布されるプラグインを既存のプラグインマネージャで制御できるようになった。あとはこのファイルをlazy.nvimが読める場所に配置してやるだけでいい。

次に外部パッケージである。
Home ManagerではVimがアクセスできるパスにパッケージを配置するextraPackagesというオプションが提供されている。私はここで必要なLanguage ServerやLinterやFormatter、そのほかNeovim上で使いたいツールを定義している。

https://github.com/natsukium/dotfiles/blob/8b65470ab0cef80760832e2586345dfd57490d05/nix/applications/nvim/default.nix#L77-L83

宣言的な管理方法のためmason.nvimなどを使う場合よりも短期的な利便性は失っている。しかし一時的に使用するだけであればnix shellnix profile install等を使って必要なツールを即座に用意できるため、それほどマイナスにはならない。

この構成の利点は必ずしもプラグインをNixで管理しなくても良いという点にある。
nixpkgsのvimPluginsは1,2週間ごとに更新されるが、常に最新のコミットを追いたいこともあるだろう。あるいはそもそもパッケージングされていないプラグインを試したいこともある。
もちろんこれらのケースを適切にNixで管理する方法もあるが、往々にしてそれらはオーバーキルである。
幸いプラグインはほとんどの場合適切な場所にgit cloneするだけで動作するので、基本はlazy.nvimやその他プラグインマネージャーに管理を任せ、依存パッケージのインストールが発生するもののみ上で示したようにnixpkgsのもので置換するという方法を採用するのも悪くないように思う。

Nixでプラグインの実行環境を提供する

ここまで、ユーザー環境におけるVimの設定に目を向けてきたが、この節では設定済みのVimを配布する方法を紹介する。これができると例えばプラグイン開発者が、必要なプラグインやプログラム、設定例を同梱したデモ環境を簡単に提供できるようになるといった嬉しさがある。

プラグインが純粋なvimscriptやluaであれば現状でも簡単に試すことができるが、もしそれが外部のコマンドや言語ランタイムに依存している場合少し試してみるという行為自体のハードルが上がる。
これもNixによって解決できる課題の一つである。
NixはGitHubなどのリモートリポジトリからソースコード一式を適切な依存パッケージとともにダウンロードし、実行ファイルを生成、実行することができる。
先に紹介したNeovimのHEADビルドの例では実行ファイルとして素のNeovimを生成する。そしてnixpkgsでは設定ファイルやプラグインをまとめたラッパーを提供できるのであった。
つまり、プラグインの設定例や依存するツールをまとめたラッパーを作成し、それを配布するというのは既に紹介したものを組み合わせることで実現できるのである。

Vimから話が逸れてしまうが、こうした取り組みは有名なツールだと例えばlsコマンドの代替を目指すezaやLocal LLMの先駆けとなったllama.cppなどが採用しており、時として面倒な環境構築なしに手元で実行して試してみることができる。
https://github.com/eza-community/eza?tab=readme-ov-file#try-it
https://github.com/ggerganov/llama.cpp
この他にも多くのプロジェクトに既にNixが入り込んでいるので、見かけたことがある方もいるかもしれない。

さて、ここではデモとしてskkeletonの例を紹介する。
skkeletonはVimでSKKを扱うためのプラグインである。このプラグインはdenops-vimに依存しており、さらに漢字に変換するための辞書が別途必要となっている。
既にdenoや辞書をインストールしてあれば導入に苦労することもないだろうが、ここではそれらをインストールしていないクリーンな環境を考えよう。必要ならNixのコンテナを用意するとよい。

説明の前に実際に私が用意した環境を試してみてほしい。次のコマンドを実行すると安定版のNeovimが起動する。

nix run github:natsukium/skkeleton/flake#neovim-skkeleton

インサートモードに入り、<C-j>を入力すればSKKによる入力ができるようになっているだろう。これはdenoやSKK辞書をインストールしていなくても正常に動作するはずだ。Vimすらも必要ない。
必要なのはNixだけである。

ここでやっていることは先に述べたように前の節までに紹介してある。こうした設定例を用意し、
https://github.com/natsukium/skkeleton/blob/59203a55b9d0905b087a9ac14e08534ee3354c8c/flake.nix#L80-L119

その設定と必要なプラグインをラッパーに追加するだけである。
必要であればmakeWrapperを使って外部のプログラムへのパスを追加すればいい。
https://github.com/natsukium/skkeleton/blob/59203a55b9d0905b087a9ac14e08534ee3354c8c/flake.nix#L121-L149

慣れるまではこのようなNixのファイルを用意するのは少々大変かもしれないがユーザーに大きなメリットをもたらす。
煩雑なセットアップを要することなくプラグインを試すことができるだけでなく、専用の環境が提供されるためユーザーの設定起因のトラブルが発生しない。採用しない場合、依存パッケージも含めプラグインがユーザー環境に残らない。
開発側としても、開発中のプラグインの動作確認が容易になるだろう。複数人で開発しているときVimのバージョンも含めたテスト環境を揃えることができるのも大きな利点となる。

最後に

この記事では言葉足らずなところも多く、Nixの魅力を十分に伝えきれていないかもしれない。読んで即戦力になるような情報も提供できていないだろう。
だが少しでもNixによって解決できることを知ってもらえれば幸いである。
dark powerによって生みだされたNixが読者の環境に光明をもたらすことを願っている。

GitHubで編集を提案

Discussion