MacOS Nix Setup (an alternative to Homebrew)
I recently got a new Macbook, and began setting up the Nix package manager to install my developer toolset. I mainly did this to try and have a working setup without installing Homebrew. Since I ran into a few issues, I wanted to briefly document what I did and why in case others wanted to try the same.
Why Nix? (and why not Homebrew or MacPorts?)
The short answer: hype.
The long answer: I’ve been frustrated with Homebrew’s user experience for years now, and used this opportunity to start afresh. The default non-Homebrew answer is the venerable MacPorts, which has been around for quite a while. Most people who aren’t functional programming or build system nerds should probably use MacPorts, as it has been around long enough to have good support documentation floating around the internet. Unfortunately I’m a sucker for hermetic builds 1, so I decided to try Nix.
In addition, Nix has one cool feature I haven’t seen anywhere else: temporarily installing
nix-shell. For example, I can install
ripgrep inside a temporary shell,
and the package is automatically cleaned up when I’m done:
nix-shell -p ripgrep
There’s a lot of cool stuff
nix-shell can do, check out some more examples
Why not Homebrew?
If Homebrew works for you, then by all means keep using Homebrew! However, I’ve grown
frustrated with it. Homebrew has spent a lot of effort making a software delivery system
work across many iterations of OSX/MacOS, and countless developers have installed
as one of the first things on a new Mac. It is with this acknowledgement of the work the
Homebrew developers have put into the software and its resulting success that I offer
Unpredictable command times. I never know if running
brew upgradeis going to take 5 seconds or 20 minutes. This usually means knowing if a program or dependency is being downloaded as a prebuilt binary or compiled on the spot. It would be nice to signal to the user if compilation is about to happen, so they can plan the install accordingly.
brew updatetimes. Homebrew’s core package database is a git repository, which is great for enabling collaboration. Nix uses the same model, but the difference lies in how the clients get the package database:
brew updatedoes a
git pullunder the hood, while
nix-channel --updatedownloads a compressed tarball each time the channel is updated. This means Homebrew updates can rely on git and GitHub itself as an efficient distribution mechanism, getting delta updates “for free” without setting up extra infrastructure. However, this design choice means that the client has to maintain its own git repo –
git gccan strike at any time, for example, and slow things down tremendously. It is also a nightmare for CI, where the lack of an updated Homebrew git cache can negatively impact build times. Meanwhile,
nix-channel --updateruns are very predictable, even if a bit inefficient – you’re downloading a 16Mb tarball when the channel is updated, but that’s it.
Bad interactions when packages are upgraded. I’m not sure exactly why this happens, but occasionally when certain packages are upgraded (such as
pythonfrom 3.7 to 3.8), related packages can break. Because Nix builds are hermetic, packages should all “work together” regardless of how the system was previously set up.
Why not MacPorts?
To be honest, MacPorts is probably just fine for your needs. I haven’t used it in forever, but my understanding is that it’s reliable and has plenty of built-up community knowledge on how to fix things. Take a look at it and see if it fits your needs! I also quickly found this post which details some differences between it and Homebrew. But hey, if you like adventure, give Nix a shot!
Why not Nix?
I felt it was honest to include a section like this.
I haven’t done a deep evaluation, but my impression is that Nix is the least documented and least mature of the bunch. Expect to do a bit more digging to solve issues, but once you get started it generally just works.
Workarounds are currently needed for Catalina and above, especially if you have an older Mac without a T2 chip.
You’ll need to understand a bit of the Nix Expression Language. It’s not as scary as it sounds, but you will have to edit a
.nixfile to install a package (there is no CLI equivalent to
brew installthat I am aware of).
Installing Nix requires two phases: installing Nix itself, and then installing
nix-darwin. The installation methods I know of involve
curling with an
sudo call inside the script2. You will have to do something extra if
you’re running Catalina3 or above.
curl -L https://nixos.org/nix/install | sh
- Catalina with a T2 chip:
sh <(curl -L https://nixos.org/nix/install) --darwin-use-unencrypted-nix-store-volume(according to the site, “unecrypted” is a misnomer as the chip will encrypt it anyway).
- Catalina without a T2 chip: follow these instructions 4.
Make sure that
nix-buildis now in your
PATH. You may have to source
$HOME/.nix-profile/etc/profile.d/nix.shin your shell’s
rcfile to get this to work.
nix-build https://github.com/LnL7/nix-darwin/archive/master.tar.gz -A installer ./result/bin/darwin-installer
You should now have
The basic workflow for using Nix is editing
~/.nixpkgs/darwin-configuration.nix and then
darwin-rebuild switch to activate those changes.
~/.nixpkgs/darwin-configuration.nix with your favorite editor. Look at
environment.systemPackages: this is the list of packages you’ll want to install.
Nix lists are space-delimited (not comma-delimited). The packages listed
there come from the Nixpkgs channel you’re subscribed to. You can list what packages are
available by running
nix-env -qaP or searching Nixpkgs online. All the
packages are prepended with
pkgs.; I used Nix’s
with pkgs; clause to prepend that by
environment.systemPackages = with pkgs; [ gitAndTools.gitFull byobu wget ];
Activating your changes
Once you’re done editing, run
darwin-rebuild switch to install your new packages! If you
made an error,
darwin-rebuild will let you know and your current environment will not be
Updating the package database
If you want to update the package database, you can run
Nix will detect conflicts and error out, such as when two packages install the same
binary. This can happen when
python38, for example, both want to create a
pydoc3. To resolve this, you can call
lowPrio to declare a package low
priority and have the other package win.
environment.systemPackages = with pkgs; [ python37 (lowPrio python38) ];
(As a side note: once you’ve installed both, look at how quickly you can uninstall and reinstall each package. It takes between 1 and 2 seconds to uninstall or reinstall python3.7, which in my opinion is ridiculously fast.)
I have to admit that I only have a shallow knowledge of Nix, and the information above has made it sufficient as a daily replacement for Homebrew. However, there is a rich ecosystem around Nix that you may want to explore further. Here are a few links to learn more:
- What does
- The Nix Ecosystem
- The Nix Expression Language
- “Nix Pills”, a detailed walkthrough of the Nix language and environment
- Tips and tricks for using
- The online Nixpkgs search
- A helpful Nix cheatsheet
Good luck and have fun!
I hope this guide was helpful! Thanks to the Nix team for making such a cool package manager. As I’m relatively new to Nix, feel free to contact me if there’s something I got wrong or could have explained more clearly. Good luck and have fun with Nix!
From the Google SRE book: a build that is “insensitive to the libraries and other software installed on the build machine.” This greatly increases reliability, because it means that whatever you have installed on your computer is less likely to affect the build process and the resulting program. This is similar to Dockerizing an application, but less extreme and less resource-intensive to run. ↩︎
This is just as risky as the classic
curl | sudo sh. Unfortunately I can’t find another way to easily install Nix, so I guess you can download https://nixos.org/nix/install and compare its signature before running it. ↩︎
Catalina gets a lot of shade for its read-only root filesystem and signed binaries, but I think it’s a step forward for general purpose computing. What I object to is that signed binaries must be signed with registered Apple developer accounts to distribute software, and these accounts cost money (anyone can run their own software with the appropriate settings, but distributing software effectively needs an Apple developer account). It would be nice if that was a bit distributed, so anyone could sign and Apple maintained a reputation database or something. I’m annoyed at Apple for mixing good security and OS maintenance practices with total, walled-garden lockdown. Admittedly, the web has a lot of the same security benefits, but we’re not at a point yet where web apps can compete with native applications. Hopefully we’ll get there one day. ↩︎
Thanks Philipp for figuring all this out and writing it down! ↩︎