I needed a way to give myself a consistent environment between my existing Windows desktop and my new Linux laptop, and Nix’s home-manager was my first choice. Now, I left the official Nix Discord server quite a while ago after seeing some unfriendly behaviour there, which makes me wary of the community at large, but the technology is solid and I was able to set up my environment with the aid of the friendly and knowledgeable inhabitants of the QueerNix server.

I started by going through the Nix Pills, which were helpful up to a certain point. The nixpkgs section wasn’t very enlightening. The same can be said of most documentation in the ecosystem. Much of my experience comes down to squinting at packages in the nixpkgs repository to see if I could apply what I saw there to my own code, not to mention more trial and error than you can shake a stick at. I still can’t claim to understand a lot of it.

At any rate, I managed to find an extremely helpful starter from someone who also makes their real configuration publicly available. I can’t overstate how useful it was to be able to contrast the two. I modeled my own approach after these, mostly copying things wholesale and experimenting. I got something working—at least at the most basic level—as a Nix flake and was able to use it with NixOS 22.05 in WSL via the installer.

nixpkgs seems to be locked to the NixOS version, as you might expect, at least at the OS level. I set up the unstable channel in my home-manager configuration, but couldn’t see how to use it at first, because something like programs.zsh-fzf-tab.enabled = true; produced an error about the program not existing, as did unstable.programs or programs.unstable. I understood at last that programs.x is for what home-manager knows about and everything else goes under home.packages. The programs.x settings usually include a package member that allows you to override the Nix package used.

Applying the same configuration on my laptop was quick and easy, so I now have a Nix profile and environment available inside my preferred terminal emulator (WezTerm) on both systems. I couldn’t use Zsh as the login shell since it’s inside the user directory… except by adding the exact path to the binary to /etc/shells, which is an effective hack. Someone on Discord said they use a custom command to switch from Bash to fish immediately when they open their terminal. I don’t think I like that approach.

I added my first unstable package via an overlay, using the examples provided by the starter. It took a while to work out the right incantations. I couldn’t determine how to run setcap on the binary as required by the package, so it needs sudo. I also realized it was only working in the home-manager configuration; NixOS needed more. I ended up removing that custom package later when I figured out how to use the unstable nixpkgs version.

Speaking of which, I often have to switch to unstable packages because the stable versions are quite old. (To be fair, I started with NixOS 22.05 and only recently upgraded to 23.) For example, Nushell was at v0.71 in stable compared to v0.82 in unstable, and hledger at v1.25 compared to v1.30.1. The nixpkgs search page is a very handy reference for all this.

I can’t configure rclone for Google Drive within Nix since one can’t split or combine the rclone config file. There’s no way to have Nix generate only part of it and configure the rest independently, at least that I know of so far. The existing home-manager feature request discusses the complications.

Amusingly enough, I can’t compile Emacs under Nix because autogen.sh says my compiler doesn’t work, even inside nix shell 'nixpkgs#gcc'. I’m sure this will be much easier to sort out than it has been on Windows, but I had to record it for posterity. I might need to use a proper Nix approach instead, in which case I’ll have to see how to make the result available outside the Nix environment.

The phantom reconfiguration

I was having constant issues with things apparently not being configured or initialized in the WSL version:

  1. The first time I start NixOS after booting into Windows, it can’t find my .zshrc and I get a broken (default) prompt and an error, which goes away when I start a new shell session.

  2. In that new session, my .zshrc seems to be either incomplete or missing some of the time, because, for instance, setopt doesn’t show extendedglob (which I set in programs.zsh.initExtra).

  3. Packages I’ve included in my home-manager list sometimes aren’t available on subsequent boots until I run home-manager switch again.

  4. Even though I’ve included the systemd stuff to make it work on Windows, it requires a nixos-rebuild --switch to make it start after a fresh boot, as far as I can tell.

The weirdest issue is actually one I forgot to mention: sometimes, when things don’t load properly (always fixed by re-opening terminals and all that), I’ll end up with a prompt that overwrites the first line of output. So, for example, running ls in a directory containing a single file will produce a single line of output but then it’ll be replaced with the prompt.

I would always run home-manager switch after each change, so it wasn’t a missing generation or anything. I also ran nixos-rebuild switch whenever I changed something at the OS level. These issues were sporadic, so debugging them was difficult and I just kept switching and starting new Zsh sessions to resolve problems. I was about ready to blame it on gremlins.

At one point, adding a package to my configuration worked like usual but adding one more left the system unable to find either in my PATH during that boot. I tried running exec zsh; it didn’t fix the issue—nor did starting a new shell session—and it also triggered the problem with the prompt that I mentioned earlier, even in subsequent sessions. As I investigated this, I noticed I had new home-manager generations from the previous day, despite all home-manager switch invocations having failed. switching again now made everything work.

I was able to confirm this odd behaviour a couple of weeks later:

Today’s experience:

  1. Boot machine.
  2. Run WezTerm to get into WSL NixOS, get error about not finding .zshrc. (Frequent occurrence.)
  3. Restart WezTerm and get proper shell, but with programs missing.
  4. Run home-manager switch and get everything working.
  5. Close WezTerm.
  6. Some hours later, within same boot, re-open WezTerm and get same missing .zshrc error.
  7. Restart WezTerm and get proper shell, but with programs missing.
  8. Look at home-manager generations and see a new one was created when I restarted the terminal.
  9. Use the documented process to reactivate the previous generation and see everything is working again.

And finally, ten days after that, realized what the problem was:

Oh dang! I did a bit of searching and found out I’ve been doing it wrong!

Why do you? You used the HM nixos module, you are not soposed to run the HM CLI. It is for use with standalone HM only.

Yes, because the systems HM runs its activation on each system boot or switch, so of course you will be reset to whatever HM stuff you have defined in your system configuration.

System module and standalone are mutually exclusive for a single user.

https://discourse.nixos.org/t/solved-home-manager-creates-a-new-faulty-generation-on-reboot/19599

It turns out I had no reason to use the NixOS home-manager module and it in fact interfered with normal functioning. I removed that and now it works fine. I haven’t even had the output issue ever since, so perhaps it was related after all. The only remaining annoyance is that home-manager switch produces three warnings, not just one, about the Git tree being dirty.

A few small issues while reinstalling

Since it took a while to understand what I was doing wrong above, I reinstalled NixOS quite a few times in various attempts to resolve the problem. I needed to install Git via nix-env -iA nixos.pkgs.git to be able to rebuild NixOS each time and again for the new user I had created to run home-manager, but I also had Git inside my home-manager configuration, resulting in conflicts. I fixed that by setting a lower priority on the global package with something along the lines of nix-env --set-flag priority 1 git-2.38.4. I should probably remove Git from the home-manager profile and put it in the NixOS packages.

The Nix installer doesn’t create the profiles directory, so there’s an extra step before running home-manager: mkdir -p ~/.local/state/nix/profiles.

Throw in some Neovim

I’m an Emacs stalwart, but, just for fun, I thought I’d set up Neovim with LazyVim. While it looked good, it worked against Nix by re-downloading and managing all plugins itself. I spent two hours trying to make it recognize plugins downloaded in advance: the plugin configuration took only 10 minutes but I struggled to make it use the correct version of LazyVim and respect the configuration due to LazyVim unconditionally loading itself from Git each time.

I opened a Discussions thread and was told installing plugins with nix and lazy is not supported (emphasis theirs), so I replaced LazyVim with a small set of plugins managed by Nix. This suffices for my purposes; namely, playing around with Nix and adding packages just because I can.

The battle between Rust and home-manager

I had trouble getting different versions of Rust to work with Emacs. First I had an explicit Rust version configured via oxalica’s rust-overlay. Then, since different projects can use different versions, I decided to use rustup like I do everywhere. However, the rustup Nix package only links two binaries: rustc and cargo. rust-analyzer, Cargo subcommands, and so on aren’t included. I can’t support different projects with different versions, short of adapting every project to use Nix too.

Meanwhile, I don’t understand how to install the OpenSSL development headers to use in a Rust crate inside Nix, which seems to be a known issue. Since Nix has its own ideas about paths, its compiler can’t use the system OpenSSL, so cargo install cargo-edit, for instance, won’t work.

I switched to managing Rust outside Nix, but then I couldn’t seem to run something like cargo watch because it defaulted to Nix’s cargo-watch binary (installed via home-manager, if I recall correctly) and explicitly running the correct binary failed. I’ve therefore removed all Rust programs from my home-manager configuration and am installing them in the host OS. This would be a problem on NixOS, but I don’t do my development there anyway due to the WSL filesystem performance issues.

Single-use Nix shells

I wanted to run psql without having to install it. I came across a suggestion to use the nix shell command:

Zshnix shell nixpkgs#postgresql

But when I tried it, I kept getting an error:

Outputzsh: no matches found: nixpkgs#postgresql_15

I spent 10 minutes trying to debug my Nix channels—which I had in fact removed based on some advice about NixOS and home-manager—until I noticed the zsh and realized it wasn’t a Nix error. Simply quoting the package made it work:

Zshnix shell 'nixpkgs#postgresql'

Now I could run psql within that shell.

Looking to the future

There’s a lot I don’t understand about the Nix ecosystem. I can’t entirely blame the lack of helpful documentation. It’s more a combination of my unfamiliarity with a Linux desktop and the alacrity with which I dove into something intended not for instant, easy application but for thoughtful, planned use. In that sense, I think of Nix like I do Rust: I’m in it for the long haul, and I’m prepared to feel foolish and ignorant as I slowly learn how it works. I believe it’ll be worth it, and I look forward to discovering all the mistakes I must have already made.

To give one immediate example of its utility, using Nix made things even easier when I needed a couple of PowerShell tasks in a justfile recently. I could have switched shells or languages to make it cross-platform but, on an impulse, I added powershell to my home-manager package list. Contrary to my expectations, it only added a little weight to the installation. I ran just and it, well, just worked.