Baking can be a pain

I started my journey into learning BitBake, Yocto and making a Linux image for embedded devices. What have I gotten myself into this time?

First things first

What even is all this I am talking about? Well the company I work for currently manages a product called LmP (Linux microPlatform) which is Linux for embedded devices, or just any device really. It is also not very micro anymore, but it is a good base for if you want to get a Linux system on any of a number of devices. It is based off of Yocto.

That is another project that just started once long ago to get as many different devices running a barebones version of Linux and that you could customize. Not unlike getting Debian on there for instance. What they proposed is a way of doing things and then having a reference/implementation of those proposed ways and it is called Poky.

Now how this all is handled is through some tooling called bitbake. This is what ties everything together.

Imagination

Imagine just putting text in files at certain locations and then invoking a command and magically it spits out an Operating System image. That is in essence what bitbake does. It is half Python/half Bash shell scripting, it has it's own syntax, you can configure variables. Those variables in turn determine the outcome of whatever it is you are trying to do. The difficult part is that you have to sort of guess what will happen as you cannot really follow it completely yourself. You rely on other so called layers and inside those layers are recipes and configurations.

Recipes

So the core of this all are the recipes. These are what let you (cross-)compile the binaries onto the final image and install them. This took some trial and error. You have to supply a reference to the source, can be a GitHub repo for example. At a certain commit or tag or branch and then also supply a License file to make sure the checksum succeeds and it will be taken into a account for your SBOM type stuff.

Then you want to go through the compile from source for whatever that project has, whether it is a Makefile or a python setup.py or what have you. Just go through the steps of it all in order to get the binary you want.

First experience

My first experience was to get the fish shell, that I love very much, as the shell of choice for my LmP. Eventually my bitbake recipe looked like this:

DESCRIPTION = "fish shell"
DEPENDS = "ncurses pcre2"
LICENSE = "MIT"
LIC_FILES_CHKSUM = "file://COPYING;md5=fbac53dd4da483010e69e10c2221f02d"

PV="3.6.1"
FILESEXTRAPATHS:prepend := "${THISDIR}/${PN}-${PV}:"

SRCREV = "f39bc9317d252ce2229ebc11a8a6fe2d98abf430"
SRC_URI = "git://github.com/fish-shell/fish-shell;protocol=https;branch=master"

S = "${WORKDIR}/git"

inherit cmake

This was certainly not the exact first version, but since that project is very well setup and follows a lot of standard, somewhat agreed upon, flows then you just have to state where the source will be located and then you inherit cmake and it will do the rest for you. Very nice.

Now came a much harder task. I wanted to get AstroNvim in there. I mean just look at the dependencies there.

Fonts were not a problem, as were the other tooling binaries.

No what was difficult was NeoVim source itself. It relied on Lua. To install Lua unto LmP is a nightmare. Well not to have it as a runtime during your final image, that part is easy. No, what happens during the bitbake stage is you cross compile things.

One of the steps of NeoVim during compilation is to use Lua to get a shared object. That is not a problem, running that is though. So image having a x86 host system cross compiling a aarch64 Lua shared object. No worries. Now imagine running that cross compiled Lua shared object as part of the rest of the cross compilation of NeoVim source code. That is where it gets tricky as the cross compiled binary cannot run on the host system, but if you use a host system binary then it cannot be used in the thereupon following compilation steps.

What to do?

Workaround

My workaround was simple. Install qemu use it with docker to easily get a throwaway system with a specific architecture. Then compile neovim on there and package it as a .deb. Then have that specific .deb be the source for bitbake. Done.

This is what the recipe for neovim looks like now:

SUMMARY = "Docker cross compiled Neovim"
DESCRIPTION = "neovim"
DEPENDS = "dpkg-native ldconfig-native"
LICENSE = "Apache-2.0"
LIC_FILES_CHKSUM = "file://${COMMON_LICENSE_DIR}/Apache-2.0;md5=89aea4e17d99a7cacdbeed46a0096b10"

PV = "0.10.0-dev"
SRC_URI = "file://nvim-linux64.deb;unpack=0 \
file://nvim-linux64_x86-64.deb;unpack=0 \
"

FILES:${PN} += "/usr/share/nvim/runtime ${libdir}"

do_install:append:x86-64() {
    ${STAGING_BINDIR_NATIVE}/dpkg --instdir=${D}/ \
    --force-architecture \
    --admindir=${STAGING_DIR}/var/lib/dpkg/ \
     -i ${WORKDIR}/nvim-linux64_x86-64.deb

    rm -r ${D}/usr/share/icons
    rm -r ${D}/usr/share/applications
}

do_install:append:aarch64() {
    ${STAGING_BINDIR_NATIVE}/dpkg --instdir=${D}/ \
    --force-architecture \
    --admindir=${STAGING_DIR}/var/lib/dpkg/ \
     -i ${WORKDIR}/nvim-linux64.deb
    rm -r ${D}/usr/share/icons
    rm -r ${D}/usr/share/applications
}

For now this is a break in the journey. Next up I will want to introduce the wasm runtime for docker and see if I can get it to work on there.