The Home Manager Journey
Home-manager is a narrowly-focused system for managing user environments with Nix. After setting it up, users under its management can have their own declarative configuration not only for what programs are available, but how those programs and services are configured.
The Home Manager Manual is quite good and should help you get it configured for your environment. I've personally configured it the flakes version of the NixOS module.
"But why?"
What if you wanted to have bash
not record curl
commands to history. You can
do this in a session something like this:
export HISTIGNORE=$HISTIGNORE:curl
Of course this won't persist across sessions. You could take it a step further and drop this export
command into your .profile
, which will work but…
…what's that icky feeling? That gnawing sense that something is wrong? You've gotten your NixOS
configuration just the way you like it1, and now there's this… this stuff you've got to do to
get your environment right. There's an extra step (echo export HISTIGNORE=$HISTIGNORE:curl >>
~/.profile
) to perform, or an extra file (~/.profile
) you have to carry around with you.
The promise of the declarative config pilgrimage was that you could ascend to a higher plane of
computation – one where clean installs are indistinguishable from machines that have been running
for years. A place where scaling your configuration from a single desktop machine under your desk to
HA VM clusters in the cloud is just a difference in how you invoke nixos-rebuild
2.
Yet here you are – running nano ~/.profile
for the fifth time today. A StackOverflow answer
made flesh3.
A holy journey that doesn't challenge you, doesn't make you question your beliefs, is hardly worth taking. I surely have not finished my journey, and so do not know all that is in store for you. Indeed, you may have taken different paths to get to where you are, so I imagine there's much for us to learn from one another… or at least I may "borrow" some useful stuff from your Nix config.
This exchange only truly works for us if transaction is on equal terms. A random .profile
checked
into a public repo might as well be a curl-pipe-to-sudo-bash as far as I'm concerned. I refuse to
touch it.
.profile
and other dotfiles like it (e.g. .bashrc
) are a bunch of opinions and spices tossed into a
blender, then baked. If you don't like the whole dish as served, your options are:
- Suck it up
- Send it back
- Try mixing in more opinions and spices, maybe pick out pieces you don't want, and just… hope for the best?
I don't want the dish you baked, I wanna know how you made it.
This is what I'm on about:
programs.bash.historyIgnore = [ "curl" ];
I can confidently pilfer the blob of Nix goo above and mix it into whatever I've got going on in my setup. It's under version control.
It's futureproof.
Today, bash
consults the HISTIGNORE
environment variable for a :
-separated list of commands to leave
out of history. If you're reading ahead, you may note that there are lots of other ways to get this
behavior using Nix using environment/session variable facilities, but none of those are quite as
well structured.
Imagine a meteor strikes Earth4, and bash
stops honoring HISTIGNORE
and looks
somewhere else for its list of things to leave out of history, say some set
command or something. In
the old .profile
-based world, you are really truly screwed. You get to dig around in your
configuration to figure out why thing used to work no longer work. Worse yet, you figure it out, but
have some systems still running bits from the old pre-meteor days. You now get to maintain two
versions of your magic .profile
: one that does the env var thing, and another that does the set
thing.
But that's not you, my sibling in Nix. You have learned your lessons well. You used home-manager
and
configured this setting as above. The world does not change for you5. Your old systems chug
along using the env var setup. Your new systems (referencing newer versions of home-manager
), use
the new set
setup under the hood.
This is transparent to you.
You have ascended. Your journey continues.
But what about unsupported programs?
You may be surprised at just how many things are configurable in home-manager
, but it's certainly
not every single program, so you're eventually going to come across something for which no
declarative configuration method is available. You'll have a dotfile in some format as the only real
means of configuration.
I should buy a boat, you say to yourself. All of this hard work, and here I am back at the start.
So what are you going to do? scp
the config from some other live system? Adopt GNU Stow as another
tool you have to learn/run/reconcile with Nix?
Stay on the path.
Treat the configuration as an opaque file
You've been tempted by the worldly pleasures of a program called cursed
(my apologies if there's
already something out there with this name, as there surely is). For the purposes of our
conversation, cursed
is available in nixpkgs
, but there aren't any NixOS or home-manager
modules for
it. To configure it, you must create a configuration file, ~/.cursedrc
.
You can check your darling dotfile into your repo and get it sucked into your config and plopped
down in your $HOME
using home-manager
:
[settings] cursed = true user = "thetraveler" # imagine this is your username
and your home-manager
config:
home.file.".cursed-config" = { source = ./.cursed-config; # this is relative to this .nix file target = ".cursedrc"; # this is the destination relative to $HOME };
This is… better than before. Your .cursedrc
is under source control, and is placed on disk when
you apply your home-manager
config.
Treat the configuration as text in your config
Now that you've given yourself some breathing room by just Making Things Work, you can start bringing the opaque blob closer to your Nix config. Delete your darling dotfile.
home.file.".cursed-config" = { target = ".cursedrc"; text = '' [settings] cursed = true user = "${config.home.username}" ''; };
This is almost the same as before, but notice that we have taken advantage of Nix to further decouple our configuration. If you change your username or apply this config to more than one user, you've made your journey a little easier.
Treat the configuration as config within your config
There's still something unsettling about our solution… It's just not very Nix-y… What if we wanted to make it easier to, say, add a new block to our config or something.
home.file.".cursed-config" = { target = ".cursedrc"; source = (pkgs.formats.toml {}).generate "cursed-toml" { settings = { cursed = true; user = config.home.username; }; newblock.whodis = "value"; }; };
Note that we switched back to using source
here… this is because the TOML generator emits output
as a derivation (i.e. it doesn't spit out a string, it spits out a file):
[newblock] whodis = "value" [settings] cursed = true user = "myusername"
This is powerful. You can now use the Nix language to not only integrate your Nix configuration into
cursed
, but you can avoid having to edit configuration in a lesser format like TOML, JSON, or
shudder YAML. You can find more supported formats here.
Treat the configuration as a call to action
If you're interested in bending cursed
to your needs, other travelers on other paths may also have
this interest. The next step here is to author a full-on module. I won't cover that in this post
(perhaps in the future our paths will cross again), but there are plenty of good resources out
there.
I'd probably start with the NixOS Manual, then look at the home-manager
specific stuff here.
The advantages of a full on module are manifold, but the first and most obvious one is that you can
enforce type restrictions. There's nothing we've talked about so far that will prevent you from,
say, setting cursed
to "me" or ["foo" "bar"]
instead of true
.