Good thing you’re posting in the “learn” category then
Let me just review your code and point out all the small things, we’ll fix the errors along the way.
Warning: Turns out there’s a lot of strange things happening here. Apologies for the wall of text, I’ll very happily answer more specific questions, too.
Let’s start with the flake.nix
:
{
description = "A very basic flake";
inputs =
{
nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
};
outputs = { nixpkgs, home-manager, ... }:
let
system = "x86_64-linux";
pkgs = import nixpkgs {
inherit system;
config = { allowUnfree = true; };
};
lib = nixpkgs.lib;
in
{
nixosConfigurations = {
generic = lib.nixosSystem {
inherit system;
modules = [
./configuration.nix
];
};
};
};
}
This looks mostly good, but there are a few things to explain. Firstly, the nixpkgs you use here will not have the allowUnfree
config option set. This is because you’re using nixpkgs.lib
, which refers to the un-doctored nixpkgs input, rather than the imported version of it.
To understand what I mean, let me explain what happens here:
pkgs = import nixpkgs {
inherit system;
config = { allowUnfree = true; };
};
lib = nixpkgs.lib;
In the first line, you tell nix to import the default.nix
file in the directory of nixpkgs
, which refers to the place where nix downloaded your nixpkgs
input to.
That is, you will be evaluating this file from the nixpkgs repository, and assigning the resulting value to the pkgs
variable.
After that, you tell nix to assign the lib
output from the nixpkgs
input to the lib
variable. That is, you take this line from the nixpkgs flake, and reassign it to lib
.
Later you then go on to use lib
to build your configuration, so pkgs
is actually completely unused, and your configuration doesn’t actually apply.
An easy fix is simply to use pkgs.lib
instead of nixpkgs.lib
. Any evaluation of nixpkgs
will always include the lib
attribute you’re used to.
With flakes, however, it’s better to avoid using import
if you can, because then we can make use of the flakes’ evaluation cache and don’t have to evaluate nixpkgs
every time. This is more important for flakes that will be used as dependencies in other flakes, but I like doing it in other flakes if I can get away with it easily.
In this case you probably can:
{
description = "A very basic flake";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-22.05";
};
outputs = {
nixpkgs,
home-manager,
...
}: let
system = "x86_64-linux";
in {
nixosConfigurations = {
generic = nixpkgs.lib.nixosSystem {
inherit system;
modules = [
./configuration.nix
# This is an inline module declaration, they work just like
# `configuration.nix` :)
({...}: {
# For bonus points, move this into configuration.nix
# instead of specifying it in your flake.nix
nixpkgs.config.allowUnfree = true;
# For bonus bonus points, use `allowUnfreePredicate`
})
];
};
};
};
}
If you really want to you can of course still do the lib = nixpkgs.lib
thing, but it’s simply renaming a short variable you only use once.
NixOS has a nixpkgs.config
option, and you can simply put the configuration in there, instead of importing nixpkgs
and modifying its configuration there. This avoids evaluating nixpkgs
multiple times 
This only works for the nixpkgs
whose lib
you use for nixosSystem
, so if you have two different nixpkgs
inputs you will probably need to change the configuration of that one the way you did. But you would use that quite differently; feel free to come back if you ever need a hand figuring it out.
On to your configuration.nix
:
pkgs.symlinkJoin {
name = "netboot";
paths = with bootSystem.config.system.build; [
netbootRamdisk
kernel
netbootIpxeScript
];
preferLocalBuild = true;
}
Surprise! This is not a NixOS configuration at all, you’re writing a derivation that builds a NixOS configuration.
Is this your intention? If not, shout, but for now I’ll assume you’re using this as an ad-hoc “install CD” where you just dd those files into a file system somehow and it actually boots with the NixOS system you configured, and that you then want to use nixos-rebuild
with this config to update.
I’ll be frank; I have not yet experimented with this kind of setup, so I don’t know if this will work. If I was going to do this I would probably use nixos-generators instead.
But I think my advice in this case would be as follows:
- Split out the NixOS configuration as an actual
nixosConfiguration
output.
- This is useful because you can then use the NixOS ecosystem as you usually would, and for example test your system with
nixos-rebuild build-vm
.
- Make a second
packages
output that does this symlinkJoin
on the nixosConfiguration
output.
- Build the package instead of the NixOS configuration when you want your image, and build the NixOS configuration if you want to update to it on the running host.
Let me explain.
Firstly, nixosConfiguration
doesn’t just take an arbitrary nix expression and magically turn it into a configuration you can build with nixos-rebuild
. In your flake.nix
, you use the lib.nixosSystem
function to build a NixOS configuration. This sets up the module system and makes the build actually work, and creates a derivation that consists of the activation script for your config.
In the modules
list, you are supposed to give it a list of NixOS modules (a set of functions taking config, lib, pkgs, ...
and returning a set of attrs that match up to configuration options), so that the function can glue them together into a configuration. What you are giving it is a derivation, i.e. instructions on how to build a store path; This cannot work, the types don’t match.
So instead, you probably want to make configuration.nix
contain only the stuff you assign to configuration
in your bootSystem
attrset, something like this:
# configuration.nix
{ config, pkgs, lib, ... }: with lib; {
imports = [
<nixpkgs/nixos/modules/installer/netboot/netboot-minimal.nix>
];
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEKy/Uk6P2qaDtZJByQ+7i31lqUAw9xMDZ5LFEamIe6l"
];
## Some useful options for setting up a new system
services.getty.autologinUser = mkForce "root";
#<snip>
}
Once you have this, you can build your system with nixos-rebuild build --impure --flake .#generic
(we’ll get to impurity later), and it will output the files in result
. But it won’t be exactly like your current config.
Next, we need to create a new output in flake.nix
so that we can package up this system like you used to. Add a new packages
output as follows:
packages.${system}.netboot = nixpkgs.legacyPackages.${system}.symlinkJoin {
name = "netboot";
paths = with self.nixosConfigurations.generic.config.system.build.topLevel; [
netbootRamdisk
kernel
netbootIpxeScript
];
preferLocalBuild = true;
};
Note the reference to self
- you will need to add self
to the arguments of the outputs function. self
here refers to the flake itself, and you can refer to outputs of the flake like this - in this case the system we built with lib.nixosSystem
.
I’m not sure this will work - I don’t remember in detail how importing nixpkgs differs from using lib.nixosSystem
. But if it does work, you should be able to now build your system with nix build --impore .#netboot
.
Finally, on impurity. In your configuration.nix
you have a couple of statements like this one:
<nixpkgs/nixos/modules/installer/netboot/netboot-minimal.nix>
The <>
syntax means “look up the first element in this path in the $NIX_PATH
variable to see where it is on my disk, and replace it with that path”.
This is obviously impure, because a) it refers to a file outside of the flake directory and b) because it relies on an environment variable. Both of these things will only be present on the device you write the code on, so it’s not reproducible. Hence, using this syntax is practically forbidden in pure mode.
Instead, we need to use our flake inputs to identify where these files are, and pass down the information into our configuration. This is not impure, because the flake inputs have tracking information (specifically, checksums calculated and written to flake.lock
) to ensure that the inputs are always the same, and so that we know how to get them.
For modules that are part of nixpkgs, there is a slightly less well-known module input called modulesPath
. We can simply use it:
# configuration.nix
{ config, pkgs, lib, modulesPath, ... }: with lib; {
imports = [
(modulesPath + "/installer/netboot/netboot-minimal.nix")
];
users.users.root.openssh.authorizedKeys.keys = [
"ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIEKy/Uk6P2qaDtZJByQ+7i31lqUAw9xMDZ5LFEamIe6l"
];
## Some useful options for setting up a new system
services.getty.autologinUser = mkForce "root";
#<snip>
}
This will take the modulesPath
of the nixpkgs instance whose lib.nixosSystem
function we are using 
For inputs that aren’t that specific nixpkgs instance, we would need to pass them down through either extraSpecialArgs
, which are added to the module arguments, or an overlay, which are added to the pkgs
argument.
And that’s it, I think at this point everything should at least have the correct types.
Didn’t think I would have to explain this much when I saw the question, hope it’s not overwhelming. I’m also not sure it will actually work, you’re doing something pretty special.
Feel free to come back and complain if it doesn’t work.