DEV Community

Cover image for Nixing Homebrew: Streamlining package management on your machine
Emilia
Emilia

Posted on • Edited on

Nixing Homebrew: Streamlining package management on your machine

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. It offers a cleaner install/uninstall experience, essential when trying something out. To use it, type the following command.

curl --proto '=https' --tlsv1.2 -sSf -L https://install.determinate.systems/nix | sh -s -- install
Enter fullscreen mode Exit fullscreen mode

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:

nix search nixpkgs neovim
Enter fullscreen mode Exit fullscreen mode

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. 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

{
  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 ];
      };
    };
  };
}
Enter fullscreen mode Exit fullscreen mode

home.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;
}
Enter fullscreen mode Exit fullscreen mode

Once these files exist in the directory of your choice, then you can run:

# 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 .
Enter fullscreen mode Exit fullscreen mode

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
  • 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.

# 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;
}
Enter fullscreen mode Exit fullscreen mode

Once you’re done, you can run this command to install these packages:

home-manager switch --flake .
Enter fullscreen mode Exit fullscreen mode

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.

{ 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;
}
Enter fullscreen mode Exit fullscreen mode

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:

home-manager switch --flake .
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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 if you’re wondering what it ended up looking like in my case.

One last note: The nix discord community 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.

Top comments (2)

Collapse
 
niftynei profile image
nifty

Amazing! Cant wait to set this up. ty!

Collapse
 
nmsmil profile image
nmsmil • Edited

Thanks for this guide. I'm trying it on a fresh macOS 14.2.1 Sonoma install, Apple M3 Max.

During Installing home-manager I ran into an error after executing
nix run github:nix-community/home-manager -- switch --flake .
I get this:
error: cached failure of attribute 'packages.aarch64-darwin'

I'm new to nix and searching for this error did not result in any pointers that I found helpful.

If you know how to fix this, let me know please.

Also, the fresh macOS install did not yet have git in order to run git add .
MacOS prompted me to install Xcode command line tools which I did.

Then a further step seemed to be needed, running git init before I could run
git add .