zimbatm edited this page Nov 23, 2018 · 23 revisions

Nix is a functional package manager. Everything is explained at: http://nixos.org/nix/

Stdlib

Direnv now supports nix as part of its stdlib.

# Usage: use_nix [...]
#
# Load environment variables from `nix-shell`.
# If you have a `default.nix` or `shell.nix` these will be
# used by default, but you can also specify packages directly
# (e.g `use nix -p ocaml`).

So you can use nix in the .envrc of any project set up with a shell.nix or default.nix and get the appropriate environment set up. Even better: you get that environment in you current shell as maintained by direnv instead of a subshell.

Also, if you edit shell.nix or default.nix, the environment will be re-evaluated automatically.

Given that Nix can set up your environment to the point of arranging binaries etc, you should be able to supplant all the layout calls with this, if you were so inclined.

Other approaches

Alternatively, you could use this is a small recipe that adds per-project custom profiles. Add this in your ~/.direnvrc:

use_nix() {
  source "$HOME/.nix-profile/etc/profile.d/nix.sh"
  export NIX_PATH=/nix/var/nix/profiles/per-user/$USER/channels
  export NIX_PROFILE=$PWD/.direnv/nix
  load_prefix "$PWD/.direnv/nix"
}

Then whenever nix-env -i is invoked from within the project, all the dependencies are installed only under that profile (but global dependencies are still accessible on the PATH).

Speeding things up

As suggested from @Mic92 https://github.com/direnv/direnv/issues/238

If the nix-shell holds enough packages, it can take some seconds to construct the environment. The following approach will use the global nixos version as a cache key to create it only once.

# ~/.direnvrc
use_nix() {
  local cache=".direnv.$(nixos-version --hash)"
  if [[ ! -e "$cache" ]] || \
     [[ "$HOME/.direnvrc" -nt "$cache" ]] || \
     [[ ".envrc" -nt "$cache" ]] || \
     [[ "default.nix" -nt "$cache" ]] || \
     [[ "shell.nix" -nt "$cache" ]];
  then
    local tmp="$(mktemp "${cache}.tmp-XXXXXXXX")"
    trap "rm -rf '$tmp'" EXIT
    nix-shell --show-trace "$@" --run 'direnv dump' > "$tmp" && \
      mv "$tmp" "$cache"
  fi
  direnv_load cat "$cache"
  if [[ $# = 0 ]]; then
    watch_file default.nix
    watch_file shell.nix
  fi
}

Persistent shell

Based on https://gist.github.com/aherrmann/51b56283f9ed5853747908fbab907316

One issue with use_nix is that if the channel gets updated or the user runs the nix GC, the shell has to be re-built upon entry. This might not always be desirable.

This approach builds the shell on first entry and only re-builds it if the shell.nix file has changed (based on mtime). And because the build results are added as GC roots, they don't get garbage-collected.

# Usage: use nix_shell
#
# Works like use_nix, except that it's only rebuilt if the shell.nix or default.nix file changes.
# This avoids scenarios where the nix-channel is being updated and all the projects now need to be re-built.
#
# To force the reload the derivation, run `touch shell.nix`
use_nix() {
  local shellfile=shell.nix
  local wd=$PWD/.direnv/nix
  local drvfile=$wd/shell.drv
  local outfile=$ws/result

  # same heuristic as nix-shell
  if [[ ! -f $shellfile ]]; then
    shellfile=default.nix
  fi

  if [[ ! -f $shellfile ]]; then
    fail "use nix_shell: shell.nix or default.nix not found in the folder"
  fi

  if [[ -f $drvfile && $(stat -c %Y "$shellfile") -gt $(stat -c %Y "$drvfile") ]]; then
    log_status "use nix_shell: removing stale drv"
    rm "$drvfile"
  fi

  if [[ ! -f $drvfile ]]; then
    mkdir -p "$wd"
    # instanciate the drv like it was in a nix-shell
    IN_NIX_SHELL=1 nix-instantiate \
      --show-trace \
      --add-root "$drvfile" --indirect \
      "$shellfile" >/dev/null
  fi

  direnv_load nix-shell "$drvfile" --run "$(join_args "$direnv" dump)"
  watch_file "$shellfile"
}

Persistent cached shell

A solution, which provides persistent, cached shells.

# Usage: use_nix [...]
#
# Load environment variables from `nix-shell`.
# If you have a `default.nix` or `shell.nix` one of these will be used and
# the derived environment will be stored at ./.direnv/env-<hash>
# and symlink to it will be created at ./.direnv/default.
# Dependencies are added to the GC roots, such that the environment remains persistent.
#
# Packages can also be specified directly via e.g `use nix -p ocaml`,
# however those will not be added to the GC roots.
#
# The resulting environment is cached for better performance.
#
# To trigger switch to a different environment:
# `rm -f .direnv/default`
#
# To derive a new environment:
# `rm -rf .direnv/env-$(md5sum {shell,default}.nix 2> /dev/null | cut -c -32)`
#
# To remove cache:
# `rm -f .direnv/dump-*`
#
# To remove all environments:
# `rm -rf .direnv/env-*`
#
# To remove only old environments: 
# `find .direnv -name 'env-*' -and -not -name `readlink .direnv/default` -exec rm -rf {} +`
#
use_nix() {
    set -e

    local shell="shell.nix"
    if [[ ! -f "${shell}" ]]; then
        shell="default.nix"
    fi

    if [[ ! -f "${shell}" ]]; then
        fail "use nix: shell.nix or default.nix not found in the folder"
    fi

    local dir="${PWD}"/.direnv
    local default="${dir}/default"
    if [[ ! -L "${default}" ]] || [[ ! -d `readlink "${default}"` ]]; then
        local wd="${dir}/env-`md5sum "${shell}" | cut -c -32`" # TODO: Hash also the nixpkgs version?
        mkdir -p "${wd}"

        local drv="${wd}/env.drv"
        if [[ ! -f "${drv}" ]]; then
            log_status "use nix: deriving new environment"
            IN_NIX_SHELL=1 nix-instantiate --add-root "${drv}" --indirect "${shell}" > /dev/null
            nix-store -r `nix-store --query --references "${drv}"` --add-root "${wd}/dep" --indirect > /dev/null
        fi

        rm -f "${default}"
        ln -s `basename "${wd}"` "${default}"
    fi

    local drv=`readlink -f "${default}/env.drv"`
    local dump="${dir}/dump-`md5sum ".envrc" | cut -c -32`-`md5sum ${drv} | cut -c -32`"

    if [[ ! -f "${dump}" ]] || [[ "${XDG_CONFIG_DIR}/direnv/direnvrc" -nt "${dump}" ]]; then
        log_status "use nix: updating cache"

        old=`find "${dir}" -name 'dump-*'`
        nix-shell "${drv}" --show-trace "$@" --run 'direnv dump' > "${dump}"
        rm -f ${old}
    fi

    direnv_load cat "${dump}"

    watch_file "${default}"
    watch_file shell.nix
    if [[ ${shell} == "default.nix" ]]; then
        watch_file default.nix
    fi
}

Shell function to quickly setup nix + direnv in a new project

# put this either in bashrc or zshrc
nixify() {
  if [ ! -e ./.envrc ]; then
    echo "use nix" > .envrc
    direnv allow
  fi
  if [ ! -e default.nix ]; then
    cat > default.nix <<'EOF'
with import <nixpkgs> {};
stdenv.mkDerivation {
  name = "env";
  buildInputs = [
    bashInteractive
  ];
}
EOF
    ${EDITOR:-vim} default.nix
  fi
}
You can’t perform that action at this time.
You signed in with another tab or window. Reload to refresh your session. You signed out in another tab or window. Reload to refresh your session.
Press h to open a hovercard with more details.