So I’m trying to use the declarative-flatpak module to declare flatpak packages in my configuration.nix, and am running into the issues mentioned in the tile (infinite recursion, option not found), seemingly no matter what I do. I’ve been trying to troubleshoot this on and off for days, and have not made any progress, so any help would be much appreciated.
I’m importing the module with npins as instructed in the readme, with the following command:
I’m not entirely sure what you mean, but I don’t recall having touched that before. Is there a step I’m missing that’s necessary to use external modules? What I’ve done is run the npins command and add flatpaks to the attribute set, as well as try to import the module in the various ways described.
I did try to use specialArgs to import the module, and it threw the same error as when importing in the modules set, though maybe I’m using it wrong? I did
specialArgs = { inherit flatpaks; };
I have my configuration split up over a few files, imported in the imports set:
# System config
{ config, pkgs, ... }:
{
imports = [
# Include the results of the hardware scan.
./../hardware-configuration.nix
];
# Bootloader.
boot.loader.systemd-boot.enable = true;
boot.loader.efi.canTouchEfiVariables = true;
# Use latest kernel.
boot.kernelPackages = pkgs.linuxPackages_latest;
networking.hostName = "nixos"; # Define your hostname.
# networking.wireless.enable = true; # Enables wireless support via wpa_supplicant.
# Enable networking
networking.networkmanager.enable = true;
# Set your time zone.
time.timeZone = "time/zone"
# Select internationalisation properties.
i18n.defaultLocale = "en_US.UTF-8";
i18n.extraLocaleSettings = {
LC_ADDRESS = "en_US.UTF-8";
LC_IDENTIFICATION = "en_US.UTF-8";
LC_MEASUREMENT = "en_US.UTF-8";
LC_MONETARY = "en_US.UTF-8";
LC_NAME = "en_US.UTF-8";
LC_NUMERIC = "en_US.UTF-8";
LC_PAPER = "en_US.UTF-8";
LC_TELEPHONE = "en_US.UTF-8";
LC_TIME = "en_US.UTF-8";
};
# Enable the X11 windowing system.
services.xserver.enable = true;
# Enable the GNOME Desktop Environment.
services.xserver.displayManager.gdm.enable = true;
services.xserver.desktopManager.gnome.enable = true;
# Configure keymap in X11
services.xserver.xkb = {
layout = "us";
variant = "";
};
# Enable CUPS to print documents.
services.printing.enable = true;
# Enable sound with pipewire.
services.pulseaudio.enable = false;
security.rtkit.enable = true;
services.pipewire = {
enable = true;
alsa.enable = true;
alsa.support32Bit = true;
pulse.enable = true;
# If you want to use JACK applications, uncomment this
#jack.enable = true;
};
}
Ah, right, I see. The npins/default.nix is not a NixOS module. Get rid of the flatpaks in your module arguments, remove that npins line, and instead do something like:
{ config, pkgs, ... }:
let
sources = import ./npins;
in {
imports = [
"${sources.flatpaks}/nixos"
];
}
You might also want to look at how you’re getting nixpkgs; your current config doesn’t use the one from your npins.
Yes! No errors, thank you very much.
If I may, I’m curious about how this works. What I think is happening is we’re pulling in the ./npins directory as it’s own option (probably not the right word?), then sourcing the flatpaks module through that option, rather than directly. Since the npins/default.nix doesn’t act as a module, if it’s imported as a module, does nix try to evaluate it in a way that produces the breakage?
Now I’m confusing myself. I would be grateful if you could try to explain, for the benefit of my curiosity.
Yeah, I was wondering about that. I believe currently I’m using nix-channel for the stable branch and this snippet in my packages sub-configuration to access a few things on unstable:
# Import the unstable branch to use for some packages
let
unstable = import
(builtins.fetchTarball https://github.com/nixos/nixpkgs/tarball/nixpkgs-unstable)
# reuse the current configuration
{ config = config.nixpkgs.config; };
in
Would this be easier/more streamlined to do through npins?
I tried writing up a full explanation, but you’re basically asking “how does NixOS work” It’s far too much for a discourse post.
Have a read through the nix language basics and module system sections of nix.dev. You’ll need to understand the difference between things nix does (variables and import) and what NixOS does (modules, options and attributes like imports).
Your wording is indeed off - ./npins isn’t an “option” or anything, it’s simply an element of the imports list. NixOS will attempt to treat anything you put in the imports list as a module (implicitly calling import on it if it’s a file/directory - and if it’s a directory that will implicitly import the default.nix file in the directory instead), and merge it into your configuration.
Kinda. Nix evaluates it just fine, and it produces a normal attrset. The function call evaluates to something like this (in fact, it should be exactly this, since I used the values from your sources.json):
Since attrsets are perfectly valid “modules” (see the modules tutorial for a more thorough explanation), NixOS will attempt to incorporate that into your configuration as if you intended to set options to these values. It will then give you an error, because there is no flatpaks option. I.e., you get this result:
There is in fact a nixpkgs option, so you don’t get an error for that, but if you only had nixpkgs in your npins you’d get an error about it being the wrong type.
The infinite recursion on the other hand is a bit more complicated to explain. This happens because NixOS modules are evaluated through a Fixed-point combinator - Wikipedia - basically a function calling itself with its own arguments. The concept is a bit mind-bendy, and a feature of lazily-evaluated functional programming languages; it’s not too complex once you reason through it by hand once, though. I’ll leave learning how that works as an exercise to the reader.
What you need to understand, though, is that the function arguments given to the module are defined by the module itself through the _module.args option. If you simply write this:
… even though undefinedVariable has no definition anywhere in the system, NixOS will attempt to evaluate the function with itself recursively, because to retrieve the value of undefinedVariable from the _module.args option it needs to evaluate every possible module imports, which includes the (completely undefined) variable itself.
… I did tell you it’s a bit complicated to explain.
But the gist is basically that your configuration could never have worked in the infrec case, you simply never defined a flatpaks argument for your module.
If you instead set specialArgs.flatpaks at the top-level eval somehow (which I’m not sure is possible with traditional nix), the recursion could have been cut short because the module system skips _module.args in that case. This is typically how flakes shove flake inputs into the module system - I’m sure you could build something similar for npins, but I haven’t seen an example of that.
See my evaluation of your npins/default.nix further up? Looks remarkably similar, doesn’t it? Basically, all npins does is just manage writing and updating that code for you.
I strongly recommend using npins over writing these stanzas by hand. This is for two reasons:
If you litter these kinds of stanzas all over your code by hand you will never update them because it’s kind of a PITA and then you run insecure software.
You - and pretty much everyone else who doesn’t know exactly what they’re doing - are currently doing it horribly wrong.
The gist is that the builtins.fetch* functions are special functions that just create store paths with the result of downloading the given url. Unlike pretty much every other way of interacting with nix, these outputs are not hashed by default.
This means that, currently, every hour nix will download a new version of that tarball, and it will constantly re-evaluate the hash and try to recreate store paths. You’ve thrown reproducibility out of the window, and your builds will be quite slow, only saved by the tarball cache.
To not commit this insanity, you have to give the builtins.fetch* functions an attrset argument - instead of just a URL (and in fact, you should not use non-string URLs, those are deprecated), and specify the hash attribute.
npins does this for you, so you cannot commit this - ridiculously common - mistake. They also make referring to and updating the channel release tarballs instead of raw GitHub artifacts feasible, so you don’t need to fetch tarballs from GitHub and get the command-not-found database, which is currently missing from your nixpkgs instance.
As a start, you could replace that bit of your code with:
… but that only works if you put all the unstable stuff in the same module in which you import your npins. You could pass unstable to _module.args so other modules can depend on it through their arguments:
… but now I’m going beyond basic configuration structure. And of course, now you know how the infrec thing happens, never refer to unstable in imports if you do that
I’d also softly suggest figuring out how to replace the nixpkgs you get from nix-channel with one from your npins. This would complete the reproducibility story, and make it so you only need to run one command to update (npins update) - assuming you also make your nixos channel refer to that tarball so your other nix commands continue to get updates. That said, I don’t actually know how to do that off the top of my head, so I don’t blame you for not having figured that out.
Replace any uses of <nixpkgs> in your nixos config with sources.nixpkgs (i.e. nixpkgs from your pins - check your code).
Set NIX_PATH:
{ config, pkgs, ... }:
let
sources = import ./npins;
in {
nix.nixPath = [
"nixpkgs=${sources.nixpkgs}"
"nixos-config=/etc/nixos/configuration.nix" # preserving the default nixos-config value
];
}
Rebuild.
At this point you could even disable the nix-channel command entirely (nix.channel.enable = false;)
And when you need to upgrade to the next stable nixpkgs version in 6 months, you can simply run step 1 again (no need to remove the old nixpkgs pin first.)
I’m fairly sure the default channel name for nixos is nixos, so that should probably be nixos=, but it’s quite possible it also falls back to nixpkgs, since I’ve never seen issues from people renaming that.
Probably worth being correct here, though, since the outdated system nixos channel will continue to exist and might pop up in some edge cases.
The whole root nixos channel thing is coming from that first entry.
i.e. <nixpkgs> will point to the root’s nixos channel. It’s still an entry called nixpkgs, though, and all code would still use <nixpkgs> - which is also what nix would rely on by default in the absence of flakes.
(And we’re blasting the default anyway by setting nix.nixPath ourselves.)
Right, you could also set nixos as well in that option, or do some fancy mapping from all the sources attribute names to NIX_PATH entries. nixos-config would also be worth preserving here.
I’m pretty sure it actually is, with your exact code, since lists are merged additively.
… I think ideally we’d just have npins support in nixos-rebuild, as well as in nixpkgs like what we have for flakes. Maybe best to have this discussion elsewhere
Well, the explanation you provided has plenty enough that I’ll be chewing on it for a while, haha!
Seriously, though thank you for taking the time and providing those resources, I think I’ll be revisiting this post often as I attempt to understand nix better…
After spending a bit too long reading wikipedia, watching videos about lambda calculus, and playing with the examples from the nix.dev page you linked, I think I have a very vague understanding of this idea… This looks kinda cool, though, if obtuse, so I’m definitely going to investigate that more.
Ah, so that’s why it always seems to forget it had already downloaded the damn thing. Thanks for the code snippet, will definitely implement this!
As for getting the stable channel through npins, that does seem like a good idea. Thank you @waffle8946 for the example, and I will also check out the article you linked, @eblechschmidt.
Yeah, it really sucks that there are so many snippets of people doing this floating around the web. Even nix.dev suggests it.
It gives the impression that nix’ caching is brittle (or nonexistent) and that the builds aren’t reproducible at all. I’ve seen at least a handful newbies declare nix broken because of this (and they’re arguably right, it shouldn’t even be possible to write code that does this).