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.
Table of Contents
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 valuelib.types.str
- Stringslib.types.nonEmptyStr
- String that can’t be “”lib.types.bool
- True or falselib.types.int
- Signed integerlib.types.float
- Floating point numberlib.types.number
- Either an integer or floatlib.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 stringslib.types.listOf
lib.types.number
- A list of numberslib.types.listOf
lib.types.path
- A list of paths, kind of likeimports
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
ifconfig.services.xserver.windowManager.xmonad.enable
is true - add
pkgs.fuzzel
instead ifconfig.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!