Featured image for coding topics.

With the addition of the %toolchain macro to the redhat-rpm-config package, packages can easily switch between the GNU Compiler Collection (GCC) and the Clang compiler. This package change is not yet supported by Fedora, and package maintainers need good reasons to switch from the GCC default to Clang. Maintainers also need to watch out for a few nuances to make (and keep) a package specification file buildable with both toolchains.

This article looks at the necessary changes and best practices to allow a spec file to build with both GCC and Clang in a variety of cases.

Preparing a spec file

In the most basic case, nothing needs to be done. The specification file will automatically pick up whatever compiler the buildroot defines as the default. You can check the default by looking at the value of the %toolchain macro in the %build section:

%build
echo "Toolchain is %toolchain"

Similarly, rpm --eval "%toolchain" will print the value of the %toolchain macro. Setting the macro to either "gcc" or "clang" will select the toolchain for the given spec file. Setting the macro explicitly is useful if the package needs to be built with a specific toolchain:

%global toolchain clang

For testing, it might make sense to add a conditional variable to the spec file, so it can be easily built with either toolchain:

%bcond_with toolchain_clang

%if %{with toolchain_clang}
%global toolchain clang
%else
%global toolchain gcc
%endif

Adding the conditional variable should suffice for the minimal setup. In some cases, you might need to make additional changes such as adjusting the BuildRequires. This is because the %toolchain macro does not automatically add the appropriate toolchain BuildRequires. With this setup, building a simple RPM via rpmbuild --with=toolchain_clang will select the Clang toolchain while not passing anything or passing --with=gcc will select GCC.

The value of the %toolchain macro will later determine the value of the various environment variables, as well as what build flags will be passed to the build system. So this macro should be set as early as possible in the file.

Build systems

Most C and C++ software projects use a popular build system such as Autotools, CMake, or Meson. These build systems usually try to follow standard practices such as respecting common environment variables, e.g., CC, CXX, or CFLAGS. They are readily supported by RPM via their respective macros, such as %cmake for CMake, %configure for Autotools, and %meson for Meson.

After setting those macros, use %make_build to actually compile the project. This macro will run make and pass a few flags for parallel builds. There are special macros to let CMake and Meson build the application, in case the build uses Ninja instead of make: %cmake_build and %meson_build.

Just using these macros works for the simplest projects. Some projects, however, do not use any of the common build systems and require special care.

Hand-written makefiles

Hand-written makefiles are still relatively common, especially in smaller software projects. Unfortunately, makefiles do not usually respect the environment variables mentioned earlier. This makes it impossible for distributions to inject their hardening C flags or change the compiler in use.

When using hand-written makefiles in a Fedora RPM spec file, the %make_build macro works as well. However, that does not define the CFLAGS environment variable and others. %set_build_flags can be used to set those flags in the makefile:

%build
%set_build_flags
%make_build

%set_build_flags also sets the CC environment variable, so if the compiler needs to be invoked explicitly, $CC should be used instead of hard-coding gcc or clang, or even /usr/bin/cc.

If the provided makefile respects the standard environment variables, you are all set with this approach. If it doesn't, it usually needs to be patched, or the spec file needs to use another environment variable, for example EXTRA_CFLAGS:

%build
%set_build_flags
%make_build EXTRA_CFLAGS="$CFLAGS"

Custom build setups

This is more of a worst-case scenario for packagers, but many projects still have custom build setups (for example, a shell script that compiles the source code) or no build setup at all.

If the project does not have any build setup, it's easiest to simply use %set_build_flags again and compile using $CC:

%build
%set_build_flags

# Compile main library
$CC lib.c -c -o lib.o $CFLAGS $LDFLAGS

However, if the project uses a custom shell script (or something similar), you're out of luck again. Using %set_build_flags is still a good idea, but you have to inspect the build script to find out what environment variables have to be set or what command-line parameters have to be passed.

Toolchain-specific changes

One common problem that spec files run into is that they want to add (or override) flags from the CFLAGS variable to keep a working build. That is of course easy to do:

%build
%set_build_flags
CFLAGS="$CFLAGS -fno-some-flag"

In this case, we assume that a new version of the compiler in use has introduced a new flag that's enabled by default, -fsome-flag. This flag causes problems when the project in question is compiled, however, so the package maintainer has decided to disable it again by passing -fno-some-flag. When trying to build this package with a different compiler, it can happen that the build fails because the compiler does not know about -fsome-flag or -fno-some-flag. When -fno-some-flag needs to be passed only for a specific compiler, check the %toolchain macro for the compiler that requires the flag:

%build
%set_build_flags
%if "%toolchain" == "gcc"
  CFLAGS="$CFLAGS -fno-some-flag"
%endif

As implemented in redhat-rpm-macros, the %toolchain macro always evaluates to either "gcc" or "clang." Note that one cannot generally check $CC the same way, because that variable might point to an obscure binary in cross-compilation scenarios.

Link-time optimization with Clang

Link-time optimization (LTO) is enabled by default on Fedora, which means that the $CFLAGS variable (set by %set_build_flags) contains a variation of the -flto flag. The flags used by link-time optimization are saved in the %_lto_cflags variable. When porting a package to compile with the Clang toolchain, it is important to know that the link-time optimization flags have to be added to the $LDFLAGS variable as well. %set_build_flags will take care of that.

When the package needs some non-standard variable, the flags from %_lto_cflags need to be added manually. In the worst case, link-time optimization has to be disabled entirely. This change is often necessary when the project has some home-grown LTO setup that expects GCC. Disabling link-time optimization can be done by setting %_lto_cflags to %{nil}:

%global _lto_cflags %{nil}

Conclusion

Supporting both the GCC and Clang toolchains is easy if the package uses a standard build system and respects standard environment variables. There are still tons of special cases, which can often be handled by checking the %toolchain macro value and handling the problem depending on the toolchain in use.

Note that currently (as of Fedora 34) all packages in Fedora are built with GCC, but there is a proposal for changing this. See Fedora's Changes/CompilerPolicy page for details.

Good documentation for these matters is scarce, but I like to look at the macros defined in the RPM repository as well as the redhat-rpm-config repository. Looking at the output of rpm --showrc can also provide insight.

Comments