Packaging a kernel module for NixOS
🚇

Packaging a kernel module for NixOS

Tags
NixNixOS
Updated at
Apr 28, 2024 5:52 AM
Published at
April 23, 2024

A guide/documentation on how to add/modify nixpkgs for NixOS to support my Vocaster One Studio audio interface, and how to use nix tools to get there. Along the way, I make my first PR, discover interesting things and issues, and learn to package a derivation.

What is the issue?

I recently got a Vocaster One Studio audio interface with a mic for streaming my live Nix sessions, but my NixOS doesn't recognize the Vocaster interface. I searched and found a Linux Alsa Focusrite Driver that supports the Vocaster interface. It seems this driver isn’t packaged for NixOS yet. I asked about my issue at the Nix Audio matrix channel and they were very helpful. I understand that I need to create a kernel module for NixOS, then submit a PR and add the module to Nixpkgs. User banditthedodge said in the Nix Audio matrix channel to package it in a derivation and add it to boot.extraModulePackages.

How are kernel modules loaded in NixOS and what are the makeFlags of the Scarlett GUI

What is the latest linux kernel available to NixOS? I go to the NixOS manual and search for kernel keyword. So we can override the Linux kernel and associated packages such as our driver using the option boot.kernelPackages. Now, with zcat /proc/config.gz we can see the kernel options but I think these are about tweaking or changing the configuration of the available options of the kernel available in the system. Then it says that kernel modules for hardware devices are generally loaded automatically by udev, and to force a module to be loaded via boot.kernelModules.

Now does this mean that the module I need is the snd_us_module? How can I check if this module is already loaded in my kernel? GPT shows me few ways to check this:

> lsmod | grep snd_usb_audio
~
# modinfo shows it exists but it may not be loaded
> modinfo snd_usb_audio
...
# another way to see if a module is loaded
> cat /proc/modules | grep snd_usb_audio

# to see if the module was loaded during boot
> dmesg | grep snd_usb_audio
~

By looking at this the module exists but it is not loaded at all. That means I need to add snd_usb_audio to boot.kernelModules in the configuration.nix. Let’s add it

> sudo hx /etc/nixos/configuration.nix
# add boot.kernelModules = [ snd_usb_audio ];
> sudo nixos-rebuild switch
...
starting the following units: systemd-modules-load.service

What does that last line means?

❯ systemctl status systemd-modules-load.service

â—Ź systemd-modules-load.service - Load Kernel Modules
     Loaded: loaded (/etc/systemd/system/systemd-modules-load.service; enabled; preset: enabled)
    Drop-In: /nix/store/xz79n2xx6l56q16yxwv2gi21g7xcmcji-system-units/systemd-modules-load.service.d
             └─overrides.conf
     Active: active (exited) since Mon 2024-04-22 10:32:43 CEST; 3min 8s ago
       Docs: man:systemd-modules-load.service(8)
             man:modules-load.d(5)
    Process: 27890 ExecStart=/nix/store/y6mag03hj8b2hnlcj682xvp2aid7yg7y-systemd-255.4/lib/systemd/systemd-modules-load (code=exited, status>
   Main PID: 27890 (code=exited, status=0/SUCCESS)
         IP: 0B in, 0B out
        CPU: 46ms

tra 22 10:32:43 nixos systemd[1]: Starting Load Kernel Modules...
tra 22 10:32:43 nixos systemd-modules-load[27890]: Inserted module 'snd_usb_audio'
tra 22 10:32:43 nixos systemd-modules-load[27890]: Module 'ctr' is built in
tra 22 10:32:43 nixos systemd[1]: Finished Load Kernel Modules.

Let’s reboot and see what happens. OK, so now we have something going:

❯ lsmod | grep snd_usb_audio
snd_usb_audio         462848  0
snd_usbmidi_lib        49152  1 snd_usb_audio
snd_ump                32768  1 snd_usb_audio
snd_hwdep              20480  2 snd_usb_audio,snd_hda_codec
mc                     94208  7 videodev,snd_usb_audio,videobuf2_v4l2,uvcvideo,videobuf2_common
snd_pcm               196608  13 snd_hda_codec_hdmi,snd_hda_intel,snd_usb_audio,snd_hda_codec,soundwire_intel,snd_sof,snd_sof_intel_hda_common,snd_compress,snd_soc_core,snd_sof_utils,snd_soc_skl,snd_hda_core,snd_pcm_dmaengine
snd                   159744  21 snd_hda_codec_generic,snd_seq,snd_seq_device,snd_hda_codec_hdmi,snd_hwdep,snd_hda_intel,snd_usb_audio,snd_usbmidi_lib,snd_hda_codec,snd_hda_codec_realtek,snd_sof,snd_timer,snd_compress,snd_soc_core,snd_ump,snd_pcm,snd_rawmidi

I'm seeing if NixOS can pick up Vocaster. The sound icon on Plasma DE shows an Audio Pro option, which could be Vocaster. The next step is to search for packages with "focusrite" on search.nixos.org. There's a alsa-scarlett-gui package for Nix. It's version 0.4.0. I'll check default.nix in the source tab and the project homepage. The package release is the same as the newest release on the project homepage. Now, I'm checking out the default.nix defined at pkgs/applications/audio/alsa-scarlett-gui/default.nix. This lets me see how this program is packaged for nixpkgs. If I see how the GUI is packaged, I may better understand how to package the kernel driver. I need to get a better grasp of the install phase of the package because I'm not sure how the build phase works, since the NixOS filesystem structure is unique.

Oh, so I see at the github page of the scarlett gui that for Vocaster support one needs to build an updated snd_usb_audio driver (or wait for 6.10). Let’s see the latest availabe kernel I can get. I’m adding into the configuration.nix the line: boot.kernelPackages = pkgs.linuxPackages_latest;. So currently my kernel version is 6.6.28. Now with sudo nixos-rebuild boot is fetching the latest kernel which looks like 6.8.7. Let’s reboot and yes oh wait, I’m adding the alsa-scarlett-gui as well to the packages list. Now reboot. And the gui shows no interface found. Let’s study the default.nix and mentally unpack the make part:

makeFlags = [ "DESTDIR=\${out}" "PREFIX=''" ];
  sourceRoot = "${src.name}/src";
  nativeBuildInputs = [ pkg-config wrapGAppsHook4 makeWrapper ];
  buildInputs = [ gtk4 alsa-lib openssl ];
  postInstall = ''
    wrapProgram $out/bin/alsa-scarlett-gui --prefix PATH : ${lib.makeBinPath [ alsa-utils ]}

    substituteInPlace $out/share/applications/vu.b4.alsa-scarlett-gui.desktop \
      --replace "Exec=/bin/alsa-scarlett-gui" "Exec=$out/bin/alsa-scarlett-gui"
  '';

What is makeFlags and how does this work on Nix? Search gives me an interesting nixpkgs github issue, with the title Do not set DESTDIR in make flags. And here we have this. So by reading the conversation in this issue there is a better way to declare these. I’m asking GPT to give me some info on DESTDIR and PREFIX.

  1. DESTDIR: This is used during the make install phase as a staging directory. It’s a temporary location where files are installed before being moved to their final destination.
  2. PREFIX: This variable typically specifies the root of where the program expects its hierarchy to start. For example, setting PREFIX=/usr/local would typically lead to binaries being installed to /usr/local/bin, libraries to /usr/local/lib, etc.

When make install is run with DESTDIR=$(out), it installs the program into the directory specified by $(out) at build time, but it doesn’t change how the program internally references its resources if those paths are hardcoded to use PREFIX. This can lead to runtime errors if the program looks for resources in a directory structure that doesn’t exist on the user’s system.

Day 2

Looking at

The Nixpkgs manual states:

makeFlags = [ "PREFIX=$(out)" ];

To summarize, I need to learn how to write this module. I'll review the general Linux installation instructions and compare them with another kernel module packaged as a derivation to understand better. That might bring more light into this. Maybe I could find the nix derivation that packages the snd_usb_audio module? On search.nixos.org I enter keywords “snd usb audio” and look into the linuxKernel package set but I’m not finding any such keyword there. Hmm, the advice I got on the Nix Audio matrix channel is to add the derivation to the boot.extraModulePackages. I should look into that, maybe there are examples over there. Searching on search.nixos.org under options we see that it is a list of additional packages supplying kernel modules and it is declared in nixos/modules/system/boot/kernel.nix . We see it defined as:


    boot.extraModulePackages = mkOption {
      type = types.listOf types.package;
      default = [];
      example = literalExpression "[ config.boot.kernelPackages.nvidia_x11 ]";
      description = lib.mdDoc "A list of additional packages supplying kernel modules.";
    };

My second idea is maybe if I look through the nixpkgs github repo I may find similar packages. Since I am on a fresh NixOS install I still don’t have it cloned. I already forked the Nixpkgs repository so I will clone it on my drive and sync my fork with the upstream repository:

❯ git clone https://github.com/stablejoy/nixpkgs.git
Cloning into 'nixpkgs'...

This takes a while so I will search through the repository on github.

I’m reading through some of these but I’m still feeling kinda lost. Let’s finish setting up the nixpkgs repository:

❯ cd nixpkgs
❯ git remote -v
origin  https://github.com/stablejoy/nixpkgs.git (fetch)
origin  https://github.com/stablejoy/nixpkgs.git (push)
❯ git remote add upstream https://github.com/NixOS/nixpkgs.git
❯ git remote -v
origin  https://github.com/stablejoy/nixpkgs.git (fetch)
origin  https://github.com/stablejoy/nixpkgs.git (push)
upstream        https://github.com/NixOS/nixpkgs.git (fetch)
upstream        https://github.com/NixOS/nixpkgs.git (push)
❯ git fetch upstream

Let’s try again and go through it. I download the driver from https://github.com/geoffreybennett/scarlett-gen2/releases/tag/v6.8-v1.0 and unpack it into a directory. The installation part in the README says:

## Installation Instructions
  35   │ 
  36   │ To build and install the kernel module:
  37   │ 
  38   │ - make sure your system is up to date (probably `dnf update` or
  39   │   `apt-get update; apt-get dist-upgrade`),
  40   │ - reboot in case your kernel was updated and you aren't already
  41   │   running it,
  42   │ - install the kernel headers for your kernel (probably `dnf install
  43   │   kernel-devel` or `apt-get install linux-headers-$(uname -r)`),
  44   │ - run the following commands from this directory:
  45   │ 
  46   │ ```bash
  47   │ KSRCDIR=/lib/modules/$(uname -r)/build
  48   │ make -j4 -C $KSRCDIR M=$(pwd) clean
  49   │ make -j4 -C $KSRCDIR M=$(pwd)
  50   │ sudo make -j4 -C $KSRCDIR M=$(pwd) INSTALL_MOD_DIR=updates/snd-usb-audio modules_install
  51   │ sudo depmod
  52   │ ```
  53   │ 
  54   │ - and reboot.
  55   │ 
  56   │ Check `dmesg | grep -A 5 -B 5 -i focusrite` to see the driver status.
  57   │ Watch `dmesg -w` in case of any issues.
  58   │ 

What is KSRCDIR? Kernel source directory? Yes. Let’s actually start building our derivation default.nix and learn along:

{ }:
{

}

Examples of packaged modules, a first iteration, looking at fetchers and hashes

So I search again for any writings on packaging NixOS modules and I find this blog at https://blog.prag.dev/building-kernel-modules-on-nixos where it says that NixOS kernel modules don’t land in lib/modules/$(uname -r). They’re in run/current-system/kernel-modules. Oh! So this brings some light into our kernel source directory line. The user also says that building the kernel module is mostly a standard process. His derivation begins with:

{ stdenv
, fetchpatch
, nukeReferences
, linuxPackages
, kernel ? linuxPackages.kernel
, version
, src
}:

Now what do I need of these? stdenv for sure but fetchpatch or fetchurl?

Processing.. Ah, as Infinisil always does, let’s look into the Nixpkgs manual at https://nixos.org/manual/nixpkgs/stable/#chap-pkgs-fetchers and read about fetchers. So fetchers are functions to obtain remote sources via various protocols and services. That makes sense. Oh an interesting distinction is between a nixpkgs fetcher and a builtin fetcher. A built-in fetcher will download and cache files at evaluation time and produce a store path. A Nixpkgs fetcher will create a (fixed-output) derivation, and files are downloaded at build time. So it seems I need a nixpkgs fetcher.

I think my derivation for now might look like this:

{ stdenv, fetchurl }:

stdenv.mkDerivation {
  name = "hello";
  src = fetchurl {
    url = "https://github.com/geoffreybennett/scarlett-gen2/releases/download/v6.8-v1.0/snd-usb-audio-kmod-6.8-v1.0.tar.gz";
    hash = "sha256-BBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBBB=";
  };
}

How do I get the hash? ChatGPT tells me there is a nix-prefetch-url tool that can get that and it already provides me with a template for my derivation with included names of the package. That’s very nice. Let’s use that since it looks ok to me to begin with. Although, I see the rec and I remember Infinisil saying it is an antipattern in Nix so we should use something else. Let’s delete just that for now:

{ lib, stdenv, fetchurl }:

stdenv.mkDerivation {
  pname = "scarlett-gen2";
  version = "v6.8-v1.0";

  src = fetchurl {
    url = "https://github.com/geoffreybennett/scarlett-gen2/archive/refs/tags/v6.8-v1.0.tar.gz";
    sha256 = "1abc234...";  # Replace with actual hash from nix-prefetch-url
  };

  buildInputs = [ ];

  buildPhase = ''
    # Your build commands
  '';

  installPhase = ''
    # Your install commands
  '';

  meta = {
    description = "Description of scarlett-gen2";
    homepage = "https://github.com/geoffreybennett/scarlett-gen2";
    license = lib.licenses.gpl2;
  };
}

Let’s find out the hash:

❯ nix-prefetch-url --type sha256 https://github.com/geoffreybennett/scarlett-gen2/archive/refs/tags/v6.8-v1.0.tar.gz

path is '/nix/store/cdla6q8n4cmr1pgh8fznjhzv0qjxs2c6-v6.8-v1.0.tar.gz'
1w58ap6d69ziki7r8fl03g6l9gsajsvqm8rf8102rfl91bx3idnc
~/Downloads/snd-usb-audio-kmod-6.8-v1.0                                                                                         43s 11:24:19
❯ 

Now the src part looks like:

src = fetchurl {
    url = "https://github.com/geoffreybennett/scarlett-gen2/archive/refs/tags/v6.8-v1.0.tar.gz";
    sha256 = "1w58ap6d69ziki7r8fl03g6l9gsajsvqm8rf8102rfl91bx3idnc";
  };

What are the build inputs? Wait I see the hash in other package is declared with hash and not sha256. Instead of using just the hexadecimal representation under the sha256 key, Nix now supports a more explicit format using the hash key. This format includes the hash type followed by the base32-encoded hash value. Let’s modify our command:

src = fetchurl {
    url = "https://github.com/geoffreybennett/scarlett-gen2/archive/refs/tags/v6.8-v1.0.tar.gz";
    hash = "sha256-10a40yqks29qmnnqsriszs4227ncginaj5jbrg525qxnsjsb7p7c";
  };

Why are the two hashes different? Wait, the two hashes are not the same and also, why am I using nix-prefetch-url. Is there a nix-prefetch-git. Also the Nix reference manual says that The command nix-prefetch-url downloads the file referenced by the URL url, prints its cryptographic hash, and copies it into the Nix store. The file name in the store is hash-baseName, where baseName is everything following the final slash in url. This is what puzzled me in the output, the nix store part.

https://ryantm.github.io/nixpkgs/builders/fetchers/#fetchpatch tells us more interesting things on fetchers, here fetchpatch works very similarly to fetchurl with the same arguments expected. It expects patch files as a source and performs normalization on them before computing the checksum. For example, it will remove comments or other unstable parts that are sometimes added by version control systems and can change over time.

Oh, this is interesting because it makes me think of CA derivations, or Content-Addressed Derivation. In the traditional input-addressed model, each package is uniquely identified by the hash of its inputs, which includes source code, dependencies, and build scripts. Any change in the inputs, even if it doesn't affect the output (like a comment or simply adding an empty line), would result in a different hash, leading to a different derivation. So in a way tools like fetchpatch are helping/hack with this transition towards CA derivation. What would normalization mean in context of CA derivations? OK, I am digging a rabbit hole. What I don’t understand is why use fetchpatch and not something other fetcher. Hmm. Next I find myself studying this thread

It’s beyond me. Then I try this:

nix-repl> builtins.fetchGit {
  url = "https://github.com/geoffreybennett/scarlett-gen2";
}

{
  lastModified = 1710434395;
  lastModifiedDate = "20240314163955";
  narHash = "sha256-dk7x/dFGz0IUecjP001z6nHiymDf2p5WeoHlWJxgsxQ=";
  outPath = "/nix/store/6yydaq02waqvpij9md7hcnn35c6520gn-source";
  rev = "f8d34b50f030acea28d2f63fb0c6dea853e80fe5";
  revCount = 3;
  shortRev = "f8d34b5";
  submodules = false;
}

There are various distinctions between fetchers and deeper ones too, in the sense of when or how they work in the evaluation model. Then I find quotes such as these: "nix-prefetch-url checks the SSL certificate, fetchurl does not." This tells me I need to move on and look again into packaging the driver module.

Hmm. Searching for fetchers, I foun

d an interesting Reddit thread where a tool called nurl is recommended. This tool generates Nix fetcher calls from repository URLs. But then it says if we want to generate Nix packages, we should check out nix-init, which builds on top of nurl. I remember watching a Jon Ringer python packaging video in which he talked about a nix-template tool or such for making Nix packaging templates. So there are more of these tools. Let's check out nix-init at https://github.com/nix-community/nix-init. The cool thing is it generates Nix Packages from URLs with some extra tweaking.

Generating a template with nix-init

Let’s try directly running the flake with the url of the scarlett project:

nix run github:nix-community/nix-init --url https://github.com/geoffreybennett/scarlett-gen2/releases/tag/v6.8-v1.0
error: unrecognised flag '--url'
Try 'nix --help' for more information.

Well that doesn’t work. Let’s first run the flake with ❯ nix run github:nix-community/nix-init but this is taking very long indeed. Downloading lots of data and then compiling it. I wonder if simply adding the nix-init or trying nix-shell -p nix-init would be faster. This has to do with how flakes work. It reminds me of that time at Fedora or Ubuntu when for installing one application in flatpack format it would download the whole flatpack tree, but again I’m getting ahead of myself. Inner workings of flakes are unknown to me. OK, nix-init starts with questions:

❯ nix run github:nix-community/nix-init
Enter url
❯ https://github.com/geoffreybennett/scarlett-gen2/releases/tag/v6.8-v1.0
Enter tag or revision (defaults to v6.8-v1.0)
❯ v6.8-v1.0
Enter version
❯ 6.8-v1.0
Enter pname
❯ scarlett-gen2
How should this package be built?
❯ stdenv.mkDerivation
Enter output path (leave as empty for the current directory)
❯ .

It generated a default.nix in the current directory. Let’s look at our iteration:

{ lib
, stdenv
, fetchFromGitHub
}:

stdenv.mkDerivation rec {
  pname = "scarlett-gen2";
  version = "6.8-v1.0";

  src = fetchFromGitHub {
    owner = "geoffreybennett";
    repo = "scarlett-gen2";
    rev = "v${version}";
    hash = "sha256-7NyztNS24yLKy0sWqWx8zB4hiP46Zo2trTgJPbEHRIE=";
  };

  meta = with lib; {
    description = "Linux kernel source tree with additional Focusrite Scarlett and Clarett support (\"Scarlett2 USB Protocol Mixer Driver";
    homepage = "https://github.com/geoffreybennett/scarlett-gen2/releases/tag/v6.8-v1.0";
    license = licenses.unfree; # FIXME: nix-init did not find a license
    maintainers = with maintainers; [ ];
    mainProgram = "scarlett-gen2";
    platforms = platforms.all;
  };
}

Oh that does look helpful. Let’s look at what we have to fix. I already see pname, repo, mainProgram repeating the same "scarlett-gen2" so we should abstract that somehow. Maybe with that inherit. But then I see rec and again, Infinisil has said in Nix-hour that rec is an anti pattern. Let’s look into that at

Finding a broken anchor in the wild

Ah, interesting. So, clicking on the mentioned nix.dev link leads to a "page not found". One of the issues that I'm still working on, and somewhat paused when the GSoC application process began, is the redirect script for nix.dev. And I myself have stumbled on an old anchor that leads to page not found. I have described the issue and opened it on github nix.dev page at:

Coming back to the rec issue I find such deep conversations such as:

I'm finding this hard to understand right now because I don't have enough experience or knowledge. I think I need to look at more examples to better understand.

Going through a packaging tutorial at nix.dev

I found a nice tutorial at

Let's try that. This looks promising, and nix.dev is actually the official NixOS documentation site. The tutorial first recommends becoming confident with reading the Nix language tutorial at:

It states that the tutorial will take 60 minutes if I go through it carefully, which means I will have to do this tomorrow or later tonight. However, I feel better about this; it looks very promising. The tutorial is interactive so not to simply repeat it over here I will try adapting it to my package. It begins with s skeleton derivation:

{ stdenv }:

stdenv.mkDerivation { };

Finding a syntax error in the example code and reporting it

Interesting, in my helix editor with loaded Nix Language server it shows an error:

    1  { stdenv }:                                                                                                                           
    2                                                                                          Multiple top-level expressions are not allowed
â—Ź   3  stdenv.mkDerivation { };                                                                                                (syntax_error)
    ~                                                                                                                 Expecting an expression
                                                                                                                        (syntax_error)

Let’s try to evaluate it:

❯ nix-instantiate --eval default.nix
error: syntax error, unexpected ';', expecting end of file

       at /home/stablejoy/src/derTut/default.nix:3:24:

            2|
            3| stdenv.mkDerivation { };
             |                        ^
            4|

Let’s try to remove the ; and evaluate it again, but this time directly from the command line:

❯ nix-instantiate --eval '{ stdenv }: stdenv.mkDerivation {}'
error: getting status of '/home/stablejoy/src/derivationTutorial/{ stdenv }: stdenv.mkDerivation {}': No such file or directory

By looking at the nix-instantiate section in the Nix reference manual I see there is --expr option that I can use like this:

❯ nix-instantiate --eval --expr '{ stdenv }: stdenv.mkDerivation {}'
<LAMBDA>

Aha! Now it works. So it seems to me I found an error again in the Nix documentation page. Let’s report that too or even better let’s try to fix it? I described on the Nix Documentation matrix channel the issue:

I'm going through the packaging tutorial at https://nix.dev/tutorials/packaging-existing-software.html and evaluating the example skeleton derivation gives a error: syntax error, unexpected ';', expecting end of file. Is this a typo?

{ stdenv }:
stdenv.mkDerivation {	};

# shouldn't it be this instead
❯ nix-instantiate --eval --expr '{ stdenv }: stdenv.mkDerivation {}'
<LAMBDA>

And infinisil responds too! That’s great!

image

Merging my first PR

I actually have no idea how to add a PR for the documentation, but I have an idea what I could do first. I can see the previous typo issues and see how that was done. Let’s see about that. For such a small issue as removing a single character ; is there a quick way to do it on github? By looking at closed it looks like everyone merged PR’s from their own fork. Example:

I'm asking ChatGPT if I can make this minor change directly on the web interface, without merging it from my own fork. Actually, I could do it as I've already created a fork. But I'm curious to know how it's done. So, I can directly edit the file. Let's do that. Ah, I understand now. I was trying to create a PR, but it seems I need to edit the code directly and then I can open a PR in the same area.

Let’s go directly to the source directory:

So after clicking the edit button github says that: “You’re making changes in a project you don’t have write access to. Submitting a change will write it to a new branch in your fork stablejoy/nix.dev, so you can send a pull request.” I think this looks fine so far. If I was doing if from my own fork I would also have to do a branch and then send a pull request. OK, I just removed the semicolon in question at the skeleton derivation. I am now supposed to click on the big green button that says: “Commit changes…” this is so exciting. In 15+ years of linuxing around I have never committed anything ever, wait, yes, I think I might cry. Look!

image

I see the option to merge and the big green button labeled "Create Pull Request!" OMG, I did it! It says "stablejoy wants to merge." Yes, I do! I cry. A bit later, infinisil has merged my PR! Something interesting to note is that when I sent my PR, infinisil still hadn't seen it. He explained it later in the matrix channel: “Ah nice, didn't see it because it has a dedicated code owner that gets pinged (which overrides the default code owner of the entire team). Thanks, merging!”

Continuing with the packaging tutorial and making first iterations of the scarlett package

I should continue with the tutorial. I’m finding more errors it seems. So I am following the tutorial but merely substituting the hello example with my scarlett driver, so far only using a different pname and version. My scarlett.nix looks like this:

#scarlett.nix
{
  lib, 
  stdenv,
  fetchzip,
}:

stdenv.mkDerivation {
  pname = "scarlett-gen2";
  version = "6.8-v1.0";

  src = fetchzip {
    url = "https://github.com/geoffreybennett/scarlett-gen2/releases/download/v6.8-v1.0/snd-usb-audio-kmod-6.8-v1.0.tar.gz";
    sha256 = lib.fakeSha256;
  };
}

And my default.nix:

# default.nix
let
  nixpkgs = fetchTarball "https://github.com/NixOS/nixpkgs/tarball/nixos-22.11";
  pkgs = import nixpkgs { config = {}; overlays = []; };
in
{
  scarlett = pkgs.callPackage ./scarlett.nix { };
}

Now the curious thing is the nixos-22.11 part so I tried to correct that with nixos-23.11 which is the current stable version but when evaluating this build with nix-build -A scarlett.nix I get a different error than in the tutorial where I am supposed to get a hash mismatch:

❯ nix-build -A scarlett.nix
error: attribute 'nix' in selection path 'scarlett.nix' not found

What is going on here? Let’s try get more errors:

❯ nix-build scarlett.nix
error: cannot evaluate a function that has an argument without a value ('lib')
       Nix attempted to evaluate a function as a top level expression; in
       this case it must have its arguments supplied either by default
       values, or passed explicitly with '--arg' or '--argstr'. See
       https://nixos.org/manual/nix/stable/language/constructs.html#functions.

       at /home/stablejoy/src/derivationTutorial/scarlett.nix:2:3:

            1| {
            2|   lib,
             |   ^
            3|   stdenv,

❯ nix-build -A default.nix
error: attribute 'default' in selection path 'default.nix' not found
❯ nix-build -A scarlett.nix
error: attribute 'nix' in selection path 'scarlett.nix' not found

Hmm. In one error default is not found and then in the other the entire name scarlett.nix is not found. Let’s try to build it with:

❯ nix-build default.nix
these 2 derivations will be built:
  /nix/store/srwfxvxlc5zns3f2ihx1amr5avnhpzxw-source.drv
  /nix/store/82zslkzb7qmnx656mbd8s54xyv1q1acs-scarlett-gen2-6.8-v1.0.drv
these 2 paths will be fetched (0.22 MiB download, 2.92 MiB unpacked):
  /nix/store/j9any8p1kj8m23094z2vc0jwns512280-glibc-locales-2.39-5
  /nix/store/9di76da9dlcvfbvj15jlah32wz0wm8v7-mirrors-list
copying path '/nix/store/j9any8p1kj8m23094z2vc0jwns512280-glibc-locales-2.39-5' from 'https://cache.nixos.org'...
copying path '/nix/store/9di76da9dlcvfbvj15jlah32wz0wm8v7-mirrors-list' from 'https://cache.nixos.org'...
building '/nix/store/srwfxvxlc5zns3f2ihx1amr5avnhpzxw-source.drv'...

trying https://github.com/geoffreybennett/scarlett-gen2/releases/download/v6.8-v1.0/snd-usb-audio-kmod-6.8-v1.0.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
  0     0    0     0    0     0      0      0 --:--:-- --:--:-- --:--:--     0
100  328k  100  328k    0     0   322k      0  0:00:01  0:00:01 --:--:-- 3225k
unpacking source archive /build/snd-usb-audio-kmod-6.8-v1.0.tar.gz
error: hash mismatch in fixed-output derivation '/nix/store/srwfxvxlc5zns3f2ihx1amr5avnhpzxw-source.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-3e+I3gX7K9oU6XJhVtWk9QpE5B81pPApWQxmkX2/CAg=
error: 1 dependencies of derivation '/nix/store/82zslkzb7qmnx656mbd8s54xyv1q1acs-scarlett-gen2-6.8-v1.0.drv' failed to build

Oh! We are now getting the error that was in the tutorial. So we are supposed to get a hash mismatch and here it is! Now we need to find the hash and Nix provides us the correct hash right there:

specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-3e+I3gX7K9oU6XJhVtWk9QpE5B81pPApWQxmkX2/CAg=

Now looking at this format sha256-A...A= I am not sure how to add it. Is the - included and what about the equality sign =? After few tries I see the correct way is:

  sha256 = "3e+I3gX7K9oU6XJhVtWk9QpE5B81pPApWQxmkX2/CAg=";

Day 3

And when our package is not in nixpkgs?

Maybe the reason I have been getting errors when evaluating the scarlett.nix is because it is not a package that is defined in nixpkgs already. So when the evaluation is pulling the nixpkgs from default.nix I get an error? Let’s try the example with hello as in the tutorial to see if there are actually any errors with the tutorial.

Oh, I am getting the same error when evaluating the hello and default.nix and GNU Hello is already in nix packages. What is going on here?

❯ nix-build -A hello.nix
error: attribute 'nix' in selection path 'hello.nix' not found

If I evaluate the default.nix then it prints out the correct error as in the tutorial:

trying https://ftp.gnu.org/gnu/hello/hello-2.12.1.tar.gz
  % Total    % Received % Xferd  Average Speed   Time    Time     Time  Current
                                 Dload  Upload   Total   Spent    Left  Speed
100 1009k  100 1009k    0     0   280k      0  0:00:03  0:00:03 --:--:--  280k
unpacking source archive /build/hello-2.12.1.tar.gz
error: hash mismatch in fixed-output derivation '/nix/store/axarych0fz65cqgmxa8vcy8gf6k2ac0w-source.drv':
         specified: sha256-AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=
            got:    sha256-1kJjhtlsAkpNB7f6tZEs+dbKd8z7KoNHyDHEJ0tmhnc=
error: 1 dependencies of derivation '/nix/store/1v7bpw8z0kqlmqizyx82hvgmzrgdh6rx-hello-2.12.1.drv' failed to build

Why is my hash always getting the = sign at the end? Ah, ok, that indicates that the encoding has been applied correctly. Then what is the issue here? Let’s try to sandbox our environment with nix-shell. So inside nix-shell nix-build hello.nix fails with the same error but nix-build default.nix builds a derivation. Oh now when I exit the nix-shell I see I can’t reproduce the error from before because:

❯ nix-build default.nix
/nix/store/ra5dpfzdbrfrz5wqlvb68q7g598c3mk6-hello-2.12.1

What do we do now? How to reproduce the previous error? How to remove this hello build from the nix store? Searching gets us this command which removes a bunch of stale links:

❯ nix-store -q --roots /nix/store/ra5dpfzdbrfrz5wqlvb68q7g598c3mk6-hello-2.12.1

Not sure what happened here. I start searching for nix store and there is a command to delete a derivation with:

❯ nix store delete /nix/store/ra5dpfzdbrfrz5wqlvb68q7g598c3mk6-hello-2.12.1

0 store paths deleted, 0.00 MiB freed
error: Cannot delete path '/nix/store/ra5dpfzdbrfrz5wqlvb68q7g598c3mk6-hello-2.12.1' since it is still alive. To find out why, use: nix-store --query --roots

Oh and here again our previous command:

❯ nix-store -q --roots /nix/store/ra5dpfzdbrfrz5wqlvb68q7g598c3mk6-hello-2.12.1

/home/stablejoy/src/derivationTutorial/hello/result -> /nix/store/ra5dpfzdbrfrz5wqlvb68q7g598c3mk6-hello-2.12.1

This is telling us how the current result link is linking to the nix store path. Now what happens when we delete the result from our working directory?

❯ rm -rf result
❯ ls
default.nix  hello.nix
❯ nix-store -q --roots /nix/store/ra5dpfzdbrfrz5wqlvb68q7g598c3mk6-hello-2.12.1


removing stale link from '/nix/var/nix/gcroots/auto/3j5jkx9666hr2scsagiaxh8mcg1w02jy' to '/home/stablejoy/src/derivationTutorial/hello/result'

If we try to build our derivation again we get a new derivation with the same hash:

❯ nix-build -A hello
/nix/store/ra5dpfzdbrfrz5wqlvb68q7g598c3mk6-hello-2.12.1

I found the error which was me making a typo each time. Instead of writing nix-build hello I was writing nix-build hello.nix which was giving the path error:

❯ nix-build -A hello
/nix/store/ra5dpfzdbrfrz5wqlvb68q7g598c3mk6-hello-2.12.1
❯ nix-build -A hello.nix
error: attribute 'nix' in selection path 'hello.nix' not found

Hmm. The tutorial also says this allows you to run nix-build -A hello to realize the derivation in hello.nix, similar to the current convention used in Nixpkgs. I have changed the nixpkgs version in default.nix from 22 to 23 and yes, the Hello package was again recompiled and a new derivation was built. Let’s move on. But I still only have one result in the directory. Does that mean the previous 22 result was overwritten? Now when we evaluate my scarlett driver it fails of course because there are no build phases declared. Hello is a very simple package.

Now comes the next part of the tutorial: packaging a larger program with dependencies. What are the dependencies of my Scarlett driver/module? The [README.md](<http://README.md>) in the source directory instructs updating the system and installing kernel headers for your kernel. Do I have the kernel headers? Ah, I see an interesting Hacker News comment on decoupling kernel headers in NixOS by a user named danieldk, a name I believe I've seen on the NixOS discourse forum.

This decoupling of build dependencies can be seen here in the kernel-headers/default.nix:

nativeBuildInputs = [
  perl elf-header
] ++ lib.optionals stdenvNoCC.hostPlatform.isAndroid [
  bison flex rsync
] ++ lib.optionals (stdenvNoCC.buildPlatform.isDarwin &&
                    stdenvNoCC.hostPlatform.isMips) [
  darwin-endian-h
  darwin-byteswap-h
];

But let’s move on. OK, searching for NixOS and kernel I see this interesting blog. Let’s see what we have here:

Oh, Mani starts a nix shell with all the necessary tools to build the linux kernel. I see the bison or flex from previous kernel-header example. Now I guess what I need is basically some kind of general header that will be included along with stdenv and fetchzip and such. Yes, this looks like when I cloned the Nix source and compiled it on my machine. Let’s move on.

Packaging out-of-tree modules

NixOS wiki has an interesting section on packaging out-of-tree modules! This may be useful for me since I am doing the same thing.

Let’s hold on to this and go back to our tutorial. I feel I need to do that a bit more. But this looks very promising.

Looking at a more elaborate example of packaging a slightly larger program than hello, we examine icat, which is hosted on GitHub. My Scarlett is also hosted there, so we can use fetchFromGitHub as the fetcher, then add owner, rev and repo fields, and calculate the hash with nix-prefetch-url. Now, we are making progress again, slowly. Our Scarlett should look a bit better now. We add the owner by looking at the address bar of the project and then the repo which is the last bit of the address and then the rev number which is v1.0 and then use the nix-prefetch-url to unpack the hash as shown in the tutorial with:

❯ nix-prefetch-url --unpack https://github.com/geoffreybennett/scarlett-gen2/releases/download/v6.8-v1.0/snd-usb-audio-kmod-6.8-v1.0.tar.gz --type sha256
path is '/nix/store/k90g10l2x3ak0i0n47y4vv57bwa8qjzn-snd-usb-audio-kmod-6.8-v1.0.tar.gz'
0208pxyr2rhcb4lz191m3zj482pmlkamcqbjx4adlazv0pg8ivyx

Now our newest iterations looks like:

# scarlett-gen2
{
  lib, 
  stdenv,
  fetchFromGitHub,
}:

stdenv.mkDerivation {
  pname = "scarlett-gen2";
  version = "6.8-v1.0";

  src = fetchFromGitHub {
    owner = "geoffreybennett";
    repo = "scarlett-gen2";
    rev = "v1.0";
    sha256 = "0208pxyr2rhcb4lz191m3zj482pmlkamcqbjx4adlazv0pg8ivyx";
  };
}

Let's nix-build -A this and be careful not to include the .nix when you type nix-build -A scarlett. As expected, it fails, but with a much nicer error than before! Yes, we need more phases in our build process. I am fetching it properly, but now the build environment needs to be fully declared.

❯ nix-build -A scarlett
this derivation will be built:
  /nix/store/plx4kqp9jc2c8c2sw70x2i8fia9qpgyr-scarlett-gen2-6.8-v1.0.drv
building '/nix/store/plx4kqp9jc2c8c2sw70x2i8fia9qpgyr-scarlett-gen2-6.8-v1.0.drv'...
Running phase: unpackPhase
unpacking source archive /nix/store/k32bcnhf8n9a33x65mprcqnyi2s722if-source
source root is source
Running phase: patchPhase
Running phase: updateAutotoolsGnuConfigScriptsPhase
Running phase: configurePhase
no configure script, doing nothing
Running phase: buildPhase
build flags: SHELL=/nix/store/m0s1xf30bdk6vfn5m6c3mhb2z8w1cib5-bash-5.2-p15/bin/bash
make: *** No targets.  Stop.
error: builder for '/nix/store/plx4kqp9jc2c8c2sw70x2i8fia9qpgyr-scarlett-gen2-6.8-v1.0.drv' failed with exit code 2;
       last 10 log lines:
       > Running phase: unpackPhase
       > unpacking source archive /nix/store/k32bcnhf8n9a33x65mprcqnyi2s722if-source
       > source root is source
       > Running phase: patchPhase
       > Running phase: updateAutotoolsGnuConfigScriptsPhase
       > Running phase: configurePhase
       > no configure script, doing nothing
       > Running phase: buildPhase
       > build flags: SHELL=/nix/store/m0s1xf30bdk6vfn5m6c3mhb2z8w1cib5-bash-5.2-p15/bin/bash
       > make: *** No targets.  Stop.
       For full logs, run 'nix log /nix/store/plx4kqp9jc2c8c2sw70x2i8fia9qpgyr-scarlett-gen2-6.8-v1.0.drv'.

Now the tutorial example fails as well because of a missing dependency which is shown in the error log. That dependency is available in the nixpkgs so the tutorial instructs to add it into the argument list and build inputs. We have here a slightly different situation. Nix does not tell us what is missing here but we have a clue there in the line no configure script, doing nothing. Yes that’s very obvious. So what do we do now? The building kernel modules blog has buildInputs = [ nukeReferences ]; and I don’t know what this means.

Oh wow, look at Rickard Nilsson asking about these references in a NixOS mailing list in 2011.

All this tells me I should look into that official out-of-kernel wiki. I should stop diggin into these references. Looking at the example I may need to add these lines:

{ stdenv, lib, fetchFromGitHub, kernel, kmod }:
# and this one
  nativeBuildInputs = kernel.moduleBuildDependencies;                       # 2

But now we have nativeBuildInputs and buildInputs in other example. So which one to use and what do they mean? Looking at the nixpkgs I find the explanation:

Add dependencies to nativeBuildInputs if they are executed during the build. Add dependencies to buildInputs if they will end up copied or linked into the final output or otherwise used at runtime. Now I don’t understand why would [ nukeReferences ]; be put in buildInputs?

New example of packaging a kernel module

It is becoming clear I will definetly need these in the beginning:

{ lib
, stdenv
, fetchFromGitHub
, kernel
, kmod
}:

The user is also adding the hardeningDisable = [ "pic" "format" ]; which I don’t think I need but we will keep this in mind somewhere. Then comes the line that looks promising: nativeBuildInputs = kernel.moduleBuildDependencies;. I’m inclined to try it out and see what kind of error I get with my scarlett package. Let’s do that. Wait, I’m starting to make all these changes and they are all untracked. It would be nice to have all this in a git repo with history too. Now do I fork the scarlett project and build the derivation within the source directory. No? I’m fetching it already. Maybe I will just keep track of my default.nix and scarlett.nix

Creating a GitHub repository for my project to keep track of changes.

I need to setup my github token and all the authentication stuff so for now I will be keeping track locally by git add . or git add default.nix scarlett.nix and then git commit -m "changes made".

Day 4

I completed my GitHub tasks in the morning. I'll skip that since ChatGPT can assist quite well with questions and git commands. So, what should I do now? It seems the tutorial is diverging slightly from my issue. Let's revisit the available links for adding the kernel module and read it again. The problem is, I'm still uncertain about which function parameters I need to add to compile this driver. Alternatively, there's a second approach where I generate errors by adding these parameters and observing the output. I suppose I'll do that when I have no other options left. So far this post looks most promising with most details. This user packaged a module for NixOS:

So before adding the package to nixpkgs I need to get the package working on my local machine. By working it means that I have to build it and then pass it into my configuration.nix through boot.extraModulePackages = [ my-scarlett-module ];After that I contribute it to nixpkgs. Hmm, let’s see if this is something that can help as well:

Ringer needs to add a kernel module using sudo modprobe i2c-dev, and optionally, two other things from this module. This driver is for a USB device. He uses his nix-template to generate a module template, but upon examining the template, I see options and configurations that don't resemble a kernel driver template. Let's continue watching. There's a timestamp later that says: "Adding conditional kernel modules." Let's see. Hmm, this seems a bit higher. They are configuring the module and systemd. My issue seems lower down because I need to build and install a module, which is a C program.

Getting stuck in the weeds, what to do next?

When in doubt read?

Linux distributions put their kernel sources in /usr/src and their kernel modules in /lib/modules/$(uname -r). Aha, I remember seeing this path in the other derivation:

makeFlags = [
    "KVERSION=${kernel.modDirVersion}"
    "KDIR=${kernel.dev}/lib/modules/${kernel.modDirVersion}/build"
    "MODULEDIR=$(out)"
  ];

But I see here the KDIR variable corresponds to the KSRCDIR variable in the driver’s installation instruction. I colored them in blue:

  47   │ KSRCDIR=/lib/modules/$(uname -r)/build
  48   │ make -j4 -C $KSRCDIR M=$(pwd) clean
  49   │ make -j4 -C $KSRCDIR M=$(pwd)
  50   │ sudo make -j4 -C $KSRCDIR M=$(pwd) INSTALL_MOD_DIR=updates/snd-usb-audio modules_install
  51   │ sudo depmod

So my current iteration that doesn’t work is here:

KSRCDIR=/lib/modules/$(uname -r)/build
make -j4 -C $KSRCDIR M=$(pwd) clean
make -j4 -C $KSRCDIR M=$(pwd)
sudo make -j4 -C $KSRCDIR M=$(pwd) INSTALL_MOD_DIR=updates/snd-usb-audio modules_install
sudo depmod