nixos-and-flakes-book/docs/nixos-with-flakes/modularize-the-configuration.md

249 lines
11 KiB
Markdown
Raw Normal View History

2023-06-23 14:29:12 +02:00
## Modularize your NixOS configuration
At this point, the skeleton of the entire system is basically configured. The current configuration structure in `/etc/nixos` should be as follows:
```
$ tree
.
├── flake.lock
├── flake.nix
├── home.nix
└── configuration.nix
```
The functions of these four files are explained below:
- `flake.lock`: An automatically generated version-lock file, which records all input sources, hash values, and version numbers of the entire flake to ensure that the system is reproducible.
- `flake.nix`: The entry file, which will be recognized and deployed when executing `sudo nixos-rebuild switch`.
- See [Flakes - NixOS Wiki](https://nixos.wiki/wiki/Flakes) for all options of flake.nix.
- `configuration.nix`: Imported as a nix module in flake.nix, all system-level configuration are currently written here.
- See [Configuration - NixOS Manual](https://nixos.org/manual/nixos/unstable/index.html#ch-configuration) for all options of configuration.nix.
- `home.nix`: Imported by home-manager as the configuration of the user `ryan` in flake.nix, that is, it contains all the configuration of `ryan`, and is responsible for managing `ryan`'s home folder.
- See [Appendix A. Configuration Options - home Manager](https://nix-community.github.io/home-manager/options.html) for all options of home.nix.
By modifying these files, you can change the status of the system and the home directory declaratively.
As the configuration increases, it will be difficult to maintain the configuration by relying solely on `configuration.nix` and `home.nix`. Therefore, a better solution is to use the nix module system to split the configuration into multiple modules and write them in a classified manner.
nix module system provide a paramter, `imports`, which accept a list of `.nix` files, and merge all the configuration defined in these files into the current nix module. Note that the word used here is "**merge**", which means that `imports` will **NOT** simply overwrite the duplicate configuration, but handle them more reasonably. For example, if I define `program.packages = [...]` in multiple modules, then `imports` will merge all `program.packages` defined in all nix modules into one list. Not only lists can be merged correctly, but attribute sets can also be merged correctly. The specific behavior can be explored by yourself.
> I only found a description of `imports` in [nixpkgs-unstable official manual - evalModules parameters](https://nixos.org/manual/nixpkgs/unstable/#module-system-lib-evalModules-parameters): `A list of modules. These are merged together to form the final configuration.`, it's a bit ambiguous...
With the help of `imports`, we can split `home.nix` and `configuration.nix` into multiple nix modules defined in diffrent `.nix` files.
Use [ryan4yin/nix-config/v0.0.2](https://github.com/ryan4yin/nix-config/tree/v0.0.2) as an example, which is the configuration of my previous NixOS system with i3 window manager. The structure of it is as follows:
```shell
├── 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
│ └── nixos-test # 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
```
For more details, see [ryan4yin/nix-config/v0.0.2](https://github.com/ryan4yin/nix-config/tree/v0.0.2).
## `mkOverride`, `lib.mkDefault` and `lib.mkForce`
You may found some people use `lib.mkDefault` `lib.mkForce` to define values in Nix files, as their names suggest, `lib.mkDefault` and `lib.mkForce` are used to set default values or force values of options.
You can read the source code of `lib.mkDefault` and `lib.mkForce` to understand them by running `nix repl -f '<nixpkgs>'` and then enter `:e lib.mkDefault`(To learn the basic usage of `nix repl`, just type `:?` to see the help information).
its source code is as follows:
```nix
# ......
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
# ......
```
So `lib.mkDefault` is used to set default values of options, it has a priority of 1000 internally,
and `lib.mkForce` is used to force values of options, it has a priority of 50 internally.
If you just 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`'s value is, the higher the actual priority is, so `lib.mkForce` has a higher priority than `lib.mkDefault`.
If you defined multiple values with the same priority, Nix will throw an error.
They are useful to modularize the configuration, as you can set default values in a low-level module(base module), and force values in a high-level module.
For example, I defined some default values in <https://github.com/ryan4yin/nix-config/blob/main/modules/nixos/core-server.nix#L30>:
```nix
{ lib, pkgs, ... }:
{
# ......
nixpkgs.config.allowUnfree = lib.mkDefault false;
# ......
}
```
And for my dekstop machine, I force the values to another value in <https://github.com/ryan4yin/nix-config/blob/main/modules/nixos/core-desktop.nix#L15>:
```nix
{ 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`
`lib.mkBefore` and `lib.mkAfter` are used to set the merge order of **list-type options**, just like `lib.mkDefault` and `lib.mkForce`, they're also useful to modularize the configuration.
I said before that if you defined multiple values with the same **override priority**, Nix will throw an error.
But with `lib.mkOrder`, `lib.mkBefore` or `lib.mkAfter`, you can define multiple values with the same override priority, they will be merged in the order you defined.
Let's running `nix repl -f '<nixpkgs>'` and then enter `:e lib.mkBefore` to take a look at its source code(To learn the basic usage of `nix repl`, just type `:?` to see the help information):
```nix
# ......
mkOrder = priority: content:
{ _type = "order";
inherit priority content;
};
mkBefore = mkOrder 500;
mkAfter = mkOrder 1500;
# The default priority for things that don't have a priority specified.
defaultPriority = 100;
# ......
```
So `lib.mkBefore` is a shortcut for `lib.mkOrder 500`, and `lib.mkAfter` is a shortcut for `lib.mkOrder 1500`.
To test the usage of `lib.mkBefore` and `lib.mkAfter`, let's create a simple Flake project:
```shell
# create flake.nix with the following content
cat <<EOF | sudo tee flake.nix
{
description = "Ryan's NixOS Flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-23.05";
};
outputs = { self, nixpkgs, ... }@inputs: {
nixosConfigurations = {
"nixos-test" = nixpkgs.lib.nixosSystem {
system = "x86_64-linux";
modules = [
# demo module 1, insert git at the head of list
({lib, pkgs, ...}: {
environment.systemPackages = lib.mkBefore [pkgs.git];
})
# demo module 2, insert vim at the tail of list
({lib, pkgs, ...}: {
environment.systemPackages = lib.mkAfter [pkgs.vim];
})
# demo module 3, just add curl to the list normally
({lib, pkgs, ...}: {
environment.systemPackages = with pkgs; [curl];
})
];
};
};
};
}
EOF
# create flake.lock
nix flake update
# enter nix repl environment
nix repl
Welcome to Nix 2.13.3. Type :? for help.
# load the flake we just created
nix-repl> :lf .
Added 9 variables.
# check the order of systemPackages
nix-repl> outputs.nixosConfigurations.nixos-test.config.environment.systemPackages
[ «derivation /nix/store/0xvn7ssrwa0ax646gl4hwn8cpi05zl9j-git-2.40.1.drv»
«derivation /nix/store/7x8qmbvfai68sf73zq9szs5q78mc0kny-curl-8.1.1.drv»
«derivation /nix/store/bly81l03kh0dfly9ix2ysps6kyn1hrjl-nixos-container.drv»
......
......
«derivation /nix/store/qpmpvq5azka70lvamsca4g4sf55j8994-vim-9.0.1441.drv» ]
```
So we can see that the order of `systemPackages` is `git -> curl -> default packages -> vim`, which is the same as the order we defined in `flake.nix`.
> Though it's useless to adjust the order of `systemPackages`, it may be helpful at some other places...