Ever since I became a NixOS hobbyist a few years ago, it’s easy to plug NixOS wherever I go. The way flakes create a reproducible development environment across projects is so easy. I could clone any repository with a flake.nix or a shell.nix file, run a simple nix develop (or nix-shell), and be completely ready to start writing code without doing any additional setup. It configures both the dependencies and environment variables I need and plops me straight into a shell that has everything set up.

michael in 🌐 molecule in liveterm on  master [⇡] is 📦 v0.1.0 via 🦀 v1.68.0-nightly
❯ nix develop

[michael@molecule:~/Projects/liveterm]$ █

To make things even easier, direnv (along with nix-direnv) can insert shell hooks so that I don’t even have to run any commands; just going into the directory itself triggers a hook that sets up my current shell, so I can keep all of my fancy prompts and highlighting and other shell features.

michael in 🌐 molecule in ~
❯ j liveterm
/home/michael/Projects/liveterm
direnv: loading ~/Projects/liveterm/.envrc
direnv: using flake
direnv: nix-direnv: using cached dev shell
direnv: using flake
direnv: nix-direnv: using cached dev shell
direnv: export +AR +AS +CC +CONFIG_SHELL +CXX +DETERMINISTIC_BUILD +HOST_PATH +IN_NIX_SHELL +LD +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_BUILD_CORES +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_CFLAGS_COMPILE +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_INDENT_MAKE +NIX_LDFLAGS +NIX_PKG_CONFIG_WRAPPER_TARGET_HOST_x86_64_unknown_linux_gnu +NIX_SSL_CERT_FILE +NIX_STORE +NM +OBJCOPY +OBJDUMP +PKG_CONFIG +PKG_CONFIG_PATH +PYTHONHASHSEED +PYTHONNOUSERSITE +PYTHONPATH +RANLIB +READELF +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +SYSTEM_CERTIFICATE_PATH +_PYTHON_HOST_PLATFORM +_PYTHON_SYSCONFIGDATA_NAME +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH ~XDG_DATA_DIRS

michael in 🌐 molecule in liveterm on  master [⇡] is 📦 v0.1.0 via 🦀 v1.68.0-nightly via ❄️  impure (nix-shell)
❯ █

The reason behind this is that on NixOS, I am able to prevent cluttering my global environment with project-specific configurations. While I do still have a global Node and Python for testing one-off things, all of my projects have their own flake file, that locks specific versions of Node and Python so I can be sure it builds in the future.

But what about projects that don’t have a flake definition file? Without some kind of existing configuration, I have no dependencies, and if I want to even build it, I would have to write a flake myself. That’s fine and all, but typically the flake.nix file lives in the root directory of the project, and it’s good practice in the Nix world to commit the flake file along with its corresponding lock file. However, the upstream project may not appreciate it if I shove new config files in their root directory.

  project
    ...
     .envrc ✗
     flake.lock ✗
     flake.nix ✗

NOTE

The indicates that I added the file to the project, and it hasn’t been committed to the repo yet.

One way to fix this is just to never commit the flake file. Always use explicit names with git add, and constantly check git status to make sure the file isn’t committed. While this is good practice anyway, it gets quite cumbersome. Some upstream projects may also be ok with adding entries into the .gitignore file for the flake files, but I wouldn’t be writing about it if these were the only solutions!

Separating the flake from the repo

direnv uses a file called .envrc to configure setup instructions whenever you go into the directory (or subdirectories). For a normal flake setup, a simple config would look something like this:

use flake

This would query for the default dev shell found in the current directory’s flake and set up my current shell accordingly. I figured this would probably take parameters, and unsurprisingly, it does! So my approach is just to create a separate directory alongside the git repository that just contains Nix flake files.

  project
    ...
     .envrc ✗
  project-dev-flake
     flake.lock
     flake.nix

Now, the flake exists in a separate directory outside of the git repo. Now .envrc needs to be updated to point to this new directory:

use flake /path/to/project-dev-flake

If you have multiple dev shells, you can also use the project-dev-flake#shell syntax to point to whichever shell you would like to automatically enter.


Ok great, now the flake file’s out of the way. But we still have this .envrc that needs to exist. This file defines the behavior of the shell hook of the directory we’re in, so in the repo for us for the hook to trigger …right?

Separating the .envrc file from the repo

So actually, the .envrc file conveniently affects all of the subdirectories of the directory the file is in, not just the current one. This way if you cd somewhere within your project hierarchy, you’re not losing all the shell hook behavior.

We can use this by moving the git repo into the dev flake instead. So now the project structure should look a bit more like this:

  project-dev-flake
    project
     .envrc
     flake.lock
     flake.nix

NOTE

Remember, since you moved the .envrc file, you will need to run direnv allow again. Depending on how you moved it, you might also need to change the path you wrote in the use flake command.

With this setup, the project directory can contain a clean clone of upstream and your flake files will create the appropriate environment.

This does create an extra layer of directory nesting, but except for copying longer paths, it really doesn’t hurt my workflow. I use autojump, which automatically just remembers where paths are, so I can just type j <project> to go to my project’s directory directly. It’s sorted by frequency, so as long as I don’t visit the -dev-flake container directory more often, my workflow doesn’t change at all.


I hope this helps you set up projects to contribute to non-NixOS projects a bit easier!