Declarative User Package Management in NixOS

NixOS
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 external 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. If this file does not exist create it.

The user’s nixpkgs configuration places us in a context
This may be familiar to those who configure the system from configuration.nix to nixpkgs.config. We populate config.nix with multiple build environments and install packages using nix-env. 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. Nix Flakes could be a more optimal way of going about this.

{
  allowUnfree = true;

  packageOverrides = pkgs: with pkgs; {

    Awesome = pkgs.buildEnv {
      name = "awesome";
      paths = [
        awesome
        lxappearance
        deepin.deepin-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
      ];
    };
  };
}

Notice that each package contains the prefix nixos. This is the default channel containing the packages. Run nix-channel --list as root to view the current channel prefixes. the above build sets in config.nix using nix -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. declared packages.

nix-env -Air nixos.Awesome nixos.Golang nixos.PHP nixos.C

Querying the installed packages with nix-env -q will show that there are four packages installed each containing the programs declared in config.nix.

$ nix-env -q
awesome
c
golang
php

The flexibility of the nix store 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
        deepin.deepin-gtk-theme
      ];
      pathsToLink = [ "/etc" "/share" "/bin" ];
    };
  };
}

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 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://github.com/NixOS/nixpkgs/archive/master.tar.gz) {};
  # Lock or pin at a specific commit to stop moving targets.
  # unstable = import (builtins.fetchTarball {
  #   url = "https://github.com/NixOS/nixpkgs/archive/0e2444aacb02b8c12416b71febca5cea416405f0.tar.gz";
  #   sha256 = "18lki60qb77h8akbzpzyang08i5iqppqz65msm7gmdhrky7f3i07"; }) {};
in
  {
    allowUnfree = true;
    
    packageOverrides = pkgs: with pkgs; {

      Awesome = pkgs.buildEnv {
        name = "awesome";
        paths = [
          awesome
          lxappearance
          deepin.deepin-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?

We 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.

{ 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 stdenv.lib; {
    inherit (src.meta) homepage;
    description = "Code formatter for Lua";
    license = licenses.asl20;
    platforms = platforms.linux;
  };
}

Test the derivation using nix-build. Run this This is how the nixpkgs repository calls most derivations. This format almost always sets you up for a clean pull request if that's your thing. inside the folder containing default.nix.

nix-build -E 'with import <nixpkgs> {}; callPackage ./default.nix {}'

If the Check out my config.nix here. succeeds, let config.nix call the derivation.

let
  unstable = import (builtins.fetchTarball https://github.com/NixOS/nixpkgs/archive/master.tar.gz) {};
in
  {
    allowUnfree = true;
    
    packageOverrides = pkgs: with pkgs; {

      Awesome = pkgs.buildEnv {
        name = "awesome";
        paths = [
          awesome
          lxappearance
          deepin.deepin-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;
  };
}

Updated 8 July 2020