# 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.` 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 `, `types.attrsOf `, `types.nullOr `. - `types.enum [ "a" "b" ]`, `types.submodule { options = { ... }; }`. - `types.lines`, `types.separatedString ` 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 {}).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.