Declarative RPM: Cleaning Up Your Spec Files
This article was written by Marcus Rueckert, Build Service Engineer at SUSE. This article originally appeared on the ‘Nordisch by Nature‘ blog under the same title and has been slightly updated for the suse.com blog.
The End of Spec File Sprawl? Enter Declarative RPM
For decades, the RPM spec file has been the “Swiss Army Knife” of Linux packaging—powerful, but often cluttered with boilerplate shell scripts and repetitive macros. As developers, we’ve watched the rise of purely functional and declarative systems like Nix with a bit of envy. We’ve asked ourselves: Why does a simple CMake project still require 50 lines of repetitive %setup, %build, and %install instructions?

The community has been listening. The push for a “cleaner” spec file isn’t just about aesthetics; it’s about reducing maintenance overhead and making packaging accessible to a new generation of developers.
Back in 2019, Florian Festi gave an insightful talk titled “Re-Thinking Spec Files,” where he shared some visionary ideas on how to simplify this process. Today, those ideas are becoming a reality. Since the release of RPM 4.20, we finally have native support for declarative builds.
Let’s dive in and see how this changes the game for openSUSE maintainers.
Exploring the Possibilities
One of the first packages in openSUSE to make the switch was gnome-text-editor. However, to keep things simple, we’ll use my converted minisign package as an example.
Name: minisign Version: 0.12 Release: 0 License: ISC Summary: A dead simple tool to sign files and verify signatures URL: https://jedisct1.github.io/minisign/ Group: Productivity/Networking/Security Source0: https://github.com/jedisct1/minisign/archive/%{version}.tar.gz BuildRequires: cmake BuildRequires: pkgconfig(libsodium) # This is where the magic happens BuildSystem: cmake BuildOption: -DCMAKE_STRIP:BOOL=OFF %description [snip] %files %doc README.md %license LICENSE %{_bindir}/%{name} %{_mandir}/man1/%{name}.1.*
Everything up to the
# This is where the magic happens
comment looks standard. But then we hit a new statement in the preamble: BuildSystem: cmake. By adding this, we instruct RPM to use CMake for the build process. The BuildOption: -DCMAKE_STRIP:BOOL=OFF line simply passes command-line options directly to the CMake build system. Aside from that, we just have the usual %description and %files sections.
You might be thinking, “It can’t be that short!”… but it actually is. And it builds perfectly.
How does it work?
To make this happen, we need to define specific macros for RPM for each BuildSystem we want to support. These macros tell RPM which other macros to call for each section:
%buildsystem_cmake_conf() %cmake %* %buildsystem_cmake_build() %cmake_declarative_build %* %buildsystem_cmake_install() %cmake_install %* %buildsystem_cmake_check() %ctest %* %buildsystem_cmake_generate_buildrequires() %{nil} %cmake_declarative_build \ cd %{__builddir} \ %cmake_build
The first thing you’ll notice is that the %cmake macro isn’t assigned to the %build section anymore. Instead, there’s a %conf section! (Who knew?) Because we skipped the explicit %prep section it automatically expands to:
%prep %autosetup -C -p1
Next is the %build section we all know and love. You’ll notice we aren’t using the standard %cmake_build macro here. We use an override called %cmake_declarative_build because the standard version assumes it is still inside the %{__builddir}. Since declarative builds move between sections, the current working directory resets to the source root, so we have to cd back in. Finally, we get our expected %cmake_install for the %install section, and %ctest for the %check section.
The coolest part? %buildsystem_cmake_generate_buildrequires()! Yes, RPM can now autogenerate BuildRequires! We can even hook it up automatically with another macro, though we don’t have a generator for CMake quite yet.
Customizing the Steps
One option to custo
mize the steps is the BuildOption we encountered above. However, you can specify BuildOption not only globally, but also per section:
BuildOption(prep): -p1 BuildOption(prep): -n %{archive_name} BuildOption(conf): -DCMAKE_STRIP:BOOL=OFF BuildOption(conf): -DENABLE_TOOLS:BOOL=ON
This is a pattern we already know from Requires(post|postun|pre|preun). In theory, you could put multiple command-line options in a single statement, but as we learned from BuildRequires, having them in one very long line is a PITA for merging. So, let’s split them up into one per line.
If you need to call custom tools before or after the standard steps, you have two hook points:
- -p == prepend
- -a == append
%conf -p autoreconf -fi %install -a %find_lang %{name} %{?no_lang_C}
As you have seen above, declaring how to handle a certain build system is easy. The macros for CMake are not in our standard cmake package yet, but you can even wrap more complex macros. For example, we could define BuildSystem: kf6 and wrap the KDE %kf6* macros. Of course, we do not have to reinvent the wheel in many places. Especially regarding BuildRequires generators, there is a lot of potential for cooperation with Fedora and other RPM-based distributions.
The Catch?
I’m sure this sounds awesome—shorter spec files and way less duplicated code. But you’re probably wondering: “What’s the downside?”
Well… it only works on SUSE Linux 16 based products, openSUSE Leap 16.x and Tumbleweed. There is no support for 15.x or older. If you need to target older distributions, you’ll have to stick to the “old way” for a little longer.

Related Articles
Aug 21st, 2024
Ooops – Not A Smooth Rollout For Registry Support
Jun 03rd, 2025
The brains behind the books: Sushant Gaurav
Jan 13th, 2026