Go back

Writing Your First Nix Overlay

Created at: 2025-01-04 18:58

Updated at: 2025-01-04 19:51 (7e671c5)

Reading time: 7 min read (1217 words)

Tags:

#nix

Table of Contents

Introduction

After about 1 year, I came back to Nix as one of my main tools for my workflow. In case, you can see my entire macOS configuration, written in Nix, here.

After having to change MacBook devices twice during the end of the last year, I really missed Nix on my daily usage, where I need to setup my config every time, instead of just running a command. That’s why I’m using Nix again after some period without it.

Now, I reached a specific scenario last week, where I need to write a specific overlay for a tool. In my case, beanprice. I’m using beancount, as my daily-driven tool to track my finances, and I’d like to track the value of my stocks in an easier way, instead of having to manually adding it everytime.

Doing some search, I discovered that the beancount’s brewfile didn’t install this binary for me. After trying the nixpkgs version of this binary, I discovered another thing: with that version, it comes with the bean-price binary, but it comes with an old version, where it didn’t contain a specific module to fetch some of the stock prices that I’d like to fetch (more specifically, the alphavantage source). For that reason, it seems to be a good argument to have an overlay for that. That’s what I did, and was my first time writing an overlay from scratch.

Writing the Module File

The overlays on Nix are a kind of function, it gives for us the ability to provide some overriding some specific configurations for a given module, package or anything else that your creativity let you think about.

In my case, what should I be doing? Writing a new overlay that would add a new package called bean-price into all Python packages modules. For that, I used this Python section as my reference to do that.

That’s the output of what I did:

{
  lib,
  buildPythonPackage,
  python3Packages,
  fetchPypi,
  isPy3k,
}:
 
buildPythonPackage rec {
  version = "1.2.1";
  format = "setuptools";
  pname = "beanprice";
 
  disabled = !isPy3k;
 
  src = fetchPypi {
    inherit pname version;
    hash = "sha256-0/W1q25z6xNjhb7mZFpJUZ6TVNNA1BK341gOxlpOGVc=";
  };
 
  # Tests require files not included in the PyPI archive.
  doCheck = false;
 
  propagatedBuildInputs = with python3Packages; [
    python
    python-dateutil
    beancount
  ];
 
  meta = with lib; {
    homepage = "https://github.com/beancount/beanprice";
    description = "Daily price quotes fetching library for plain-text accounting ";
    longDescription = ''
      A script to fetch market data prices from various sources on the internet and render them for plain text accounting price syntax (and Beancount).
 
      This used to be located within Beancount itself (at v2) under beancount.prices. This repo will contain all future updates to that script and to those price sources.
    '';
    license = licenses.gpl2Only;
    maintainers = [ ];
  };
}

What we’re doing here? We’re calling the buildPythonPackage function accordingly to the Python interpreter. And will run four steps, accordingly to this documentation here: buildPhase, installPhase, postFixup and installCheck.

With that, we will be able to build and have our own binary of a given package, that, in case, we’re fetching all those informations based on the src with the fetchPypi function.

Finally, we had this overlay done, now, we need to let both nixpkgs and home-manager know about that.

Consuming the Overlays

In my opinion, as a good pattern for that, I like to have the overlay being “exported” on a main file. For my config, I wrote a overlays/default.nix that does it for me:

# /overlays/default.nix
 
final: prev:
 
let
  pythonPackageExtensionsOverrides = self: super: {
    bean-price = prev.callPackage ./bean-price {
      inherit (super) buildPythonPackage isPy3k;
    };
  };
in
{
  pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [pythonPackageExtensionsOverrides];
}

Now, what I’m doing is: getting all my overrides related to python and overriding it on pythonPackagesExtensions. Which basically override it for every Python extension (python3, python, python39, python312, etc).

Now, we just need to add it on the imported nixpkgs and on home-manager, to guarantee that everything is good.

From another post that I wrote last year, where I teach how to install Nix + nix-darwin + home-manager on macOS. My flake.nix config was like that:

flake.nix config without overlays
{
  inputs = {
    nix-homebrew.url = "...";
    home-manager.url = "...";
  };
 
  outputs = inputs @ { self, nix-homebrew, home-manager ... }: let
    nixpkgsConfig = {
      config.allowUnfree = true;
    };
  in {
    darwinConfigurations = let
      inherit (inputs.nix-darwin.lib) darwinSystem;
    in {
      machine = darwinSystem {
        system = "aarch64-darwin";
 
        specialArgs = { inherit inputs; };
 
        modules = [
          ./hosts/mbp/configuration.nix
          inputs.home-manager.darwinModules.home-manager
          {
            nixpkgs = nixpkgsConfig;
 
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users.noghartt = import ./home/home.nix;
          }
        ];
      };
    };
  }
}

Now, we will need to adjust it in two parts to guarantee that everything is good:

{
  inputs = {
    nix-homebrew.url = "...";
    home-manager.url = "...";
+   nixpkgs.url = "github:NixOS/nixpkgs";
  };
 
- outputs = inputs @ { self, nix-homebrew, home-manager, ... }: let
+ outputs = inputs @ { self, nix-homebrew, home-manager, nixpkgs, ... }: let
+   overlays = [ (import ./overlays) ];
+
+   system = "aarch64-darwin";
+
+   pkgs = import nixpkgs { inherit system overlays; };
+
    nixpkgsConfig = {
+     inherit overlays;
+
      config.allowUnfree = true;
    };
  in {
+   packages = pkgs;
+
    darwinConfigurations = let
      inherit (inputs.nix-darwin.lib) darwinSystem;
    in {
      machine = darwinSystem {
        system = "aarch64-darwin";
 
        specialArgs = { inherit inputs; };
 
        modules = [
          ./hosts/mbp/configuration.nix
          inputs.home-manager.darwinModules.home-manager
          {
            nixpkgs = nixpkgsConfig;
 
            home-manager.useGlobalPkgs = true;
            home-manager.useUserPackages = true;
            home-manager.users.noghartt = import ./home/home.nix;
          }
        ];
      };
    };
  }
}

With that, you will be able to add both overlays to nixpkgs side, so you can add the overlays at a system-level, for NixOS, nix-darwin, or any place you’re using it. And, inheriting overlays at the nixpkgsConfig at L16, will let you use the overlay in home-manager level.

Now, in your home-manager config, you can do something like that:

{ pkgs, config, lib, ... }:
 
{
  home.stateVersion = "25.05";
 
  home.packages = with pkgs; [
    fava
    python3Packages.beancount
    python3Packages.bean-price
  ];
}

Solving Conflicts on Overlays

One issue that I had on this bean-price overlay is, as I said, the nixpkgs version of the beancount adds the bean-price binary. So, both packages are conflicting to decide which of the binaries should be chosen.

The first solution that I saw, was adding a meta.priority = 10; on the overlay. But haven’t success, it still getting the outdated version of the bean-price binary. In that case, I did a different approach:

# /overlays/default.nix
 
final: prev:
 
let
  pythonPackageExtensionsOverrides = self: super: {
    bean-price = prev.callPackage ./bean-price {
      inherit (super) buildPythonPackage isPy3k;
    };
+
+   beancount = super.beancount.overrideAttrs (oldAttrs: {
+     postInstall = ''
+       ${oldAttrs.postInstall or ""}
+       rm $out/bin/bean-price
+     '';
+   });
  };
in
{
  pythonPackagesExtensions = prev.pythonPackagesExtensions ++ [pythonPackageExtensionsOverrides];
}

I added an overlay to beancount package too, where add a step on postInstall that just remove the binary related to /bin/bean-price. With that, everything works as expected.

DISCLAIMER!

I didn’t find any other way to solve this issue. Everyone that knows a better, elegant approach to do it, feel free to reach me out. I would really like to have a better way to solve this.

Also, if I find something, I come back to this post and update it accordingly to what I find.

Conclusion

This is just a brief tour around how you can approach to write your first overlay. And a good introduction to understand how useful is Nix at all, you can do a lot of things or extend things that you already had as you want. It’s a powerful tool in the right hands.