You patched a tool and dnf overwrote it
You downloaded the source code for a utility, applied a patch that fixes a hardware-specific bug, and ran make install. The program works. You run dnf upgrade the next day and the package manager silently replaces your patched binary with the upstream version. You need to package the software properly so Fedora tracks it, handles dependencies, and lets you roll back cleanly. You found a .spec file online and it looks like a mix of configuration directives and shell commands. You are not reading a runtime config. You are reading a build recipe.
What is actually happening
An RPM spec file is a plain-text instruction set for rpmbuild. It does not execute on your running system. It tells the build system where to fetch source archives, how to compile them, where to place the resulting binaries, and how to track license and changelog data. Think of it as a manufacturing blueprint. The factory reads the blueprint, pulls the raw materials, runs the assembly line, and stamps out a finished .rpm package. Your system only ever sees the stamped package. The spec file stays in the source repository.
Fedora packages follow strict conventions. The spec file enforces them. It separates build-time dependencies from runtime dependencies, stages files in a temporary root before packaging, and applies distribution tags so the same source can build for Fedora 41, Fedora 42, or EPEL 9 without manual edits. The build process runs in an isolated environment. It never touches your live filesystem until you explicitly install the resulting RPM.
Read the blueprint before you run the factory. A missing file declaration or a hardcoded path will produce a package that breaks your system on upgrade.
The structure and minimal example
Every spec file follows the same skeleton. The top section holds metadata tags. The middle sections are shell scripts that run during the build. The bottom sections declare file ownership and version history. You do not need to memorize every tag. You only need to understand the flow and where to place your commands.
Here is a minimal spec file that packages a single compiled binary. Read the comments to see how each block maps to the build lifecycle.
# Metadata block: tells rpm what this package is and what it needs
Name: myapp
Version: 1.0.0
Release: 1%{?dist}
Summary: A simple example application
License: MIT
URL: https://example.com/myapp
Source0: %{name}-%{version}.tar.gz
# BuildRequires lists tools needed only during compilation
BuildRequires: gcc make
# Requires lists libraries needed when the user runs the program
Requires: glibc
%description
A minimal example showing RPM spec file structure.
%prep
# %autosetup unpacks Source0 and applies any PatchN files automatically
%autosetup
%build
# %configure runs ./configure with Fedora-standard prefix flags
%configure
# %make_build compiles using all available CPU cores
%make_build
%install
# %make_install copies files into the temporary buildroot directory
%make_install
%files
# %license moves the license file to /usr/share/licenses/myapp
%license LICENSE
# %doc places documentation in /usr/share/doc/myapp
%doc README.md
# Declare the actual binary so rpm tracks it for upgrades and removal
%{_bindir}/myapp
%changelog
* Fri Apr 18 2026 Your Name <you@example.com> - 1.0.0-1
- Initial release
Macros and path conventions
The spec file relies heavily on macros. Macros are variables that expand to paths or values during the build. They keep your package portable across architectures and Fedora releases. Hardcoding absolute paths breaks when directory layouts change or when you build for multilib targets.
%{_bindir} # Expands to /usr/bin on standard Fedora installs
%{_libdir} # Expands to /usr/lib64 on 64-bit systems, /usr/lib on 32-bit
%{_sysconfdir} # Expands to /etc for configuration files
%{_datadir} # Expands to /usr/share for architecture-independent data
%{buildroot} # Points to the temporary staging directory during %install
%{?dist} # Injects the release tag like .fc41 or .fc42
%{name} # Repeats the Name: tag value for consistent filenames
The %{?dist} tag is mandatory for Fedora packages. It ensures your package version string matches the release cycle. Without it, dnf treats your package as a generic upstream build and may refuse to update it alongside official repositories. Fedora's release cadence is six months. The N-2 release goes EOL when N+1 ships. The dist tag keeps your package aligned with that cycle.
Convention aside: Fedora separates user-modified configuration in /etc/ from package-owned files in /usr/. If your application ships a default config, declare it in %files under %{_sysconfdir}/myapp.conf. The package manager will warn you before overwriting it during upgrades. Never put runtime configs in /usr/lib/. That directory is strictly for package-managed binaries and libraries.
Edit /etc/. Never edit /usr/lib/. Package managers will overwrite manual changes the moment an update ships.
Building the RPM
You do not run rpmbuild as root. Building packages as root breaks file ownership tracking and can corrupt your system. Use your normal user account. The build system creates a temporary staging area called %{buildroot}. All files installed during %install go there. rpmbuild then scans that directory, compares it to your %files list, and packages the differences.
Here is how to set up the build environment and compile the package.
# Install the packaging toolchain and developer utilities
sudo dnf install rpm-build rpmdevtools
# Create the standard ~/rpmbuild directory tree with correct permissions
rpmdev-setuptree
# Place your spec file in the SPECS directory
cp myapp.spec ~/rpmbuild/SPECS/
# Place the source tarball in the SOURCES directory
cp myapp-1.0.0.tar.gz ~/rpmbuild/SOURCES/
# Build both the source RPM and the binary RPM
rpmbuild -ba ~/rpmbuild/SPECS/myapp.spec
The -ba flag tells rpmbuild to build both the source package and the binary package. The source RPM contains the spec file, the original tarball, and any patches. You can share it with other developers so they can rebuild or modify the package. The binary RPM lands in ~/rpmbuild/RPMS/x86_64/ on a standard desktop. If the build fails, check the ~/rpmbuild/BUILD/myapp-1.0.0/ directory for compiler errors. The build log prints to stdout, but the working directory preserves the failed state for inspection.
Run the build in a clean environment first. A leftover object file from a previous make will mask missing dependencies.
Verify it worked
Do not install the RPM immediately. Inspect it first. A broken %files section or a missing dependency will cause dnf to reject the package or leave your system in a half-upgraded state.
# List the contents and metadata of the built package
rpm -qpi ~/rpmbuild/RPMS/x86_64/myapp-1.0.0-1.fc41.x86_64.rpm
# Extract the package to a temporary directory without installing it
rpm2cpio ~/rpmbuild/RPMS/x86_64/myapp-1.0.0-1.fc41.x86_64.rpm | cpio -idmv
# Install locally to test dependency resolution
sudo dnf install --local ~/rpmbuild/RPMS/x86_64/myapp-1.0.0-1.fc41.x86_64.rpm
Check the output of rpm -qpi. Verify the BuildRequires and Requires lines match what the application actually needs. Run the binary from the extracted directory. If it crashes with a missing library, add that library to the Requires: tag and rebuild. You can use ldd ./myapp on the extracted binary to find missing shared objects. Map those objects to Fedora package names using dnf provides */libname.so.
Inspect the package before you install it. A silent dependency miss will break your workflow later.
Common pitfalls and error patterns
The build will fail or produce a broken package if you miss a few standard rules. You will encounter specific error messages that point directly to the violation.
You will see Error: Installed package: myapp-1.0.0-1.fc41.x86_64 does not own file: /usr/bin/myapp if you forgot to declare the binary in the %files section. Every file installed during %install must be listed in %files. Unlisted files are orphaned. They stay on disk after dnf remove and confuse the package manager.
You will see warning: Macro expanded in commented text or build failures if you put shell commands directly in the metadata block. Keep the preamble strictly for tags. Shell commands belong in %prep, %build, %install, or %post.
You will hit File not found: /usr/lib64/myapp.so if you hardcode architecture paths. Use %{_libdir}. The build system runs in a clean chroot on Fedora's official infrastructure. Hardcoded paths break cross-architecture builds and multilib support.
You will see Error: Transaction test error: package myapp conflicts with myapp if you install a local RPM that shares a version string with a repository package. Always bump the Release: tag or use a different epoch when testing local builds alongside official repos.
Run rpmlint myapp.spec before you distribute the package. It catches missing tags, incorrect macro usage, and policy violations. Fix the warnings before you push to a repository.
Run rpmlint before every build. Policy warnings today become broken packages tomorrow.
When to use this vs alternatives
Use a native RPM spec file when you need the package to integrate with dnf, receive automatic updates, and follow Fedora filesystem standards. Use Flatpak when you are distributing a desktop application that requires sandboxed access to user files and needs to run across multiple Linux distributions without recompilation. Use a simple shell script or /usr/local/bin drop when you are packaging a quick internal tool that only runs on one machine and does not need dependency tracking. Stay on the official Fedora repositories when you only need standard tools and do not want to maintain build infrastructure.