Is it possible to build a C++ binary on Ubuntu 25.10 and run it on 22.04? Find out!
Find a file
2026-02-13 13:48:13 +01:00
.gitignore initial commit 2026-02-12 21:58:21 +01:00
CMakeLists.txt tabs... oops 2026-02-13 13:40:56 +01:00
hello.cpp initial commit 2026-02-12 21:58:21 +01:00
README.md mdash 2026-02-13 13:48:13 +01:00

Hello World

Running a C++ program compiled on Ubuntu 25.10 with features from C++23 on an older distribution (Ubuntu 22.04)

The program

It's nothing fancy, just a "Hello, world" with std::println.

#include <print>

int main()
{
    std::println("Hello, world");
}

The CMakeLists.txt

cmake_minimum_required(VERSION 3.10)

project(hello LANGUAGES CXX)

set(CMAKE_CXX_STANDARD 23)
set(CMAKE_BUILD_TYPE Release)

add_executable(hello
    "hello.cpp"
)

First attempt

We build and run it on Ubuntu 25.10:

jakob@ubuntu-25-10:/.../hello/build$ cmake ..
-- The CXX compiler identification is GNU 15.2.0
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done (0.2s)
-- Generating done (0.0s)
-- Build files have been written to: /.../hello/build
jakob@ubuntu-25-10:/.../hello/build$ make
[ 50%] Building CXX object CMakeFiles/hello.dir/hello.cpp.o
[100%] Linking CXX executable hello
[100%] Built target hello
jakob@ubuntu-25-10:/.../hello/build$ ./hello 
Hello, world

And then try to run it on Ubuntu 22.04:

jakob@ubuntu-22-04:/.../hello/build$ ./hello 
./hello: /lib/aarch64-linux-gnu/libstdc++.so.6: version `GLIBCXX_3.4.31' not found (required by ./hello)

As expected, it failed since the executable built on the newer OS was linked to a version of libstdc++ not found on the older distribution.

Fortunately, this problem can be solved.

Bundling libstdc++

We modify the CMakeLists file and add a function:

function(bundle_glibcxx target)
    if(NOT CMAKE_CXX_COMPILER_ID STREQUAL "GNU")
        message(FATAL_ERROR "bundle_glibcxx() requires g++ (GNU C++ compiler).")
    endif()

    execute_process(
        COMMAND ${CMAKE_CXX_COMPILER} -print-file-name=libstdc++.so
        OUTPUT_VARIABLE LIBSTDCXX_PATH
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )

    if(NOT EXISTS "${LIBSTDCXX_PATH}")
        message(FATAL_ERROR "Could not locate libstdc++.so")
    endif()

    get_filename_component(LIBSTDCXX_REAL "${LIBSTDCXX_PATH}" REALPATH)
    get_filename_component(LIBSTDCXX_NAME "${LIBSTDCXX_REAL}" NAME)
    string(REGEX MATCH "libstdc\\+\\+\\.so\\.[0-9]+" LIBSTDCXX_SONAME "${LIBSTDCXX_NAME}")

    add_custom_command(TARGET ${target} POST_BUILD
        COMMAND ${CMAKE_COMMAND} -E copy_if_different
            "${LIBSTDCXX_REAL}"
            "$<TARGET_FILE_DIR:${target}>/${LIBSTDCXX_NAME}"
        COMMAND ${CMAKE_COMMAND} -E create_symlink
            "$<TARGET_FILE_DIR:${target}>/${LIBSTDCXX_NAME}"
            "$<TARGET_FILE_DIR:${target}>/${LIBSTDCXX_SONAME}"

        COMMENT "Bundling libstdc++ ..."
    )

    set_target_properties(${target} PROPERTIES
        BUILD_RPATH "$ORIGIN"
    )
endfunction()

This will find the libstdc++ that the executable is missing and bundle it so we can distribute it alongside the executable.

We build and run the executable:

$ ./hello
./hello: /lib/aarch64-linux-gnu/libc.so.6: version `GLIBC_2.38' not found (required by /.../hello/build/libstdc++.so.6)
./hello: /lib/aarch64-linux-gnu/libc.so.6: version `GLIBC_2.36' not found (required by /.../hello/build/libstdc++.so.6)

Unfortunately, libstdc++ was linked with a newer version of libc than is present on our system (2.35). This means we'll also have to bundle that.

Bundling libc

We find and copy libc just like we did libstdc++:

    execute_process(
        COMMAND ${CMAKE_CXX_COMPILER} -print-file-name=libc.so.6
        OUTPUT_VARIABLE LIBC_PATH
        OUTPUT_STRIP_TRAILING_WHITESPACE
    )

We run it. Moment of truth, and ...

$ ./hello
./hello: symbol lookup error: /.../hello/build/libc.so.6: undefined symbol: __tunable_is_initialized, version GLIBC_PRIVATE

we fail. It seems it's just impossible to build on a newer system and compile for an old one. Lesson learned. We have to find another way of targeting a wide range of distributions while using the latest C++ features.

Part 2

The solution

We can't achieve the original goal — running a hello world built on Ubuntu 25.10 on 22.04 — but we can refocus on a broader goal — figuring out how to build an executable so that it will run on as many distributions as possible.

It seems that we have to compile the program on the oldest distrubution we can find — preferable one that's still supported. I chose Debian 11 (released on August 14th, 2021, supported till August 31st, 2026).

We install CMake and gcc from the repositories and that's it, right? Wrong. The version of CMake and gcc in the Debian repositories is too old to support C++23. I don't know exactly why CMake needs to specifically add support for new standards but that's how it is.

Getting GCC and CMake

Luckily, CMake is distributed in binary form. Just download it and extract to an appropriate directory like /opt.

wget https://github.com/Kitware/CMake/releases/download/v4.2.3/cmake-4.2.3-linux-aarch64.tar.gz
tar -xvf cmake-4.2.3-linux-aarch64.tar 
sudo mv cmake-4.2.3-linux-aarch64 /opt/

We have no such privilege with gcc — we'll have to build it from source. We'll download it, extract the archive and follow the build procedure.

You have to specify where gcc will be installed before building it. This means it cannot be run from elsewhere which is a really bad design decision (if it even was a decision).

wget https://ftp.gwdg.de/pub/misc/gcc/releases/gcc-15.2.0/gcc-15.2.0.tar.xz
tar -xvf gcc-15.2.0.tar.xz
cd gcc-15.2.0
./contrib/download_prerequisites
mkdir build
cd build/
../configure --prefix=/opt/gcc-15 --disable-multilib --enable-languages=c,c++
time make -j8
sudo make install

Now we'll add both to PATH.

export PATH="/opt/gcc-15/bin:$PATH"
export PATH="/opt/cmake-4.2.3-linux-aarch64/bin:$PATH"

Building

Finally, it's time to build the executable. We will modify CMakeLists to remove the part where we were bundling libc. We still need to bundle libstdc++ for older distros (not older than Debian 11, of course, but old enough for their libstdc++ to not support the C++23 we are using).

We buid the same as before.

mkdir build && cd build
cmake ..
make

Excluding the artefacts that CMake leaves behind, we have the following bundle:

$ ls
hello  libstdc++.so.6  libstdc++.so.6.0.34

Now we can verify that Hello world runs on as many distros as possible.

Testing

I used OrbStack on MacOS to set up as many Linux distributions (they make it very easy, just click a button). The vast majority of the distros worked without a problem. These are: Debian, Ubuntu, Fedora, OpenSUSE, CentOS, Arch, Gentoo, Kali, Rocky, Void, AlmaLinux, Oracle, and Devuan. You can find their outputs at the bottom.

Failures

The only distros I found where the executable couldn't run are NixOS (Unstable, I couldn't install 25.05 for some reason) and Alpine (version 3.20).

I expected something like this from NixOS since I've used it in the past, but I know nothing about Alpine. LDD said everything was fine on NixOS but the executable still wouldn't run. On Alpine, LDD said it couldn't find libgcc_s.so.1 and some symbols in libstdc++.

Now I know why (future Jakob here) — it's because Alpine doesn't use glibc — it uses musl libc. I guess this can be an advantage as an install of Alpine is ~35 MB compared to Ubuntu's ~800 MB, but it isn't good for portability.

Conclusion

There are many distributions of Linux and no-one guarantees executables built on one distro will be compatible with another. As we have seen, it's possible to compile an executable on one distro and have it run on many others, but process of using the oldest possible distribution to link against on old version of glibc and compiling gcc to be able to use the latest C++ standard is ridiculous.

Bundling libstdc++ is inevitable (you have to do it on Winows too) but there should be a way to target an old version of glibc on the latest Ubuntu/Fedora/OpenSUSE/Kali/Gentoo/... distributon. We shouldn't have to resort to wasting time on setting up environments when we could spend that time developing software.

If I were compiling a more complex program than a hello world, who knows if this process would work. I might have to try that some time.

Appendix

jakob@debian:/.../hello/build$ ./hello 
Hello, world
jakob@ubuntu-22-04:/.../hello/build$ ./hello 
Hello, world
jakob@ubuntu-25-10:/.../hello/build$ ./hello 
Hello, world
[jakob@fedora-41 build]$ ./hello 
Hello, world
jakob@opensuse-tumbleweed:/.../hello/build> ./hello 
Hello, world
[jakob@centos-10 build]$ ./hello 
Hello, world
[jakob@arch build]$ ./hello 
Hello, world
jakob@gentoo-latest /.../hello/build $ ./hello 
Hello, world
jakob@kali-latest:/.../hello/build$ ./hello 
Hello, world
[jakob@rocky-9 build]$ ./hello 
Hello, world
[jakob@void-latest build]$ ./hello 
Hello, world
[jakob@alma-8 build]$ ./hello 
Hello, world
[jakob@oracle-linux-8 build]$ ./hello 
Hello, world
jakob@devuan:/.../hello/build$ ./hello 
Hello, world
[jakob@nixos-unstable:/.../hello/build]$ ./hello
-bash: ./hello: cannot execute: required file not found
alpine-3-20:/.../hello/build$ ./hello 
-sh: ./hello: not found