Top_level_attributes_explained
Understanding Top-Level Attributes in NixOS Modules

This explanation is based on insights from Infinisil, a prominent figure in the Nix community, to help clarify the concept of top-level attributes within NixOS modules.
The Core of a NixOS System: system.build.toplevel
In a NixOS system, everything is built from a single “system derivation.” The
command nix-build '<nixpkgs/nixos>' -A system
initiates this build process.
The -A system
part tells Nix to focus on the system
attribute defined in
the '<nixpkgs/nixos>'
file (which is essentially ./default.nix
within the
Nixpkgs repository).
This system
attribute is specifically the NixOS option system.build.toplevel
. Think of system.build.toplevel
as the very top of the configuration
hierarchy for your entire NixOS system. Almost every setting you configure
eventually influences this top-level derivation, often through a series of
intermediate steps.
Key Takeaway: system.build.toplevel
is the ultimate output that defines your entire NixOS system.
How Options Relate: A Chain of Influence
Options in NixOS are not isolated; they often build upon each other. Here’s an example of how a high-level option can lead down to a low-level system configuration:
- You enable Nginx with
services.nginx.enable = true;
. - This setting influences the lower-level option
systemd.services.nginx
. - Which, in turn, affects the even lower-level option
systemd.units."nginx.service"
. - Ultimately, this leads to the creation of a systemd unit file within
environment.etc."systemd/system"
. - Finally, this unit file ends up as
result/etc/systemd/system/nginx.service
within the finalsystem.build.toplevel
derivation.
Key Takeaway: Higher-level, user-friendly options are translated into lower-level system configurations that are part of the final system build.
The NixOS Module System: Evaluating Options
So, how do these options get processed and turned into the final system
configuration? That’s the job of the NixOS module system, located in the
./lib
directory of Nixpkgs (specifically in modules.nix
, options.nix
,
and types.nix
).
Interestingly, the module system isn’t exclusive to NixOS; you can use it to manage option sets in your own Nix projects.
Here’s a simplified example of using the module system outside of NixOS:
let
systemModule = { lib, config, ... }: {
options.toplevel = lib.mkOption {
type = lib.types.str;
};
options.enableFoo = lib.mkOption {
type = lib.types.bool;
default = false;
};
config.toplevel = ''
Is foo enabled? ${lib.boolToString config.enableFoo}
'';
};
userModule = {
enableFoo = true;
};
in (import <nixpkgs/lib>).evalModules {
modules = [ systemModule userModule ];
}
You can evaluate the config.toplevel
option from this example using:
nix-instantiate --eval file.nix -A config.toplevel
Key Takeaway: The NixOS module system is responsible for evaluating and merging option configurations from different modules.
How the Module System Works: A Simplified Overview
The module system processes a set of “modules” through these general steps:
Importing Modules: It recursively finds and includes all modules specified in
imports = [ ... ];
statements.Declaring Options: It collects all option declarations defined using
options = { ... };
from all the modules and merges them. If the same option is declared in multiple modules, the module system handles this (details omitted for simplicity).Defining Option Values: For each declared option, it gathers all the value assignments (defined using
config = { ... };
or directly at the top level if nooptions
orconfig
are present) from all modules and merges them according to the option’s defined type.
Important Note: Option evaluation is lazy, meaning an option’s value is only computed when it’s actually needed. It can also depend on the values of other options.
Key Takeaway: The module system imports, declares, and then evaluates option values from various modules to build the final configuration.
Top-Level Attributes in a Module: imports
, options
, and config
Within a NixOS module (the files that define parts of your system configuration) , the attributes defined directly at the top level of the module’s function have specific meanings:
imports
: This attribute is a list of other module files to include. Their options and configurations will also be part of the evaluation.options
: This attribute is where you declare new configuration options. You define their type, default value, description, etc., using functions likelib.mkOption
orlib.mkEnableOption
.config
: This attribute is where you assign values to the options that have been declared (either in the current module or in imported modules).
Key Takeaway: The top-level attributes imports
, options
, and config
are the primary ways to structure a NixOS module.
The Rule: Move Non-Option Attributes Under config
If you define either an options
or a config
attribute at the top level of
your module, any other attributes that are not option declarations must be
moved inside the config attribute.
Let’s look at an example of what not to do:
{ pkgs, lib, config, ... }:
{
imports = [];
# Defining an option at the top level
options.mine.desktop.enable = lib.mkEnableOption "desktop settings";
# This will cause an error because 'environment' and 'appstream'
# are not 'options' and 'config' is also present at the top level.
environment.systemPackages =
lib.mkIf config.appstream.enable [ pkgs.git ];
appstream.enable = true;
}
This will result in the error: error: Module has an unsupported attribute 'appstream' This is caused by introducing a top-level 'config' or 'options' attribute. Add configuration attributes immediately on the top level instead, or move all of them into the explicit 'config' attribute
.
Key Takeaway: When you have options
or config
at the top level, all
value assignments need to go inside the config block.
The Correct Way): Using the config
Attribute
To fix the previous example, you need to move the value assignments for
environment.systemPackages
and appstream.enable
inside the config attribute:
{ pkgs, lib, config, ... }:
{
imports = [];
# Defining an option at the top level
options.mine.desktop.enable = lib.mkEnableOption "desktop settings";
config = {
environment.systemPackages =
lib.mkIf config.appstream.enable [ pkgs.git ];
appstream.enable = true;
};
}
Now, Nix knows that you are declaring an option (options.mine.desktop.enable
)
and then setting values for other options (environment.systemPackages
,
appstream.enable
) within the config
block.
Key Takeaway: The config
attribute is used to define the values of
options.
Implicit config
: When options
is Absent
If your module does not define either options
or config
at the top level,
then any attributes you define directly at the top level are implicitly
treated as being part of the config.
For example, this is valid:
{ pkgs, lib, config, ... }:
{
environment.systemPackages =
lib.mkIf config.appstream.enable [ pkgs.git ];
appstream.enable = true;
}
Nix will implicitly understand that environment.systemPackages
and
appstream.enable
are configuration settings.
Key Takeaway: If no explicit options or config are present, top-level attributes are automatically considered part of the configuration.
Removing an Option: What Happens to config
Even if you remove the options
declaration from a module that has a config
section, the config = { environment.systemPackages = ... };
part will still
function correctly, assuming the option it’s referencing (appstream.enable
in this case) is defined elsewhere (e.g., in an imported module).
Key Takeaway: The config
section defines values for options, regardless
of whether those options are declared in the same module.