Building the Linux Kernel with Nix, NixOS and Gitea


December 29, 2024

Last updated: December 29, 2024

I recently bought a Surface Go 2 tablet computer and decided to run Linux on it. Many distributions suffer from something called dependency hell, which basically means that upgrading one package can break another package. This makes having a reliable system a nightmare.

It reminds me of the time when I was pursuing my PhD in a research lab and all of my colleagues were using Ubuntu and had to reinstall the OS every few months because of broken dependencies.

Nix and NixOS solve this problem by providing a purely functional package manager and a Linux distribution, respectively. This means that you can have multiple versions of the same package installed on your system without any conflicts. In effect this means that you can have a reliable system that you can upgrade without fear of breaking it. Above that NixOS allows you to roll back to a previous configuration if something goes wrong. This effectively solves the problem of dependency hell.


Enough of the sales pitch...

I decided to install NixOS on my Surface Go 2. However, in order to try get the cameras working, I had to build the Linux kernel with the necessary patches.

In this post, I will show you first how one can build the Linux kernel with Nix and NixOS, and apply kernel patches. Then I will show you how to create a tarball of the built kernel and import it in a Nix configuration.

Finally, I will show you how to set up a CI pipeline with Gitea to build the kernel automatically, because building the kernel on this low-powered device takes a long time.

Take the following custom-kernel.nix file as an example:

{ config, pkgs, ... }:
{
boot.kernelPackages = pkgs.linuxPackages_6_6;
boot.kernelPatches = [
{
name = "enable-ov5693";
patch = null;
extraConfig = ''
MEDIA_SUPPORT y
VIDEO_DEV y
VIDEO_CAMERA_SENSOR y
VIDEO_OV5693 y
'';
}
# cameras patch from linux-surface
{
name = "cameras-patch";
patch = null;
url = "https://github.com/linux-surface/linux-surface/raw/master/patches/6.12/0012-cameras.patch";
sha256 = "sha256-0759cl1xq3jdq6hd97y8gx8671g0y899b7azv623bzxjf1jz40ng";
}
];
}

This file tells Nix to build the Linux kernel version 6.6 with some custom patches. One of the patches sets a couple of kernel configuration options to enable the OV5693 camera sensor. The other patch is the cameras patch from the linux-surface project, which refers to a specific patch file (this one from Github) and its sha256 hash.

To build the kernel, run the following command:

nix-build '<nixpkgs/nixos>' -A config.system.build.kernel --arg configuration ./custom-kernel.nix

This will build the kernel and place the resulting kernel image in the result symlink in the current directory. If you would like to build using all available cores, prepend the command with export NIX_BUILD_CORES=0.

In total:

export NIX_BUILD_CORES=0
nix-build '<nixpkgs/nixos>' -A config.system.build.kernel --arg configuration ./custom-kernel.nix

To create a tarball of the built kernel, run the following commands:

mkdir result-writable
cp -rL result/lib/modules/6.6.63/kernel result-writable/kernel
sudo chmod -R u+rwX,g+rwX,o+rX result-writable
tar -czf custom-kernel.tar.gz -C result-writable .
rm -rf result-writable

This will create a tarball named custom-kernel.tar.gz in the current directory. This tarball can be used to import the kernel in a Nix configuration.

If you can upload the built kernel publicly, you can import it in a Nix configuration like this:

# Use the custom kernel packages
boot.kernelPackages = pkgs.linuxPackagesFor (pkgs.linuxPackages_6_6.kernel.override {
src = pkgs.fetchurl {
url = "https://example.com/custom-kernel.tar.gz";
sha256 = "put-the-actual-sha256-hash-here";
};
});

This will use the custom kernel packages in the Nix configuration.

If you can't upload the kernel publicly, you can use the following Nix expression to import the kernel:

boot.kernelPackages = pkgs.linuxPackagesFor (pkgs.linuxPackages_6_6.kernel.override {
src = pkgs.fetchLocal {
path = "/path/to/your/local/kernel.tar.gz";
sha256 = "put-the-actual-sha256-hash-here";
};
});

I use Gitea as a self-hosted Git service. Using Gitea Actions, it is possible to set up a CI pipeline to build the kernel automatically. This is especially useful if your server is more powerful than the Surface Go 2, speeding up the build process.

Here is an example .gitea/workflows/build-kernel.yml file:

name: Build Custom Kernel
on:
push:
branches:
- master
jobs:
build-kernel:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Set up Nix
run: |
sudo apt-get update
sudo apt-get install -y \
curl \
git \
xz-utils \
ca-certificates
curl -L https://nixos.org/nix/install | sh -s -- --daemon
- name: Build custom kernel
run: |
export NIX_BUILD_CORES=0
nix-build '<nixpkgs/nixos>' -A config.system.build.kernel --arg configuration ./custom-kernel.nix
env:
PATH: /nix/var/nix/profiles/default/bin:/nix/var/nix/profiles/default/sbin:/bin:/sbin:/usr/bin:/usr/sbin
- name: Prepare kernel artifacts
run: |
mkdir result-writable
cp -rL result/lib/modules/6.6.63/kernel result-writable/kernel
sudo chmod -R u+rwX,g+rwX,o+rX result-writable
tar -czf custom-kernel.tar.gz -C result-writable .
rm -rf result-writable
- name: Upload kernel artifacts
uses: actions/upload-artifact@v3
with:
name: custom-kernel
path: ./custom-kernel.tar.gz

This file sets up a CI pipeline that builds the kernel on every push to the master branch. The Nix package manager is added to the default Ubuntu image. Then it uses the same commands as before to build the kernel and create a tarball of the built kernel. Finally, it uploads the tarball as an artifact.


I have yet to figure out how to promote the artifact to a release, but I will update this post when I do. As of now, I manually download the artifact from the Gitea Actions page, and then create a new release and upload the tarball with it. This takes a bit of manual work, but allows you to have a publically available kernel tarball.

I hope this post was helpful to you.