バージョン管理された人

subversionで管理されてます

【NTT Communications Advent Calendar 2023】Nix と home-manager で dotfiles を管理する話

この記事は、 NTT Communications Advent Calendar 2023 9日目の記事です。

みなさんは dotfiles をどのように管理されているでしょうか? Bash や Zsh といったスクリプトによってインストールを管理したり、 git でホームディレクトリを管理する、 ChefAnsible のような構成管理ツールを用いて管理する方法があると思います。 自分もシェルスクリプトによる管理をおこなったり、 Ansible 等の構成管理ツールの利用や 自前の構成管理ツール をつくって管理していましたが、現在は Nixhome-manager というものを使って管理しています。 今回はこの Nix と home-manager による dotfiles の管理をどのように行えばよいのか、それらによって管理する Pros/Cons にはどのようなものがあるのかについて解説します。

Nix

まずそもそも Nix とはどういったものなのかについて解説します。

Nix とは NixOS という Linux ディストリビューションに搭載された aptyum/dnf のようなパッケージ管理システムです。 Nix はドキュメントにも記載してありますが、 純粋関数型パッケージマネージャー です。

Nix is a purely functional package manager.

Introduction - Nix Reference Manual

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 ManualNix 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.nixhome.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 ボタンをクリックするとパッケージが提供されているかを検索してくれます。

NixOS Search で nodejs パッケージを探している

NixOS Search における nodejs の検索結果

パッケージを更新する場合は nix-channel--update オプションを利用し、 home-managerswitch コマンドを利用します。

$ 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 の記事もお楽しみに。