NervesLocalSetup

How to get going with Nerves on Linux using a VM. Assumes you know how to use Elixir and qemu, and that you are operating on a Debian Linux system or something tolerably close to it.

Up to date as of April 2024.

Setup and install

The Nerves intro documentation is a little… scattered. It wants you to go through the livebook or something, which is fine but if you don’t want to do that and/or don’t have hardware to run it on right there, it takes a bit of diggging to set up. You have to go to Getting Started, which isn’t actually enough to get you started because you actually have to dig down further to Installation first. And none of it tells you anything about how to get going on a virtual machine, which seems like the obvious place to start when you don’t necessarily want to buy hardware for this, or don’t know what hardware you want to use yet.

So, let’s go through the setup process in order, and get all the pieces sorted out. First, install basic stuff:

sudo apt install build-essential automake autoconf git squashfs-tools ssh-askpass pkg-config curl libmnl-dev elixir libssl-dev libncurses5-dev bc m4 unzip cmake python3 libwxgtk3.2-dev

Next install fwup, which appears to be a firmware image builder/bundler tool. It isn’t in apt, so we will install it from Github. Note that fwup is NOT the same as fwupd, helpfully. I have no idea whether they’re related.

wget https://github.com/fwup-home/fwup/releases/download/v1.10.2/fwup_1.10.2_amd64.deb
sudo apt install ./fwup_1.10.2_amd64.deb

Ok, this is where we hit our first hiccup. We SHOULD just have to do this:

# Update hex and rebar to latest (hex-2.0.6 and rebar3 1-14), and install Nerves base stuff.
# Requires Elixir >= 1.11.2
mix local.hex
mix local.rebar
mix archive.install hex nerves_bootstrap

# Create project template
mix nerves.new hello_nerves
cd hello_nerves

# Set MIX_TARGET, which specifies your target board.  With fish shell:
set -x MIX_TARGET x86_64
# bash:
# export MIX_TARGET=x86_64

# Install deps and build firmware image
mix deps.get
mix firmware

But if you’re like me and running on Debian 12 Bookworm, when you do mix deps.get this will conveniently print an error message starting with something like this:

==> hello_nerves
Nerves environment
  MIX_TARGET:   x86_64
  MIX_ENV:      dev

** (Mix) Major version mismatch between host and target Erlang/OTP versions
  Host version: 25
  Target version: 26
...

Given that OTP 25 was released in late 2022 and OTP 26 still isn’t in the Debian Unstable repo, and people are already talking about what the OTP 27 release will bring, it seems a safe bet to bypass the the Debian repo and install our own version of Erlang/Elixir. At least until I have the time and energy to become a Debian maintainer and give a hand to poor Sergei Golovan who has been packaging Erlang mostly-singlehandedly since 2006. So I guess I’ll try this asdf tool that the Nerves docs talk about. No, not that asdf. No, not that one either. This one..

Sigh.

Ok, to install:

# The Nerves docs at https://hexdocs.pm/nerves/installation.html#linux say to use branch v0.11.0 
# but I had some Issues with it and 0.14.0 seems to work more smoothly
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0

# There's also shell files for sh/bash/zsh, elvsh, nushell, and powershell; I'm frankly impressed.
source ~/.asdf/asdf.fish

# Tell asdf which things you want it to use
asdf plugin add erlang
asdf plugin add elixir

# Tell asdf to download and build the versions of things you want.
# It gives some scary-looking warnings along the lines of "asdf_26.0.2 is 
# not a kerl-managed Erlang/OTP installation" but seems to work fine in practice.
asdf install erlang 26.0.2
asdf install elixir 1.15.4-otp-26

# Tell asdf which versions of things to use by default
asdf global erlang 26.0.2
asdf global elixir 1.15.4-otp-26

Now running erl or iex should print out “OTP 26” among the version text. If you want to use your system-wide installs then just open a new shell and don’t source ~/.asdf/asdf.fish or whatever. Despite my bitching asdf seems pretty sensible, it puts everything into its install dir and you just run the shell script to access it, and if it doesn’t know what version of something to use it just asks you. Not perfect, but it’s something. Don’t want it anymore? Just delete ~/.asdf. I’m sure you can also set tools and versions and stuff in a file in the project dir, but I’m not interested enough to find out yet.

Right, on with the show. You have your MIX_TARGET still set correctly after all that rigmarole? You still in your Elixir project dir? Well it’s probably garbage now if you tried to build it with the wrong version of Elixir, doing mix firmware I get the following error:

|nerves| Building OTP Release...                                                                                                                                                    [216/1805]
                                               
* [Nerves] validating vm.args                                                                  
** (Mix) Incompatible vm.args.eex
                                               
The procedure for starting IEx changed in Elixir 1.15. The rel/vm.args.eex for                 
this project starts IEx in an incompatible way for the version of Elixir you're                
using and won't work.                                                                          
                                                                                               
To fix this, either change the version of Elixir that you're using or make the                 
following changes to vm.args.eex:              
                                                                                               
Please remove the following lines:           
                                               
* rel/vm.args.eex:47:                                                                                                                                                                         
  -user Elixir.IEx.CLI                                                                                                                                                                        
                                               
Please ensure the following lines are in rel/vm.args.eex:                                      
  -user elixir         
  -run elixir start_iex  

You can probably fix it, but easier to just nuke your project and start over.

cd ..
rm -rf hello_nerves

mix local.hex
mix local.rebar
mix archive.install hex nerves_bootstrap
mix nerves.new hello_nerves
cd hello_nerves

# Get deps for our target and build the actual firmware.
set -x MIX_TARGET x86_64
mix deps.get
mix firmware

You should now have a hello_nerves.fw file in _build/x86_64_dev/nerves/images/hello_nerves.fw, or wherever mix firmware has told you it lives. On my system it is 26 MB, which is pretty ok for a full Linux system with busybox, Erlang and Elixir. The .fw file is just a zip archive with whatever layout fwup has decided it needs. Peeking into it, it’s some metadata, a few grub files, and a squashfs filesystem containing about as little as humanly possible to run a Linux system.

Whew! Ok. This is a lot, but it’s actually notably better than most embedded workspace setups I’ve had to deal with.

Actually running stuff

Now that we have a disk image, let’s make a VM that can actually run this. The Nerves x86_64 target seems… fairly underused at the moment, but we don’t need to do anything fancy. No information on how to turn this .fw file into something qemu can actually boot, but investigating the fwup docs gives us the answer:

fwup -a -d myimage.img -i _build/x86_64_dev/nerves/images/hello_nerves.fw -t complete

-a is “apply the firmware update”, so copying an image into something instead of just building it. -d is the “device” file, -i is the image, -t complete is the “task” to perform, whatever that means. So now we have myimage.img sitting in the current dir, which inspection proves to be a raw disk image; you can look at the partition table with cfdisk or your tool of choice. Oh well then, let’s fire it up in qemu:

qemu-system-x86_64 -drive file=myimage.img,format=raw -m 512 -display curses

It boots, decompresses Linux, and then appears to hang forever. Well that’s inconvenient. The last lines are:

ALSA device list:                                                               
  No soundcards found.                                                          
input: ImExPS/2 Generic Explorer Mouse as /devices/platform/i8042/serio1/input/input3                                                                           
Waiting for root device PARTUUID=04030201-02...                                 
tsc: Refined TSC clocksource calibration: 3593.117 MHz                          
clocksource: tsc: mask: 0xffffffffffffffff max_cycles: 0x33caed1ef19, max_idle_ns: 440795230794 ns                                                              
clocksource: Switched to clocksource tsc

Incorrect detours

So it looks like Linux does early boot stuff and then decides to send its output somewhere else? Somewhere qemu doesn’t show by default with -display curses. That’s rather odd, the Nerves x86_64 platform documentation says IEx terminal Display - tty0, so it should be printing to the normal qemu screen. Looking at the man page for qemu, it appears we might want -nographic, which says it redirects everything to the terminal, including serial ports and stuff. But that appears even worse, just hanging forever on Booting partition A..., though at least this time qemu lets us do C-a x to exit the emulator. So it’s either trying to output its info to somewhere we can’t see it (I’m SSH’ed into a server, so I have no graphics on the dev machine at the moment), or it’s not even managing to boot linux properly. Probably the first option, since nothing else in the config changed, but playing “where the hell is my output going” is one of those joys of embedded programming in general. I’m a bit rusty on qemu though, and it appears this system has version 7.2.9 rather than the version 6.2.0 all my osdev scripts are written for. C-a c gets us to qemu’s very useful and critically underdocumented system console, which shows… that things appear to be working properly and the instruction pointer is just sitting in a tight loop somewhere?

Huh. FINE I’ll fire up VNC over a heckin’ cell phone internet connection and look at it for real! And I see… the same thing. clocksource: Switched to clocksource tsc, and then nothing. …WAIT after a few minutes it printed out random: crng init done. Is the linux kernel for some reason getting Really Confused about being in a VM? qemu’s hardware defaults are usually pretty good about that, if only because it pretends to be a Gateway shitbox from like 2003. Maybe that’s too old for Nerves’ chopped-down kernel to appreciate.

Let’s try something where Nerves probably works well, such as a RPi. Problem there is I have no idea how well qemu works emulating it, but here goes…

set -X MIX_TARGET=rpi0
mix deps.get
mix firmware

#fwup -a -d myimage.img -i _build/rpi0_dev/nerves/images/hello_nerves.fw -t complete
# Wait we can use nerves to create the image for us, just give it a normal filename to write to.
# Would that work with an x86_64 target?  Huh, good question; I didn't even try it 'cause I assumed it wouldn't.
# But some experimenting shows that it DOES in fact work.
mix firmware.burn --device rpi0.img 

# qemu complains if the SD card image is not a power of 2 size
qemu-img resize -f raw rpi0.img 512M

qemu-system-aarch64 -machine raspi0 -drive file=rpi0.img,format=raw

HAH, there we– wait, no, nope, it just has a blank screen. Goddammit. …nope, I’m just an idiot, the nerves_system_rpi0 docs say “IEx terminal: UART ttyAMA0”. Well I don’t really want to figure out the incantation to make qemu output the serial terminal to the console right now. The Nerves docs for the rpi0 platform do note that output can be switched to the HDMI output, but doing so “won’t be a good development experience”.

:-|

No no, this is fine, qemu is always this bitchy. The RPi3 Nerves docs say that the RPi3 terminal is “HDMI and USB keyboard”. Let’s try that…

set -X MIX_TARGET=rpi3
mix deps.get
mix firmware
mix firmware.burn --device rpi3.img 
qemu-img resize -f raw rpi3.img 512M
qemu-system-aarch64 -machine raspi3b -drive file=rpi3.img,format=raw

…same result. Maybe VNC over cell phone is a mistake?

Ok after a lot of messing around and general mysteries, it appears that the clocksource: Switched to clocksource tsc is a red herring, and the random: crng init done message is just it saying that it has enough hardware entropy (from me desperately mashing stuff into the keyboard) to seed the kernel RNG. The real error is a few lines above that, where it says waiting for root device PARTUUID=04030201-02, but running blkid on the image file shows that it does indeed have PTUUID=“04030201”….

AHA! It works if I tell qemu to use the virtio drive type instead of qemu’s default IDE interface! It does seem like a safe assumption that Nerves would not bother compiling IDE support into its kernel. So it boots if I run it with qemu-system-x86_64 -drive file=amd64.img,format=raw,if=virtio -m 512.

This drops me into an iex terminal full of convenient Unix-style shortcuts like ls/0 and pwd/0, and I can indeed run the HelloNerves.hello() function and get the expected output.

whew

The Whole Shebang

All the false turns and stuff are left in here on purpose, but that makes this harder to use as a reference. So here’s the whole working process I came up with:

# Install basic deps
sudo apt install build-essential automake autoconf git squashfs-tools ssh-askpass pkg-config curl libmnl-dev elixir libssl-dev libncurses5-dev bc m4 unzip cmake python3 libwxgtk3.2-dev

# Install fwup
wget https://github.com/fwup-home/fwup/releases/download/v1.10.2/fwup_1.10.2_amd64.deb
sudo apt install ./fwup_1.10.2_amd64.deb

# Install asdf
git clone https://github.com/asdf-vm/asdf.git ~/.asdf --branch v0.14.0
source ~/.asdf/asdf.fish

# Tell asdf which things you want it to use
asdf plugin add erlang
asdf plugin add elixir

# Tell asdf to download and build the versions of things you want.
# It gives some scary-looking warnings along the lines of "asdf_26.0.2 is not a kerl-managed Erlang/OTP installation"
# but seems to work fine in practice.
# This will take a few minutes 'cause it has to build the Erlang system from source.
asdf install erlang 26.0.2
asdf install elixir 1.15.4-otp-26

# Tell asdf which versions of things to use by default
asdf global erlang 26.0.2
asdf global elixir 1.15.4-otp-26

# Install nerves and create template project
mix local.hex
mix local.rebar
mix archive.install hex nerves_bootstrap
mix nerves.new hello_nerves
cd hello_nerves

# Set target machine and get target deps
set -x MIX_TARGET x86_64
mix deps.get

# build stuff and make fwup package
mix firmware

# Turn that into a disk image file called hello.img
mix firmware.burn --device hello.img 

# Run the sucker in qemu
qemu-system-x86_64 -drive file=hello.img,format=raw,if=virtio -m 512

…and now that have figured all that out I see that there is in fact a build target called mix nerves.gen.qemu_script. RIP.

Conclusions

As mentioned, in my experience setting up embedded toolchains always sucks ass. This is the real reason that Arduino and Raspberry Pi succeeded; not ’cause the hardware was amazing, not even only because it was cheap, but because made it easy to get started and use the tools with minimum hassle so that newbies could start experimenting quickly, and people who kinda knew what they were doing but were new to the domain (like me) spent a minimal amount of time screaming at header paths and linker errors. Toolchains like clang and Rust’s cargo show that cross-compiling doesn’t have to be a nightmare, but it’s still an imperfect process.

So all in all, Nerves seems to do a pretty nice job of getting you going with embedded programming. It uses other tools like fwup and asdf but is a thin enough layer over them that it doesn’t hide much of what’s going on, just provides nice shortcuts. Not mentioned here but apparently it also uses Buildroot to construct kernels and platform packages. So these are generally tools you want to be using anyway. It definitely gives you a sane toolkit for building embedded linux systems, so you don’t have to figure out how to do it from scratch, which is extremely useful. If I had to build an embedded Linux system without Nerves, I now know far, far more about how to do it well than I did before I went through this. That said, there’s still some hiccups:

  • As mentioned, the documentation is somewhat scattered and bass-ackwards from what I expected/wanted. But that’s a Hard problem because different people learn in different ways.
  • IMO VM’s deserve more love. As a prototyping and exploration tool they should not be underrated.
  • More/better troubleshooting docs would be nice, but they tend to come from the system being used, and Nerves is still a very small community.

Lots of things seem to be just due to the small community. Erlang/Elixir programmers are already a niche, and embedded programmers are a very different niche, and the Venn diagram of the overlap is rather small. Still, it’s better than it looks. For example, the hardware support appears a little anemic, being basically Raspberry Pi, Beaglebone, and a few miscellaneous things… but there’s more unofficial platform packages out there and it doesn’t seem too difficult to create a custom one. It’d be be useful if they advertised that a little more, and more docs on how to create your own platform would be nice. I already have a bunch of devices in mind where Nerves would be useful, or at least fun… Anbernic handheld consoles and other crazy handheld consoles, Gateworks industrial boards for when I want a RPi that can survive being banged around a bit and someone else is paying for it, a large pile of Pine64 single-board computers and touch devices…

So yeah. A bit rough, but very very promising. I am excited.