DEV Community

Cover image for Flake My Life - setting up a minimal flake on NixOS
domagoj miskovic
domagoj miskovic

Posted on

Flake My Life - setting up a minimal flake on NixOS

These days I have been exploring lambda calculus through Nix language which will be the theme of another blog post. I've been trying out builtins commands with attribute sets, learning more about flakes, watching some great content on NixOS on YouTube, and playing with nix repl and nix-instantiate --eval. Overall, I think it's a great time to be learning Nix.

However, I haven't transformed my NixOS configuration into a flake yet. This time, I refuse to simply copy and paste an advanced configuration like I did when I started with NixOS.

The configs I managed to reproduce in the past were from:

  1. utdemir/dotfile-nix - Utdemir was a great help when I ran into issues with reproducing the config. Our conversation on GitHub issue #29 was my first engagement on GitHub, and it was a such a positive and inspiring experience.

Your issue was really useful! Always good to know what's the pain points on a fresh install. To be honest, installing this repository from scratch likely won't be a click&forget experience, since I do not routinely test installing from scratch and using other machines other than mine. Let me know if I can help with anything (even if you're not using this repo).

  1. gvolpe.com/blog/xmonad-polybar-nixos Reproducing this setup took me a couple of days, especially when dealing with the xmonad tiling window manager, which seemed very difficult to set up on NixOS.

  2. Th0rgal/horus-nix-home This config was another great learning experience with a visually appealing theme. The setup was simple enough, requiring only a few tweaks to the sub-modules.

Going all over again, this time with a basic minimal setup.

Although I would love to use any of these three configurations again, I decided to take a more incremental approach. I wanted to find a minimal setup that I could build on with better understanding before attempting more advanced configurations. I discovered the NixOS-starter-config repository, which offers minimal and standard flake templates.

If this is your first trying flakes, or you're attempting to migrate your (simple) config to it; you should use the minimal version.

If you're here looking for inspiration/tips/good practices (and you already use flakes), or you're migrating a config that already has overlays and custom packages; try the standard version.

OK, let's try the minimal one for starters and then build on that. The goal is to transform an already installed basic NixOS system running the Plasma DE (so no complicated setups with tiling managers yet!) and then build on that.

We are first instructed to create a git directory (because flakes by default work with git):

cd ~/Documents # this can be anywhere basically
git init nix-config
cd nix-config
Enter fullscreen mode Exit fullscreen mode

So now we have a flake.nix, a flake.lock, and two separate directories home-manager and nixos. So we can inspect now the files inside of these directories. I recommend not just simply copy pasting one's own configuration.nix or home.nix because the configuration.nix in the nixos folder is also a template. Yeah.. I see there are these overlays and overrides which I have no idea how they work. That's the next other thing I have to learn about. I do remember there is a NixCon 2019 or so, a talk by Graham about overrides and NixOS internals so that's for sure in the next 10 talks I plan to watch on Nix.

nix = {
    # This will add each flake input as a registry
    # To make nix3 commands consistent with your flake
    registry = lib.mapAttrs (_: value: { flake = value; }) inputs;

    # This will additionally add your inputs to the system's legacy channels
    # Making legacy nix commands consistent as well, awesome!
    nixPath = lib.mapAttrsToList (key: value: "${key}=${value.to.path}") config.nix.registry;
Enter fullscreen mode Exit fullscreen mode

Now here we already face the first obstacle: I do not understand what this means. I read already couple times about this flakes registry. Is this only on my machine? Where is this registry? And what are the nix3 commands? Is this a typo? What is the number 3 next to the nix command?

I will ask GPT4 and copy paste this piece of code into it.

OK, so this piece of code defines two attributes: registry and nixPath. That's clear. How are these attributes defined and what do they actually do and what for?

registry: The purpose of the registry attribute is to create an attribute set that maps each input flake to a registry entry. A registry entry in Nix is a way to map a flake reference to a specific flake. In this case, we are creating a registry entry for each input flake with the same name as the input itself.

So what does this mean. After much talking with GPT it shows me that this registry is not some online package registry of flakes but a local registry simply defined with this piece of code in my configuration.nix. So I can input one or many flakes that will be pulled into the rebuild cycle. Now this does remind of sub-modules that we can import into a configuration.nix or a default.nix but this isn't the same. The flakes are referenced and not simply combined into one flake. Or they are in a way when defined in flake.nix?

The registry is a user-defined mapping of flake references to specific flakes, and it is not a global, system-wide resource like a package registry in other package managers.

To achieve this, the lib.mapAttrs function is used. lib.mapAttrs is a higher-order function that takes two arguments: a function and an attribute set. It applies the provided function to each key-value pair in the attribute set and returns a new attribute set with the same keys and the resulting values.

Now what and how does this function work? Just by reading this it looks like a general mapping function that maps an operation to a list of things transforming the first list into a new list, or to be precise in our case an attribute set.

registry = lib.mapAttrs (_: value: { flake = value; }) inputs;
Enter fullscreen mode Exit fullscreen mode

Let's check the Nix reference manual at https://nixos.org/manual/nix/stable/language/builtins.html#builtins-mapAttrs to see the what this lib.mapAttrs does:

Apply function f to every element of attrset. As an example:

nix-repl> builtins.mapAttrs (name: value: value * 10) { a = 1; b = 2; }
{ a = 10; b = 20; }
Enter fullscreen mode Exit fullscreen mode

So here we come to a higher-order mapping function which is the bread and butter of functional programming. Nix is a pure functional language based on lambda calculus so these concepts such as map, reduce, filter, and fold pop out often. This is the thing I love about Nix. It reminds me on Haskell which is dear to my heart but still conceptually difficult for me. Nix might be actually a nice step towards learning Haskell eventually and also Rust with its functional constructs. So let's simplify this a bit and maybe use a simpler map function with numbers and list of numbers to see how it works:

Let's create a function that would double its input. So if we give it a number 2 it will give us back a 4:

nix-repl> double = x: x * 2

nix-repl> double 2
4
Enter fullscreen mode Exit fullscreen mode

Ok that seems easy enough. Now let's make a list of numbers:

nix-repl> numbers = [1 2 3 4 5 6]

nix-repl> numbers
[1 2 3 4 5 6]
Enter fullscreen mode Exit fullscreen mode

Great! We have a doubling function and a list of numbers. Can we apply our double function to a list of numbers as same as we applied it to a single number?

nix-repl> double numbers
error: value is a list while an integer was expected

       at «string»:1:5:

            1|  x: x * 2
             |     ^
Enter fullscreen mode Exit fullscreen mode

Nope. We can't. What do we do now? Now we come to the idea: How could we map the double function to each number of the list numbers? Or in other words, the map function takes two arguments: the function to be applied and the list of elements to which the function will be applied. Now is there a map function in Nix that we can use? Yes there is! And it's right above the mapAttrs function in the Nix reference manual at https://nixos.org/manual/nix/stable/language/builtins.html#builtins-map

So in Nix we have the map function that maps over lists and we have the mapAttrs function that maps over attribute sets.
So the solution is simple then:

nix-repl> map double numbers
[ 2 4 6 8 10 12 ]

# same could be done with a square function

nix-repl> square = x: x * x

nix-repl> map square numbers
[ 1 4 9 16 25 36 ]
Enter fullscreen mode Exit fullscreen mode

But by observing this behavior we can tell that this will not work with attribute sets because attribute sets have keys and values which is also a kind of map so we are talking about different data structures. So our mapping function needs to be modified. Mindblowing right? All of this smells like a Functor but I won't deep dive into that right now.

Actually by studying the mapAttrs function on our double and numbers list will show the difference:

let
  lib = import <nixpkgs/lib>;
  doubleValue = _: value: value * 2;
  numbers = { one = 1; two = 2; three = 3; };
  doubledNumbers = lib.mapAttrs doubleValue numbers;
in
  doubledNumbers
Enter fullscreen mode Exit fullscreen mode

We need to import the <nixpkgs/lib> because our simple map is built into nix while the mapAttrs is defined in the lib library which is a part of nixpkgs packages. Save that file as let's say mapAttrs.nix and then:

 nix-instantiate --eval mapAttrs.nix 
{ five = <CODE>; four = <CODE>; one = <CODE>; six = <CODE>; three = <CODE>; two = <CODE>; }
Enter fullscreen mode Exit fullscreen mode

Wow! we can see it works! Yes, the <CODE> parts are not showing because Nix is a lazy language, it only evaluates what is needed or to put it in better terms:

Nix only evaluates the function for a specific key-value pair when the corresponding value is accessed in the resulting attribute set.

If we add --strict option to our evaluation then we can see the result and the attribute sets will be evaluated:

 nix-instantiate --eval --strict mapAttrs.nix 
{ five = 10; four = 8; one = 2; six = 12; three = 6; two = 4; }
Enter fullscreen mode Exit fullscreen mode

Wonderful! But what else can we see from this? We can see that the objects in this resulting set are unordered. While lists in Nix are ordered collections of elements that preserve the order the attribute sets are unordered because they are designed to be accessed by their keys, rather than the order of the elements.

So what about our flake?

Let's leave our deep dive into mapping functions and look again our flake registry definition.

registry = lib.mapAttrs (_: value: { flake = value; }) inputs;
Enter fullscreen mode Exit fullscreen mode

So the result of our function over the flake attribute set will be stored in the registry attribute set which is basically a variable called registry. Then it become pretty obvious by now what the lib.mapAttrs does - a function that transforms the key-value pairs, and an attribute set to apply the transformation on. But now we come to the second part of the definition which is a lambda function that takes two arguments, _: and values: Now the underscore is used in cases when the key is not used in the function body same as in our previous example when we doubled the attribute set with doubleValue = _: value: value * 2; In this case we are interested in the doubling of the value while the key is empty. This is a concept known in functional language but I still need to see more examples of it.

OK I have talked more with GPT and this quote actually brings things nicely in line:

When you apply lib.mapAttrs (_: value: { flake = value; }) inputs;, it will iterate over the key-value pairs in the inputs attribute set, and for each key-value pair, it will create a new attribute set with the attribute flake set to the original value. The resulting registry attribute set will have the same keys as the inputs attribute set, but the values will be wrapped in a new attribute set with the flake attribute.

So if our inputs attribute set looks like this:

{
  nixpkgs = ...;
  flake-utils = ...;
}
Enter fullscreen mode Exit fullscreen mode

After applying the registry function it will look like this:

{
  nixpkgs = { flake = ...; };
  flake-utils = { flake = ...; };
}
Enter fullscreen mode Exit fullscreen mode

This finally makes so much sense. Now I just need to reread all this exploration couple times, meditate on it, sleep over it, and then again try to install flakes. Flake my life, this isn't easy at all but is super interesting. I never had this much fun configuring the system. I am learning programming and configuring at the same time while on top of it all everything is functional and there are lambda functions everywhere. How amazing is that!

But wait did I setup my flake? No I didn't. But I learned a lot and I have been doing this for hours today whole day. So much from my simple flake setup. But I will flake myself soon! I need to rest and watch some Nix talk and then try out the flake bit again. Then post about it next time.

Bill Murray happy

Top comments (0)