When I first started using Linux in 2006 I remember dreaming of a Linux Console. The idea maybe wasn’t so far fetched at the time, the PlayStation 3 had just been released with OtherOS support which allowed users to install Linux (or BSD). Still, it seemed that a Linux-first console would only ever be a dream. Now in 2022, Valve’s Steam Deck is a hackable Linux-first portable console.
Today, we’ll be putting Nix on it, because what’s Linux without Nix?
Just wanna try it? Jump to the fun part. Want NixOS instead?
A different, spicier kind of fun can be found here.
The Steam Deck is a portable computer that has a Nintendo Switch-like form factor and touts an AMD x86_64 processor. It has WiFi, Bluetooth, and a USB-C port which you can plug a hub into, allowing the attachment of HDMI, mice, keyboards, power, or ethernet cables.
It runs a flavour of Arch Linux called SteamOS, and guides users to use
Flatpak and Flathub. This
is a fantastic solution and provides users access to a wide variety of
software, but as a developer I tend to want more exotic stuff that exists in
nixpkgs
.
In case you’d not seen one yet, here’s a picture of mine:
My Deck, alongside the peripherals I use with it: PS5 Controller, a USB-C hub, and an Ergodox.
Installing Nix on the Steam Deck has a few special steps. Let’s review how a Nix install process looks, then how the Deck works, and finally we can explore a working approach to install Nix.
How a Nix install works
A normal Nix install process on Linux works roughly like this:
- Create a folder called
/nix
- Unpack the Nix distribution tarball into
/nix
- Create some Nix daemon users and a group (affecting
/etc
) - Call
systemctl link
on some systemd units from/nix
(affecting/etc
) - Sprinkle some magic in the detected shell profiles (in
/etc
) to ensurenix
is on$PATH
On Mac, where creating a /nix
is forbidden (by the creators, Apple), we can modify /etc/synthetic.conf
to create a stub which we can mount an APFS volume to. The installation otherwise proceeds as normal.
On the Steam Deck, creating /nix
also requires special steps. Unfortunately, there is no feature similar to /etc/synthetic.conf
.
Why does the Deck need these special steps? Let’s take a look at the Steam Deck itself and figure out why we can’t just run the familiar Nix installer.
The Deck & SteamOS
The Steam Deck ships with an Arch Linux based distribution called SteamOS — a special image of it to be even more precise. Normally Arch Linux is a perfectly fine target for Nix, but there are a couple particularities around this distribution that impact how we can install Nix.
Disk Topology
The Deck uses an A/B boot system (like some Android phones), which means it has two parallel installations, booting into one and updating the other. This means, if it ever fails after an update it can safely roll back to a known good state.
NixOS user? Sound familiar? It’s like the generation selector in your bootloader, but instead of pointing your boot to different Nix store paths, it points to entirely different partitions!
See how there is A
and B
copies of most partitions?
This looks a heck of a lot different than my development machine:
Checking for encryption with blkid | grep crypto_LUKS
showed all partitions were unencrypted, this makes sense since the Deck never asks for a password, even for sudo
, until you set one. It’s a bit unfortunate Valve did not opt to protect their user’s data in the event this portable device was stolen, but it’s room to improve.
This A/B boot system means even if rootfs
partitions get modified, those changes may get wiped out at any time. The system may update or choose to boot into the other ‘letter’ for some other reason. We want something that is update-proof and will survive a change of ‘letter’.
One partition that persists across reboots and has enough space to contain a thick, chunky Nix store is the home
partition. Our Nix install can keep persistent data there.
Read-Only Filesystem
Reviewing the mount
output is a bit misleading. While the /
mount says it is rw
, it is normally not.
This isn’t a scary vendor lockdown security feature or anything, it’s mostly to prevent the user from being surprised when the A/B boot happens. SteamOS comes with a steamos-readonly
executable we can use to toggle this read-only feature at any time, this can allow us to make small changes to the root filesystem as long as we don’t expect them to persist across boots.
Because of this, if we wanted, we could create a /nix
path on the rootfs
each boot by making the root momentarily writable.
Not all of the device is read-only though! We can write to places like /etc/
, but not to /lib
, /usr
, or /bin
.
Recalling the rough steps from the install process, this isn’t a problem! So long as we work out the machinery to ensure /nix
is available, the Steam Deck looks otherwise like a normal system to Nix.
Enabling an Install
As we discovered, creating the /nix
directory in a safe way that persists will be our primary challenge.
Since it wouldn’t be a great idea to store the Nix Store on the rootfs
partitions, we must decide somewhere else. The most immediately obvious answer is /home/nix
, since that is a large, persistent location.
With an existing /home/nix
, we can use a bind mount to mount that to /nix
. First, a /nix
path needs be created somehow!
Luckily, with /etc
writable, we can drop systemd units into /etc/systemd/system
that will set up /nix
for us.
We’ll create a nix-directory.service
unit which creates the /nix
path, and a nix.mount
unit which depends on that.
Sadly, that’s not quite enough to enable a full install though. Since the Nix install process involves systemctl link $UNIT
, some of the systemd units are not available during systemd’s startup. Therefore we must reload the systemd daemon itself after the nix.mount
unit is started. In order to do that, we follow the same method as Flatcar Linux does here.
Let’s cover what these units look like then test them out with the Nix installer! If you’re feeling brave I invite you to help us test an experimental Nix installer we’ve been working on which has a special codepath just for the Steam Deck. Otherwise, follow along below to try the traditional install script.
But first, just in case:
- Not sure how to get to ‘Desktop mode’? Hit the Steam button, go to ‘Power’, go to ‘Switch to Desktop’
- Not sure how to get a terminal? In ‘Desktop Mode’ hit the logo in the bottom left corner, in the search bar type “Terminal”, select ‘Konsole’
- Not sure how to edit files? You can use
vim
if you are familiar, otherwise trynano
from the terminal.
Putting it all together
Want to follow along without a Deck? Learn how to set up a Deck VM with this article.
There are only four Steam Deck specific steps, three are to create the systemd units. The final one is to enable one of those units.
Create the systemd units at the noted paths, I suggest using a keyboard plugged into the Deck if you can, or enable SSH via sudo systemctl start sshd
, reviewing the IP address via ip a
, and setting a password. If those options are unavailable, hit the Steam and X buttons to summon the keyboard.
The above unit is the first in our chain of units, it checks if a /nix
folder exists, and if necessary, calls steamos-readonly disable
, creates /nix
, then calls steamos-readonly enable
again. It also attempts to do some cleanup as it stops, but that part is unnecessary.
This mount unit performs a bind mount from /home/nix
to /nix
. It’ll create /home/nix
for us, but sadly it cannot create /nix
, relying on the nix-directory.service
before it.
This final unit in the chain restarts the systemd daemon, allowing it to properly resolve any previously broken symlinks during the boot, before starting or enabling them if necessary.
Tailscale user? A similar strategy can be used after performing
systemd-sysext merge
if you happen to also use Tailscale on your Steam Deck to make sure it starts at boot.
After creating the units, we need to enable (and start) the last, causing the ones it requires to also start:
Now we can just run the Nix installer like normal:
Follow the prompts, call exec $SHELL
(or open a new shell, or reboot) and Nix should work on command line!
Feel free to reboot a few times, or even update your Steam Deck. As far as I’ve experimented, it should keep working!
If you cause a instant, hard, full power loss (such as Ctrl+C’ing the VM) before it can properly fsync()
, you may see an error like error: expected string 'Derive(['
. To resolve this error, run nix store gc
. You can avoid this by running sync
before killing the device.
Find out more about building Linux systems using Nix
An invitation to experiment
Part of the reason we wanted to explore Nix on the Steam Deck is that we’re currently experimenting with a new Nix installer, and we were curious what we could learn from adding support for a specific device which had special requirements, such as the Steam Deck.
If you feel like experimenting (and don’t mind things breaking) feel encouraged to try out our prototype! Don’t worry, if you don’t like it, it includes an uninstaller so you can roll back and do your install with the traditional scripts.
You can run it like this:
If you don’t feel like being experimental, this is what it looks like:
Hate it? Uninstall it:
Our prototype has the working name of nix-installer
. It supports different installation ‘planners’ (such as the steam-deck
), can be used as a Rust library, has fine grained logging, and can uninstall a Nix it installed.
It has no runtime dependencies (though it will try to sudo
itself if you forget) or build time dependencies (other than Rust/C compilers) and should build trivially inside or outside Nix for x86_64 and aarch64, Linux (glibc
or musl
based) and Mac.
We are currently distributing fully reproducible and hermetic nix
based experimental builds for all supported platforms. The installer is Open Source (LGPL) and written in entirely in Rust. (Nix is still not in Rust — sorry!)
You are welcome to explore the code here. Don’t worry, we’re very excited to talk about it at length in a future article. Stay tuned for more!
We’ve been working with other installer working group contributors like (alphabetical) Cole, Michael, Solène, Théophane, Travis, and others to build
nix-installer
and better understand what a next-generation Nix installer would look like, thank you so much for all your help, hard work, and advice.
Conclusion
We explored how the Steam Deck takes certain measures to protect users from accidentally losing changes when the system updates and swaps due to its A/B booting, we also explored how we can use persistent systemd units to create a /nix
path on the Steam Deck which bind mounts to a persistent /home/nix
directory. In order to ensure that the units linked from the /nix
path are loaded, we also learnt we can have a unit which reloads the systemd daemon, and how this resolves the issue.
Using these techniques, we successfully installed Nix on the Steam Deck using both the traditional installer, as well as a prototype that we’ve been working on.