RPM package build

⚠️ This will be brief topic introduction not Production manual.

Intro

Recently I came across topic which was new to me and I've never worked on. Building RPM package. Usually application deployment involves VM, DB, Loadbalancer, Storage. Any automation (eg. Ansible, Terraform, Helm, etc.) can be used. But some deployments do use client workstation or even client infrastructure. One solution is to use Linux package deployed in remote infrastructure. As RHEL does cover 43.1% enterprise servers 1 it's natural choice. Here RPM comes to the picture.

Requirements

Idea was to build dummy 'app' which will be installed as RPM. Package should install app and service. App need to provide log messages like start, ongoing ping and stop.

For that idea we already require:

  • RPM compatible build platform
    • docker image: rockylinux:9 and rockylinux:9-ubi-init
  • RPM dev tools
  • app:
    • bash script printing messages from config
    • config
  • service

RPM package build

source: https://developers.redhat.com/articles/2021/05/21/build-your-own-rpm-package-sample-go-program

On RPM compatible OS install:

  • rpm-build rpmdevtools systemd-rpm-macros - main dev tools
  • rpmdev-setuptree - will build RPM directory ~/rpmbuild containing: BUILD, RPMS, SOURCES, SPECS, SRPMS.

SOURCES

Here you need to place your app and required extras like in my case service

├── SOURCES     ├── hello-world-1        ├── hello-world.conf        ├── hello-world.service        └── hello-world.sh     └── hello-world-1.tar.gz  

Create archive from sources:

cd SOURCES
tar -czf hello-world-1.tar.gz hello-world-1

RPM package does deliver:

  • sources (*.tar.gz)
  • package signature
  • delta update

RPM build does default to rely on archive which is being referred in spec Source0: %{name}-%{version}.tar.gz and required by %setup -q Using the %setup macro

The %setup macro:

  • Ensures that we are working in the correct directory.
  • Removes residues of previous builds.
  • Unpacks the source tarball.
  • Sets up some default privileges.

Then I'm adding -q flag which limits the verbosity of the %setup macro. Only tar -xof is executed instead of tar -xvvof.

This can be replaced by %autosetup autosetup which is combining %setup and %patch into one.

Version bump workflow In case we will be releasing new packages we will need to use Versioning. This is why we include the version number in the path SOURCES/hello-world-1.

Version 2:

cd SOURCES
cp hello-world-1 hello-world-2
<modifications>
<changelog update in hello-world.spec>
tar -czf hello-world-2.tar.gz hello-world-2

Source files:

SPEC

RPM does use *.spec to define package details - content, actions, etc.

Example spec can be found in: Creating_the_spec_file or Your First RPM Package

├── SPECS     └── hello-world.spec

Spec file does define steps to get all installed ( %{?systemd_requires}, %prep, %setup -q, %install, %post, %files) and what need to happen when package is being removed (%preun).

Following: spec / Dependencies / Requires - %post

Denotes the dependency must be present right after the package is installed, and is used a strong ordering hint to break possible dependency loops. A post-dependnecy is free to be removed once the install-transaction completes. - %preun Denotes the dependency must be present in before the package is is removed, and is used a strong ordering hint to break possible dependency loops.

As I'm about to have systemd in use %{?systemd_requires} and related details to it handling proper service state. More on systemd scriptlets: Fedora Project / systemd

  • %systemd_post %{name}.service - is taking care to make sure service is being started once package installed.
  • %systemd_preun %{name}.service - is stopping service before removing package.

Worth to mention is one addition in %files - %config(noreplace) - indicates that the file in the package should be installed with extension .rpmnew if there is already a modified file with the same name on the installed machine. config and Jon Warbrick: rpm config

Spec file:

Extras

As I'm about to use container to create package I'm defining Dockerfile and build script: build.sh

Extra files:

Build

Initiate build

./build.sh

once build completed RPM should land in dist and be ready to be installed on other systems

tree
└── dist  
   └── hello-world-1-2.el9.noarch.rpm

Verify

Container

Install & verify service

Due to use of containers, we need to make sure it is running systemd as PID 1. Therefore we need to use modified rockylinux:9 -> rockylinux:9-ubi-init designed to execute multi-service within container.

  • Run rockylinux:9-ubi-init with extra parameters to get all required privileges and mount directory with generated rpm
docker run -d \
--name hello-world \
--privileged \
--cgroupns=host \
-v /sys/fs/cgroup:/sys/fs/cgroup:rw \
-v $(pwd)/dist:/rpms \
rockylinux/rockylinux:9-ubi-init
  • Install package
docker exec hello-world bash -c "dnf install -y /rpms/hello-world-1-2.el9.noarch.rpm --allowerasing"

Inspect logs

  • Check service
docker exec hello-world systemctl status hello-world
  • Check journalctl
docker exec hello-world journalctl -u hello-world --no-pager

The service status and logs are now visible via systemctl and journalctl. Start, ping and Stop.

Lint the package

  • Verify RPM itself
# access container and install rpmlint
dnf install rpmlint

rpmlint rpms/hello-world-1-2.el9.noarch.rpm
# use rpmlint -i to get more details

rpm -V hello-world # package name

# list content of package
rpm -ql hello-world

What rpmlint actually checks?

rpmlint is a tool for checking common errors in rpm packages. It can be used to test individual packages and spec files before uploading or to check an entire distribution. https://linux.die.net/man/1/rpmlint

https://rpm-packaging-guide.github.io/#checking-rpms-for-sanity

It can be used to verify spec and rpm package itself.

Cleanup

docker stop hello-world && docker rm hello-world