{"slug": "linux-apps-that-maybe-run", "title": "Linux Apps That Maybe Run", "summary": "Linux software distribution remains fragmented, with experts divided between containerized apps and granular permission models, while shared library soname bumps frequently break applications after OS updates, frustrating developers accustomed to Windows' simpler model.", "body_md": "## Linux Apps That Maybe Run\n\nLatest update:\n\nAccording to some prominent fellows, whom I won't name to protect the\nguilty, the only viable way to distribute software for Linux is to\nship a container. Not to make an rpm/deb & serve it from a repo, like\nsome insignificant companies that control half of the Internet do, but\nto collect all (or most) dependencies & put them alongside the\napplication. Only this way, experts say, guarantees a high probability\nof surviving in the Linux wilderness.\n\nAnother set of leading figures argue that shipping a container is\nnecessary but not sufficient: in the days of rogue AI agents &\ncatastrophic vulnerabilities hiding in every software corner, an\napplication should (some insists on the word *must*) be incapable of\ndoing anything without the user's permission.\n\nI thought about that a little, & came to the conclusion that a\nlayperson couldn't care less. I'm not talking about a guy who feels\npersonally offended when an ntp client dares to send a udp packet\nwithout asking first, I'm talking about an individual who downloads an\n\"app\" not to enjoy perusing a granular permission model, but to\naccomplish *a* task.\n\nI noticed how not a few Windows developers are at a loss to what to make\nof Linux software distribution models. This post is for them.\n\n### Not a GUI\n\n*Theoretically*, if you write simple network microservices, you can\nabandon all 3rd-party libraries, even libc itself, & stick to syscalls\nof the Linux kernel ABI. As long as Linus is in charge, your programs\nwill always work.\n\nOut of curiosity, I once tried to write a simple nolibc TCP server &\n[described the\nexperience](https://henry-flower.dreamwidth.org/521944.html). (For\nconvenience, the text is in Ukrainian.)\n\nNow, let's get back to real life.\n\n### DLLs & sonames\n\nIn the Linux world a DLL is called a *shared library*. A plain stock\nFedora Live ISO with no developer tools installed, contains thousands\n(3977 in f44) of them.\n\nThe 2 major Linux GUI toolkits (GTK & Qt) use a multitude of 3rd-party\nshared libraries that they don't control, & when you link against a\nGUI library in a particular distro, there is 0 guarantees that your\nprogram will work in the next distro release. Even if the shared\nlibraries of the GUI toolkit stay the same, some transitive dependency\nmay acquire a *soname bump*, & a dynamic linker will refuse to run\nyour program.\n\nA shared library named *foo* has actually several names, that look\ndifferent to various library consumers:\n\n- you use\n`-lfoo`\n\nargument when you invoke a linker to link your\nprogram against the *foo* library;\n- during that building step, the linker searches for\n*libfoo.so* file\n(in a set of known directories);\n- while the source code of the library could live across many files of\narbitrary names, a build step that produces the library itself, puts\nthe result in\n*libfoo.so.x.y.z* file.\n- when you invoke your program, the dynamic linker searches for\n*libfoo.so.x* file (in a set of known directories).\n\nThe last name, *libfoo.so.x*, e.g., `libfoo.so.3`\n\n, is called the\n*soname*, & contains the major version number. Thus, the infamous\n*soname bump* means an increment of that number, due to incompatible\nchanges in ABI. As soon as it happens, you're required to rebuild your\nprogram. On certain occasions you may cheat, symlinking `libfoo.so.3`\n\nto a new `libfoo.so.4`\n\n, but no user does that, your application just\nstops working after an OS update.\n\nHere is an example for `giflib`\n\npackage on Fedora. As usual, to\nirritate newcomers, the library is split between 2 packages:\n\n``` bash\n$ rpm -ql giflib giflib-devel | grep .so | xargs stat -c %N\n'/usr/lib64/libgif.so.7' -> 'libgif.so.7.1.0'\n'/usr/lib64/libgif.so.7.1.0'\n'/usr/lib64/libgif.so' -> 'libgif.so.7'\n\n$ objdump -p /usr/lib64/libgif.so.7.1.0 | grep -i soname\n  SONAME               libgif.so.7\n```\n\nThe program ld.so (the dynamic linker, at the time of writing\n`/lib64/ld-linux-x86-64.so.2`\n\n) looks for `libgif.so.7`\n\n. A linker,\nduring a build step, uses `libgif.so`\n\n(no version number). The latter\nfile is absent in the user-faced `giflib`\n\npackage.\n\n### Living on the edge\n\nIf you listen to proponents of assorted form of containerisation long\nenough, you may think that soname bumps (or other events of ruinous\nnature) happen every week. In reality, even rather old versions of\nfairly complex programs like Google Chrome, despite being designed to\nbe updated daily, run ~fine on current Linux.\n\nE.g., I fished out a .deb variant of Chrome 71 (Dec of 2018), unpacked\nit & successfully ran the ancient browser on Fedora 44. It\nwas linked against GTK3 (modern Chrome doesn't use GTK), a toolkit\nthat any distro will continue to ship for at least 20 years.\n\nNot all applications are that lucky, though. If you have a program\ncompiled during the forgotten v1.0.x OpenSSL years, chances that a\nregular user would have skills to find (or more precisely, bother to\nlook for, unless extremely motivated) a compatible .so file is\npractically nil.\n\nHere comes a point, where 2 schools of thought appear:\n\nYou rely on dependencies target distros provide, rebuilding the\nprogram when the time comes. The movement attracts too few\nadherents nowadays, & we won't talk about it, for this\nphilosophical tradition is considered passé in high society.\n\nYou protect yourself from the disappearance of old shared libraries\nfrom the repos by shipping the most fragile of them with the\nprogram. How do you know which ones should be brought in? You\ndon't, & resort to guessing.\n\n### AppImage\n\nIn the Windows world you can put necessary DLLs in the same directory\nwith the executable. In Linux, by default, the dynamic linker searches\nfor .so files exclusively in a predefined set of directories,\nreconfigurable only by a superuser.\n\nA temporal (meaning, it has a long life) fix for changing the\ndirectory lookup is to invoke ld.so manually, or set `LD_LIBRARY_PATH`\n\nenv variable. This requires a tiny shell wrapper for a program\nexecutable, & this is what every developer starts with when he thinks\nabout the chilling *software distribution on Linux*.\n\nWhat do you do after putting everything under 1 directory? In the\ndistant past, folks would create a .tar.gz file with a content like\nso:\n\n```\nfoobar/ # many files in this directory\nfoobar.sh\nREADME\n```\n\nThe user would unpack the archive, ignore the readme, & run `foobar.sh`\n\n.\n\nBy today's standards this is regarded as too confusing. First of all,\nwhat is this .tar.gz, a Japanese nesting doll? Second, a user vainly\ntries to start `foobar.sh`\n\nfrom within a GUI archiver app.\n\nIn 2010s, there was a popular feeling that \"something has to be done\"\nabout motley tarballs, hence a bunch of solutions appeared. One of the\nfew survivors is AppImage, even though back in ~2016, when it hit the\nLinux scene hard, many decided it was [\"unnecessarily complicated\"](https://news.ycombinator.com/item?id=11187622).\n\nI don't find it complicated at all. To show how it works, we'll ①\nwrite a hello world program that acquires the word \"World\" from a\nshared library, ② make an \"appimage\" from the program.\n\nOur source code:\n\n``` bash\n$ cat libfoo.c\nchar* greeting() { return \"World\"; }\n$ cat app.c\n#include <stdio.h>\nchar* greeting();\nint main() { printf(\"Hello, %s!\\n\", greeting()); }\n```\n\nWe [compile it](appimage-shared-lib/app.mk) like so:\n\n```\ncc -fPIC   -c -o libfoo.o libfoo.c\ncc -shared -Wl,-soname,libfoo.so.1 -o libfoo.so.1.0.0 libfoo.o\nln -sfn libfoo.so.1.0.0 libfoo.so.1\nln -sfn libfoo.so.1 libfoo.so\ncc   -L. -lfoo  app.c   -o app\n```\n\nThe app consists of an executable & a shared library with soname\n`libfoo.so.1`\n\n:\n\n``` php\n$ stat -c %N *.so* app\n'libfoo.so' -> 'libfoo.so.1'\n'libfoo.so.1' -> 'libfoo.so.1.0.0'\n'libfoo.so.1.0.0'\n'app'\n```\n\nIf we run it as is, it predictably fails:\n\n```\n$ ./app\n./app: error while loading shared libraries: libfoo.so.1: cannot open shared object file: No such file or directory\n```\n\nSo we write a wrapper:\n\n``` bash\n$ cat AppRun\n#!/bin/sh\n\ndir=$(dirname \"$(readlink -f \"$0\")\")\nexport LD_LIBRARY_PATH=$dir\nexec \"$dir/app\" \"$@\"\n```\n\nAn appimage is a squashfs image with a prepended \"runtime\". The latter\nis a [static ELF executable](https://github.com/AppImage/type2-runtime/releases) that mounts its payload (the squashfs\nimage) & runs a program with a hard-coded name \"AppRun\". At no point\nroot is required.\n\nYou make a ready-to-go runnable \"appimage\" in 3 steps:\n\nCreate a squashfs image `app.sqsh`\n\n:\n\n``` bash\n $ mksquashfs app AppRun libfoo.so.* app.sqsh -quiet -no-progress\n $ unsquashfs -l app.sqsh\n squashfs-root\n squashfs-root/AppRun\n squashfs-root/app\n squashfs-root/libfoo.so.1\n squashfs-root/libfoo.so.1.0.0\n```\n\nCatenate the \"runtime\" with it:\n\n``` bash\n $ cat type2-runtime app.sqsh > app.appimage\n```\n\nAdd the executable bit:\n\n``` bash\n $ chmod +x app.appimage\n```\n\nThat's it.\n\n```\n$ ./app.appimage\nHello, World!\n```\n\nObviously, the .appimage file extension isn't strictly required. You\ncan use .exe if you're so maliciously inclined.\n\nGUI archivers won't show the content of an appimage, therefore\nbewildered users cannot start `AppRun`\n\nfrom within the archiver.\n\nThe general disadvantages of the approach are:\n\n- a downloaded .appimage from the web won't have an executable bit;\n- it requires a suid\n`fusermount`\n\nutility on the host;\n- there is no requirements for sandboxing of any kind: by downloading\nan .appimage you can't know without extracting its payload whether\nthe author thought about one;\n- no protection from missing dependencies: if you didn't put a\nnecessary library inside the image, you're in the same boat with\nthe suppliers of a mere tarball.\n\nStill, for me it's the je ne sais quoi that makes the AppImage format\ncharming. The construct is so simple to implement, that you can\nreplace the static runtime binary with a 22-lines shell script:\n\n``` bash\n$ cat type2-lunch.sh\n#!/bin/sh\n\nset -e\nself=`readlink -f \"$0\"`\noffset=$(grep -abo -m1 \"$(printf 'hsqs\\002')\" \"$self\" | cut -d: -f1)\n\n[ \"$offset\" ] || { echo No squashfs image attached 1>&2; exit 1; }\n[ \"$PRINT_OFFSET\" ] && { echo \"$offset\"; exit 0; }\n\ntmp=`mktemp -d /tmp/appimage.XXXXXX`\n\nclean() {\n    set +e\n    fusermount -u \"$tmp\"\n    rmdir \"$tmp\"\n}\n\ntrap clean 0 1 2 15\n\nsquashfuse_ll -o offset=\"$offset\" \"$self\" \"$tmp\"\n\"$tmp\"/AppRun \"$@\"\nexit $?\n```\n\nIt looks for a magic number position & uses it as an offset when\nmounting an image. The step #2 from above thus can be replaced as:\n\n``` bash\n$ cat type2-lunch.sh app.sqsh > app.appimage\n```\n\n## Not a container\n\nUnfortunately, DLLs are not the only dependencies you'll\nencounter. GTK programs, for example, won't function properly or even\nstart without compiled GSettings schemas, MIME database caches, icon\nsets, & so on.\n\nThis is where the proponents of containerisation smile very widely: you\ncan copy beloved .so files all day long, but when any of them have a\nhard-coded path to `/lib64/gdk-pixbuf-2.0/2.10.0/loaders.cache`\n\nfile,\nall your backbreaking work becomes useless if a target host lacks one.\n\nWithout resorting to heavyweight Docker-like hammers, you can employ\n*mount namespaces*, a feature that has been in Linux > 20 years, but\nonly gained momentum among regular folks since the rise of\nFlatpak. One of the core components of the latter is a sandboxing\nutility called [bwrap](https://manpages.debian.org/unstable/bubblewrap/bwrap.1.en.html). From a user perspective, it does a glorified\nchroot(2), but without the need of superuser privileges. bwrap is\nshipped by default in all desktop variations of 3 major distros. You don't need to\ntouch or rely on Flatpak to use it.\n\nIf we [make a minimal directory tree](appimage-shared-lib/bwrap.mk)\nfor the hello world program above (this includes the dynamic linker\ntoo)\n\n``` php\ncontainer\n├── bin -> usr/bin\n├── lib64 -> usr/lib64\n└── usr\n    ├── bin\n    │   └── app\n    └── lib64\n        ├── ld-linux-x86-64.so.2\n        ├── libc.so.6\n        ├── libfoo.so -> libfoo.so.1\n        ├── libfoo.so.1 -> libfoo.so.1.0.0\n        └── libfoo.so.1.0.0\n```\n\nthen we can run our \"app\" executable without additional wrappers that\nexport `LD_LIBRARY_PATH`\n\n:\n\n``` bash\n$ bwrap --bind container / /bin/app\nHello, World!\n```\n\nHow far you can go with that? To prove to myself that this works not\nonly for toy programs, I employed Fedora's dnf repos to fetch all the\ndependencies for Celluloid (an mpv frontend) in such a way, that a\nresulting \"guest\" included the necessary libraries for hardware video\ndecoding & pipewire/pulseaudio/alsa communication with the host.\n\nIt worked, although the size of such a \"guest\" failed to inspire:\n\n``` bash\n$ du -hs container/\n993M    container/\n```\n\nIf you zip it, it contracts to 358 MB. How does this compare to a\nFlatpak version?\n\nAlthough the advantage of Flatpak here is in decoupling of what they\ncall \"runtime\" from an application itself, I'm not a huge fan of it\nfor the [amount of bloat](https://lwn.net/Articles/1020571/) it brings\ninto a Linux desktop mess.\n\nAnyhow, the fake container approach could be a solution if you don't\ncare about disk space. The [github example](https://github.com/gromnitsky/not-a-container) with which I did the mpv\nexperiment, contains several spec examples for more lightweight\nprograms.\n\nFor the sake of pedantry, I also tried the 2026 celluloid \"container\"\non a Debian 9.0 (2017) VM with its 4.9.0 kernel. I had to compile\nbwrap myself & set sysctl `kernel.unprivileged_userns_clone`\n\nto 1, but\nthe thing worked flawlessly. No rational user, of course, will ever do\nthat for any application, but it's the principle that counts.\n\n``` bash\n$ f=google-chrome-stable_71.0.3578.98-1_amd64.deb\n$ sha1sum $f | awk '{print $1}'\na4bffb66d9fe055a9baab366d4dd94c96ce47d24\n$ dpkg-deb -x $f .\n$ opt/*/*/google-chrome --no-sandbox --user-data-dir=`pwd`/1\n```\n\n- Ubuntu, Fedora, Debian.\n\nTags: [ойті](../../../t/da44800a6e351c6e94a3d3b6060d2be1.html)\n\nAuthors: [ag](../../../a/4e42f7dd43ecbfe104de58610557c5ba.html)", "url": "https://wpnews.pro/news/linux-apps-that-maybe-run", "canonical_source": "https://sigwait.org/~alex/blog/2026/06/13/qAp2Gu.html", "published_at": "2026-06-19 16:00:59+00:00", "updated_at": "2026-06-19 16:08:14.657509+00:00", "lang": "en", "topics": ["developer-tools"], "entities": ["Fedora", "GTK", "Qt", "giflib", "Linus Torvalds"], "alternates": {"html": "https://wpnews.pro/news/linux-apps-that-maybe-run", "markdown": "https://wpnews.pro/news/linux-apps-that-maybe-run.md", "text": "https://wpnews.pro/news/linux-apps-that-maybe-run.txt", "jsonld": "https://wpnews.pro/news/linux-apps-that-maybe-run.jsonld"}}