Why Nix#
One of the things nix provides is a way to define entire systems and environments as reproducable, declarative code. The same configuration file provides identical results across machines reducing drift and inconsistencies.
Modular System#
Whether a component needs to be added or removed this is all able to be done via code.
Nix Store#
The Nix store is the foundation of how Nix manages software and system state. It is a content-addressed directory where all packages, dependencies, and build outputs are stored in isolated immutable paths.
Each path is derived from a hash of its inputs. This design allows multiple versions of the same package to coexist, allows reproducability, and allows for reliable rollbacks to previously built store paths.
Below shows the substituters which are cache endpoints where pre-built binaries exist. The binaries can be pulled down instead of building the packages locally.
$ nix config show | grep substituters
substituters = https://cache.nixos.org/
trusted-substituters =Flakes#
One of the things I really like about flakes is the ability to create development environments quickly.
Using the flake.nix file below I am able to easily create three different environments.
Below is an example from another project I created which was for eBPF development.
In the example I was able to create three different dev shells for three separate environments.
This felt like a good example to show off the basic usage and features of flakes.
In the next post I go deeper into the flake I created for the k3s environment.
{
description = "eBPF development shells";
inputs.nixpkgs.url = "github:nixos/nixpkgs";
outputs = {
self,
nixpkgs,
}: {
devShells.x86_64-linux = let
pkgs = import nixpkgs {system = "x86_64-linux";};
in {
bpf = pkgs.mkShell {
name = "ebpf-bpf-shell";
buildInputs = with pkgs; [
llvmPackages.clang-unwrapped
llvmPackages.lld
llvmPackages.bintools
libbpf
linuxHeaders
bpftools
];
shellHook = ''
echo "BPF compilation shell"
export CC=$(which clang)
export LD=ld.lld
export LINUX_HEADERS=${pkgs.linuxHeaders}/include
export LIBBPF_INCLUDE=${pkgs.libbpf}/include
alias build-bpf='clang -O2 -g -target bpf -D__TARGET_ARCH_x86 -I. -I$LIBBPF_INCLUDE -c dns_connect.bpf.c -o dns_connect.bpf.o'
'';
};
user = pkgs.mkShell {
name = "ebpf-user-shell";
buildInputs = with pkgs; [
clang
llvmPackages.lld
libbpf
pkg-config
glibc.dev
zlib
];
shellHook = ''
echo "User-space eBPF compilation shell"
export CC=$(which clang)
export LD=ld.lld
export LIBBPF_INCLUDE=${pkgs.libbpf}/include
export GLIBC_SYSROOT=${pkgs.glibc.dev}
export CFLAGS="--sysroot=$GLIBC_SYSROOT -isystem $GLIBC_SYSROOT/include -isystem $GLIBC_SYSROOT/include-fixed"
alias build-user='clang -O2 -g dns_user.c -I$LIBBPF_INCLUDE $(pkg-config --cflags --libs libbpf) -o dns_user'
'';
};
golang = pkgs.mkShell {
name = "ebpf-golang-shell";
buildInputs = with pkgs; [
go
clang
llvm
libbpf
bpftools
pahole
linuxHeaders
];
shellHook = ''
export CGO_ENABLED=0
echo "User-space eBPF GOLANG compilation shell"
'';
};
};
};
}Using these three commands from the flake I can develop for the bpf environment, my golang userspace environment, and my c userspace environment.
$ nix flake show
git+file:///home/jack/projects/ebpf-dns-example?ref=refs/heads/sys_enter_sendto&rev=8fbe7cc64c2666e17195db68995238ca12475b43
└───devShells
└───x86_64-linux
├───bpf: development environment 'ebpf-bpf-shell'
├───golang: development environment 'ebpf-golang-shell'
└───user: development environment 'ebpf-user-shell'Then running any of the three drops me into a shell environment with all the tools I need to develop for that environment:
$ nix flake develop .#bpf
$ nix flake develop .#golang
$ nix flake develop .#user