Plan 9: Setting up 9front on a Raspberry Pi

Written in Apr 10, 2021 by Lucas S. Vieira

Table of Contents

Hello, everyone. Today I wanted to upgrade my Raspberry Pi to the latest release of 9front, however the source code was not compiling on arm64, so I decided to reinstall 9front on my Raspberry Pi.

The following steps were built atop some quick notes I did so I never forget this stuff. There's a small chance I'll update this post with more stuff.

Last updated at [2022-11-18 Fri 00:43]



Figure 1: My local Raspberry Pi setup.

Here's what you'll need:

  • A Raspberry Pi (I used a Pi 3 Model B+ for this example);
  • An SD Card which can store 9front's files (up to 2GB will do);
  • A spare keyboard and a spare 3-button mouse (most wheel mouses are compatible);
  • A 9front Raspberry Pi image (use one compatible with your Pi);
  • An HDMI cable (and some TV or monitor to connect your RPi to);
  • An Ethernet cable to plug the Pi to your router (or whatever device you use);
  • A computer running a Linux distribution with the programs:
    • arp-scan, for device discovery on local network;
    • 9front's spin of the drawterm program, which can be found on its website.
  • Drawterm, specifically one compatible with changes made to 9front.

Downloading the Linux programs and 9front Pi image is a trivial task, so I'll let you figure this out.

You'll also want to write the SD Card image to your SD Card. Extract it from the .img.gz file, and write the .img file to your SD Card using dd. I'll let you figure this one out to.

Plug the SD Card to the card slot of your Raspberry Pi, and plug the ethernet cable to your Pi and to your router (or to whatever network device you have at your disposal). The important thing here is to ensure that the Raspberry Pi and your Linux computer have access to the same network.

Furthermore, plug both the mouse, the keyboard and the HDMI cable to your Raspberry Pi. This will be necessary only on the initial configuration, but if you want to update 9front's kernel anytime, you'll need to plug these before booting again – the reasons will be described later.

A quick note on Drawterm

I never really got Drawterm to work with audio on Linux with its default build configuration, so I recommend using the sndio driver. If you manage to build it, simply run sndiod -dd on a console window and any sound playback should work with Drawterm. Notice though that it may affect playback on other Linux programs, as it blocks audio channels for playback.

Automatic boot

Let's start with the basics. Suppose you have written your 9front image to an SD Card, plugged it in you Raspberry Pi, and plugged the power in. You also have a mouse and a keyboard connected, and the RPI is plugged to your TV or monitor.

Once the RPI boots, you'll be greeted with the Plan 9 Console, which may be graphical or not. In any case, you'll need to press Return a couple times to enter the bootargs, username, etc. Pressing Return will select the default option for everything.

The first step is removing these annoying prompts. Since the bootargs and the user are always the same, we can configure the plan9.ini file so that they are not asked.

At this point, you'll probably be at the rio window manager. Remember that, regardless of your keyboard, 9front will use the US keyboard layout.

Open up a terminal, mount the DOS partition of the RPI and go to its mount directory by using these commands:

9fs pidos
cd /n/pidos

On the RPI image of 9front, there is not plan9.ini file; it is actually generated on boot by taking the commands written on the file cmdline.txt.

You may then open the file cmdline.txt with sam or acme, or even edit it using ed.

Ensure that the following is written on the file:

console=0 user=glenda nobootprompt=local!/dev/sdM0/fs

console is a parameter which is already expected to be there, and user is straightforward. nobootprompt suppresses the query for the value of bootargs when booting 9front. This is because, if we used bootargs instead, we'd only be setting up bootargs's default value, so the boot would hang expecting a Return key press anyway.

Notice that the user glenda is default to most Plan 9 distributions, so do not change that, unless you know what you're doing.

Make sure that there is a line break at the end of the file, and save it.

You can now reboot the RPi by using the reboot command on any command line to test it. Make sure you keep keyboard, mouse and video still attached.

Configuring remote access

Next, we'll configure 9front so that, after booting, it will automatically allow other machines to connect to it remotely.

Specifically, what we'll be doing here is starting a CPU Server, although the RPi will still work as other kinds of servers as well (for example, it will still provide local storage).

Open the file $home/lib/profile by using sam, acme or ed.

There is a switch structure comparing the value of a $service environment variable, which relates to the way the user has connected to the current session. In short, you're currently on the terminal case; but we're interested on the cpu case, which is the remote connection of a computer to a CPU Server.

Before this switch structure, add the following lines. You may change them according to your needs, but keep the user glenda.

auth/factotum -g 'key proto=p9sk1 dom=plan9 user=glenda !password=glenda'
aux/listen1 -t tcp!*!rcpu /rc/bin/service/tcp17019 -R &

A few command line programs are used here, so we'll take some time to understand what exactly are they doing – for further info, check out their manpages (seriously, please do. Using Plan 9 means reading manpages).

factotum talks to the Auth Servers which are running. The specific command above registers a password to a specific user glenda which may be trying to remotely connect to a cpu server. The password may be anything you like, and there are ways to encrypt this information, but we won't be doing that.

listen1 is a simplified way to open up the current cpu server to a remote connection… and let's leave the explanation at that.

ipconfig sets up an IP address for your Raspberry Pi on your current network (you may notice that tools such as ip/ping won't work until this command is run!).

These commands are positioned in such a way that, no matter when or how a user starts a session in 9front, they will always be run. And at this point, even if the RPi were powered on without keyboard, mouse or video output, it would still log in to the user glenda, therefore running this script.

Luckly, running these commands several times will not affect the behaviour of our system.

The 9front instance running on RPi may not be remotely accessed using drawterm from another operating system, or by using /bin/ncpu. We won't be covering the latter one in this post, though.

You can now temporarily turn off your RPi. Run fshalt from any console, await the "done halting" message, and unplug it from power; remove all cables, except the ethernet cable to your network device.

You can now turn the RPi on again. And this time, nothing but the power chord and the Ethernet cable are needed.

Connecting remotely

Now we'll connect remotely to our RPi which runs 9front.

Note that remote access makes you unable to access the DOS partition (pidos), so if you want to edit cmdline.txt again or update your kernel, you'll need to reboot your RPi with video output and keyboard/mouse input.

On Linux, first find out what is the IP of the Raspberry Pi on your network.

sudo arp-scan --local

This will ask you for your password, and will show you some output:

Interface: xxxx, type: xxxx, MAC: xx:xx:xx:xx:xx:xx, IPv4: 192.168.2.x
Starting arp-scan 1.9.7 with 256 hosts (
...	xx:xx:xx:xx:xx:xx	Raspberry Pi Foundation

4 packets received by filter, 0 packets dropped by kernel
Ending arp-scan 1.9.7: 256 hosts scanned in 1.889 seconds (135.52 hosts/sec). 4 responded

In my case, my rpi has an IP on my local network.

Let's use drawterm to connect. We just have to run the following command; the -a argument should point to a machine running the auth server, and -h points to the machine running the CPU (host) server. In our case, they're on the same machine. -u just defines a user, which is "glenda".

drawterm -a -h -u glenda

This will open a Drawterm window containing the following prompt:

glenda@plan9 p9sk1 password:

Type the password previously defined with factotum (glenda in this example), then press Return. Note that it will not echo. You may see an error related to ipconfig (since it is being run for a second time – first time was on boot). It will then give you a prompt:


From there, type rio and press enter to get a graphical interface.

You'll notice that there is nothing onscreen, since we haven't started rio by telling it to run some script (normally riostart, on a local terminal). Let's change that.

Logging in remotely to a graphical session

The riostart script assumes that you are booting 9front by plugging an HDMI cable, a keyboard and a mouse to it, so we need an adapted version of it for a drawterm session.

Create a console window and cd to $home/bin/rc. You'll notice that the bin folder on $home has two subfolders, one called rc and another with the name of your architecture, probably arm or arm64.

rc is where all executable rc scripts go.

Let's make a new one.

touch riostart-remote

Then open it using sam or acme, and add a shebang #!/bin/rc at its beginning.

From now on your config is related to your taste. I like to have some windows opened on boot with some information, such as stats, clock and winwatch.

Here is my config as an example. You may even copy it from your Linux system and paste it to your file (which is better done through Acme), although faces will most likely fail since we haven't configured the Plumber yet, and bar is an extra util which will be discussed at the end.

window 0,0,161,117 stats -lmisc
window 161,0,278,117 clock
window 278,0,478,117 winwatch -e '^(winwatch|stats|clock|faces)'
window 478,0,877,117 faces -i
window -scroll bar

Make sure that this file ends with a line break.

Now, make it executable.

chmod +x riostart-remote

Let's head back and edit $home/lib/profile. Scroll to the case of the switch called cpu – that's the case for a remote connection to our cpu server.

After the definition of the function fn cpu%, let's add a few lines (make sure to add indentation):

if(! webcookies >[2]/dev/null)
        webcookies -f /tmp/webcookies
rio -i riostart-remote

Make sure that these are the last lines of that case.

webcookies starts the manager for HTTP cookies. If it fails to run, then it tries running again, using the file /tmp/webcookies as default cookies file.

webfs binds a web file system interface so that we may browse the web using abaco or mothra.

plumber binds an interprocess messaging system to the file system so that we may do interesting stuff, e.g. open a PDF from command prompt by plumbing its name. We'll have a moment to set it up properly at the end.

The last line starts rio, and instructs it to run riostart-remote on startup.

You can now close drawterm after saving the file, and then login again through the command above. It will not cause instability on 9front; drawterm is basically just a terminal, so it is isolated from the rest of the system.

Upon logging in again through drawterm, you'll be automatically greeted by rio.

Binding the local filesystem

One interesting way to use the 9fs protocol indirectly is to have your local filesystem avaiable to access on your drawterm session.

This means that your Linux files can be accessed from your drawterm session, in an isolated manner, so that the files are only visible on said drawterm session – not for another session, not for another user.

This is done automatically once you log into your 9front box, and the root of your filesystem is then mounted to /mnt/term. So all you need to do is create a "link" to your folder somewhere.

On Linux, we'd think of symlinks. Here, we just need to bind a folder to another.

Edit $home/lib/profile again. Right before starting webfs, add the following:

bind -a /mnt/term/home /usr >[2]/dev/null

This will bind all files in /mnt/term/home/ to /usr. So all files in /usr (e.g. your 9front home folder /usr/glenda) will be kept, plus all files in /mnt/term/home/ will also be seen under /usr.

So for example, if your user folder on Linux is at /home/user, it will now be available on 9front (through a drawterm session only) at /usr/user.

Notice that, if you wish to create new files under /usr, you'll have to use -ac instead of -a on the bind command. For more info, see bind(1).

This is particularly useful for transferring files between Linux and 9front, even allowing for direct usage of remote storage through the 9fs protocol.

You may want to experiment with binding other directories.


Figure 2: An in-progress screenshot of the configuration. This screenshot was taken from Drawterm.

Setting up the Plumber

The plumber(4) is a very important interprocess message protocol, which is initialized by the command plumber when we log onto a session.

Right now, the plumber will not work; however, with minimal setup, we'll be able to allow it to perform simple redirection defaults, such as opening image and PDF files with page, or text files with acme or sam, or opening URLs in mothra.

Create a file $home/lib/plumbing. Then, using Acme or Sam, edit it so it looks like the following:

editor = sam
include basic

This is all we need, though you can add more rules prior to these lines. For more info on the syntax to plumbing rules, see plumb(6).

Here's my own plumbfile. Here, I just set Acme as my default text editor, and made clear that mothra is my default web browser (specifically, I state that I want to open webpages using mothra's alt display mode). These variables are then used on the basic plumbing rules, which we've included at the end of the file.

editor  = acme
browser = 'mothra -a'
include basic

Now you can cd to folders containing images/pdfs and, after listing the files, mark their name with the mouse, middle-click and select 'plumb' to open them in a new window, with their proper application. With web links, you just have to mark them anywhere and plumb them by the same means.

This will also allow you to open webpages by plumbing links, and will also affect the opening of emails once you've configured it.

You can update manually the plumbing rules by using the following command:

cp $home/lib/plumbing /mnt/plumb/rules

Or you may log in to a new session to see your changes take effect.

Extra stuff


Figure 3: Another screenshot of Plan 9, this time taken directly within it. The screenshot was taken from rio and saved directly to my Linux hard drive

Setting up Git

Git for Plan 9 can be set up with easy steps, as it is stated on their repository. The following just repeats the instructions on the linked repo.

NOTE: As of now (2022-11-18), Git for Plan 9 is contained by default on 9front. You shouldn't need to perform the installation steps anymore.


First we obtain a bootstrap copy.

cd /tmp
hget | tar xvz
cd git9
mk all
mk install

Now we'll get a copy which can have new updates pull'ed every once in a while. I did that by creating a $home/src directory, then performing the following operations:

cd $home/src
git/clone gits://
cd git9
mk all
mk install

Setting up username

The next step is setting up a username and an email.

Create a file $home/lib/git, which can be done with touch. Then use Acme or Sam to edit it and add stuff like the following:

	name=Your Name

Make sure you add the newline at the end of the file.

Setting up an SSH key for Git

You might need to create an SSH key to authorize your 9front box on websites such as SourceHut or GitHub, which work best with SSH interaction.

Start by creating an SSH private key, that won't be shared. Notice that this step may take a while:

auth/rsagen -t 'service=ssh' >$home/lib/sshkey

Now, create an SSH public key that will be shared with your Git Forge:

auth/rsa2ssh $home/lib/sshkey >$home/lib/

Finally, just send the private key to factotum(4) so you may now use it. You may also add this line to your profile so that it is always available on login.

cat $home/lib/sshkey >/mnt/factotum/ctl

The reference to these steps may be found here.

Now you can look at the manpages git(1) and gitfs(4) to get started.

Setup automatic time synchronization

If you're someone like me who is not currently in January 1st 1970, you may want to set up Plan 9 to fetch the current date from the internet. Luckly, that's an easy thing to do.

This is something that is set up on 9front's installation on other platforms, but since our RPi image comes with 9front preinstalled, we need to manually setup our timezone.

Looking at the directory /adm/timezone, you'll see a list of many timezones. All you need to do is copy your timezone so that it overwrites local. Here's how I did with mine:

cp /adm/timezone/Brazil_East /adm/timezone/local

Let's discuss a manual update of your RPi time. You can just run something like this:

aux/timesync -n

Or, if you're in Brazil just like me:

aux/timesync -n

Now, open again your $home/lib/profile file. Right after setting up ipconfig, add the previous command.

9front is supposed to run this command on boot through /rc/bin/cpurc or /rc/bin/termrc, but unfortunately, none of these will work prior to having an IP address.

However, you may want to edit these files to include the following somewhere – just make sure that the files end with a newline, after editing:


(Or use the brazillian NTP server instead).

Reboot to see your changes take effect.

This info was somewhat copied from here, and I recommend looking at the wiki as well.

Bottom bar

There's an interesting widget I stumbled across, which shows some information – the current date, specially, which I miss the most.

This widget is part of a suite of experiments with rio called riow, which enables keyboard window management on rio – but I haven't tried using that yet.

If you already installed git9, you can clone and build bar:

cd $home/src
cd bar
mk install

You'll get a binary /bin/bar on your system. Now edit your $home/bin/rc/riostart-remote file, and add the following line at the end of it (remember to leave a dangling newline afterwards!):

window bar

You don't need to specify the window size or position, bar will take care of that.


Lua 5.4 works fine on Plan 9, and there are some libraries that enhance the interoperability of Lua with Plan 9.

Go to $home/src and clone the lu9 repository:

cd $home/src

Now, we need to fetch a few dependencies, and then build and install them:

cd $home/src/lu9
mk pull
mk install

To use lu9, you can either run a Lua file with lu9 file.lua or run an interactive shell through lu9 -i.


There is an awesome implementation of Scheme by Nils M. Holm which not only runs on Plan 9, but was also built for it.

I also recommend taking a look at the author's book, which basically teaches you to build it from scratch.

There is a repository on GitHub which runs fine on any Plan 9 architecture, plus can be installed on the system.

cd $home/src
cd s9fes
mk inst

To use Scheme 9 from Empty Space, just run the program s9.

cpu% s9
Scheme 9 from Empty Space (Reimagined)
> (+ 1 2 3)
> ,l draw-tree
; loading from /usr/glenda/lib/s9fes/draw-tree.scm
> (draw-tree '(a b c))
 |       |       |      
 a       b       c      


Before anything, you seriously should be using Acme or Sam. But if you still insist on using Vim, there is a way to have Vim 7.1 working on Plan 9.

Run the following:

cd /tmp
hget | gunzip -c | tar x
cd vim71/src
mk -f install

After installation, you'll have xxd and vim installed on your system. xxd is useful for generating hexdumps.


Mothra is a browser that works for the simplest tasks, but you may want to use a browser which renders HTML properly.

You'll want to compile Netsurf from source. This will take a while, and I recommend you to use whatever external storage you can to do this.


Figure 4: Comparison between rendering on both Mothra (left) and NetSurf (right).

Personally, since I'm connecting to my RPi from a Linux Drawterm session, I take advantage of that (by using the bindings previously described) so that every built file is stored on Linux instead of the RPi. After building, I then installed on the Plan 9 system itself.

First of all, create a folder where you can clone Netsurf. My projects folder was previously bound using bind -ac to $home/projects.

mkdir $home/projects
bind -ac /mnt/term/home/alchemist/projects $home/projects
mkdir $home/projects/plan9
cd $home/projects/plan9

Now, clone the repository which has the proper mkfile for building Netsurf's port. You'll need Git9 for that; refer to the previous Git configuration section.

cd nsport

After that, fetch other libraries which are crucial for building netsurf. You can use the fetch script on nsport's repository root.

Note that fetching all library repositories will take a while.

cd nsport
fetch clone https

Every once in a while, you may want to update the Netsurf source code so you can get new patches and improvements. You can cd to this same repo and run, before building:

fetch pull

Now all we have to do is build Netsurf and install it to our system.

Needless to say, this might take long, specially considering the bottleneck related to file transfers between 9front's filesystem cache and Linux's filesystem.

mk install

You may also want to edit your $home/lib/plumbing to use Netsurf as your default plumbing action for web links. Just replace your browser with netsurf.

Back to last page