Create RPM package

Build an RPM package on Fedora by setting up the rpmbuild directory tree, writing a spec file, and running rpmbuild to produce an installable .rpm artifact.

You built a tool that works on your machine

You wrote a shell script or compiled a small utility that runs perfectly on your laptop. Now you need to deploy it to three other machines or share it with a colleague. Copying binaries into /usr/bin and symlinking libraries works until the next system update overwrites your files. Packaging the software as an RPM gives you a clean install path, automatic dependency tracking, and a safe uninstall command.

What the packaging system actually does

An RPM package is just a compressed archive containing your files, metadata, and a set of install scripts. The magic happens in the spec file. Think of the spec file as a manufacturing blueprint. It tells the build system where to find the source code, how to compile it, where to place the final binaries, and which files belong to the package. The rpmbuild tool reads that blueprint, creates a temporary isolated filesystem, runs your instructions inside it, and then packages the result. You never touch the live system during the build. The package manager handles the actual installation later.

The isolation is intentional. Fedora packages run in a clean chroot-like environment during compilation. This prevents your build from accidentally pulling in system headers that will not exist on the target machine. It also guarantees that the package declares every dependency it actually needs. Trust the build system. Manual file edits drift, packaged dependencies stay consistent.

Set up the isolated build environment

Fedora provides a standardized directory structure for building packages. This keeps your source files, build artifacts, and final binaries strictly separated. A botched build cannot overwrite your system files because the build happens in a user-owned directory tree.

Here's how to install the build environment and initialize the directory structure.

sudo dnf install rpmdevtools rpm-build gcc make -y
# rpmdevtools provides helper scripts for scaffolding and linting
# rpm-build contains the rpmbuild compiler itself
# gcc and make are standard C/C++ build dependencies for most projects
rpmdev-setuptree
# Creates ~/rpmbuild with SPECS, SOURCES, BUILD, RPMS, and SRPMS
# This matches the Fedora packaging convention exactly

Run ls ~/rpmbuild to verify the tree exists. The SPECS directory holds your recipe files. The SOURCES directory holds your original code archives. The BUILD directory is a temporary workspace for compilation. The RPMS and SRPMS directories collect the finished outputs. Keep this tree intact. Re-running rpmdev-setuptree on an existing directory is safe, but deleting it manually breaks your build cache.

Prepare the source archive

The build system expects a single compressed archive containing your project files. The archive name must match the Source0 tag in your spec file. This naming convention prevents version collisions and makes auditing straightforward.

Here's how to package your project into the expected archive format.

mkdir -p myapp-1.0
# Create a directory matching the Name-Version convention
echo '#!/bin/bash' > myapp-1.0/myapp
echo 'echo "Hello from myapp"' >> myapp-1.0/myapp
chmod +x myapp-1.0/myapp
# Ensure the script is executable before packaging
tar czf ~/rpmbuild/SOURCES/myapp-1.0.tar.gz myapp-1.0/
# Compress the directory and place it in the SOURCES folder
# The filename must exactly match the Source0 macro later

Write the spec file blueprint

This is the core of the process. The spec file defines metadata, build steps, and file ownership. Every line serves a purpose. Fedora's packaging guidelines enforce strict formatting to ensure consistency across thousands of packages.

Here's a minimal working spec file that follows Fedora conventions.

Name:           myapp
Version:        1.0
Release:        1%{?dist}
# %{?dist} expands to .fc41 or .fc42 automatically
# This prevents your package from clashing with official repos
Summary:        A simple example application
License:        MIT
Source0:        %{name}-%{version}.tar.gz
# References the tarball in the SOURCES directory
BuildArch:      noarch
# Tells the system this package contains no compiled binaries

%description
A basic example RPM for Fedora.
# This text appears in dnf search and package managers

%prep
%autosetup -n %{name}-%{version}
# Extracts Source0 and applies any patches automatically
# The -n flag overrides the default directory name extraction

%build
# Nothing to compile for a shell script
# Leave this section empty or add make commands for C projects

%install
rm -rf %{buildroot}
# Clean the temporary root directory before installing files
install -Dm 0755 myapp %{buildroot}/usr/bin/myapp
# Copies the script to the correct path with executable permissions
# -D creates parent directories automatically

%files
/usr/bin/myapp
# Lists every file the package owns. Be explicit.
# Wildcards are discouraged for simple packages.

%changelog
* Fri Apr 18 2026 Your Name <you@example.com> - 1.0-1
- Initial package
# Tracks every release. Format is strict for repo tools.

Save this as ~/rpmbuild/SPECS/myapp.spec. Notice the %{?dist} macro in the Release line. Fedora repositories use this to separate packages across major versions. If you omit it, your package will claim to work on every Fedora release, which breaks dependency resolution when you upgrade the OS. The %buildroot macro points to a temporary directory that mimics a real Linux filesystem. rpmbuild mounts this directory, runs your %install commands inside it, and then packages whatever ends up there. Never hard-code paths in %install. Always use %{buildroot} as the prefix.

Compile and package the software

The build command reads the spec file, executes the %prep, %build, and %install sections inside a clean environment, and outputs the final RPM. You can build just the binary package or generate a source package alongside it.

Here's how to compile the spec file into an installable RPM.

rpmbuild -bb ~/rpmbuild/SPECS/myapp.spec
# -bb builds only the binary RPM package
# Use -ba if you also need a source RPM for distribution
# The tool reads the spec file and executes each section in order

The build process will print progress lines for each section. If it succeeds, the final package lands in ~/rpmbuild/RPMS/noarch/. The architecture directory matches your BuildArch setting. A C program targeting x86_64 would appear in RPMS/x86_64/ instead. Dependency resolution happens automatically during the %install phase. The build system scans your binaries for shared library calls and writes Requires: tags into the package metadata. You rarely need to declare runtime dependencies manually unless you are packaging a language-specific tool.

Audit before you install

Never install a package without inspecting it first. RPM provides query tools that let you audit file lists, dependencies, and metadata without touching the filesystem.

Here's how to audit the package contents and install it safely.

rpm -qpl ~/rpmbuild/RPMS/noarch/myapp-1.0-1.fc41.noarch.rpm
# -q queries the package, -p specifies a file path, -l lists files
# Confirms exactly what will be written to disk
sudo dnf install ~/rpmbuild/RPMS/noarch/myapp-1.0-1.fc41.noarch.rpm
# dnf handles local files the same way it handles remote repos
# It checks dependencies and registers the package in the database

Run rpm -qi myapp after installation to see the full metadata. The package manager now tracks every file. Running dnf remove myapp later will cleanly delete /usr/bin/myapp and update the database. Always verify the file list matches your expectations. A stray configuration file or log directory in the wrong place breaks system upgrades.

Lint the spec file

Fedora's packaging guidelines are strict for a reason. Inconsistent spec files cause repository conflicts, break automated rebuilds, and confuse users. The rpmlint tool checks your spec file against hundreds of rules before you ever publish it.

Here's how to run the official quality checker on your spec file.

sudo dnf install rpmlint -y
# rpmlint is the standard validation tool for Fedora packages
rpmlint ~/rpmbuild/SPECS/myapp.spec
# Scans for missing macros, incorrect permissions, and guideline violations
# Warnings are informational. Errors usually block repo submission.

Read the output carefully. rpmlint will flag missing %description text, incorrect changelog formatting, or files that should be marked as %ghost. Fix the errors before distributing. A clean rpmlint run is the difference between a hobby script and a production-ready package. Run rpmlint after every spec file edit. Future-you will thank you.

Common pitfalls and exact error messages

Packaging trips up beginners in predictable ways. The build system is strict because it protects the host system.

If you forget to list a file in the %files section, the build succeeds but the file never appears on the target system. The package manager will not track it, and dnf remove will leave it behind. Always run rpm -qpl to verify.

If you use a wildcard like /usr/bin/* in %files, rpmlint will flag it immediately. Wildcards capture temporary build artifacts, debug symbols, and test files. List every path explicitly.

If your spec file contains a hard-coded release like Release: 1.fc41, the package will refuse to install on Fedora 42. The dependency resolver sees a version mismatch and blocks the transaction. You will see this exact error:

Error: package myapp-1.0-1.fc41.noarch requires dist = fc41, but you have dist = fc42

Always use %{?dist}.

If you compile a C program without setting BuildRequires: gcc, the build fails silently or aborts with a missing tool error. Add BuildRequires: lines for every compiler or library needed during compilation. Runtime dependencies go in Requires:.

SELinux denials are another common trap. If your package installs a script to /usr/local/bin instead of /usr/bin, the security policy will block execution. You will see Permission denied even with chmod +x. Stick to standard FHS paths. The package manager sets the correct SELinux contexts automatically during installation. Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/.

Choose the right distribution method

Packaging decisions depend on your distribution scope and maintenance tolerance.

Use RPM packaging when you need system integration, automatic updates through dnf, and strict file ownership tracking. Use Flatpak when you are distributing a graphical application with bundled libraries and want sandbox isolation. Use AppImage when you need a single portable binary that runs without root privileges or system modification. Use pip or npm when your project is a language-specific library meant for virtual environments. Stick to manual /usr/local installation when you are testing a quick prototype on a single machine and do not care about clean removal.

Where to go next