Arch LinuxからNixOSへ雑に乗り換える

農工大アドカレ13日目！折り返しに入りました！

:::note
この記事は Arch → NixOS へのハードルを下げることを目的としています。dotfiles盆栽が趣味でないような人にはオススメできないかも?
:::

## NixOS に乗り換えたきっかけ

半年ほど前のある日、メインPCが崩壊(物理)。新しいPCでまた _Arch Install Battle_ するのも飽きてきたので、話題のNixOSに挑戦してみることにしました。

ちまた(Twitter)でNixOSの話をよく見かけて興味もあったので、ちょうど良いタイミングでしたね。

## NixOS乗換案内

まずはこちらの記事を参考にしつつ、手を動かしてみました。

この記事に詳しく書いてありますが、Nixはパッケージマネージャであると同時に言語でもあります。Nix言語で必要な設定を記述することで、環境構築を完全にコード化できるのが特徴の一つです。

https://zenn.dev/asa1984/articles/nixos-is-the-best

この記事の通り、NixOSをインストールし、`home-manager`を導入すると、新しい`dotfiles/`は以下のような感じになりました。

```
├── configuration.nix
├── hardware-configuration.nix
├── flake.lock
├── flake.nix
├── home-manager
│   ├── apps.nix
│   ├── browser.nix
│   ├── default.nix
│   ├── desktop.nix
│   ├── dev.nix
│   ├── git.nix
│   └── zsh.nix
└── README.md
```

シェルやターミナルなどの各種設定は `dotfiles/home-manager/` 下に書いていきます。例えば`zsh`であれば以下の様になります。`.zshrc` に自由に設定を書くかわりに、`home-manager`に用意されているオプションを用いて設定することができます。

```nix: home-manager/zsh.nix
{ pkgs, config, ... }: {
  programs.zsh = {
    enable = true;
    autocd = true;
    enableCompletion = true;
    autosuggestion.enable = true;
    defaultKeymap = "emacs";
    syntaxHighlighting.enable = true;
    historySubstringSearch.enable = true;
    shellAliases = {
      g = "git";
      ls = "ls --color=auto";
      ll = "ls -l";
      la = "ls -a";
      l = "ls";
    };
    history = {
      size = 10000;
      path = "${config.xdg.dataHome}/zsh/history";
    };
  };
  programs.starship.enable = true;
}
```

設定項目にはエディタ補完も効きますし、良い感じに環境設定を記述できます。

## 今まで育ててきたdotfilesは？

ところが、そろそろこんな疑問を感じることでしょう。

**「Neovim とか Hyprland とかの設定ファイル凄い量あるんだけど、、、全部これやるの？」**

**「`home-manager`に設定項目がないツールを使っているんだけど、、、」**

例えば僕のNeovimの設定はLua言語で1000行以上ありますので、Lua版とNix版の両方の設定をメンテナンスしつづけるのは大変すぎます。そもそも、この先ずっとNixのある環境しか使わないとは考えづらいので、どうにか今まで育ててきた`dotfiles`を活かしたいところです。

## 雑にシンボリックリンクを張りまくろう

解決策です。

新しい`dotfiles/`の中に今までの`dotfiles`を`home`という名前でクローンしてきます。

```
dotfiles
|- home/ # これまでのdotfiles
|- home-manager/ # home-manager の設定
|- configuration.nix # OSの設定
|- hardware-configuratioin.nix
|- flake.nix
-  flake.lock
```

次に、`home-manager`で必要なパッケージをインストールするよう記述します。Hyprlandだとこんな感じ。

```nix
# インストールするパッケージ
home.packages = with pkgs; [
  # Hyprland =========================
  hyprland
  hyprpaper
  hypridle
  hyprlock
  hyprpicker
  # Hyprland Utilities ===============
  swaynotificationcenter
  networkmanagerapplet
  grim
  slurp
  # terminal =========================
  kitty
  foot
];
```

次に、そのパッケージの本来の設定ファイルの位置に`home/`からシンボリックリンクを張るよう記述します。

```nix
# ディレクトリごとシンボリックリンクを張る
home.file.".config/hypr" = {
  source = ../home/.config/hypr;
  recursive = true;
};
```
宣言的にシンボリックリンクを作れるの、最高! 

Hyprland以外も続々とシンボリックリンクを張っていきます。

```nix
# ターミナル
home.file.".config/kitty" = {
  source = ../home/.config/kitty;
  recursive = true; # ディレクトリごと
};
# Neovim
home.file.".conig/nvim" = {
  source = ../home/.config/nvim;
  recursive = true;
};
# SKK
home.file.".config/nvim" = {
  source = ../home/.config/nvim;
  recursive = true;
};
# ideavimrc
home.file.".ideavimrc".source = ../home/.ideavimrc;
# などなど……
```

これで色々今まで通り動くようになりました！雑にNixOSに移行できてきていますね!

## トラブルシューティング: Neovimの言語サーバーが動かない

これまで、言語サーバーは[Mason.nvim](https://github.com/williamboman/mason.nvim)というプラグインで管理してきましたが、雑に言ってしまうと、NixOS環境では動かせません。しかし、私の言語サーバーの設定は Mason と密な関係にあるので、これを`Mason`非依存にする必要があります。

といっても、MasonのLSPハンドラーは [nvim-lspconfig](https://github.com/neovim/nvim-lspconfig) の実行パスとしてをMasonの管理下のパスを指定するだけですので、剥がすのはそこまで難しくありませんでした。

`home-manager`がある環境ではMasonを無効化し、`home-manager`が無い環境では動くように分岐を書いておきます。

```lua
return {
	"neovim/nvim-lspconfig",
	dependencies = {
		{ "folke/neoconf.nvim" },
		{
			"williamboman/mason.nvim",
			enabled = not vim.fn.executable("home-manager"),
			event = "VeryLazy",
		},
		{
			"williamboman/mason-lspconfig.nvim",
			enabled = not vim.fn.executable("home-manager"), -- home-manager がある場合のみ
			cmd = { "LspInstall", "LspUninstall" },
		},
		{
			"b0o/schemastore.nvim",
			ft = { "json", "yaml", "toml" },
		},
		{ "dmmulroy/ts-error-translator", ft = "typescript" },
	},
	opts = {
		format = { timeout_ms = 50000 },
	},
	config = function()
		local lspconfig = require("lspconfig")
		local server_list = {
			"astro",
			"bashls",
			"biome",
			"clangd",
			"cmake",
			"cssls",
			"cssls",
			"denols",
			"docker_compose_language_service",
			"dockerls",
			"efm",
			"eslint",
			"graphql",
			"html",
			"jsonls",
			"jsonls",
			"lemminx",
			"nil_ls",
			"lua_ls",
			"mdx_analyzer",
			"pyright",
			"pyright",
			"stylelint_lsp",
			"tailwindcss",
			"taplo",
			"texlab",
			"ts_ls",
			"matlab_ls",
			"tinymist",
			"vimls",
			"yamlls",
		}

		-------------------------------------
		-- Handlers for each language server
		-------------------------------------
		local setup_handler = function(server_name)
			if server_name == "efm" then
				return
			end

			local default_opts = {
				capabilities = vim.tbl_deep_extend(
					"force",
					vim.lsp.protocol.make_client_capabilities(),
					require("cmp_nvim_lsp").default_capabilities()
				),
			}
			local opts = {}
			if server_name == "denols" then
				-- INFO: Neccessary for avoiding conflict with other js severs
				opts = {
					root_dir = lspconfig.util.root_pattern("deno.json"),
					init_options = {
						lint = true,
						unstable = true,
						suggest = {
							imports = {
								hosts = {
									["https://deno.land"] = true,
									["https://cdn.nest.land"] = true,
									["https://crux.land"] = true,
								},
							},
						},
					},
				}
			elseif server_name == "eslint" then
				opts.on_attach = function(client, bufnr)
					vim.api.nvim_create_autocmd("BufWritePre", {
						buffer = bufnr,
						command = "EslintFixAll",
					})
				end
			elseif server_name == "stylelint_lsp" then
				opts.filetypes = { "css", "scss", "less", "sass" } -- exclude javascript and typescript
			elseif server_name == "jsonls" then
				opts.settings = {
					json = {
						schemas = require("schemastore").json.schemas(),
						validate = true,
					},
				}
			elseif server_name == "yamlls" then
				opts.settings = {
					yaml = {
						schemaStore = {
							enable = true,
							url = "",
						},
						schemas = require("schemastore").yaml.schemas(),
					},
				}
			elseif server_name == "tinymist" then
				opts.settings = {
					exportPdf = "onType",
					formatterMode = "typstyle",
				}
			end
			lspconfig[server_name].setup(vim.tbl_deep_extend("force", default_opts, opts))
		end

		-----------------------------------------------
		-- Setup ls with mason or without mason
		-----------------------------------------------
		if vim.fn.executable("home-manager") then
			for _, server in ipairs(server_list) do
				setup_handler(server)
			end
		else
			require("mason").setup({
				PATH = "append",
			})
			local mason_lsp = require("mason-lspconfig")
			mason_lsp.setup({
				ensure_installed = server_list,
			})
			mason_lsp.setup_handlers({ setup_handler })
		end

		local function on_list(options)
			vim.fn.setqflist({}, " ", options)
			vim.api.nvim_command("cfirst")
		end

		vim.lsp.buf.definition({ on_list = on_list })
		vim.lsp.buf.references(nil, { on_list = on_list })
		vim.diagnostic.config({
			virtual_text = {
				source = true,
			},
		})
		vim.api.nvim_create_autocmd("LspAttach", {
			callback = function(args)
				local client = vim.lsp.get_client_by_id(args.data.client_id)
				if not client then
					return
				end
				client.server_capabilities.semanticTokensProvider = nil
				if client.server_capabilities.inlayHintProvider then
					vim.lsp.inlay_hint.enable(true)
				end
			end,
		})
	end,
	event = "BufReadPre",
}
```
一方、`home-manager`では以下のように設定します。プラグインはLazy.nvimで管理するので、言語サーバー類をNixでインストールします。

```nix
{ pkgs, ... }: {
  programs.neovim = {
    enable = true;
    viAlias = true;

    extraLuaPackages = ps: [ ps.magick ps.tiktoken_core ];
    extraPackages = with pkgs; [
      # tree-sitter
      imagemagick
      deno
      nodejs
      tree-sitter
      # lsp, formatter, linter
      biome
      clang-tools
      cmake-language-server
      matlab-language-server
      csharp-ls
      dockerfile-language-server-nodejs
      efm-langserver
      haskell-language-server
      lua-language-server
      nil
      nixpkgs-fmt
      nixpkgs-lint
      nodePackages.eslint
      nodePackages.prettier
      pyright
      python311Packages.debugpy
      ruff
      rust-analyzer
      shellcheck
      stylelint
      stylua
      tailwindcss-language-server
      taplo
      tinymist
      typescript-language-server
      typstyle
      vim-language-server
      vscode-langservers-extracted
      yaml-language-server
      yamlfmt
      yamllint
      # dap
    ];
  };

  home.file.".config/nvim" = {
    source = ../home/.config/nvim;
    recursive = true;
  };


  home.file.".skk" = {
    source = "${pkgs.skkDictionaries.l}/share/skk";
    recursive = true;
  };

  home.file.".clang-tidy".source = ../home/.clang-tidy;
  home.file.".clang-format".source = ../home/.clang-format;
  home.file.".ideavimrc".source = ../home/.ideavimrc;
}
```

これで Neovim on NixOS 環境が完成です。……こんなこと言っておきながらいずれ完全にnix依存な設定にするかもしれませんが(その場合先の記事の著者さんの[dotfiles](https://github.com/asa1984/dotfiles)が綺麗な構成していると思います。


おまけで、私のdotfilesも載せておきます。かなり適当な記事になってしまいましたが、最後まで読んでくださりありがとうございました。

https://github.com/ojii3/dotfiles