Developing inside a Toolbox container

When I wanted to make a patch for some library, I always disliked installing all of the build dependencies on my system and messing with LD_LIBRARY_PATH (link) to be able build and run the patched library. I never took time to uninstall build dependencies afterwards and LD_LIBRARY_PATH is a step that I would’ve liked to skip when possible.

If you are having similar issues and are using Fedora then you can use the Toolbox containers to avoid these issues.

Toolbox Containers

The Toolbox repo describes the software as:

Toolbox is a tool that offers a familiar package based environment for developing and debugging software that runs fully unprivileged using Podman.

The toolbox container is a fully mutable container; when you see yum install ansible for example, that’s something you can do inside your toolbox container, without affecting the base operating system.

Basically the Toolbox container is its own environment that can still access your home folder, display, dbus, etc. You can install packages in it with regular dnf install vim and run programs on your host desktop.

Example Development Workflow

To show an example of developing with Toolbox lets say we have a program foo that is having an issue and we suspect the driver in the Mesa libs to be the culprit.

First thing we need to do is to create a container.

$ toolbox create -c mesa-devel

This will create a container named mesa-devel somewhere in the hidden folders in your home directory.

We can then enter the container.

$ toolbox enter mesa-devel
⬢$

Notice how the prompt starts with , this is a clue that you are inside a container so that you don’t mix it up with you host environment.

If we now try to run our program foo in the container (our home folder is readable/writable) it will not run since it depends on the Mesa libs that are not installed in it. To fix this we install them as we would on a host.

⬢$ sudo dnf install mesa-dri-drivers

Now the program runs but we can’t debug the driver, we are missing the debug symbols. We need to install the debug package.

⬢$ sudo dnf debuginfo-install mesa-dri-drivers

This is the first thing that would clutter our system but now it is installed in a container!

With any luck we find the code that could be causing our issue, so we clone the source code repo to make a patch. But before we can build that code we need to install its build dependencies.

⬢$ sudo dnf builddep --skip-unavailable mesa

After we build the libs we can install them directly to container’s / without a package manager. If you care enough (and you probably should unless this is a throwaway container) about not overwriting the installed files from the mesa-dri-drivers package uninstall that package first.

Now running the program inside the container with just ./foo (no LD_LIBRARY_PATH needed!) will use the newly compiled libs, while running it in a host terminal will use the system libs. And both will run on the same desktop since the display is shared! Of course this only works if the API/ABI of the lib stays the same, otherwise we need to compile the program for each environment.

All that is left now is to patch the driver code.

When we are done with that we can exit the container like exiting a normal shell.

⬢$ exit
$

The installed packages in a container persist so we can enter it anytime later.

If we don’t need the container any more we can safely delete it.

$ toolbox rm mesa-devel

This way our system stays clean while the upstream hopefully got an useful patch.

What I really like about this workflow is that all the development is done inside a container! There is no need to track what packages we have installed in order to remove them later, we can install different version of the packages to check for regressions, we can also install files directly to root or do other unsafe things without the fear of breaking our main system,… This opens up a lot of possibilities.

A Possible Improvement

A good extension to this workflow would be to use fedpkg(link) that uses the rpm package of the lib to build and install it. This way we would be compiling the lib with the same compile flags as used by the distro which can save us a lot of time of manually configuring the build options. The result would also be a rpm which can be installed and uninstalled with dnf.

To do this we probably just need to point the package’s spec file to our local clone of the source code. Or maybe we can even point it directly to upstream repo and let the rpm tools clone it, if we can of course change the source code afterwards.

Let this be an exercise for the reader.


See also