Upgrade clang on old linux distributions

Introduction

If one is stuck on an old linux distribution, but needs a newer compiler, one possible solution is to compile clang manually. Note that clang is shipped with llvm, so we are going to build llvm.

Also, building software on an old linux distribution is one of the way of creating portable software that can run on a variety of target systems. Your binary compiled on debian 8 will run on any system that has glibc >= 2.19, like:

In this post, we'll assume the target distribution is debian 8.0, which reached EOL on June 30, 2020. One can adapt the following instruction to older or different distributions. If one is on macOS, macports is a better alternative, as it's easier and faster than manually compiling clang.

Prerequisites

The target distribution ships with gcc 4.9.2, and llvm 7.1.0 is the latest version that can be compiled with that compiler. Per the documentation, this is the required software for building it:

Also, we'll use ninja instead of gnu make to speed up the build.

So, we'll install the basic packages needed for building software:

apt-get install build-essential ninja-build python2.7

Upgrade cmake

Llvm 7.1.0 requires cmake >= 3.4.3, unfortunately debian 8 ships with cmake 3.0.2. So we need to upgrade cmake first.

We'll use $HOME/llvm as a working directory:

mkdir -p ~/llvm
cd ~/llvm

Then we download cmake's source code:

# If using curl
curl -LO 'https://github.com/Kitware/CMake/releases/download/v3.23.3/cmake-3.23.3.tar.gz'

# If using wget
wget 'https://github.com/Kitware/CMake/releases/download/v3.23.3/cmake-3.23.3.tar.gz'

And extract it:

tar xf cmake-3.23.3.tar.gz
cd cmake-3.23.3

We are going to disable the use of openssl in our custom cmake build, because we don't need fetch_content and our target distribution ships with an outdated openssl anyway. So the build instructions are:

./bootstrap --parallel=$(nproc) --prefix=/usr/local -- -DCMAKE_USE_OPENSSL=OFF
make -j$(nproc)
sudo make install
hash -r

Finally, clean the build directory because we don't need it anymore:

cd ~/llvm
rm -r cmake-3.23.3

Acquire source tarballs

Go to the download page for the target version, and download: LLVM source code, Clang source code, compiler-rt source code, libc++ source code, libc++abi source code, LLD Source code. We'll put everything in our working directory:

cd ~/llvm

# If using curl
curl -LO 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/llvm-7.1.0.src.tar.xz'
curl -LO 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/cfe-7.1.0.src.tar.xz'
curl -LO 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/clang-tools-extra-7.1.0.src.tar.xz'
curl -LO 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/compiler-rt-7.1.0.src.tar.xz'
curl -LO 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/libcxx-7.1.0.src.tar.xz'
curl -LO 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/libcxxabi-7.1.0.src.tar.xz'
curl -LO 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/lld-7.1.0.src.tar.xz'

# If using wget
wget 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/llvm-7.1.0.src.tar.xz'
wget 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/cfe-7.1.0.src.tar.xz'
wget 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/clang-tools-extra-7.1.0.src.tar.xz'
wget 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/compiler-rt-7.1.0.src.tar.xz'
wget 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/libcxx-7.1.0.src.tar.xz'
wget 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/libcxxabi-7.1.0.src.tar.xz'
wget 'https://github.com/llvm/llvm-project/releases/download/llvmorg-7.1.0/lld-7.1.0.src.tar.xz'

Extract sources

This is straightforward, we just extract each tarball to the current directory:

for i in *.tar.xz; do tar xf "$i"; done

Move sources into place

Llvm require each of its components to live in specific parts of the source tree, so it can detect them at build time and build them. So invoke the following commands to move them into place (adapt to your specific llvm version if needed):

mv cfe-7.1.0.src                llvm-7.1.0.src/tools/clang
mv clang-tools-extra-7.1.0.src  llvm-7.1.0.src/tools/clang/tools/extra
mv compiler-rt-7.1.0.src        llvm-7.1.0.src/projects/compiler-rt
mv libcxx-7.1.0.src             llvm-7.1.0.src/projects/libcxx
mv libcxxabi-7.1.0.src          llvm-7.1.0.src/projects/libcxxabi
mv lld-7.1.0.src                llvm-7.1.0.src/tools/lld

Build llvm

If the following, we are building llvm in Release mode, into /usr/local, using ./Build as a build directory, using ninja as a build system, and we're going to disable building documentation, examples, and tests:

cd ~/llvm/llvm-7.1.0.src

cmake \
    -DCMAKE_BUILD_TYPE=Release \
    -DCMAKE_INSTALL_PREFIX=/usr/local \
    -B Build \
    -G Ninja \
    -DLLVM_INCLUDE_DOCS=OFF \
    -DLLVM_INCLUDE_EXAMPLES=OFF \
    -DLLVM_INCLUDE_TESTS=OFF \
    .
cmake --build Build
sudo cmake --build Build --target install
hash -r

If all goes well, llvm and clang should be installed into /usr/local. We can check that with the following commands:

llvm-config --version
clang --version

We can also try compiling a small executable with clang to test whether it works fine:

printf "#include<stdio.h>\nint main() {\nprintf(\"It works\\\\n\");\nreturn 0;\n}\n" > /tmp/hello.c
clang -o /tmp/hello /tmp/hello.c
/tmp/hello
rm /tmp/hello{,.c}

If the above printed It works, then the llvm installation has succeeded.

But wait! there's more. We can also check whether llvm's C++ support was correctly installed and is working fine for compiling C++17 code:

cat <<EOF>/tmp/hello17.cpp
#include <iostream>
int main() { std::cout << u8"C++17 works\n"; return 0; }
EOF
clang++ -o /tmp/hello17 -std=gnu++17 -stdlib=libc++ /tmp/hello17.cpp
/tmp/hello17
rm /tmp/hello17{,.cpp}

If the above printed C++17 works then the clang++ installation is functional.

Finally, we can clean our working directory because it's not needed anymore:

cd ~
rm -r ~/llvm