<- Explanation: Flake.nix output function body
In the last section we completed our explanation of the flake.nix
file. You
should now understand the following:
# For reference
myprofile = home-manager.lib.homeManagerConfiguration {
inherit pkgs;
modules = [ ./home.nix ];
}
- Your profile is the result of a function call of
home-manager.lib.homeManagerConfiguration
- We passed in a single argument to this function
- The
pkgs
attribute is fromnixpkgs
and contains all 64 bit linux packages - The
modules
attribute is a list that (in this case) contains a single value, which is a path- That path is our
home.nix
file
- That path is our
- The
Note that we could provide multiple "modules" here, but for now we have a single entry point.
So what even IS a module? Basically, it's a self-contained single function
that takes some inputs and produces some outputs. Specifically, it takes an
attribute set of inputs that contains things like pkgs
and then produces an
attribute set of outputs that contains things like a list of packages to install
or configuration files to create, which is then processed by some tool like
NixOS or Home Manager to do some useful thing.
That's a lot of words at this point, so let's look at our home.nix
file,
which is itself a module.
{ lib, pkgs, ... }: {
home = {
packages = with pkgs; [
hello
cowsay
];
username = "myusername";
homeDirectory = "/home/myusername";
stateVersion = "23.11";
};
}
Most of this syntax should all be familiar to you, except for the with
. We'll
come back to that later.
First, let's check on some overall structure. Remember that every Nix file needs to be a single expression, which we explained back in the first explanatory section? In this case, the single expression of this file is a function. This function takes a single argument, which is an attribute set.
# Single argument, an attribute set
# vvvvvvvvvvvvvvvvvv
{ lib, pkgs, ... }: #...
# ^ The : means it's a function
Recall as well from our earlier explanation that this argument must be an
attribute set that contains lib
and pkgs
. The ...
means we may get some
other arguments and that's okay, we just don't care about them because we're
terrible people efficient power users.
Where do lib
and pkgs
come from? They're provided by the module system.
You'll also see config
, which we'll use later. You can also pass in your own
special arguments,
but that's outside the scope of this guide.
Ok, so it's a function. What does it return? If you said "another attribute set", you're getting the hang of Nix! It's always another attribute set.
What's in the attribute set? We have a top level home
attribute, which then
contains packages
, username
, homeDirectory
, and stateVersion
. We'll
get into these in a moment.
Note that a module basically runs in two "modes". The first is the simpler way
where the top level attributes are just values that are used for configuration,
like we have here. The other is separating out the top level attributes into
a new attribute set assigned to config
, and then providing some options
definitions that are can be used to modify the module as a whole. The majority
of the modules you write will probably be the second way, which we'll get to,
but I wanted to show this first way because it's simpler and now you'll
recognize it if you see it in the wild instead of wondering where config
is.
So basically: a module is a file that contains a function. That function takes a single argument (as all Nix functions do). The argument is an attribute set which contains some number of inputs that are provided by the module system. The output is an attribute set that contains some configuration values.
For more information and much deeper dives:
There's one new thing in this file that we haven't seen before: the with
keyword.
{ lib, pkgs, ... }: {
home = {
# ... other stuff ...
packages = with pkgs; [
hello
cowsay
];
};
}
The simple version is that with
just tells Nix that for any variable being
used in the following expression, if it's not declared already, check whatever's
in with
.
For an even simpler version, here's another way to write the same thing
as above, just without the with
:
{ lib, pkgs, ... }: {
home = {
# ... other stuff ...
packages = [
pkgs.hello
pkgs.cowsay
];
};
}
You can hopefully see how this is useful when you have a large number of
something.x
an something.y
in an expression. Ok, finally we get to the
good stuff.
Congratulations on coming this far. This is where the fun actually begins. Welcome to your new candy store at MyNixOS.
The link above is specifically to all the top level options that are available
in Home Manager. If you look at the list you'll see home
.
Then if you follow that, you'll see all the options we can specify in the home
attribute. Looking through this list, you'll see packages
.
Take a moment to explore this site to look up the other home
values we've set:
username
, homeDirectory
, and stateVersion
. I'll wait.
The one confusing one is likely stateVersion
. This one's likely going to be
a little magical for a bit, but the basic idea is that it locks the whole setup
to a specific schema so that the tool knows how to interpret it. This way new
changes can be made to the overall system without breaking older setups. The
short version as a practical day-to-day user: set it and then never, ever change
it or even look at it again.
Did you find the others? Great, let's look at what other fun things we can do.
Let's start with something simple that shouldn't break anything in your existing setup, but will show you the potential of where this is going. Let's add... a file. Innovation.
Before we start, I want to make something clear: in the long run, you actually probably won't use this very often. You'll want to prefer using specific configuration options that will generate config files, or using other functionality to build shell scripts, etc. However, it's very easy to get started with this, and it can be useful for sketching out some ideas or just getting a file somewhere that you want it. Mostly it's a great excuse to show you some more new syntax.
First, check the docs here
to reference what we're about to do. Specifically, we're interested in the
text
field to get started. We could also use source
to copy a complicated
file, but we'll use text
because it's simpler and we can start doing more
interesting templating in a moment.
I'll skip to the good stuff and give you a working starting point. Then we're going to make this more interesting.
{ lib, pkgs, ... }: {
home = {
# ... other stuff ...
file = {
"hello.txt".text = "Hello, world!";
};
};
}
Notice this "hello.txt"
acts as the <name>
in the documentation. Also recall
that we could do this too:
"hello.txt" = {
text = "Hello, world!";
};
When you apply this change, you should now be able to do the following:
cat ~/hello.txt
And you should see your Hello, world!
text!
So what is this file? If you do ls -l ~/hello.txt
you'll see that it's a
link to a file in /nix/store
, and it's read-only. You cannot edit this file
(or at least, you shouldn't), unless you edit your home manager setup.
Congratulations, you've made a text file. Now let's make it an interesting text file.
So what's an interesting text file? How about a script that greets us? Let's
use the executable
option
to make it something we can run.
{ lib, pkgs, ... }: {
home = {
# ... other stuff ...
file = {
"hello.txt" = {
text = "echo 'Hello, world!'";
executable = true;
};
};
};
}
Again, this is NOT actually the long term solution you want for writing your
own utility scripts. The long term solution you want is probably
writeShellApplication
in nixpkgs, which is available in pkgs.writeShellApplication
. This requires
some significant extra work and understanding, so bookmark this later and return
to it once you're more comfortable with Nix generally. If you want a taste of
what this might look like, here's what I do to generate a bunch of helpful
wrapper scripts in my own setup.
With that in mind, let's apply our change and see what we've done.
# Notice it's now marked as executable
ls -l ~/hello.txt
# And as expected, we can run it directly!
~/hello.txt
Ok, cool... but what if we want to run more than a single command? How do we
do new lines in our script? We use ''
notation!
{ lib, pkgs, ... }: {
home = {
# ... other stuff ...
file = {
"hello.txt" = {
text = ''
#!/usr/bin/env bash
echo "Hello, world!"
echo '*slaps roof* This script can fit so many lines in it'
'';
executable = true;
};
};
};
}
The ''
notation allows us to just write as many lines as we want, and it will
match the leftmost indentation to move everything over as you would expect.
Once again, apply the changes and run it to see that it echos both lines. You
can also cat ~/hello.txt
to see that it's all there.
Remember how I said Nix isn't just glorified JSON? Let's prove it by adding something more dynamic to our script.
First, let's extract our username into a variable. If you thought about let in
notation, good job!
While we're here, we'll notice that our home directory
has our username hardcoded in. We'll also notice that we probably don't need
to explicitly set the homeDirectory
variable,
but that it's an excellent excuse to look at how to use variables inside
strings.
Basically, "my string has ${thing} in it"
will inject the value of thing
into the string. We can also do simple string combining with "my string has " + thing + " in it"
.
Let's see how we can use this for our home directory.
# We could use 'let in' before any expression, but we want the username to be
# available at a high level so we'll put it here.
{ lib, pkgs, ... }: let
username = "myusername";
in {
home = {
# ... other stuff ...
# Did you think you were escaping 'inherit'? It's baaaack... remember
# that this is just "username = username;"
inherit username;
# Inject the user name into the home directory
homeDirectory = "/home/${username}";
# Note we could also do this, but using direct interpolation
# looks a lot cleaner once we do anything complicated.
# homeDirectory = "/home/" + username;
};
}
This also works for our script.
{ lib, pkgs, ... }: let
username = "myusername";
in {
home = {
# ... other stuff ...
file = {
"hello.txt" = {
text = ''
#!/usr/bin/env bash
echo "Hello, ${username}!"
echo '*slaps roof* This script can fit so many lines in it'
'';
executable = true;
};
};
};
}
At this point you should be getting ideas. Think about all your configuration
files, your bashrc, your zshrc, your vimrc, your emacs config, your gitconfig,
your tax returns, everything that you keep tweaking and having to copy around
everywhere. Now imagine generating those files, supplying inputs to change
them in a type safe, reproducible way to the point that you can bring up a fresh
VM and just running make
to have everything exactly the way you want it.
You may not do it with home.file
in the end, but you should start getting
a sense of the power of Nix!
At this point, you should have a basic understanding of what a 'module' is,
where to find some home manager options, and how to set them. You also learned
some new Nix syntax using with
, multiline strings, and string interpolation.
When I get time, I'd like to write a section on how to start splitting up your
configuration into multiple modules. If you want a head start, check out the
imports
attribute and realize that modules can import other modules which
can import other modules and so on.
Before you start writing everything in home.file
, remember that there's almost
always a better way built into Nix to do what you're probably trying to do.
As an example, check out bash
and try to enable Home Manager to manage your bashrc. You can start adding
things like bash aliases, functions, and environment variables.
The important thing is that at this point, you should have a working Home Manager setup and the basic ability to start experimenting with it yourself. As time goes on you'll develop your own setup in a way that makes sense to you, that you can build on and maintain.
Enjoy Nix!