OVERHEAD

Running cage foot in an initramfs

Shoe-in Linux

Relevant links:

Explainer

As described in the links above, it’s possible to replace the kernel’s built-in virtual terminals (VTs) with a userspace terminal.

This isn’t what this post is about.

Instead, this whole thing started because I wanted to get the cage foot command combo working in a non-transitioning initramfs image, which has it’s own benefits distinct from removing VTs such as: Reintroducing scrollback, being able to use more sophisticated fonts, and even the ability to use your mouse.

However, there are no step-by-step guides for running cage foot in VTs, so I had to figure this out myself…

The (semi-)step-by-step guide

I say “semi” because this isn’t a set of exact instructions for how to make the initramfs, though if you stick around you’ll be finding out how I made mine using the mkosi tool in some future posts, but rather, these are things you need to keep in mind if you attempt this yourself.

Cage

Cage is a Wayland kiosk, a compositor that runs a single maximised app (think ATM screens), which is a perfect fit for what we’re doing. It’s small enough to not increase the size of the initramfs substantially, and it only handles single apps.

If you start another app inside of a cage session, it’ll open that app on top of the previous one and leave the other app running in the background, which could be useful as a basic substitute for the screen or tmux commands.

Mesa

Since we’re dealing with an initramfs image, and keeping the size down is a priority, we’ll want to exclude installing GPU drivers. Thankfully, the kernel already provides the necessary DRM device at /dev/dri/card0 for software rendering. Unfortunately, that still leaves the mesa package as a dependency of the wlroots package which is a dependency of cage. Great.

Yes, /dev/dri/card0 is from SimpleDRM, not /dev/fb0 which is the older framebuffer system. If none of this note makes sense to you, don’t worry about it.

Technically speaking, wlroots doesn’t need Mesa when using software rendering, which you can use by setting WLR_RENDERER=pixman as an environment variable (where “pixman” is a pixel manipulation library), however, wlroots is compiled with a dynamic dependency on /usr/lib/libgbm.so.1.0.0 which must be satisfied for wlroots to even be executed.

The smart way to get around this would be to compile wlroots without GBM and GLES2 support, as well as any other renderers you don’t need while you’re at it, thus removing the dependency on “libgbm”. Buuuuut I was too lazy.

Instead, you can install mesa, move /usr/lib/{libgbm.so,libgbm.so.1,libgbm.so.1.0.0} to a safe location, uninstall mesa, and then move the libraries back again. This is… actually completely fine, as the first two files are actually symlinks, and the real library file is only about ~60KiB (1KiB = 1024 bytes).

Be careful not to remove the libdrm package, which is pulled in as a dependency of Mesa, as Cage does need that library too. Explicitly installing libdrm should prevent it from being uninstalled by most package managers.

xorg-xwayland

As of writing, Cage has a bug where it always expects /usr/bin/Xwayland to exist, even when you’re trying to run a Wayland-only app. So using the exact same strategy as I used for mesa, you’ll need to do the same thing with that binary up until the next release of Cage is available.

Keyboard layout & mouse cursors

Currently, in the Wayland ecosystem, there is no standardised configs to tell the compositor about your keyboard layout, unlike Xorg which has /etc/vconsole.conf among other configs, so we need to deal with that (unless you’re using a United States keyboard layout. Then you’re already set you lucky motherfu-).

Thankfully, this is really simple for Cage. Just set the XKB_DEFAULT_LAYOUT environment variable to the correct value, however, be aware the value isn’t always the same as the one you’d set KEYMAP to in /etc/vconsole.conf (e.g. KEYMAP=uk vs XKB_DEFAULT_LAYOUT=gb).

foot is a minimal Wayland terminal, which perfectly pairs with Cage for similar size reasons. And also similarly, we’re gonna need to change some things.

foot.ini

When trying to run cage foot normally, you’ll likely notice major visual artefacting as soon as you type or clear the screen, however, this is easy to fix with a simple config addition:

/etc/xdg/foot/foot.ini config snippet which sets damage-whole-window=1 under the [tweak] section.

[tweak]
damage-whole-window=1

Unfortunately, as the option’s name implies, this causes the entire screen to be refreshed when any part of the screen needs updating, which noticeably degrades performance (especially when moving the mouse cursor around). I don’t know why this issue occurs, but for now, this is the only working solution I have to offer.

Addendum

As it turns out, this artefacting doesn’t appear when using WLR_RENDERER=pixman, and was most likely a bug I experienced from another renderer wlroots was using during my testing. So, you probably don’t need this and should definitely test if foot works properly for you without the tweak first.

Wrong working directory

When you first run cage foot, you might notice that the current working directory isn’t set to the user’s home directory, but instead, it’s set to /. I don’t really know why this happens, however, including something like the following in your shells start-up scripts can fix this:

~/.bashrc snippet which changes the working directory to $HOME if $TERM is set to “foot”.

if [ "$TERM" = "foot" ]; then cd "$HOME"; fi
Addendum

The behaviour described above turns out to be expected, and setting the current working directory to the correct location is documented in Foot’s wiki page.

Fonts

Since you don’t need to deal with the VT font limitations, you can use virtually any monospace Freetype font you like (aka. ttf-*-mono fonts). I personally use ttf-fira-mono, which is admittedly a pretty large addition to the initramfs image, but…

No that’s it, I ain’t got a good reason, other than it looks damn nice.

Don’t forget to set your font in the foot.ini config file as mentioned two sections ago.

Terminfo

In /usr/share/terminfo, you’ll find various files corresponding to different terminals, sometimes with multiple files per terminal. These files describe what the terminal is capable of, and significantly affect how certain tools behave (e.g. without foot’s terminfo files, journalctl will complain and refuse to use less), which makes all the unused terminfo files good candidates for removal.

In this case, /usr/share/terminfo/f/foot* (for foot) and /usr/share/terminfo/l/linux (for the VT) are all the files we need (unless you add other terminals) which saves about 700KiB from the compressed initramfs size.

Removing /usr/share/foot/themes in favour of manually configuring foot is also a good space-saving trick.

Replacing getty

I haven’t even thought about compiling the kernel without VTs, however, I have figured out a simple way to replace getty@.service, the service that normally runs on VTs, with the cage foot command combo I’ve been explaining here.

To get straight to the point, and let the file’s comments do the talking, here’s the service file:

Contents of the /usr/lib/systemd/system/cage-foot@.service systemd service file.

# This is a system unit for launching Cage with auto-login as the
# user configured here. For this to work, wlroots must be built
# with systemd logind support.

# Note: getty will still be started on new TTYs even without enabling
# it, which you can test in VMs with `sudo chvt N`. And, switching
# back to foot works too.

[Unit]
Description=Cage Wayland compositor running Foot on %I.
# Make sure we are started after logins are permitted. If Plymouth is
# used, we want to start when it is on its way out.
After=systemd-user-sessions.service plymouth-quit-wait.service
# Since we are part of the graphical session, make sure we are started
# before it is complete.
Before=graphical.target
# D-Bus is necessary for contacting logind, which is required.
Wants=dbus.socket systemd-logind.service
After=dbus.socket systemd-logind.service
# Replace getty if it exists.
Conflicts=getty@%i.service
After=getty@%i.service
# Fallback to getty if the service fully fails after repeatedly restarting.
OnFailure=getty@%i.service

[Service]
Type=simple
# Set variable to software rendering to skip attempting other renderers.
Environment="WLR_RENDERER=pixman"
# For setting XKB_* variables.
EnvironmentFile=/etc/environment
ExecStart=/usr/bin/cage -- /usr/bin/foot
# Cleanly shutdown cage to keep the keyboard working in getty after.
ExecStop=/usr/bin/exit
Restart=always
# Stop the service if many restarts happen in quick succession.
StartLimitBurst=5
User=rescue
# Log this user with utmp, letting it show up with commands 'w' and
# 'who'. This is needed since we replace (a)getty.
UtmpIdentifier=%I
UtmpMode=user
# A virtual terminal is needed.
TTYPath=/dev/%I
TTYReset=yes
TTYVHangup=yes
TTYVTDisallocate=yes
# Fail to start if not controlling the virtual terminal.
StandardInput=tty-fail

# Set up a full (custom) user session for the user, required by Cage.
# This is the name of the '/etc/pam.d/' file.
PAMName=cage

[Install]
WantedBy=graphical.target
DefaultInstance=tty7

Replace the value of User=rescue with whatever regular user you’ve configured in your initramfs, and add any XKB_DEFAULT_* variable keypairs you need to /etc/environment.

This service file was heavily based off the one from the Cage Wiki.

And additionally, you’ll need the following file:

Contents of the /etc/pam.d/cage PAM config file for authenticating cage’s user session.

auth		required	pam_unix.so nullok
account		required	pam_unix.so
session		required	pam_unix.so
session		required	pam_systemd.so

After copying these files into the initramfs, enable cage-foot@tty1.service and disable any getty@ttyN.service services that are configured. When you switch VTs, you’ll get the standard getty service where you can manually start the cage-foot@ttyN service (where “N” is the TTY number, which you can find with the tty or who commands).

And finally, the end

It was a painstaking journey to discover all this myself, about 3-4 days of looking up obscure error codes and pouring through man pages and issue trackers, but after all that, I’d say it was worth it.

Of course, this isn’t the only thing I’ve been working on since the last blog post, although I do realise it has been a while, and in the next couple of posts, I’ll be telling you all about the mkosi tool.