Creating a portable single-executable in C/C++ on Linux is not particularly easy.
The dominant libc for Linux, glibc, says "static linking is highly discouraged", see [1] among many other sources for the subtle problems it causes.
There are solutions like [2], [3], and [4] but they all have downsides and don't compare at all to e.g., the go (or even rust) single-executable story.
[2] Link glibc anyway and try to avoid the pitfalls.
[3] Link glibc dynamically but everything else static, and build your exe using an ancient toolchain (or better, a new toolchain built for an ancient libc) so that it works on most distros.
[4] Give up on glibc and statically link an alternative libc like musl.
Or just dynamic link to libc and static link the rest as much as you can. If a binary dynamically links to libc, but is otherwise statically linked, all good no?
Yes, that was option [3]. The problem is that glibc symbols are versioned and update fairly regularly, so your binary likely won't run on anything with a much older glibc, and glibc versions vary a lot across distros (e.g., some are much slower moving so use very old glibc).
To work around this you need to try to build on an old of a distro as possible, so you use only old symbols... but you don't want to build on an ancient compiler since it might not support the language version or compiler flags you need, so you first build a new old toolchain on the old distro against the old libc, or find a docker container from someone who has already gone though this pain.
Of course, this also means you can't use new libc features: libc is more than just the C runtime, it's also the shim layer between userspace and the kernel, so you might miss out on newer system calls (or have to write the wrappers manually, etc).
The major library version number (e.g., 5 vs 6) isn't enough: the symbols even within lib.so.6 are versions and some may update every glibc version, so in general you may have problems building against a new libc 6 then running on a system with an old libc 6 (you'll get a runtime error from the dynamic loader complaining a missing versioned symbol).
Rust links glibc dynamically, and can use musl for some platforms. By default most (all?) of the musl targets are linked statically, but they should be able to use dynamic linking for it as well.
The good news is that there has been a long time since glibc had breaking changes, and the formal policy of the kernel reflects on it and makes future breaking changes unlikely.
It depends what you mean by breaking changes, right? Do you mean they try to avoid breaking ABI in a forwards compatible way? That is, building against new glibc then runing against old glibc should work?
I don't think so: they only make it ABI backwards compatible (compile against old glibc, run against new) and API compatible (recompile your source against different versions should work).
I just checked by glibc and it had > 200 symbols versioned 2.34 which is the current glibc version. If you use any of those you can't run against older glibc.
> Do you mean they try to avoid breaking ABI in a forwards compatible way?
Ok, I can see what I have written that could read this way, but the context is completely about backward compatibility. Forward compatibility isn't a concept that appears often in software.
It's not about how you wrote it, it's that the "single executable" concept needs both directions of compatibility if you dynamically link libc on any old distro.
E.g., you might use a relatively recent distro in your GitHub action (or whatever) to build this binary, but then anyone running CentOS or whatever can't run it because they ship an older libc version and required symbols aren't available.
If libc is backwards compatible, you immediately have a compatibility matrix that says where your code will run. Quite like you have for any kind of release on any environment.
You seem to want some way to have your code run on any platform whatever features you use in it. That you can't have. Of course if you want to support CentOS 6, you have to avoid any new symbol.
The problem isn't new symbols, it's that existing symbols can get the new version depending on what version you compile it against. On my glibc ~200 symbols have the newest version (2.34) but these are largely all methods that have been around forever. A binary built here won't run in most places.
The dominant libc for Linux, glibc, says "static linking is highly discouraged", see [1] among many other sources for the subtle problems it causes.
There are solutions like [2], [3], and [4] but they all have downsides and don't compare at all to e.g., the go (or even rust) single-executable story.
---
[1] https://stackoverflow.com/questions/57476533/why-is-statical...
[2] Link glibc anyway and try to avoid the pitfalls.
[3] Link glibc dynamically but everything else static, and build your exe using an ancient toolchain (or better, a new toolchain built for an ancient libc) so that it works on most distros.
[4] Give up on glibc and statically link an alternative libc like musl.