NixOSの設定をモジュール化する
この時点で既にシステムの構築が完了されているとします。現在の/etc/nixos
は次のようになっているはずです:
$ tree
.
├── flake.lock
├── flake.nix
├── home.nix
└── configuration.nix
これら4つのファイルの機能は次のとおりです:
flake.lock
: 自動で生成されたバージョンロック用のファイルです。Flakes全体の全てのinput source、ハッシュ値、バージョン番号が再現性のために記録されています。flake.nix
: The entry file that will be recognized and deployed when executingsudo nixos-rebuild switch
. See Flakes - NixOS Wiki for all options of flake.nix.configuration.nix
: flake.nixでNix moduleとしてインポートされ、全てのシステム設定がここに記述されています。このファイルの全てのオプションを確認するには Configuration - NixOS Manual を参照してください。home.nix
: fleke.nixでryan
の設定としてHome-Managerからインポートされ、全てのryan
の設定とryan
のホームディレクトリが含まれています。このファイルの全てのオプションを確認するには Appendix A. Configuration Options - Home-Manager を参照してください。
これらのファイルを変更することで、システムやホームディレクトリの状態を宣言的に変更できます。
しかし、configuration.nix
やhome.nix
のみに全てを保存していると設定が肥大化したときに保守が困難になります。より良い方法はNix moduleシステムを用いて設定を複数のNix moduleに分割し、カテゴリごとに分類して記述することです。
Nixモジュールシステムには、現在のNix moduleに複数の.nix
ファイルをマージするimports
というパラメータがあります。imports
は単に重複している設定を上書きするだけではなく、もっと賢くいい感じにしてくれます。例えば、program.packages = [...]
と複数のモジュールで定義されていた場合、これらの全てのリストをマージします。 Attribute setsも同様に正確にマージされます。この挙動を自身の目で確認してみましょう。
I only found a description of
imports
in Nixpkgs-Unstable Official Manual - evalModules Parameters:A list of modules. These are merged together to form the final configuration.
It's a bit ambiguous...
imports
が賢いおかげで、home.nix
とconfiguration.nix
を複数の.nix
ファイルで構成されるNix modulesに分割することができます。以下にpackages.nix
のmodulesの構成例を示します:
{
config,
pkgs,
...
}: {
imports = [
(import ./special-fonts-1.nix {inherit config pkgs;}) # (1)
./special-fonts-2.nix # (2)
];
fontconfig.enable = true;
}
This module loads two other modules in the imports section, namely special-fonts-1.nix
and special-fonts-2.nix
. Both files are modules themselves and look similar to this.
{ config, pkgs, ...}: {
# Configuration stuff ...
}
Both import statements above are equivalent in the parameters they receive:
Statement
(1)
imports the function inspecial-fonts-1.nix
and calls it by passing{config = config; pkgs = pkgs}
. Basically using the return value of the call (another partial configuration [attritbute set]) inside theimports
list.Statement
(2)
defines a path to a module, whose function Nix will load automatically when assembling the configurationconfig
. It will pass all matching arguments from the function inpackages.nix
to the loaded function inspecial-fonts-2.nix
which results inimport ./special-fonts-2.nix {config = config; pkgs = pkgs}
.
以下のリポジトリは、モジュール化を開始する際に非常に有用なテンプレートです:
ryan4yin/nix-config/i3-kickstarter はi3ウィンドウマネージャを用いた以前の私のNixOSの設定です。構造は以下のようになっています:
├── flake.lock
├── flake.nix
├── home
│ ├── default.nix # here we import all submodules by imports = [...]
│ ├── fcitx5 # fcitx5 input method's configuration
│ │ ├── default.nix
│ │ └── rime-data-flypy
│ ├── i3 # i3 window manager's configuration
│ │ ├── config
│ │ ├── default.nix
│ │ ├── i3blocks.conf
│ │ ├── keybindings
│ │ └── scripts
│ ├── programs
│ │ ├── browsers.nix
│ │ ├── common.nix
│ │ ├── default.nix # here we import all modules in programs folder by imports = [...]
│ │ ├── git.nix
│ │ ├── media.nix
│ │ ├── vscode.nix
│ │ └── xdg.nix
│ ├── rofi # rofi launcher's configuration
│ │ ├── configs
│ │ │ ├── arc_dark_colors.rasi
│ │ │ ├── arc_dark_transparent_colors.rasi
│ │ │ ├── power-profiles.rasi
│ │ │ ├── powermenu.rasi
│ │ │ ├── rofidmenu.rasi
│ │ │ └── rofikeyhint.rasi
│ │ └── default.nix
│ └── shell # shell/terminal related configuration
│ ├── common.nix
│ ├── default.nix
│ ├── nushell
│ │ ├── config.nu
│ │ ├── default.nix
│ │ └── env.nu
│ ├── starship.nix
│ └── terminals.nix
├── hosts
│ ├── msi-rtx4090 # My main machine's configuration
│ │ ├── default.nix # This is the old configuration.nix, but most of the content has been split out to modules.
│ │ └── hardware-configuration.nix # hardware & disk related configuration, autogenerated by nixos
│ └── my-nixos # my test machine's configuration
│ ├── default.nix
│ └── hardware-configuration.nix
├── modules # some common NixOS modules that can be reused
│ ├── i3.nix
│ └── system.nix
└── wallpaper.jpg # wallpaper
上記の構造に必ず当てはめる必要はありません。自分の好きなように構成できます。重要なのは、imports
を用いて全てのsub moduleをmain moduleにインポートすることです。
lib.mkOverride
, lib.mkDefault
, and lib.mkForce
In Nix, some people use lib.mkDefault
and lib.mkForce
to define values. These functions are designed to set default values or force values of options.
You can explore the source code of lib.mkDefault
and lib.mkForce
by running nix repl -f '<nixpkgs>'
and then entering :e lib.mkDefault
. To learn more about nix repl
, type :?
for the help information.
Here's the source code:
# ......
mkOverride = priority: content:
{ _type = "override";
inherit priority content;
};
mkOptionDefault = mkOverride 1500; # priority of option defaults
mkDefault = mkOverride 1000; # used in config sections of non-user modules to set a default
mkImageMediaOverride = mkOverride 60; # image media profiles can be derived by inclusion into host config, hence needing to override host config, but do allow user to mkForce
mkForce = mkOverride 50;
mkVMOverride = mkOverride 10; # used by ‘nixos-rebuild build-vm’
# ......
In summary, lib.mkDefault
is used to set default values of options with a priority of 1000 internally, and lib.mkForce
is used to force values of options with a priority of 50 internally. If you set a value of an option directly, it will be set with a default priority of 1000, the same as lib.mkDefault
.
The lower the priority
value, the higher the actual priority. As a result, lib.mkForce
has a higher priority than lib.mkDefault
. If you define multiple values with the same priority, Nix will throw an error.
Using these functions can be very helpful for modularizing the configuration. You can set default values in a low-level module (base module) and force values in a high-level module.
For example, in my configuration at ryan4yin/nix-config/blob/c515ea9/modules/nixos/core-server.nix, I define default values like this:
{ lib, pkgs, ... }:
{
# ......
nixpkgs.config.allowUnfree = lib.mkDefault false;
# ......
}
Then, for my desktop machine, I override the value in ryan4yin/nix-config/blob/c515ea9/modules/nixos/core-desktop.nix like this:
{ lib, pkgs, ... }:
{
# import the base module
imports = [
./core-server.nix
];
# override the default value defined in the base module
nixpkgs.config.allowUnfree = lib.mkForce true;
# ......
}
lib.mkOrder
, lib.mkBefore
, and lib.mkAfter
In addition to lib.mkDefault
and lib.mkForce
, there are also lib.mkBefore
and lib.mkAfter
, which are used to set the merge order of list-type options. These functions further contribute to the modularization of the configuration.
I haven't found the official documentation for list-type options, but I simply understand that they are types whose merge results are related to the order of merging. According to this understanding, both
list
andstring
types are list-type options, and these functions can indeed be used on these two types in practice.
As mentioned earlier, when you define multiple values with the same override priority, Nix will throw an error. However, by using lib.mkOrder
, lib.mkBefore
, or lib.mkAfter
, you can define multiple values with the same override priority, and they will be merged in the order you specify.
To examine the source code of lib.mkBefore
, you can run nix repl -f '<nixpkgs>'
and then enter :e lib.mkBefore
. To learn more about nix repl
, type :?
for the help information:
# ......
mkOrder = priority: content:
{ _type = "order";
inherit priority content;
};
mkBefore = mkOrder 500;
defaultOrderPriority = 1000;
mkAfter = mkOrder 1500;
# ......
Therefore, lib.mkBefore
is a shorthand for lib.mkOrder 500
, and lib.mkAfter
is a shorthand for lib.mkOrder 1500
.
To test the usage of lib.mkBefore
and lib.mkAfter
, let's create a simple Flake project:
# flake.nix
{
inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.11";
outputs = {nixpkgs, ...}: {
nixosConfigurations = {
"my-nixos" = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
({lib, ...}: {
programs.bash.shellInit = lib.mkBefore ''
echo 'insert before default'
'';
programs.zsh.shellInit = lib.mkBefore "echo 'insert before default';";
nix.settings.substituters = lib.mkBefore [
"https://nix-community.cachix.org"
];
})
({lib, ...}: {
programs.bash.shellInit = lib.mkAfter ''
echo 'insert after default'
'';
programs.zsh.shellInit = lib.mkAfter "echo 'insert after default';";
nix.settings.substituters = lib.mkAfter [
"https://ryan4yin.cachix.org"
];
})
({lib, ...}: {
programs.bash.shellInit = ''
echo 'this is default'
'';
programs.zsh.shellInit = "echo 'this is default';";
nix.settings.substituters = [
"https://nix-community.cachix.org"
];
})
];
};
};
};
}
The flake above contains the usage of lib.mkBefore
and lib.mkAfter
on multiline strings, single-line strings, and lists. Let's test the results:
# Example 1: multiline string merging
› echo $(nix eval .#nixosConfigurations.my-nixos.config.programs.bash.shellInit)
trace: warning: system.stateVersion is not set, defaulting to 24.11. Read why this matters on https://nixos.org/manual/nixos/stable/options.html#opt-system.stateVersio
n.
"echo 'insert before default'
echo 'this is default'
if [ -z \"$__NIXOS_SET_ENVIRONMENT_DONE\" ]; then
. /nix/store/60882lm9znqdmbssxqsd5bgnb7gybaf2-set-environment
fi
echo 'insert after default'
"
# example 2: single-line string merging
› echo $(nix eval .#nixosConfigurations.my-nixos.config.programs.zsh.shellInit)
"echo 'insert before default';
echo 'this is default';
echo 'insert after default';"
# Example 3: list merging
› nix eval .#nixosConfigurations.my-nixos.config.nix.settings.substituters
[ "https://nix-community.cachix.org" "https://nix-community.cachix.org" "https://cache.nixos.org/" "https://ryan4yin.cachix.org" ]
As you can see, lib.mkBefore
and lib.mkAfter
can define the order of merging of multiline strings, single-line strings, and lists. The order of merging is the same as the order of definition.
For a deeper introduction to the module system, see Module System & Custom Options.