Recently, I received my new MacBook Pro with M1 chip, and it was a good time to tackle a long-standing to-do: try out the Nix package manager.
It's not a deep-dive style post (I'm not qualified), but a quick summary of why and how I use Nix in my day-to-day activity.
I'll start with what I want from a package manager:
With some degree of success, Nix addresses all of these points.
At the moment of writing, I had to run a single command, and it went smoothly:
Check the official docs since this post might be dated at the moment of reading.
Nix uses a store — disk partition on macOS — for storing its stuff: packages, generations, etc. It is immutable. The state of the system is called generation
. If greatly simplified, it describes how to symlink packages from the store to your system. When you change something in your system via Nix, it creates a new generation
instead of mutating the previous one. Thus, if you messed something up, you can rollback to the previous state. With that in mind, let's proceed to the practical parts.
flakes
or any other Nix feature that is unstable. I have enough bleeding edge in my life. Also, I don’t use profiles (explicitly). As far as I understand, if I haven’t touched this feature, Nix implicitly uses a single default profile. It works for me. There are two ways you can manage globally installed packages with Nix:
nix-env
is built-in. It is similar to how you handle packages with brew
. You run one command to install packages. You run another command to update or uninstall packages.
Another option is the Home Manager. One of its features is package management via the configuration file. To install a package, you need to add it to the config:
And run the command:
To uninstall a package, remove it from the config, and run the same command. Don't forget to switch
after changing the config.
Home Manager is not only about managing installed packages. This tool allows handling your dotfiles, too.
For example, you can configure your shell:
Or Git:
If you need to apply some dotfiles that Home Manager doesn't handle, you can use raw file
:
It will symlink such files to your ~
.
Check out its documentation for all the options. There are many. Also, here are my dotfiles with Home Manager based setup.
nix-darwin
for managing operating system via Nix. While Home Manager is for your home directory, nix-darwin
is for your macOS. Home Manager can be integrated with it. Personally, I don’t use it. But letting know here that it exists. One of my primary motivators to learn Nix is the ability to configure shell on a per-project basis without polluting global space. For example, if a project needs a specific C compiler, I wouldn’t need to install it globally. Instead, I would ask Nix to make it available within an isolated shell only for this specific project. And it would prepare such shell for me. Also, it can replace such tools as fnm
, nvm
, rvm
, rbenv
, etc., since it’s possible to install a specific version of a package within this isolated environment. It’s a bit awkward, though. I will elaborate on it a bit later in this post.
So, how my workflow looks from a usage perspective:
cd
into a project directory.Yeah, it’s that simple. Let's see how to configure it.
Even though you can use a nix-shell
command to enter a Nix shell, I prefer to autoload it on cd
. direnv
helps with it.
First, enable it in the Home Manager configuration:
If you manage your shell with Home Manager, you're good to go. Otherwise, check direnv
docs on how to hook it into your shell.
Next, configure your project.
Place a file called shell.nix
at the project's root directory:
When it’s done, you can test it by running a nix-shell
command, which should take you into the shell, where all these packages are available. By default, it uses bash
, but direnv
is smart enough to take you to your shell of choice.
To let direnv
know that it should load Nix shell here, create the following .envrc
file in the root of the project:
And run the command (you need to do this only once):
Done. Now, every time you cd
here, shell with ruby
and node
stuff will be loaded for you.
The good news is that it is possible with Nix. The bad news is that it's a bit awkward.
First, you should check if a specific version is available under the corresponding name. For example, Node.js has the following packages:
If v12.22.9
is what you need, use nodejs-12_x
as a package identifier. Otherwise, you have to fetch it from the git history of Nixpkgs repo.
To simplify this process, Marcelo Lazaroni created this tool. Let's say you need Node v12.22.7
. Go to the URL and search for a nodejs
package. In the results table, find 12.22.7
, and click on the commit sha to see the instructions.
Using the code from the instructions, shell.nix
would look like this:
If the version doesn’t exist, you can find build instructions of the closest version in the Nixpkgs repo and modify it locally to build it in your shell. I never did that, but it should work.
Since the Nix store is immutable, it might grow over time. So it makes sense to clean things up from time to time.
In shell.nix
, you can define shell hooks. Useful to export environment variables or execute scripts. Unfortunately, aliases wouldn't work in zsh
.
I don't think it's called bundles, but anyway. Besides top-level packages, Nixpkgs has additional top-level entries that bundle together a set of packages. For example, OCaml tools or Ruby gems. I use two of them at the moment: ocamlPackages
and vimPlugins
.
You search for a specific package using the following command:
It's useful to explore the Nixpkgs repository directly to figure out what bundles are available and at which paths. Some pointers:
ocamlPackages
bundle.Don't use Github search. It's unreliable. Pull the repo and execute a search against your channel's branch.
Example of my shell.nix
for PPX development:
If you find that some dependency constraints don't meet, you can override dependencies of dependencies. For example, if your project has the following package.json
, running yarn
would fail due to nixpkgs's yarn
having nodejs
dependency, which is v16_x
at the moment of writing, and which is used in the context of yarn
.
To fix it, you need to override nodejs
of this package:
Some dependencies might fail to build due to the following (or similar) error:
It means that they rely on the Apple framework, which is unavailable. To fix this, you can add darwin.apple_sdk.frameworks.<MISSING_FRAMEWORK>
to the shell.nix
.
If you rely on this channel, you get frequent updates. That's the point of using it. But as far as I understand, your unpinned dependencies might change their versions unexpectedly since no lock files exist (flakes
should address that). Something to keep in mind.
I find myself using those most of the time:
I'll try to add more notes here as I go.