Nix is a package manager, a functional language and a build system. It allows you to use any version of any package whenever you want. Arrange groups of packages into profiles, have an ability to rollback if something broke, write declarative build and environment recipes.
Channels
Channel - a git repo with package definitions, like a package repository
# List channels
$ nix-channel --list
# nixpkgs https://nixos.org/channels/nixpkgs-unstable
# ↑ ↑
# name URL
# Add a channel
$ nix-channel --add https://nixos.org/channels/nixpkgs-unstable nixpkgs
$ nix-channel --update
# Remove a channel
$ nix-channel --remove nixpkgs
Profiles
Profile is a set of installed packages. You can have multiple profiles with different versions of different packages and easily switch between them.
On the picture above you can see how two different profiles contain different versions of Subversion, so they coexist on the same OS.
# Create a new profile
$ mkdir /nix/var/nix/profiles/new
$ nix-env -p /nix/var/nix/profiles/new -i nix
# Install package in a profile
$ nix-env -p /nix/var/nix/profiles/new -iA nixpkgs.firefox
# Install package from the store in a profile
$ nix-env -p /nix/var/nix/profiles/new -i /nix/store/wl8vzvvyzpdahrhb482s7ykdy9rxsgxm-python3-3.10.10
# Install specific version of a package
$ nix-env -p /nix/var/nix/profiles/new -iA wget -f https://github.com/NixOS/nixpkgs/archive/8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8.tar.gz
To install a specific version of a package, you need to provide a nixpkgs revision that contains that exact package version. Here’s a nice website that simplifies getting that revision link: https://lazamar.co.uk/nix-versions
If you don’t specify the profile for nix-env, it will operate on the current user’s profile (installation of a package in the current profile is as simple as nix-env -iA nixpkgs.firefox
). Here’s how to switch the current profile btw
$ nix-env -S /nix/var/nix/profiles/new
So all the commands below can be ran on a specific profile if you specify it.
# Show packages installed in the current profile
$ nix-env --query "*"
# Remove a package from the current profile
$ nix-env -e chromium
If you remove a package from a profile, it will stay in the Nix store. To remove unused packages (packages that are not used in any profiles), run
$ nix-collect-garbage
Generations
Every time you install or uninstall something, a new generation of a profile is created. You can easily rollback if you did anything wrong
# Roll one generation back
$ nix-env --rollback
# List generations
$ nix-env --list-generations
# Output:
# 1 2023-03-16 23:40:16
# 2 2023-03-16 23:41:01
# ...
# 25 2023-06-01 21:41:55
# 26 2023-06-02 14:38:49 (current)
# Switch generation
$ nix-env -G 23
nix-shell
nix-shell allows you to start a shell session with a specific environment, which can be specified either with the -p and -I cmd flags, or with a .nix file
# Start a shell with Node.JS and curl installed. The specified packages will be
$ nix-shell -p nodejs curl
# Start a shell with a specific Node.JS version
$ nix-shell -p nodejs -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/34bfa9403e42eece93d1a3740e9d8a02fceafbca.tar.gz
# Specify the shell to run (--command *shell* flag) and do not use the current shell's PATH (--pure flag)
$ nix-shell -p zsh curl --command zsh --pure
Full list of nix-shell arguments
With nix-shell you can also create self-contained reproducible scripts
#!/usr/bin/env nix-shell
#! nix-shell -i bash
#! nix-shell -p jq curl
#! nix-shell -I nixpkgs=https://github.com/NixOS/nixpkgs/archive/nixos-22.11.tar.gz
echo "curl version:"
curl --version
echo "jq version:"
jq --version
If you run such a script and it just shows nothing, it’s probably downloading the nixpkgs revision.
In the nix-shell -i bash
line, we set the interpreter for the script with the -i
flag. The packages are specified as always. And for the last, the nixpkgs revision is also specified
The Nix language
Time to dive into the Nix language. Take a look at this example to put in a shell.nix
file. If you do not understand it, don’t worry, you will likely not have to write something like this in Nix
let
multipleAndDouble = a: b: a * b * 2;
curlOriginUrl = "https://github.com/NixOS/nixpkgs/archive/8ad5e8132c5dcf977e308e7bf5517cc6cc0bf7d8.tar.gz";
wgetOriginUrl = "https://github.com/NixOS/nixpkgs/archive/7592790b9e02f7f99ddcb1bd33fd44ff8df6a9a7.tar.gz";
channelFromUrl = url: import (builtins.fetchTarball { url = url; }) {}
in { pkgs ? import <nixpkgs> {}, params ? { "text" = "Some text to show!"; }, a, b }:
pkgs.mkShell rec {
packageToInclude = if multipleAndDouble a b > 20
then with channelFromUrl wgetOriginUrl; wget
else let curlOrigin = channelFromUrl curlOriginUrl;
in curlOrigin.curl;
packages = [
packageToInclude
];
shellHook = let textToShow = params.text; in ''
echo ${textToShow}
'';
}
If you are familiar with Haskell, this syntax will look familiar to you. If you don’t understand explanations below, open a Haskell book.
After let
we define some local variables for the expression that goes after in
. Each binding ends with a semicolon.
We define a function named multipleAndDouble
by assigning a lambda to this name. In Nix lambdas are defined like this: arg1: arg2: arg3: expr
and functions are called like this: func arg1 arg2 arg3
. Everything’s curried, so if func
takes 3 arguments, func arg1
returns a function that takes the rest of the arguments.
func arg1 arg2 arg3
== (func arg1) arg2 arg3
== ((func arg1) arg2) arg3
Then we define a couple things that’ll be used later.
So the expression we’re putting in our shell.nix
file is a lambda. Its argument is a set(object/map) that’s being destructurized. Default values for keys are specified with ?
.
If we didn’t pass params
with --arg params ...
, then { "text" = "Some text to show!"; }
is used.
In the lambda body, pkgs.mkShell
is applied to a recursive set. Recursive set is prefixed with the rec
keyword and can refer to its own keys.
packageToInclude
’s definition includes an if
statement. If the result of applying multipleAndDouble
to a
and b
is greater than 20, then the package we’re gonna fetch is curl and otherwise it’s wget. [Would be] easy to read [if i didn’t use both with
and let
for demonstration purposes]. with
makes set’s keys available at lexic scope after the semicolon.
with channelFromUrl wgetOriginUrl; wget
can be written as (channelFromUrl wgetOriginUrl).wget
and let curlOrigin = channelFromUrl curlOriginUrl; in curlOrigin.curl
can be written as (channelFromUrl curlOriginUrl).curl
Now, to the keys that are used by pkgs.mkShell
packages
must be a list of executable packages we want to include in the nix-shell session. By the way, list elements in Nix are separated by spaces.
shellHook
is a command that’ll be executed when shell starts. We assign a string with an echo
command, and use string interpolation to display textToShow
with echo
on shell startup.
inputsFrom
also exists, it adds all the dependencies from given packages as dependencies.
# Will output "Some text to show!" which is a default value and have wget available because 2*10*2 > 20
$ nix-shell shell.nix --arg a "2" --arg b "10" --pure
# Will output "Hello world!" and have curl available because 2*2*2 < 20
$ nix-shell shell.nix --arg a "2" --arg b "10" --pure --arg params "{ \"text\" = \"Hello world!\"; }"
Note that Nix values are passed in –arg, so if you want to pass a string, it’d be something like --arg strArg '"some text"'
A more common example of a shell.nix file
{ pkgs ? import <nixpkgs> {} }:
pkgs.mkShell {
name = "flask-development-env";
buildInputs = with pkgs; [
python39
python39Packages.flask
python39Packages.numpy
];
shellHook = ''
export FLASK_APP=app.py
export FLASK_ENV=development
'';
}
Derivations
Derivation - a build task
What we were importing from pkgs
are called derivations, not packages and mkShell is just a wrapper around mkDerivation. It’s possible to create an own derivation and pass it to packages
Create a directory hello-world
and a src
directory inside of it.
Put a hello world in C in hello-world/src/main.c
and the following in hello-world/default.nix
:
with import <nixpkgs> {};
stdenv.mkDerivation {
# Derivation name
name = "hello";
# Package name
pname = "hello;"
version = "1.0";
# The build-time dependencies
buildInputs = [
gcc
];
src = ./src;
# A shell script to build the package
# If omited, make will be executed
buildPhase = ''
gcc -o hello main.c
'';
# A shell script to install the package
installPhase = ''
mkdir -p $out/bin
cp hello $out/bin/
'';
meta = {
description = "A hello world in C";
};
}
It’s also possible to use a build script like this:
# builder.sh
source $stdenv/setup
buildPhase() {
gcc -o hello main.c
}
installPhase() {
mkdir -p $out/bin
cp hello $out/bin/
}
genericBuild
And just put builder = ./builder.sh;
And now, to build the package, run nix-build
. default.nix
and shell.nix
are the only two reserved names. You can run nix-build default.nix
too
See all the nix-build
parameters at the Nix reference manual.
What will happen is addition of that hello world to the Nix store and creation of a link named resul
to that directory in Nix store. So the final file structure will look like this
hello-world
├── default.nix
├── result -> /nix/store/lp9nnvp5092li1869vbh1kk5i5yrm9cg-hello
│ └── bin
│ └── hello
└── src
└── main.c
You can add that hello
to your profile by running nix-env -i
followed by its path in Nix store.
In my case, it’s
$ nix-env -i /nix/store/lp9nnvp5092li1869vbh1kk5i5yrm9cg-hello
mkDerivation
’s src can also be pkgs.fetchurl
, pkgs.fetchzip
, pkgs.fetchgit
and even pkgs.fetchFromGitHub
. Here’s a list
To use those, you may need to have an SHA256 of something and you can find it by setting the sha256 parameter to pkgs.lib.fakeSha256 and then copying the actual SHA256 from the error
src = pkgs.fetchgit {
url = "https://gitlab.inria.fr/nix-tutorial/chord-tuto-nix-2022";
rev = "069d2a5bfa4c4024063c25551d5201aeaf921cb3";
sha256 = pkgs.lib.fakeSha256;
};
error: hash mismatch in fixed-output derivation '/nix/store/a69pkgkga8zkpys0v5vci7wy6s2faz98-chord-tuto-nix-2022-069d2a5.drv':
specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
got: sha256-ff4fYS8vJ/Qk8YYpb9kyP/kmJ2xM83f+wrY43NMKijM=
error: 1 dependencies of derivation '/nix/store/lw4rcbn9vlr4g8rc8sx5sfz1dydq3a5r-chord-0.1.0.drv' failed to build
So far those were the essentials of Nix!
Coming soon: flakes
Dm me at @unsafe_andrew for feedback!