農工大アドカレ13日目!折り返しに入りました!
この記事は Arch → NixOS へのハードルを下げることを目的としています。dotfiles盆栽が趣味でないような人にはオススメできないかも?
NixOS に乗り換えたきっかけ
半年ほど前のある日、メインPCが崩壊(物理)。新しいPCでまた Arch Install Battle するのも飽きてきたので、話題のNixOSに挑戦してみることにしました。
ちまた(Twitter)でNixOSの話をよく見かけて興味もあったので、ちょうど良いタイミングでしたね。
NixOS乗換案内
まずはこちらの記事を参考にしつつ、手を動かしてみました。
この記事に詳しく書いてありますが、Nixはパッケージマネージャであると同時に言語でもあります。Nix言語で必要な設定を記述することで、環境構築を完全にコード化できるのが特徴の一つです。
この記事の通り、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
に用意されているオプションを用いて設定することができます。
{ 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だとこんな感じ。
# インストールするパッケージ
home.packages = with pkgs; [
# Hyprland =========================
hyprland
hyprpaper
hypridle
hyprlock
hyprpicker
# Hyprland Utilities ===============
swaynotificationcenter
networkmanagerapplet
grim
slurp
# terminal =========================
kitty
foot
];
次に、そのパッケージの本来の設定ファイルの位置にhome/
からシンボリックリンクを張るよう記述します。
# ディレクトリごとシンボリックリンクを張る
home.file.".config/hypr" = {
source = ../home/.config/hypr;
recursive = true;
};
宣言的にシンボリックリンクを作れるの、最高!
Hyprland以外も続々とシンボリックリンクを張っていきます。
# ターミナル
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というプラグインで管理してきましたが、雑に言ってしまうと、NixOS環境では動かせません。しかし、私の言語サーバーの設定は Mason と密な関係にあるので、これをMason
非依存にする必要があります。
といっても、MasonのLSPハンドラーは nvim-lspconfig の実行パスとしてをMasonの管理下のパスを指定するだけですので、剥がすのはそこまで難しくありませんでした。
home-manager
がある環境ではMasonを無効化し、home-manager
が無い環境では動くように分岐を書いておきます。
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でインストールします。
{ 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が綺麗な構成していると思います。
おまけで、私のdotfilesも載せておきます。かなり適当な記事になってしまいましたが、最後まで読んでくださりありがとうございました。