Modulix provides a module system tailored for the configuration of flake-based single user NixOS and Home Manager systems. It also provides a flake generator that allows to decentralize flake inputs.
- Introduction
- Getting started
- Module structure
- Configuration structure
- Suggested way to structure configurations
- Modulix generator
- nixd support
- Differences to Combined Manager
The main advantage of Modulix is the elimination of the following separations:
- Splitting your configuration into NixOS and Home Manager modules, which are typically put into different files, even though they may be semantically related.
- Putting all flake inputs into
flake.nix
, even though they may only be relevant to one module.
Modulix provides a module system consisting of modules that can add flake inputs, import NixOS and Home Manager modules, and define NixOS and Home Manager options.
Another advantage of not using the NixOS or Home Manager module system directly is the freedom you gain when declaring your own options, because you don't have to worry about your options colliding with any NixOS or Home Manager options.
- Create a
config.nix
file and fill it with attributes as described in the configuration structure section. - Convert your NixOS and Home Manager configurations to Modulix modules. You can convert only parts of your existing configurations by importing the rest in
osImports
andhmImports
. - Generate your flake using the Modulix generator, or
mxg
, which this project's flake exposes as a package. You will need to regenerate it when your flake inputs change. For more information on the generator, see Modulix generator. - Finally, you can build and activate your system as usual.
{
lib, # A slightly modified version of the nixpkgs library, extended with the Home Manager library
utils, # Additional utility functions and constants, which are also available as an argument to NixOS modules
inputs, # The flake inputs
pkgs,
configName, # The name of the configuration being evaluated
useHm, # Whether the current configuration uses Home Manager
hmUsername, # The Home Manager username, for which the configuration is being evaluated
options,
config,
...
}: {
# Add inputs
inputs = {
name1.url = "...";
name2.url = "...";
};
imports = [];
osImports = []; # Import NixOS modules
hmImports = []; # Import Home Manager modules
options.someOption.enable = lib.mkEnableOption "..."; # Declare Modulix options
config = {
# Define assertions and warnings, just like with NixOS or Home Manager modules
assertions = [
{
assertion = false;
message = "Assertion message";
}
];
warnings = ["Warning message"];
os.someOsOption = true; # Define NixOS options
hm.someHmOption = true; # Define Home Manager options
otherOption.enable = true; # Define Modulix options
};
}
The modulixSystems
function exposed by this project's flake accepts configurations in the following form. Normally you don't need to call it directly, because flakes generated by the Modulix generator call it.
For more advanced use cases, you can also use the nixosSystem
function exposed by this project's flake to get the evaluated NixOS system for a single provided configuration.
{
initialInputs.name.url = "..."; # Add flake inputs that do not exist in any module. nixpkgs, home-manager, and modulix are automatically added by the Modulix generator
defaultSystem = "..."; # Set the default system. Defaults to x86_64-linux if not set
defaultHmUsername = "..."; # Set the default Home Manager username
globalModules = []; # Add Modulix modules, which are included in every Modulix configuration
configurations = {
config1 = {
system = "..."; # Specify the system for this configuration. Defaults to the defaultSystem
useHm = false; # Whether Home Manager should be enabled for this configuration. Defaults to true
hmUsername = "..."; # Set the Home Manager username. Defaults to defaultHmUsername
modules = []; # Add Modulix modules to this configuration
prefix = []; # Change the prefix shown before option paths in error messages. Not necessary in most cases
};
config2.modules = [];
config3.modules = [];
};
outputs = inputs: {}; # Add additional flake outputs
}
I would suggest structuring personal Modulix configurations as follows:
.
├── config.nix
├── hosts
│ ├── host1
│ │ ├── default.nix
│ │ └── hardware-configuration.nix
│ └── host2
│ ├── default.nix
│ └── hardware-configuration.nix
└── modules
├── default.nix
├── top-level-module.nix
├── category1
│ ├── default.nix
│ └── module.nix
└── category2
├── default.nix
├── module1.nix
├── module2.nix
└── subcategory
├── default.nix
├── module1.nix
└── module2.nix
Visualization created with eza.
- Within
config.nix
, create a configuration for each of your hosts. - Create a host directory for each of your hosts, containing the
hardware-configuration.nix
generated bynixos-generate-config
. - Split your configuration into Modulix modules, where each module should be responsible for only one service that you might want to enable or disable for each of your hosts. The module should add flake inputs and import NixOS and Home Manager modules as needed.
- Each Modulix module should reside in a different file, whose path in the file system should resemble the path to the options that module provides.
- Each Modulix module should have at least one enable option.
- Other parts of your config shouldn't directly define options that another module is responsible for. Instead, that other module should declare high-level options that other modules or hosts can then define.
- For each depth in the modules tree, there should be a
default.nix
file that imports each module at that depth and sets defaults when it is enabled. This way, you benefit from nice defaults for each host, but you can also easily disable whole categories of modules if you need to. - The root of your module tree should be added to the
globalModules
parameter of yourconfig.nix
.
If your modules depend on each other and you want to express this easily, you can add the following module. To use it, just set the dependencies
option to a list of string paths to other modules. String paths are similar to how you would access the definitions of it's declared options, e.g. category2.subcategory.module1
.
{
lib,
options,
config,
...
}: let
inherit (lib) concatMap concatStringsSep drop flip getAttrFromPath mkOption removeSuffix splitString;
inherit (lib.types) listOf str;
in {
options.dependencies = mkOption {
type = listOf str;
internal = true;
default = [];
description = "Allows modules to express dependencies that must be enabled for the module to work properly. If the dependencies are not enabled, an error is thrown.";
};
config.assertions = flip concatMap options.dependencies.definitionsWithLocations (
x: let
dependent = removeSuffix ".nix" (concatStringsSep "." (drop 5 (splitString "/" x.file)));
in
flip map x.value (x: {
assertion = getAttrFromPath (splitString "." x ++ ["enable"]) config;
message = "${dependent} depends on ${x}, but ${x} is disabled.";
})
);
}
Modulix provides a flake generator called Modulix Generator, or mxg
for short, which this project's flake exposes as the mxg
package.
There is a need for this generator because nix requires flake inputs to be defined within the flake.nix
file, see this issue.
You can add inputs.modulix.packages.${pkgs.system}.mxg
to os.environment.systemPackages
or hm.home.packages
to expose the generator directly, or you can create your own rebuild script.
The Modulix generator should be fast enough that it shouldn't have a noticeable impact on performance if it is run on every rebuild using a rebuild script. Here is a sample rebuild script using the Modulix generator and nh:
pkgs.writeShellScriptBin "rb" "${lib.getExe inputs.modulix.packages.${pkgs.system}.mxg} && ${lib.getExe pkgs.nh} os $* /etc/nixos"
The generator automatically searches all your Modulix modules and creates a flake.nix
file with all the inputs it finds. It also automatically adds the nixpkgs
, home-manager
and modulix
inputs if it doesn't find them, so you don't have to add them.
The generator accepts some optional options to tweak its behavior:
- The generator normally doesn't overwrite
flake.nix
files that don't contain the autogeneration warning. You can change this with the--force
or-f
flag. - You can tell the generator which directory to find your configuration in by using the
--path
or-p
option. This option defaults to/etc/nixos
.
Modulix supports option auto-completion using the nixd language server, using the following nixd configuration:
{
"options": {
"modulix": {
"expr": "(builtins.getFlake \"LOCATION\").modulixConfigurations.NAME.options"
}
}
}
Replace LOCATION
with your configuration location, e.g. /etc/nixos
, and NAME
with the name of a configuration you have defined in your config.nix
. If you define the language server configuration inside a Modulix module, you can set NAME
to the module argument configName
.
Modulix grew out of my fork of FlafyDev's Combined Manager project, so there are many similarities between the two projects. However, there are also some significant differences:
- Combined Manager depends on a patched version of nix instead of a flake generator. Since there is no binary cache for this patched version of nix, you have to build nix locally whenever a new version of nix is released. Using a patched version also makes your configuration less easily reproducible.
- Modulix has a cleaner and more streamlined approach to adding flake inputs, as well as NixOS and Home Manager modules, because it uses a slightly patched version of the nixpkgs library, allowing the module system to be extended with these new concepts.
- Modulix supports option completion with nixd.
- Modulix includes quite a bit of code to give you more informative error messages.
- Modulix supports top-level assertions and warnings.