Panfactum LogoPanfactum
Development ShellCustomizing

Customizing the devShell

The Panfactum stack comes with a preloaded "devShell" that contains both essential and convenience utilities for working with the stack. This shell automatically launches when you open your organization's stack repository in a terminal.

You can also include additional utilities that are specific to your organization. This guide walks through the most common customization options.

Initial Installation

When you first install the devShell, the flake.nix in the root of your repository should have the following contents:

{
  inputs = {
    flake-utils.url = "github:numtide/flake-utils"; # Utility for generating flakes that are compatible with all operating systems
    panfactum.url = "github:panfactum/stack/edge.24-11-13"; # Make sure this matches your version of the Panfactum Stack
  };

  outputs = { panfactum, flake-utils, ... }@inputs:
    flake-utils.lib.eachDefaultSystem
    (system:
      {
        devShell = panfactum.lib.${system}.mkDevShell { };
      }
    );
}

Adding New Tools

panfactum.lib.${system}.mkDevShell takes an input called packages that can take addition nix packages (derivations) to be added to your devShell. There are a few common ways to do this:

From nixpkgs

nixpkgs is the largest software repository in the world (reference), so an enormous amount of tooling is readily available to you.

To add a package such as nodejs_20, update your flake.nix with the following:

{
  inputs = {
    flake-utils.url = "github:numtide/flake-utils"; # Utility for generating flakes that are compatible with all operating systems
    panfactum.url = "github:panfactum/stack/edge.24-11-13"; # Make sure this matches your version of the Panfactum Stack
+   nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; # 'nixos-24.05' can be any ref from the nixpkgs repo https://github.com/NixOS/nixpkgs
  };

+ outputs = { nixpkgs, panfactum, flake-utils, ... }@inputs:
    flake-utils.lib.eachDefaultSystem
    (system:
+     let
+        pkgs = import nixpkgs {
+         inherit system;
+         config = { allowUnfree = true; };
+       };
+     in
      {
        devShell = panfactum.lib.${system}.mkDevShell {
+         packages = [
+           pkgs.nodejs_20
+         ];
        };
      }
    );
}

All packages defined in the nixpkgs GitHub repo are updated together, and many updates are made to the repository each day. pkgs refers to the set of packages defined by a specific commit in nixpkgs. That commit is determined by the nixpkgs.url field in your flake.nix.

However, if you specify a branch such as nixos-24.05 (via inputs.pkgs.url = "github:NixOS/nixpkgs/nixos-24.05;"), the commit hash is pinned in the flake.lock file and will not be automatically updated when you run nix flake update.

In general, we recommend pinning the commit directly in the flake.nix to avoid ambiguity. For example, nixpkgs.url = "github:NixOS/nixpkgs/c8e74c2f83fe12b4e5a8bd1abbc090575b0f7611";.

You may find that your want to include specific versions of separately tools that are on different commits of nixpkgs from each other. Fortunately, you can very easily include multiple commits of "views" of nixpkgs:

{
  inputs = {
    flake-utils.url = "github:numtide/flake-utils";
    panfactum.url = "github:panfactum/stack/edge.24-11-13";
+   nixpkgs1.url = "github:NixOS/nixpkgs/c8e74c2f83fe12b4e5a8bd1abbc090575b0f7611";
+   nixpkgs2.url = "github:NixOS/nixpkgs/dd53ef232cf8398c6832dcc9e6c4194bf109e70e";
  };

+ outputs = { nixpkgs1, nixpkgs2, panfactum, flake-utils, ... }@inputs:
    flake-utils.lib.eachDefaultSystem
    (system:
+     let
+        pkgs1 = import nixpkgs1 {
+         inherit system;
+         config = { allowUnfree = true; };
+       };
+        pkgs2 = import nixpkgs2 {
+         inherit system;
+         config = { allowUnfree = true; };
+       };
+     in
      {
        devShell = panfactum.lib.${system}.mkDevShell {
+         packages = [
+           pkgs1.nodejs_20
+           pkgs2.python3
+         ];
        };
      }
    );
}

Custom Scripts

You may want to include custom shell scripts in your developer environment.

Imagine that you have the following script called foobar.sh that you want users to be able to call by executing foobar anywhere in the developer environment:

#!/usr/bin/env bash

echo "Foobar"

You would include this in your flake.nix as follows:

{
  inputs = {
    flake-utils.url = "github:numtide/flake-utils"; # Utility for generating flakes that are compatible with all operating systems
    panfactum.url = "github:panfactum/stack/edge.24-11-13"; # Make sure this matches your version of the Panfactum Stack
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; # 'nixos-24.05' can be any ref from the nixpkgs repo https://github.com/NixOS/nixpkgs
  };

  outputs = { nixpkgs, panfactum, flake-utils, ... }@inputs:
    flake-utils.lib.eachDefaultSystem
    (system:
      let
         pkgs = import nixpkgs {
          inherit system;
          config = { allowUnfree = true; };
        };
     in
      {
        devShell = panfactum.lib.${system}.mkDevShell {
          packages = [
+           (pkgs.writeShellScriptBin "foobar" (builtins.readFile ./foobar.sh))
          ];
        };
      }
    );
}

Wrapping an Existing Tool

Occasionally, you may want to use a package but automatically pass in default flags or environment variables for users of your developer environment.

There is a handy package called makeWrapper that allows you to do this.

For example, let's say you want to always add -n cilium to calls of the cilium CLI. Simply create the following cilium.nix file (make sure to git add it):

{ pkgs }:
pkgs.symlinkJoin {
  name = "cilium-cli";
  paths = [ pkgs.cilium-cli ];
  buildInputs = [ pkgs.makeWrapper ];
  postBuild = ''
    wrapProgram $out/bin/cilium \
      --add-flags "-n cilium"
  '';
}

You would include this in your flake.nix as follows:

{
  inputs = {
    flake-utils.url = "github:numtide/flake-utils"; # Utility for generating flakes that are compatible with all operating systems
    panfactum.url = "github:panfactum/stack/edge.24-11-13"; # Make sure this matches your version of the Panfactum Stack
    nixpkgs.url = "github:NixOS/nixpkgs/nixos-24.05"; # 'nixos-24.05' can be any ref from the nixpkgs repo https://github.com/NixOS/nixpkgs
  };

  outputs = { nixpkgs, panfactum, flake-utils, ... }@inputs:
    flake-utils.lib.eachDefaultSystem
    (system:
      let
         pkgs = import nixpkgs {
          inherit system;
          config = { allowUnfree = true; };
        };
     in
      {
        devShell = panfactum.lib.${system}.mkDevShell {
          packages = [
+           (import ./cilium.nix { inherit pkgs; })
          ];
        };
      }
    );
}

Build a Custom Utility

If you are looking to package a custom binary, you will need to learn how nix derivations work.

We strongly encourage you to go through the nix pills tutorial for a good beginner overview.

The nixpkgs manual contains very detailed instructions for best practices in creating nix packages for the most popular language ecosystems.

Adding Additional Shell Hooks

panfactum.lib.${system}.mkDevShell takes an input called shellHook that takes an arbitrary bash snippet (as a string) that will be executed every time the devShell loads.

This can be extremely helpful for loading default environment variables, installing pre-commit hooks, issuing warnings, or automatically running any other repository maintenance activities.

You can add the hook as follows:

{
  inputs = {
    flake-utils.url = "github:numtide/flake-utils"; # Utility for generating flakes that are compatible with all operating systems
    panfactum.url = "github:panfactum/stack/edge.24-11-13"; # Make sure this matches your version of the Panfactum Stack
  };

  outputs = { panfactum, flake-utils, ... }@inputs:
    flake-utils.lib.eachDefaultSystem
    (system:
      {
        devShell = panfactum.lib.${system}.mkDevShell {
          shellHook = ''
            export GREETING="Hello World!"
            echo "$GREETING"
          '';
        };
      }
    );
}

Triggering Automatic Updates

You may find it easier to split out your devShell customizations into multiple files. If you want the devShell to reload automatically when those files change, you need to add new nix_direnv_watch_file directives to your .envrc file.

For example, if you want to watch all files in a directory called shell, your .envrc file should contain the line

nix_direnv_watch_file shell/*

Additional Flake Customization

While we provide a customized builder for the outputs.devShell field of your Nix flake, you should feel free to take advantage of the other fields for adding additional functionality. These will not interfere with Panfactum at all.

For example, you can add packages (nix build), checks (nix check), and even apps (nix run).

For more information on flake development, see the flake documentation.