Adding doom emacs using home manager

I am trying to add doom emacs by following this guide, but I run into an error when trying to replicate it. How can I fix my error?

Error:

error: undefined variable 'nix-doom-emacs'

       at /nix/store/kffdji84k8ad8b4dhhm3x1ksqgsmpr4z-source/home-manager/default.nix:15:5:

           14|   imports = [
           15|     nix-doom-emacs.hmModule
             |     ^
           16|     ./alacritty.nix
(use '--show-trace' to show detailed location information)

Flake.nix:

{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
    nix-doom-emacs = {
      url = "github:nix-community/nix-doom-emacs";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };
  outputs = { self, nixpkgs, home-manager, nix-doom-emacs, ... }:
  let
    name = "heremoh";
  in {
    nixosConfigurations = {
      host1 = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./hosts/host1/configuration.nix
          home-manager.nixosModules.home-manager
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users.${name} = import ./home-manager;
          }
        ];
      };
      host2 = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./hosts/host2/configuration.nix
          home-manager.nixosModules.home-manager
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users.${name} = import ./home-manager;
          }

        ];
      };
      nonNixOS = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          home-manager.nixosModules.home-manager
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users.${name} = import ./home-manager;
          }
        ];
      };
    };
  };
}

home-manager/default.nix

args@{ pkgs, ... }:
let
  nixos-system = args ? "nixosConfig";
  xinitrc_source = "exec dwm";
in {
  imports = [
    nix-doom-emacs.hmModule
  ];

  home.stateVersion = "22.11";

  programs.home-manager.enable = true;

  home.packages = with pkgs; [
    fd
  ] ++ (if nixos-system then [
    firefox
  ] else []);

  programs.doom-emacs = {
    enable = true;
    doomPrivateDir = ./doom.d;
  };

  home.file = {
    ".config/nix/nix.conf".text                       = "experimental-features = nix-command flakes";
  } // (if nixos-system then {
    ".xinitrc".text                                   = xinitrc_source;
  } else {});
}

Flake inputs aren’t magically transported to your modules. You need to shove them into the module system - and apply any overlays and import modules - by hand. This has become one of my pet peeves, actually, it’d be pretty easy to change nixpkgs.lib.nixosSystem to integrate with flakes much better. One day I’ll sit down and write some kind of library or RFC for it.

Anyway, in the mean time, you have two options; do everything in flake.nix where your inputs are still in scope (you’re currently partially doing this, but it would be awkward to find a way to get doom-emacs in there), or pass your inputs into the module arguments. Documentation often recommends the former because it’s easier to show as an example flake.nix, but I’ve come to much prefer the latter - and it would be much easier in examples if we fixed nixosSystem, too.

If you’re anything like me when I first started learning about flakes, you have no idea what I’m talking about though, so let me start by explaining module arguments:


When you write something like this:

args@{ pkgs, ... }:

You’re actually defining a function. You’re telling nix that your function takes the argument pkgs, and that it should not fail if any other arguments are given, but put them all as attributes in a variable called args instead.

I.e., in this function, args.pkgs will exist and be equivalent to pkgs. There may also be other attributes in args (if this function is evaluated as a NixOS module, you’ll for example have args.lib).

So if this is a function, how do we actually get anything into the pkgs variable? You’re not actually calling it anywhere yourself, so me calling this a function definition will be very abstract to you.

When you execute nixpkgs.lib.nixosSystem, nix will take any “modules” (e.g. function declarations like that one), and give them all the arguments defined in _module.args, run the functions, take the resulting attribute sets, merge them all together, and finally do stuff™ (build all kinds of files and eventually an “activation” script, which nixos-rebuild then typically executes) with the resulting definitions.

The arguments are as defined in that doc (still sad it’s invisible by default now), and by default include pkgs, lib, config, etc. - the things people usually access in their modules.

This is ultimately what NixOS is, and how it is a distinct project from nix. The cool thing about flakes is that it doesn’t hide all this stuff behind magic channels, so you can actually see it!

The downside is that it has (IMO) not yet been suitably integrated with flakes, so you need to understand it to know how to get your flake inputs into this system.

By the way, home-manager works exactly like this as well, it’s just a different function. This is important, because home-manager has its own, separate, equivalent of module._args.


Now for actually solving your problem.

If you’ve read the code I linked, you’ll have seen that both nixosSystem and home-manager’s module accept a variant of (extra)?specialArgs, which you can use to add additional arguments.

I’d recommend you do exactly that - add your flake inputs as additional arguments. You do that like so (just for your first host, repeat for the others):

outputs = { self, nixpkgs, home-manager, nix-doom-emacs, ... } @ inputs:
  let
    name = "heremoh";
  in {
    nixosConfigurations = {
      host1 = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./hosts/host1/configuration.nix
          home-manager.nixosModules.home-manager
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users.${name} = import ./home-manager;
            home-manager.extraSpecialArgs.flake-inputs = inputs;
          }
        ];
        specialArgs.flake-inputs = inputs;
      };

I’m using the @ inputs syntax here to put all your inputs in a variable, and then assign it to specialArgs, as well as home-manager’s extraSpecialArgs, which will now make an argument called flake-inputs available in all your NixOS and home-manager modules.

Now you can very easily grab your doom-emacs input in your home-manager config:

args@{ pkgs, flake-inputs, ... }:
let
  nixos-system = args ? "nixosConfig";
  xinitrc_source = "exec dwm";
in {
  imports = [
    flake-inputs.nix-doom-emacs.hmModule
  ];

And it should all work!

You can also now put the importing of the home-manager module in an imports declaration somewhere in your configuration.nix, and define the home-manager configuration options in there.

If you understand the module system, with some clever coding you can really refactor that flake.nix and make it much nicer. See mine for some inspiration if you like: dotfiles/flake.nix at fd7871d24def13447163d78aa4edb119ec266714 · TLATER/dotfiles · GitHub


On an unrelated note, this is exactly what I’d propose. Make flake-inputs a top-level argument of nixosSystem, and automatically import any modules defined in those flake inputs, apply all overlays, and make flake-inputs available as a module argument.

It’d make for much cleaner usage examples in third party repos and save us all a lot of typing.

8 Likes

Wow! Thank you for what is likely the best answer I have ever gotten. You not only helped me solve my problem, but even taught me stuff I really wanted to know, but was not able to ask about. Have a great day!

2 Likes

I second that. It takes a bunch of stuff that I am painfully aware of not having had the time to figure out properly for myself and joins a whole bunch of loose ends in a coherent, clear and pretty concise description. This is very useful. It’s a gem that is hidden from view, except to the minority that stumbles across it. Thank you so much @TLATER !

And this really struck a chord:

Spot on!