---
title: "Infecting the Steam Link with NixOS"
slug: infecting-the-steam-link-with-nixos
url: https://listedarticles.com/articles/infecting-the-steam-link-with-nixos
canonical_url: https://feyor.sh/blog/infecting-the-steam-link-with-nixos/
content_type: blog_post
language: en
published_at: 2026-09-26T12:00:00.000Z
updated_at: 2026-09-26T15:09:24.470Z
author: "George Huebner"
author_url: https://feyor.sh/
authored_by: human
topics: ["Open Source", "Systems Programming", "Hardware", "Programming"]
license: all-rights-reserved
word_count: 2996
reading_minutes: 13
citation: "George Huebner. \"Infecting the Steam Link with NixOS.\" 26 Sept 2026. https://feyor.sh/blog/infecting-the-steam-link-with-nixos/ (all-rights-reserved)"
# The full text follows. The web page shows an extract and sends readers
# to the source above; quote the citation and link the canonical URL.
---

# Infecting the Steam Link with NixOS

> While rummaging through my closet the other day I discovered a [Steam Link](https://en.wikipedia.org/wiki/Steam_Link Hardware_device) I had bought on flash sale way back in 2018 still dutifully humming away all these years later. It occured to me that having an always on low power Arm device with Ethernet, WiFi, Bluetooth, and several USB ports would be handy, so thus began my journey to get NixOS running on the Steam Link.

While rummaging through my closet the other day I discovered a [Steam Link](https://en.wikipedia.org/wiki/Steam_Link#Hardware_device) I had bought on flash sale way back in 2018 still dutifully humming away all these years later.
It occured to me that having an always-on low power Arm device with Ethernet, WiFi, Bluetooth, and several USB ports would be handy, so thus began my journey to get NixOS running on the Steam Link.

As it turns out, a fellow named [fijam](https://heap.ovh/getting-linux-on-valve-steam-link.html) already figured out the hard parts involved in running a custom Linux distro on the Steam Link.
The most notable obstacle is that the bootloader will only boot kernels signed by Valve; to get around this, we can boot into the Valve-blessed kernel and then `kexec` our new kernel.
However, the kernel shipped with the Steam Link was not built with `CONFIG_KEXEC` enabled.
This is where things get *really* clever: we can cobble the pertinent kexec source files into a minimal kernel module that adds the `kexec` syscall to the running system!

Several people have successfully used this technique to get other distros<sup>[1](https://feyor.sh#fn:1)</sup> booting, but they all seem to have just copied the `kexec` binary and kernel module from fijam’s website.
fijam seems like a lovely person and all, but I’m wary of downloading kernel modules from the interwebs so I decided to compile it myself.

## Booting the thing

Compiling a NixOS userspace and kernel/initrd is pretty simple; you just pass the correct `system` (and because I’m using `__splicedPackages`/`crossSystem`, also `pkgs`) to `lib.nixosSystem` and add your modules.
Choosing the target architecture was slightly less straightforward: Valve’s steamlink toolchain uses `armv7a`, but importing nixpkgs with `crossSystem.config = “armv7a-unknown-linux-gnueabihf”` interacts [poorly with the Go build plumbing](https://github.com/NixOS/nixpkgs/blob/fbe840e7184ed15fd5b1ca8f1a8746462a38d59c/lib/systems/default.nix#L596-L599), so I used the (seemingly) equivalent `armv7l` instead.

## Nix

```
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  outputs = { self, nixpkgs }:
    let
      inherit (nixpkgs) lib;
      system = "armv7l-linux";
      # hostSystem should be linux but does not have to be arm (x86 should work)
      hostSystem = "aarch64-linux";
      pkgs = (import nixpkgs {
        system = hostSystem;
        crossSystem = {
          config = "armv7l-unknown-linux-gnueabihf";
        };
      }).__splicedPackages;
    in {
      nixosConfigurations.steamlink = lib.nixosSystem {
        inherit system pkgs;
        modules = [
          # ...
        ];
      };
    };
}
```
The real challenge is compiling a kernel module for a vendored fork of a 13 year old kernel; the [NixOS wiki](https://wiki.nixos.org/wiki/Linux_kernel#Packaging_out-of-tree_kernel_modules) is actually pretty helpful here and points out some footguns related to the default hardening flags in stdenv.

## Nix

```
kexecMod = let
  inherit (pkgs) stdenv;
  inherit (self.nixosConfigurations.steamlink.config.system.build) kernel oldKernel;
in stdenv.mkDerivation {
  pname = "kexec_mod";
  version = "0.0.1";
  src = ./kexec_mod;
  postPatch = ''
    for f in machine_kexec.c kexec.c relocate_kernel.S; do
      substituteInPlace "$f" --subst-var-by KERNEL ${oldKernel}
    done
  '';
  nativeBuildInputs = kernel.moduleBuildDependencies;
  makeFlags = [
    "ARCH=${stdenv.hostPlatform.linuxArch}"
    "CROSS_COMPILE=${stdenv.cc.targetPrefix}"
    "KDIR=${oldKernel}"
    "INSTALL_MOD_PATH=$(out)"
  ];
  env.NIX_CFLAGS_COMPILE = toString [
    "-std=gnu89"
    "-fno-pie"
  ];
  inherit (kernel) hardeningDisable;
  meta = {
    description = "kexec functionality as a kernel module for old kernels";
    homepage = "https://github.com/lukas2511/steamlink-sdk";
    license = lib.licenses.gpl2;
    platforms = [ system ];
  };
};
```
(See [Files](https://feyor.sh/blog/infecting-the-steam-link-with-nixos/#files) for the kexec_mod source code.)

In order to get this to build we need to point Kbuild to a Linux kernel checkout that has been built with `make modules`<sup>[2](https://feyor.sh#fn:2)</sup>.
(Note that I’m using the `moduleBuildDependencies` attribute of the comparatively modern `kernel` from my NixOS configuration.)

## Nix

```
oldKernel = let
  inherit (pkgs) stdenv fetchFromGitHub buildPackages fetchpatch writeText;
  inherit (self.nixosConfigurations.steamlink.config.system.build) kernel;
in stdenv.mkDerivation {
  pname = "linux-steamlink";
  version = "3.8.13";
  src = fetchFromGitHub {
    owner = "ValveSoftware";
    repo = "steamlink-sdk";
    rootDir = "kernel";
    rev = "62b4d098d1472c3534dd098ca2a0e0e10712f1c6";
    hash = "sha256-3Q8JjNmkRFCkdt8E+ol+E/c2sy+X5I7UYhsHAfYBdWs=";
  };
  sourceRoot = "source";
  patches = [
    (fetchpatch {
      url = "https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/linux3.4-ARM-8933-1-replace-Sun-Solaris-style-flag-on-section.patch";
      hash = "sha256-KRNI4070H0AFMCZl7pYnIbin6lbp68/xuf6yOPvmYdI=";
    })
    (fetchpatch {
      url = "https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch";
      hash = "sha256-9hq5xGeJRL/ESHofOh4MAOAGV2IlDRYvvpxyxk3MXlw=";
    })
    (writeText "0001-gcc-bug85745.diff"
      ''
        diff --git a/arch/arm/include/asm/uaccess.h b/arch/arm/include/asm/uaccess.h
        index 74b17d0..dc64fa2 100644
        --- a/arch/arm/include/asm/uaccess.h
        +++ b/arch/arm/include/asm/uaccess.h
        @@ -164,7 +164,7 @@
         #define __put_user_check(x,p)''\t''\t''\t''\t''\t''\t''\t${"\\"}
         ''\t({''\t''\t''\t''\t''\t''\t''\t''\t${"\\"}
         ''\t''\tunsigned long __limit = current_thread_info()->addr_limit - 1; ${"\\"}
        -''\t''\tregister const typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"}
        +''\t''\tregister typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"}
         ''\t''\tregister const typeof(*(p)) __user *__p asm("r0") = (p);${"\\"}
         ''\t''\tregister unsigned long __l asm("r1") = __limit;''\t''\t${"\\"}
         ''\t''\tregister int __e asm("r0");''\t''\t''\t''\t${"\\"}
      '')
  ];
  postPatch = ''
    substituteInPlace arch/arm/boot/compressed/piggy.xzkern.S --replace-fail '#alloc' ' "a"'
    substituteInPlace arch/arm/mach-berlin/Makefile.boot --replace-fail '/bin/bash' '${stdenv.shell}'
    substituteInPlace arch/arm/boot/compressed/Makefile --replace-fail '${"\t"}@$(check_for_multiple_zreladdr)' '${"\t"}echo LDFLAGS_vmlinux = ''${LDFLAGS_vmlinux}${"\n\t"}@$(check_for_multiple_zreladdr)'
    cp include/linux/compiler-gcc4.h include/linux/compiler-gcc${lib.versions.major buildPackages.stdenv.cc.version}.h
  '';
  inherit (kernel) nativeBuildInputs;
  depsBuildBuild = [
    buildPackages.stdenv.cc
  ];
  makeFlags = [
    "ARCH=${stdenv.hostPlatform.linuxArch}"
    "LOCALVERSION=-mrvl"
    "CROSS_COMPILE=${stdenv.cc.targetPrefix}"
  ];
  env.NIX_CFLAGS_COMPILE = toString [
    "-std=gnu89"
    "-Wno-error=address"
    "-Wno-error=dangling-pointer"
    "-Wno-error=missing-attributes"
  ];
  inherit (kernel) hardeningDisable;
  configurePhase = ''
    make bg2cd_penguin_mlc_defconfig $makeFlags
    echo "CONFIG_KEXEC=y" >> .config
    echo "CONFIG_KERNEL_XZ=y" >> .config
    make olddefconfig $makeFlags
  '';
  postBuild = ''
    make modules $makeFlags -j$NIX_BUILD_CORES
  '';
  installPhase = ''
    mkdir $out
    cp -r * $out/
  '';
  dontFixup = true;
};
```
It took several hacks to get things building on a modern version of GCC, but eventually I was able to get the 3.8.13-mrvl kernel and the kexec_mod kernel module building.

Now that we have `kexec_mod.ko`, we need our new initrd and kernel (which all come from our NixOS config), the device tree blob for the Steam Link (which has been [upstreamed](https://github.com/torvalds/linux/blob/6812ce4e4379ffc99c52401ec28f0d7ffbc36206/arch/arm/boot/dts/synaptics/berlin2cd-valve-steamlink.dts) to Linux so we can get it from `hardware.deviceTree.package`), a copy of the `kexec` userland binary (compiled with `pkgsStatic` so we can run it on non-NixOS), and a small script to tie everything together:

## Bash

```
fts-set steamlink.crashcounter 0 # required to prevent factory reset after a few reboots
mkdir -p /mnt/disk/proc /mnt/disk/sys /mnt/disk/dev
mount -t proc proc /mnt/disk/proc
mount -o rbind /sys /mnt/disk/sys
mount -o rbind /dev /mnt/disk/dev
insmod /mnt/disk/kexec_load.ko
chroot /mnt/disk/ /kexec --load /zImage \
                         --initrd /initrd \
                         --dtb /berlin2cd-valve-steamlink.dtb \
                         --command-line "init=/init root=/dev/sda2 rootwait rw usbcore.autosuspend=-1"
chroot /mnt/disk/ /kexec -e
```
You could juggle these files manually and upload them to a USB drive yourself, but it’s much easier to use the [sd-image](https://github.com/NixOS/nixpkgs/blob/5f5458dc42bf4391dc5f85e7682decb8c78e8756/nixos/modules/installer/sd-card/sd-image.nix) NixOS module to create a disk image instead:

## Nix

```
usb-image = { modulesPath, config, ... }: {
  imports = [
    (modulesPath + "/installer/sd-card/sd-image.nix")
  ];
  image.extension = lib.mkForce "img";
  sdImage = let
    dtb = "berlin2cd-valve-steamlink.dtb";
    kexecScript = ./kexec-nixos;
    inherit (config.system.build) kernel initialRamdisk;
  in {
    compressImage = false;
    firmwarePartitionName = "STEAMLINK";
    rootVolumeLabel = "NIXOS";
    populateFirmwareCommands = ''
      pushd firmware
      files=(
        ${kernel}/${config.system.boot.loader.kernelFile}
        ${initialRamdisk}/${config.system.boot.loader.initrdFile}
        ${config.hardware.deviceTree.package}/${dtb}
        ${self.packages.${system}.kexecMod}/lib/modules/3.8.13-mrvl/extra/kexec_load.ko
        ${pkgs.pkgsStatic.kexec-tools}/bin/kexec
      )
      for f in ''${files[@]}; do
        cp $f ./
      done
      # factory_test/run.sh will run before this has a chance to
      # enable ssh; uncomment if not booting straight into NixOS
      # mkdir -p steamlink/config/system
      # touch steamlink/config/system/enable_ssh.txt
      mkdir -p steamlink/factory_test
      cp ${kexecScript} steamlink/factory_test/run.sh
      popd
    '';
    populateRootCommands = "";
  };
};
```
Testing that the `kexec` handoff works was really tricky because the HDMI output doesn’t work with the kernel I’m using, and since I decided not to open up the device to get at the UART I was flying completely blind.
I decided to test with a minimal (slop) Busybox-based initramfs that rebooted after a variable amount of time to indicate success.

## Nix

```
initramfs = pkgs.buildPackages.runCommand "build-initramfs" {}
  ''
    mkdir initramfs; cd initramfs
    mkdir -pv {etc,proc,sys,usr/{bin,sbin}}
    cp -a ${pkgs.pkgsStatic.busybox}/{bin,sbin} .
    chmod 755 ./{bin,sbin}
    cat <<EOF > init
    #!/bin/sh
    mount -t proc none /proc
    mount -t sysfs none /sys
    mount -t devtmpfs devtmpfs /dev
    mkdir -p /mnt
    try_mount() {
      dev="$1"
      fs="$2"
      if [ "$fs" = auto ]; then
        mount -o rw "$dev" /mnt 2>/dev/null || return 1
      else
        mount -t "$fs" -o rw "$dev" /mnt 2>/dev/null || return 1
      fi
      marker=kexec-mounted-ok
      if [ -f /mnt/zImage ]; then
        marker=kexec-steamlink-ok
      fi
      {
        echo "device=$dev"
        echo "fs=$fs"
        cat /proc/partitions
      } > "/mnt/$marker" 2>/dev/null && sync
      umount /mnt
      sleep 10
      reboot -f
    }
    for dev in /dev/mmcblk*p* /dev/sd[a-z][0-9]* /dev/vd[a-z][0-9]*; do
      [ -b "$dev" ] || continue
      try_mount "$dev" vfat
      try_mount "$dev" ext4
      try_mount "$dev" auto
    done
    sleep 45
    reboot -f
    EOF
    chmod +x init
    find . -print0 | ${lib.getExe pkgs.buildPackages.cpio} --null -ov --format=newc > $out
  '';
```
Once I knew that worked, I switched to the NixOS initrd with `boot.initrd.network.enable = true` and used a Netcat based reverse shell to my laptop’s IP for further debugging.

## Nix

```
debugModule = { lib, ... }: {
  boot.initrd.systemd.enable = lib.mkForce false;
  boot.initrd.kernelModules = [ "pxa168_eth" ];
  boot.initrd.availableKernelModules = [ "reset_berlin" ];
  boot.initrd.network.enable = true;
  boot.initrd.network.udhcpc.enable = false;
  boot.kernelParams = [
    "ip=192.168.2.2::192.168.2.1:255.255.255.0:stm-link:eth0:off"
  ];
  boot.initrd.network.postCommands = ''
    mac_peer=192.168.2.1
    stm_link_ip=192.168.2.2
    echo "initrd net debug: interfaces: $(ls /sys/class/net)" > /dev/kmsg
    for iface_path in /sys/class/net/*; do
      iface="''${iface_path##*/}"
      [ "$iface" != lo ] || continue
      echo "initrd net debug: configuring $iface" > /dev/kmsg
      ip link set dev "$iface" up || true
      ip address flush dev "$iface" || true
      ip address add "$stm_link_ip/24" dev "$iface" || true
    done
    (
      while true; do
        ping -c 1 -W 1 "$mac_peer"
        sleep 2
      done
    ) &
    (
      while true; do
        rm -f /tmp/revsh
        mkfifo /tmp/revsh
        /bin/ash -i < /tmp/revsh 2>&1 | nc "$mac_peer" 4444 > /tmp/revsh
        rm -f /tmp/revsh
        sleep 2
      done
    ) &
  '';
};
```
The main things I needed to figure out at this stage were adding `reset_berlin` to `boot.initrd.availableKernelModules` to allow reading from the USB drive and using the old NixOS initrd system in lieu of the new systemd-based version (`boot.initrd.systemd.enable = lib.mkForce false`).

Finally I was able to boot into userspace and connect over SSH! 🥳

That having been said, the USB image I was booting from was weighing in at a hefty 2.3GB… surely we can do better.

## Trimming the fat

I was surprised that there wasn’t a definitive guide for reducing NixOS closure sizes; I found some NixOS Discourse questions and a few blog posts, but the most useful writeups were [NixOS is a good server OS, except when it isn’t](https://sidhion.com/blog/nixos_server_issues) and [I can haz smoller NixOS ISOs?](https://natkr.com/2026-06-19-nixos-but-smol/).
Those are good resources, but because we’re targeting actual hardware instead of a VM we must necessarily be more conservative in what we cut.

## Nix

```
minimal = { modulesPath, pkgs, ... }: {
  imports = [
    (modulesPath + "/profiles/minimal.nix")
    (modulesPath + "/profiles/headless.nix")
    # (modulesPath + "/profiles/perlless.nix")
  ];
  disabledModules = [
    (modulesPath + "/profiles/base.nix")
  ];
  boot.loader = {
    grub.enable = false;
    systemd-boot.enable = false;
    supportsInitrdSecrets = false;
  };
  boot.initrd.systemd.enable = lib.mkForce false;
  boot.initrd.availableKernelModules = lib.mkForce [
    "reset_berlin"
    "uas"
  ];
  boot.kernelModules = [
    "pxa168_eth"
    "mwifiex_sdio"
    "btmrvl_sdio"
  ];
  hardware.firmware = lib.mkForce (with pkgs; [
    (runCommand "marvell-firmware" {} ''
      mkdir -p $out/lib/firmware/mrvl
      cp ${linux-firmware}/lib/firmware/mrvl/sd8897_uapsta.bin $out/lib/firmware/mrvl/
    '')
    wireless-regdb
  ]);
  documentation.enable = false;
  programs.command-not-found.enable = lib.mkDefault false;
  networking.networkmanager.enable = false;
  networking.firewall.enable = false;
  xdg.icons.enable  = false;
  xdg.mime.enable   = false;
  xdg.sounds.enable = false;
  fonts.fontconfig.enable = false;
  programs.nano.enable = false;
  system.disableInstallerTools = true;
  system.switch.enable = false;
  system.nixos-init.enable = false;
  nix.enable = false;
  systemd.services.register-nix-paths = lib.mkForce {};
};
```
Here are the main things that came up during the slim-ening:

- We only need one firmware blob from the massive `linux-firmware` package (1.8GB compressed!), so we can save a huge amount of space by only adding that blob to`hardware.firmware`  - On a similar note it should be possible to use a kconfig tailored for the Steam Link hardware to build a smaller kernel, but this seemed like more trouble than it was worth
- OpenSSH does not seem to like it when `i18n.glibcLocales` or the`security.wrappers` module are removed
- The `kexec` boot flow renders most of the NixOS system administration utilities irrelevant; as a matter of fact, Nix itself is not very useful on such a system, so we can save space by getting rid of that too
- I feel like it should be possible to disable `boot.initrd` and`boot.kernel` because we provide the kernel/initrd/DTB from the FAT32 partition when we`kexec` , but I was never able to disable these without breaking the boot process
- If I could switch to the systemd-based initrd I could enable the “perlless” NixOS module to remove Perl from the system closure for additional savings

After reaching a point of diminshing returns and most new changes breaking my system, I declared the 1.2GB disk image I had to be “good enough”.

## Files

The Nix flake I used and the source for the `kexec_mod` kernel module can be downloaded [here](https://feyor.sh/infecting-the-steam-link-with-nixos/steamlink-nixos.tar.gz).
The same `flake.nix` is reproduced below for your convenience.

## 
      Nix
      flake.nix
    
  
  

  ```
{
  inputs.nixpkgs.url = "github:NixOS/nixpkgs/nixos-unstable";
  outputs = { self, nixpkgs }:
    let
      inherit (nixpkgs) lib;
      system = "armv7l-linux";
      # hostSystem should be linux but does not have to be arm (x86 should work)
      hostSystem = "aarch64-linux";
      pkgs = (import nixpkgs {
        system = hostSystem;
        crossSystem = {
          config = "armv7l-unknown-linux-gnueabihf";
        };
        overlays = [
          # https://github.com/NixOS/nixpkgs/issues/388309
          (self: super: {
            efivar = self.emptyDirectory;
            efibootmgr = self.emptyDirectory;
          })
        ];
      }).__splicedPackages;
    in {
      nixosModules = {
        # https://sidhion.com/blog/nixos_server_issues
        # https://discourse.nixos.org/t/how-to-have-a-minimal-nixos/22652/4
        minimal = { modulesPath, pkgs, ... }: {
          imports = [
            (modulesPath + "/profiles/minimal.nix")
            (modulesPath + "/profiles/headless.nix")
          ];
          disabledModules = [
            (modulesPath + "/profiles/base.nix")
          ];
          boot.loader = {
            grub.enable = false;
            systemd-boot.enable = false;
            supportsInitrdSecrets = false;
          };
          # systemd initrd did not work for me; you could add the perlless profile if you got it working
          boot.initrd.systemd.enable = lib.mkForce false;
          boot.initrd.availableKernelModules = lib.mkForce [
            "reset_berlin"
            "uas"
          ];
          boot.kernelModules = [
            "pxa168_eth"
            "mwifiex_sdio"
            "btmrvl_sdio"
          ];
          hardware.firmware = lib.mkForce (with pkgs; [
            (runCommand "marvell-firmware" {} ''
              mkdir -p $out/lib/firmware/mrvl
              cp ${linux-firmware}/lib/firmware/mrvl/sd8897_uapsta.bin $out/lib/firmware/mrvl/
            '')
            wireless-regdb
          ]);
          documentation.enable = false;
          programs.command-not-found.enable = lib.mkDefault false;
          networking.networkmanager.enable = false;
          networking.firewall.enable = false;
          xdg.icons.enable  = false;
          xdg.mime.enable   = false;
          xdg.sounds.enable = false;
          fonts.fontconfig.enable = false;
          programs.nano.enable = false;
          system.disableInstallerTools = true;
          system.switch.enable = false;
          system.nixos-init.enable = false;
          nix.enable = false;
          systemd.services.register-nix-paths = lib.mkForce {};
        };
        usb-image = { modulesPath, config, ... }: {
          imports = [
            (modulesPath + "/installer/sd-card/sd-image.nix")
          ];
          # in theory it should be possible to disable the kernel
          # and initrd for the nixos rootfs for some significant
          # space savings (because we're passing a copy of the
          # kernel and initrd from the STEAMLINK partition to kexec
          # directly, but in practice I never got that working)
          boot.kernelParams = [
            "root=/dev/disk/by-label/${config.sdImage.rootVolumeLabel}" "rootwait" "rw"
            "usbcore.autosuspend=-1"
          ];
          image.extension = lib.mkForce "img";
          sdImage = let
            dtb = "berlin2cd-valve-steamlink.dtb";
            kexecScript = pkgs.buildPackages.writeScript "kexec-nixos" ''
              #!/bin/sh
              fts-set steamlink.crashcounter 0
              mkdir -p /mnt/disk/proc /mnt/disk/sys /mnt/disk/dev
              mount -t proc proc /mnt/disk/proc
              mount -o rbind /sys /mnt/disk/sys
              mount -o rbind /dev /mnt/disk/dev
              insmod /mnt/disk/kexec_load.ko
              chroot /mnt/disk/ /kexec --load /zImage \
                                       --initrd /initrd \
                                       --dtb /berlin2cd-valve-steamlink.dtb \
                                       --command-line "init=${config.system.build.toplevel}/init ${toString config.boot.kernelParams}"
              chroot /mnt/disk/ /kexec -e
            '';
            inherit (self.packages.${system}) kernel initialRamdisk;
          in {
            compressImage = false;
            firmwarePartitionName = "STEAMLINK";
            rootVolumeLabel = "NIXOS";
            populateFirmwareCommands = ''
              pushd firmware
              files=(
                ${kernel}/${config.system.boot.loader.kernelFile}
                ${initialRamdisk}/${config.system.boot.loader.initrdFile}
                ${config.hardware.deviceTree.package}/${dtb}
                ${self.packages.${system}.kexecMod}/lib/modules/3.8.13-mrvl/extra/kexec_load.ko
                ${pkgs.pkgsStatic.kexec-tools}/bin/kexec
              )
              for f in ''${files[@]}; do
                cp $f ./
              done
              # factory_test/run.sh will run before this has a chance to
              # enable ssh; uncomment if not booting straight into NixOS
              # mkdir -p steamlink/config/system
              # touch steamlink/config/system/enable_ssh.txt
              mkdir -p steamlink/factory_test
              cp ${kexecScript} steamlink/factory_test/run.sh
              popd
            '';
            populateRootCommands = "";
          };
        };
      };
      nixosConfigurations.steamlink = lib.nixosSystem {
        inherit system pkgs;
        modules = [
          self.nixosModules.minimal
          self.nixosModules.usb-image
          ({ ... }: {
            # your NixOS config here!
            services.tailscale.enable = true;
            services.openssh = {
              # might be able to remove security wrappers if using static openssh
              # see https://sidhion.com/blog/nixos_server_issues#:~:text=While%20looking%20through%20the%20lvm%20stuff
              # package = pkgs.pkgsStatic.openssh;
              enable = true;
              settings = {
                PermitRootLogin = "yes";
              };
            };
            users.users.root.openssh.authorizedKeys.keys = [ "..." ];
            hardware.bluetooth.enable = true;
            networking = {
              hostName = "steamlink";
              useDHCP = true;
              interfaces.eth0 = {
                useDHCP = true;
                # prefer DHCP but use a static IP for debugging over a direct ethernet serial line to your host machine
                ipv4.addresses = [{
                  address = "169.254.31.216";
                  prefixLength = 16;
                }];
              };
              wireless = {
                enable = true;
                networks  = {
                  "WiFi" = {
                    psk = "hunter2";
                  };
                };
              };
            };
          })
        ];
      };
      packages.${system} = {
        inherit (self.nixosConfigurations.steamlink.config.system.build) kernel initialRamdisk sdImage;
        default = self.packages.${system}.sdImage;
        oldKernel = let
          inherit (pkgs) stdenv fetchFromGitHub buildPackages fetchpatch writeText;
          inherit (self.packages.${system}) kernel;
        in stdenv.mkDerivation {
          pname = "linux-steamlink";
          version = "3.8.13";
          src = fetchFromGitHub {
            owner = "ValveSoftware";
            repo = "steamlink-sdk";
            rootDir = "kernel";
            rev = "62b4d098d1472c3534dd098ca2a0e0e10712f1c6";
            hash = "sha256-3Q8JjNmkRFCkdt8E+ol+E/c2sy+X5I7UYhsHAfYBdWs=";
          };
          sourceRoot = "source";
          patches = [
            (fetchpatch {
              url = "https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/linux3.4-ARM-8933-1-replace-Sun-Solaris-style-flag-on-section.patch";
              hash = "sha256-KRNI4070H0AFMCZl7pYnIbin6lbp68/xuf6yOPvmYdI=";
            })
            (fetchpatch {
              url = "https://gitlab.com/postmarketOS/pmaports/-/raw/aa289aa350071e6afc54f6b6704ba28971b50466/device/.shared-patches/linux/gcc10-extern_YYLOC_global_declaration.patch";
              hash = "sha256-9hq5xGeJRL/ESHofOh4MAOAGV2IlDRYvvpxyxk3MXlw=";
            })
            (writeText "0001-gcc-bug85745.diff"
              ''
                diff --git a/arch/arm/include/asm/uaccess.h b/arch/arm/include/asm/uaccess.h
                index 74b17d0..dc64fa2 100644
                --- a/arch/arm/include/asm/uaccess.h
                +++ b/arch/arm/include/asm/uaccess.h
                @@ -164,7 +164,7 @@
                 #define __put_user_check(x,p)''\t''\t''\t''\t''\t''\t''\t${"\\"}
                 ''\t({''\t''\t''\t''\t''\t''\t''\t''\t${"\\"}
                 ''\t''\tunsigned long __limit = current_thread_info()->addr_limit - 1; ${"\\"}
                -''\t''\tregister const typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"}
                +''\t''\tregister typeof(*(p)) __r2 asm("r2") = (x);''\t${"\\"}
                 ''\t''\tregister const typeof(*(p)) __user *__p asm("r0") = (p);${"\\"}
                 ''\t''\tregister unsigned long __l asm("r1") = __limit;''\t''\t${"\\"}
                 ''\t''\tregister int __e asm("r0");''\t''\t''\t''\t${"\\"}
              '')
          ];
          postPatch = ''
            substituteInPlace arch/arm/boot/compressed/piggy.xzkern.S --replace-fail '#alloc' ' "a"'
            substituteInPlace arch/arm/mach-berlin/Makefile.boot --replace-fail '/bin/bash' '${stdenv.shell}'
            substituteInPlace arch/arm/boot/compressed/Makefile --replace-fail '${"\t"}@$(check_for_multiple_zreladdr)' '${"\t"}echo LDFLAGS_vmlinux = ''${LDFLAGS_vmlinux}${"\n\t"}@$(check_for_multiple_zreladdr)'
            cp include/linux/compiler-gcc4.h include/linux/compiler-gcc${lib.versions.major buildPackages.stdenv.cc.version}.h
          '';
          inherit (kernel) nativeBuildInputs;
          depsBuildBuild = [
            buildPackages.stdenv.cc
          ];
          makeFlags = [
            "ARCH=${stdenv.hostPlatform.linuxArch}"
            "LOCALVERSION=-mrvl"
            "CROSS_COMPILE=${stdenv.cc.targetPrefix}"
          ];
          env.NIX_CFLAGS_COMPILE = toString [
            "-std=gnu89"
            "-Wno-error=address"
            "-Wno-error=dangling-pointer"
            "-Wno-error=missing-attributes"
          ];
          inherit (kernel) hardeningDisable;
          configurePhase = ''
            make bg2cd_penguin_mlc_defconfig $makeFlags
            echo "CONFIG_KEXEC=y" >> .config
            echo "CONFIG_KERNEL_XZ=y" >> .config
            make olddefconfig $makeFlags
          '';
          postBuild = ''
            make modules $makeFlags -j$NIX_BUILD_CORES
          '';
          installPhase = ''
            mkdir $out
            cp -r * $out/
          '';
          dontFixup = true;
        };
        kexecMod = let
          inherit (pkgs) stdenv;
          inherit (self.packages.${system}) kernel oldKernel;
        in stdenv.mkDerivation {
          pname = "kexec_mod";
          version = "0.0.1";
          src = ./kexec_mod;
          postPatch = ''
            for f in machine_kexec.c kexec.c relocate_kernel.S; do
              substituteInPlace "$f" --subst-var-by KERNEL ${oldKernel}
            done
          '';
          nativeBuildInputs = kernel.moduleBuildDependencies;
          makeFlags = [
            "ARCH=${stdenv.hostPlatform.linuxArch}"
            "CROSS_COMPILE=${stdenv.cc.targetPrefix}"
            "KDIR=${oldKernel}"
            "INSTALL_MOD_PATH=$(out)"
          ];
          env.NIX_CFLAGS_COMPILE = toString [
            "-std=gnu89"
            "-fno-pie"
          ];
          inherit (kernel) hardeningDisable;
          meta = {
            description = "kexec functionality as a kernel module for old kernels";
            homepage = "https://github.com/lukas2511/steamlink-sdk";
            license = lib.licenses.gpl2;
            platforms = [ system ];
          };
        };
      };
    };
}
```
1. 
Right before I published this I discovered [someone else](https://github.com/Ondra-Zik/steamlink-nixos) had vibed their way to a bootable NixOS install, although their config is more _slop_py, doesn’t handle reboots correctly, and uses a bunch of unnecessary binary blobs instead of building from source.[↩︎](https://feyor.sh#fnref:1)
2. 
Although using `make modules_prepare` lets us build successfully, the resulting kernel module will not have the right vermagic and symbol addresses and will not be accepted by`insmod` :NOTE: “modules_prepare” will not build Module.symvers even if `CONFIG_MODVERSIONS` is set; therefore, a full kernel build needs to be executed to make module versioning work.( [source](https://www.kernel.org/doc/html/latest/kbuild/modules.html%20) )[↩︎](https://feyor.sh#fnref:2)
