You need a package that isn't in the repos
You found a niche tool on GitHub that solves a problem you face daily. The developer only ships a tarball. You could extract it, run make install, and hope the binaries land in /usr/local/bin. Or you could wrap it in an RPM so Fedora tracks the files, handles dependencies, and lets you remove it cleanly later. The second option takes longer upfront but saves hours of cleanup when you inevitably forget where the config files went.
What the build system actually does
An RPM is not just a compressed archive. It is a database entry paired with a payload. The package manager tracks every file, its permissions, its SELinux context, and the scripts that run before and after installation. When you build an RPM from source, you are telling the build system exactly how to compile the code, where to put the resulting binaries, and which files belong to the package versus which files are just documentation.
Think of the spec file as a manufacturing blueprint. rpmbuild is the factory floor. The factory reads the blueprint, fetches the raw materials, runs the assembly line in an isolated room, and stamps the final product with a cryptographic signature. The isolation matters. Your live system never touches the compilation artifacts until you explicitly install the final package. This prevents half-compiled binaries from breaking your desktop or server.
Run rpmbuild from a clean workspace. Future-you will thank you when the build fails and you need to start over.
Set up the workspace
Fedora does not let you compile packages as root. The build process runs in a sandboxed environment that drops privileges automatically. You need the toolchain and a standardized directory layout. Run these commands to prepare your system.
sudo dnf install -y rpm-build rpmdevtools rpmlint
# rpm-build provides rpmbuild and the core macro definitions
# rpmdevtools includes rpmdev-setuptree and rpmdev-bumpspec
# rpmlint catches spec file mistakes before they break your system
rpmdev-setuptree
# Creates ~/rpmbuild with SPECS, SOURCES, BUILD, RPMS, and SRPMS
# The toolchain expects this exact layout. Do not rename the directories.
Place your source archive in the SOURCES directory. The build system will look there automatically when you reference Source0 in the spec file.
cp myapp-1.0.tar.gz ~/rpmbuild/SOURCES/
# The filename must match the Source0 tag exactly
# rpmbuild verifies the checksum against the spec file later
# Keep original archives here. Never modify them in place.
Write the spec file
The spec file controls every phase of the build. It uses a macro language that expands variables at runtime. You do not need to memorize every macro. You only need to understand the standard sections and how they map to the compilation lifecycle.
Create the spec file in the SPECS directory. Here is the metadata and source declaration section.
Name: myapp
Version: 1.0
Release: 1%{?dist}
Summary: A custom command-line utility
License: MIT
URL: https://github.com/example/myapp
Source0: %{name}-%{version}.tar.gz
BuildRequires: gcc make cmake
# %{?dist} appends .fc39 or .fc40 to prevent version collisions
# Source0 must match the exact filename in ~/rpmbuild/SOURCES/
# BuildRequires lists compilation tools, not runtime libraries
Here is the build lifecycle and file manifest section.
%description
myapp processes log files and exports them to CSV format.
It requires no external libraries and runs as a standard user.
%prep
%autosetup -n %{name}-%{version}
# Extracts the tarball and applies any patches listed later
# The -n flag overrides the default directory name if needed
%build
%cmake
%make_build
# Runs cmake and make with parallel jobs matching your CPU count
# Fedora macros inject security hardening flags automatically
%install
rm -rf %{buildroot}
# Clears the temporary root directory from previous failed builds
%make_install
# Copies binaries into %{buildroot}/usr without touching your system
%files
%license LICENSE
%doc README.md
/usr/bin/myapp
# Lists every file the package owns. Missing files cause build failures.
# Wildcards are discouraged. List paths explicitly.
The %{?dist} tag appends the Fedora release identifier. This prevents version collisions when you move packages between releases. The %autosetup macro handles extraction and patching in one step. The %build and %install macros inject Fedora's security flags automatically. Do not override CFLAGS unless you have a specific compiler requirement.
Edit the spec file until it matches your project structure. Guesswork breaks the build.
Resolve dependencies and compile
The spec file lists BuildRequires for the compilation environment. These are not runtime dependencies. They are the tools needed to compile the code. Install them before attempting the build.
sudo dnf builddep ~/rpmbuild/SPECS/myapp.spec
# Reads the BuildRequires tags and installs them from enabled repos
# Fails immediately if a required package is missing or disabled
# Does not install runtime dependencies. Those go in Requires:
Run the build command from your home directory. Do not run it from inside ~/rpmbuild. The working directory matters for macro expansion.
rpmbuild -ba ~/rpmbuild/SPECS/myapp.spec
# -b means build, -a means all architectures (binary and source)
# Compiles in ~/rpmbuild/BUILD, stages files in %{buildroot}
# Outputs final RPMs to ~/rpmbuild/RPMS and ~/rpmbuild/SRPMS
The command will print a wall of text. Watch for the final lines. A successful build ends with a list of generated files and their SHA256 checksums. If it stops early, the error message will point to the exact line in the spec file or the compilation log.
Check the build log before you panic. The failure reason is usually on the last three lines.
Debug a failed build
Build failures fall into three categories. Missing dependencies, macro errors, and file manifest mismatches.
If the compiler complains about missing headers, your BuildRequires list is incomplete. Add the development package, like libfoo-devel, and run dnf builddep again. Development packages ship the header files and .pc files that cmake or configure needs.
If you see error: Invalid tag: %autosetup, you misspelled a macro or are using a macro that requires a macro package you did not install. The rpm-build package provides the standard macros. Specialized macros live in packages like cmake-rpm-macros or python-rpm-macros. Install the correct macro package and retry.
If the build stops with RPM build errors: Installed (but unpackaged) file(s) found:, you compiled a file that landed in %{buildroot} but did not list it in the %files section. Add the path to %files or exclude it with %exclude. The package manager refuses to install files it does not track.
Read the actual error before guessing. The build log points to the exact line. Fix the spec file and run rpmbuild again.
Verify the result
Install the binary package and check the metadata. Use dnf instead of rpm for installation. dnf resolves runtime dependencies and updates the package database correctly.
sudo dnf install ~/rpmbuild/RPMS/x86_64/myapp-1.0-1.fc*.x86_64.rpm
# Installs the package and registers it with the system database
# Wildcards handle the exact dist tag automatically
# dnf handles dependency resolution better than raw rpm
rpm -qi myapp
# Displays version, release, install date, and description
rpm -ql myapp
# Lists every file owned by the package on disk
Run rpmlint against the spec file and the generated RPM. It checks for policy violations, missing dependencies, and incorrect file permissions.
rpmlint ~/rpmbuild/SPECS/myapp.spec ~/rpmbuild/RPMS/x86_64/myapp-*.rpm
# Scans for Fedora packaging guidelines violations
# Warnings are usually safe to ignore. Errors must be fixed.
# Run this before distributing to anyone else.
If the package installs a systemd service, check the unit status immediately. Use journalctl -xeu myapp.service to see recent log lines and the current state in one view. Always check status before restarting.
Run rpmlint before you distribute the package. A clean scan means the package follows Fedora standards and will not break on a fresh system.
Common pitfalls and error messages
The build process fails in predictable ways. Recognizing the error saves time.
If you see error: File not found: /home/user/rpmbuild/SOURCES/myapp-1.0.tar.gz, the filename in Source0 does not match the actual archive. Check the spelling and the version number.
SELinux denials appear after installation if the package creates files in unexpected locations. The build system generates file contexts automatically based on the %files list. If you place a config file in /etc/myapp/, add %config(noreplace) /etc/myapp/config.ini to the %files section. This tells the package manager to preserve user edits during upgrades. Config files in /etc/ are user-modified. Files in /usr/lib/ ship with the package. Edit /etc/. Never edit /usr/lib/.
Macro expansion failures look like error: Invalid tag: %autosetup. This happens when you misspell a macro or use a macro that requires a macro package you did not install. Install the correct macro package and retry.
Read the actual error before guessing. The build log points to the exact line. Fix the spec file and run rpmbuild again.
When to build locally versus using alternatives
Use local rpmbuild when you need a quick custom package for your own machine or a small team. Use mock when you want to verify that the package builds cleanly on a different Fedora release or architecture without installing build tools on your host system. Use dnf download --source when you want to modify an existing Fedora package and keep the original spec file structure intact. Use a third-party repository when the software is already packaged by the community and you want automatic updates. Stay on the official repos when the package is available and meets your requirements.
Trust the package manager. Manual file edits drift, snapshots stay.