shell.nixとdirenvでプロジェクトごとに補完をいい感じにする
概要
HERPでは shell.nix をgit repositoryのrootに置いて依存関係を解決しているが、PATH がprojectごとに設定されるだけなので 例えば kubectl の補完が有効になるわけではない。
その課題を解決するために shell.nix にzshの補完もうまく済ませるワザップを今回は書く。
zsh completion について
zsh の completionの仕組みをざっくり書くと、 FPATHをいい感じにして compinit すれば FPATHから解決されるcompletion用のscriptが実行され補完される
手法
FPATH用のderivation
まず、completionを集めたderivationを作る。
buildEnv というderivationの特定のディレクトリの集合体を作る関数があるのでそれを使う
FPATH = "${nixpkgs.buildEnv {
name = "zsh-comp";
paths = buildInputs;
pathsToLink = "/share/zsh";
}}/share/zsh/site-functions";
FPATHの設定
上記で作った FPATH を $HOME/.zshrc で追加された FPATH に足せばいいのだが
安易な方法として shell.nix にFPATHを足してしまうと、そのまま上書きされて既存の補完の全てを失ってしまう
なので、 project_FPATH のような環境変数に一時的に保存する。
このまま nix-shell --run zsh のように実行する場合は, ZDOTDIR をshell.nix中で上書きするなどのコードをを足せば問題ないのだが、
いちいち実行するのが面倒なので direnvを使っている場合は工夫が必要になる。
use nix
path_add FPATH $project_FPATH
以下の理由でこのコードは動きそうに見えて動かない(人によっては動く)
direnvは .envrc の評価をbash上で行っており、評価した結果の差分を既存のshellに足すような実装になっている。(コレは echo $0 してあげるとわかる)
実はzshの FPATH は exportされていない変数なので (コレは echo ${(t)FPATH} でわかる、コレをlocal変数と言っていいのかはわからない) bash上で評価した際に存在していない変数として扱われFPATHが上書きされる
なので direnvの評価が始まる前、つまり ~/.zshrc などで export FPATH (typeset でも可) しておく必要がある
export FPATH してない人間向け対応
世の中には export FPATH していない人間もいるので、そう言った人が projectに入って direnv allow した瞬間に壊れて cd するのも困るのは不親切という考え方もあるので対応をする。(考え方によっては学習機会を奪っている。人間は困らないと進化しないので)
上記で書いたように 評価がbash上で行っているので export -p した中に FPATH が含まれていなければ export FPATH されていないことがわかる
よって以下のようになる
use nix if export -p|grep "declare -x FPATH" ; then path_add FPATH $project_FPATH fi
compinitの設定
あとは compinit するだけなのだが毎回 compinit していてはダルい(ダルい)
なので direnv がhookで評価した後にFPATHが書き変わっていれば compinit するようなhookを作ってあげれば良い
export COMPINIT_DIFF="" _chpwd_compinit() { if [ -n "$IN_NIX_SHELL" -a "$COMPINIT_DIFF" != "$DIRENV_DIFF" ]; then compinit -u COMPINIT_DIFF="$DIRENV_DIFF" echo "compinited !" fi } if [[ -z ''${precmd_functions[(r)_chpwd_compinit]} ]]; then precmd_functions=( ''${precmd_functions[@]} _chpwd_compinit ) fi if [[ -z ''${chpwd_functions[(r)_chpwd_compinit]} ]]; then chpwd_functions=( ''${chpwd_functions[@]} _chpwd_compinit ) fi
FPATH の diffを取ると言ったが色々考えた結果,nix-shell中でdirenv が実行されたときにcompinitされればいいので上記のようにした。(compinit以外のこともしたくなるかもしれない)
蛇足として一応書いておくが autoload compinit は他でするように
終わりに
nix-shell と direnv の組み合わせは環境を揃える手法として有名だが、実は補完まで揃えることができる。
各個人のzshrcに追記する必要はあるがオススメできる手法である。
余談
HERPのSREチームはnixを使って各種ツールの環境を揃えている。