I love applications that can be packaged as a single executable file. A lot of languages can create files like that now, including C, C++, Rust, Go, and even C#. But not every language can compile to a single executable like that, even even ones that do might need some extra files bundled with the executable.
There is a lot of overlap between this topic and self-extracting archives. Check those out for ideas if you are going to roll your own solution.
One popular method I see these days is AppImages. AppImage is a format for bundling Linux applications, along with their dependencies, in order to create applications that work without installation or root, and the whole thing gets packaged in a single file.
They are extremely convenient, and they work pretty well. We’ve deployed AppImages to production and used them to bundle tools and dependencies without messing with developer or server systems.
An AppImage is basically a directory that contains all the files for an application. This directory is turned into a compressed disk image with squashfs. A small runtime that is prepended to the squashfs image handles executing the application. This is done by mounting the image as a temporary mount point, and then executing the application inside it. This runtime also handles things like extracting the disk image to execute it without mounting.
In the time we used AppImages, we ran into two small problems.
The first one is the dependence on libfuse. Running an AppImage normally, without extracting it first with
--appimage-extract, requires FUSE and libfuse. A huge appeal of AppImages for us was being able to deploy applications without any superuser privileges and without modifying the system in any way. This dependency on libfuse means either we need to modify the system to install stuff, or we need to extract the image and get rid of the “single-file executable” benefits.
The other downside is also related to mounts. In order to make startups faster, and not require extra disk space or RAM when executing the application, AppImages create a temporary mount instead of extracting themselves every time you execute them. This is a good way to get free performance.
But we ran into some commercial system monitoring tools that give alerts to sysadmins when a new mount is created. This is meant for checking the disks, stuff like making sure everything is in /etc/fstab and will be mounted again on reboots etc. But this system doesn’t understand temporary mounts, and thinks everything is a physical drive. When we execute AppImages regularly, that monitoring system creates useless alerts. It’s not something we can disable, and it’s not a bug we can fix ourselves. And frankly mounting stuff feels too much like modifying the system, even if FUSE lets us do it without extra permissions.
Those two minor issues inspired me to come up with an alternative solution.
An ugly and/or great solution
I wrote a Shell script generator in Python. It takes the application to bundle as a directory, just like AppImages. The output is a single executable that runs the application, just like AppImages. But the internals? That’s very much unlike AppImages.
We first take a statically-compiled binary of the zstd compressor. We encode this binary as base64, and embed this into our Shell script as a heredoc. The script un-base64’s that into a temporary file, and makes it executable.
We then tar our application folder, pipe that through zstd to compress it, encode it as base64, and embed it into our Shell script. The script pipes that through a base64 decoder, decompresses is with the zstd binary we unpacked earlier, and un-tars it into another temporary directory.
After this, the application is executed normally from this temp directory. After our shell scripts exits, all the temp files are deleted from the system.
All of this works, and we’ve deployed applications with this strategy. The only downside seems to be a small latency when executing the application, and this is due to the decompression and extraction of the embedded tar file.
This project was both fun and useful in real life. If I come back to it in the future, I’m planning to investigate some of the things below.
- Instead of base64, embed binary data after the script and unpack with dd.
- Instead of a shell script with embedded data, make a statically compiled executable that can be prepended to archives.
- Instead of extracting a zstd binary and using it to decompress data, compile the decompressor into the “runtime” as a static library.
- Allow stuff like
--appimage-extractto extract application files somewhere without executing them.