RPM Spec files conditionals and forcing package versions

Posted on Feb 8, 2024

This whole blog post was born from the idea to clarify how to nicely force a newer compiler on older distros. But of course we have to start with the basics.

RPM conditionals basics

RPM supports two main forms of conditionals %if and %ifarch. The last one is easy … it lets us do architecture specific things:

%ifarch x86_64 aarch64
[snip]
%endif

The %if is a bit more flexible. We can use it to compare strings.

%if "%_repository" == "openSUSE_Tumbleweed"
[snip]
%endif

First of all you notice that both end with %endif. Second if you want to ensure that RPM treats the 2 parts as a string you have to wrap them in ".

Last but not least we can use RPMs conditionals to compare numbers for us.

%if %{suse_version} >= 1600
[snip]
%endif

You can find the value for things like suse_version, fedora_version/fedora in the project config of the corresponding project. Though accessing values like this can lead to errors when trying to build for multiple distributions. Luckily RPM has a solution for this. Similar to bash we can expand a variable only when it is actually defined. The syntax for this is %{?suse_version}. We can combine this with a default value 0%{?suse_version}. If the variable is not set we will get a value of 0, which is then handled as false. If it is set we get e.g. 01600 which then is casted to the integer 1600 by rpm. Non zero values are treated as true.

%if 0%{?suse_version}
# do something suse specific
%endif
%if 0%{?fedora_version}
# fedora specific thing
%endif

One last important bit applies to less than conditionals

%if 0%{?suse_version} < 1600
# This would also be true on every non suse distro
%endif

The correct solution is:

%if 0%{?suse_version} && 0%{?suse_version} < 1600
# first check if suse_version is non zero.
%endif

Recent additions to the whole conditionals

Assuming we have a spec file with

BuildRequires: gimp

That allows us to do:

%if %{pkg_vcmp gimp >= 2.99}
%define gimp_suffix 3
%global _gimpplugindir %(gimptool-2.99 --gimpplugindir)/plug-ins/
%else
%global _gimpplugindir %(gimptool-2.0 --gimpplugindir)/plug-ins/
%endif

BuildRequires:  (pkgconfig(gimp-2.0) if gimp < 2.99)
BuildRequires:  (pkgconfig(gimp-3.0) if gimp >= 2.99)

The %{pkg_vcmp pkgname >= value} allows us to do things based on the version of packages in the build environment. The (...) is an example for boolean/rich dependencies. Those can not just be used in BuildRequires, but also in Requires and so on. See the link above.

Responsible usage

Recently I reviewed a package and ran into this construct.

%if 0%{?sle_version} && 0%{?sle_version} < 160000
BuildRequires:  gcc13
BuildRequires:  gcc13-c++
%endif

[snip]

%build
%if 0%{?sle_version} && 0%{?sle_version} < 160000
export CC=gcc-13
%endif

Looks ok to begin with, but there are a few issues.

  1. We are repeating a complex conditional.
  2. The conditional is not limiting on what it wants to be responsible for. That means another packager can come along and use the same conditional as it fits their use case too. If we later want to clean up the newer GCC we might cleanup the 2nd addition as well even though they are unrelated.

How can we do this better?

%if 0%{?sle_version} && 0%{?sle_version} < 160000
%define needs_newer_gcc_on_sle15 1
%endif

%if 0%{?needs_newer_gcc_on_sle15}
BuildRequires:  gcc13
BuildRequires:  gcc13-c++
%endif

[snip]

%build
%if 0%{?needs_newer_gcc_on_sle15}
export CC=gcc-13
%endif

Our conditinal is descriptive and we have the conditional only once. If we wanted a slightly more elegant solution:

%if 0%{?sle_version} && 0%{?sle_version} < 160000
%define force_gcc_version 13
%endif

BuildRequires:  gcc%{?force_gcc_version}
BuildRequires:  gcc%{?force_gcc_version}-c++

[snip]

%build
%if 0%{?force_gcc_version}
export CC="gcc-%{?force_gcc_version}"
export CXX="g++-%{?force_gcc_version}"
%endif

Sources List and Patches

You should not have SourceXY or PatchXY lines guarded by conditionals in the preamble. This would break rebuilding the source package for other distros. You want to make the usage of the sources or patches later on optional. You can even do this with %autosetup and %autopatch.

With a list of patches like this:

Patch1:    foo.patch
Patch2:    bar.patch
Patch3:    foobar.patch

You would do:

# do not automatically apply patches
%autosetup -p1 -N
%autopatch -p1 -m 1 -M 2
%if 0%{?suse_version} && 0%{?suse_version} < 1600
%autopatch -p1 -m 3
%endif

For more details see the official documentation.

Last but not least the old %patchXY syntax seems to be deprecated in future rpm versions in favor of %patch XY. Though my recommendation is: if possible migrate to %autosetup and %autopatch.

bcond_with(out)

One last note: If you want to be able to turn off a feature, even a conditional one from the osc build/rpmbuild command line, do not use %define or %global in your conditional.

%if 0%{?suse_version} >= 1699
%define fancy_feature 1
%endif
[snip]
%if 0%{?fancy_feature}
...
%endif

This becomes

%if 0%{?suse_version} >= 1699
%bcond_without fancy_feature
%else
%bcond_with    fancy_feature
%endif
[snip]
%if %{with fancy_feature}
...
%endif

If you build against Tumbleweed or ALP with plain osc build openSUSE_Tumbleweed the feature will be turned on. If you want to test later if a Leap release can support this feature now, you can do osc build --with=fancy_feature 15.6. Or turn it off on Tumbleweed for testing.

E.g. in the ruby spec files you will find

%bcond_without build_docs
%bcond_without run_tests

So if I am in a hurry to just test a build fix: osc build --without=run_tests --without=build_docs

Important: %bcond_without means “Add an option –without but enable the feature by default”. %bcond_with means “Add an option –with but disable it by default”.

Since rpm 4.17 conditional builds got a few more tricks up its sleeves.

%if 0%{?suse_version} >= 1699
%bcond fancy_feature 1
%else
%bcond fancy_feature 0
%endif
[snip]
%if %{with fancy_feature}
...
%endif

And we can shorten this even more

%bcond fancy_feature 0%{?suse_version} >= 1699

[snip]
%if %{with fancy_feature}
...
%endif

You can define defaults for other %bcond statements based on other bcond conditionals.

%bcond gnutls 1
%bcond openssl %{without gnutls}
%bcond extra_tests %[%{with gnutls} && %{with sqlite}]

For all the details see the upstream documentation.