Nixing Homebrew: Streamlining package management on your machine
Tags: nix, productivity
Published: 2023-09-18 03:08:44

As someone who deals with package management on the daily, whether it’s `pnpm` or `cargo`, I have grown tired of `brew` rather lacklustre proposition. If you’re happy with `brew`, by all means, stick with it! I’m not vibing with it anymore though.
## Why I do consider brew so lacking
There are quite a few reasons for this:
- It’s slow. I don’t think ruby itself is the main reason, but the rather simplistic nature of brew’s architecture plays a part.
- You can only interact with the CLI. This is pretty bad, as there’s no file that declares your dependencies like a `package.json` or a `Cargo.toml`. This makes multi-machine management an exercise in frustration.
- As a consequence of this, managing versions is… not exactly ideal either. Yes, things like `fnm`, `virtualenv`, and `rustup` are cool, but they only exist to fill a gap in most OS package managers. And that’s just programming languages, sometimes I want to avoid upgrading a binary, like postgres.
- Another missing feature is the lack of lock files.  `yarn` ’s release was a transformative moment in the javascript ecosystem. It established how important these were, at a time where `npm` did not provide them.
- Auto upgrades can be harmful. This is due to the lack of versioning, but I’ve had breakage in my dev environment because adding a package also upgraded majors on other packages. This behaviour can be improved with `HOMEBREW_NO_INSTALL_UPGRADE` or `HOMEBREW_NO_AUTO_UPDATE`, but they introduce other problems, and they aren’t the default.
- It doesn’t handle configuration management. In my dotfiles, I still have to invoke `stow` . And even if I use a script, `stow` sometimes generates its symbolic links too eagerly, bothersome when applications clutter their configuration directory. Yes, looking at you `fish` 😠
- It’s a mess on apple silicon. Once you’re past the initial humps, it’s okay-ish. OS upgrades will still be painful, and the intel experience remains simpler.
- It’s less nice than linux package managers, and I run/manage linux machines aside from my MBP, so I’d love a common tool, or something at least on par. My only BSD is a truenas server which I manage via a local web interface.

And yes, homebrew bundle exists, and fixes some of these issues. But that means it sits in an awkward space, where it fills some of the expectations, but falls short due to the parent tool not catering to that paradigm. And the fact that you can inadvertently work around it is not helping. It’s also mentioned once on brew.sh, which does not inspire confidence.
## Getting an alternative
There aren’t a lot of package managers that can fill that void. Fink and MacPorts are pretty much macOS only, and linux package managers tend to be linux-only. They can run on macOS, but it’s a path full of pain, and it’s a lot of manual steps. Which is the opposite of what I’m looking for, automation is the name of the game here. The only viable solution I was able to find was nix. Nix is many things beyond a simple package manager, but I want to stay simple for now. The goal is to manage my applications, and my dotfiles. There might be something else I’m not aware of, in which case I’d love to hear about it!
## Getting on with nix
### Installing nix
You might be tempted to use the command that nixos.org recommends, but will be better off using another path: the Determinate Nix installer. The reasons are detailed on the [Zero to Nix website](https://zero-to-nix.com/concepts/nix-installer). It offers a cleaner install/uninstall experience, essential when trying something out. To use it, type the following command.
```sh
curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
```
That should bring nix to your machine, using the optimal setup for macOS, and that's it. You now have a working nix installation on your Mac. To check that things are in working order, you can type the following command:
```sh
nix search nixpkgs neovim
```

Now that we’re done with the installation, this is where a lot of tutorials would tell you to install nix-darwin. Don’t bother: it’s a lot of additional complexity, in an intrusive package. It allows you to tweak things at the OS level, but you pay too big a price. What’s worth it though, is [`home-manager`](https://github.com/nix-community/home-manager). Using a flake, it’ll allow us to forget (or rather never learn) about a lot of nix commands, and get a pre-packaged shell with all the stuff you declared.
### Installing home-manager
Home-manager is what is going to be handling my dotfiles. It also enabling me to let a flake manage everything rather than editing nix’s configuration. Here’s the minimal configuration I went with, separated in two files. Replace `<username>` with whatever yours is to get going (`echo $USER` will tell you what you username is if you aren’t sure):
#### `flake.nix`
```nix
{
  description = "Emilia's dotfiles";

  # inputs are other flakes you use within your own flake, dependencies
  # if you will
  inputs = {
    # unstable has the 'freshest' packages you will find, even the AUR
    # doesn't do as good as this, and it's all precompiled.
    nixpkgs.url = "github:nixos/nixpkgs/nixpkgs-unstable";
    home-manager = {
      url = "github:nix-community/home-manager";
      inputs.nixpkgs.follows = "nixpkgs";
    };
  };

  # In this context, outputs are mostly about getting home-manager what it
  # needs since it will be the one using the flake
  outputs = { nixpkgs, home-manager, ... }: {
    homeConfigurations = {
      "<username>" = home-manager.lib.homeManagerConfiguration {
		# darwin is the macOS kernel and aarch64 means ARM, i.e. apple silicon
        pkgs = nixpkgs.legacyPackages.aarch64-darwin;
        modules = [ ./home.nix ];
      };
    };
  };
}
```
#### `home.nix`
```nix
{ ... }: {
  # This is required information for home-manager to do its job
  home = {
    stateVersion = "23.11";
    username = "<username>";
    homeDirectory = "/Users/<username>";
    packages = [ ];
  };
  programs.home-manager.enable = true;
  # I use fish, but bash and zsh work just as well here. This will setup
  # the shell to use home-manager properly on startup, neat!
  programs.fish.enable = true;
}
```

Once these files exist in the directory of your choice, then you can run:
```sh
# if your directory is part of a git repo, flake will only use files that
# are properly tracked. This caught me off-guard multiple times!
git add .
# This will run home-manager after downloading it, the same way npx would
# in JS land.
nix run github:nix-community/home-manager -- switch --flake .
```
This will install everything necessary by reading your `flake.nix`, create a `flake.lock` to ensure reproducibility, and do all the necessary setup. Restart with a new shell to get all the necessary goodies. To make sure it all worked: `home-manager` should now be in your `PATH`, and can be invoked on the command line.
### Adding your packages
Before installing a package, we need to find it. Finding it can be done one of two ways:
- On the nixOS website: [NixOS Search](https://search.nixos.org/packages)
- On the command-line: `nix search nixpkgs <package>`

Once you’ve determined what the package is, get its name, and then we can go add stuff to our `home.nix`.

```nix
# We add pkgs since it's available as an argument, thanks to our inputs
{ pkgs, ... }: {
  home = {
    stateVersion = "23.11";
    username = "emiliazapata";
    homeDirectory = "/Users/emiliazapata";
	# Then we add the packages we want in the array using pkgs.<name>
    packages = [
      pkgs.git
      pkgs.neovim
    ];
  };
  # This is to ensure programs are using ~/.config rather than
  # /Users/<username/Library/whatever
  xdg.enable = true;

  programs.home-manager.enable = true;
  programs.fish.enable = true;
}
```

Once you’re done, you can run this command to install these packages:
```sh
home-manager switch --flake .
```

### Adding your configuration
To manage your dotfiles, it’s going to be in the `home.nix` file again. There are two ways to go about it:
- Leverage `home-manager` and the nix language to the max, by putting the entire configuration in nix files. And figure out what needs to go where with its… questionable documentation
- Tell it to put specific files in specific places. 

We’re gonna go with the second option, as it’s a lot less work, and doesn’t tie you to the nix ecosystem.
```nix
{ pkgs, ... }: {
  home = {
    stateVersion = "23.11";
    username = "emiliazapata";
    homeDirectory = "/Users/emiliazapata";
    packages = [
      pkgs.git
      pkgs.neovim
    ];
	# Tell it to map everything in the `config` directory in this
    # repository to the `.config` in my home directory
	file.".config" = { source = ./config; recursive = true; };
  };
  xdg.enable = true;

  programs.home-manager.enable = true;
  programs.fish.enable = true;
}
```

And now, don’t forget to `git add` all the necessary files. Home manager will also not override files it hasn’t managed, and will ignore that silently. You will need to remove the files in your `.config`, in order to get home-manager to manage them. Once you’ve checked everything, same command as last time:
```sh
home-manager switch --flake .
```

And that’s it! You now have a nix-managed dotfiles repository, with declared packages. To ensure it all went without a hitch, you can `ls -a` in one of the configuration directory you handled. It should show the symbolic links pointing to somewhere in `/nix/store/` like so:
```
lrwxr-xr-x@ 1 username  staff    84B 17 Sep 11:38 init.lua@ -> /nix/store/7svm3r3xpwhgnfsp2g0wmpycai1fvxan-home-manager-files/.config/nvim/init.lua
```

It’s a pretty lengthy article, but I wanted to be thorough and explain the **why** which tends to be hand-waved away, especially in the nix ecosystem! You can find my dotfiles and the recent changes I made in my [dotfiles repo](https://github.com/synecdokey/dotfiles) if you’re wondering what it ended up looking like in my case.

One last note: The [nix discord community](https://discord.gg/RbvHtGa) is pretty okay, and they helped me figure a lot of that stuff out. If you feel adventurous enough to try nix out, you might want to come hang there.