Precompiled kernel

I have a Surface book 2 and use nixos-hardware to install a kernel with custom patches to enable the touchscreen, webcam, etc.

The linux-surface team on github releases precompiled kernels as rpm, deb and tar.zst (for Arch). What I wonder is if it would be possible to use one of these kernels for NixOS?

There are three reasons for this: 1) to avoid recompiling the kernel so often (I know NixOS uses a cache for its packages too, but it doesn’t seem to cache the surface kernel, or rather, it seems to want to recompile the kernel locally each time the flake.lock is updated, even if the kernel version is not changed for some reason), 2) I use tmpfs as root (2 GB) and a recompilation of the kernel requires much more space, so I need to have the tmp-folder mounted on a btrfs subvolume during a rebuild, which I would rather avoid, and 3) as an experiment to see what is possible.

I used the template for the Opera browser to see how a deb package could be installed in NixOS and combined it with the template for the linux-libre kernel to create a template that would download and extract the deb kernel with the correct folder structure. The only problem is that NixOS still wants to download the sources and recompile during a rebuild, after having installed the deb package.

I doubt that you will be able to use any of the precompiled kernels due to how nix and nxOS work. You will need to live with the recompilation.

Of course you can set up your own cache using cachix (a hosted service) or attic (self-hosted). You could build the kernel and any updates on a more powerful machine and then push them to the cache. The surface would then just pull the caches content after evaluation of the configuration.

In general a 2 GiB /tmp is rather small for nix in general, depending on how you use it. In my experience 10 GiB should suffice for building most things, some extreme builds (you usually only hit when contributing to staging) require 50 GiB of /tmp.


Technically you need to provide a kernel with kernel-modules as expected by NixOS. To do this, you need to understand how to replace it (via boot.kernelPackages I guess), elaborate derivations to transform rpm to kernel image and tree of kernel modules and have look to nixpkgs/nixos/modules/system/boot/kernel.nix to identify potential issues


Note that Arch’s package format would be simpler to handle here as it’s just a single simple tarball with a few additional metadata files.

1 Like

For a Python application packaged with rpm I used rpmextract for unPack phase it works reasonably.

1 Like

More information on this.

The kernel seems to need three things:

  2. bzImage
  3. lib folder with the compiled modules

Using this:

  boot.kernelPackages = pkgs.linuxPackagesFor (pkgs.linux.override {
    argsOverride = {
      src = pkgs.stdenv.mkDerivation {

I was able to get the kernel to install without errors. Unfortunately, cpupower won’t install since it tries to add patches to the kernel sources (which aren’t present).

linux> Running phase: installPhase
linux> Running phase: fixupPhase
linux> shrinking RPATHs of ELF executables and libraries in /nix/store/ajh51mvam3raxr46b48bzrzbfq8xczx8-linux-6.6.10
linux> checking for references to /build/ in /nix/store/ajh51mvam3raxr46b48bzrzbfq8xczx8-linux-6.6.10...
linux> patching script interpreter paths in /nix/store/ajh51mvam3raxr46b48bzrzbfq8xczx8-linux-6.6.10
linux> stripping (with command strip and flags -S -p) in  /nix/store/ajh51mvam3raxr46b48bzrzbfq8xczx8-linux-6.6.10/lib
cpupower> Running phase: unpackPhase
cpupower> unpacking source archive /nix/store/ajh51mvam3raxr46b48bzrzbfq8xczx8-linux-6.6.10
linux-config> Running phase: unpackPhase
linux-config> unpacking source archive /nix/store/ajh51mvam3raxr46b48bzrzbfq8xczx8-linux-6.6.10
cpupower> source root is linux-6.6.10
cpupower> Running phase: patchPhase
cpupower> applying patch /nix/store/mnlqwys9dxznjcvq4w3s0435s9ql8qv8-bridge-stp-helper.patch
cpupower> can't find file to patch at input line 5
cpupower> Perhaps you used the wrong -p or --strip option?
cpupower> The text leading up to this was:
cpupower> --------------------------
cpupower> |diff --git a/net/bridge/br_private.h b/net/bridge/br_private.h
cpupower> |index aea3d13..8fcbf81 100644
cpupower> |--- a/net/bridge/br_private.h
cpupower> |+++ b/net/bridge/br_private.h
cpupower> --------------------------
cpupower> File to patch:
cpupower> Skip this patch? [y]
cpupower> Skipping patch.
cpupower> 1 out of 1 hunk ignored
linux-config> source root is linux-6.6.10
error: builder for '/nix/store/6drv31sa6lwhxn5lh1bmxxbpc42dmcwv-cpupower-6.1.74.drv' failed with exit code 1
error: 1 dependencies of derivation '/nix/store/5mcv7cvabqxrv7in9ila511w7h939k3f-system-path.drv' failed to build
error: 1 dependencies of derivation '/nix/store/lbxqrn7sg8ln2j2gw1h58h5k4rpwdykg-unit-cpufreq.service.drv' failed to build
error: 1 dependencies of derivation '/nix/store/i6q8g4gd7wpsya8avxf015p0hyqwx4kh-nixos-system-nixos-23.11.20240122.d7f206b.drv' failed to build

By adding the sources to the package, (so it contains the three things above + the sources), cpupower doesn’t throw an error anymore, but I get this error:

linux-config> error: unused option: USB_SERIAL_GENERIC
linux-config> error: unused option: VGA_SWITCHEROO
linux-config> error: unused option: VIDEO_STK1160_COMMON
linux-config> error: unused option: VIRTIO_MMIO_CMDLINE_DEVICES
linux-config> warning: unused option: WW_MUTEX_SELFTEST
linux-config> error: unused option: XPOWER_PMIC_OPREGION
linux-config> warning: unused option: XZ_DEC_TEST
error: builder for '/nix/store/hphhkcxb06sj8ha5fmdcli9zaw6n00nh-linux-config-6.1.74.drv' failed with exit code 255
error: 1 dependencies of derivation '/nix/store/ib07lps97hynnh2vlrrr371ig5hcjkpg-linux-6.1.74.drv' failed to build
error: 1 dependencies of derivation '/nix/store/ac4hr3jqfb7jsfivr95xhg7zk0480rhl-nixos-system-nixos-23.11.20240122.d7f206b.drv' failed to build

So for some reason it starts to compile kernel 6.1.74 (which is not the one I install) using the config from the sources I provided instead. Is it hopeless?

If you need kernel source-dependant tools/OOT modules, I’d try to take the kernelPackages of the same ABI and only override their kernel attribute with the binaries.

The tools and OOT modules would then not technically be built against this exact kernel artifact but, because they’re the same ABI, they should remain compatible.

1 Like

cpupower isn’t something I have added myself, it just wants to compile automatically when I try to add the new kernel (it’s probably defined as the standard CPU frequency scaler somewhere). Is there a way to avoid stuff like cpupower to compile?

Or another idea: could one perhaps provide the kernel sources where the modules are already compiled, so that the make command skips most of the compile time?

I saw that I didn’t specify version in my command, which explains why it wanted to build for 6.1.74 (which is the standard version for the linux package), so now linux-config starts compiling the kernel from source. So there are no more error messages, but I can’t avoid the compilation yet.

To have sources with compiled modules didn’t work:

linux-config> *** The source tree is not clean, please run 'make ARCH=x86_64 mrproper'

Why can’t the compiled sources be stored somewhere so that NixOS just recompiles what needs to be recompiled? Why do all kernel modules need to be recompiled when not switching kernel version?

Then it’s probably in systemPackages by default?

Not sure make would recognise those due to mtimes being stripped.

Because the source changed.

What I meant btw. was:

  boot.kernelPackages = pkgs.linuxPackages_6_1 // {
    kernel = pkgs.runCommand ...; # extract tarball into $out here to reach the same top-level structure as regular kernel

Because the source changed.

Sorry, I meant when not changing kernel version. For some reason it rebuilds it even then…

However, I tried this:

  boot.kernelPackages = pkgs.linuxPackages_6_6 // {
    version = "6.6.13";
    modDirVersion = "6.6.13";
    kernel = pkgs.runCommand "extract-kernel" {} ''
      tar xvf /home/.../linux-6.6.13.tar.gz "$out"

And it builds without error :slight_smile:!

But the cameras aren’t discovered afterwards (one of the things the surface kernel does is to patch the kernel so that the webcam works).

I even tried to use just put the kernel folder from /nix/store/ into the tar file, but that doesn’t work. It seems something else is missing, but it’s almost there!

I am not sure pkgs.runCommand runs at all. I put ls in there and it built (without running the command). It seems the reason it builds without error is just because it installs the 6.6.13 kernel from the cache.

This seems like a rather hopeless endeavor…

When I finally got pkgs.runCommand to run it couldn’t find the source since it had no access to the home directory, nor the $src variable. The sources didn’t want to download with wget either (even if I included wget as a buildInput) because of a “failure in name resolution”.

It seems that “setting buildCommand overrides all the phases in a stdenv derivation.”: Using the `src` attribute in `runCommand`

Since this doesn’t seem to work (and seems to go against how NixOS is intended to work) I am thinking of abandoning this project.

But is it possible to hinder the kernel from recompiling? Right now the kernel is built every time a nix flake update is performed even if the kernel version hasn’t changed.

Is there any way to avoid this from happening? Or should I ask the people at nixos-hardware to see if they can cache the builds in some way? Or should I not update the flakes that often? Is it unnecessary to update that often?

What is the correct way to go forward?

This is expected because any Nix build is run inside a sandbox which does not have general access to the filesystem or the internet.

You must either import the file directly by referencing it as a Nix path data type or fetch it in a FOD (fixed-output derivation).

If you’re fetching this from the internet anyways, I’d forgo the indirection and do something like this:

  kernel = pkgs.fetchzip {
    url = "";
    hash = "";

How exactly are you pulling in this kernel and how often do you update? You should only see a rebuild on update or every few weeks after a staging-next merge.

The “correct” way would be to get the surface patches into the upstream kernel; making all of this unnecessary.

The easier solution would probably be to use a remote builder for the kernel build that is a lot more powerful than your laptop. I.e. a desktop computer or

1 Like

I add it as a flake:

  inputs = {
    nixpkgs.url = "github:nixos/nixpkgs/nixos-23.11";
    home-manager.url = "github:nix-community/home-manager/release-23.11";
    home-manager.inputs.nixpkgs.follows = "nixpkgs";
    hardware.url = "github:NixOS/nixos-hardware/master";
  outputs = {
  } @ inputs: let
    inherit (self) outputs;
  in {
    nixosConfigurations = {
      nixos = nixpkgs.lib.nixosSystem {
        specialArgs = {inherit inputs outputs;};
        modules = [


I am sorry, but I have tried in so many different ways and the kernel variable doesn’t want to get changed. Could you please provide me with the code that just changes the kernel variable? I have so far tried to do it in three different ways:

  1. Using pkgs.linux.override:
  boot.kernelPackages = pkgs.linuxPackagesFor (pkgs.linux.override {
    argsOverride = {
      src = pkgs.stdenv.mkDerivation {
                kernel = ...

I can change other variables with this, but not the kernel one. When I provide src, etc, it builds, but it starts compiling the kernel from source, so it’s like the variable isn’t read.

  1. Using the code you provided:
  boot.kernelPackages = pkgs.linuxPackages_6_6 // {
     kernel = ...;

Here, it just skips the row setting kernel entierly. No error message, but it just grabs the stock kernel from cache.

  1. I also tried (using this: Linux kernel - NixOS Wiki) to write an overlay:
    (self: super: {
      linuxPackages = super.linuxPackages.extend (lpself: lpsuper: {
        kernel = super.linuxPackages.kernel.overrideAttrs (oldAttrs: {
          kernel = ...;

but here’s the error message:

       … while calling the 'head' builtin

         at /nix/store/5hwz775f3grzikafj1sbwx4lqkjwqswb-source/lib/attrsets.nix:922:11:

          921|         || pred here (elemAt values 1) (head values) then
          922|           head values
             |           ^
          923|         else

       … while evaluating the attribute 'value'

         at /nix/store/5hwz775f3grzikafj1sbwx4lqkjwqswb-source/lib/modules.nix:807:9:

          806|     in warnDeprecation opt //
          807|       { value = builtins.addErrorContext "while evaluating the option `${showOption loc}':" value;
             |         ^
          808|         inherit (res.defsFinal') highestPrio;

       (stack trace truncated; use '--show-trace' to show the full trace)

       Failed assertions:
       - CONFIG_DEVTMPFS is not enabled!
       - CONFIG_CGROUPS is not enabled!
       - CONFIG_INOTIFY_USER is not enabled!
       - CONFIG_SIGNALFD is not enabled!
       - CONFIG_TIMERFD is not enabled!
       - CONFIG_EPOLL is not enabled!
       - CONFIG_NET is not enabled!
       - CONFIG_SYSFS is not enabled!
       - CONFIG_PROC_FS is not enabled!
       - CONFIG_FHANDLE is not enabled!
       - CONFIG_CRYPTO_USER_API_HASH is not enabled!
       - CONFIG_CRYPTO_HMAC is not enabled!
       - CONFIG_CRYPTO_SHA256 is not enabled!
       - CONFIG_DMIID is not enabled!
       - CONFIG_AUTOFS_FS is not enabled!
       - CONFIG_TMPFS_POSIX_ACL is not enabled!
       - CONFIG_TMPFS_XATTR is not enabled!
       - CONFIG_SECCOMP is not enabled!
       - CONFIG_TMPFS is not yes!
       - CONFIG_BLK_DEV_INITRD is not yes!
       - CONFIG_EFI_STUB is not yes!
       - CONFIG_BINFMT_ELF is not yes!
       - CONFIG_UNIX is not enabled!
       - CONFIG_INOTIFY_USER is not yes!
       - CONFIG_NET is not yes!

The error messages thrown are almost always unintelligible for me.

I have tried looking into the code for the kernel package here:, but it doesn’t help me in finding a way to change the variable.

That confuses me because the value should be eval’d by the system builder:

I can reproduce by trying to throw in the kernel attr and I found the reason:

This uses the extend mechanism of the package set (which is like an overlay) to override some things in the super.kernel (new nomenclature: prev). Problem is that super.kernel is the previous set’s kernel and this does not consider our “changes” using the set update operator //.

So what you’d have to do instead is this:

  boot.kernelPackages = pkgs.linuxPackages_6_6.extend (prev: final: {
    kernel = ...;

I can successfully get a throw to stop eval using this.

BTW, I know that the nix-community has a separate binary cache and that probably includes the surface kernel. Have you ever enabled that?

Unfortunately this first threw the error:
" error: attribute ‘override’ missing"

So I changed it to

  boot.kernelPackages = pkgs.linuxPackages_6_6.extend (prev: final: {
    kernel = final.kernel.override (originalArgs: {
      src = ...

But that does nothing (I assume kernel has no src argument?). Is there a way to list all arguments for a variable?

I will look at the nix-community binary cache. Thanks for all the help and your patience…

The apply function of the kernelPackages option tries to add configured patches etc. to the kernel via override. You’ll have to provide a stub .override function which simply returns your unchanged kernel:

  boot.kernelPackages = pkgs.linuxPackages_6_6.extend (prev: final: rec {
    kernel = runCommand ... // {
      override = _: kernel;

Unfortunately that just throws

       error: attribute 'config' missing

I added the cache with

  nix.settings = {
    substituters = [
   trusted-public-keys = [


and created a btrfs subvolume to play with. On this subvolume I purged the surface kernel from the store, but when I rebuilt the system it started compiling, so I don’t believe it’s cached.