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
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
andhome-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 themkHosts
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:
- A set defining configuration values.
- 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 totrue
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 insideconfig
.
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 themodule2
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
andhome-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, themkModules
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 theconfig.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.