Nix and Direnv with Flakes
Last year I wrote about nix and direnv as I explored the potential convenience of an isolated, project-specific environment. There were some interesting initial learnings about nix, but I didn’t really know what I was doing. Now, I still don’t know what I’m doing, but I’ve been doing it for longer. As an example, I’m going to walk through how I set up a flake-driven development environment for this blog with direnv.
My blog is built with Hugo. I also use Python to run content generation and extraction scripts, which require a few Python libraries. I needed to write a flake to use nix to install all these components.
There seems to be a lot of different ways to write flakes.
I didn’t know which to pick, so created a flake.nix
file and prompted gpt-4 via Cursor to
write a flake that provides a dev shell with hugo and python
It outputted
{ description = "A shell with Hugo and Python";
inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; };
outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; in { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ pythonEnv hugo ]; }; });}
Next I prompted it to install the python library arrow
and it gave me
{ description = "A shell with Hugo and Python";
inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; flake-utils.url = "github:numtide/flake-utils"; };
outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; pythonEnv = pkgs.python3.withPackages (ps: with ps; [ arrow ]); in { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ pythonEnv hugo ]; }; });}
This structure was enough to get where I needed for this project.
It sounds like some folks believe the use of flake-utils
is an anti-pattern.
I also came across flake-parts
while looking for ways to solve this problem.
As I iterated, after adding each new piece, I ran nix develop -c $SHELL
(thanks Davis for this tip) to validate the flake would build and that the dependency worked within the environment (e.g. I would run python
then import pytz
to confirm the library had been installed).
This is the final product:
{ description = "My Blog built with Hugo";
inputs = { nixpkgs.url = "github:nixos/nixpkgs/release-23.11"; flake-utils.url = "github:numtide/flake-utils"; };
outputs = { self, nixpkgs, flake-utils, ... }: flake-utils.lib.eachDefaultSystem (system: let pkgs = import nixpkgs { inherit system; }; pythonEnv = pkgs.python3.withPackages (ps: with ps; [ arrow python-frontmatter pytz sqlite-utils ]); in { devShells.default = pkgs.mkShell { buildInputs = with pkgs; [ pythonEnv hugo ]; }; });}
To wire up the auto-activation, I created an .envrc
with the following content
use flake
Finally, I ran direnv allow
within my blog folder.
Now, when I cd into this folder, I’m immediately dropped into an environment containing all the dependencies defined in the flake (direnv also continues to use the same shell so I don’t need to worry about specifying it manually as before).
When I cd out, these are all unloaded so they don’t clutter up my system.
$ cd blogdirenv: loading ~/dev/blog/.envrcdirenv: using flakedirenv: nix-direnv: using cached dev shelldirenv: export +AR +AS +CC +CONFIG_SHELL +CXX +HOST_PATH +IN_NIX_SHELL +LD +LD_DYLD_PATH +MACOSX_DEPLOYMENT_TARGET +NIX_BINTOOLS +NIX_BINTOOLS_WRAPPER_TARGET_HOST_aarch64_apple_darwin +NIX_BUILD_CORES +NIX_CC +NIX_CC_WRAPPER_TARGET_HOST_aarch64_apple_darwin +NIX_CFLAGS_COMPILE +NIX_DONT_SET_RPATH +NIX_DONT_SET_RPATH_FOR_BUILD +NIX_ENFORCE_NO_NATIVE +NIX_HARDENING_ENABLE +NIX_IGNORE_LD_THROUGH_GCC +NIX_LDFLAGS +NIX_NO_SELF_RPATH +NIX_STORE +NM +PATH_LOCALE +RANLIB +SIZE +SOURCE_DATE_EPOCH +STRINGS +STRIP +__darwinAllowLocalNetworking +__impureHostDeps +__propagatedImpureHostDeps +__propagatedSandboxProfile +__sandboxProfile +__structuredAttrs +buildInputs +buildPhase +builder +cmakeFlags +configureFlags +depsBuildBuild +depsBuildBuildPropagated +depsBuildTarget +depsBuildTargetPropagated +depsHostHost +depsHostHostPropagated +depsTargetTarget +depsTargetTargetPropagated +doCheck +doInstallCheck +dontAddDisableDepTrack +mesonFlags +name +nativeBuildInputs +out +outputs +patches +phases +preferLocalBuild +propagatedBuildInputs +propagatedNativeBuildInputs +shell +shellHook +stdenv +strictDeps +system ~PATH
It was nice to get this working end to end and get a perspective on what developer experience could look like with nix, but still somewhat unsatisfying to not have a consistent starting point for creating a flake for a project.
I plan to continue researching flake-parts
, flake-utils
and flake templates to get a sense of best practices in this area.
Recommended
Managing Multiple Tool Versions with Nix
This post is extremely similar to nix flakes and direnv. Here, I repeated my process, but with a little more thought and a little less language model...
Installing Python Packages with Nix
I've been meaning to try out Simon's llm package for a while now. From reading the docs and following the development, it's a modular,...
Sandboxed Python Environment
Disclaimer: I am not a security expert or a security professional.