Nix と home-manager で宣言的に環境を管理する


Nix パッケージマネージャーを入れたら、絶対に入れておきたいのが home-manager です。
home-manager は nix を用いてユーザー環境を宣言的に管理するためのツールで、

- インストールするパッケージ
- `~/.profile`, `~/.bashrc`, `~/config/fish`などの設定ファイル
- fish やら tmux やら firefox やらのプラグイン
- email のアカウント

など、様々なユーザー設定を一枚のファイルで宣言的に管理できるようにしてくれる優れものです。

nix と home-manager だけいれておけば、PC いれかえてもコマンド一発でセットアップ完了も夢ではありません。
(実際仕事で PC 入れ替えるときにそれをしました。)

詳しいことは[home-manager の github ページ](https://github.com/nix-community/home-manager)とか[マニュアル](https://rycee.gitlab.io/home-manager/)を参照すれば分かるのですが、英語でしかも長いので、自分用に要点だけまとめようと思います。

## home-manager のインストール

home-manager を使うには Nix パッケージマネージャーが必要なので、まずは Nix をインストールします。

```terminal
> curl -L https://nixos.org/nix/install | sh
> nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs
> nix-channel --update
```

`nixchannel --add <url> [channel-alias]` でチャンネルの登録ができます。リポジトリの登録みたいなものです。

home-manager のインストールも、まずは home-manager のチャンネルの登録から行います。

```terminal
> nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager
> nix-channel --update
```

そして以下のコマンドを打つことでインストールできます。

```terminal
> nix-shell '<home-manager>' -A install
```

## パッケージ管理

home-manager を使ってユーザー環境を設定するには、`~/.config/nixpkgs/home.nix`を作成します。

例えば、以下の設定は、git と GNU/hello をインストールします。

```nix:.config/nixpkgs/home.nix
{ pkgs, ... }:
{
    # ここは home-manager をインストールしたときに自動で入る
    programs.home-manager.enable = true;
    home.username = "hnakano";
    home.stateVersion = "20.09";

    # ここでインストールするパッケージを宣言する
    home.packages = [
        pkgs.git
        pkgs.hello
    ];
}
```

それで設定を反映するためには以下のコマンドを打ちます。

```terminal
> home-manager switch
```

この設定ファイルは、Nix expression と呼ばれる独自の言語で書かれています。
正直ほとんど json みたいなものなので、あまり文法は気にしなくていいのですが、気になる人のために最低限の文法を解説します。

### home-manager を使うための最低限の文法

#### repl
Nix expression の repl があります。コマンドラインで `nix repl` と入力すれば repl に入れます。

```terminal
> nix repl
Welcome to Nix version 2.3.7. Type :? for help.

nix-repl>
```

`:?` と入力すれば repl コマンドのヘルプが表示されます。 `:q` で repl から抜けられます。

#### 基本の型
数値、ブール、文字列の型があります。
文字列の結合は`+`です。

```shell
nix-repl> 1 #interger
1

nix-repl> 3.14 #float
3.14

nix-repl> true #boolean
true

nix-repl> "hogehoge" #string
"hogehoge"

nix-repl> "hoge" + "fuga" #string concatenate
"hogefuga"
```

#### `let ... in`

変数への値の代入は `let ... in` を使います。
セミコロンの位置に気をつけてください。

```shell
nix-repl> let x = 1; y = 2; z = x + y; in z * 2
6
```

repl の中でだけ、`let ... in` を使わずに値の代入ができます。
しかし、通常の Nix expression ではこのような書き方はできないので気をつけてください。

```shell
nix-repl> x = 3

nix-repl> y = 4

nix-repl> x + y
7
```

#### List

リストは`[ a b c ]` です。要素の区切りは半角スペースか改行です。
ちなみに型は一致させる必要が**ありません**

```shell
nix-repl> [ 1 "hoge" true ]
[ 1 "hoge" true ]

nix-repl> [
            1
            2
            3
          ]
[ 1 2 3 ]
```

リストの要素にアクセスするには`builtins.elemAt` というビルトイン関数を使います。関数適用にかっこは不要です。

```shell
nix-repl> x = [ 1 2 3 ]

nix-repl> builtins.elemAt x 0
1

nix-repl> builtins.elemAt x 1
2
```

#### 関数

Nix の関数は `arg: body` という形です。コロンの左側が引数になります。
2 引数関数は`arg1: arg2: body`です。

```shell
nix-repl> hello =  x: "hello, " + x  

nix-repl> hello "world"
"hello, world"
```

基本的に Nix では無名関数しか作れません。

```shell
nix-repl> x: y: x + y
«lambda @ (string):1:1»
```

なので、関数に名前を付けたければ、`let` 式を使います

```shell
nix-repl> let
            add = x: y: x + y;
            add2 = add 2; #部分適用ができる
          in
          add2 3 #add 2 3 に同じ
5
```

#### Attribute set

`{ key1 = value1; key2 = value2; }` という形の、中括弧で key = value のペアを囲んだものを Attribute set といいます。
`key = value;` のペアのお尻に必ずセミコロンが必要です。

`x.key1` という形で値にアクセスできます。

```shell
nix-repl> let
            x = {     
              hoge = "hoge";
              fuga = "fuga";
            };
          in x.hoge
"hoge"
```

##### attribute set を引数に取る関数

attribute set を引数にとる関数は、普通に `arg: body` の形で定義できます。

```shell
nix-repl> let
            f = x: x.hoge + x.fuga;
          in f { hoge = "hoge"; fuga = "fuga"; }
"hogefuga"
```

しかし、その代わりに `{ key1, key2 }: body` という形でも定義できます。
この形だと、引数の key の名前が厳密に一致しないとエラーになります。
`{key, key2, ... }: body` のように、引数に`...`を入れるとこのエラーを避けられます。

```shell
nix-repl> f = { hoge, fuga }: hoge + fuga

nix-repl> let x = { hoge = "hoge"; fuga = "fuga"; }; in f x
"hogefuga"

nix-repl> let y = { hoge = 3; fuga = 4; piyo = true; }; in f y
error: anonymous function at (string):1:2 called with unexpected argument 'piyo', at (string):1:50

nix-repl> g = { hoge, ... }: "hello, " + hoge

nix-repl> let y = { hoge = "world"; fuga = 4; piyo = true; }; in g y
"hello, world"
```

#### その他

Nix Expression の文法に関する参考

- 公式のマニュアル https://nixos.org/manual/nix/stable/#chap-writing-nix-expressions 
- Nix Pills(公式ブログ) https://nixos.org/guides/nix-pills/basics-of-language.html

### 設定ファイルを読む

改めて最初に定義した設定ファイルを読んでみます。

```nix:./config/nixpkgs/home.nix
{ pkgs, ... }:
{
    # ここは home-manager をインストールしたときに自動で入る
    programs.home-manager.enable = true;
    home.username = "hnakano";
    home.stateVersion = "20.09";

    # ここでパッケージを宣言する
    home.packages = [
        pkgs.git
        pkgs.hello
    ];
}
```

まず、この式は`pkgs`というキーをもつ Attribute set を引数にとる関数であるということが分かります。
`pkgs` には通常 `<nixpkgs>` つまり、Nix のパッケージコレクションが入ります。

`{ key1.key2 = value; }` というのはネストした attribute set の構文糖で、`{ key1 = { key2 = value; }; }`を表しています。

構文糖を使わなければ、この設定ファイルは以下のようになります。

```nix
{ pkgs, ... }:
{
    programs = {
        home-manager = {
            enable = true;
        };
    };

    home = {
        username = "hnakano";
        stateVersion = "20.09";
        packages = [
            pkgs.git;
            pkgs.hello;
        ];
    };
}
```

即ち、この関数は `{ pkgs = ..; ... }`という Attribute set を引数にとり、
`{ programs = { ... }; home = { ... }; }` という Attribute set を返す関数ということです。

`home-manager`コマンドは、この返り値を評価して設定をユーザー環境に反映します。

## 他の設定項目

home-manager を使って色々設定してみます。
home-manager で設定可能な内容は[home-manager のマニュアル](https://rycee.gitlab.io/home-manager/options.html)を参照してください。

```nix:.config/nixpkgs/home.nix
{
  home.packages = with pkgs; [ nixfmt exa bat source-han-code-jp ];

  programs.git = {
    enable = true;
    userName = "hnakano863";
    userEmail = "myEmail@gmail.com";
    extraConfig.pull.rebase = false;
  };

  fonts.fontconfig.enable = true;

  programs.bash = {
    enable = true;
    profileExtra = ''
      export XDG_DATA_DIRS=$HOME/.nix-profile/share''${XDG_DATA_DIRS:+:}$XDG_DATA_DIRS
      export LIBGL_ALWAYS_INDIRECT=1
      export DISPLAY=$(cat /etc/resolv.conf | grep nameserver | awk '{print $2}'):0
      if [ -e $HOME/.nix-profile/etc/profile.d/nix.sh ]; then
          . $HOME/.nix-profile/etc/profile.d/nix.sh;
      fi
    '';
    initExtra = ''
      if [ -z $IN_NIX_SHELL ]; then exec fish fi
    '';
    shellAliases = {
      ls = "exa";
      cat = "bat";
    };
  };

  programs.fish = {
    enable = true;
    interactiveShellInit = ''
      set -gx fish_user_paths $HOME/go/bin $HOME/.local/bin
    '';
  };
      
  programs.tmux = {
    enable = true;
    clock24 = true;
    keyMode = "vi";
    shortcut = "a";
    terminal = "screen-256color";
  };
}
```

若干説明していない文法事項があるので、説明します。

まず、`home.packages`のところで`with pkgs; [...]` という形があります。
これは、いちいち `[ pkgs.nixfmt pkgs.exa ... ]` などと`pkgs.` を書くことを避けるための構文です。

次に、`programs.bash.profileExtra`などで出て来るシングルクォート 2 つで囲まれている文字列ですが、
こちらは indented string というタイプの文字列です。

indented string の中ではインデントが無視されるので、シェルスクリプトを書くのに便利です。
実際、`programs.bash.profileExtra`は`~/.profile`に反映されます。

最後に、`programs.bash.enable = true;` にしている場合、`home-manager switch` を実行すると、`~/.profile`や`~/.bashrc`などを置き換えようとします。
このとき、ユーザー環境にすでに`~/.bashrc`などの既存の設定ファイルが存在する場合、`home-manager`コマンドは「設定ファイルあるんだが」とエラーを吐いて失敗してしまいます。
これを避けるために、事前に`mv ~/.bashrc ~/.bashrc.bk` などして既存のファイルを退避させておきましょう。