Nix: Better way for fun and profit

3 min read Original article ↗

Every project depends on existing software that is beyond your control. Nix DSL enables you to declaratively specify your projects dependencies, a repo or a tar-ball down to the file digest of its content, which is what gives nix superpowers of being a deterministic and reproducible package manager. This means that if your inputs stays the same, nix guarantees that it produces the exact same output regardless of when and where. Below is a flake that pulls in latest version of Clojure into your project.

{
  # optional attribute
  description = "My awesome Clojure/ClojureScript project";

  # required attribute
  inputs = {
    # nix dsl fns useful for writing flakes
    flake-utils.url = "github:numtide/flake-utils/v1.0.0";
    # Pins state of the packages to a specific commit sha
    pinnedPkgs.url = "github:NixOS/nixpkgs/c46290747b2aaf090f48a478270feb858837bf11";
  };

  # required attribute
  outputs = { self, flake-utils, pinnedPkgs }@inputs :
  flake-utils.lib.eachDefaultSystem (system:
  let pinnedSysPkgs = inputs.pinnedPkgs.legacyPackages.${system};
  in
  {
    devShells.default = pinnedSysPkgs.mkShell {
      packages = [
        pinnedSysPkgs.clojure
      ];

      # commands to run in the development interactive shell
      shellHook = ''
        echo To get Clojure REPL, Run:
        echo clojure
        echo To get ClojureScript REPL, Run:
        echo clj -Sdeps \'{:deps {org.clojure/clojurescript {:mvn/version "1.11.132"}}}\' -M -m cljs.main --repl
      '';
    };
    packages = {
      docker = pinnedSysPkgs.dockerTools.buildLayeredImage {
        name = "My awesome Clj docker image built by nix";
        tag = "latest";
        contents = [pinnedSysPkgs.clojure];
      };
    };
  });
}

Do not worry too much about not understanding above nix dsl code. The most important thing to know is that it is nix dsl referred to as a flake that specifies its inputs and outputs declaratively. Save above code as `flake.nix`, which is a convention, then run `nix develop` to get an interactive shell with Clojure in your path. Nix can do way more than this. However, I recommend you just start with solving project dependencies problem. Above flake gives you following benefits:

  • Ability to pin the exact versions of your project dependencies.
  • Cross platform development environment that works both in MacOS and various flavors of Linux.
  • Determinate and reproducible development environment that eliminates "it works on my machine" tooling issues.

One important thing to notice here is the way I chose to reference the url inputs of the flake. I deliberately used tags or commit sha to prevent the state of the urls (thus the state of the nix DSL) change under me, which defeats the purpose of having a determinate and reproducible way to get a development environment. I have following bash script that prints available tags and corresponding commit hash:

 git_tag_sha () {
   repo="$1"
   echo "********************************************************"
   echo "Available release and commit sha for pinning are:"
   echo "********************************************************"
   printf "\033[1m%-12s %s\033[0m\n" "release" "commit sha"
   curl -s https://github.com/$repo/tags | grep -oP 'href="\K[^"]*(releases/tag|nixpkgs/commit)[^"]*' | awk -F '/' 'NR%2{tag=$NF; next} {printf "%-12s %s\n", tag, $NF}'
   echo
   echo "****************************************************************************"
   echo "Please replace the commit sha of following line to pin pkgs to a commit sha: "
   echo "pinnedPkgs.url = github:$repo/<commit>"
   echo "****************************************************************************"
   echo
}
# You can run it like this:
 git_tag_sha "NixOS/nixpkgs"