Declarative User Package Management in NixOS

NixOS is a Linux distribution built around the nix
package manager. The configuration of the entire system is
In theory, NixOS is the easiest
Linux distribution to use because of its declarative nature. In
theory…
allowing for immutability and reproducibility.
Let’s take a look at managing a user’s package environment declaratively in
NixOS version 20.03
. The
The nix
expression language is not particularly difficult. It’s the scope and
flexibility of the abstraction layer that makes the initial NixOS learning
experience painful.
of NixOS dictates that there are
numerous declarative methods. We can use attribute sets, build environments, or
even community
Home manager is a system
that manages a user’s environment. It’s similar to the system’s
configuration.nix
setup.
like
home manager.
Declarative Package Management
We will
combine
the command nix-env
with the user’s config.nix
to achieve a declarative
package setup. The user specific configuration is located at
~/.config/nixpkgs/config.nix
. Create this file if it does not exist.
The user’s nixpkgs
configuration config.nix
places us in a context
This may be
familiar to those who
configure the system from configuration.nix
to
nixpkgs.config
. Populate config.nix
with multiple build environments and
install packages using nix-env
. This can
be achieved more optimally with
nix flake
(manual)
or you could use nix-shell
for projects and temporary environments. The
following is a basic config.nix
that makes
Beware of package name collisions that
result in infinite recursion. Notice that Awesome
(with a capital) is used
instead of awesome
in one of the overrides.
of
package overriding.
Check out my config.nix
here.
{
allowUnfree = true;
packageOverrides = pkgs: with pkgs; {
Awesome = pkgs.buildEnv {
name = "awesome";
paths = [
awesome
lxappearance
paper-gtk-theme
];
};
Golang = pkgs.buildEnv {
name = "golang";
paths = [
go
];
};
PHP = pkgs.buildEnv {
name = "php";
paths = [
php74
];
};
C = pkgs.buildEnv {
name = "c";
paths = [
gnumake
meson
ninja
gcc
];
};
};
}
Install the above build sets in config.nix
using nix-env -Air
. The -A
flag
is for the attribute path, -i
to install the packages and -r
for removing
all
This is
like running an
apt install
or pacman -Syu
that intelligently erases all user packages
before installing.
packages.
nix-env -Air nixos.Awesome nixos.Golang nixos.PHP nixos.C
Distribution independent installations of nix
default to the nixpkgs
Each package contains the prefix
nixos
. This is the default NixOS channel containing the packages. Run
nix-channel --list
to view the current user’s channel
prefixes.
as the default channel name.
nix-env -Air nixpkgs.Awesome nixpkgs.Golang nixpkgs.PHP nixpkgs.C
Querying the installed packages with nix-env -q
shows that there are four
packages installed each containing the programs declared in config.nix
.
$ nix-env -q
awesome
c
golang
php
One could exploit the recursive aspect and group packages further by any metric. Below we add another set that groups the above declarations by machine.
{
allowUnfree = true;
packageOverrides = pkgs: with pkgs; rec {
Machine1 = pkgs.buildEnv {
name = "machine1";
paths = [ Awesome Golang PHP C ];
};
# Package declarations for Awesome, Golang, PHP, & C ...
};
}
This should evaluate quicker than
the first install command.
and querying this package with
nix-env
shows that there is one package installed containing a program set of
other program sets declared within config.nix
.
$ nix-env -Air nixos.Machine1
$ nix-env -q
machine1
This is immensely beneficial for use cases that prioritize keeping root
system
build times
Faster configuration
switches.
Users can install packages independent of the
system’s configuration.nix
declaratively. The nix
store also allows
This gives the option of only
exposing the binaries from /bin
.
exposing program paths to the
user’s environment.
{
allowUnfree = true;
packageOverrides = pkgs: with pkgs; {
Awesome = pkgs.buildEnv {
name = "awesome";
paths = [
awesome
lxappearance
paper-gtk-theme
];
pathsToLink = [ "/etc" "/share" "/bin" ];
};
};
}
Instantly remove all packages with nix-env -Air
by switching to an empty
environment.
nix-env -Air
List past environment switches with --list-generations
. You can further
manipulate the environment lineage by using the generation arguments in the
nix-env
manual.
$ nix-env --list-generations
368 2021-08-13 23:16:36
369 2021-08-22 00:01:38
370 2021-08-22 00:05:47
371 2021-08-22 00:23:03
Unstable Declarative Package Management
The above is nice and all, but what if a package is not in the nixos
stable
channel? We can add the
The unstable
channel is based on the master repository
and contains more packages than the stable channel.
channel from
the system side, but what happens if its too unstable?
Since this is NixOS
— we can mix and match. In config.nix
bring in the
unstable
channel directly. We
This
is wonderful. Getting a quick and clean install of programs like
Ungoogled Chromium
isn’t so easy in other environments.
ungoogled-chromium
from
the
The paranoid user can set any
package archive and
pin specific versions.
channel as an example.
let
unstable = import (builtins.fetchTarball "https://releases.nixos.org/nixos/unstable/nixos-21.03pre265961.891f607d530/nixexprs.tar.xz") {};
# Or lock at a specific commit to stop moving targets.
# unstable = import (builtins.fetchTarball {
# url = "https://releases.nixos.org/nixos/unstable/nixos-21.03pre265961.891f607d530/nixexprs.tar.xz";
# sha256 = "1hwwb4n15bbqxnbqffq4kfb369vz65sq74p537fqdp6i4ywpqsyh"; }) {};
in
{
allowUnfree = true;
packageOverrides = pkgs: with pkgs; {
Awesome = pkgs.buildEnv {
name = "awesome";
paths = [
awesome
lxappearance
paper-gtk-theme
unstable.ungoogled-chromium
];
pathsToLink = [ "/etc" "/share" "/bin" ];
};
};
}
Advanced Declarative Package Management
Here’s a scenario: You’re reading some Lua code and you
need a lua
formatting tool. It looks like
LuaFormatter will do the job, but that
package is neither in the stable
or unstable
channel. You install luarocks
— the lua
package manager, but something
luaformatter
attempts to compile its
submodules from the read only nix
path of luarocks
.
happens
when it tries to install luaformatter
. What now?
Create a
This is similar to writing
a PKGBUILD
or APKBUILD
. The ease of writing a derivation
depends on
upstream’s
build and install complexity.
for luaformatter
that
config.nix
can call and build to expose the package to the user’s environment.
Create a packages
directory and a folder containing default.nix
at
~/.config/nixpkgs/packages/luaformatter
.
{ lib, stdenv, fetchFromGitHub, cmake }:
stdenv.mkDerivation rec {
pname = "LuaFormatter";
version = "1.3.3";
src = fetchFromGitHub {
sha256 = "1dfqsh6v8brnwzg3lgi7228lw08qqfy4ghbjyvwn7mr82fy1xcnd";
rev = version;
repo = pname;
owner = "Koihik";
fetchSubmodules = true;
};
buildInputs = [ cmake ];
meta = with lib; {
inherit (src.meta) homepage;
description = "Code formatter for Lua";
license = licenses.asl20;
platforms = platforms.linux;
};
}
Test the derivation
using nix-build
. Run
There is nothing special about this
expression. The default.nix
here is in the correct form for callPackage
to
understand. A derivation can contain anything — though I prefer a
default.nix
to always return a package, a shell.nix
to return a shell, a
module.nix
a module, a flake.nix
a flake, and so on.
nix
This is how the nixpkgs
repository
calls most derivations.
This format sets you up for a
pull request if that’s your
thing.
with the --expr
argument inside the folder containing
default.nix
.
nix-build -E 'with import <nixpkgs> {}; callPackage ./default.nix {}'
Use the --keep-failed
(-K
) argument to save the temporary build folder on
failure. This allows testing fixups in the failed build environment.
nix-build -K -E 'with import <nixpkgs> {}; callPackage ./default.nix {}'
Call the derivation
if the build succeeds with pkgs.callPackage
from your
config.nix
let
unstable = import (builtins.fetchTarball "https://releases.nixos.org/nixos/unstable/nixos-21.03pre265961.891f607d530/nixexprs.tar.xz") {};
in {
allowUnfree = true;
packageOverrides = pkgs: with pkgs; {
Awesome = pkgs.buildEnv {
name = "awesome";
paths = [
awesome
lxappearance
paper-gtk-theme
unstable.ungoogled-chromium
(callPackage ./packages/luaformatter/default.nix {})
];
pathsToLink = [ "/etc" "/share" "/bin" ];
};
};
}
Think of a derivation
as an abstraction of the typical application build
process. In the above derivation
we imported stdenv
— the standard
environment that contains common tools and dependencies most programs need. The
fetchFromGitHub
Or rather an attribute
set containing attributes.
contains methods that extend the
standard environment allowing us to pull in repositories the
Nix builds in a sand–boxed environment by
default, so arbitrary network connections are not allowed outside the scope of
attributes.
way. The application luaformatter
requires
cmake
, so it is sourced as one of the buildInputs
— another attribute
within the derivation
context.
A derivation
can be extended in multiple ways, allowing us to
Most applications and libraries are
already built for us.
build any application. The nixpkgs
repository has an
overview
of building artifacts in popular environments like
Python.
In tricky application builds, step down a level of abstraction to fix it up
using the derivation’s
phase attributes.
Finally a derivation
can be paired with a
module allowing us to hide away the above work
into a simple
Writing a module is a story
for another day.
of attributes that a user can easily understand. This is what most users care
about. The following is a made up example from the simple derivation above.
{
programs.awesome.enable = true;
programs.awesome.luaFormat = true;
# Or
programs.awesome = {
enable = true;
luaFormat = true;
};
}