Overlays and Version Pinning in the Nix-Config Overlays Module
Table of Contents
Overview and video
This article describes how nix overlays work at a high level and walks through how to use the overlays module with in nix-config.
If you prefer, I also have a video on YouTube that covers the same content.
Introduction
First off, I'm happy to say that restoration work for the basement flood I experienced in March 2025, due to a dishwasher failure, has largely been resolved. I'm back home, comfortable, my study/office is the best it has ever been, and most importantly my routines are back to normal.
Recently I ran in to a problem after updating a mesa driver, where the Aquamarine rendering library using by Hyprland would crash when launching Hyprland. In an attempt to workaround the issue I tried pinning the version of mesa used in my flake using both overrides and overlays. Ultimately, I wasn't able to workaround the issue but the exercise was a great opportunity to experiment with overlays, expand my own understanding of them, and share the results with you here.
I'll be focusing on using overrides and overlays for version pinning but the examples discussed should be helpful regardless of use case.
Overrides versus Overlays
In brief, overrides are functions used to modify the attribute values of single packages, whereas overlays can apply multiple overrides more broadly, across all of nixpkgs.
Overrides
The two most commonly used override functions are <pkg>.override
and <pkg>.overrideAttrs
.
<pkg>.override
is used to override the arguments passed to a package. For example, consider some package in nixpkgs called foo
that handles two configurable arguments, arg1
and arg2
, that are both set to false
by default.
pkgs.foo.override
can be used to modify the values of the two default arguments as follows:
pkgs.foo.override {
arg1 = true;
arg2 = true;
};
In this case, the package version of foo
is unchanged but some options related to foo
have been overridden with our own values. In this example, we're simply overriding arg1
and arg2
with the value true
.
override
also accepts a function as an argument, conventionally called prev
or previous
, providing access to the previous/pre-override values but I won't detail that here.
<pkgs>.overrideAttrs
, in contrast, is used to override the package's derivation attributes. If you're unfamiliar with the term derivation, it is basically the set of attributes that define how a package will be constructed and therefore, this is were we can override the package version. overrideAttrs
also accepts up to two functions as arguments, conventionally called prevAttrs
and finalAttrs
, that provide access to the previous/pre-override attribute values and the final/overridden attributes values. If only one function is provided as an argument, the function interprets it as prevAttrs
. I won't go into detail about that here as we'll be discussing similar function arguments later on for overlays.
Continuing with our example, let's say that the package foo
is derived from a set of attributes declaring, among other things, that nix should build foo
version 1.2.
pkgs.foo.overrideAttrs {
version = "1.0";
};
The above override would result in foo
version 1.0 being used to construct the package instead of version 1.2. In reality, when pinning a package to a specific version, there are additional attributes that must be overridden but I will delve into that a bit more later on.
For a more practical example relevant to pinning the version of mesa
, I could have modified the code where mesa
is declared as the graphics package directly in my host config for ghost
as follows.
nix-config/hosts/linux/ghost/default.nix
--------------------
- hardware.graphics.package = pkgs.unstable.mesa;
+ hardware.graphics.package = pkgs.unstable.mesa.overrideAttrs {
+ version = "25.1.6";
+ src = {
+ ...
+ };
};
As of writing this, the version of mesa included in unstable is 25.2.0, and overriding the version attribute here would force nix to use version 25.1.6 instead. In this case however, the override only applies to mesa
when it is called in the "ghost" host config. At this point, using overrideAttrs
in the host-specific config may actually make the most sense for testing because it limits the effect of the override to one host. However, let's continue on and see how we can make use of overlays
so that the override will apply to any other hosts in the config.
Overlays
Like overrides, overlays can be used to change and extend nixpkgs but the modifications will apply across nixpkgs
as opposed to the potentially limited context of where an override is applied on it's own.
According to the Overlays section of the nixpkgs Manual "Overlays are Nix functions which accept two arguments, conventionally called self and super, and return a set of packages."
Currently, the standard convention for these arguments is actually final
and prev
.
final: prev: { };
prev
corresponds to whatever the previous evaluation of nixpkgs
was that occurred prior to the overlay being evaluated. In other words, prev
is the pre-overlay, or previous, set of nixpkgs.
final
corresponds to the evaluation of prev
being overridden with n overlays. In other words, final
is the post-overlays set of nixpkgs.
Said another way, declarations made in overlays will override prev
, resulting in the final
set of packages.
To simplify this in a visual manner, consider an example where the package foo
has the following set of attributes.
foo = {
color1 = "red";
color2 = "blue";
};
We can create an overlay that overrides color2
with the value "green" like so
final: prev:
{
foo = prev.foo.override { color2 = "green"; };
}
In this case, nixpkgs
would be returned with an instance of foo
where foo.color2
evaluates as "green". However, because overlays pass in the final
evaluation as an argument, we can also reference the end result within the overlay itself.
For example:
final: prev:
{
foo = prev.foo.override { color2 = "green"; };
bar = prev.foo.color2;
baz = final.foo.color2;
}
final.foo.color2 evaluates as "green"
final.bar.color2 evaluates as "blue"
final.baz.color2 evaluates as "green"
The beauty of final
being an argument itself, is that we can also access it within the overlay.
In this example, final.foo.color2
and final.baz.color2
would evaluate as "green" but final.bar.color2
would evaluate as "blue" because the value prev.foo.color2
was prior to the overridden value being applied.
This can be further complicated by adding more overlays on top of overlays, in which case, final
always refers to the final nixpkgs
, after all overlays have been applied, but prev
always refers to the previous, non-overlaid nixpkgs
. There is a visualization of the overlay data flow on the NixOS Wiki.
There are more in-depth explanations and under-the-hood descriptions available in the official Nixpkgs Reference Manual as well as the official NixOS Wiki, all of which are referenced at the end of this article under Additional Reading and References heading.
With this high-level understanding of both overrides and overlays, lets look at their implementation in nix-config.
Nix-Config Overlays Module
In nix-config/flake.nix
, we import our overlays module, which declares the default overlays for the entire flake.
nix-config/overlays/default.nix
--------------------
#
# This file defines overlays/custom modifications to upstream packages
#
{ inputs, ... }:
let
# Adds my custom packages
additions =
final: prev:
(prev.lib.packagesFromDirectoryRecursive {
callPackage = prev.lib.callPackageWith final;
directory = ../pkgs/common;
});
linuxModifications = final: prev: prev.lib.mkIf final.stdenv.isLinux { };
modifications = final: prev: {
# example = prev.example.overrideAttrs (prevAttrs: let ... in {
# ...
# });
};
stable-packages = final: prev: {
stable = import inputs.nixpkgs-stable {
inherit (final) system;
config.allowUnfree = true;
#overlays = [
#];
};
};
unstable-packages = final: prev: {
unstable = import inputs.nixpkgs-unstable {
inherit (final) system;
config.allowUnfree = true;
#overlays = [
#];
};
};
in
{
default =
final: prev:
(additions final prev)
// (modifications final prev)
// (linuxModifications final prev)
// (stable-packages final prev)
// (unstable-packages final prev);
}
This modules splits up all of the overlays into organizational categories, declaring each of them in the let
block and combining them all together in the in
block.
The first three overlay categories are:
additions
where we overlaynixpkgs
with all of our custom packages innix-config/pkgs/common
so they can be accessed the same way in nix-config.linuxModifications
where we would declare any linux-specific overlay values that we would only want to be built in if the host is running linux.modifications
where we would declare overlays that aren't platform specific.
In these examples, you can see both prev
and final
being used to access the pre-overlay and post-overlay variations of nixpkgs
. Simple enough because the examples are only one layer deep, so to speak.
We'll get to the last two overlay categories in the module later because they add some additional complexity.
Let's look at adding some code to modifications
that will pin mesa
to a specific version.
# ...
modifications = final: prev: {
mesa = prev.mesa.overrideAttrs {
version = "25.1.2";
src = prev.fetchFromGitLab {
domain = "gitlab.freedesktop.org";
owner = "mesa";
repo = "mesa";
rev = "mesa-25.1.2";
sha256 = "sha256-oE1QZyCBFdWCFq5T+Unf0GYpvCssVNOEQtPQgPbatQQ=";
};
};
}
};
# ...
Here you can see that we are using overrideAttrs
within the overlay function to modify the version
attribute and the src
attribute is being overridden with information required to fetch and use the corresponding package. This will effectively pin any reference to pkgs.mesa
to version "25.1.2".
Let's go an additional layer deeper though and set this up so that the overlay is applied only when pkgs.unstable.mesa
is used. To do this, we'll look at the stable-packages
and unstable-packages
sections.
#...
stable-packages = final: prev: {
stable = import inputs.nixpkgs-stable {
inherit (final) system;
config.allowUnfree = true;
#overlays = [
#];
};
};
unstable-packages = final: prev: {
unstable = import inputs.nixpkgs-unstable {
inherit (final) system;
config.allowUnfree = true;
#overlays = [
#];
};
};
#...
Here we add an additional layer of overlays.
First, we overlay nixpkgs
with stable
, which specifically imports nixpkgs-stable
from our flake inputs.
Similarly, we overlay nixpkgs with unstable
, which specifically imports nixpkgs-unstable
from our flake inputs.
This means that regardless of what version our inputs.nixpkgs.url
points to (unstable or 25.05, currently being the latest version of stable)) we will always be able to access a specific, stable nixpkgs through pkgs
in our config.
Similarly, we can also always access nixpkgs-unstable
, through pkgs
.
Consider a scenario where inputs.nixpkgs.url
(our "base" version of nixpkgs) points to 25.05, inputs.nixpkgs-stable
also points to 25.05, and inputs.nixpkgs-unstable
points to unstable. The overlays allow us to access unstable packages via pkgs.unstable
. Likewise, if we decide to change our base to unstable for some reason, the overlays allow us to access stable packages via pkgs.stable
. This effectively allows packages in nix-config to be pinned to whatever version is provided by default in stable or unstable.
As a practical example, consider our earlier look at the ghost
host config where were declared what version of mesa
to use.
hardware.graphics.package = pkgs.unstable.mesa;
If we want to switch to whatever stable version of nixpkgs is defined in the flake inputs, we can simple change this to:
hardware.graphics.package = pkgs.stable.mesa;
Or, if we wanted it to use whatever "base" version of nixpkgs was declared for the majority of the flake, we could remove the secondary attribute and simply write:
hardware.graphics.package = pkgs.mesa;
This complexity adds convenience but gets slightly confusing when we want to overlay stable
or unstable
.
When trying different pinned versions of mesa, I wanted them to only be pinned in unstable, so we will add the overlay to the unstable overlays list.
#...
unstable-packages = final: prev: {
unstable = import inputs.nixpkgs-unstable {
inherit (final) system;
config.allowUnfree = true;
overlays = [
(unstable_final: unstable_prev: {
mesa = unstable_prev.mesa.overrideAttrs {
version = "25.1.2";
src = prev.fetchFromGitLab {
domain = "gitlab.freedesktop.org";
owner = "mesa";
repo = "mesa";
rev = "mesa-25.1.2";
sha256 = "sha256-oE1QZyCBFdWCFq5T+Unf0GYpvCssVNOEQtPQgPbatQQ=";
};
};
})
];
};
};
#...
Note that here, we're adding code to the overlays list within an overlay and need to define distinct arguments, namely unstable_prev
and unstable_final
to ensure the correct packages are being referenced.
Adding Versatility
As a little bonus, let's consider how we can make switching between pinned version a little more convenient.
#...
unstable-packages = final: prev: {
unstable = import inputs.nixpkgs-unstable {
inherit (final) system;
config.allowUnfree = true;
overlays = [
(unstable_final: unstable_prev: {
mesa = unstable_prev.mesa.overrideAttrs (
previousAttrs:
let
version = "25.1.2";
hashes = {
"25.1.5" = "sha256-AZAd1/wiz8d0lXpim9obp6/K7ySP12rGFe8jZrc9Gl0=";
"25.1.4" = "sha256-DA6fE+Ns91z146KbGlQldqkJlvGAxhzNdcmdIO0lHK8=";
"25.1.3" = "sha256-BFncfkbpjVYO+7hYh5Ui6RACLq7/m6b8eIJ5B5lhq5Y=";
"25.1.2" = "sha256-oE1QZyCBFdWCFq5T+Unf0GYpvCssVNOEQtPQgPbatQQ=";
};
in
rec {
inherit version;
src = prev.fetchFromGitLab {
domain = "gitlab.freedesktop.org";
owner = "mesa";
repo = "mesa";
rev = "mesa-${version}";
sha256 = if hashes ? ${version} then hashes.${version} else "";
};
}
);
})
];
};
};
#...
By declaring the version and a set of various version-specific hashes in a let
block we can easily change the version that is pinned by modifying version
.
Notice that if the version we declare doesn't correspond to a value in hashes, sha256 will be declared as an empty string. Rebuilding will fail, but it will also provide us with the appropriate hash, as follows. It's worth noting that nix store pretch-file
is available on command line for pre-fetching hashes prior to building, which can be faster.
It's also worth noting that pinning packages to older versions may result in the package having to be built locally from source (which occurs automatically during rebuild) if there isn't a pre-built instance available in the nix cache any longer.
So to recap the end results of the unstable overlay:
- we overlaid
pkgs
/nixpkgs
, withunstable
, and overlaidpkgs.unstable
with a pinned version ofmesa
. hardware.graphics.package = pkgs.mesa
will build mesa version 25.0.7, because that is the version ofmesa
included with by the "base" nixpkgs used in our flake, which is nixos-25.05.hardware.graphics.package = pkgs.unstable.mesa
was set up to build whatever the latest version ofmesa
is in unstable nixpkgs (which would be 25.2.0 as of today, Aug 11) but then we then we overlaidpkgs.unstable.mesa
with an override that pinsmesa
to version 25.1.2.