blog.ryota-ka.me

dotfilesをNix + Home Managerに移行した

「ゴミの収集が止まる年末に大掃除をするのは非合理だ」という言説を見かけた.「一理ある」と感じたので,特に家の大掃除などはしないことに決め,代わりにdotfilesの大掃除をすることにした.プロたるもの,日頃から*1自らの仕事環境に対する投資を惜しんではならない.

筆者はNixユーザであるため,この年末年始の休暇を用いてHome Managerを導入したいと考えており,実際に移行を行った.

この記事では,Home Managerの利点や導入手順,実際の導入にあたって取った選択やtipsを紹介する.

なお,筆者が用いているのはIntelおよびARMプロセッサのmacOS Montereyで,Zsh, Neovim, tmuxなどの上で生活している.もちろんNixはGNU Linuxにも対応しているので,仮に今後Linuxマシンをセットアップする機会があっても,滞りなく開発環境を構築できることが期待される.

きっかけ#

ryota-ka/dotfilesリポジトリ内にはこれまでも,各種の依存をHomebrewでインストールしたり,必要なシンボリックリンクを張ったりするインストールスクリプトとしてのMakefileを用意していた.しかし今年の初頭に,マシンにインストールする開発用ソフトウェアはすべてHomebrewからNix(nix-env)に移行しており,実際の作業環境との間で大きな乖離が生じていた.

昨今,開発用端末としてIntel MacからARM Macへの移行を見据える必要性が生じ,開発検証用端末として社用のMacBookが手元に届いた.しかし,上記のような状況のため,快適とは程遠い環境での検証を強いられていた.そのため,この機会にセットアップが簡便に行えるdotfilesを用意することにした.

余談だが,手元のHomebrewの環境は完全に壊れてしまっている.もう使っていないので特に問題はないのだが,経験上Rubyの環境はよく壊れるのであまり関わりたくない*2

$ brew doctor
/usr/local/Homebrew/Library/Homebrew/standalone/sorbet.rb:11:in `require': cannot load such file -- sorbet-runtime-stub (LoadError)
        from /usr/local/Homebrew/Library/Homebrew/standalone/sorbet.rb:11:in `<top (required)>'
        from /usr/local/Homebrew/Library/Homebrew/startup/sorbet.rb:9:in `require'
        from /usr/local/Homebrew/Library/Homebrew/startup/sorbet.rb:9:in `<top (required)>'
        from /usr/local/Homebrew/Library/Homebrew/startup.rb:10:in `require_relative'
        from /usr/local/Homebrew/Library/Homebrew/startup.rb:10:in `<top (required)>'
        from /usr/local/Homebrew/Library/Homebrew/global.rb:4:in `require_relative'
        from /usr/local/Homebrew/Library/Homebrew/global.rb:4:in `<top (required)>'
        from /usr/local/Homebrew/Library/Homebrew/brew.rb:29:in `require_relative'
        from /usr/local/Homebrew/Library/Homebrew/brew.rb:29:in `<main>'

Nix について#

えっ,まだNixを使っていない!?HERPでは必修ですよ!!*3

Home Manager について#

Home Managerは,俗に"dotfiles"と呼ばれる各種ソフトウェアの設定ファイルや,ユーザ環境にインストールされるべきパケッジを宣言的に記述するためのツールである.設定の記述はNix expression languageを用いて行う.例えば,以下のような設定ファイルを記述した上で$ home-manager switchを実行すると,NixでインストールされたGitが利用可能になり,生成されたGitの設定ファイルへのシンボリックリンクが ~/.config/git/config に作成される.

{ pkgs, ... }:

{
  home.username = "ryota-ka";
  home.homeDirectory = "/Users/ryota-ka";
  home.stateVersion = "22.05";

  programs.git = {
    enable = true;

    userName = "Ryota Kameoka";
    userEmail = "ok@ryota-ka.me";
    aliases = {
      br = "branch";
      co = "checkout";
    };
  };
}

インストール手順は公式マニュアルに譲るが,手元の環境では最近$NIX_PATHがセットされないという現象が生じており,インストールに当たって以下の環境変数を設定した.

$ export NIX_PATH=nixpkgs=~/.nix-defexpr/channels/nixpkgs:home-manager=~/.nix-defexpr/channels/home-manager

また,インストール後,$ home-manager switchを実行する段階で,既にインストールされているパケッジと競合する旨のエラーが発生した.優先度が同じだと競合するようなので,以下のコマンドを実行して優先度を調整した.

$ nix-env --set-flag priority 10 nix
$ nix-env --set-flag priority 10 home-manager-path

これまでの ryota-ka/dotfiles#

移行前のryota-ka/dotfilesリポジトリは概ね以下のようなディレクトリ構造を持っていた.

.
├── .gitconfig
├── .gitignore
├── .gitignore_global
├── .tmux.conf
├── .vim/
│   ├── (ftdetect/やftplugin/など)
│   └── plugin/
│       └── (様々な設定ファイルたち)
├── .vimrc (.vim/をruntimepathに追加し,VimPlugでインストールされるプラグインを記述する)
├── .zsh/
│   ├── .zlogin
│   ├── .zshrc
│   ├── functions/
│   │   └── (独自に定義した便利関数たち)
│   ├── functions.zsh
│   └── (その他主に.zloginから読まれる様々な設定ファイルたち)
├── .zshenv
├── Makefile
└── karabiner.json

基本的にはこれらのファイルへのシンボリックリンクをホームディレクトリに作成するという戦略を取っていた.例えば,~/.gitconfig/path/to/the/repo/.gitconfigを指すリンク,~/.vim/path/to/the/repo/.vim/を指すリンク,といった具合である.

また,~/.zsh.zsh/ディレクトリへのシンボリックリンクを作成し,$ZDOTDIRとして~/.zshを指定する設定を.zshenv内に記述していた.

.vimrcへのシンボリックリンクは~/.config/nvim/init.vimにも作成しており,Neovimからも利用可能にしていた..vim/以下のファイルは,.vimrcset runtimepath+=~/.vimと記述することで参照されるようになっていた.

方針・設定など#

Home Managerはデフォルトでは~/.config/nixpkgs/home.nixという設定ファイルを参照しようとするが,-fオプションを渡すと別のファイルを指定することができる.そこで,リポジトリ直下にhome.nixというファイルを作成することにした.設定を変更した際には,$ home-manager -f ./home.nix switchを実行することで,記述した設定内容がユーザ環境に反映される.以下では-fオプションは省略する.

知らぬ間に世間ではXDG Base Directory Specificationが普及しており,Home Managerもこれに準拠している.例えばGitの設定ファイルは~/.gitconfigではなく($XDG_CONFIG_HOMEを特に設定していない場合)~/.config/git/configに置かれることになる.

大学生の頃から8年近くに渡って継ぎ足してきたdotfilesなので,とりわけ記述量の多いZshやNeovimの設定ファイルに関しては,すべてをNix expression languageの側に寄せるのは初めの一歩としては歩幅が大きすぎる.そこで,これらに関しては既存の.zshファイルや.vimファイルをうまく活用する形で移行の実現を試みた.

Git#

.gitconfigに書いていた設定項目はさほど量が多くないため,すべての設定を素直に移行することができた.特に工夫した点もないので解説の必要もないが,以下の点は特筆すべきである.

筆者はgit-diff(1)の表示にdiff-highlightを用いている.

このexecutableがインストールされるパスが,HomebrewでGitをインストールする場合とNixでGitをインストールする場合とで異なるのだが,いちいちアドホックに$PATHを通すのも馬鹿馬鹿しい.しかし,Nix expression languageで設定を記述すれば,Gitがインストールされるディレクトリ名が事前に計算できるため,信頼性の高い絶対パスを埋め込むことができる.

programs.git = {
  enable = true;

  extraConfig = {
    interactive = {
      diffFilter = "${pkgs.git}/share/git/contrib/diff-highlight/diff-highlight";
    };
  };
};

このような設定から以下のような記述が生成される.

[interactive]
	diffFilter = "/nix/store/mf88kd4884mc47bk43ayp75x97km8hvf-git-2.33.1/share/git/contrib/diff-highlight/diff-highlight"

Zsh#

Zshの設定のコア部分を抜き出したものが以下である.

programs.zsh = {
  enable = true;

  defaultKeymap = "emacs";

  # ファイルが生成されるディレクトリ($ZDOTDIR)
  dotDir = ".config/zsh";

  # zsh-syntax-highlightingを有効化する
  enableSyntaxHighlighting = true;

  # .zshenvに追記される内容
  envExtra = ''
    if [ -e ~/.nix-profile/etc/profile.d/nix.sh ]; then
      . ~/.nix-profile/etc/profile.d/nix.sh
    fi
  '';

  # .zloginに追記される内容
  loginExtra = ''
    FPATH=${./.zsh/functions}:$FPATH

    . ${./.zsh/foo.zsh}
    . ${./.zsh/bar.zsh}
    . ${./.zsh/baz.zsh}
  '';
};

この状態で$ home-manager switchを実行すると,NixでZshがインストールされ,~/.config/zsh/以下に.zshenv, .zshrc, .zloginなどのファイルが作成される.

作成された~/.config/zsh/.zshrcは概ね以下のようになっている.ただしコメントや空行は適宜省略した.

また~/.config/zsh/.zloginは以下のような内容になっている.

loginExtraに渡すstringにpath型の値を埋め込むことで,当該ファイル(e.g. .zsh/foo.zsh)が/nix/store/ディレクトリ以下に入っていることがわかる..zsh/foo.zsh.zsh/functions/*.zshはもはや$ZDOTDIRとして指定するディレクトリ(~/.config/zsh/)以下には存在しないが,ファイルの位置が適切に解決されるなるならば,もとより入れておく必要もなかったことに気付く.このようにして,既存の.zshファイルとHome Managerとのブリッジングを行った.

Neovim#

筆者はVimPlugを用いて各種プラグインを管理している.Nixのエコシステムではpkgs.vimPlugins.*で種々のVimプラグインのderivationが利用可能であり,Home Managerもprograms.neovim.pluginsにプラグインのリストを渡すことで,Neovimのパケッジ・マネジャー機構を用いて統合を行ってくれる.しかし,順調に乗り換えられるか否かという不確実性を嫌い,今回は引き続きVimPlugを用いてプラグインを管理することにした.

VimPlugの標準的なインストール方法は,cURLを用いて所定のパスにファイルを配備するというものであるため,Nixとの相性が悪い.そこで,VimPlugだけはNix経由で調達することにした.pkgs.vimPlugins.vim-plugというderivationが存在するので,これをビルドした結果のディレクトリ内に存在するplug.vimというファイルをinit.vimから読み込むことにする.

programs.neovim = {
  enable = true;

  extraConfig = ''
    " 従前の設定ファイルを置いているディレクトリをruntimepathに追加
    set runtimepath+=${./.vim}

    " VimPlugをロード
    source ${pkgs.vimPlugins.vim-plug}/plug.vim

    " VimPlugで管理するプラグインの一覧(かつての.vimrc)をロード
    source ${./plugins.vim}
  '';
  withNodeJs = true;
};

ここでwithNodeJs = true;に注目したい.これはNode.jsproviderを有効化するオプションである.仮に自前でNode.js providerをセットアップしようとすれば,何らかの手法でNode.jsをインストールし,$ npm install -g neovimを実行してライブラリをインストールする必要がある.また,Python providerをセットアップしようとすれば,何らかの手法でPython 3をインストールし,$ pip install neovimでライブラリをインストールしなければならない.各言語固有のパケッジ・マネジャーを用いてライブラリをインストールしていくと環境がとっ散らかりがちだが,withNodeJsオプションやwithPython3オプション(デフォルトで有効)を指定するだけでNix側でインストールの面倒を見てくれ,必要なものを勝手に調達してくれるのはなかなか体験がいい.また,Nixに管理を任せることで,不要になった際にはガーベジ・コレクションさえ行ってくれる.

さて,上記の設定を元に$ home-manager switchを実行すると,以下のような~/.config/nvim/init.vimが生成される.

Zshのときと同じく,参照したいファイルないしディレクトリへのパスを埋め込んだ文字列を元に,Home Managerに設定ファイルを作らせる事によって,当該ファイルやディレクトリをNixの管理下に置き,既存の.vimファイルとのブリッジングを行っている.

その他のソフトウェア#

direnvなどもHome Managerに統合されている.direnvを使うには,インストールした上で,普段使っているシェルの設定ファイルに自分でhookを一行書き加える必要があるが,Home Managerを用いるとそのような設定を.zshrcに自動的に差し込んでくれる.これは「ユーザ環境の設定」という行為を一箇所で包括的に行うからこそできる芸当だろう.

読むべき資料#

以下のページに,設定可能なオプションが網羅的に列挙されている.

しかし,網羅的であるがゆえに自分の興味の範疇を出たものも多いであろう.そこで,GitHub上でHome Managerの実装を参照し,興味のあるソフトウェアのmoduleを個別に眺めていくことをおすすめする.例えば以下はprograms.neovimに対応するmoduleである.

また,筆者が実際に行った設定を眺めるのも参考になるだろう.

脚注#

*1: 年末に大掃除をするのではなく普段から掃除をしましょう.

*2: 個人の感想です.

*3: 社内の開発におけるNix活用に関しては別の機会に記事にしたいと思っている.