Nix on WSL

nix wsl

I constantly hop to three hetrogenous development environments: macOS for daily drive; Ubuntu on WSL for side projects, including this blog. Unlike the production environment, — a VPS server orchestrated by saltstack; the personal development environments are loosely managed. I installed packages in an ad-hoc fashion, and managed the dotfiles with vcsh.

I considered to adopt some configuration orchestation for easy replication. Maybe not saltstack, as it feels rather heavyweight. I also felt a little overwhelmed by the cognitive overhead, cast by the plugin manager, such as vim-plug, prezto, and tpm; and version managers, such as pyenv, rbenv, and nvm.

Introduction to Nix

My colleague, Matt introduced me to the Nix, a tool to make

reproducible, declarative and reliable systems

Nix takes a unique approach for package management: it abandoned the Filesystem Hierarchy Standard (FHS) maintained by the Linux Foundation. The packages are stored in the nix/store, such as /nix/store/176bs3fwqhhvbha5ddvfkzz85aq5qvkm-libevent-2.1.12. The shared objects, aka .so files, are NOT copied to the RPATH. They are linked directly from the store, and executables are symbol linked to the shell search path. Using tmux as a concrete example:

 ldd ~/.nix-profile/bin/tmux
        linux-vdso.so.1 (0x00007ffec29f8000)
        libutil.so.1 => /nix/store/a3syww9igm49zdzq3ibzw9m8ccvsgxla-glibc-2.32/lib/libutil.so.1 (0x00007f82535bc000)
        libncursesw.so.6 => /nix/store/j2y9dgxlp4g5vfqcsylzdlci3bygrmlv-ncurses-6.2/lib/libncursesw.so.6 (0x00007f8253548000)
        libevent-2.1.so.7 => /nix/store/176bs3fwqhhvbha5ddvfkzz85aq5qvkm-libevent-2.1.12/lib/libevent-2.1.so.7 (0x00007f82534ee000)
        libresolv.so.2 => /nix/store/a3syww9igm49zdzq3ibzw9m8ccvsgxla-glibc-2.32/lib/libresolv.so.2 (0x00007f82534d5000)
        libc.so.6 => /nix/store/a3syww9igm49zdzq3ibzw9m8ccvsgxla-glibc-2.32/lib/libc.so.6 (0x00007f8253314000)
        libpthread.so.0 => /nix/store/a3syww9igm49zdzq3ibzw9m8ccvsgxla-glibc-2.32/lib/libpthread.so.0 (0x00007f82532f1000)
        /nix/store/a3syww9igm49zdzq3ibzw9m8ccvsgxla-glibc-2.32/lib/ld-linux-x86-64.so.2 => /nix/store/a3syww9igm49zdzq3ibzw9m8ccvsgxla-glibc-2.32/lib64/ld-linux-x86-64.so.2 (0x00007f82535c3000)

This approach allows multiple-versioned shared objects stored in the system, since they no longer compete the same slot in the /usr/lib. We can install multiple-versioned node, but only one can be symbol linked in the shell search path. Nix has nix-shell to cope with this limitation: it cherry pick artifacts to represent the system state in a different perspective.

NixOS and home manager leverage the Nix expression for declarative system states, such as services, and dotfiles. We will revisit the NixOps when migrating the VPS server to NixOS.

Nix on WSL

Assume we use the Debain as our base system, first install the required packages to bootstrap the Nixpkgs.

sudo apt-get update
sudo apt-get install curl xz-utils ca-certificates

Install the nixpkgs, and source the nix.sh to enter the promised land.

curl -L https://nixos.org/nix/install | sh
source $HOME/.nix-profile/etc/profile.d/nix.sh

Note we do not install the full-fledged NixOS in WSL as the the systemd in WSL still lack some key features. I also prefer managing the dependent services via docker containers.

Subscribe the home-manager channel, and install the home-manager.

nix-channel --add https://github.com/nix-community/home-manager/archive/master.tar.gz home-manager

nix-channel --update

nix-shell '<home-manager>' -A install

Then we can edit $HOME/.config/nixpkgs/home.nix, and run home-manager switch to apply the change. Here is one simple example:

{ config, pkgs, ... }:

let
  locale = "C.UTF-8";
  homedir = builtins.getEnv "HOME";
  username = builtins.getEnv "USER";
in
{
  home = {
    packages = with pkgs; [
      htop
      ripgrep
    ];
    sessionVariables = {
      LANG = locale;
      LC_ALL = locale;
      TMUX_TMPDIR = "/tmp";
    };

    username = username;
    homeDirectory = homedir;
    stateVersion = "21.03";
  };

  programs.home-manager.enable = true;

  # tmux
  programs.tmux = {
    enable = true;
    terminal = "screen-256color";
    shortcut = "a";
    baseIndex = 1;
    keyMode = "vi";
    sensibleOnTop  = true;
    plugins = with pkgs; [
      tmuxPlugins.pain-control
      tmuxPlugins.copycat
      tmuxPlugins.yank
      tmuxPlugins.logging
    ];
  }
}

In this code snippet:

You may checkout my nix-config here.

Troubleshooting

You may encounter the following permission error as being written:

nix-shell '<home-manager>' -A install
...
error: moving build output '/nix/store/nnj0sc87fycmcv97inqvdzl1ghr1gwkp-nmd' from the sandbox to the Nix store: Permission denied

This is due to the Nix#4295, you can workaround this by setting sandbox = false in /etc/nix/nix.conf.

You may also encounter the error that tmux complains that the path /run/user/1000 is not found during launch. This is caused by the default secureSocket option, see here. We can explicitly setup TMUX_TMPDIR for single-user mode.

On-demand environment

With nix-shell and direnv, we can jump to a dedicated working environment when switch the current work directory. First, let’s enable the direnv in the home.nix:

  # Use nix-direnv integration
  programs.direnv = {
    enable = true;
    nix-direnv.enable = true;
  };

Then in the root of the project folder, create a shell.nix such as:

{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell rec {
  buildInputs = with pkgs; [
    nodejs-14_x
    (yarn.override { nodejs = nodejs-14_x; })
  ];
}

This would create a nix-shell with latest nodejs-14.x with yarn installed.

Closing thoughts

Nix and home manage provides a cohesive abstraction for package and configuration management. It was quite delightful to work with Nix expressions as a novice. I would continue explore the Nix ecosystem, stay tuned.