As a compulsive distro-hopper and programmer who likes to try new languages a lot of my time is spent on setting up new systems and packages. Usually I end up with a hodge-podge of multiple docker images, Node/Ruby/Python versions flying around and general upkeep & maintenance is difficult. Which is what always made Nix so appealing to me.
I will not go into the details of Nix but just give a few one-liners. Nix is -
- A Linux distro
- A package manager
- A programming language
among other things. I do not use the NixOS distro (although it looks appealing), I mainly use the package manager and programming language (not an expert by any means). Here is a little explanation of these -
- Nix is a purely functional package manager. This means that it treats packages like values in purely functional programming languages such as Haskell — they are built by functions that don’t have side-effects, and they never change after they have been built. Nix stores packages in the Nix store, usually the directory
/nix/store
, where each package has its own unique subdirectory. Read more here. - The Nix programming language is very declarative, like JSON or TOML but it also has functions (hence it is a functional language). See more here.
- So in a very, very short sentence - you can use Nix packages and the Nix language to declare an "environment" you need. This environment will not only be repeatable (i.e. coded) but also completely reproducible (i.e. everyone will have the same environment) due to the very nature of Nix.
I recommend avoiding pain and setting up Nix from here - https://zero-to-nix.com/start/install
A very basic nix file can look like this -
# shell.nix
let
nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-24.05";
pkgs = import nixpkgs { config = {}; overlays = []; };
in
pkgs.mkShellNoCC {
packages = with pkgs; [
cowsay
lolcat
];
GREETING = "Hello, Nix!";
shellHook = ''
echo $GREETING | cowsay | lolcat
'';
}
Here we get the packages from a certain repository and install 2 packages "cowsay" and "lolcat". If you now run -
nix-shell
_____________
< Hello, Nix! >
-------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
You will see the packages being installed, and the shell hook greeting you. Please note that your base Linux host may or may not have these packages but Nix will download them from the Nix repos into "/nix/store/" and set the right PATH and do everything for you.
This is great because now we want to move to bigger things like setting up a basic project in a new language, let's say in Deno or Gleam and I do not want to pollute my host system.
Before we move on, I want to talk a bit about Nix Flakes -
- Nix flake is not an "official" system but it is so well used that it should not go away now
- Flakes are a "system" for managing dependencies between Nix
- Flakes allow distribution and extension of Nix expressions
- Flakes allow you to "lock down" dependencies, like lock files found in most package managers
Again I do not claim to be a Flake expert. But below is a flake which is just enough to setup Deno -
# flake.nix
{
description = "Hello Deno";
# Flake inputs
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
};
# Flake outputs
outputs = { self, nixpkgs }:
let
# Systems supported
allSystems = [
"x86_64-linux" # 64-bit Intel/AMD Linux
# "aarch64-linux" # 64-bit ARM Linux
# "x86_64-darwin" # 64-bit Intel macOS
# "aarch64-darwin" # 64-bit ARM macOS
];
# Helper to provide system-specific attributes
forAllSystems = f: nixpkgs.lib.genAttrs allSystems (system: f {
pkgs = import nixpkgs { inherit system; };
});
in
{
# Development environment output
devShells = forAllSystems ({ pkgs }: {
default = pkgs.mkShell {
# The Nix packages provided in the environment
packages = with pkgs; [
zsh
lolcat
deno
];
# Environment variables
env = {
GREETING = "Hello from Deno!";
};
# A hook run every time you enter the environment
shellHook = ''
echo $GREETING | lolcat
'';
};
});
};
}
Flakes provide a lot of reusability, for example functions to build for multiple CPU architecture. All Flake files have 2 main sections - inputs & outputs. Flakes are also very declarative, so you can pretty much read the file above and get an idea of what is going on. Main thing here is that we have pulled a "deno" package. So now if you run -
nix develop
you will get all your dependencies and nix will drop you in a bash shell. If you want to stay in your shell you can also do -
nix develop -c $SHELL
You will now have "deno" executable available. You can also do for example -
code .
This would open VSCode in the context of the nix shell which means VSCode has the Deno executable available. You can do the same with emacs or neovim (whatever you want). You can now git clone this project and get started and running with Deno. Exit the nix shell and you are back to your pristine host system.
Not only is your host system clean but now you can also have a per project development environment with the dependencies locked down and available to all. This is now my default setup for all projects. Nix can do much more like build your final distribution or even build Docker images in a reproducible way. All in all - Nix is just great!