Add NixOS module example with a note file for it
This commit is contained in:
parent
0e84296b79
commit
7208051274
27
04-nixos-module/flake.lock
generated
Normal file
27
04-nixos-module/flake.lock
generated
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
{
|
||||||
|
"nodes": {
|
||||||
|
"nixpkgs": {
|
||||||
|
"locked": {
|
||||||
|
"lastModified": 1776169885,
|
||||||
|
"narHash": "sha256-l/iNYDZ4bGOAFQY2q8y5OAfBBtrDAaPuRQqWaFHVRXM=",
|
||||||
|
"owner": "NixOS",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"rev": "4bd9165a9165d7b5e33ae57f3eecbcb28fb231c9",
|
||||||
|
"type": "github"
|
||||||
|
},
|
||||||
|
"original": {
|
||||||
|
"owner": "NixOS",
|
||||||
|
"ref": "nixos-unstable",
|
||||||
|
"repo": "nixpkgs",
|
||||||
|
"type": "github"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": {
|
||||||
|
"inputs": {
|
||||||
|
"nixpkgs": "nixpkgs"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"root": "root",
|
||||||
|
"version": 7
|
||||||
|
}
|
||||||
56
04-nixos-module/flake.nix
Normal file
56
04-nixos-module/flake.nix
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
{
|
||||||
|
# Defines a minimal NixOS module with one option and one config effect,
|
||||||
|
# exposes it as `nixosModules.default`, and verifies it by evaluating a
|
||||||
|
# throwaway NixOS configuration that imports the module.
|
||||||
|
description = "A minimal NixOS module";
|
||||||
|
|
||||||
|
inputs = {
|
||||||
|
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
|
||||||
|
};
|
||||||
|
|
||||||
|
outputs =
|
||||||
|
{ self, nixpkgs, ... }:
|
||||||
|
let
|
||||||
|
system = "x86_64-linux";
|
||||||
|
pkgs = nixpkgs.legacyPackages.${system};
|
||||||
|
|
||||||
|
# Throwaway NixOS configuration used only by the check below.
|
||||||
|
# We never build `system.build.toplevel`, so bootloader and
|
||||||
|
# `fileSystems` assertions never fire; reading a single config
|
||||||
|
# attribute is enough to prove the module evaluates.
|
||||||
|
testConfig = nixpkgs.lib.nixosSystem {
|
||||||
|
inherit system;
|
||||||
|
modules = [
|
||||||
|
self.nixosModules.default
|
||||||
|
{
|
||||||
|
system.stateVersion = "24.11";
|
||||||
|
playground.greeter = {
|
||||||
|
enable = true;
|
||||||
|
name = "flakes";
|
||||||
|
};
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# Other flakes consume the module via:
|
||||||
|
# imports = [ inputs.nix-playground.nixosModules.default ];
|
||||||
|
nixosModules.default = import ./module.nix;
|
||||||
|
|
||||||
|
# Builds only if the merged config's greeting matches expectation.
|
||||||
|
checks.${system}.greeting =
|
||||||
|
pkgs.runCommand "greeting-check"
|
||||||
|
{
|
||||||
|
got = testConfig.config.environment.etc."greeting".text;
|
||||||
|
expected = "hello, flakes";
|
||||||
|
}
|
||||||
|
''
|
||||||
|
if [ "$got" = "$expected" ]; then
|
||||||
|
echo ok > $out
|
||||||
|
else
|
||||||
|
echo "unexpected greeting: $got" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
'';
|
||||||
|
};
|
||||||
|
}
|
||||||
23
04-nixos-module/module.nix
Normal file
23
04-nixos-module/module.nix
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
{ lib, config, ... }:
|
||||||
|
|
||||||
|
let
|
||||||
|
cfg = config.playground.greeter;
|
||||||
|
in
|
||||||
|
{
|
||||||
|
# One option namespace keeps the example focused.
|
||||||
|
options.playground.greeter = {
|
||||||
|
enable = lib.mkEnableOption "the playground greeter";
|
||||||
|
|
||||||
|
name = lib.mkOption {
|
||||||
|
type = lib.types.str;
|
||||||
|
default = "world";
|
||||||
|
description = "Name placed in the generated /etc/greeting file.";
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
# `mkIf` discards the whole config block when enable is false, so a
|
||||||
|
# system that imports the module but leaves it disabled pays nothing.
|
||||||
|
config = lib.mkIf cfg.enable {
|
||||||
|
environment.etc."greeting".text = "hello, ${cfg.name}";
|
||||||
|
};
|
||||||
|
}
|
||||||
152
notes/006-nixos-modules.md
Normal file
152
notes/006-nixos-modules.md
Normal file
@ -0,0 +1,152 @@
|
|||||||
|
# NixOS Modules
|
||||||
|
|
||||||
|
This note covers `04-nixos-module/`, which defines the smallest useful NixOS module: one option, one effect, exposed as a flake output other flakes can import.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. What a Module Is
|
||||||
|
|
||||||
|
A NixOS module is a function from module arguments (`{ lib, config, pkgs, ... }`) to an attrset with two sections:
|
||||||
|
|
||||||
|
- `options`: declarations of typed configuration points.
|
||||||
|
- `config`: values assigned to options declared by this module, and by other modules in the set.
|
||||||
|
|
||||||
|
The NixOS module system collects every module, merges their `options` declarations, merges their `config` assignments, resolves the merged `config` against the merged `options`, and exposes the final result as a single `config` value. Your system configuration is just another module in the set.
|
||||||
|
|
||||||
|
A module can also appear as a plain attrset when it has no arguments. The function form is the general case.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. The Module in `04-nixos-module/`
|
||||||
|
|
||||||
|
`module.nix` declares one option namespace, `playground.greeter`, with two options:
|
||||||
|
|
||||||
|
- `enable`: a boolean toggle, produced by `lib.mkEnableOption`.
|
||||||
|
- `name`: a string with default `"world"`, produced by `lib.mkOption`.
|
||||||
|
|
||||||
|
When `enable` is true, the `config` block defines `environment.etc."greeting".text` to `"hello, ${cfg.name}"`. `lib.mkIf cfg.enable { ... }` wraps the block so it contributes nothing when disabled, rather than setting options to null or empty values.
|
||||||
|
|
||||||
|
The module has no `imports`, no `pkgs` usage, and no `services.*` interaction. That keeps the example focused on declaring an option and conditionally assigning one piece of config.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Exposing the Module From a Flake
|
||||||
|
|
||||||
|
A flake ships modules through the `nixosModules` output:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
outputs = { self, nixpkgs, ... }: {
|
||||||
|
nixosModules.default = import ./module.nix;
|
||||||
|
};
|
||||||
|
```
|
||||||
|
|
||||||
|
Downstream flakes consume it like any other module:
|
||||||
|
|
||||||
|
```nix
|
||||||
|
{
|
||||||
|
inputs.playground.url = "github:you/nix-playground?dir=04-nixos-module";
|
||||||
|
|
||||||
|
outputs = { self, nixpkgs, playground }: {
|
||||||
|
nixosConfigurations.my-host = nixpkgs.lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
playground.nixosModules.default
|
||||||
|
./configuration.nix
|
||||||
|
];
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`nixosModules.<name>` is the conventional path; nothing in the module system enforces the name, but tools and users expect it.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Verifying a Module Without Building a System
|
||||||
|
|
||||||
|
`nix flake check` needs the module to be exercised somehow. The example uses the smallest viable approach: evaluate a throwaway NixOS configuration that imports the module, set the options to known values, and read one attribute back out.
|
||||||
|
|
||||||
|
```nix
|
||||||
|
testConfig = nixpkgs.lib.nixosSystem {
|
||||||
|
inherit system;
|
||||||
|
modules = [
|
||||||
|
self.nixosModules.default
|
||||||
|
{
|
||||||
|
system.stateVersion = "24.11";
|
||||||
|
playground.greeter = { enable = true; name = "flakes"; };
|
||||||
|
}
|
||||||
|
];
|
||||||
|
};
|
||||||
|
|
||||||
|
checks.${system}.greeting = pkgs.runCommand "greeting-check" {
|
||||||
|
got = testConfig.config.environment.etc."greeting".text;
|
||||||
|
expected = "hello, flakes";
|
||||||
|
} ''
|
||||||
|
[ "$got" = "$expected" ] || { echo "got=$got"; exit 1; }
|
||||||
|
echo ok > $out
|
||||||
|
'';
|
||||||
|
```
|
||||||
|
|
||||||
|
Three things to notice:
|
||||||
|
|
||||||
|
- The test config never triggers `system.build.toplevel`, so assertions that normally demand `fileSystems."/"`, a bootloader, and a hostname do not fire. Reading a single value through `.config.…` is a pure option lookup.
|
||||||
|
- `pkgs.runCommand` receives the computed greeting as an environment variable `got`. The comparison happens at build time, inside the sandbox, so a mismatch fails the build.
|
||||||
|
- The check only proves evaluation and one config value. Richer modules deserve richer checks: multiple configurations, negative cases, or a full `pkgs.nixosTest` VM.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Option Types and Merging
|
||||||
|
|
||||||
|
`lib.types` covers the common cases:
|
||||||
|
|
||||||
|
- `types.bool`, `types.int`, `types.str`, `types.path`, `types.package`.
|
||||||
|
- `types.listOf <t>`, `types.attrsOf <t>`, `types.nullOr <t>`.
|
||||||
|
- `types.enum [ "a" "b" ]`, `types.submodule { options = { ... }; }`.
|
||||||
|
- `types.lines`, `types.separatedString <sep>` for accumulating strings.
|
||||||
|
|
||||||
|
Each type defines how multiple assignments merge. Booleans OR together by default, strings with `types.lines` concatenate with newlines, lists with `types.listOf` concatenate, attrsets with `types.attrsOf` deep-merge. When you need to override the merge, use `lib.mkForce`, `lib.mkDefault`, `lib.mkBefore`, or `lib.mkAfter`.
|
||||||
|
|
||||||
|
This merge behavior is why multiple modules can each add packages to `environment.systemPackages` without stepping on each other: the list type accumulates.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Commands to Try
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd 04-nixos-module
|
||||||
|
|
||||||
|
nix flake show # confirms nixosModules.default and checks.*.greeting
|
||||||
|
nix flake check # evaluates the module and builds the check
|
||||||
|
|
||||||
|
# Inspect the module's effect on the test configuration:
|
||||||
|
nix eval .#checks.x86_64-linux.greeting.drvAttrs.got
|
||||||
|
|
||||||
|
# Flip the option off in a scratch eval to confirm mkIf elides the config:
|
||||||
|
nix eval --impure --expr '
|
||||||
|
let
|
||||||
|
flake = builtins.getFlake (toString ./.);
|
||||||
|
sys = (import <nixpkgs> {}).lib.nixosSystem {
|
||||||
|
system = "x86_64-linux";
|
||||||
|
modules = [
|
||||||
|
flake.nixosModules.default
|
||||||
|
{ system.stateVersion = "24.11"; playground.greeter.enable = false; }
|
||||||
|
];
|
||||||
|
};
|
||||||
|
in sys.config.environment.etc ? greeting
|
||||||
|
'
|
||||||
|
```
|
||||||
|
|
||||||
|
The last command should print `false`: with `enable = false`, the `mkIf` block contributes nothing, so `environment.etc.greeting` is never declared.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. When to Reach for a Module
|
||||||
|
|
||||||
|
Modules earn their weight in a few recurring situations:
|
||||||
|
|
||||||
|
- The same configuration needs to turn on across several hosts or flakes.
|
||||||
|
- Consumers deserve typed, documented knobs instead of raw config snippets.
|
||||||
|
- The integration belongs alongside `services.*`, `environment.*`, `users.*`, or `systemd.*` rather than next to them.
|
||||||
|
- A change set benefits from option merging, so multiple modules can contribute without overwriting each other.
|
||||||
|
|
||||||
|
For a one-off tweak to a single host, a plain `configuration.nix` snippet is usually smaller. The module system pays off once the same configuration needs reuse, parameterization, or validation.
|
||||||
Loading…
x
Reference in New Issue
Block a user