NixOS Conditional Config and Custom Options

Associated Youtube Video: https://youtu.be/Qull6TMQm4Q

Associated Odysee Video: https://odysee.com/@LibrePhoenix:8/custom-options-and-if-else-statements-in:1

Previous Tutorial: Methods of Making Your NixOS Config More Modular

Next Tutorial: Program a Modular Control Center for Your Config Using Special Args in NixOS Flakes

This discussion goes a bit further than my previous post in terms of building modularity into your NixOS configuration via if-else statements (conditionals) and your own options.

Creating Your Own Options!

Utilizing the options already defined in nixpkgs is great, but what if you want to define an arbitrary option yourself? Trying to simply define it will not work:

{ config, pkgs, ... }:

{
  my.arbitrary.option = "This is my own option"; # this doesn't work
}

Instead, you must first define it as an option, using the options part of the attribute set and create it with the lib.mkOption function:

{ config, lib, pkgs, ... }:

{
  options = {
    my.arbitrary.option = lib.mkOption {
      type = lib.types.string;
      default = "stuff";
    };
  };
}

Data Types for Option Values

A good list of the types can actually be found in the nixpkgs source code here. A few common types include:

  • lib.types.anything - Accept anything as a value
  • lib.types.str - Strings
  • lib.types.nonEmptyStr - String that can’t be “”
  • lib.types.bool - True or false
  • lib.types.int - Signed integer
  • lib.types.float - Floating point number
  • lib.types.number - Either an integer or float
  • lib.types.path - A path on the file system (usually set by using ./ “relative/path/to/whataver”)
  • lib.types.listOf - A list, used by passing another type, such as:
    • lib.types.listOf lib.types.anything - A list of anything (i.e. multiple different types)
    • lib.types.listOf lib.types.str - A list of only strings
    • lib.types.listOf lib.types.number - A list of numbers
    • lib.types.listOf lib.types.path - A list of paths, kind of like imports

I Added Options And Now I’m Getting Errors When Rebuilding!

If you try to rebuild with some of your own options defined along with the rest of your configuration, it will fail:

# This doesn't work
{ config, lib, pkgs, ... }:

{
  options = {
    my.arbitrary.option = lib.mkOption {
      type = lib.types.string;
      default = "stuff";
    };
  };

  # The configuration below will not build
  # as-is if you're define your own options
  # as shown above

  # Bootloader
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  boot.loader.efi.efiSysMountPoint = "/boot/efi";

  # Networking
  networking.hostName = "nixos-tutorial"; # Define your hostname.
  networking.networkmanager.enable = true; # Use networkmanager

  # etc..
  # the rest of your config

}

So, what’s going on here? If you don’t define your own options, then Nix assumes that everything written inside of the main attribute set returned by the file is a part of the config attribute set. Thus the following two examples are equivalent:

# Same as example below
{ config, lib, pkgs, ... }:

{
  # Bootloader
  boot.loader.systemd-boot.enable = true;
  boot.loader.efi.canTouchEfiVariables = true;
  boot.loader.efi.efiSysMountPoint = "/boot/efi";

  # Networking
  networking.hostName = "nixos-tutorial"; # Define your hostname.
  networking.networkmanager.enable = true; # Use networkmanager

  # etc..
  # the rest of your config
}
# Same as example above
{ config, lib, pkgs, ... }:

{
  config = {
    # Bootloader
    boot.loader.systemd-boot.enable = true;
    boot.loader.efi.canTouchEfiVariables = true;
    boot.loader.efi.efiSysMountPoint = "/boot/efi";

    # Networking
    networking.hostName = "nixos-tutorial"; # Define your hostname.
    networking.networkmanager.enable = true; # Use networkmanager

    # etc..
    # the rest of your config
  };
}

The problem comes up if we define the options attribute set, Nix will not automatically assume that the rest of the configuration is a part of the config attribute set. Thus, we must place the rest of the configuration within the config attribute set. An example might look like this:

# This should work now
{ config, lib, pkgs, ... }:

{
  options = {
    my.arbitrary.option = lib.mkOption {
      type = lib.types.string;
      default = "stuff";
    };
  };

  config = {
    # Bootloader
    boot.loader.systemd-boot.enable = true;
    boot.loader.efi.canTouchEfiVariables = true;
    boot.loader.efi.efiSysMountPoint = "/boot/efi";

    # Networking
    networking.hostName = "nixos-tutorial"; # Define your hostname.
    networking.networkmanager.enable = true; # Use networkmanager

    # etc..
    # the rest of your config
  };

}

This actually shows us the overall structure a NixOS module must technically have:

{ config, pkgs, ... }:

{
  imports = [
    # list of other modules to import
  ];

  options = {
    # list of new options that can be set in config
  };

  config = {
    # where configuration options are set
  };

}

Options Are More Than Just Variables!

Do remember that you can redefine your own options in multiple Nix files (modules) using lib.mkForce or lib.mkOverride. This can allow certain values to have higher priority to set the actual value. For a more detailed explanation, check out my previous tutorial.

# arbitrary-option.nix
{ config, lib, pkgs, ... }:

{
  imports = [ ./force-the-value.nix ]

  options = {
    my.arbitrary.option = lib.mkOption {
      type = lib.types.string;
      default = "stuff";
    };
  };

  config = {
    my.arbitrary.option = "something else";
  };
}
# force-the-value.nix
{ config, lib, pkgs, ... }:

{
  config = {
    # lib.mkForce is used to override the definition from the `arbitrary-option.nix` file above
    my.arbitrary.option = lib.mkForce "a super amazing thing";
  };
}

Conditionals (If-else Statements)

Conditionals (if-else statements) are very common in other programming languages, allowing your program to make decisions. In Nix, conditionals look somewhat strange if you’ve learned how to write if-else statements in other programming languages.

In Bash, for example, if else statements are written like this:

if CONDITION; then
  BODY;
else
  BODY;
fi

The idea is essentially if (condition), then do something, otherwise, do something else.

Nix is of course different. With Nix we’re only allowed to declare things; we’re not allowed to do anything (which is why Nix is so difficult). This means that with Nix we use if-else statements to declare the value of a variable or something in an attribute set. For example:

let
  x = true;
  message = if (x == true) then "x is true" else "x is false";
in
  message
"x is true"

That’s a trivial example, but how could we utilize this in our config? Let’s say you have to decide between two different packages based upon your desktop environment or window manager. In my case, I might be deciding between XMonad or Hyprland, and I would want rofi on XMonad and fuzzel on Hyprland:

{ config, pkgs, ... }:
{
  environment.systemPackages = with pkgs; [
    alacritty
    vim
    emacs
  ] ++
  (if (config.services.xserver.windowManager.xmonad.enable == true)
    then [ pkgs.rofi ]
  else
    (if (config.programs.hyprland.enable == true)
      then [ pkgs.fuzzel ]
      else []));
}

The above code snippet sets the systemPackages to include alacritty, vim and emacs (things that I always want), but then joins that list with the result of the if-else statement using the ++ operator. The if-else statement then decides to:

  • add pkgs.rofi if config.services.xserver.windowManager.xmonad.enable is true
  • add pkgs.fuzzel instead if config.programs.hyprland.enable is true
  • not add anything else otherwise

The End

Next time we will look at another method of extending the power of this modularity through specialArgs and extraSpecialArgs, which allow you to pass a kind of “global” variable from a flake.nix into all of your Nix modules!

Donation Links

If you have found my work to be helpful, please consider donating via one of the following links. Thank you very much!