Set up nixos + home manager via flake

Hello,
I’m sorry in advance if questions like mine have been asked before. I’m new to Nix and the Nix language (coming from GNU/Guix) and I’ve read through the documentation I could find, most notably:

  • NixOS manual
  • NixOS wiki
  • Home Manager manual

But I couldn’t quite find the answers I’m looking for. For my system configuration with nix (including managing the user directories) I have some prerequisites that are important to me:

  • I want to pin the channels I’m using to a certain commit, and update to the latest commit manually (As I understand flakes are perfect for that, so I began setting up one)
  • I want to manage my system configuration using nix (obviously) and my home configuration using home-manager
    • But I don’t want to seperate the two (standalone mode?) rather, when I run nixos-rebuild switch, it should apply changes in both configurations at once. If I understood correctly, this can be achieved if using home manager as a nix module
    • And even more importantly, I don’t want to separate my home- and system configuration (meaning have one file configuration.nix for system and a home.nix for home). I’d like to be able to have one file (later down the road I’d like to split the configuration in various smaller files for different purposes [programming.nix, games.nix, ...]) that contains both configuration for my system- and my home environment. I like this approach because that way, I can contain a feature into a single file. Let’s say setting up my WM. I’d involve both changes to the system and the home configuration

After researching a bit I started to look at some example configuration and took DAlperin’s nix config as a starting point. I used their flake.nix as a template and tried building from there.

You can find my config here. Now, as far as I can tell, this is working. Taking a look at modules/common.nix it contains both system- and home relevant configuration. I guess my question is, is this the correct way of doing it? If I take a look at the relevant parts of the home manager manual regarding this, the line

home-manager.users.jdoe = import ./home.nix;

bugs me, because this is precisely what I’m trying to avoid (having a separate config file for home). So I left it out. However, I can already see some flaw in my config. In modules/common.nix I hardcode home-manager.users.db. Later down the line I plan on having having different files for different users and assigning them to machines in flake.nix, so the way I’m doing it at the moment wouldn’t work anymore. Besides that, I haven’t tried adding another file to my setup yet (let’s say modules/shell.nix, wouldn’t another declaration of

home-manager.users.db = { pkgs, ... }: {
  # ...
}

overwrite the first one?

I’m sorry for the wall of text. At the moment, my setup seems to be working. I can rebuild my system using sudo nixos-rebuild --flake .# and both system- and home configurations get applied. However before moving forward I’d thought about taking a step back and asking for advice on whether this approach is the correct one.

Thanks in advance!

You can just replace import ./home.nix with the verbatim contents of home.nix.

Feel free to mix home-manager configuration with NixOS configuration. I’ll offer my own flake as an example if you’re curious. Look at my i3 file for one where I’m including both NixOS and home-manager configuration.

You can rebuild both NixOS and home-manager together using the nixosConfiguration output, and I can also rebuild just the homeConfiguration part on its own (by extracting the home-manager object out of my nixosConfiguration). It’s the best of both worlds!

The other issue you mentioned was having different configurations for different users or machines. There are a couple of strategies for how to do this:

  1. Each machine can choose to import different Nix files. So you could have common Nix files shared between all machines, and then other files that are specific to certain hosts or classes of hosts.

  2. You can define options for your own configuration, and have each host declare the value they choose for that option. Then define your settings referencing the option instead of hardcoding them.

  3. You can set default settings with lib.mkDefault as one value and then overwrite them in a specific host’s configuration (or use lib.mkForce from your host config).

  4. You can use functions or conditionals to define your settings dynamically based on attributes of the host. It’s a programming language, so it’s as flexible as you want to make it!

In my flake, I use a combination of all of these. They can all be useful in different ways but everyone defines their flake how they like. There isn’t exactly a “standard” way to lay everything out that fits everyone’s needs, so do what makes sense to you.

3 Likes

Feel free to mix home-manager configuration with NixOS configuration. I’ll offer my own flake as an example if you’re curious. Look at my i3 file for one where I’m including both NixOS and home-manager configuration.

Your configuration is awesome! Already learning a bunch just reading through it! I’ll dive in and see where it leads me, thanks so far!

What I don’t quite understand is how you set your config.user by just looking at your config. I understand you define a global record globals which contains your user. Your machine config, inherits it, and then passes it in the modules array to nixpkgs.lib.nixosSystem. Looking at this function, it seems to be responsible for building up the config object. I don’t yet quite understands how it wires the record you passed as a module to the resulting system user. But maybe that’s just nix magic I need not to understand?

In any case, many things are clearer now, thanks again! I’m sure more problems will come up in the upcoming days and weeks.

I’ve tried get a simple configuration working using your config as a base. You can see it here.

Unfortunately, I can’t build it, I get the error:

error: The option `fullName' does not exist. Definition values:
- In `/nix/store/.../flake.nix': "Demis Balbach"

If I comment out fullName I get the same error with user. I’ve looked at the file for quite some time now, and can’t find a syntax error. I’d appreciate any help. Thanks in advance.

Hey, glad you liked it! Also, it’s good to have feedback on this repo so I know how to improve the documentation. Your issue totally makes sense, and I think I can help set you straight. (TL;DR: here is where I set those options.)

I’m not an expert so I might be explaining some of the terminology incorrectly, but here goes. You’re right that the way that the config object is build is a bit of “nix magic”. Part of it has to do with NixOS modules and the laziness of the language.

Two things to understand. The first is the magic that occurs between the nixpkgs.lib.nixosSystem function and module definitions. The nixos-rebuild command is going to use the nixosSystem function to pass arguments to your modules that you didn’t define — this includes pkgs, lib, config — then use those modules to construct a NixOS system.

Every NixOS module is allowed to declare three things: imports, options, and config. If you don’t specify any of these, then NixOS just assumes you’re defining just a config as a shorthand.

Imports refer to other files which are themselves modules.

Options declare what are essentially variables that you expect to be fulfilled somewhere in the config. The important thing: these become part of the config object and can be referenced with config.optionsName inside any module.

Config defines what the options do, by setting the values of options, both yours and the built-in ones. Basically, NixOS is defining options all the way down (until it builds a derivation and activates it). Yes, you can refer to config while inside the config block!

During nixos-rebuild, the module system essentially grabs every module declared in your nixosSystem and smashes them together (lazily). The special arguments are passed to the modules, all the imports are pulled in recursively, options are merged together, and so are the configs. So one config expression can set an option which is used by another config expression and so on.

This means:

  1. That you can declare an option in any module, as long as it’s eventually defined at some point somewhere (and as long as you avoid infinite recursion). It’s super powerful, but large repos can be confusing if you have to jump around to find the definitions.

  2. You don’t have all of the capabilities in your flake.nix file and in the nixosSystem function that you do in the imported modules files because those contain NixOS “magic”. For example, you don’t have access to pkgs, nor do you have access to the config object.

4 Likes

Just had my first successful nixos-rebuild thanks to your input!

Thanks so much for your explanations, they’ve helped a lot. Regarding the options, this is still a bit blurry to me. I understand that mkOption exposes a variable to the config. So user = mkOption {...} allows me to use config.user. However I’m still not quite certain how that option gets its initial value from globals.user, but I’m fine with not understanding it at the moment, I’m sure it’ll come in time.

I’m excited to build up my config now. Thanks again!

EDIT: Unfortunately, I was quick to run into a problem I can’t seem to fix. I cannot seem to access config properly inside home-manager.users.${config.user}.

If you take a look at my common module (line 67-68), if I uncomment either of those lines, I get the error:

error: attribute 'homePath' missing

In case of uncommenting the first line, and

error: attribute 'dataHome' missing

in case of the second line. So something about how I access properties inside config is wrong. I’ve looked through your config and noticed that you do the same thing. Not quite sure what I’m doing wrong here. Also, after looking at the source code of the xdg module, my understanding is that config.xdg.dataHome should be set after doing xdg.enable = true because of this:

## ...
 in mkIf cfg.enable {
      xdg.cacheHome = mkDefault defaultCacheHome;
      xdg.configHome = mkDefault defaultConfigHome;
      xdg.dataHome = mkDefault defaultDataHome;
      xdg.stateHome = mkDefault defaultStateHome;
## ...

But apparently it’s not.

EDIT: Actually line 68 of my common module, e.g. #xdg.dataHome = "${config.xdg.dataHome}/.local/share"; doesn’t make sense anyway, but you can look here to see where I tried to access config.xdg.dataHome. It’ll also fail with

error: attribute 'dataHome' missing

I think you’re really close! Let me help clarify a couple of things:

  • Here is where I’m declaring the homePath option. It’s a similar issue to the one you had before, but you’re getting a different error because this time the missing option is being referenced here instead of attempting to be set.

  • You can’t access the option xdg.dataHome because it was never declared by either you or the built-in NixOS options. You can search for it in the NixOS options list and see it’s not there. However, it is in home-manager, as you would expect. To reference an option in home-manager, you have to use the entire object chain: config.home-manager.users.${config.user}.xdg.dataHome.

  • Finally, how your flake fills out the user option:

    • The user is in the globals attrset, which is passed to your slimboy.nix file expression as an argument.
    • The globals attrset is then passed as part of the modules argument in the nixosSystem function, which takes a list of either attrsets or paths to files containing NixOS modules.
    • If the attrset is not a function, NixOS simply assigns the key-value pairs of the attrset to the corresponding options. However, there’s nothing stopping you from explicitly assigning each attribute to each option. (I see you already figured out how to create in-line modules without splitting up into other files).
1 Like

You’ve been such a tremendous help, you can’t imagine. Thanks again for taking the time to explain a couple of things. It’s only been like 3-4 days into my Nix journey but I’ve already learned so much.

I’ve been hard at work getting some basic modules implemented. I’ve had a couple of issues but was able to resolve them on my own thanks to your previous input. Of course, they’re all barebones at the moment. Individual configuration will come later. I was just keen on getting the structure done in a way I like it.

What you taught me about options has become a key feature of my config. Looking at your config and researching it a bit, I’ve been able to design my modules quite modular. I’ve defined some options that some features will overwrite and others will read, for example config.os.wayland determines if wayland is running on my system. My sway module will set that value to true. Then I have another option that sets the default terminal based on config.os.wayland. Then again, in my alacritty module I overwrite that option and the sway module will just use whatever terminal is defined. So either foot or alacritty, or any other terminal I might add a module for in the future.

This gives me the freedom to mix and match whichever components I see fit. I really like this approach!

1 Like

Glad I could help, seems like you’ve already got a great approach going. At this rate, I’ll probably be stealing ideas from you soon!

Hello,

Up until now, whenever any module of mine needs to expose options to the outside world, I’ve used lib.mkOption inside the options object. The way I understand it, an option is best used if said option can be changed from anywhere.

But I also use it for constants a module declares as of now. This works, but the boilerplate is pretty cumbersome, like

someOption = lib.mkOption {
  type = ...;
  description = ...;
  default = ...;
};

I’d like for some modules to declare consts that can be read from anywhere throughout my configuration, but obviously never changed. I tried putting someConst = "foo"; inside the options object but that doesn’t work.

I guess I could just define them in flake.nix, possible inside the globals rec. But I don’t really do that. Preferably, some module deep down the tree exposed these consts (because they are inherent to the module itself - let’s say provider settings for a certain mail account), but they would not only be visible to this module, but to everything so that I can use them somewhere else. Is this possible?

Hopefully I understand this correctly: you want your modules to declare constant values, which are then exposed to the rest of your config (e.g. as options) without the possibility of being altered.

Unfortunately, I’m not sure of a way to prevent an option from being modified in another module. All of the modules are essentially the same file when merged together. That said, you could declare lib.mkForce "my value" in order to increase the priority of your value. The highest priority (closest to 0) value of any option will be chosen among two that are used; only equal priorities will attempt to be merged.


Regarding the boilerplate problem, some suggestions:

You could create the option and leave out most of the boilerplate. I believe type exists to enforce type adherence, but it’s not required:

someOption = lib.mkOption { default = "myconstantvalue"; };

You can even define a wrapper function to shorten it slightly:

mkConst = const: (lib.mkOption { default = const; });
someOption = mkConst "myconstantvalue";

You could also create a submodule option, which is an option that contains any number of possible sub-option instances (you see in this the NixOS search all the time). You only need to declare it once; maybe something like this:

constant = lib.mkOption {
  default = {};
  description = "A constant to declare";
  type = lib.types.attrsOf (lib.types.submodule {
    options = {
      value = lib.mkOption { description = "Constant value."; };
    };
  });
};

Then you can throw them all over your config instead of declaring an individual option for everything:

config = {
  constant.hello.value = "world";
  constant.requiredVersion.value = lib.mkForce "23.05";

  system.stateVersion = config.constant.requiredVersion.value;
};
1 Like

Thanks again for your input. Thinking about it, it wasn’t really about declaring a const, but more about removing boilerplate, so in the end I took the mkConst approach, with that I can declare an option in one line.

I hope I can as you one more question. I’ve recently began to set up my mail accounts (for now primary and work). I’m far from done. I want to use notmuch for indexing emails. I plan on using it’s hooks quite extensively.

I know that I can declare hooks with programs.notmuch.hooks, however, I have a set of shellscripts/commands for each email account I’d like to add. So for example, the preNew hook for my primary email should be:

programs.notmuch.hooks.preNew = ''
  notmuch tag +primary -- path:primary/** and tag:new
'';

For the work email it should be the same, just replacing primary with work in that command. I know that I can add this snippet of code to each of my mail modules, and it will work. However I don’t want to write all these commands twice, or maybe more than that if I decide to add another mail account.

So I’d like to abtract it. My first thought was to iterate over the attribute keys inside home-manager.users.${config.user}.accounts.email.accounts, because each key will be one account. I’ve looked through attribute set functions but I couldn’t find any that would allow me to do that.

In plain JS, I’d do something like

Object.keys(home-manager.users.${config.user}.accounts.email.accounts).reduce((acc, key) => acc += `\nnotmuch tag +${key} -- path:${key}/** and tag:new`, "")

Maybe not the prettiest thing in the work but it should do the trick. However I couldn’t find a nix equivalent of Object.keys (foldr seems to be good for reduce).

After that, I thought a simple function that takes an argument and splits out the string with the replaced values is probably easiest. Something like

mkPreHook = box: (''
  notmuch tag +${box} -- path:${box}/** and tag:new
'');

And then use it in each mailbox module like so

programs.notmuch.hooks.preNew = mkPreHook "primary";

But I couldn’t get it to work at all, I’d run into

error: value is a function while a set was expected

I had it declared in

config = let
## here
in {
 ## ...
 ## use it here
}

I don’t know if this even is the correct approach. But if it is, I guess I’m looking for a way to declare a ‘global’ function somewhere, and then use it in the mail modules, give it the name of the respective mailbox and then let it return the appropiate commands so I can use them in the hooks.

I’m not sure why your mkPreHook function didn’t work… to the degree that I copied it on my own machine and tried it out. Mine seemed to build without a problem.

My only guess would be that your bad build is somehow caching poorly, or maybe something was staged vs. committed in your flake and the behavior isn’t what you think it was. I double-checked my own build and everything, but I can’t reproduce the problem!

For your Object.keys question, the function you want is builtins.attrNames. See here, but I have no idea how I found it. Obviously, the docs / SEO is a problem.

I think your approach should work fine. Obviously, you have to decide how much abstraction you want to create for yourself. Perhaps you can look through the home-manager options code, as it may also be looping through different accounts to setup centralized mail configuration.