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.CDistribution 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.CQuerying 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
phpOne 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
machine1This 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 -AirList 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:03Unstable 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;
};
}