Skip to content

PsychoLlama/nixos-modules-presentation-2025-01

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

2 Commits
 
 
 
 
 
 
 
 
 
 

Repository files navigation

title Extensible NixOS Modules
sub_title Encouraging Words for Sisyphus
author Jesse Gibson
theme
name
catppuccin-frappe

NixOS Modules are Wonderful

  • Encapsulating: programs.foo.enable is all I need to know.
  • Unifying: Settings are Nix expressions.
  • Composable: Modules can use other modules.

NixOS Modules are Horrible

  • 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.

Baseline Experience

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.

Configs Split into "Profiles"


Personal Config

# profiles/personal.nix
{
  environment.systemPackages = [
    pkgs.some-cool-tech
    pkgs.probably-malware
  ];
}
# Personal Config
{
  imports = [
    ./profiles/common.nix
    ./profiles/personal.nix
  ];

  # ...
}

Work Config

# profiles/work.nix
{
  environment.systemPackages = [
    pkgs.business-software
    pkgs.bazel # eww
  ];
}
# Work Config
{
  imports = [
    ./profiles/common.nix
    ./profiles/work.nix
  ];

  # ...
}

Kaboom

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 (NixOS vs nix-darwin).

Upgrades shouldn't force you to solve every problem at once.

Profiles Split into "Presets"

Personal Config

# 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
  ];

  # ...
}

Work Config

# 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
  ];

  # ...
}

Kaboom?

package ‘business-software-2.2.0’ failed to evaluate!
{
  # Disable it and go about your day.
  presets.business-software.enable = lib.mkForce false;
}

Anatomy of a Good Preset

{
  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.

Anatomy of a Good Profile

{
  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.

Extending the Platform

{
  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.*.

Extending the Platform

{
  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

Format and Language Generators

  • 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})
  '';
}

Keep Settings in Nix

{
  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.

Going further: Meta-Modules

For complex services, design higher-level modules.

  • lab.profiles.router
    • lab.services.dns
      • services.coredns
    • lab.services.dhcp
      • services.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 {
    # ...
  };
}

Summary of Patterns

  • 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.

Code Structure

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.)

Flake Exports

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;
  };
}

Advantages

  1. Extend your profiles from other machines and private flakes.
  2. Like your DSLs? Pull them into your other flakes.
  3. Share it with the world (scary).

Questions?

(Tips and tricks to follow)

Utility Functions

{
  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}")
        ];
    };
  };
}

Eval for Faster Feedback

nix eval '.#nixosConfigurations.my-machine.config.*'
  • Faster iteration while developing DSLs.
  • Incrementally build expressions.

Testing

pkgs.testers.runNixOSTest {
  imports = [
    self.nixosModules.nixos-platform

    {
      name = "my-test";
      nodes.machine = {
        # ...
      };

      testScript = ''
        machine.start()
        machine.shell_interact()
      '';
    }
  ];
}

Recommendations

  • Test platforms, not configs.
  • Ideal when realizing the config is dangerous, stateful, or scheduled.
  • Create a sandbox test.

Custom Module Namespaces

let mod = lib.evalModules {
  modules = [
    # ...
  ];
};
mod.config # ...

Recommendations

  • Noogle.dev
  • lib.pipe for complex functions
  • Use home-manager as a default
  • Comment EVERYTHING

Rules

  1. Presets, Profiles, Platforms.
  2. Modules should not have side effects.
  3. The file system should match the module system.
  4. Aggressively extend the platform with DSLs.

FINAL SLIDE

Jesse Gibson

https://github.com/PsychoLlama/dotfiles

About

"Extensible NixOS Modules", presented at the NYC Nix meetup. `nix run` to view slides.

Topics

Resources

Stars

Watchers

Forks

Contributors