nix
EXPERIMENT
We are experimenting with using Nix for developer environments.
Overview
The nix package manager is a "purely functional package manager". From that manual:
This means that it treats packages like values in purely functional programming languages such as Haskell — they are built by functions that don’t have side-effects, and they never change after they have been built. ... You can have multiple versions or variants of a package installed at the same time. This is especially important when different applications have dependencies on different versions of the same package ... An important consequence is that operations like upgrading or uninstalling an application cannot break other applications, since these operations never “destructively” update or delete files that are used by other packages.
Nix is used to manage the dependencies of a project to ensure that everyone is using the same version of all tools.
Why Nix? Why not Docker?
Docker for local development provides many of the same advantages of nix: reproducible environments with strict versioning. However, docker for development, while awesome in many ways, also has some downsides.
Advantages of Docker
The biggest downside of native development is that our deployments are on Linux and it is possible for libraries and code paths to behave differently on macOS than on Linux. We currently use native development environments and haven't really hit something severe yet, so this is more of a theoretical concern for Truss projects right now.
Advantages of Nix
When doing local development using docker, you need to share your mac filesystem with the docker container. Docker for mac has to keep the files in sync between your mac and the docker container. That synchronization has historically had poor performance. This means that operations that access the filesystem inside docker are slow AND use significant CPU. Running the same code outside the container is sometimes 25-50% faster and that penalty is hard to swallow.
Using Docker for development also complicates committing code. When using pre-commit you need access to the development environment when committing code. This means either running git inside docker or trying to find a way to run your pre-commit hooks inside docker.
Running git inside docker has problems because it makes using a hardware device for code signing like a yubikey much more complicated and difficult.
Running pre-commit inside docker is not a supported configuration by pre-commit which takes us even farther away from the well worn path.
Finally, configuring local tooling like editors to use the docker development environment is generally much more complicated that configuring it to use a native development environment.
For all of these reasons, a native development environment seems like a less risky path forward.
Using Nix seems like a way to get almost all of the advantages of docker and a native development environment.
Differences with homebrew
Homebrew has more packages than nix
, and installs them globally. For
certain things, that might work great. However, that doesn't give
projects reproducible installs.
One other difference is that versions of particular packages may be more
up to date in one or the other. E.g. as of this writing, homebrew's
watchman
version is 2021.06.07.00
but nix
is on 4.9.0
, which is
from sometime in 2017 or 2018.
Installation
MacOS requires that we add an argument to the installation command. We also want to do the single user installation. So to install, run:
sh <(curl -L https://nixos.org/nix/install)
See the macOS installation instructions for more details.
Getting started
After installing nix
, you should have ~/.nix-profile/bin
in your
PATH.
Quick HOWTO
For a template for how to set up a project see ./HOWTO.md.
Extra Setup (Only Fish Shell Users)
If you're using the fish shell, check out nix-env.fish.
Add this to your ~/.config/fish/config.sh
set -g fish_user_paths "/Users/[your_os_username]/.nix-profile/bin" $fish_user_paths
Working With Packages
This is more of a quick overview, but more details can be found in the nix basic package management docs.
Installing New Packages
nix-env -i <package>
E.g.
nix-env -i direnv
Seeing Installed Packages
nix-env -q
Looking for Packages
nix-env -qa <package name>
E.g.
nix-env -qa python3
Uninstalling a Package
nix-env -e <package>
E.g.
nix-env -e chamber
Profiles
Profiles can be used at a global level and on a per-repo basis.
Default
When it's installed, nix
creates a profile which will store all the
packages you install by default. It will initially only contain the
packages needed for nix
itself to work properly.
If you want, you could tell it to use a different profile. Either on a one-off command or by switching the active profile. This is covered more in the Profiles: Advanced section.
Default profile is in: /nix/var/nix/profiles/per-user/<username>/profile/
E.g. /nix/var/nix/profiles/per-user/felipe/profile/
Repo Profiles
Ideally you'll have a profile defined at a repo level that will house your repo's dependencies.
You can use the NIX_PROFILE
environment variable when working with this
type of profile so that nix
commands know to use that profile, e.g.
when installing repo dependencies. E.g.
export NIX_PROFILE=/nix/var/nix/profiles/myproject
Then you can pre-pend this repo profile to your PATH
when you're working
with the repo, while keeping your default profile in the PATH
behind the
repo profile to still have access to nix
commands.
See Working with Existing Nix Expressions for more details on how to set this up.
Working with Existing Nix Expressions
For more info see the nix expressions docs.
If you have a project or directory with a pre-defined default.nix
file,
you can install have nix
install the packages defined in it. Usually
this file lives in a nix
directory, and you can use it like this:
nix-env -p /nix/var/nix/profiles/<profile name> -f ./nix -i
Create an .envrc
file in a directory with some environment variables
your project needs:
cat <<ENVRC > .envrc
export NIX_PROFILE=/nix/var/nix/profiles/myproject
PATH_add ${NIX_PROFILE}/bin
ENVRC
On first run, you should get a message indicating that you will have to
explicitly authorize direnv
to load the file:
direnv: error .envrc is blocked. Run `direnv allow` to approve its content.
Running this should fix it:
$ direnv allow
direnv: loading .envrc
direnv: export +DB_HOST +DB_NAME +DB_PASSWORD +DB_PORT +DB_USER -PS2
Your local environment variables should be updated now. Any time the .envrc
file has changes, you will need to re-approve the file, but it will load
automatically otherwise.
Nix installations are immutable, so by default you cannot make changes
like installing additional global software via go get
or npm install -g
.
To use a local directory for installing go binaries, add to your
.envrc
:
export GOPATH=$PWD/.gopath
PATH_add ./.gopath/bin
To use a local directory for installing npm binaries:
export NPM_CONFIG_PREFIX=$PWD/.npmglobal
PATH_add ./.npmglobal/bin
Learn More About Nix
There are several nix guides
that you may find helpful in learning more about how nix
works
if you are curious.
Advanced Usage
Profiles: Advanced
In the main profiles section we mention having profiles at a global and repo level. You can also have profiles on a per-project basis.
E.g. for MilMove, you might want a profile that contains things like
aws-vault
, chamber
, etc. which spans more than just the mymove
repo. On the other hand, you may not want to use a global profile
(like the default one mentioned in the next section) because you may
need different versions of aws-vault
for different projects.
Creating a Profile
You can create a new profile wherever you want, though convention seems
to be to do so in
/nix/var/nix/profiles/
or
/nix/var/nix/profiles/per-user/<username>/
To create a new profile, you can run the command below. Note that this won't actually set this new profile as the active one, that's in the next section.
nix-env -p /nix/var/nix/profiles/<profile name> -i nix nss-cacert
E.g.
nix-env -p /nix/var/nix/profiles/milmove -i nix nss-cacert
The -p
is telling it the path to the profile you want to use for
this nix
command. This can be used to work with profiles that aren't
the active profile. It can also be used with profiles that don't yet
exist (like here) or with exising profiles.
You may remember the -i
is used for installing, so we're installing
nix
and nss-cacert
into our new profile.
This is needed because of an issue if you just switch to a new profile.
If you don't install nix
itself, you lose access to nix
commands.
And without nss-cacert
, you can't install packages because nix
will
get SSL cert errors.
For more info see the nix github issue
Switching Profiles
To switch profiles, run
nix-env -S <path to profile>
E.g.
nix-env -S /nix/var/nix/profiles/milmove
Note that it is possible to switch to a profile that doesn't exist yet
and break nix
commands. See creating profiles section for more info.
More Profile Info
See the nix profiles docs for more info.
Uninstalling
Uninstalling nix
takes a few steps:
- Remove the
nix
entry fromfstab
usingsudo vifs
- Destroy the data volume using
diskutil apfs deleteVolume 'Nix Store'
- Remove the
nix
line from/etc/synthetic.conf
(w/sudo
)