Import but don’t import your NixOS modules

I’m surprised how common this was; I wasn’t thinking much about when I import but now I have to rethink it a lot for NixOS modules.

6 Likes

Yeah, avoid import in the module system, you can basically always code around it.
However, you can fix the locations by using the (undocumented) _file attribute.

Example flake.nix:
{
  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs?ref=nixos-unstable";
  };

  outputs =
    { nixpkgs, ... }:
    {
      nixosConfigurations.test = nixpkgs.lib.nixosSystem {
        modules = [
          (
            { modulesPath, ... }:
            {
              imports = [ "${modulesPath}/profiles/minimal.nix" ];

              boot.loader.grub.enable = false;
              fileSystems."/".device = "nodev";
              nixpkgs.hostPlatform = "x86_64-linux";
              system.stateVersion = "24.05";
            }
          )
          (
            { options, lib, ... }:
            {
              _file = ./fake-file.nix;
              options.test1 = lib.mkEnableOption "";
            }
          )
          (
            { options, lib, ... }:
            {
              _file = "I can put anything here";
              options.test2 = lib.mkEnableOption "";
            }
          )
        ];
      };
    };
}
$ nix eval .#nixosConfigurations.test.options.test1.definitionsWithLocations
[ { file = "/nix/store/qq60cq5hpisslzbif7s43x1x77wq4f3j-source/fake-file.nix"; value = false; } ]

$ nix eval .#nixosConfigurations.test.options.test2.definitionsWithLocations
[ { file = "I can put anything here"; value = false; } ]
1 Like

Neat to learn about that attribute but Fix it in what sense when it’s used with import ./file ?

The import keyword simply takes the contents of another file and drops it in-place where you import-ed, which is why using import messes up location info.
Using your example, if you set something like _file = ./greet.nix; in your greet.nix you would get the correct location information.
It’s not a pattern I’d recommend often, but it’s good to know about, especially as location information is also used in error messages.

3 Likes

On the same topic of not using import in NixOS configuration, one bad example is from Home Manager documentation.

{
  description = "NixOS configuration";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    home-manager.url = "github:nix-community/home-manager";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = inputs@{ nixpkgs, home-manager, ... }: {
    nixosConfigurations = {
      hostname = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./configuration.nix
          home-manager.nixosModules.home-manager
          {
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users.jdoe = import ./home.nix;

            # Optionally, use home-manager.extraSpecialArgs to pass
            # arguments to home.nix
          }
        ];
      };
    };
  };
}

The first problem is about having an attribute set within a list of modules which would cause it to be unnamed. Thus the attribute set can be move to a new file.

The second problem is about having jdoe = import ./home.nix; which will have the same problem with the location of definitions, as the definitions would be listed as coming from the location of the importing module. In this case we can cheat as jdoe is a submodule option (i-e a module), and submodules like modules can have an imports attribute to list all modules to be imported. As the Home Manager module extends the users.<name> NixOS option with Home Manager options, we can list the Home Manager module in the imports list of the user.

Thus the previous example should probably be rewritten as:

flake.nix :

{
  description = "NixOS configuration";

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
    home-manager.url = "github:nix-community/home-manager";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
  };

  outputs = inputs@{ nixpkgs, home-manager, ... }: {
    nixosConfigurations = {
      hostname = nixpkgs.lib.nixosSystem {
        system = "x86_64-linux";
        modules = [
          ./configuration.nix
          home-manager.nixosModules.home-manager
          ./nixos-jdoe-module.nix
        ];
      };
    };
  };
}

nixos-jdoe-module.nix :

{
  home-manager.useGlobalPkgs = true;
  home-manager.useUserPackages = true;

  # Import the ./home.nix Home Manager module in the users.<name> submodule.
  home-manager.users.jdoe.imports = [ ./home.nix ];

  # Optionally, use home-manager.extraSpecialArgs to pass
  # arguments to home.nix
}
1 Like

Love it; I bet you’d get a lot of the win though just changing the one line in the documentation to:

home-manager.users.jdoe.imports = [ ./home.nix; ]

(While keeping the embedded module)

Should we open a PR?

I wonder if this attribute can be automatically set on import
that would make the UX much simpler.

No, it can’t. Builtins should never change their behaviour between nix versions (except for experimental ones like fetchTree, etc.) You can of course fork nix and try to initiate that behaviour yourself, but using a nix fork means the result of eval will be different from everyone else. The other option would be a wrapper function, which would have to handle some funny edge cases.

The best option is just, as you realised, don’t use import in the module system.