Adventures of installing third-party software on Steam Deck

Ever since I got my Steam Deck I’ve been wanting to use it for other general Linux stuff. Today I use it to supplement my main computer, an Apple Silicon Mac. Having an x86-64 Linux device that I can ssh in to do things like build and use x86-64 Docker images has been useful.1

Thing is, there isn’t really a good way to install stuff that isn’t on Steam or available as a Flatpak. So I tried a few options, and these are my experiences with them.

Disable read-only on the root filesystem

This is definitely the easiest method of them all. Just using steamos-readonly disable will let you install and upgrade packages using pacman like standard Arch Linux. (Make sure to then do steamos-readonly enable after!)

The main problem with this one is that OS upgrades will wipe out changes that you make. This can be countered with a script that auto-reinstalls the packages you want. But there are other potential problems. The OS partition does not have a lot of free space to it; as of SteamOS 3.5, around 2 GiB. If you want to install a lot of packages or some very large ones, that’s gonna give you a bad time.

Additionally, pacman may not be able to properly handle packages that just vanish without a true uninstall. Packages leave files around in places like /etc, and when trying to re-install, it will complain about files that already exist. (One quick way to work around this is --overwrite \* if you don’t care about what it might overwrite.)

One other thing if you specifically try to set up a development environment on Steam Deck. SteamOS strips out a lot of unnecessary files from packages such as man pages and header files. Re-installing may help with this, but it will very quickly wipe out what little free space there is on the OS partition.

Final problem is that the SteamOS package repository is not kept in constant sync with the Arch Linux repository. Packages will be outdated. This affects some worse than others, like web browsers. You could potentially work around this by installing newer packages from archlinux.org, but then you can run into even more issues, especially with SteamOS’s glibc being older.

systemd-sysext

I first heard of systemd-sysext by this blog post by Alberto Garcia. It is basically “system extensions” that are used as an overlay on /usr and /opt. It doesn’t require disabling the read-only seal on the OS partition.

This is a neat idea, but it has more problems to it. You need to manually extract package contents and then pack them into a filesystem image. Because of this, it won’t be properly installed and pacman won’t do things like run install hooks. This one is a bigger problem if what you want to install contains stuff like kernel modules or systemd units. dkms, if it’s even installed, won’t find it and build the modules for you, you have to do it yourself. And systemd won’t automatically load units in an extension because extension images are not loaded until later.

It also only modifies /usr and /opt. Packages that add to /etc and other places need their files copied manually. It’s also still tied to the OS version and extensions need to be rebuilt after every OS upgrade. It does however not have the free space problem of the OS partition, so you can install large packages this way.

I used this method for a while as I was willing to put up with the limitations and install some large packages like VMware Workstation. I automated rebuilding extensions and even the building of kernel modules and setting up systemd units.

I had a script that would use pacman to download packages that were not already installed in the base system, and then extract them to make an extension image. This was when pacman’s database was still at /var/lib/pacman, so using it to do anything but install packages to the system worked fine. However SteamOS 3.5 (still in preview at the time of this writing) moved it to /usr/lib/holo/pacmandb, so now I need to disable read-only on the OS partition to do this.

After my script broke I decided to stop using this method. It required too much manual work to use.

Overlay filesystems

I experimented with Linux’s overlay filesystem. This would “mount” /usr as read-write, but any changes go to another location. (Before SteamOS 3.5, it would be a good idea to make /var/lib/pacman into an overlay too.)

I didn’t try it for very long, mostly because I found unmounting to be problematic. I didn’t realize at the time that this may be because directory overlays on the default SteamOS setup may be problematic due to casefolding on the /home partition (for reference, the root filesystem uses btrfs). This was mentioned in the systemd-sysext blog post.

NOTE: systemd-sysext can also use extensions from plain directories (i.e skipping the mksquashfs part). Unfortunately we cannot use them in our case because overlayfs does not work with the casefold feature that is enabled on the Steam Deck.

I know there also exists rwfus, another solution that makes an overlay on /usr. As I understand it, it uses a btrfs disk image, which is probably how it gets around this issue. I haven’t tried it but it seems neat.

This also runs into the same issue with systemd units is extensions, in that they don’t get loaded automatically.

Nix

I’ve heard about Nix before but never seriously gave it a try. Until I saw that SteamOS 3.5 was including a /nix directory at the root (and offloading it to the /home partition), specifically to make it easier to install without modifying the root filesystem.

This might be the best solution overall. It persists on OS upgrades and it properly installs packages. But it has a learning curve to it. It took me a while to figure out how to use it, and I still feel like I don’t really understand how it manages packages, or how profiles work, and all that.

I’ve also already run into one problem: installing services is a bit more tricky. Stuff like Docker adds systemd units. I can’t get systemd to recognize and use them. I need to manually keep re-linking docker.service and docker.socket to /etc/systemd/system. Only for it to not automatically start up anyway. If anyone has an idea on how to fix this, I would love to know.

My current setup: Nix, for the most part

Considering that it solves the issue of persisting across updates and doesn’t modify the OS partition, it’s the one I currently intend to use going forward.

Given my issue with setting up systemd units, I have decided to combine it with disabling read-only to install Docker with pacman. It’s the easiest way to get it working for now.

Additional note: /usr/local

I don’t know when this change was made but I noticed recently SteamOS includes /usr/local as an “offloaded” directory. That would make installing software there convenient since it will persist in updates.

Turns out I was wrong, it does not offload this directory by default (despite being in /home/.steamos/offload). You could still create your own mount over /usr/local though.

Sidenote to this additional note: I made a Docker image that uses the SteamOS repositories. If you want to build something for SteamOS, consider checking it out.

This whole thing is what I get for trying to use my Steam Deck as anything but a gaming console. 🙃

Other things I didn’t try

I completely forgot the existence of Homebrew on Linux, a port of Homebrew for macOS. uyjulian made a gist about setting it up on Steam Deck. It installs everything to /home/linuxbrew.

Another common solution is Distrobox. As I understand it, it runs other distributions using podman but integrates it with the host OS.

I’m leaving these here in case someone else wants to check them out.

Notes

  1. I know x86-64 images can be used on an ARM64 Mac with emulation, but it can be very slow and sometimes compilers randomly fail. I don’t know why that happens. Using an actual x86-64 PC is more useful. â†Šī¸Ž

Bypassing Warp’s login requirement

I recently came across an application called Warp (warp.dev) that claims to be a “blazingly fast, Rust-based terminal” and is “fully native”. It has some interesting features such as blocks to separate the output of different commands.

It comes with some extra things though: required login and forced telemetry. The latter can be blocked with a firewall such as Little Snitch. However the former requires some more work to bypass.

When I first downloaded it and saw that it required a login, I almost decided to just trash it (as would most people I imagine). But I figured I might instead create an account so I can see how much effort it would take to force it to run with no details.

Warp has configuration data in 3 places (that I know of):

  • ~/Library/Application Support/dev.warp.Warp-Stable
  • ~/Library/Preferences/dev.warp.Warp-Stable.plist
  • An application password entry in the “login” keychain

The directory in “Application Support” has an SQLite database called warp.sqlite, but it contains no interesting details about logins or sessions or related things. (It also had a file called telemetry_events.json which was an empty list – I imagine this would have more details if I didn’t block Warp’s network connections)

The plist has at least 4 keys: Shortcuts, FontSize, SystemTheme, and ChangelogVersions.

After I didn’t find any details here, I looked online and found out that Warp uses the keychain for login information. This makes sense, I guess I’m not used to messing with the keychain (most of the applications I use don’t use it).

Warp’s addition in Keychain Access.

The “password” in the keychain entry is JSON containing login details. The format looks like this after I logged in with GitHub (formatted for readability):

{
    "id_token": {
        "id_token": "xxx",
        "refresh_token": "xxx",
        "expiration_time": "2022-07-10T00:50:24.964731-07:00"
    },
    "refresh_token": "xxx",
    "local_id": "xxx",
    "email": "example@example.com",
    "display_name": "ihaveahax",
    "photo_url": "https://avatars.githubusercontent.com/u/590576?v=4",
    "screen_name": "ihaveamac"
}

So the question now is, how many of these can I change until Warp stops letting me into the actual terminal? I started poking at this by inserting custom JSON into the keychain.

The security command has the ability to view, add, and delete stuff from the Mac keychain. (To view Warp’s entry: security find-generic-password -s 'dev.warp.Warp-Stable' -g)

I made a quick custom script to make it easy to generate JSON and modify the keychain entry. After some prodding, I figured out the minimal amount of data required is only “expiration_time“. Every other key can be empty.

{"display_name":"","email":"","id_token":{"expiration_time":"2022-07-10T00:50:24.964731-07:00","id_token":"","refresh_token":""},"local_id":"","photo_url":"","refresh_token":"","screen_name":""}

This can be inserted into the keychain with one command:

security add-generic-password -a 'User' -s 'dev.warp.Warp-Stable' -U -w '{"display_name":"","email":"","id_token":{"expiration_time":"2022-07-10T00:50:24.964731-07:00","id_token":"","refresh_token":""},"local_id":"","photo_url":"","refresh_token":"","screen_name":""}'

This will probably change in the future if they decide to tighten the restrictions on logins, but for the time being one can use the offline features of Warp without giving any data to the developers. (Except for whatever I had to give to get past the first step.)

(It’s worth pointing out that Warp is a company that received $23 million from venture capital funding.)


This is the script to generate and insert into the keychain.

from json import dumps
from subprocess import run

data = {'display_name': 'ihaveahax',
 'email': 'ian@ianburgwin.net',
 'id_token': {'expiration_time': '2022-07-10T00:50:24.964731-07:00',
              'id_token': 'xxx',
              'refresh_token': 'xxx'},
 'local_id': 'xxx',
 'photo_url': 'https://avatars.githubusercontent.com/u/590576?v=4',
 'refresh_token': 'xxx',
 'screen_name': 'ihaveamac'}

data['id_token']['id_token'] = ''
data['id_token']['refresh_token'] = ''
#data['id_token']['expiration_time'] = ''
#data['id_token']['expiration_time'] = 'a'
data['local_id'] = ''
data['refresh_token'] = ''
data['photo_url'] = ''
data['email'] = ''
data['screen_name'] = ''
data['display_name'] = ''

run(['security', 'add-generic-password', '-a', 'User', '-s', 'dev.warp.Warp-Stable', '-U', '-w', dumps(data)])