“Programming Practice Record” is a public account dedicated to sharing software development practices and technologies. We welcome you to follow us and engage in mutual learning and progress.
Distributing software on Linux, especially in a corporate environment, is most reliably done by packaging it as an RPM. This is akin to creating a standard installation package for your program, allowing for easy installation, upgrading, and uninstallation on systems like CentOS and Red Hat.
This process may seem complex, but the concept is quite clear. This article will complete the process in two steps:
-
Build: First, compile the C++ code into an executable binary file.
-
Package: Then, package this binary file along with its configuration files, documentation, etc., into an RPM package.
This “do the work first, package later” separation method is very flexible and reliable. This article will guide you through the entire process step by step.

1. Preparation
Before getting started, ensure that the environment and tools are ready.
1. Install Packaging Tools
Open the terminal and use the <span>yum</span> command to install the necessary tools.
# (Taking CentOS/RHEL as an example)sudo yum install -y rpm-build rpmdevtools gcc-c++ cmake
2. Create Packaging Directory
Run the following command, which will automatically create a standard packaging directory in your home directory (e.g., <span>~/rpmbuild</span>).
rpmdev-setuptree
After this, we will mainly interact with the <span>SOURCES</span> (for raw material tarballs) and <span>SPECS</span> (for packaging specifications) directories.
2. Step One: Write Code and Compile Program
We will start with a simple C++ program that reads a configuration file <span>/etc/greeter.conf</span> and then prints a greeting.
1. Create Project Directory
mkdir greeter-appcd greeter-app
2. Write C++ Source Code (main.cpp) In the <span>greeter-app</span> directory, create a <span>main.cpp</span> file.
// File: main.cpp#include <iostream>#include <fstream>#include <string>// This function reads information from the configuration filestd::string get_message_from_config(const std::string& path) { std::ifstream config_file(path); if (!config_file.is_open()) { return "Default User"; // Use default value if the config file does not exist } std::string line; while (std::getline(config_file, line)) { size_t delimiter_pos = line.find('='); if (delimiter_pos != std::string::npos) { std::string key = line.substr(0, delimiter_pos); // Trim whitespace from key key.erase(0, key.find_first_not_of(" ")); key.erase(key.find_last_not_of(" ") + 1); if (key == "recipient") { std::string value = line.substr(delimiter_pos + 1); // Trim whitespace from value value.erase(0, value.find_first_not_of(" ")); value.erase(value.find_last_not_of(" ") + 1); return value; } } } return "Default User"; // Use default value if not found}int main() { const std::string config_path = "/etc/greeter.conf"; std::string recipient = get_message_from_config(config_path); std::cout << "Hello, " << recipient << "!" << std::endl; return 0;}
3. Write Build Script (CMakeLists.txt) Also in the <span>greeter-app</span> directory, create a <span>CMakeLists.txt</span> file to instruct the compiler on how to compile the code.
# File: CMakeLists.txtcmake_minimum_required(VERSION 3.10)project(greeter)add_executable(greeter main.cpp
4. Compile to Generate Binary File The professional approach is to create a <span>build</span> directory and compile inside it, so as not to pollute the source directory.
# Create and enter build directorymkdir buildcd build# Generate build configuration with cmake, then compile with makecmake ..make
After compilation, the <span>build</span> directory will contain a file named <span>greeter</span>, which is our program. You can test it immediately:
./greeter
Since it cannot find the configuration file, it will output:<span>Hello, Default User!</span>. At this point, the first step is complete.
3. Step Two: Prepare Files for Packaging
Next, we need to place everything that needs to be included in the RPM package into a temporary “staging” directory. The structure of this directory must exactly match the structure where the files will ultimately be installed in the system.
1. Create Staging Directory and Organize Files
# Go back to the project root directory greeter-app/cd ..# Create staging directory, keeping the name consistent with "Name-Version"mkdir -p greeter-1.0/usr/binmkdir -p greeter-1.0/etc mkdir -p greeter-1.0/usr/share/doc/greeter-1.0# Copy filescp ./build/greeter greeter-1.0/usr/bin/# Note: In our spec file, permissions are set during the %install phase,# so the chmod here is technically not necessary, but it doesn't hurt to add it.chmod 755 greeter-1.0/usr/bin/greeter echo "recipient = RPM User" > greeter-1.0/etc/greeter.confecho "Greeter App v1.0" > greeter-1.0/usr/share/doc/greeter-1.0/README.md
Now, everything in the <span>greeter-1.0</span> directory is the complete content to be packaged.
2. Create Compressed Package <span>rpmbuild</span> tools typically handle <span>.tar.gz</span> format compressed packages.
# Compress everything in the staging directory into a tarballtar -czvf greeter-1.0.tar.gz greeter-1.0# Move the tarball to the rpmbuild "SOURCES" repositorymv greeter-1.0.tar.gz ~/rpmbuild/SOURCES/
4. Step Three: Write the RPM “Specification” (SPEC File)
This is the most critical step in the entire process. We need to write a <span>.spec</span> file, which serves as a detailed “installation manual” that tells <span>rpmbuild</span><span><span> how to create this RPM package.</span></span>
Create a <span>greeter.spec</span> file in the <span>~/rpmbuild/SPECS/</span> directory:
vim ~/rpmbuild/SPECS/greeter.spec
Then copy the following content into it:
%define debug_package %{nil}Name: greeterVersion: 1.0Release: 1%{?dist}Summary: A simple greeting programLicense: MITURL: https://example.com/greeterSource0: %{name}-%{version}.tar.gzRequires: libstdc++BuildArch: x86_64%descriptionThis is a "greeter" program that prints a greeting. The recipient can be configured in the /etc/greeter.conf file.%prep%setup -q%build%installrm -rf %{buildroot}install -d -m 755 %{buildroot}%{_bindir}install -d -m 755 %{buildroot}%{_sysconfdir}install -d -m 755 %{buildroot}%{_docdir}/%{name}-%{version}install -m 755 usr/bin/greeter %{buildroot}%{_bindir}/greeterinstall -m 644 etc/greeter.conf %{buildroot}%{_sysconfdir}/greeter.confinstall -m 644 usr/share/doc/greeter-1.0/README.md %{buildroot}%{_docdir}/%{name}-%{version}/README.md%files%attr(0755, root, root) %{_bindir}/greeter%attr(0644, root, root) %config(noreplace) %{_sysconfdir}/greeter.conf%attr(0644, root, root) %doc %{_docdir}/%{name}-%{version}/README.md%changelog* Tue Apr 09 2024 Your Name <[email protected]> - 1.0-1- First packaging
5. Step Four: Start Packaging
Now that the specification is written, let <span>rpmbuild</span> get to work!
# -bb means "build binary package only" (build binary)rpmbuild -bb ~/rpmbuild/SPECS/greeter.spec
Upon success, you will find the packaged file in <span>~/rpmbuild/RPMS/x86_64/</span>.
6. Step Five: Verify and Use
Finally, let’s check the results of our labor.
1. Check RPM Package Information
# View basic information about the packagerpm -qip ~/rpmbuild/RPMS/x86_64/greeter-1.0-1.el8.x86_64.rpm# View which files are included in the packagerpm -qlp ~/rpmbuild/RPMS/x86_64/greeter-1.0-1.el8.x86_64.rpm
2. Install, Run, and Uninstall
# Install (requires sudo)sudo yum localinstall ~/rpmbuild/RPMS/x86_64/greeter-1.0-1.el8.x86_64.rpm# Check if the file is in the correct location and permissions are correctls -l /usr/bin/greeter# Run the programgreeter# This time it should output: Hello, RPM User!# Check the configuration filecat /etc/greeter.conf# Uninstallsudo yum remove greeter
If you can successfully install, run, and uninstall, then you have succeeded!
Conclusion
As you can see, the entire process is quite clear: Write Code -> Compile -> Prepare Files -> Write Specification -> Package. Once you understand how to write the <span>spec</span> file, packaging any program into an RPM becomes very simple. This not only makes your software delivery more professional but also simplifies subsequent maintenance and management.
Appendix: Common Macros in RPM SPEC Files
When writing SPEC files, we extensively use macros in the form of <span>%{macro_name}</span><span><span>. These macros are pre-defined variables in the RPM system that automatically expand to the correct paths or values based on the build environment (such as operating system, CPU architecture, etc.). Using macros is key to ensuring the portability, maintainability, and standardization of software packages.</span></span>
Why Must We Use Macros?
-
Portability: The biggest benefit. For example, on older 32-bit systems, the library file directory might be
<span>/usr/lib</span><span><span>, but on modern 64-bit systems, it is </span></span><code><span>/usr/lib64</span><span><span>. If you hardcode </span></span><code><span>/usr/lib64</span><span><span> in your SPEC file, your package will not build correctly on 32-bit systems. However, using the </span></span><code><span>%{_libdir}</span><span><span> macro, </span></span><code><span>rpmbuild</span><span><span> will automatically expand it to the correct path based on the current environment.</span></span> -
Maintainability: If the system path standards change, you do not need to modify hundreds of SPEC files; you only need to update the macro definitions.
-
Consistency & Standards: The default values of these macros follow widely accepted Filesystem Hierarchy Standard (FHS), ensuring that files are installed in predictable locations that comply with community standards.
Core Build Macros
These are the most special and important macros in the packaging process.
| Macro | Description | Example Usage |
|---|---|---|
<span>%{buildroot}</span> |
Points to a temporary, empty root directory, also known as the “build root”. During the <span>%install</span> phase, all files must first be installed to this directory.<span>rpmbuild</span><span><span> will automatically create and manage it during packaging. Its path is usually </span></span><code><span>~/rpmbuild/BUILDROOT/%{name}-%{version}-%{release}.%{arch}</span><span><span>.</span></span> |
<span>install -d %{buildroot}%{_bindir}</span> |
Common Directory Macros
These macros define the paths of standard directories in the system and are the absolute mainstay of the <span>%install</span> and <span>%files</span> phases.
| Macro | Default Value (for x86_64) | Description and Usage |
|---|---|---|
<span>%{_prefix}</span> |
<span>/usr</span> |
The main prefix for most software installations. |
<span>%{_exec_prefix}</span> |
<span>%{_prefix}</span> |
The directory prefix for architecture-specific files, usually the same as <span>%{_prefix}</span>. |
<span>%{_bindir}</span> |
<span>%{_exec_prefix}/bin</span> (<span><code><span>/usr/bin</span>) |
Directory for user executable files. This is one of the most commonly used macros. |
<span>%{_sbindir}</span> |
<span>%{_exec_prefix}/sbin</span> (<span><code><span>/usr/sbin</span>) |
Directory for executable files used by system administrators. |
<span>%{_libdir}</span> |
<span>%{_exec_prefix}/lib64</span> or <span>lib</span> |
Directory for shared library files. It automatically handles the distinction between <span>lib</span><span><span> and </span></span><code><span>lib64</span><span><span>, which is extremely important!</span></span> |
<span>%{_libexecdir}</span> |
<span>%{_exec_prefix}/libexec</span> |
Directory for auxiliary binaries that are called internally by programs and are not intended for direct user execution. |
<span>%{_sysconfdir}</span> |
<span>/etc</span> |
Directory for system-level configuration files. |
<span>%{_datadir}</span> |
<span>%{_prefix}/share</span> (<span><code><span>/usr/share</span>) |
Directory for architecture-independent shared data files, such as icons, documentation, examples, etc. |
<span>%{_docdir}</span> |
<span>%{_datadir}/doc</span> (<span><code><span>/usr/share/doc</span>) |
Directory for package documentation. The <span>%doc</span><span><span> directive usually creates subdirectories here.</span></span> |
<span>%{_includedir}</span> |
<span>%{_prefix}/include</span> (<span><code><span>/usr/include</span>) |
Directory for C/C++ header files for use by other software development. |
<span>%{_mandir}</span> |
<span>%{_datadir}/man</span> (<span><code><span>/usr/share/man</span>) |
Directory for man pages. |
<span>%{_localstatedir}</span> |
<span>/var</span> |
Directory for variable state data, such as logs, caches, lock files, etc. |
Package Metadata Macros
These macros’ values come from the tags you define in the header of the SPEC file and can be referenced in the script sections (such as <span>%install</span><span><span>).</span></span>
| Macro | Source | Description |
|---|---|---|
<span>%{name}</span> |
<span>Name:</span> |
The name of the package. |
<span>%{version}</span> |
<span>Version:</span> |
The version number of the package. |
<span>%{release}</span> |
<span>Release:</span> |
The release number of the package. |
<span>%{arch}</span> |
Automatically detected | The CPU architecture of the current build, such as <span>x86_64</span><span><span>, </span></span><code><span>aarch64</span><span><span>.</span></span> |
How to View the Real Values of Macros?
You can use the <span>rpm --eval</span><span><span> command at any time to see what values any macro will expand to on your system. This is a very powerful debugging tool.</span></span>
Example:
# Check what the library file directory actually is$rpm --eval '%{_libdir}'/usr/lib64# Check the documentation directory$rpm --eval '%{_bindir}'/usr/bin# View combined results$rpm --eval '%{name}-%{version}'greeter-1.0# (Note: The above command needs to be run in a directory where name and version are defined in a spec file,# or manually provide these values)# If you want to test in an environment without a spec file$rpm --define 'name greeter' --define 'version 1.0' --eval '%{name}-%{version}'greeter-1.0
I hope this article is helpful to you. If you find the content valuable, feel free to follow and share.
