How do you maintain development environment nix shells?

I’m trying to avoid installing develop environment for different programming languages all into environment.systemPackages.

I’m using NixOS & flakes, and I need some best practice suggestion about maintaining these development environment nix shells, considering these following points:

  1. every programming language have a basic development environment nix shell.
  2. each main programming framework of specific programming language might have a development environment nix shell based on this language’s basic shell.
  3. each project might have it’s own nix shell based on it’s languages’ basic shell or framework’s basic shell.
  4. we need a convenient way to switch between different nix shells
  5. we need a convenient way to make these nix shells available for IDEs

Do you have the same problem? And what is your using/suggested solution?

Have you looked at direnv?

It has nix integration and can automatically load nix shells. Theres IDE plugins available too.

For different language shells, you can make a flake with templates and use those to instantiate project shells.

1 Like

I create a separate flake.nix file for every repository, and add a .envrc file with the content use flake to enable direnv to automatically enable the environment when I cd into the directory.

There is also a direnv plugin for vscode and one for IntellJ, so using direnv solves problems 4 and 5. You might find some info online about installing direnv-nix integration, but this isn’t necessary anymore, direnv supports nix-shell and nix flake out-of-the-box these days.

Problems 1-3 sound like you want some sort of basic inheritance, but you could achieve that with functions quite easily.

With flakes, you can have a separate repository (let’s say “env-templates”) from which every repository imports its base configuration.
You can get an idea for these in the official templates repository.

These templates are designed to be inserted as a file into your repository on initialization (with nix flake init -t or nix flake new -t), but I assume you will want to update the base configurations centrally, so a better idea is to add the flakes in your “env-templates” as inputs to your environments.

A base configuration might look like this:

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/23.05";

  outputs = { self, nixpkgs }:
    let
      supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
      pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system});
    in
    {
      mkShell = { addPackages ? [], addShellHook ? "", overridePackages ? [], overrideShellHook ? "" }:
      forAllSystems (system: {
        pkgs.${system}.mkShellNoCC {
          packages = with pkgs.${system};  (if overridePackages == [] then [
            python3
          ] else overridePackages) ++ extraPackages;

          shellHook = (if overrideShellHook == "" then ''
             MY_ENV_VAR=1
          '' else overrideShellHook) ++ addShellHook;
          
        };
      });
    };
}

Which then (if it has the path python/flake.nix in a repository env-templates belonging to louchen on gitlab) can be imported in a project like this:

{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/23.05";

  inputs.python-env.url = "gitlab:louchen/env-templates?dir=python";
  inputs.python-env.inputs.nixpkgs.follows "nixpkgs";

  outputs = { self, nixpkgs, python-env }:
    let
      supportedSystems = [ "x86_64-linux" "x86_64-darwin" "aarch64-linux" "aarch64-darwin" ];
      forAllSystems = nixpkgs.lib.genAttrs supportedSystems;
      pkgs = forAllSystems (system: nixpkgs.legacyPackages.${system});
    in
    {
      devShells = forAllSystems (system: {
        default = python-env.mkShell.${system} {
          extraPackages = with pkgs; [ jq ];
        };
      });
    };
}

devShells is the output that nix develop and direnv use to set up the environment.

This only shows one level; we’re building the environment for the project flake from the language flake directly, but you can extrapolate from this and add another layer for frameworks.

You can also simplify this code quite a bit with flake-utils, and the setup for different languages might not be as simple as what I presented here (for example, with python you need to use python3.withPackages to install python packages into your environment), but a general structure like this should work quite well.

1 Like