Building .rpm Package From Scratch
RPM packaging follows a more structured, more source-oriented workflow than the direct directory-and-build approach used for .deb — worth experiencing hands-on rather than just described. This chapter builds a real .rpm from a small Python script, mirroring the previous package-building chapter’s full install-trace-remove cycle, on the RPM side this time.
Step 1: A Minimal Python Script
mkdir -p ~/build/hello-py
nano ~/build/hello-py/hello.py#!/usr/bin/env python3
print("Hello from a real .rpm package!")chmod 755 ~/build/hello-py/hello.pyStep 2: Setting Up The rpmbuild Directory Structure
RPM packaging expects a specific, standardized directory layout, conventionally under ~/rpmbuild:
sudo dnf install rpmdevtools
rpmdev-setuptreels ~/rpmbuildBUILD BUILDROOT RPMS SOURCES SPECS SRPMSEach directory has a fixed purpose: SPECS holds the spec file describing how to build the package (covered next), SOURCES holds the actual source material, BUILD and BUILDROOT are working directories used during the build itself, and RPMS/SRPMS are where the finished binary and source packages end up.
Step 3: Packaging The Source
RPM convention expects source material as a tarball, even for something this small:
mkdir -p ~/build/hello-tool-1.0.0
cp ~/build/hello-py/hello.py ~/build/hello-tool-1.0.0/
tar -czf ~/rpmbuild/SOURCES/hello-tool-1.0.0.tar.gz -C ~/build hello-tool-1.0.0Step 4: The Spec File
The spec file is the RPM equivalent of the .deb control file, but considerably more involved — it doesn’t just describe the package, it describes the actual build process step by step.
nano ~/rpmbuild/SPECS/hello-tool.specName: hello-tool
Version: 1.0.0
Release: 1%{?dist}
Summary: A minimal example package
License: MIT
BuildArch: noarch
Source0: %{name}-%{version}.tar.gz
%description
A tiny hands-on example built for the Package Management section.
%prep
%setup -q
%install
mkdir -p %{buildroot}/usr/bin
install -m 755 hello.py %{buildroot}/usr/bin/hello-tool
%files
/usr/bin/hello-tool
%post
echo "hello-tool was installed successfully." >&2
%changelog
* Mon Jan 15 2026 You <[email protected]> - 1.0.0-1
- Initial packageA few sections worth understanding individually:
BuildArch: noarch— since this is a plain Python script with no compiled, architecture-specific binary involved, it can run on any architecture. Compare this to the C-based.debexample from the last chapter, which genuinely was tied to a specific architecture.%prep— prepares the source for building;%setup -qextracts the source tarball automatically, quietly.%install— this is the section doing the actual work of placing files where they belong, using%{buildroot}as a stand-in for “the root of the eventual installed system” during the build itself, not the real/of your actual machine.%files— the list of files this package actually owns and will install — this must match exactly what%installactually placed in the buildroot, which is the source of one of the most commonrpmbuilderrors, covered below.%post— directly equivalent to the.debpostinstscript from the last chapter — runs automatically right after installation.
Notice the permission is set explicitly with install -m 755 during %install, rather than requiring a separate chmod step beforehand the way the .deb build did — a genuinely cleaner approach, since the correct permission is declared as part of the same command that places the file.
Step 5: Building The Package
rpmbuild -ba ~/rpmbuild/SPECS/hello-tool.spec-ba builds both the binary RPM and the source RPM in one pass. The finished package lands under RPMS, organized by architecture:
ls ~/rpmbuild/RPMS/noarch/hello-tool-1.0.0-1.fc40.noarch.rpmNote
fc40 in this file is fedora version, yours might be different.
Step 6: Installing And Verifying
sudo dnf install ~/rpmbuild/RPMS/noarch/hello-tool-1.0.0-1.fc*You should see the %post message print. Confirm it runs:
hello-toolTrace exactly what got installed, using the query tools from the rpm chapter:
rpm -ql hello-tool/usr/bin/hello-toolrpm -qi hello-toolName : hello-tool
Version : 1.0.0
Summary : A minimal example packageAs a bonus, confirm the package’s integrity checking works exactly as described in the rpm chapter:
rpm -V hello-toolNo output — everything matches exactly what was installed, confirming the verification mechanism genuinely works against your own package, not just ones from an official repository.
Step 7: Removing, With Full Verification
sudo rpm -e hello-toolrpm -q hello-toolpackage hello-tool is not installedwhich hello-toolReturns nothing — the file is genuinely gone, and since this example had no configuration file involved, there’s no .rpmsave to check for either. A complete, traceable removal, mirroring the .deb example precisely, using entirely different underlying tooling.
Common Pitfalls
A %files list that doesn’t match what %install actually placed. This is the single most common rpmbuild error — if %install creates a file that isn’t listed in %files, the build fails outright with an “Installed (but unpackaged) file(s) found” error, deliberately preventing you from shipping a package with untracked files.
Forgetting BuildArch: noarch for genuinely architecture-independent content like a script — without it, rpmbuild defaults to tagging the package with your build machine’s actual architecture, needlessly restricting where it can be installed.
Spec file section order matters. Unlike a loosely-structured configuration file, a spec file’s sections follow a fixed, expected order — %prep before %install, %files after both — and getting this wrong produces confusing build errors rather than a working package.
What’s Next
You’ve now built and fully traced a package through both major ecosystems — a compiled C program as a .deb, a Python script as an .rpm — using each family’s own idiomatic build process. The next chapter goes deeper into the maintainer-script lifecycle briefly previewed in both build walkthroughs, covering every hook point available on each side and why running as root during installation is a genuine security consideration worth taking seriously.