Declarative User Package Management in NixOS
NixOS is a Linux distribution built around the nix
package manager. The configuration of the entire system is
Let’s take a look at managing a user’s package environment declaratively in
NixOS version 20.03
. 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.
configuration.nix
setup.
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
configuration.nix
.
nixpkgs.config
. Populate config.nix
with multiple build environments and
install packages using nix-env
. This can
be achieved more optimally with nix flake
or you could use
nix-shell
for projects and temporary environments. The following is a basic config.nix
that makes
Awesome
(with a capital) is used instead of awesome
in one of the
overrides.
config.nix
here.
nix
{
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 apt install
or pacman -Syu
that intelligently erases all user packages
before installing.
shell
nix-env -Air nixos.Awesome nixos.Golang nixos.PHP nixos.C
Distribution independent installations of nix
default to the nixpkgs
nixos
. This is the default NixOS channel containing the packages. Run
nix-channel --list
to view the current user’s channel
prefixes.
shell
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
.
shell
$ 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.
nix
{
allowUnfree = true;
packageOverrides = pkgs: with pkgs; rec {
Machine1 = pkgs.buildEnv {
name = "machine1";
paths = [ Awesome Golang PHP C ];
};
# Package declarations for Awesome, Golang, PHP, & C ...
};
}
nix-env
shows that there is one package installed containing a program set of
other program sets declared within config.nix
.
shell
$ nix-env -Air nixos.Machine1
$ nix-env -q
machine1
This is immensely beneficial for use cases that prioritize keeping root
system
build times configuration.nix
declaratively. The nix
store also allows
/bin
.
nix
{
allowUnfree = true;
packageOverrides = pkgs: with pkgs; {
Awesome = pkgs.buildEnv {
name = "awesome";
paths = [
awesome
lxappearance
paper-gtk-theme
];
pathsToLink = [ "/etc" "/share" "/bin" ];
};
};
}
Instantly
nix-env -Air
by switching to an empty environment.
shell
nix-env -Air
List previous environment switches with --list-generations
. You can further
manipulate the environment lineage by using the generation arguments in the
nix-env
manual.
shell
$ 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
Since this is NixOS
— we can mix and match. In config.nix
bring in the
unstable
channel directly. We ungoogled-chromium
from
the
nix
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
.
luaformatter
. What now?
Create a PKGBUILD
or APKBUILD
. The ease of writing a derivation
depends on
upstream’s
build and install complexity.
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
.
nix
{ 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
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
nixpkgs
repository
calls most derivations.
This format sets you up for a
pull request if that’s your
thing.
--expr
argument inside the folder containing
default.nix
.
shell
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.
shell
nix-build -K -E 'with import <nixpkgs> {}; callPackage ./default.nix {}'
Call the derivation
if the build succeeds with pkgs.callPackage
from your
config.nix
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
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
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
nix
{
programs.awesome.enable = true;
programs.awesome.luaFormat = true;
# Or
programs.awesome = {
enable = true;
luaFormat = true;
};
}