Creating and hosting your own rpm packages and yum repo

5 minute read     Updated:

Alex Couture-Beil %     Alex Couture-Beil

This tutorial is a follow up to creating and hosting your own deb and apt repo, but is written for creating rpm packages for redhat-based Linux distributions such as Fedora, CentOS, and Rocky Linux.

Prerequisites

This tutorial assumes you are using CentOS 8, and that the following packages are installed:

sudo yum install -y createrepo rpm-build rpm-sign wget gcc python3 yum-utils

If you don’t have a CentOS machine, you can still follow along using docker, just run

docker run -ti --rm centos:8 /bin/bash

to create a temporary CentOS environment to follow along with.

Step 0: Creating a Simple Hello World Program

We will be using the same basic hello world program that was used in the previous tutorial. In this tutorial, we will assume you have a binary located under ~/example/hello-world-program/hello-world.

To quickly create a hello world binary using C, run:

mkdir -p ~/example/hello-world-program
echo '#include <stdio.h>
int main() {
    printf("Hello World!\\n");
    return 0;
}' | gcc -o ~/example/hello-world-program/hello-world -x c -

This tutorial will only cover distributing binaries in rpm packages and will not cover creating source-based rpms. There’s other tutorials that cover creating spec files – the main goal of this tutorial is to document how to package a pre-compiled binary, and show how to generate your own self-hosted yum repository.

Step 1: Creating a rpm Package

Redhat-based Linux distributions use .rpm packages to package and distribute programs.

let’s start by making a directory to work out of:

mkdir -p ~/example/rpm-work-dir

Inside this directory, we will create a spec file which contains package metadata such as the name, and summary of the package:

cd ~/example/rpm-work-dir
echo "Summary: A simple program that prints hello
Name: hello-world
Version: 1.0.0
Release: 1
URL: https://example.com
Group: System
License: example # https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Software_License_List
Packager: Example Team
Requires: bash
BuildRoot: ~/example/rpm-work-dir # this should be replaced with your working directory where the spec is saved

%description
An example package containing a hello-world binary

%install
mkdir -p %{buildroot}/usr/bin/
cp ~/example/hello-world-program/hello-world %{buildroot}/usr/bin/hello-world

%files
/usr/bin/hello-world

%changelog
* Thu Jun 17 2021 alex <alex@earthly.dev>
- initial example
" > hello-world.spec

Next, we will use rpmbuild to produce the rpm:

rpmbuild --target "x86_64" -bb hello-world.spec

This will output an rpm under /root/rpmbuild/RPMS/x86_64/hello-world-1.0.0-1.x86_64.rpm.

We can inspect the contents with:

rpm -qpivl --changelog --nomanifest /root/rpmbuild/RPMS/x86_64/hello-world-1.0.0-1.x86_64.rpm

which will output something similar to:

Name        : hello-world
Version     : 1.0.0
Release     : 1
Architecture: x86_64
Install Date: (not installed)
Group       : System
Size        : 10832
License     : example # https://fedoraproject.org/wiki/Licensing:Main?rd=Licensing#Software_License_List
Signature   : (none)
Source RPM  : hello-world-1.0.0-1.src.rpm
Build Date  : Fri 18 Jun 2021 09:21:39 PM UTC
Build Host  : 83f4a9d4e295
Relocations : (not relocatable)
Packager    : Example Team
URL         : https://example.com
Summary     : A simple program that prints hello
Description :
An example package containing a hello-world binary
* Thu Jun 17 2021 alex <alex@earthly.dev>
- initial example

-rwxr-xr-x    1 root    root                    10832 Jun 18 21:21 /usr/bin/hello-world
drwxr-xr-x    2 root    root                        0 Jun 18 21:21 /usr/lib/.build-id
drwxr-xr-x    2 root    root                        0 Jun 18 21:21 /usr/lib/.build-id/b9
lrwxrwxrwx    1 root    root                       31 Jun 18 21:21 /usr/lib/.build-id/b9/fea765874fcd0863841bc8f5d1aa1d65396751 -> ../../../../usr/bin/hello-world

You can also inspect the content via less /root/rpmbuild/RPMS/x86_64/hello-world-1.0.0-1.x86_64.rpm. Under the hood less will invoke /usr/bin/lesspipe.sh. You can perform a global regular expression print (grep) against the script to find the same command being used:

grep rpm /usr/bin/lesspipe.sh

This should print the line where the same rpm -qpivl ... command is being invoked:

*.rpm) rpm -qpivl --changelog --nomanifest -- "$1"; exit $? ;;

Next, we can install our rpm via:

yum install -y /root/rpmbuild/RPMS/x86_64/hello-world-1.0.0-1.x86_64.rpm

Then finally, we can test it was installed by running

hello-world

which should print

Hello World!

Step 2: Creating a yum Repository

Next we’re going to create a yum repository which can be uploaded to a server to make it easier to share your package with other users.

Our first step will be to import a pgp key which we will be using to sign our rpm packages and repo, to allow our users to verify the packages have not been tampered. Creating pgp keys was covered in our previous tutorial; in particular, we will assume you have public and private keys stored under ~/example/pgp-key.public and ~/example/pgp-key.private respectively.

First let’s import our private key, so we have access to it for signing the repo:

gpg --import ~/example/pgp-key.private

This should output something similar to:

gpg: directory '/root/.gnupg' created
gpg: keybox '/root/.gnupg/pubring.kbx' created
gpg: /root/.gnupg/trustdb.gpg: trustdb created
gpg: key E1933532750E9EEF: public key "example <example@example.com>" imported
gpg: key E1933532750E9EEF: secret key imported
gpg: Total number processed: 1
gpg:               imported: 1
gpg:       secret keys read: 1
gpg:   secret keys imported: 1

We will then configure the rpm tools to use this key for signing our packages and repository:

echo "%_signature gpg
%_gpg_name E1933532750E9EEF" > /root/.rpmmacros

Don’t forget to replace E1933532750E9EEF with your key’s ID.

Let’s create a directory for our packages:

mkdir -p ~/example/packages/

Then copy our rpm(s) into this directory:

cp /root/rpmbuild/RPMS/x86_64/hello-world-1.0.0-1.x86_64.rpm ~/example/packages/.

Since we didn’t configure a key in step 1, the rpm we created was not signed. We can add a signature to it by running:

rpm --addsign ~/example/packages/*.rpm

If you forgot to create /root/.rpmmacros, or forgot to update the key ID, you might see an error such as You must set "%_gpg_name" in your macro file or gpg: signing failed: No secret key.

Once all the packages are signed, we will use createrepo to create the repository:

cd ~/example/packages/
createrepo .

Finally, we will sign the repodata metadata by running:

gpg --detach-sign --armor repodata/repomd.xml

Step 3: Testing the Repository

We are going to use python to temporarily create a web server to serve the contents of our repository:

cd ~/example
python3 -m http.server

We will create a config for this server:

echo "[example-repo]
name=Example Repo
baseurl=http://127.0.0.1:8000/packages
enabled=1
gpgcheck=1
gpgkey=http://127.0.0.1:8000/pgp-key.public" > ~/example/example.repo

Next let’s configure our machine to use this new repository:

yum-config-manager --add-repo http://127.0.0.1:8000/example.repo

Then we can install our package from the self-hosted repository with:

yum install -y hello-world

In this example, we were hosting the entire contents of ~/example, which may include your private key if you followed this tutorial completely and created it under ~/example/pgp-key.private. In practice, you would never serve a directory containing your private key.

Appendix A: A Complete Example using Earthly

A complete example has been created under github.com/earthly/example-yum-repo/Earthfile.

This Earthfile contains all the above steps from this tutorial in a single location, which can be run directly in a single shot with:

earthly -P github.com/earthly/example-yum-repo:main+test

Alternatively, you can clone the repo and run +test directly.

Alex Couture-Beil %
Alex Couture-Beil

Alex enjoys writing code, growing vegetables, and the great outdoors.