Introduction

Modulix is a NixOS configuration framework that simplifies host and module management using a structured approach, built upon haumea.

In short, modulix allows you to define your hosts and modules by organizing them into a directory structure:

.
├── hosts
│   ├── host1
│   └── host2
├── modules
│   ├── module1.nix
│   └── module2.nix
└── flake.nix

Modulix's source code is available on GitHub under the MIT license. You can see the implementation in the src directory.

→ Getting started

Getting Started

Installation

To use modulix, you first need to enable the following experimental Nix features:

experimental-features = nix-command flakes pipe-operators

Then add modulix to your flake inputs:

flake.nix:

{
    inputs = {
        nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
        home-manager = {
            url = "github:nix-community/home-manager";
            inputs.nixpkgs.follows = "nixpkgs";
        };
        modulix = {
            url = "github:anders130/modulix";
            inputs.nixpkgs.follows = "nixpkgs";
            inputs.home-manager.follows = "home-manager";
        };
    };

    ...
}

While overriding the nixpkgs and home-manager inputs is not required, it's recommended for consistency across your configuration.

Using mkHosts

Once modulix is included in your flake, you can use the mkHosts function to define your hosts:

flake.nix:

{
    ...

    outputs = inputs: {
        nixosConfigurations = inputs.modulix.lib.mkHosts {
            inherit inputs;
            src = ./hosts; # optional (defaults to ./hosts)
            flakePath = "/path/to/flake"; # optional (for lib.mkSymlink)
            modulesPath = ./modules; # optional
            specialArgs = {
                hostname = "nixos";
                # put in your specialArgs like above
            };
        };
    };
}

The mkHosts function will assume a directory structure like this:

.
├── hosts
│   ├── host1
│   │   ├── config.nix  # special args for the host
│   │   └── default.nix # the configuration for the host
│   └── host2
│       ├── config.nix
│       └── default.nix
├── modules
│   ├── module1.nix
│   └── module2.nix
└── flake.nix

Each host has:

  • A config.nix → Defines values for the specialArgs defined in the mkHosts function.
  • A default.nix → The configuration for the host.

You can switch to a specific host using:

nixos-rebuild switch --flake .#host1

Special arguments (specialArgs)

The specialArgs attribute in mkHosts allows you to define arguments that are passed to all files. These arguments provide default values that can be overridden by each host's config.nix file.

For example:

mkHosts {
    ...
    specialArgs = {
        argument1 = "value1";
        argument2 = "value2";
    };
}

This defines argument1 and argument2 as special arguments with their respective default values. If the config.nix file of a host provides a different value, it will override the default.

config.nix

Each host has a config.nix file, which can either be:

  1. A set defining configuration values.
  2. A function that takes the flake's inputs and returns a set.

The configuration set can include:

{
    isThinClient = false; # if true, lib.mkSymlink will use the store path instead of the flake path
    system = "x86_64-linux"; # the system of the host
    username = "nixos"; # the username of the host
    modules = []; # additional modules to add to the host
}

Additionally, config.nix can override the specialArgs values defined in mkHosts.

Example config.nix (as a function):

inputs: {
    system = "x86_64-linux";
    username = "user1";
    hostname = "host1";
    modules = [inputs.some-module.nixosModules.some-module];
}

Modules

The modules directory contains files that define your reusable configurations (modules). The directory structure determines how modules are loaded:

modules
├── module1.nix
├── module2
│   ├── submodule1.nix
│   └── submodule2.nix
└── category
    └── module3.nix

This structure results in the following configuration format:

{
    modules = {
        module1.enable = true;
        module2 = {
            submodule1.enable = true;
            submodule2.enable = true;
        };
        category.module3.enable = true;
    };
}

How Modules Work

  • Each module's configuration requires the enable option to be set to true to be enabled.
  • The enable option is created automatically if it doesn't exist.
  • Multi-file modules (default.nix inside a directory) are loaded as a single module when enabled.

Writing Modules

Basic Module Example

A simple module that enables a service:

modules/simple.nix

{
    services.foo.enable = true;
}

Defining Custom Options

Modules can define their own options:

{
    options.foo = lib.mkOption {
        type = lib.types.str;
        default = "bar";
    };
    config = cfg: {
        services.foo.name = cfg.foo;
    };
}

The cfg argument contains the module's options, making them available inside config.

Importing external modules

Modules can also import other modules. This is useful, when you want to use options from another input:

{inputs, ...}: {
    imports = [inputs.some-module.nixosModules.some-module];

    options = { ... };

    config = { ... };
}

Multi-file modules

Modules can span multiple files. A directory containing a default.nix is treated as a multi-file module:

modules
├── module1.nix
└── module2
    ├── default.nix
    ├── extra.nix
    └── extra2.nix
  • If module2 is enabled, all nix code in the module2 directory will be loaded.
  • Each file can define its own options and config, and all options remain available in cfg.

Check out the example directory to see it in action.

API Reference

The following sections document everything in the library.

If you are using modulix with flakes, that would be inputs.modulix.lib.

mkHosts

Source: src/mkHosts.nix

Type: { inputs, src?, flakePath?, helpers?, modulesPath?, specialArgs?, sharedConfig? } -> { ... }

Arguments:

  • inputs : { ... }

    Inputs of the flake. Must contain self, nixpkgs and home-manager.

  • (optional) src : Path

    Path to the hosts directory. Defaults to ./hosts.

  • (optional) flakePath : String

    Full path to the flake as string. Defaults to null.

  • (optional) helpers : { ... } | args: { ... }

    Additional functions to add to the library. Can be a function or a set of functions. If it is a function, it will be called with the arguments passed to each file to make functions be able to use configuration values. If it is just a set of functions, they will just be added to the lib. Defaults to {}.

    Example:

    helpers = args: {
        getUsername = "got ${args.username}";
    };
    
  • (optional) modulesPath : Path

    Path to the modules directory. Defaults to null. If set, the mkModules function will be used to create the modules.

  • (optional) specialArgs : { ... }

    Special arguments for all hosts. Defaults to {}. These arguments will be passed to each file and can be overridden by the config.nix file of each host.

  • (optional) sharedConfig : { ... } | args: { ... }

    Shared configuration for all hosts (accepts args). Defaults to {}. Can be a function or a set of functions. If it is a function, it will be called with the arguments passed to each file to make functions be able to use configuration values.

mkModule

Source: src/mkModule.nix

Type: (hostArgs : { ... }) -> (createEnableOption : Bool) -> (path : Path) -> (args : { ... }) -> { ... }

Arguments:

  • hostArgs : { ... }

    the arguments passed to each file, including pkgs

  • createEnableOption : Bool

    whether to create an enable option for the module

  • path : Path

    the path to the module

  • args : { ... }

    contents of the modules file. This can contain imports, options and config attributes but does not have to.

Result:

The function returns a set with the following attributes:

  • imports : [ ... ]

    the imports of the module

  • options : { ... }

    the options of the module

  • config : { ... }

    the config of the module

The result will look like this:

{
    imports = [...];
    options.path.to.module = {
        enable = mkEnableOption "module name";
        ...
    };
    config = mkIf cfg.enable {
        ...
    };
}

mkModules

Source: src/mkModules.nix

Type: (path : Path) -> [ (args : { ... }) -> { ... } ]

Arguments:

  • path : Path

    the path to the modules directory

Result:

The function returns a list containing one function that takes the arguments passed to each file and returns a set with the following attributes:

{
    imports = [
        module1
        module2
        ...
    ];
}

The modules are created by the mkModule function.

mkRelativePath

Source: src/mkRelativePath.nix

Type: (root : Path) -> (path : Path) -> String

Arguments:

  • root : Path

    the root path.
    Example: inputs.self

  • path : Path

    the path to get the relative path of (relative to root)

Result:

This function is used to get the relative path of a file from a given root path.

mkRelativePath ./. ./path/to/file
# returns "path/to/file"

Usage inside files managed by mkHosts:

This function is configured by the mkHosts function to be used in a more convenient way:

mkRelativePath ./path/to/file
# returns "path/to/file"

mkSymlink

Source: src/mkSymlink.nix

Type: (args : { ... }) -> Path -> { ... }

Arguments:

  • args : { ... }

    This must contain the following attributes:

    • self : Path

      the flake path

    • flakePath : String

      the flake path as absolute path

    • hmConfig : { ... }

      the home-manager config of the current user

    • isThinClient : Bool

      whether the store path should be used instead of the flake path

  • path : Path

    the path to the file you want to symlink

Result:

This function is used to create a symlink.

Specifically, this function creates the following set:

{
    recursive = true;
    source = <drv>;
}

Usage inside files managed by mkHosts:

This function is configured by the mkHosts function to be used in a more convenient way:

mkSymlink ./path/to/file

recursiveLoadEvalTests

Source: src/recursiveLoadEvalTests.nix

Type: { src, inputs? } -> {}

Arguments:

  • src : Path

    the path to the directory containing the tests

  • (optional) inputs : { ... }

    inputs passed into each test to make functions available

Result:

This function is used to load and evaluate tests from a directory (much like haumea.lib.loadEvalTests). But it will flatten the directory structure to allow for nested directories and add them to the tests name for easier debugging.

internal

Internal functions used primarily for testing.

internal.adjustTypeArgs

Source: src/internal/adjustTypeArgs.nix

Type: { ... } -> { ... }

Removes the type and _type attributes from the given set to prevent infinite recursion.

internal.cleanupModule

Source: src/internal/cleanupModule.nix

Type: { ... } -> { (imports : [ ... ]); (options : { ... }); (config : { ... }); }

uses internal.adjustTypeArgs to adjust a module in tests.

internal.configure

Source: src/internal/configure.nix

Type: (hostArgs : { ... }) -> (helpers : { ... }) -> { ... }

Arguments:

  • hostArgs : { ... }

    the arguments passed to each file, including pkgs

  • helpers : { ... }

    helper functions given by the user to extend the lib set

Result:

The function returns an extended lib set with all the functions of modulix, the nixpkgs lib and the user's helpers.

internal.enableOptionResult

Source: src/internal/enableOptionResult.nix

Type: (moduleName : String) -> { ... }

Simplifies comparison of the enable option of a module. Used in tests.

internal.mkModules

Source: src/internal/mkModules.nix

Type: (hostArgs : { ... }) -> (path : Path) -> [ ... ]

Arguments:

  • hostArgs : { ... }

    the arguments passed to each file, including pkgs

  • path : Path

    path to the modules directory

Result:

A list of modules.

{
    imports = [
        module1
        module2
        ...
    ];
}

Used by mkHosts and mkModule to create modules.

See Also

  • haumea - The library modulix uses for handling hosts and other functionalities. It's great for building nix-based tools.
  • anders130/dotfiles - My personal dotfiles, which make extensive use of modulix.