A previous article in the Fedora Magazine describes how toolboxes can be saved in a container image repository and restored on the same or a different machine. The method described there works well for complex scenarios where setting up the toolbox takes considerable time or effort. But most of the time, toolboxes are simpler than that, and saving them as container images is… well, wasteful. Let’s see how we can store and use our toolboxes in a cheaper way.
Problem statement
- Define toolboxes in simple text files so that they are easy to transfer or version in a GIT repository.
- Have the ability to describe a toolbox in a declarative way.
- Have everything set up automatically so that the toolbox is ready to use upon initial entry.
- The system should support customizations or extensions to accommodate any corner cases that were not foreseeable from the start.
Proposed solution
Toolboxes are just instances of a container image. However, they have access to the home directory and use the same configuration as the host shell. On the other hand, each toolbox has a special file, visible only inside the toolbox, that keeps the name of the toolbox. This is /run/.containerenv. Combining these two facts presents a possible route to the desired state: simply keep whatever initialization or configuration steps as simple rc files and use a convention to source them only for a specific toolbox. Consider, step by step, how this can be accomplished (using bash as an example because it’s the default shell in Fedora. Other shells should support the same mechanisms).
For the system to work, two things are needed:
- A few toolbox definitions.
- Something to take those definitions and turn them into toolboxes.
Storing toolbox Definitions
The first point is easy. Put the files anywhere. For example, they can reside in~/.bashrc.d/toolboxes. (This path is used throughout the rest of the discussion.) Each rc file is named after the toolbox it configures. For example, ~/.bashrc.d/toolboxes/taskwarrior.rc for a toolbox named taskwarrior to be used for tasks and time tracking. The contents of this file is described later in this article.
Converting Definitions into toolboxes
For the second point, to keep things nice and tidy, place the orchestration logic in a separate file in the ~/.bashrc.d directory. For example, call it toolbox.rc. It should have the following content:
function expose(){
[ -f "$1" ] || echo -e "#!/bin/sh\nexec /usr/bin/flatpak-spawn --host $(basename $1) \"\$@\"" | sudo tee "$1" 1>/dev/null && sudo chmod +x "$1"
}
function install_dependencies(){
[ -f /.first_run ] || sudo dnf -y install $@
}
if [ -f "/run/.toolboxenv" ]
then
TOOLBOX_NAME=$( grep -oP "(?<=name=\")[^\";]+" /run/.containerenv )
if [ -f "$HOME/.bashrc.d/toolboxes/${TOOLBOX_NAME}.rc" ]
then
. "$HOME/.bashrc.d/toolboxes/${TOOLBOX_NAME}.rc"
fi
if ! [ -f /.first_run ] ; then
[[ $(type -t setup) == function ]] && setup
sudo touch /.first_run
fi
fi
With the orchestration logic set up, some toolboxes are needed to orchestrate. Continuing with the taskwarrior toolbox example, a definition is added. Create a file called ~/.bashrc.d/toolboxes/taskwarrior.rc and add the following line in it:
install_dependencies task timew
This practically says that in this toolbox these two packages are to be installed: task and timew.
Creating toolboxes
Now, create and enter the toolbox:
$ toolbox create taskwarrior
$ toolbox enter taskwarrior
When the newly created toolbox is entered, dnf runs automatically to install the declared dependencies. Exit the toolbox and enter it again and you will get the prompt directly because all the packages are already installed.
It is now possible to stop the container and recreate the toolbox and everything will be installed back automatically.
$ podman stop timewarrior
$ toolbox rm timewarrior
$ toolbox create taskwarrior
$ toolbox enter taskwarrior
Getting More Complicated
Now complicate things a bit. What if some random commands need to run to set up the toolbox? This is done using a special function, called setup(), which is called from toolbox.rc, as shown above. As an example, here is the setup for a toolbox for working with AWS resources. It is named awscli and the associated configuration file is ~/.bashrc.d/toolboxes/awscli.rc:
setup() {
curl "https://awscli.amazonaws.com/awscli-exe-linux-x86_64.zip" -o "awscliv2.zip"
unzip awscliv2.zip
sudo ./aws/install
}
Another use case to cover is exposing host commands inside the toolbox. Consider a toolbox for file management that opens pdf files in a browser previously installed as a flatpak on the host. Employ the expose() function, also defined in toolbox.rc, to give access to flatpak inside the toolbox. Below is the vifm.rc file used to create the vifm toolbox:
install_dependencies vifm fuse-zip curlftpfs shared-mime-info ImageMagick poppler-utils
expose /usr/bin/flatpak
With that, from inside the toolbox, start the browser using:
$ flatpak run com.vivaldi.Vivaldi
Extending the toolbox
The last point in the problem statement (wish list) was extensibility. What is shown thus far is just a basis that already works pretty well. However, one can see how this system is adaptable and can evolve to meet other requirements. For example, new functions can be defined, both as implementations in toolbox.rc or as hooks to be called if defined inside the toolbox configuration file. Also, the existing functionality can be mixed and matched depending on current needs. The last example, for instance, demonstrates a combination of dependencies and host command access. Do not forget that the toolbox configurations defined in ~/.bashrc.d/toolboxes are ultimately just shell files and you can add everything that the shell supports: aliases, custom functions, environment variables, etc.
Troubleshooting
Problem: nothing is happening when I enter a new toolbox.
The ~/.bashrc might not be set up to source files from ~/.bashrc.d. Check that the following block exists in ~/.bashrc:
# User specific aliases and functions
if [ -d ~/.bashrc.d ]; then
for rc in ~/.bashrc.d/*; do
if [ -f "$rc" ]; then
. "$rc"
fi
done
fi
unset rc
Problem: when I enter a toolbox I get a command not found error.
The order in which files are sourced might not be correct. Try renaming ~/.bashrc.d/toolbox.rc to ~/.bashrc.d/00-toolbox.rc, then delete and recreate the toolbox.
Meg
I prefer to just extend toolbox with a custom Dockerfile. It’s not as seamless as this approach, because you need to build and specify the image when running toolbox, but it solves the same problem statement, is more transparent, and there’s no need for bash scripting. It also has the added benefit of being able to create different dev environments for use with other tools that use containers, like VS Code’s Dev Containers.
Joachim Katzer
I like this approach very much because of its simplicity and expandability. I was looking for such a possibility myself for some time. I wasn’t aware of the files /run/.toolboxenv and /run/.containerenv, visible only in toolbox environments, on which all this is based on.
However, it didn’t work by default on my Silverblue installation: ~/.bashrc is only executed in interactive shells, but not when entering a toolbox. I had to add the following line to my ~/.bash_profile:
[ -f “/run/.toolboxenv” ] && source ~/.bashrc
The script ~/.bashrc.d/toolbox.rc is an excellent starting point.
Andrea
Why do not pass definitely to Distrobox?
James
Whilst this looks like a good idea, to do anything really useful will result in a degree of complexity. I wrote a bash framework to provision containers, (https://github.com/millerthegorilla/podbash) but I switched to using ansible as it was easier/better specified.
And toolboxes are simply podman containers with the same tight coupling with the host system that Distrobox offers, so all the podman commands work fine.