これまでdotfilesはシェルスクリプトでプリミティブに記述する方針で管理していたが、周囲でNixが良いという声を聞くことが多くなり、home-manager & nix-darwin を利用するよう移行してみた。
https://github.com/ntsk/dotfiles
管理対象としては、zsh, nvim, git, tmux, tig などのコアツールの設定を上記のrepositoryで管理している。
自身の開発環境として、仕事ではmacOSを利用しているがプライベートではmacOSとArch Linuxを併用している。
Homebrewの場合にはBrewfile、Pacmanでは pacman -Qqe の出力を Pacfile として管理して sudo pacman -S --needed $(comm -12 <(pacman -Slq | sort) <(sort $HOME/dotfiles/Pacfile)) でインストールするなどして使い分けていた。
そのほかにも結構色々な箇所で if [[ "$(uname -s)" == "Darwin" ]]; then を書いていてカオスになってきたのもあったので、この機会に整理したかったのがモチベーションとしてあった。
NixとFlakes
Nixは純粋関数型のパッケージマネージャ。パッケージを関数型プログラミングにおける値のように扱い、各パッケージは依存グラフのハッシュによってユニークに扱われる。すべてのパッケージは /nix/store 配下に隔離されたディレクトリに格納されるため、異なるバージョンが干渉することなく共存できる。
また、再現性を高める仕組みとしてFlakesがある。Flakesは flake.nix をプロジェクトの単一のエントリーポイントとし、flake.lock で依存関係のバージョンを固定する。また、評価時に環境変数などへのアクセスを制限することで、より純粋な評価を実現しているとのこと。
これによってdotfilesで設定されている環境と全く同じバージョンの環境をすぐに再現できる。
home-manager
https://github.com/nix-community/home-manager
home-managerはNixを利用してユーザー環境を管理するためのツール。ホームディレクトリ配下のdotfilesやユーザーレベルのパッケージを宣言的に管理できる。
以下のように記述することで、パッケージのインストールとdotfilesの配置を一括で行える。
{ pkgs, ... }:
{
home.packages = with pkgs; [
ripgrep
fd
gh
ghq
jq
tig
tmux
];
programs.git = {
enable = true;
lfs.enable = true;
userName = "ntsk";
userEmail = "ntsk@ntsk.jp";
};
programs.fzf = {
enable = true;
enableZshIntegration = true;
};
programs.zsh = {
enable = true;
autosuggestion.enable = true;
syntaxHighlighting.enable = true;
};
programs.neovim = {
enable = true;
defaultEditor = true;
viAlias = true;
vimAlias = true;
};
}home.packages でインストールするパッケージを列挙し、programs で各プログラムの設定を行う。これまでHomebrewで行っていたパッケージ管理と、シンボリックリンクで行っていたdotfilesの配置を、Nixの設定ファイル一つで宣言的に管理できる。
home.fileとxdg.configFile
既存の設定ファイルをそのまま利用する場合は、home.file や xdg.configFile を使ってシンボリックリンクを作成できる。
home.file はホームディレクトリ直下にファイルを配置する。.zshrc や .gitconfig など、ホームディレクトリに置く設定ファイルに使用する。
{
home.file = {
".tigrc".source = ./.tigrc;
".editorconfig".source = ./.editorconfig;
};
}xdg.configFile は ~/.config 配下にファイルを配置する。XDG Base Directory仕様に従うアプリケーションの設定ファイルに使用する。
{
xdg.configFile = {
"wezterm".source = ./wezterm;
"nvim".source = ./nvim;
};
}どちらも .source で参照先のファイルやディレクトリを指定すると、Nix Store経由でシンボリックリンクが作成される。また、.text を使うことでファイルの内容を直接記述することもできる。
{
home.file.".gemrc".text = ''
gem: --no-document
'';
}programs オプション
programs オプションを利用することで、設定をNixコードとして直接記述できる。
programs はhome-managerが提供するアプリケーション固有の設定モジュール。programs.<name>.enable = true でアプリケーションを有効にすると、パッケージのインストールと設定ファイルの生成が行われる。各アプリケーションが持つ設定項目は、Nixのオプションとして型付けされており、ビルド時に値の検証が行われる。
例えば、zshの履歴設定は以下のように記述できる。
{
programs.zsh = {
enable = true;
history = {
path = "$HOME/.zsh_history";
size = 1000;
save = 100000;
ignoreDups = true;
extended = true;
};
};
}型チェックがビルド時に行われるので、誤った値を入力された際にビルドエラーにできる。
また、エイリアスや関数も shellAliases や initExtra で定義できる。
{
programs.zsh = {
shellAliases = {
repos = "cd $(ghq root)/$(ghq list | fzf)";
glog = "git log --graph --decorate --oneline";
};
initExtra = ''
function gco() {
git checkout $(git branch -a | fzf | sed 's/remotes\/origin\///')
}
'';
};
}shellAliases は文字列から文字列へのマッピング、initExtra は .zshrc に追記する文字列を渡すことができる。
nix-darwin
https://github.com/nix-darwin/nix-darwin
nix-darwinはmacOS向けのNix設定を管理するためのツール。home-managerがユーザー環境を管理するのに対し、nix-darwinはシステムレベルの設定を管理する。
Dockの設定やキーボードショートカット、Finderの設定などをNixで宣言的に記述できる。
{ username, ... }:
{
system.defaults = {
dock = {
autohide = true;
tilesize = 44;
show-recents = false;
};
finder = {
AppleShowAllExtensions = true;
AppleShowAllFiles = true;
ShowPathbar = true;
ShowStatusBar = true;
};
NSGlobalDomain = {
AppleInterfaceStyle = "Dark";
KeyRepeat = 2;
InitialKeyRepeat = 15;
NSAutomaticCapitalizationEnabled = false;
NSAutomaticSpellingCorrectionEnabled = false;
};
};
launchd.user.agents.keyboard-remap = {
serviceConfig = {
ProgramArguments = [
"/usr/bin/hidutil"
"property"
"--set"
''{"UserKeyMapping":[{"HIDKeyboardModifierMappingSrc":0x700000039,"HIDKeyboardModifierMappingDst":0x7000000E0}]}''
];
RunAtLoad = true;
};
};
}Dockの自動非表示、Finderでの隠しファイル表示、キーリピートの速度、CapsLockからCtrlへのリマップなど、新しいmacOSをセットアップする際に毎回システム環境設定から変更していた項目をコードとして管理できる。 できる限りソフトを入れない派閥なので、Karabiner-Elementsなどを使用せずに一発で設定を反映できるのは嬉しい。
macOSではnix-darwinとhome-managerを組み合わせて使用し、Linuxではhome-managerのみを使用するようにしている。
インストールスクリプト
新しいマシンで環境を構築する際は、install.sh を実行する。
curl -fsSL https://raw.githubusercontent.com/ntsk/dotfiles/main/bin/install.sh | bashこのスクリプトは以下の処理を順番に実行する。
- dotfilesリポジトリを
~/dotfilesにクローン - Nixのインストール
- 設定の適用(macOSではnix-darwin、Linuxではhome-manager)
Determinate Nix Installer
Nixのインストールには Determinate Nix Installer を使用している。
Determinate Nixは、Determinate Systemが提供しているNixのダウンストリームディストリビューションで、公式のNixと互換性を保ちつつ、追加機能や改善が行われている。
公式のNixではFlakesやnix-commandが長期にわたってexperimentalなままなのだが、Determinate Nixではこれらを安定版として提供している。公式インストーラではFlakesを使用するために ~/.config/nix/nix.conf で experimental-features = nix-command flakes を設定する必要があるが、Determinate Nixではこの設定が不要となる。
設定の適用
設定の適用は、macOSでは darwin-rebuild switch、Linuxでは home-manager switch が実行される。
case "$(uname -s)" in
Darwin)
sudo darwin-rebuild switch --flake "$DOTFILES_DIR/nix#$(get_nix_system)" --impure
;;
*)
home-manager switch --flake "$DOTFILES_DIR/nix#$(get_nix_system)" --impure
;;
esac結局分岐してるじゃないかと思うかもしれないが、macOSで home-manager switch を実行しても問題ない。
この場合はシステム設定は反映されずユーザー設定だけが反映される。自身の環境では、nix-darwinのモジュールとしてhome-managerを統合しているので、darwin-rebuild switch を実行すると内部的にhome-managerも有効化される。ユーザー設定とシステム設定を分けることもできるが、コマンド一発で常に開発環境とシステム設定が同じ状態で再現されて欲しいのでこのようにしている。
home-manager switch は設定のビルドと有効化を一度に行うコマンドで、ビルド時に設定に誤りがないか評価され、問題がなければ構築された設定をユーザー環境に反映する。
設定を変更した後も、再度 switch を実行することで同様の状態に戻すことができる。アトミックなので途中で失敗しても以前の状態が維持される。
段階的な移行
導入初期はhome-managerでパッケージのインストール + home.fileとxdg.configFileを利用してひたすら既存のシンボリックリンクを貼るだけの設定を書いていた。既存のdotfilesから移行する場合などは、それがシンプルで移行しやすいと思う。
しかし一度移行するとあらゆるものをNixで書きたくなってくる。dotfilesのNixの割合を増やすのが楽しくなってくるので、そういったモチベーションが出てきた時にprograms などに設定を移行したり、nix-darwinを利用したりすると良いと思う。
現状の自身のパッケージマネージャの棲み分けとして、Homebrewを禁止したということはなく環境構築時にプリインストールして欲しい必須のツールはNix、それ以外のスポットで入れてみたりというようなツールはHomebrewなどOS毎のパッケージマネージャ、言語のランタイムの切り替えはmiseという使い分けになっている。 社内のプロジェクトでチームにNixを強要しようとも思ってないので、このくらいの緩さでやっているが信仰が強くなったらそのうち移行したくなるかもしれない。
CI
CIでビルドとインストールのテストや、定期的なパッケージのアップデートを自動化している。
ビルドとインストールのテスト
macOSとLinuxの両環境でビルドとインストールのテストを実行している。
jobs:
nix-build:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v6
- uses: cachix/install-nix-action@v31
- run: |
SYSTEM=$(uname -m)-$(uname -s | tr '[:upper:]' '[:lower:]')
nix build "./nix#homeConfigurations.${SYSTEM}.activationPackage" --impure
install-test:
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest, ubuntu-latest]
steps:
- uses: actions/checkout@v6
with:
path: dotfiles
- run: mv dotfiles $HOME/dotfiles
- run: $HOME/dotfiles/bin/install.sh定期的なflake.lockの更新
update-flake-lock を利用することで、nix flake update の実行とPRの作成を自動化している。
name: Update flake.lock
on:
schedule:
- cron: '0 1 * * *'
jobs:
update:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: DeterminateSystems/nix-installer-action@v21
- uses: DeterminateSystems/update-flake-lock@v28
with:
path-to-flake-dir: nix/
pr-title: "Update flake.lock"
pr-labels: dependenciesパッケージ差分の可視化
flake.lock が更新ではどのパッケージがどのバージョンになったのか差分がパッと分からなかったので、nvd を利用してパッケージの差分をコメントするようにした。
name: Flake Diff
on:
pull_request:
paths:
- 'nix/flake.lock'
jobs:
diff:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- uses: actions/checkout@v6
with:
ref: ${{ github.base_ref }}
path: base
- uses: cachix/install-nix-action@v31
- run: nix build ./base/nix#homeConfigurations.x86_64-linux.activationPackage --impure -o base-profile
- run: nix build ./nix#homeConfigurations.x86_64-linux.activationPackage --impure -o pr-profile
- run: nix run 'nixpkgs#nvd' -- diff ./base-profile ./pr-profile > diff.txt
# PRにコメント感想
Nixは学習コストが高いと言われがちだが、Claude Codeと一緒に作業したら学びながらサクサクできて良かった。
再現性の高い環境を一発で構築できるというのも嬉しい。 デザイナーなど非エンジニアのメンバーが同じrepositoryを操作する必要のあるプロジェクトなどでは、個人の環境に依存した環境構築のトラブルなどが起こりづらく良いと思う。