or 'why I won't use docker anymore' 👽
from 22/07/2024, by xtrm — 6m read
Welcome to this short and sweet article on getting rid of Docker containers and VMs using one of Nix's greatest features: dev-shells.
This tutorial will be straight to the point without too much boiletplate in terms of explanations. I will also not take too much time explaining the nix language that will be utilized, but we won't dive very deep in terms of functionnality. Think of it like good ol' JSON and it'll be fine.
If you want to familiarize yourself with the official documentation, you can do so here.
First off let's get our definitions straight:
A dev-shell is a shell that we'll be using to get dependencies, libraries, and tools for development on a project. This follows the Nix philosophy of context-dependant and minimal scoping. This basically means that this dev-shell will only be available to us when we ask it to, and will not modify the base system in any way.
That being said, let's create our first dev-shell! To declare a dev-shell, we'll need to create a shell.nix
file. Let's see a very empty one:
## Our little development shell
{
pkgs ? import <nixpkgs> { }
}:
pkgs.mkShell {
# A shell!
}
This looks like black magic, but let's unpack it. This is basically a function:
{ ... }
at the top is the function inputs pkgs
, which is a reference to our system's nixpkgs (don't worry about the rest):
is the function code, calling mkShell
with some arguments, defined in { }
To run this file and create our shell, we can use the nix-shell
command.
We either need to point it to the shell file, for example: nix-shell my-simple-shell.nix
, or we can name our file shell.nix
, and nix-shell
will automatically detect it:
$ ls
shell.nix
$ nix-shell
## ...
[nix-shell]$ echo Hi!
Hi!
There we go, our first shell!
For now we don't specify anything, so it does pretty much nothing, but it's there!
We can exit the shell via the exit
builtin or by doing CTRL + D
, which cuts stdio and returns nicely.
[nix-shell]$ exit
exit
$ # Returned to bash
There's quite a few things we can have inside our shell, such as environment variables. We can also add a shell hook that will be run on shell initialization.
We can go a bit further and ask it to fetch us a Nix package (those can be found on NixOS's nixpkgs search page), that we can use in our hook. That way anyone using our shell will have the same experience.
Let's try that with cowsay
.
Here's our modified file:
{
pkgs ? import <nixpkgs> { }
}:
pkgs.mkShell {
# We define the hook in a multi-line string
# Notice the interpolation with ${...}
shellHook = ''
echo "Hello, $WHO!" | ${pkgs.cowsay}/bin/cowsay
'';
# Let's also define an environment variable
WHO = "Nix";
}
When running this with nix-shell
, Nix downloads and configures the cowsay
package for us, and we get a nice output:
$ nix-shell
## <snip>
_____________
< Hello, Nix! >
-------------
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
[nix-shell]$ echo $WHO
Nix
At this stage, we only asked Nix to use the package for the shell hook, so it is not available to us on the PATH
for example:
[nix-shell]$ cowsay
bash: cowsay: command not found
...and our libraries as well. This is where it gets interesting.
We can add a nativeBuildInputs
section to our mkShell
call, and declare packages that will be added to our PATH
. Similarly, we can also declare libraries inside which will become available to our compilers.
Let's now move cowsay
to be in our PATH
. Here's the updated file:
{
pkgs ? import <nixpkgs> { }
}:
pkgs.mkShell {
# We use the 'with <>;' syntax to simplify declaration...
nativeBuildInputs = with pkgs; [
# ...this is the same as 'pkgs.cowsay'
cowsay
];
}
Accessing the new shell now grants us access to cowsay
, and exitting it removes it, neat.
$ nix-shell
## <snip>
[nix-shell]$ echo "Hi" | cowsay
____
< Hi >
----
\ ^__^
\ (oo)\_______
(__)\ )\/\
||----w |
|| ||
[nix-shell]$ exit
exit
$ echo "Hi" | cowsay
bash: cowsay: command not found
That's about it for the basics of Nix shells.
you've done it! go you!!!!
You should read the official documentation and familiarize yourself with the other aspects and little tricks available to you.
For the curious, here's our shell.nix
file for the rt 42 project. have fun :)
{
pkgs ? import <nixpkgs> { }
}:
let
stdenv = pkgs.stdenvAdapters.useMoldLinker pkgs.clang17Stdenv;
in
(pkgs.mkShell.override { inherit stdenv; }) {
buildInputs = with pkgs; [
SDL2
readline
vulkan-headers
vulkan-loader
vulkan-tools
];
LD_LIBRARY_PATH="${pkgs.vulkan-loader}/lib";
nativeBuildInputs = with pkgs; [
norminette
valgrind
gdb
];
}