Dev Environment Setup With Nix on MacOS

In the last couple of days, I played bit with Nix on MacOS. This is a summary of my current setup.

This article does not go into details about what Nix is or why you would want to use it. For more information on that, read What Is Nix and Why You Should Use It.

Installation

To install Nix on MacOS as a multi-user installation, run:

sh <(curl -L https://nixos.org/nix/install) --darwin-use-unencrypted-nix-store-volume --daemon

Since / is read-only since MacOS Catalina, the installation script will create an APFS volume for the Nix store and mount it at /nix. See manual.

You may need to source the nix profile at this point.

source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
source /nix/var/nix/profiles/default/etc/profile.d/nix.sh

If you want to configure the operating system via Nix, you can install Nix Darwin. I skipped this for now and only use Nix for installing packages and managing the home directory. To see the options you can set with Nix Darwin, refer to the Nix Darwin manual.

To manage the home directory, I’m using Home Manager. If you are using Nix Darwin, you can install it as a Nix Darwin module. If not, you can use the standalone installation.

Installing Packages

To allow installing unfree software via Nix, create the file ~/.config/nixpkgs/config.nix with this content:

{
  allowUnfree = true;
}

To find a package, run:

nix search kubectl

Add the packages you want to install for your user to ~/.config/nixpkgs/home.nix. It might look something like this:

{ config, pkgs, lib, ... }:

{
  programs.home-manager.enable = true;

  home.username = "myusername";
  home.homeDirectory = "/Users/myusername";

  home.stateVersion = "20.09";

  home.packages = [
    pkgs._1password
    pkgs.awscli
    pkgs.circleci-cli
    pkgs.fish
    pkgs.git
    pkgs.google-cloud-sdk
    pkgs.graphviz
    pkgs.htop
    pkgs.kubectl
    pkgs.kubernetes-helm
    pkgs.kubetail
    pkgs.hugo
    pkgs.jq
    pkgs.minikube
    pkgs.nodejs-12_x
    pkgs.plantuml
    pkgs.python3
    pkgs.tasksh
    pkgs.taskwarrior
    pkgs.terraform
    pkgs.tldr
    pkgs.tree
    pkgs.watson
    pkgs.yarn
    pkgs.yq
  ];
}

After changing the configuration, run:

home-manager switch

Git

You can use Home Manager to configure Git by adding something like this to .config/nixpkgs/home.nix:

{ config, pkgs, lib, ... }:

{
  # ...
  programs.git = {
    enable = true;
    userEmail = "email@example.com";
    userName = "Mrs. Developer";
    signing.key = "1234ABCD";
    signing.signByDefault = true;
    ignores = [ "*~" ".DS_Store" ];
    extraConfig = {
      core = {
        editor = "nano";
      };
      url = {
        "git@github.com:" = {
          insteadOf = "https://github.com/";
        };
      };
      pull = {
        rebase = true;
      };
    };
  };
}

Refer to the manual for all available options.

Note: If there is already a git configuration file, you need to remove or rename it before the new configuration can be applied. This holds true for any other configuration files as well.

Environment Variables

You can set environment variables with home.sessionVariables:

{ config, pkgs, lib, ... }:

{
  # ...
  home.sessionVariables = {
    EDITOR = "nano";
  };
}

Fish

Configure Fish with Home Manager by adding this to ~/.config/nixpkgs/home.nix:

{ config, pkgs, lib, ... }:

{
  # ...
  programs.fish = {
    enable = true;
  };
}

Oh My Fish Plugins

You can add any Oh My Fish plugin without actually installing Oh My Fish by adding it to programs.fish.plugins.

{ config, pkgs, lib, ... }:

{
  # ...
  programs.fish = {
    enable = true;

    plugins = [
      {
        name = "bass";
        src = pkgs.fetchFromGitHub {
          owner = "edc";
          repo = "bass";
          rev = "50eba266b0d8a952c7230fca1114cbc9fbbdfbd4";
          sha256 = "0ppmajynpb9l58xbrcnbp41b66g7p0c9l2nlsvyjwk6d16g4p4gy";
        };
      }

      {
        name = "foreign-env";
        src = pkgs.fetchFromGitHub {
          owner = "oh-my-fish";
          repo = "plugin-foreign-env";
          rev = "dddd9213272a0ab848d474d0cbde12ad034e65bc";
          sha256 = "00xqlyl3lffc5l0viin1nyp819wf81fncqyz87jx8ljjdhilmgbs";
        };
      }
    ]
  };
}

The options for pkgs.fetchFromGithub are:

  • owner: The Github repo owner name.
  • repo: Guess what.
  • rev: The commit hash or tag.
  • sha256: The hash of the extracted directory.

To find the correct value for sha256:

  1. Set sha256 = lib.fakeSha256.
  2. Run home-manager switch.

In the output, you should see something similar to:

hash mismatch in fixed-output derivation '/nix/store/74010535gk31hpxnmsbwda8dgz2i8ajq-source':
  wanted: sha256:0000000000000000000000000000000000000000000000000000
  got: sha256:00xqlyl3lffc5l0viin1nyp819wf81fncqyz87jx8ljjdhilmgbs

You can now copy that sha256 hash.

Oh My Fish Themes

You can install Oh My Fish themes just the same way:

  programs.fish = {
    # ...

    plugins = [
      # ...

      {
        name = "bobthefish";
        src = pkgs.fetchFromGitHub {
          owner = "oh-my-fish";
          repo = "theme-bobthefish";
          rev = "a2ad38aa051aaed25ae3bd6129986e7f27d42d7b";
          sha256 = "1fssb5bqd2d7856gsylf93d28n3rw4rlqkhbg120j5ng27c7v7lq";
        };
      }
    ]
  };

If you have a look at .config/fish/conf.d/plugin-bobthefish.fish (or any other plugin file in the same folder), you will see that by default, only $plugin_dir/conf.d/*.fish, $plugin_dir/key_bindings.fish and $plugin_dir/init.fish are sourced. However, Oh My Fish themes have all the necessary fish files in the root directory of the plugin. To actually activate the theme, you need to append the plugin file like this:

{ config, pkgs, lib, ... }:

{
  # ...
  programs.fish = {
    # ...
  };

  xdg.configFile."fish/conf.d/plugin-bobthefish.fish".text = lib.mkAfter ''
    for f in $plugin_dir/*.fish
      source $f
    end
    '';
}

After you run home-manager switch, .config/fish/conf.d/plugin-bobthefish.fish will be updated and the theme will be activated when you open a new shell.

Sourcing the Nix profile

In the current state, the nix profile is not sourced automatically. To change that, add this:

  programs.fish = {
    # ...

    loginShellInit = ''
      if test -e /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
        fenv source /nix/var/nix/profiles/default/etc/profile.d/nix-daemon.sh
      end

      if test -e /nix/var/nix/profiles/default/etc/profile.d/nix.sh
        fenv source /nix/var/nix/profiles/default/etc/profile.d/nix.sh
      end
      '';
  };

Note that this code makes use of the foreign-env plugin installed above.

Set up a config repository

I think it is a good idea to put your configuration into a Git repository. One way to do that is described in the tutorial The best way to store your dotfiles: A bare Git repository.

Basically you initialize a bare git repository and define an alias to make working with this repo easier.

git init --bare $HOME/.cfg
alias config='git --git-dir=$HOME/.cfg/ --work-tree=$HOME'
config config --local status.showUntrackedFiles no

You can now commit the config that we have so far with:

config add ~/.config/nixpkgs
config commit -m "add nix configuration"

To make the alias always available in Fish, add it to .config/nixpkgs/home.nix:

  programs.fish = {
    # ...

    shellAliases = {
      config = "git --git-dir=$HOME/.cfg/ --work-tree=$HOME";
    };
  };

You can set up a new machine like this:

git clone --bare <git-repo-url> $HOME/.cfg
alias config='git --git-dir=$HOME/.cfg/ --work-tree=$HOME'
config config --local status.showUntrackedFiles no
config checkout

Outside of Nix

Brew

You can now install nearly all the packages you need with Nix instead of Homebrew, but if you want to install GUI tools via the command line, you will still need to use Homebrew Cask. To make it easier to sync the casks across machines, you can create a Brewfile and add it to the config repo.

If you already have casks installed and want to create a Brewfile for the first time, run:

brew bundle dump

The result will look something like this:

tap "homebrew/bundle"
tap "homebrew/cask"
tap "homebrew/core"
tap "homebrew/services"
cask "docker"
cask "firefox"
cask "flux"
cask "iterm2"
cask "karabiner-elements"
cask "keybase"
cask "sublime-merge"
cask "sublime-text"
cask "typora"

You can add this file to the repo. To install new packages after editing the file, run:

brew bundle install

To uninstall packages after removing them from the file, run:

brew bundle cleanup -f

You can optionally install mas to manage software that is installed via the App Store in the Brewfile as well (see Homebrew Bundle readme).

iTerm2

iTerm2 has an option to load the preferences from a custom location (iTerm2 → Preferences → General → Preferences → Load preferences from a custom folder or URL).

You can set the location to ~/.config/iterm2, click Save Current Settings to Folder and add the config to the repo.

Sublime Text 3

You can copy the User folder of Sublime Text 3 to the config folder, delete the original directory and create a symlink.

mkdir ~/.config/sublime-text-3
cp ~/Library/Application\ Support/Sublime\ Text\ 3/Packages/User ~/.config/sublime-text-3
rm -rf ~/Library/Application\ Support/Sublime\ Text\ 3/Packages/User
ln -s ~/.config/sublime-text-3/User/ ~/Library/Application\ Support/Sublime\ Text\ 3/Packages/User

Add a .gitignore file:

echo ".SublimeREPLHistory
oscrypto-ca-bundle.crt
Package Control.ca-bundle
Package Control.ca-certs/
Package Control.ca-list
Package Control.cache/
Package Control.last-run
Package Control.merged-ca-bundle
Package Control.system-ca-bundle
Package Control.user-ca-bundle" >> ~/.config/sublime-text-3/User/.gitignore

And commit the folder to the repo.

Nix Shell

Instead of making packages available globally, you can add a file called shell.nix in your projects, which may look something like this:

{ pkgs ? import <nixpkgs> {} }:

with pkgs;

mkShell {
  buildInputs = [
    elixir_1_10
    nodejs-12_x
  ];
}

You can get into a shell with these packages available by running:

nix-shell

Direnv

You can use direnv to automatically load the environment when changing into a project folder.

Add this to ~/.config/nixpkgs/home.nix:

{ config, pkgs, lib, ... }:

{
  # ...

  programs.direnv.enable = true;
  programs.direnv.enableNixDirenvIntegration = true;

  programs.fish = {
    # ...

    loginShellInit = ''
      # ...
      eval (direnv hook fish)
      '';
  };
}

Don’t forget to run home-manager switch.

Create a file called .envrc in your project directory.

echo use_nix > .envrc

Then run:

direnv allow .

From now on, the environment will be automatically loaded whenever you cd into the directory and unloaded when you leave it.

Next

I haven’t looked into handling services like databases or message brokers in a Nix shell environment yet. One solution for running a local PostgreSQL database is described in the article Using Nix in Elixir projects.

If you spotted an error, need a random compliment or have any other remarks, don’t hesitate to drop a message.