Can anyone help explain to me how I pass values in Nix?

I have been playing around with defining my homelab in a flake for a few days now. But I am struggling with the Nix language (my background is mostly in C# and Rust).

I got a basic flake working, but now I am trying to learn how to expand my system (generalize, DRY it up). and part of this is enabling default options that I can then override.

But I am utterly stumped as to how to pass those options along. The flake itself can be found here.

myConfig is intended to hold the options and I want to be able to use and adjust them per nixosConfiguration. But when I run nix flake check I always get the following error:

error:
       … while checking flake output 'nixosConfigurations'
         at /nix/store/f680dvnry4y9wvdmj40p4rwf7ndimmpm-source/flake.nix:38:5:
           37|   in {
           38|     nixosConfigurations = {
             |     ^
           39|       luna = nixpkgs.lib.nixosSystem {

       … while checking the NixOS configuration 'nixosConfigurations.luna'
         at /nix/store/f680dvnry4y9wvdmj40p4rwf7ndimmpm-source/flake.nix:39:7:
           38|     nixosConfigurations = {
           39|       luna = nixpkgs.lib.nixosSystem {
             |       ^
           40|         system = "x86_64-linux";

       (stack trace truncated; use '--show-trace' to show the full trace)

       error: attribute 'myConfig' missing
       at /nix/store/450afzqlzzgw6wnyc3dwysf3i5yxyqkr-source/lib/modules.nix:508:28:
          507|         builtins.addErrorContext (context name)
          508|           (args.${name} or config._module.args.${name})
             |                            ^
          509|       ) (lib.functionArgs f);

I have tried different incantations based on things I have gleemed from other people’s configs on github, but I can’t seem to figure it out. What am I missing?

Don’t try to do that in the flake.nix. That way madness lies - if you don’t fully understand the nix language/module system yet, you will never get it to work, and if you do fully understand them, you completely lose separation of concerns and your code becomes a mess.

The style of your config comes from people reading guides on how to use various third party flakes that just show the minimum viable solution with no regards to code quality. I’d recommend a completely different approach.

Rather than this mess:

        modules = [
          ./general
          ./luna/configuration.nix
          home-manager.nixosModules.home-manager
          ({pkgs, myConfig, ... }: {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.sharedModules = [
              inputs.sops-nix.homeManagerModules.sops
            ];
            home-manager.users.kekkon = import ./general/home.nix;

            # Optionally, use home-manager.extraSpecialArgs to pass
            # arguments to home.nix
            home-manager.extraSpecialArgs = myConfig // {
              extraProfile = ''
                export NIXOS_CONFIG=/home/kekkon/nix-flakes/flake.nix
              '';
            };
          })
          vscode-server.nixosModules.default
          ({config, pkgs, myConfig, ...}: {
            services.atuin.enable = myConfig.enableAtuin or false;
            services.vscode-server.enable = true;
          })
          disko.nixosModules.disko
          sops-nix.nixosModules.sops
          ({...}: {
            nixpkgs.config.allowUnfree = true;
          })
        ];

Split all of that out into your NixOS modules. I’d start by moving the module imports to the imports of general/default.nix. Pass the various flake inputs to the module system with specialArgs so you can do that.

Next, grab the little inline modules you wrote and just expand them into general/default.nix. Ultimately you’ll have something like this:

# general/default.nix
{ inputs, ... }: {
  imports = with inputs; [
    home-manager.nixosModules.home-manager
    vscode-server.nixosModules.default
    disko.nixosModules.disko
    sops-nix.nixosModules.sops
  ];

  nixpkgs.config.allowUnfree = true;

  # home-manager options
  home-manager.useGlobalPkgs = true;
  home-manager.useUserPackages = true;
  home-manager.sharedModules = [
    inputs.sops-nix.homeManagerModules.sops
  ];
  home-manager.users.kekkon = import ./general/home.nix;

  # Optionally, use home-manager.extraSpecialArgs to pass
  # arguments to home.nix
  # This obviously won't work, make it a home-manager module instead
  home-manager.extraSpecialArgs = myConfig // {
    extraProfile = ''
      export NIXOS_CONFIG=/home/kekkon/nix-flakes/flake.nix
    '';
  };

  # vscode options
  services.atuin.enable = myConfig.enableAtuin or false;
  services.vscode-server.enable = true;

  # whatever other options you had in here previously
}

Besides the obvious readability improvements, this also allows you to use the NixOS module systen for DRY purposes. Importing general/default.nix is now enough to enable all those options, you don’t need to repeat them for each nixosConfiguration in your flake.nix.

Break the little subsections into other modules and add them to the general imports as you see fit, I can totally see having a separate module for your home-manager config for example.


After that, if you still want to go with your custom options approach, learn how to write a module for that and write one. You almost have one, but you need to add it to your imports or modules, and I suspect learn a bit about how the imports, options, and config sections and such work.

That said, I’ve personally done that before and found it rather cumbersome in the long run. Learning to use the NixOS module system (and especially lib.mkForce and lib.mkDefault) resulted in much better code quality.

Nowadays I would just write a module that you only add to those configurations which should have that stuff enabled. So your nixosConfigurations eventually looks like this:

nixosConfigurations = {
  luna = nixpkgs.lib.nixosSystem {
    system = "x86_64-linux";
    modules = [
      ./general
      ./general/autin.nix
      ./luna/configuration.nix
    ];
    specialArgs = { inherit inputs; };
  };

  sol = nixpkgs.lib.nixosSystem {
    system = "x86_64-linux";
    modules = [
      ./general
      ./sol/configuration.nix
    ];
    specialArgs = { inherit inputs; };
  };

  titan = nixpkgs.lib.nixosSystem {
    system = "x86_64-linux";
    modules = [
      ./general
      ./general/zellij.nix
      ./titan/configuration.nix
    ];
    specialArgs = { inherit inputs; };
  };
};

I’d even go a step further and only import those modules in the imports of the respective configuration.nixes to make all that completely opaque at the flake.nix level.

8 Likes

That’s actually a much simpler approach. If the module is not included, the settings are not set… and I could use mkDefault and mkForce to override certain options in case of interdependencies then?

I’m gonna refactor and try it out now, but I’ll likely have questions later.

EDIT:

  1. Question: what if I want my user to use a different shell based on the device? Say on luna I want bash but on sol I want fish? I assume I’d have to use mkIf somehow, but how do I make my modules detect if bash or fish is enabled (or both)?
  2. Question: if multiple modules write to home-manager.users.kekkon.bash.profileExtra, how do I make them append instead of overwrite? Is there something like += in C#?

EDIT 2: For anyone else finding this that’s on the same journey as me. If you want to refer to config set elsewhere, accept config as a parameter for your module.

Example in my zellij.nix:

{ inputs, config, ... }:
let
  lib = inputs.nixpkgs.lib;
in
{
  config.home-manager.users.kekkon.programs = {
    zellij = {
      enable = true;
      enableBashIntegration = config.home-manager.users.kekkon.programs.bash.enable or false;
    };
  };

  config.services.openssh.extraConfig = lib.mkIf (config.services.openssh.enable) ''
    AcceptEnv ZELLIJ
    AcceptEnv ZELLIJ_*
  '';
}
1 Like

You can still refer to different modules from both, or if it’s only a specific device you want a different shell on you could try:

# sol/configuration.nix
programs.bash.enable = lib.mkForce false;
programs.fish.enable = true;

Assuming you refer to home-manager modules, this will also switch off all related configuration files.

You’ve already discovered the third option, which is to use config to special case on config.programs.bash.enable, and that makes sense whenever you have something else that depends on this setting.

By the way, you can pass your NixOS config to your home-manager modules if you want to depend on host settings: dotfiles/nixos-config/default.nix at d6dd373a4de4e0f33224883c690fa6536d57ab89 · TLATER/dotfiles · GitHub

Be careful with this, though, that arg will be unset if you ever attempt using home-manager standalone, in which case you will need to play with _module.args or such to fix that.

Since its type is type.lines the default is for the settings from multiple modules to be appended together.

If you don’t want such types to append, use lib.mkForce to mark a specific one as the canonical true value. This is often useful when you need to set a specific setting on one device, or override NixOS defaults.

Alternatively, you can use lib.mkOverride to set a specific priority if you actually want to merge certain modules or such.

1 Like

Oh, thanks for those dot files. I see you also use sops and that’s the next thing I’m butting my head against, so I’ll have a browse to see what I can learn.

I get the distinct feeling that there’s a lot about nix that one is expected to know, but isn’t very well documented for beginners. I often feel like I’m missing a cookbook style book that also explains WHY it’s this way. Then again, it’s a pretty new paradigm to me (closed I’ve come to this way of configuring things is playing around with Gentoo).

Incidentally: are you Dutch? If so: Hallo Noorderbuur! :smile:

EDIT 2: I’ve been thinking it over and if I understand correctly, for what I originally wanted: I would basically have to create a module that contains all my options and add that to config then import it everywhere and read those settings? But it would be a mess because it just creates another top level node in the dependency graph and you can already interconnect things, but I just don’t really know how yet?

I get the distinct feeling that there’s a lot about nix that one is expected to know, but isn’t very well documented for beginners. I often feel like I’m missing a cookbook style book that also explains WHY it’s this way. Then again, it’s a pretty new paradigm to me (closed I’ve come to this way of configuring things is playing around with Gentoo).

Part of the problem is that some people promote experimental features like flakes to beginners who have yet to learn the Nix expression language. The official documentation sensibly documents standard, and not experimental, features. So by starting with this flake you are choosing the most difficult possible way to get started.

You may want to read the Nix Pills series for a gentler introduction.

1 Like

I hadn’t come across nix pills yet. Thanks!

1 Like

Also, if you haven’t checked it out I recommend the Learn X in Y minutes mini-tutorial on the Nix language. It will help make sense of much of the documentation and examples:

https://learnxinyminutes.com/docs/nix/

1 Like

Besides the issue with experimental stuff (nix is under heavy development, so you’ll often butt your head against the slightly rougher edges), this has also simply been a bit of a historic issue with nix. Even when you stick to only the official docs, you’re going to have issues.

Respectfully, the nix pills are among the worst offenders when it comes to grokkability by newer users. They’re an excellent deep-dive into how stuff works, but as a new user that just serves to confuse.

They are cool, and reading especially the chapters on callPackage eventually is very useful once you start messing with packages, but not for new users, and they don’t really help with NixOS (I personally read a recommendation for them when I started and was very confused - though in fairness no other remotely passable docs existed back then).

The learnxiny stuff is cute, but it’s basically just a language cheat
sheet for things people IME don’t typically struggle with anyway (and teaches things that are rarely touched in a NixOS module, since it focuses on nix and not NixOS).


IMO the problem is more that of all the various tutorials, most focus on nix itself, i.e. the language and build system. Newbie users that struggle typically install NixOS as a cool distro to try out, but it is a completely different beast to nix and there’s very little to help understand it.

I’d suggest https://nix.dev as a starting ground, it both has language basics (that explain a bit more than the learnxiny thing, given it’s an actual community effort, though it lacks a cheat-sheet style summary) and various guides for package making. It’s also where the community is focusing documentation efforts right now. It still won’t help much with these NixOS questions, though.

There’s also the unofficial flakes book, which actually covers NixOS topics better than pretty much any traditional nix guides I’ve seen: Introduction to Flakes | NixOS & Flakes Book

Finally, the NixOS manual itself is of course mandatory reading. Note that there is separately the nixpkgs manual and the nix manual - and since NixOS uses both nixpkgs and nix you’ll often need to reference all three.

They’re large reference docs, but some sections of them are quite useful to new users.

But yeah, bottom line, most of what you’re looking for does not really exist. Personally, I learned by doing and reading nixpkgs source code.

1 Like

Yep, pretty much.

It wouldn’t be that bad, but you would have realized while using it that NixOS is designed quite well and just cleverly using the module system feels more natural than trying to shunt in variables everywhere. It’s hard to put it in words, and ultimately subjective since it’s a code style thing, but module composition means you don’t really need to create high level option variables to set in the first place. They’re treated more as base building blocks, i.e. ways to define services and such.

That’s not to say you should never use them, you could totally give it a shot for specific settings if you think it fits your use case better. Starting off with a sane NixOS configuration before you experiment is probably advisable though :wink:

Guilty as charged :slight_smile:

1 Like

You have both given me a lot of excellent reading, for which you have my thanks!

Since my system isn’t really holding any data yet, I am going to simplify my approach and restart without flakes and simplify my model somewhat.

Once I can get something working in “production”, I can then use the old hardware to start testing flakes and more advanced concepts.

UPDATE: I persisted anyhow, and partially because of your dotfiles @TLATER I managed to get nix flake check to pass again.

The hardest challenge was to make it not complain about the structure of the attrSet.

1 Like

This is wise. Learn the basics and then move on when you feel more confident. Enjoy the journey!