This is a follow-up post to my prior one NixOS Option Inspection. Many thanks to @roberth who followed up on my issue and helped explain it. 🙏
If you are using NixOS, you’ve likely encountered the module system. It’s NixOS’s super-power and what makes it incredibly easy to share, reuse and configure systems based on Nix
{
imports = [ ./hello.nix ];
services.hello = {
enable = true;
greeter = "Bob";
};
}
In a prior post, I wrote about how it can be challenging to work backwards ⏪ from a NixOS option’s value to where it was defined.
Turns out, the answer in the post was relatively simple and Nixpkgs has a gesture for discovering the answer via definitionsWithLocations
.
Turns out, in order to get definitionsWithLocations
to play nice, you have to avoid a surprisingly common footgun 🥵.
Let’s use an incredibly simple example to demonstrate the bug.
First let’s make our module greet.nix
; it’s a trivial module that sets a single option. We also have a config.nix
which you could think of as our configuration.nix
in a typical NixOS installation that defines the option.
# greet.nix
{...}: {
greet.name = "hi";
}
# config.nix
let
modules = {
greet = import ./greet.nix;
};
system = {lib, ...}: {
imports = [
modules.greet
];
options = {
greet.name = lib.mkOption {
type = lib.types.str;
};
};
};
in (import <nixpkgs/lib>).evalModules {
modules = [ system ];
}
Next, let’s import this module, à la imports
keyword attribute in a NixOS module.
❗I have chosen to create a container attrset modules to mimic what we might accomplish in a Nix Flakes.
Everything looks OK, and evaluates correctly.
❯ nix-instantiate --eval config.nix -A config.greet.name
"hi"
If we try definitionsWithLocations
however we don’t get what we expected. 🤮
❯ nix repl
Nix 2.23.2
nix-repl> :l ./config.nix
Added 7 variables.
nix-repl> :p options.greet.name.definitionsWithLocations
[
{
file = "<unknown-file>";
value = "hi";
}
]
Did you spot the problem ? 🕵️♂️🧐🤔
It was the import ./greet.nix
🤯
We imported the Nix expression into our modules container and as a result lost any traceability back to the originating file.
Luckily, the fix is simple! Don’t import
within an imports
👌
# This is better ✅
modules = {
greet = ./greet.nix;
};
# This is also good ✅
imports = [
./greet.nix
];
With the applied fix, the results are what we want, and the world makes sense again. 😌
❯ nix repl
Nix 2.23.2
Type :? for help.
nix-repl> :l ./config.nix
Added 7 variables.
nix-repl> :p options.greet.name.definitionsWithLocations
[
{
file = "greet.nix";
value = "hi";
}
]
How common is this footgun ?
Turns out incredibly common (anecdotally) since there is nothing from preventing one from doing it; and it still “works”. The Nix Flake’s format makes this setup increasingly more common as well.
For a demonstration of how easy the fix is, I contributed a patch to agenix in #277 that fixes the issue.
diff --git a/flake.nix b/flake.nix
index 587138e..3a68940 100644
--- a/flake.nix
+++ b/flake.nix
@@ -23,13 +23,13 @@
}: let
eachSystem = nixpkgs.lib.genAttrs (import systems);
in {
- nixosModules.age = import ./modules/age.nix;
+ nixosModules.age = ./modules/age.nix;
nixosModules.default = self.nixosModules.age;
- darwinModules.age = import ./modules/age.nix;
+ darwinModules.age = ./modules/age.nix;
darwinModules.default = self.darwinModules.age;
- homeManagerModules.age = import ./modules/age-home.nix;
+ homeManagerModules.age = ./modules/age-home.nix;
homeManagerModules.default = self.homeManagerModules.age;
overlays.default = import ./overlay.nix;
Don’t import
within an imports