I think the best way to package an application on Linux is as a statically linked single executable.
However, not all libraries are suitable for static linking. For example, glibc. Even if you force glibc to be statically linked, some operations will still load dynamic libraries through dlopen, which means there will be two versions of glibc in the process’s address space.
So, sometimes you have to use dynamic linking, but only for very basic libraries like glibc. For everything else, it’s better to use static libraries.
Not all applications stick to only dynamically linking these basic libraries, though. Updating the build method for different kinds of applications can be inconvenient. When this happens, I usually follow what Python C extensions do.
When distributing Python wheels, we typically use auditwheel to copy the necessary dynamic libraries into a separate folder and then use patchelf to update the RPATH.
Another common issue is glibc compatibility. If you build an application on a system with a newer version of glibc, it will link to the newer symbols, which means it won’t run on systems with an older version of glibc. The solution is pretty straightforward: just build the application on an older system, like Ubuntu 16.04.
There are other solutions for distributing applications, such as AppImage and Docker images.
However, none of them can solve the situation when an application contains a shared library (.so) that needs to be preloaded, such as heaptrack.
All of these solutions also have their own drawbacks.
For example, Docker images require a Docker environment, and when applications need to access files in the host OS, users must mount the files to the Docker container, which is very inconvenient.
(The Docker environment requirement can be eliminated - there’s a project called dockerc that can convert a Docker image to a single executable file. It still uses container technology but is more convenient than installing a Docker environment because it’s battery-included.)
AppImage is better than Docker, but it requires FUSE support, meaning the runtime environment needs libfuse installed (this can be solved by using a statically-linked AppImage runtime).