RPM Spec files conditionals and forcing package versions
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.
- We are repeating a complex conditional.
- 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.