| title | Extensible NixOS Modules | ||
|---|---|---|---|
| sub_title | Encouraging Words for Sisyphus | ||
| author | Jesse Gibson | ||
| theme |
|
- Encapsulating:
programs.foo.enableis all I need to know. - Unifying: Settings are Nix expressions.
- Composable: Modules can use other modules.
- Dynamically Typed: Errors happen at evaluation. (If you're lucky.)
- Infinite Recursion: Good luck bisecting.
- Discoverability: Where did that setting come from?
Good patterns avoid the pain and leverage the power.
nixos-generate-config{
imports = [ ./hardware-configuration.nix ];
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
system.stateVersion = "24.11";
# ...
}... but it never stops at just one host.
# profiles/personal.nix
{
environment.systemPackages = [
pkgs.some-cool-tech
pkgs.probably-malware
];
}# Personal Config
{
imports = [
./profiles/common.nix
./profiles/personal.nix
];
# ...
}# profiles/work.nix
{
environment.systemPackages = [
pkgs.business-software
pkgs.bazel # eww
];
}# Work Config
{
imports = [
./profiles/common.nix
./profiles/work.nix
];
# ...
}error: Package ‘somepkg-2.2.0’ in /nix/store/ynjyhwksmz6rxipx3r0h8gyj42lvd4ak-source/pkgs/some-pkg.nix
a) To temporarily allow broken packages, you can use an environment variable
for a single invocation of the nix tools.
$ export NIXPKGS_ALLOW_BROKEN=1
Note: When using `nix shell`, `nix build`, `nix develop`, etc with a flake,
then pass `--impure` in order to allow use of environment variables.
b) For `nixos-rebuild` you can set
{ nixpkgs.config.allowBroken = true; }
in configuration.nix to override this.
c) For `nix-env`, `nix-build`, `nix-shell` or any other Nix command you can add
{ allowBroken = true; }
to ~/.config/nixpkgs/config.nix.
- The new package broke your workflow and you want to downgrade.
- An option isn't supported by some of your environments (
NixOSvsnix-darwin).
Upgrades shouldn't force you to solve every problem at once.
# presets/some-cool-tech.nix
{
options.presets.some-cool-tech = {
enable = lib.mkEnableOption "Use pkgs.some-cool-tech";
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
pkgs.some-cool-tech
];
};
}# profiles/personal.nix
{
presets.some-cool-tech.enable = true;
presets.probably-malware.enable = true;
}# Personal Config
{
imports = [
./profiles/common.nix
./profiles/personal.nix
];
# ...
}# presets/business-software.nix
{
options.presets.business-software = {
enable = lib.mkEnableOption "Use pkgs.business-software";
};
config = lib.mkIf cfg.enable {
environment.systemPackages = [
pkgs.business-software
];
};
}# profiles/work.nix
{
presets.business-software.enable = true;
presets.bazel.enable = true;
}# Work Config
{
imports = [
./profiles/common.nix
./profiles/work.nix
];
# ...
}package ‘business-software-2.2.0’ failed to evaluate!
{
# Disable it and go about your day.
presets.business-software.enable = lib.mkForce false;
}{
options.presets.programs.alacritty = {
enable = lib.mkEnableOption "Install and configure Alacritty";
};
config.programs.alacritty = lib.mkIf cfg.enable {
enable = true;
package = pkgs.unstable.alacritty;
settings = {
# ...
};
};
}- Single Responsibility: Only manages one program or service.
- Clearly Named: Mirrors the module it configures.
- Deferred: No side effects unless enabled.
Config files should live with the preset.
{
options.profiles.common = {
enable = lib.mkEnableOption "Use common presets";
};
config = lib.mkIf cfg.enable {
presets = {
programs.foo.enable = lib.mkDefault true;
programs.bar.enable = lib.mkDefault true;
services.baz.enable = lib.mkDefault true;
};
programs.basic.enable = lib.mkDefault true;
};
}# hosts/personal.nix
{
# ...
profiles.common.enable = true;
}- Deferred: No side effects unless enabled.
- Defaults: Easy to disable presets without
lib.mkForce. - Pragmatic: Doesn't force everything into a preset.
Profiles can evolve into presets.
{
options.presets.programs.glow = {
enable = lib.mkEnableOption "Install and configure pkgs.glow";
};
config = lib.mkIf cfg.enable {
home.packages = [
pkgs.glow # NO!
];
};
}Not everything exists in programs.*.
{
options.programs.glow = {
enable = lib.mkEnableOption "Whether to enable the `glow` markdown viewer";
package = lib.mkPackageOption pkgs "glow" {};
settings = lib.mkOption {
type = yaml.type;
default = { };
description = "Configuration written to `$XDG_CONFIG_HOME/glow/glow.yml`";
};
};
config = lib.mkIf cfg.enable {
home.packages = [ cfg.package ];
xdg.configFile."glow/glow.yml".source = yaml.generate "glow.yml" cfg.settings;
};
}- Overridable: Package can be configured, patched, or downgraded.
- Few Assumptions: Useful even without the preset. Another cut point.
- Nix-based Settings: RFC-0042
pkgs.formats.*(json, toml, yaml, ini, ...)lib.generators.*(lua, plist, ...)lib.hm.*(zsh, nushell, hyprconf, ...)
... or your own binding:
{
extraConfig = ''
settings = json.loads(${json.generate "settings.json" cfg.settings})
'';
}{
programs.foo.settings = {
run = pkgs.writers.writeRust "do-something" { } "...";
theme = ./themes/onedark;
};
}{
programs.jujusu.settings.user = {
name = config.programs.git.userName;
email = config.programs.git.userEmail;
};
}- Easily use paths and derivations like
pkgs.writers.*. - Settings are shared with all other modules.
- Extensible and mutable in downstream hosts.
For complex services, design higher-level modules.
lab.profiles.routerlab.services.dnsservices.coredns
lab.services.dhcpservices.kea
lab.services.gateway- (NixOS networking stuff)
For simpler DSLs, merge it into the existing program or service.
{
options.programs.nushell.libraries = {
enable = lib.mkEnableOption "Manage the library search path";
path = lib.mkOption {
type = types.listOf (types.either types.str types.path);
description = "Libraries visible in the search path";
default = [ ];
};
};
config.programs.nushell = lib.mkIf cfg.enable {
# ...
};
}- Presets: Very opinionated configs and services. Specific to a single program or service.
- Profiles: Enables groups of programs, services, and presets.
- Platforms: Extends the native platforms with new programs, services, and module options. NO CONFIGS.
platforms/
nixos/*
home-manager/
modules/
programs/*
services/*
$USERNAME/
presets/
services/*
programs/
glow.nix
profiles/
common.nix
{
options.psychollama.profiles.common = {
enable = lib.mkEnableOption "Use common programs and services";
};
}{
options.psychollama.presets.programs.glow = {
enable = lib.mkEnableOption "Install and configure pkgs.glow";
};
}- Organized by Platform: It's clear what platform options are available by the file's location.
- Options Mirror the File System: It's easy to find the definition for any option.
- Namespaced: Organized under username. (More on this later.)
Organize nixosModules by platforms and configs:
# flake.nix
{
nixosModules = {
nixos-platform = ./platforms/nixos/modules;
nixos-configs = ./platforms/nixos/modules/USERNAME;
nix-darwin-platform = ./platforms/nix-darwin/modules;
nix-darwin-configs = ./platforms/nix-darwin/modules/USERNAME;
home-manager-platform = ./platforms/home-manager/modules;
home-manager-configs = ./platforms/home-manager/modules/USERNAME;
};
}- Extend your profiles from other machines and private flakes.
- Like your DSLs? Pull them into your other flakes.
- Share it with the world (scary).
(Tips and tricks to follow)
{
options.lab.services.dhcp = {
lib.toClientId = mkOption {
type = types.functionTo types.str;
readOnly = true;
description = ''
Convert an IPv4 address to a DHCP client identifier. Useful when you
want to "hard-code" the IP but keep the router, DNS, and other fields
dynamic.
'';
default =
ip4:
lib.pipe ip4 [
# ["127", "0", "0", "1"]
(lib.splitString ".")
# [127, 0, 0, 1]
(lib.map lib.strings.toInt)
# ["7F", "0", "0", "1"]
(lib.map lib.trivial.toHexString)
# ["7F", "00", "00", "01"]
(lib.map (lib.strings.fixedWidthString 2 "0"))
# "7F:00:00:01"
(lib.concatStringsSep ":")
# "FE:01:7F:00:00:01"
(id: "FE:01:${id}")
];
};
};
}nix eval '.#nixosConfigurations.my-machine.config.*'- Faster iteration while developing DSLs.
- Incrementally build expressions.
pkgs.testers.runNixOSTest {
imports = [
self.nixosModules.nixos-platform
{
name = "my-test";
nodes.machine = {
# ...
};
testScript = ''
machine.start()
machine.shell_interact()
'';
}
];
}- Test platforms, not configs.
- Ideal when realizing the config is dangerous, stateful, or scheduled.
- Create a
sandboxtest.
let mod = lib.evalModules {
modules = [
# ...
];
};mod.config # ...- Noogle.dev
lib.pipefor complex functions- Use
home-manageras a default - Comment EVERYTHING
- Presets, Profiles, Platforms.
- Modules should not have side effects.
- The file system should match the module system.
- Aggressively extend the platform with DSLs.
Jesse Gibson