You downloaded a spec file and the build failed
You downloaded a .spec file from Pagure, ran rpmbuild -ba, and got a wall of SELinux denials or missing dependency errors. Or maybe you built a package on your laptop, copied it to a server, and it crashed because a shared library version did not match. Building RPMs directly in your home directory feels fast until your local environment starts dictating what gets packaged.
What is actually happening
rpmbuild compiles and packages software directly inside your active user environment. It links against whatever libraries are already installed on your system. That approach works for quick local tests but breaks reproducibility. Your resulting RPM will implicitly depend on your personal package set. When you move that binary to another machine, it fails.
mock solves this by spinning up a sterile chroot that matches a specific Fedora release. It starts with an empty root filesystem, installs only the declared build dependencies, compiles the package, and hands you back a clean RPM. Think of rpmbuild as cooking in your own kitchen with whatever ingredients are already in your fridge. mock is a commercial test kitchen that starts empty, purchases exactly what the recipe calls for, and cleans up after itself.
Fedora packaging guidelines require reproducible builds. The official build system uses mock under the hood. Setting up your local environment to mirror that process keeps your packages compatible with the wider ecosystem. Fedora releases a new version every six months. The N-2 release goes EOL when N+1 ships. Building against a fixed release string ensures your package works on the target version, not just on whatever happened to be updated on your host yesterday.
Run mock for anything you plan to share. Local compilation is only for debugging.
Install the tools and prepare the directory tree
You need three packages to get started. rpm-build provides the compiler and packaging macros. mock handles the isolated chroot environment. fedora-packager installs the standard macro set and development tools that most .spec files expect.
Here is how to install the packages and create the standard directory hierarchy.
sudo dnf install rpm-build mock fedora-packager -y # installs the build toolchain, isolation runner, and standard macros
mkdir -p ~/rpmbuild/{BUILD,BUILDROOT,RPMS,SOURCES,SPECS,SRPMS} # creates the six directories rpmbuild expects
The directory structure is not optional. rpmbuild hardcodes these paths into its macro system. SOURCES holds tarballs and patches. SPECS holds the .spec file. BUILD is where the source extracts and compiles. BUILDROOT stages the final filesystem layout before packaging. RPMS and SRPMS catch the output binaries and source packages.
You can override these paths in ~/.rpmmacros, but the default ~/rpmbuild tree works for ninety percent of developers. Custom macro paths introduce drift when you share scripts with other packagers. Stick to the standard layout.
Create the directories exactly as shown. Missing folders cause silent failures during the %install phase.
Fix SELinux contexts for local builds
Fedora ships with SELinux in enforcing mode by default. The policy restricts where compilation can happen to prevent accidental system modification. If you run rpmbuild without the correct context, the build process will refuse to write to protected paths and abort.
Here is how to apply the correct security context to your build directory.
sudo semanage fcontext -a -t rpm_build_t "~/rpmbuild(/.*)?" # tells SELinux this path is safe for compilation
restorecon -Rv ~/rpmbuild # applies the new context to existing files and subdirectories
The rpm_build_t context allows the build process to execute compilers, run makefiles, and stage files without triggering denials. You only need to run this once. SELinux remembers the context rule across reboots.
SELinux policy files live in /usr/share/selinux/. User modifications belong in /etc/selinux/. The semanage command writes to /etc/selinux/targeted/contexts/files/file_contexts.local. This keeps your custom rules safe from package updates. Never edit files in /usr/share/selinux/. They will be overwritten on the next dnf upgrade --refresh.
Verify the context with ls -ldZ ~/rpmbuild. The output must show rpm_build_t. If it shows user_home_t, the build will fail.
Apply the context before you compile. SELinux denials are harder to debug than missing dependencies.
Configure mock for reproducible builds
Local compilation is fine for debugging, but mock is the standard for anything you plan to share or install on another machine. It isolates your host system from the build environment. Your host Python version, your custom C compiler flags, and your third-party repositories stay out of the package.
Here is how to install the configuration file for your target Fedora release.
sudo dnf install mock-config-fedora-40 -y # provides the chroot definition for Fedora 40
sudo usermod -aG mock $USER # adds your user to the mock group for unprivileged builds
newgrp mock # reloads group membership so the current shell recognizes the change
The mock-config-fedora-XX packages live in /etc/mock/. They define the base image, the repository URLs, and the SELinux policies for the chroot. You can install multiple config packages side by side. mock will not overwrite them.
mock caches repository metadata and installed packages in /var/cache/mock/. The first build downloads the base image and resolves dependencies. Subsequent builds reuse the cache. This keeps local testing fast without sacrificing isolation.
Add yourself to the mock group. mock refuses to run as root by design. It requires an unprivileged user to enforce the isolation boundary. The group membership grants access to the chroot filesystem without granting full system privileges.
Install the config package for your target release. Build against the exact version you intend to support.
Run a test build
Place your .spec file in ~/rpmbuild/SPECS/. Run the command below to trigger an isolated build.
mock -r fedora-40-x86_64 ~/rpmbuild/SPECS/my-package.spec # spins up the chroot and executes the spec file
mock downloads the base image on the first run. It caches the repository metadata and the installed packages. Subsequent builds for the same release finish much faster. The tool resolves build dependencies automatically. If the .spec file declares BuildRequires: gcc, mock installs it inside the chroot. Your host system remains untouched.
The finished RPMs land in ~/mock-results/. The directory name matches the release string you passed to -r. You can install the output directly with sudo dnf install ./mock-results/fedora-40-x86_64/*.rpm.
Use the -r flag to specify the exact chroot configuration. Omitting it forces mock to guess your host version, which defeats the purpose of isolation. Always pass the release string explicitly.
Run the build and watch the output. The chroot handles the heavy lifting.
Verify the environment
You need to confirm that mock recognizes your target release and that your user has the correct permissions.
Here is how to list available chroot configurations.
mock --list # prints every installed mock-config package and its architecture variants
If your target release does not appear, install the matching mock-config-fedora-XX package. If the list prints but builds fail with permission errors, verify your group membership.
id -nG | grep mock # confirms your user is in the mock group
Reboot or run newgrp mock if the group is missing. mock refuses to run as root by design. It requires an unprivileged user to enforce the isolation boundary.
Check the build logs in /var/lib/mock/ when a transaction completes. The root.log file shows package installation steps. The build.log file shows compiler output. Read both files before assuming the package is broken.
Verify the output RPM with rpm -qpi ./mock-results/fedora-40-x86_64/*.rpm. The metadata must match your target release. If it references a different Fedora version, your chroot configuration is misaligned.
Check the logs before you distribute. A silent dependency mismatch breaks downstream systems.
Common pitfalls and what the errors look like
Builds fail for three predictable reasons. Missing build dependencies, SELinux denials, and host environment leakage.
You will see this error inside the mock log when the .spec file omits a BuildRequires line.
Error: Package 'gcc' is not installed
Error: Nothing provides gcc needed by my-package-1.0-1.fc40.x86_64
mock does not guess. It installs exactly what the spec declares. Add the missing dependency to the spec file and rerun.
You will see Permission denied or SELinux is preventing... when running rpmbuild directly without the rpm_build_t context. Run restorecon -Rv ~/rpmbuild and verify the context with ls -ldZ ~/rpmbuild.
You will see FileNotFoundError or ImportError when a Python package tries to import a module that exists on your host but not in the chroot. This is host leakage. Switch to mock immediately. It catches these mismatches before you distribute a broken package.
Read the mock log file in /var/lib/mock/ when a build fails. The last twenty lines usually contain the exact compiler flag or missing header that broke the transaction. Do not guess. The log tells you precisely which step failed.
Check the actual error before rewriting the spec file. Most failures are missing BuildRequires lines, not broken code.
When to use rpmbuild versus mock
Use rpmbuild when you are debugging a single macro expansion or testing a quick patch on your current machine. Use mock when you are preparing a package for distribution, testing against a specific Fedora release, or ensuring reproducible builds. Use the official koji build system when you are submitting to Fedora or EPEL and need automated testing across multiple architectures. Stay on mock for local development. It mirrors the production pipeline without the network overhead.