How to Use rpmbuild on Ubuntu to Create an RPM Package

build rpm linux, build rpm ubuntu

Install the rpm package — it contains rpmbuild — then build with dependency checking off and the package format pinned to v4.

sudo apt install rpm          # universe; ships /usr/bin/rpmbuild
rpmbuild --nodeps --define '_rpmformat 4' -ba ~/rpmbuild/SPECS/helloworld.spec

Those two extra flags are the whole difference between a package that installs on the RHEL box you built it for and one that does not. The rest of this page explains why, and corrects three things the previous version of this article got wrong.

Ubuntu 26.04 ships rpm 6, and that changes the output

Querying the Ubuntu archive via the Launchpad API shows the jump: Ubuntu 24.04 (noble) publishes rpm 4.18.2+dfsg-2.1build2, while Ubuntu 26.04 (resolute) publishes rpm 6.0.1-1build1, in universe. Unpacking that package’s macros file shows the consequence:

%_rpmformat 6
%_binary_payload  %[ %_rpmformat >=6 ? "w19.zstdio" : "w9.gzdio" ]

So on Ubuntu 26.04 an unmodified rpmbuild -ba emits the v6 format with a zstd payload. Building the spec below with rpm 6.0.2 twice — once at the default, once with --define '_rpmformat 4' — the difference shows up in the package’s own dependency list:

$ rpm -qp --requires helloworld-1.0-1.aarch64.rpm   # default (format 6)
rpmlib(LargeFiles) <= 4.12.0-1
rpmlib(PayloadIsZstd) <= 5.4.18-1

$ rpm -qp --requires helloworld-1.0-1.aarch64.rpm   # --define '_rpmformat 4'
rpmlib(CompressedFileNames) <= 3.0.4-1
rpmlib(FileDigests) <= 4.6.0-1
rpmlib(PayloadFilesHavePrefix) <= 4.0-1

rpmlib() dependencies are satisfied by the installing rpm binary itself, not by anything in the target’s package database. The v4 build asks for nothing newer than rpm 4.6. The default build demands zstd payload support. If the target’s rpm does not advertise that feature, the install fails on a dependency the user cannot install their way out of. Pin the format unless you know the target.

Why BuildRequires can never be satisfied on a Debian host

This is the correction that matters most, and the old version of this page missed it entirely. Run the spec unmodified and you get:

error: Failed build dependencies:
	gcc is needed by helloworld-1.0-1.aarch64

gcc is installed. rpm cannot see it. BuildRequires is resolved against the rpm database, and on a Debian-family host that database is empty — rpm -qa returns zero packages, because everything on the system was installed by dpkg. No amount of apt install build-essential will ever satisfy an rpm BuildRequires line. You pass --nodeps and take responsibility for the build dependencies yourself.

The old page’s troubleshooting section blamed missing dependency errors on “libraries Ubuntu does not install by default” and told you to apt install them. That advice cannot work.

%_topdir is already correct — delete that step

The old version told you to write ~/.rpmmacros containing %_topdir %(echo $HOME)/rpmbuild. Reading the macros file shipped in the Ubuntu rpm-common package:

%_topdir		%{getenv:HOME}/rpmbuild

That is already the default. rpm --eval '%_topdir' confirms it at runtime. The .rpmmacros step sets the value to what it already was. Skip it, and skip the useradd makerpm step too — rpmbuild refuses to run as root by default anyway, and your own account is fine.

Create the tree, minus the ceremony:

mkdir -p ~/rpmbuild/{BUILD,RPMS,SOURCES,SPECS,SRPMS}

BUILDROOT is not in that list and does not need to be. rpm 4.20 and later create a per-package build directory; the build log shows it making BUILD/helloworld-1.0-build/BUILDROOT on its own.

%{?dist} expands to nothing here

The spec’s Release: 1%{?dist} is copied from Fedora convention, where redhat-rpm-config defines %dist as .el9, .fc42 and so on. Ubuntu’s rpm package does not define it — grepping the shipped macros file finds only commented-out #%distribution and #%disttag lines. The result, from rpmspec -q on the spec below:

helloworld-1.0-1.aarch64

Not 1.el9. If your release naming depends on the dist tag, set it explicitly: rpmbuild --define 'dist .el9' ....

A spec that builds

Name:           helloworld
Version:        1.0
Release:        1%{?dist}
Summary:        A simple Hello World program
License:        GPL-2.0-or-later
URL:            https://example.com
Source0:        helloworld-1.0.tar.gz
BuildRequires:  gcc

%description
A basic Hello World program written in C.

%prep
%setup -q

%build
gcc -o helloworld helloworld.c

%install
install -d $RPM_BUILD_ROOT/usr/local/bin
install -m 755 helloworld $RPM_BUILD_ROOT/usr/local/bin

%files
/usr/local/bin/helloworld

%changelog
* Tue Feb 18 2025 MakerPM <maker@example.com> - 1.0-1
- Initial RPM release

Three changes from the old version. %defattr(-,root,root,-) is gone — it has been the default since rpm 4.4 and adds nothing. rm -rf $RPM_BUILD_ROOT at the top of %install is gone, because rpm already cleans the buildroot and the line would delete the directory install -d then has to recreate. And the changelog date is fixed: the old entry read Mon Feb 18 2025, which rpm rejects with

warning: bogus date in %changelog: Mon Feb 18 2025 MakerPM - 1.0-1

because 18 February 2025 was a Tuesday. rpm checks the weekday against the date.

Do not use alien to test your own package

The old page suggested converting the RPM to a .deb with alien and installing it. That tests alien, not your package. It cannot tell you whether your Requires resolve on the target distribution, whether your scriptlets run, or whether your file ownership survives — which are the things that actually break. Inspect it instead, then install it on the real target:

rpm -qpi  helloworld-1.0-1.x86_64.rpm     # metadata
rpm -qpl  helloworld-1.0-1.x86_64.rpm     # file list
rpm -qp --requires helloworld-1.0-1.x86_64.rpm
rpm -qp --scripts  helloworld-1.0-1.x86_64.rpm

And the caveat that belongs on every cross-distro build page: rpm’s automatic dependency generator records what it finds on the build host. A binary linked on Ubuntu against glibc 2.42 gets a Requires that a RHEL 9 machine cannot meet, no matter how clean the spec is. If the package contains compiled code, build it in a container of the target distribution. Ubuntu’s rpmbuild is for producing correct packaging — noarch content, config bundles, spec development — not for producing target-ready binaries.

What was executed here

The build, the format comparison, the BuildRequires failure, the empty rpm database, the bogus-date warning and the %{?dist} expansion were all run on macOS 26.6 (arm64) using upstream rpm 6.0.2 from Homebrew — hence aarch64 in the output above instead of x86_64. The Ubuntu-specific facts (rpm 6.0.1 in resolute, 4.18.2 in noble, %_topdir, %_rpmformat, the absence of %dist, and /usr/bin/rpmbuild living in the rpm package) come from the Launchpad API and from unpacking the real rpm_6.0.1-1build1_amd64.deb and rpm-common packages out of the Ubuntu archive. No Ubuntu machine was used.

Check this yourself. Every command and every block of output on this page is reproduced by /verify/rpmbuild-ubuntu-create-rpm.sh. Download it and run it: it creates its own scratch files, prints one line per claim, cleans up after itself, and exits non-zero if any claim here turns out to be wrong. If it disagrees with this page, the page is wrong.

Photo of author
Sudhir P. founded HeatWare.com in 1999 and has built and operated it full-stack ever since; it is now used by more than 88,000 people. He writes here about the PostgreSQL, MySQL, Linux and DevOps work that keeps it running. Articles are rewritten only after the commands in them have actually been run, and the verification scripts are published alongside them so anyone can check the claims. Reach him at blog@heatware.net.