{
"$type": "site.standard.document",
"canonicalUrl": "https://isabelroses.com/blog/custom-lib-nixossystem",
"coverImage": {
"$type": "blob",
"ref": {
"$link": "bafkreigyfzf7bptef7gffrldlisk7x7eaywy5oabardpjow7k5kepankie"
},
"mimeType": "image/png",
"size": 11126
},
"description": "How I came to write my own lib.nixosSystem",
"path": "/blog/custom-lib-nixossystem",
"publishedAt": "2025-02-07T00:00:00.000Z",
"site": "at://did:plc:qxichs7jsycphrsmbujwqbfb/site.standard.publication/3mn5hmwkcmb22",
"tags": [
"learning",
"nix"
],
"textContent": "Introduction\n\nI've been using NixOS for a while now, and my biggest issue was that I have a\nlot of machines. Which leads to having a lot of different\nsystems on my\nflake because of that hardware. The\nnormal solution would be to use the module system, but a new issue arises when\nwe add nix-darwin. We suddenly start to\nfail\neval over issues because some modules don't exist that are in the normal NixOS\nmodule system. So my new issue is that I can no longer use my small abstraction\nover\nlib.nixosSystem.\nI would have to expand the abstraction to include\nlib.darwinSystem\nsince I can no longer unconditionally import all modules I use if they don't\nexist in nix-darwin? But what if I don't want to do that? What if I want to\nwrite my own lib.nixosSystem or my own lib.darwinSystem? What if I call it\nmkSystem.\nWell that's exactly what I did. And this article covers how I got to that point\nand how my custom “builder” later evolved into\neasy-hosts.\n\nThe research\n\nTo start we should read the documentation for lib.evalModules.\nFrom which we find that there are 4 arguments, but 3 that we really care about.\nThose being modules, specialArgs and class. The modules argument is a\nlist of modules, which are files, functions or attrsets that will be merged and\nthen evaluated. The specialArgs argument is a attrset of arguments that are\npassed to the modules, but are not evaluated with the file structure in mind.\nThe class argument is a nominal type that ensures that only compatible\nmodules are imported. This is a very important argument, as it allows us to\nhave different modules for different systems.\n\nHowever, I'm not much a fan of reading documentation. So instead lets dig into\nthe source code and pick it apart. To find out what we need to get our custom\nlib.nixosSystem working. To do this I identified 4 main files in the nixpkgs\nrepository:\n\n- flake.nix\n- nixos/lib/eval-config.nix\n- nixos/lib/eval-config-minimal.nix\n- lib/modules.nix\n\nThese files may seem somewhat arbitrary, but they are listed in the order they\nare called. The flake.nix file has our lib.nixosSystem function that calls\nour nixos/lib/eval-config.nix file which is a light wrapper around our final\nlib/modules.nix. So lets walk through those files in order and see what\nexactly they do.\n\nflake.nix\n\nThis file contains our lib.nixosSystem function, which takes args as an\nargument. The\ndocumentation\nlists a set of known arguments being modules, specialArgs and\nmodulesLocation, it also specifies some additional legacy arguments system\nand pkgs (both of which are now redundant).\n\nThe lib.nixosSystem then imports the nixos/lib/eval-config.nix file whilst\npassing lib, and the remaining args to it. It also sets system to\nnull as well as adding nixpkgs.flake.source to nixpkgs output derivation.\n\nnixos/lib/eval-config.nix\n\nThis file immediately points us to the fact that it is a “light wrapper” around\nlib.evalModules. This file also has a large collection of arguments most of\nwhich will be the defaults. A good example of this is baseModules which\ndefaults to a list of modules from the nixpkgs repo. The most important\narguments from this file are specialArgs, lib and modules. For the\nmost part these come from the prior flake.nix file.\n\nAs we read down the file we notice that there are two additional modules that\nare going to be added. These are the pkgsModule and the modulesModule.\nThese appear to be pretty strange names at first, but the pkgsModule will\nset nixpkgs.system if system was not null, and will set\nnixpkgs.pkgs if pkgs is not null. The modulesModule will add\nconfig._module.args as an attrset of noUserModules, baseModules,\nextraModules and modules. So now we know some of the arguments that are\ngiven to lib.evalModules lets see what that does.\n\nnixos/lib/eval-config-minimal.nix\n\nThis file is a small wrapper upon lib.evalModules, but it gives us a little\nbit of guidance on how to use the class argument. As well as showing us the\ndefault for modulePath which is going to be passed as a special arg.\n\nlib/modules.nix\n\nThis file won't add much to our understanding of lib.evalModules, but it\nstill is the definition of the function, so it's good to keep in mind if we have\nany issues later down the line.\n\nThe implementation\n\nNow that we have our key inputs of class, modules and specialArgs we can\nstart implementing our own lib.nixosSystem.\n\nGetting the basics\n\nLet us start in our very own flake.nix by writing the following code. This\nwill give us a basic template to work with and you, the reader, knows how to\nstart.\n\nNow that we have a very bare bone flake.nix we can get started on the\nmkSystem function. Let's also create the mksystem.nix file. We are going to\nadd some basic args that we know we are going to need later such as modules,\nspecialArgs and class. And we are going to add some defaults to these args\nsuch that we don't get any errors if they are not provided.\n\nAdding modulesPath\n\nNow that we have our basic template down. Let's start by adding the\nmodulesPath, most people probably recognize this from when they first\ninstalled nix and read their hardware-configuration.nix file and saw\nsomething along the lines of modulesPath + /installer/scan/not-detected.nix.\nThat is why we are starting with this, so that our hardware-configuration.nix\nwill work.\n\nThe key to this is going to be including modulesPath in our specialArgs.\nThis is because specialArgs should only be used with arguments that need to\nbe evaluated when resolving module structure.\n\nSo close and yet so far\n\nOnly adding modulePath is a bit useless however, so we can't exactly replace\nour lib.nixosSystem's yet. So let's work on that. We are going to start by\nimporting baseModules, this will provide us with a base set of modules that\nprovide abstractions over configuring your system. We can use the modulePath\nand get the module list.\n\nIt actually works?\n\nWe now have a _mostly_ functional replacement. Depending on your configuration\nmay actually work as it is now! To keep progressing we are going to have to go\nback to the modulesModule from earlier. we need this such that some nixpkgs\nmodules will work, one of these is the documentation module\nwhich will be a hard module to ignore, when so many people use it.\n\nTo fix this we are going to introduce a new module which contains\nconfig._module.args and takes a set of attrs that will be passed to each\nmodule. I'm sure most of you recognize these when writing a module and adding\nsomething like { pkgs, config, ... } to the top of a file. The\nconfig._module.args option should be used when trying to pass arguments to\nall modules, but should not be evaluated with file structure in mind.\n\nAdding some of our own modules\n\nEven better, now we have completely replaced lib.nixosSystem with our own\nmkSystem function. But let's be real. That's not enough for us. We should\nstart abstracting some common themes between our systems. Some big examples of\nthis are networking.hostName and nixpkgs.hostPlatform. And while were at it\nlets also re-add the nixpkgs.flake.source from the original lib.nixoSystem,\nas well as adding inputs as a special arg. As most people do this anyway,\nI think it's a safe assumption we should add it. For further reading about\npassing inputs to modules check nobbz's blog on getting inputs to flake modules.\n\nI just added name as an additional argument to our mkSystem function. This\nallows us to set the hostname of our system. The way I opted to write it allows\nfor us to use mapAttrs on our nixosConfigurations. This will mean that we\nneed to change how the original flake.nix works though.\n\nFurthermore, notice how I lied about settings nixpkgs.hostPlatform. If your curious why, maybe you\nshould read my last blog post about it. (Shameless plug)\n\nDarwin compatibility\n\nOne of the main reasons I wanted to make this was to support\nlib.darwinSystem. So let's address that now.\n\nTo introduce Darwin support we are allowing users to set the class\nargument to darwin from there we can determine what modules to import.\nAs a result of this you may notice that Darwin has a different set of\nmodules which introduced some new options to set for this system type. This\nincludes nixpkgs.source and darwinVersionSuffix and darwinRevision. Some\nof these are for commands like darwin-version.\n\nYou may also notice that we had to add system = eval.config.system.build.toplevel\nback into the final eval produced by our Darwin eval. This is required to swap\nto the configuration, otherwise it won't work at all. This is because the\nsystem output is used by darwin-rebuild, to identify the final build\noutput.\n\nThe final touch\n\nThe final and maybe the best bit is adding inputs'. For those who are unaware\nof flake-parts, you probably are not aware of the greatness that is inputs'.\nThe diff below shows the advantage of using inputs' over inputs for\naccessing packages.\n\nIs that not awesome? So how can we replicate that for ourselves?\n\nWhat we will need to do is map over all inputs, and their outputs and select\nthe output dependent on the host platform, if a system dependent output exists,\notherwise it will leave it as is. We can achieve that with the following code:\n\nOr if you are using flake-parts, you may prefer using the following code instead:\n\nSo let's add that to our mkSystem function.\n\nConclusion\n\nAnd that's it! We have a fully functional mkSystem function that can replace\nboth lib.nixosSystem and lib.darwinSystem. This was quite the task, and\nalthough this blog post seems to reduce the quite simple. I've spent a lot of\ntime on this, both when researching how to create the custom builder and\nwriting and maintaining the latest rendition in the form of a flake module\ncalled easy-hosts. If you enjoyed\nthis post, please consider donating on ko-fi\nor github sponsors.",
"title": "My custom lib.nixosSystem",
"updatedAt": "2026-04-20T00:00:00.000Z"
}