【NTT Communications Advent Calendar 2023】Nix と home-manager で dotfiles を管理する話
この記事は、 NTT Communications Advent Calendar 2023 9日目の記事です。
みなさんは dotfiles をどのように管理されているでしょうか? Bash や Zsh といったスクリプトによってインストールを管理したり、 git でホームディレクトリを管理する、 Chef や Ansible のような構成管理ツールを用いて管理する方法があると思います。 自分もシェルスクリプトによる管理をおこなったり、 Ansible 等の構成管理ツールの利用や 自前の構成管理ツール をつくって管理していましたが、現在は Nix と home-manager というものを使って管理しています。 今回はこの Nix と home-manager による dotfiles の管理をどのように行えばよいのか、それらによって管理する Pros/Cons にはどのようなものがあるのかについて解説します。
Nix
まずそもそも Nix とはどういったものなのかについて解説します。
Nix とは NixOS という Linux ディストリビューションに搭載された apt や yum/dnf のようなパッケージ管理システムです。 Nix はドキュメントにも記載してありますが、 純粋関数型パッケージマネージャー です。
Nix is a purely functional package manager.
purely functional なパッケージマネージャーとはどういったものなのかというとビルドされたパッケージは副作用のないビルド方法を記載した関数によってビルドされ、そのビルドされたものは変更されることがないというものです。 Haskell の値をパッケージマネージャーに見立ててもらうとわかりやすいと思いますが、 Haskell においては値は副作用のない関数によって計算され、その値はその後に変更されることがありません。 これをパッケージ管理で実現したものが Nix になります。
ビルド方法を記載する関数が purely functional であるため、再度ビルドしても同じ生成物が作成されるようになっています。 このため、一度ビルドができてしまえば常に同じ環境を実現できるようになっています。
Nix Language
Nix では Nix Language という言語でパッケージのビルド方法等を記載します。
Nix Language は ドキュメント によると
- ドメイン特化
- 宣言的
- 純粋
- 関数型
- 遅延性
- 動的型付け
という特徴を持つ言語です。
Nix Language は基本的は文字列型や数値型、真理値型等が存在します。
その他にも絶対パスや相対パスも表現できます(先頭が #
である行はコメント行です)。
# integer 1 # floating 3.14 # string "single line string" '' multi line string '' # string interpolation "toString ${toString 3}" # boolean true # null null # absolute path /etc # relative path ./foo.json
その他にもリストや集合といった複合型もあります。
# list [ 1 "123" ] # set { x = 1; y = 2; }
また重要なコンポーネントである関数は次のように記述します。
# f(x) = x + 1 x: x + 1 # f(x, y) = x + y x: y: x + y # function call: f(100) = 100 + 1 (x: x + 1) 100
他にも値を束縛するための let ~ in ~
や if ~ then ~ else ~
といった制御構文や関数の引数におけるパターンマッチ等が存在します。
より詳細に知りたい方は Nix Reference Manual の Nix Language セクションを読んでいただければと思います。
home-manager
そんな Nix を利用してユーザー環境を管理できるようにした仕組みが home-manager です。 home-manager を利用するとホームディレクトリーに dotfiles を設置したり、必要なパッケージを事前にインストールしたりといったことが可能になります。
ユーザー環境の管理自体は簡単で、次のコマンドを実施すると構成を管理する ~/.config/home-manager/home.nix
が生成されます。
nix-shell --run sh '<home-manager>' -A install
~/.config/home-manager/home.nix
には次のような内容を記載します。
{ config, pkgs, ... }: { home.username = "<ユーザー名>"; home.homeDirectory = "<home.username ユーザーのホームディレクトリーのパス>"; # ここは ~/.config/home-manager/home.nix を生成したときにつくコメントを参考にして設定する。 # デフォルトだとコマンド実行時にバージョンが記載されている home.stateVersion = "<home-manager のバージョン>"; home.packages = [ # 使用したいパッケージをここに記載する。 # 以下は vim と kubectl をインストールしている例 pkgs.vim pkgs.kubectl ]; home.file = { # ~/.ssh/config の内容をここに記載している。 # ここに `<ファイルのパス>.text = "<ファイルの内容>"` のように記載しておくと `<ファイルの内容>` を持つ # ファイルが `<ファイルのパス>` で指定したパスに作成される。 # ただし、実体は作成されるファイルはシンボリックリンクになっており、実際は nix によって管理される # readonly なファイルになる点に注意。 ".ssh/config".text = '' Host github HostName github.com User git ''; }; }
home-manager の制御は home-manager
コマンドを通して行います。
基本的には switch
サブコマンドを実行すれば環境が用意されます。
$ kubectl version kubectl: command not found # 環境を準備する $ home-manager switch $ kubectl version Client Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.9", GitCommit:"c1de2d70269039fe55efb98e737d9a29f9155246", GitTreeState:"clean", BuildDate:"2022-07-13T14:26:51Z", GoVersion:"go1.17.11", Compiler:"gc", Platform:"linux/amd64"} Server Version: version.Info{Major:"1", Minor:"23", GitVersion:"v1.23.9", GitCommit:"c1de2d70269039fe55efb98e737d9a29f9155246", GitTreeState:"clean", BuildDate:"2022-07-13T14:19:57Z", GoVersion:"go1.17.11", Compiler:"gc", Platform:"linux/amd64"} # ~/.ssh/config をみてみる $ cat ~/.ssh/config Host github HostName github.com User git
home-manager で switch
サブコマンドによって生成されるファイルはシンボリックリンクになっています。
実態は Nix が管理しているファイルになっています。
$ ls ~/.ssh/config lrwxr-xr-x 1 user usergroup 84 Dec 10 02:50 config -> /nix/store/pr31jgjnwnpmaf58w35jyrajilfzcail-home-manager-files/.ssh/config
~/.config/home-manager/home.nix
の home.file
に設定した内容を書き換えて swtich
サブコマンドを実行するとこの内容が書き変わります。
{ ... }: { home.file = { ".ssh/config".text = '' Host gitlab HostName gitlab.com User git ''; }; }
# dotfiles を再生成 $ home-manager switch trace: warning: optionsDocBook is deprecated since 23.11 and will be removed in 24.05 trace: warning: optionsDocBook is deprecated since 23.11 and will be removed in 24.05 trace: warning: optionsDocBook is deprecated since 23.11 and will be removed in 24.05 /nix/store/7871wampdlaqva7mpa616gkmjn4c38qj-home-manager-generation Starting Home Manager activation Activating checkFilesChanged Activating checkLaunchAgents Activating checkLinkTargets Activating writeBoundary Activating linkGeneration Cleaning up orphan links from /home/user No change so reusing latest profile generation 139 Creating home file links in /home/user Activating batCache No themes were found in '/home/user/.config/bat/themes', using the default set No syntaxes were found in '/home/user/.config/bat/syntaxes', using the default set. Writing theme set to /home/user/.cache/bat/themes.bin ... okay Writing syntax set to /home/user/.cache/bat/syntaxes.bin ... okay Writing metadata to folder /home/user/.cache/bat ... okay Activating copyFonts Activating installPackages replacing old 'home-manager-path' installing 'home-manager-path' Activating onFilesChange Activating setupLaunchAgents There are 75 unread and relevant news items. Read them by running the command "home-manager news". # ~/.ssh/config を見てみる $ cat ~/.ssh/config Host gitlab HostName gitlab.com User git
そのため、 Nix の環境さえ用意できれば dotfiles を IaC で管理できます。
書き換えたければ ~/.config/home-manager/home.nix
を書き換え、 home-manager switch
を実行すれば環境がそのまま用意されます。
その他にも ~/.config/home-manager/home.nix
にある home.packages
に必要なパッケージを記載すればパッケージのインストールもしてもらえます。
もしインストールされたものを削除したい場合は nix-gabage-collect
コマンドを実行すると不必要な環境が Nix から綺麗さっぱり削除されます。
# インストールされたコマンドは Nix の環境にインストールされ、 ~/.nix-profile/bin に # シンボリックリンクがはられます。 # インストールされたパッケージや依存物は ~/.config/home-manager/home.nix の home.packages を書き換えただけだと # Nix の環境にあるキャッシュは削除されません。 # このコマンドは home-manager の機能というよりは Nix が提供する Nix 環境の GC を実施するコマンドです nix-gabage-collect
また、 Nix の導入が済んでしまえば一時的にパッケージを利用する場合は nix-shell
コマンドを利用すると一時的にパッケージをインストールして利用できます。
# Node.js と pnpm を一時的にインストールした環境に入る $ nix-shell -p nodejs nodePckages.pnpm these 6 derivations will be built: /nix/store/0i23yycgl4g3ysh919zrhqw6s7mv0clq-reconstructpackagelock.js.drv /nix/store/10sbwpqks6vpixp3najqz2zr6bfbz2av-linkbins.js.drv /nix/store/cay26jcn3vgnhsimhrpbqjmv39w5syg7-addintegrityfields.js.drv /nix/store/dxwm67zm4j9rlprgy7qj7j10iywbharh-install-package.drv /nix/store/vgc54qkgm1h9haaw95gkpahzss8hb1n3-pinpointDependencies.js.drv /nix/store/win93b0d7agghd066d888bjbh5brrri0-pnpm-8.10.2.drv these 5 paths will be fetched (46.54 MiB download, 408.80 MiB unpacked): /nix/store/1ipdb6jghbj1zbnxwydghq8gkgf6v3ca-cctools-llvm-11.1.0-973.0.1-dev /nix/store/gf6d8v8qj8rbpnyydyrv2m0vjcfw3bh6-cctools-port-973.0.1-dev /nix/store/8sgv1kbxwx641j6v4sv63zh5p01w473k-node-sources /nix/store/x551k64la7wmg4xj07yxfdd60wq2rd4y-pnpm-8.10.2.tgz /nix/store/4ag0qy6w79b67xslwgi26ax3i91a19zc-tarWrapper copying path '/nix/store/8sgv1kbxwx641j6v4sv63zh5p01w473k-node-sources' from 'https://cache.nixos.org'... copying path '/nix/store/x551k64la7wmg4xj07yxfdd60wq2rd4y-pnpm-8.10.2.tgz' from 'https://cache.nixos.org'... copying path '/nix/store/4ag0qy6w79b67xslwgi26ax3i91a19zc-tarWrapper' from 'https://cache.nixos.org'... building '/nix/store/cay26jcn3vgnhsimhrpbqjmv39w5syg7-addintegrityfields.js.drv'... building '/nix/store/dxwm67zm4j9rlprgy7qj7j10iywbharh-install-package.drv'... building '/nix/store/10sbwpqks6vpixp3najqz2zr6bfbz2av-linkbins.js.drv'... building '/nix/store/vgc54qkgm1h9haaw95gkpahzss8hb1n3-pinpointDependencies.js.drv'... building '/nix/store/0i23yycgl4g3ysh919zrhqw6s7mv0clq-reconstructpackagelock.js.drv'... copying path '/nix/store/gf6d8v8qj8rbpnyydyrv2m0vjcfw3bh6-cctools-port-973.0.1-dev' from 'https://cache.nixos.org'... copying path '/nix/store/1ipdb6jghbj1zbnxwydghq8gkgf6v3ca-cctools-llvm-11.1.0-973.0.1-dev' from 'https://cache.nixos.org'... building '/nix/store/win93b0d7agghd066d888bjbh5brrri0-pnpm-8.10.2.drv'... unpacking sources patching sources updateAutotoolsGnuConfigScriptsPhase configuring no configure script, doing nothing building installing unpacking source archive /nix/store/x551k64la7wmg4xj07yxfdd60wq2rd4y-pnpm-8.10.2.tgz pinpointing versions of dependencies... patching script interpreter paths in . ./pnpm/bin/pnpx.cjs: interpreter directive changed from "#!/usr/bin/env node" to "/nix/store/fwgfw6i5q1hv49bgfl96bmzv72l98khy-nodejs-18.18.2/bin/node" ./pnpm/bin/pnpm.cjs: interpreter directive changed from "#!/usr/bin/env node" to "/nix/store/fwgfw6i5q1hv49bgfl96bmzv72l98khy-nodejs-18.18.2/bin/node" ./pnpm/dist/node_modules/node-gyp/bin/node-gyp.js: interpreter directive changed from "#!/usr/bin/env node" to "/nix/store/fwgfw6i5q1hv49bgfl96bmzv72l98khy-nodejs-18.18.2/bin/node" ./pnpm/dist/node-gyp-bin/node-gyp: interpreter directive changed from "#!/usr/bin/env sh" to "/nix/store/zzpm4317hn2y29rm46krsasaww9wxb1k-bash-5.2-p15/bin/sh" No package-lock.json file found, reconstructing... npm WARN config production Use `--omit=dev` instead. rebuilt dependencies successfully npm WARN config production Use `--omit=dev` instead. up to date, audited 1 package in 183ms found 0 vulnerabilities linking bin 'pnpm' linking bin 'pnpx' post-installation fixup checking for references to /private/tmp/nix-build-pnpm-8.10.2.drv-0/ in /nix/store/kyprrxixg263c82lzy9gmfpj97kkl32p-pnpm-8.10.2... patching script interpreter paths in /nix/store/kyprrxixg263c82lzy9gmfpj97kkl32p-pnpm-8.10.2 rewriting symlink /nix/store/kyprrxixg263c82lzy9gmfpj97kkl32p-pnpm-8.10.2/bin to be relative to /nix/store/kyprrxixg263c82lzy9gmfpj97kkl32p-pnpm-8.10.2 [nix-shell:~]$ pnpm --version 8.10.2 [nix-shell:~]$ node --version v18.18.2
また、 shell.nix
というものをディレクトリに用意しておけば、 nix-shell
コマンドだけ実行すればそのまま一時的な環境に入ることもできます。
$ ls shell.nix $ cat shell.nix { pkgs ? import <nixpkgs> {}, ... }: pkgs.mkShell { nativeBuildInputs = [ pkgs.nodejs pkgs.pnpm ]; } $ nix-shell [nix-shell:~/Test/Directory]$ pnpm --version 8.10.2 [nix-shell:~/Test/Directory]$ node --version v18.18.2
Nix が提供するパッケージを探す場合は NixOS Search を利用します。
こちらに必要なパッケージ名をサーチボックスに入れて Search
ボタンをクリックするとパッケージが提供されているかを検索してくれます。
パッケージを更新する場合は nix-channel
の --update
オプションを利用し、 home-manager
の switch
コマンドを利用します。
$ nix-channel --update this derivation will be built: /nix/store/ikk0zi83gc4x263imdgdx7d721wh33il-darwin.drv building '/nix/store/ikk0zi83gc4x263imdgdx7d721wh33il-darwin.drv'... this derivation will be built: /nix/store/csfs0vgc39fwjsz6bx22a9iyx5zx3gwh-home-manager.drv building '/nix/store/csfs0vgc39fwjsz6bx22a9iyx5zx3gwh-home-manager.drv'... unpacking channels... $ home-manager switch # ここからは長い出力がある
まとめ
ここまでで Nix + home-manager で dotfiles を管理する方法をここまでに記載してきました。 Nix は慣れるまで少し扱いが難しく、また日本語の情報は少ないので扱いにこなれるまで時間はかかります。 しかし、導入が済んでしまえば dotfiles を用意しつつしかも環境を綺麗に保ちながらパッケージの導入ができます。 是非とも今回の記事を活用してよりよい dotfiles 環境を構築してみてください。
もし例が気になる場合は自分が環境をつくってリポジトリに公開している ので、そちらを確認してください。
次回の NTT Communications Advent Calendar 2023 の記事もお楽しみに。