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.