Easy Environments

Simple isolated sdk setup with a complex language

My Journey

For the longest time I switched between different tooling to manage the SDKs I have installed on my system. I started off with just installing them via my OS' package manager before trying SDKMAN and deciding I didn’t really like it. After that I tried ASDF and used it for a long time, that was until I remembered another amazing tool I have somehow forgotten about, Nix. After realizing Nix can be used on other operating systems than just Linux, I started using it, and fell in love with it again.

Installing Nix

Most people who use Nix will end up using the main NixCPP implementation which has installation instructions on the NixOS website, but I ended up recently using Lix which is another implementation of the language and package manager focused on correctness. For now it is pretty much 1-to-1 with the NixCPP implementation but that will later change. To install Lix you can just run the following:

1
curl -sSf -L https://install.lix.systems/lix | sh -s -- install

It should be noted however if you’re running on macOS, specifically macOS 15 (in beta as of the time of writing this), you will need to specify a certain flag to change the build user prefix for Nix’s user group as macOS recently added some new users. To do this just pass --nix-build-user-prefix 305 and it should work, besides that, just follow the prompts and you’ll have Lix installed on your system!

Note: when running the installer it will ask if you want flakes enabled, to follow this blog post and guide you want them enabled, so make sure to enter yes when prompted otherwise when we get to setting up a flake it won’t work and you’ll have to manually modify your nix config to enable them yourself.

Environment Setup

The Nix language is fairly complex and there is not many resources on everything you’d need to know about it, nor do I really have all the resources I’d need to explain it, however I am going to explain at least how to get a dev shell setup with a Nix flake, so that’s what we’re going to explain here.

Creating a Flake

To get started we’ll want to make a folder for project and inside of that folder we’re going to create a flake.nix file, this is where we are going to store the configuration for our dev shell and environment. A very basic flake configuration will look something similar to the following.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
{
  inputs = {
    utils.url = "github:numtide/flake-utils";
  };

  outputs = { self, nixpkgs, utils }:
    utils.lib.eachDefaultSystem(system:
      let pkgs = nixpkgs.legacyPackages.${system}; in {
        devShells.default = pkgs.mkShell {
          nativeBuildInputs = with pkgs; [];
          buildInputs = with pkgs; [];
        };
      });
}

To explain this at a surface level, inputs is a list of other flakes we want to include, while as outputs is what our flake “exports” or exposes. In this case we are adding numtide’s flake-utils to allow us to easily make a per-system configuration. With that, we are declaring that pkgs is equal to the nixpkgs collection for our specific system and then finally declaring our dev shell.

There is a lot of options we can pass to pkgs.mkShell however the thing we care about in this case is the buildInputs and nativeBuildInputs parameters which specify the libraries/software needed at run-time and build-time respectively. To make this more clear, for example if your project needed something like GLFW and GLEW to be able to run, you would add those to the buildInput parameter as they are needed at runtime, while as for example something like meson or cmake would be added to nativeBuildInputs as those are only needed to actually build stuff.

Searching Packages

1
nix search nixpkgs 'nodejs'

It should be noted that the search command supports regex, so for example if we want to find all versions of NodeJS available, we can do something like this instead when searching through packages.

1
nix search nixpkgs 'nodejs_(latest|[0-9]{2})$'

This will in return give us results like the following and be a lot more precise.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
* legacyPackages.x86_64-darwin.nodejs_18 (18.20.2)
  Event-driven I/O framework for the V8 JavaScript engine

* legacyPackages.x86_64-darwin.nodejs_20 (20.14.0)
  Event-driven I/O framework for the V8 JavaScript engine

* legacyPackages.x86_64-darwin.nodejs_22 (22.3.0)
  Event-driven I/O framework for the V8 JavaScript engine

* legacyPackages.x86_64-darwin.nodejs_latest (22.3.0)
  Event-driven I/O framework for the V8 JavaScript engine

Once we have the packages we need, we can now add them to our dev shell configuration by adding them to the buildInputs or nativeBuildInputs parameters in our flake.

Starting the Shell

We can now finally start up a shell with our SDK and packages by doing the following.

1
2
# The "-c $SHELL" is just so it uses our preferred shell
nix develop -c $SHELL

And now you’re in a shell environment where your packages and SDKs are available!

The Epilogue

Nix is a very powerful and complex tool, I could not even begin to describe all the options, settings, commands, and niches about it in a single blog post. I highly recommend looking up documentation on it (as much as it is honestly lacking). Some useful resources are things like nix.dev, divnix also works on a book which can be really helpful. The fork I use also provides it’s own docs and also a wiki.

Hopefully this post was helpful to you in some way. I know Nix has made my dev experience a lot smoother, and I hope it can do the same for you. I wish you the best luck in your journey. 😉