This is the last post of the trilogy dedicated to how to set up a well structured Python project, developed with professional style, suitable to be used within the context of a Continuous Integration toolchain. This time we focus on how to package all we have done so far as RPM packages, showing how to break down everything into subpackages that also perform post installation tasks.
If you haven't read the previous two posts, you must do it right now since they are requisite to understand this post. In addition to that this post relies onto objects that are being created in the previous posts.
Read them in the following order:
Packet Management With RPM
The Redhat Package Manager (RPM) is an open packaging system developed by Red Hat Linux many, many years ago. Besides being used by Red Hat based or derived Linux distributions, it is used also by other distributions such as Suse.
RPM stores information of the installed packages within a system-wide database (the RPM db) where a lot of metadata such as software version and dependencies are collected.
The database can be managed and queried using the rpm command line utility: it can be run by any user, but only the "root" user has write access to the RPM database files.
This means that any user can query the RPM issuing commands like this:
rpm -qi openssh-server
output is as follows:
Name : openssh-server
Version : 8.0p1
Release : 6.el8_4.2
Architecture: x86_64
Install Date: Thu 24 Jun 2021 06:31:12 PM UTC
Group : System Environment/Daemons
Size : 1034496
License : BSD
Signature : RSA/SHA256, Fri 11 Jun 2021 12:29:28 AM UTC, Key ID 15af5dac6d745a60
Source RPM : openssh-8.0p1-6.el8_4.2.src.rpm
Build Date : Fri 11 Jun 2021 12:16:02 AM UTC
Build Host : ord1-prod-x86build003.svc.aws.rockylinux.org
Relocations : (not relocatable)
Packager : infrastructure@rockylinux.org
Vendor : Rocky
URL : http://www.openssh.com/portable.html
Summary : An open source SSH server daemon
Description :
OpenSSH is a free version of SSH (Secure SHell), a program for logging
into and executing commands on a remote machine. This package contains
the secure shell daemon (sshd). The sshd daemon allows SSH clients to
securely connect to your SSH server.
these are the information about an installed RPM package called "openssh-server": we requested to display this information by specifying that we want to query the RPM database ("-q" command switch) about information ("-i" command switch) on the already installed RPM package called "openssh-server".
The Redhat Package Manager installs packages of RPM format: this kind of package is a sophisticated archive that does not only pack a set of files and directories within a package file, but can also run scripts and evaluate conditionals.
It is made by putting a header structure on top of a CPIO archive.
The package itself is has four sections:
- the file identifier (magic number) used to identify the file contents as an RPM package
- the signature to verify the integrity of the package
- the header or "tagged" data containing package information, version numbers and copyright
- the payload, that is the actual CPIO archive containing the program files.
let's download the "epel-release" RPM package to give it a closer look:
yumdownloader epel-release
after a while, "epel-release-8-13.el8.noarch.rpm" RPM package gets downloaded to our system.
Since files provided by a RPM package are packaged within a CPIO archive, they can easily listed also without using the rpm command line utility itself as follows:
rpm2cpio epel-release-8-13.el8.noarch.rpm | cpio -i --list
output is as follows:
./etc/pki/rpm-gpg/RPM-GPG-KEY-EPEL-8
./etc/yum.repos.d/epel-modular.repo
./etc/yum.repos.d/epel-playground.repo
./etc/yum.repos.d/epel-testing-modular.repo
./etc/yum.repos.d/epel-testing.repo
./etc/yum.repos.d/epel.repo
./usr/lib/systemd/system-preset/90-epel.preset
./usr/share/doc/epel-release
./usr/share/doc/epel-release/README-epel-8-packaging.md
./usr/share/licenses/epel-release
./usr/share/licenses/epel-release/GPL
74 blocks
RPM provides several benefits - an interesting one is showing information about what a package provides even before we install it:
rpm -q --provides epel-release-8-13.el8.noarch.rpm
output is as follows:
config(epel-release) = 8-13.el8
epel-release = 8-13.el8
please note how this time we queried a package that is not installed yet .
RPM can be queried also to know what a package requires:
rpm -q --requires epel-release-8-13.el8.noarch.rpm
output is as follows:
config(epel-release) = 8-13.el8
redhat-release >= 8
rpmlib(CompressedFileNames) <= 3.0.4-1
rpmlib(FileDigests) <= 4.6.0-1
rpmlib(PayloadFilesHavePrefix) <= 4.0-1
rpmlib(PayloadIsXz) <= 5.2-1
RPM can be queried also to know if a package does execute at install or uninstall anything and the statements that are run at that time.
For example, we can query the already installed package "openssh-server" as follows:
rpm -q --scripts openssh-server
the output is as follows:
preinstall scriptlet (using /bin/sh):
getent group sshd >/dev/null || groupadd -g 74 -r sshd || :
getent passwd sshd >/dev/null || \
useradd -c "Privilege-separated SSH" -u 74 -g sshd \
-s /sbin/nologin -r -d /var/empty/sshd sshd 2> /dev/null || :
postinstall scriptlet (using /bin/sh):
if [ $1 -eq 1 ] ; then
# Initial installation
systemctl --no-reload preset sshd.service sshd.socket &>/dev/null || :
fi
preuninstall scriptlet (using /bin/sh):
if [ $1 -eq 0 ] ; then
# Package removal, not upgrade
systemctl --no-reload disable --now sshd.service sshd.socket &>/dev/null || :
fi
postuninstall scriptlet (using /bin/sh):
if [ $1 -ge 1 ] ; then
# Package upgrade, not uninstall
systemctl try-restart sshd.service &>/dev/null || :
fi
- preinstall scriptlet is run before installing the RPM package contents
- postinstall scriptlet is run after installing the RPM package contents
- preuninstall scriptlet is run before uninstalling the RPM package
- postuninstall scriptlet is run after uninstalling the RPM package
These are of course only a few of the many benefits provided by RPM, so to let you get the gist.
Setup The RPM Development Environment
Install The Required Tools
RPMs are built by having the rpmbuild command line utility: it is provided by the rpmbuild RPM package. We can install it along with other handy tools such as "rpmdevtools" and "rpmlint" as follows:
sudo dnf install -y rpm-build rpmdevtools rpmlint
Generate the RPM Development Tree
The building of RPM packages happens within the context of the so called RPM development tree: this directory tree is on a per user basis and can be easily generated as follows:
rpmdev-setuptree
it creates the following directory tree:
~/rpmbuild/
├── SOURCES
├── SPECS
├── BUILD
├── RPMS
└── SRPMS
where:
- SOURCES is the directory where archives containing the source files must be put. Archives can be tar archives, either compressed or not.
- SPECS is the directory that contains the ".spec" files: these are the files that contain all the necessary statements that describe the RPM package.
- BUILD is the directory used during the build process, where temporary files are stored, created, moved etc.
- RPMS is directory that holds the built RPM packages
- SRPMS is the directory that holds the ".src.rpm" packages: these are RPM packages that provide the archive containing the source files along with the SPEC file, so providing everything is needed to rebuild the RPM package.
Packaging as RPM
Now that we know at least the basics of RPM, we can see how to generate an RPM package that ships the Python package we created in the previous two posts.
Generate a stub SPEC file
First and foremost let's get back to the Python3 project we developed in the previous two posts - change directory to the root of the Carcano's "foolist" project:
cd ~/fooproject
and create the "RPM" and "RPM/SPECS" directories:
mkdir RPM
mkdir RPM/SPECS
cd RPM/SPECS
RPM are built by having the rpmbuild command line utility reading the SPEC file: we can easily generate a stub SPEC file as follows:
rpmdev-newspec carcano_foolist
the outcome is the "carcano_foolist.spec" file beneath "RPM/SPECS" directories: we use this stub and complete it with the missing settings that are necessary to package "carcano_foolist" as RPM.
Configure the SPEC file
We begin by editing "RPM/SPECS/carcano_foolist.spec" file by defining a few RPM macros at the very first two lines of the file:
%{!?_version: %define _version 0.0.1 }
%global srcname carcano_foolist
- the first line is about the "_version" macro, used to set the version of the built RPM packages: it can be explicitly set using the "--define" command line switch of rpmbuild, avoiding having to hard-code it within the SPEC file. The first line checks if macro is defined, and if it isn't defines it with "0.0.1" as default value.
- the second line defines the "srcname" macro with "carcano_foolist" as a value at the global scope.
Define the Package
We can go on filling-in the stub describing the package by completing the package information tags:
Name: python-%{srcname}
Version: %{_version}
Release: 1%{?dist}
Summary: Full Features Python3 Sample Project
License: LGPLv3+
Source0: %{pypi_source}
we can safely delete the URL package information tag since in this example we do not make use of it. This is the meaning of each of the above tags:
- Name: the name of the software being packaged - it must not contain spaces
- Version: the version of the software being packaged: it should be as close as possible to the format of the original software's version, although there is the constraint of avoiding o dashes
- Release: the minor release; note that it is up to the package builder to determine which build represents a new release and to update the release manually. Note that also here there is the constraint of avoiding o dashes
- Summary: a one-line description of the packaged software
- License: the license terms applicable to the software being package
- Source0: the Source tag has the purpose to both show where the software's developer has made the original sources available and provide the name of the original source file. This means that it can be either filename or URL to locate the archive containing the sources. We can specify as many sources as we need simply by incrementing the number at the end of the "Source" word.
"%{pypi_source}" is a RPM macro that can be used when having to deal with sources from PyPI to generate the proper URL: we can evaluate the macro so to see how it gets expanded by simply specifying the "--eval" command switch of the rpm command line utility:
rpm --eval "%pypi_source carcano_foolist 0.0.9-py3-none-any whl"
the output is as follows:
https://files.pythonhosted.org/packages/source/c/carcano_foolist/carcano_foolist-0.0.9-py3-none-any.whl"
RPM macros are snippets of "lua" language code: you can get the dump of all of them, along with their lua code, by simply specifying the "--showrc" command switch of the rpm command line utility as follows:
rpm --showrc
if you want to focus on the lua code of a certain macro, you have only to pipe it to sed - for example, to see the source code of "%{pypi_source}" macro we previously saw:
rpm --showrc | sed -n -e '/pypi_source/,/}/ p'
the output is:
-13: pypi_source %{lua:
local src = rpm.expand('%1')
local ver = rpm.expand('%2')
local ext = rpm.expand('%3')
local url = rpm.expand('%__pypi_url')
-- If no first argument, try %srcname, then %pypi_name, then %name
-- Note that rpm leaves macros unchanged if they are not defined.
if src == '%1' then
src = rpm.expand('%srcname')
end
if src == '%srcname' then
src = rpm.expand('%pypi_name')
end
if src == '%pypi_name' then
src = rpm.expand('%name')
end
-- If no second argument, use %version
if ver == '%2' then
ver = rpm.expand('%version'):gsub('~', '')
end
-- If no third argument, use the preset default extension
if ext == '%3' then
ext = rpm.expand('%__pypi_default_extension')
end
local first = string.sub(src, 1, 1)
print(url .. first .. '/' .. src .. '/' .. src .. '-' .. ver .. '.' .. ext)
}
From the code we can guess that if we want to use a different URL we have to set it by defining the "%__pypi_url" macro before using "%{pypi_source}" macro.
Now that we have a better understanding of RPM macros, we can go on defining / completing the following package information tags:
BuildArch: noarch
BuildRequires: python3-devel python3-setuptools
Requires: python3
this is the meaning of each of the above package information tags:
- BuildArch: the architecture the software is targeted to
- BuildRequires: the RPM packages that are required to build this RPM
- Requires: the packages that are required to run the software packaged within this RPM - they can be either RPM package or virtual packages provided by one or more RPM packages that use the provided tag. Note that version comparisons may also be included by following the package name with <, >, =, >=, or <=, and a version specification
then we complete the description tag as follows - note how this tag is multi-line so to let us provide a thorough description:
%description
Full Featured Python3 Sample Project
Define the SubPackages
What we have done so far has the sole purpose of defining the tags for the base RPM package: in this project we create two different RPM sub-packages.
Our first subpackage is "python3-carcano_foolist-common": it's purpose is packaging only the modules delivered by the "carcano_foolist" Python package itself :
%package -n python3-%{srcname}-common
Summary: %{summary}
BuildRequires: python3-devel
%{?python_provide:%python_provide python3-%{srcname}-common}
%description -n python3-%{srcname}-common
Python3 packages of the sample project
the lines to explain here are:
- the 1st is used do declare the subpackage using the "%package" information tag (note the "-n" option)
- the 4th is used to automatically guess the provided Python packages
- the 6th is used to provide the description of the subpackage using the "%description" information tag with the "-n" option followed by the name of the subpackage
The second and last subpackage is "python3-carcano_foolist" - let's define it as follows:
%package -n python3-%{srcname}
Summary: %{summary}
BuildRequires: python3-devel
Requires: python3-%{srcname}-common
%{?python_provide:%python_provide python3-%{srcname}}
%description -n python3-%{srcname}
Python3 scripts and resources of the sample project
This ends the part related to the definition of the package information tags.
Prepare the Build (the %prep Stage)
We are getting closer to the more interesting parts - let's complete the %prep section as follows:
%prep
%autosetup -n %{srcname}-%{_version}
this section is executed as the %prep stage of the RPM building process. It runs the "%autosetup" macro specifying the name of the directory where to extract the contents of the package into ("-n" option).
Build the Package (the %build Stage)
We are now ready for the actual build process: it is defined in the %build section - replace the default contents of the stub and complete it as follows:
%build
unset RPM_BUILD_ROOT
%{__python3} setup.py bdist_wheel
this section is executed as the %build stage of the RPM building process.
Install the Package into a different root directory tree (the %install Stage)
We are eventually at the install process: it is defined in the %install section - replace the default contents and complete it as follows:
The purpose of the install process is to install the files into a root directory tree different from the actual root directory - it runs during the %install stage:
%install
[ "%{buildroot}" != "/" ] && rm -rf %{buildroot}
mkdir %{buildroot}
mkdir %{buildroot}/usr
cd "%{_builddir}/%{srcname}-%{_version}/dist"
%{__python3} -m pip install --target %{buildroot}%{python3_sitelib} %{srcname}-%{_version}-py3-none-any.whl
mkdir %{buildroot}/%{_sysconfdir}
mkdir %{buildroot}/%{_sysconfdir}/fooapp
mkdir %{buildroot}/%{_sysconfdir}/rsyslog.d
mkdir %{buildroot}/usr/bin
cp %{_builddir}/%{srcname}-%{_version}/bin/logging.conf %{buildroot}/%{_sysconfdir}/fooapp/logging.conf
cp %{_builddir}/%{srcname}-%{_version}/share/doc/fooapp/rsyslog/fooapp.conf %{buildroot}/%{_sysconfdir}/rsyslog.d/fooapp.conf
cp %{_builddir}/%{srcname}-%{_version}/bin/fooapp.py %{buildroot}/usr/bin/fooapp.py
Run Unit Tests (the %check stage)
As he tbest practice requires, the contents of the Python package must succeed the unit tests before going on and packaging it as RPM. The section aimed at this is the %check section, so let's create it as follows:
%check
cd "%{_builddir}/%{srcname}-%{_version}"
unset RPM_BUILD_ROOT
%{__python3} setup.py nosetests >/dev/null
it runs during the %check stage.
List The Files To Be Packed Into Each Of The SubPackages
Now that the Python package has been prepared extracting its contents, built, installed into a chrootd tree and tests it has eventually come the time to pack the outcome into the RPM packages: the purpose of the %files sections is exactly to list the files that must be put into each of the packages.
This is the snippet of the %files section that specifies which files must be put inside the python3-carcano_foolist-common RPM package:
Are you enjoying these high quality free contents on a blog without annoying banners? I like doing this for free, but I also have costs so, if you like these contents and you want to help keeping this website free as it is now, please put your tip in the cup below:
Even a small contribution is always welcome!
%files -n python3-%{srcname}-common
%{python3_sitelib}/carcano/foolist/__init__.py
%{python3_sitelib}/carcano/foolist/__pycache__/__init__.cpython-36.opt-1.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/__init__.cpython-36.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/foolist.cpython-36.opt-1.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/foolist.cpython-36.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/foolistitem.cpython-36.opt-1.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/foolistitem.cpython-36.pyc
%{python3_sitelib}/carcano/foolist/foolist.py
%{python3_sitelib}/carcano/foolist/foolistitem.py
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/INSTALLER
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/METADATA
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/RECORD
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/WHEEL
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/top_level.txt
This is the snippet of the %files section that specifies which files must be put inside the python3-carcano_foolist RPM package:
%files -n python3-%{srcname}
%config %{_sysconfdir}/fooapp/logging.conf
%config %{_sysconfdir}/rsyslog.d/fooapp.conf
/usr/bin/fooapp.py
Add Post-Installation statements
The RPM package python3-carcano_foolist requires also to execute a few post installation statements: since it provides an additional configuration file to rsyslog, it also requires reloading rsyslog to apply the new configuration.
Post-installation tasks are defined within the %post section, where as tasks that must run after an uninstall should be put into %postun section:
%post -n python3-%{srcname}
systemctl restart rsyslog
%postun -n python3-%{srcname}
systemctl restart rsyslog
of course we must scope the %post and %postun sections so as to refer them only to the "python3-carcano_foolist" RPM package.
Add the Changelog
The %changelog section is used to provide details about the evolution of the packaged software across the releases.
%changelog
* Mon Jun 14 2021 Marco Antonio Carcano <me@mydomain.tld>
First release
Of course it is mandatory to fill-in it, but do not underestimate it: you must be tidy, since it can provide valuable information to who is installing the RPM package.
The whole SPEC file
For your convenience, this is the full listing of "RPM/SPECS/carcano_foolist.spec" file:
%{!?_version: %define _version 0.0.1 }
%global srcname carcano_foolist
Name: python-%{srcname}
Version: %{_version}
Release: 1%{?dist}
Summary: Full Features Python3 Sample Project
License: LGPLv3+
Source0: %{pypi_source}
BuildArch: noarch
BuildRequires: python3-devel python3-setuptools
Requires: python3
%description
Full Featured Python3 Sample Project
%package -n python3-%{srcname}-common
Summary: %{summary}
BuildRequires: python3-devel
%{?python_provide:%python_provide python3-%{srcname}-common}
%description -n python3-%{srcname}-common
Python3 packages of the sample project
%package -n python3-%{srcname}
Summary: %{summary}
BuildRequires: python3-devel
Requires: python3-%{srcname}-common
%{?python_provide:%python_provide python3-%{srcname}}
%description -n python3-%{srcname}
Python3 scripts and resources of the sample project
%prep
%autosetup -n %{srcname}-%{_version}
%build
unset RPM_BUILD_ROOT
%{__python3} setup.py bdist_wheel
%install
[ "%{buildroot}" != "/" ] && rm -rf %{buildroot}
mkdir %{buildroot}
mkdir %{buildroot}/usr
cd "%{_builddir}/%{srcname}-%{_version}/dist"
%{__python3} -m pip install --target %{buildroot}%{python3_sitelib} %{srcname}-%{_version}-py3-none-any.whl
mkdir %{buildroot}/%{_sysconfdir}
mkdir %{buildroot}/%{_sysconfdir}/fooapp
mkdir %{buildroot}/%{_sysconfdir}/rsyslog.d
mkdir %{buildroot}/usr/bin
cp %{_builddir}/%{srcname}-%{_version}/bin/logging.conf %{buildroot}/%{_sysconfdir}/fooapp/logging.conf
cp %{_builddir}/%{srcname}-%{_version}/share/doc/fooapp/rsyslog/fooapp.conf %{buildroot}/%{_sysconfdir}/rsyslog.d/fooapp.conf
cp %{_builddir}/%{srcname}-%{_version}/bin/fooapp.py %{buildroot}/usr/bin/fooapp.py
%check
cd "%{_builddir}/%{srcname}-%{_version}"
unset RPM_BUILD_ROOT
%{__python3} setup.py nosetests >/dev/null
%files -n python3-%{srcname}-common
%{python3_sitelib}/carcano/foolist/__init__.py
%{python3_sitelib}/carcano/foolist/__pycache__/__init__.cpython-36.opt-1.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/__init__.cpython-36.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/foolist.cpython-36.opt-1.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/foolist.cpython-36.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/foolistitem.cpython-36.opt-1.pyc
%{python3_sitelib}/carcano/foolist/__pycache__/foolistitem.cpython-36.pyc
%{python3_sitelib}/carcano/foolist/foolist.py
%{python3_sitelib}/carcano/foolist/foolistitem.py
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/INSTALLER
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/METADATA
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/RECORD
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/WHEEL
%{python3_sitelib}/carcano_foolist-%{_version}.dist-info/top_level.txt
%files -n python3-%{srcname}
%config %{_sysconfdir}/fooapp/logging.conf
%config %{_sysconfdir}/rsyslog.d/fooapp.conf
/usr/bin/fooapp.py
%post -n python3-%{srcname}
systemctl restart rsyslog
%postun -n python3-%{srcname}
systemctl restart rsyslog
%changelog
* Mon Jun 14 2021 Marco Antonio Carcano <me@mydomain.tld>
First release
since we are using nose to perform unit-tests, we must ensure that it is installed:
sudo dnf install -y python3-nose
in addition to that, let's ensure we have an as current as possible version of wheel and setuptools:
sudo python3 -m pip install -U wheel setuptools
Add the "rpm" target to the Makefile
As we did in the previous post, since we love to work within the context of a Continuous Integration toolchain, let's modify the Makefile we created so far by adding the "rpm" target as follows:
rpm: sdist
$(info -> Makefile: packaging as RPM ...)
[ -d ~/rpmbuild ] || mkdir ~/rpmbuild
[ -d ~/rpmbuild/SOURCES ] || mkdir ~/rpmbuild/SOURCES
[ -d ~/rpmbuild/SPECS ] || mkdir ~/rpmbuild/SPECS
mv src/dist/carcano_foolist-${RELEASE}.tar.gz ~/rpmbuild/SOURCES
cp RPM/SPECS/carcano_foolist.spec ~/rpmbuild/SPECS
cd ~/rpmbuild/SPECS; rpmbuild --define "_version ${RELEASE}" -ba carcano_foolist.spec
we can now build everything as a whole as follows:
export RELEASE="0.0.2"
make rpm
if everything has properly been setup, the output is as follows:
-> Makefile: validating RELEASE=0.0.2 format
-> Makefile: cleanup previous builds ...
-> Makefile: building the sdist distribution package ...
running sdist
running egg_info
creating carcano_foolist.egg-info
writing carcano_foolist.egg-info/PKG-INFO
...
Checking for unpackaged file(s): /usr/lib/rpm/check-files /home/grimoire/rpmbuild/BUILDROOT/python-carcano_foolist-0.0.2-1.el8.x86_64
Wrote: /home/grimoire/rpmbuild/SRPMS/python-carcano_foolist-0.0.2-1.el8.src.rpm
Wrote: /home/grimoire/rpmbuild/RPMS/noarch/python3-carcano_foolist-common-0.0.2-1.el8.noarch.rpm
Wrote: /home/grimoire/rpmbuild/RPMS/noarch/python3-carcano_foolist-0.0.2-1.el8.noarch.rpm
Executing(%clean): /bin/sh -e /var/tmp/rpm-tmp.0G9mQa
+ umask 022
+ cd /home/grimoire/rpmbuild/BUILD
+ cd carcano_foolist-0.0.2
+ /usr/bin/rm -rf /home/grimoire/rpmbuild/BUILDROOT/python-carcano_foolist-0.0.2-1.el8.x86_64
+ exit 0
We now have a complete project we can use into a continuous improvement loop: this means that we can go-on with the regular development cycle, fixing errors and adding features when needed and rebuilding things.
Digitally sign the RPM package
Before publishing an RPM package into a repository from where it can be downloaded, we must digitally sign it: by doing so, tools such as yum, dnf, zipper and such can verify the signature by using the public key that we publish.
Generate The GPG Key
Signing is made using the GNU Privacy Guard (GPG), so the very first thing is generating the GNU Privacy Guard (GPG) key pair used to sign the RPM packages.
Let's create the GPG key pair by entering the following command:
gpg --expert --pinentry-mode loopback --full-gen-key
This is the transcript of my answer to the questions:
Please select what kind of key you want:
(1) RSA and RSA (default)
(2) DSA and Elgamal
(3) DSA (sign only)
(4) RSA (sign only)
(7) DSA (set your own capabilities)
(8) RSA (set your own capabilities)
(9) ECC and ECC
(10) ECC (sign only)
(11) ECC (set your own capabilities)
(13) Existing key
(14) Existing key from card
Your selection? 1
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want? (2048)
Requested keysize is 2048 bits
RSA keys may be between 1024 and 4096 bits long.
What keysize do you want for the subkey? (2048)
Requested keysize is 2048 bits
Please specify how long the key should be valid.
0 = key does not expire
= key expires in n days
w = key expires in n weeks
m = key expires in n months
y = key expires in n years
Key is valid for? (0) 10y
Key expires at Sat 25 Oct 2031 05:00:56 PM UTC
Is this correct? (y/N) y
GnuPG needs to construct a user ID to identify your key.
Real name: The Grimoire Of A Modern Linux Professional
Email address: ci@grimoire.carcano.ch
Comment:
You selected this USER-ID:
"The Grimoire Of A Modern Linux Professional <ci@grimoire.carcano.ch>"
Change (N)ame, (C)omment, (E)mail or (O)kay/(Q)uit? o
Let's confirm typing "o", and wait for everything to complete.
Let's now have a look at the generated keys:
gpg --list-keys
the output is as follows:
gpg: checking the trustdb
gpg: marginals needed: 3 completes needed: 1 trust model: pgp
gpg: depth: 0 valid: 1 signed: 0 trust: 0-, 0q, 0n, 0m, 0f, 1u
gpg: next trustdb check due at 2031-10-25
/home/grimoire/.gnupg/pubring.kbx
---------------------------------
pub rsa2048 2021-10-27 [SC] [expires: 2031-10-25]
2A6018DE3B4282CE229A897E5BA662C0EBD6F747
uid [ultimate] The Grimoire Of A Modern Linux Professional <ci@grimoire.carcano.ch>
sub rsa2048 2021-10-27 [E] [expires: 2031-10-25]
The public key related to the private one used to sign must be published on a repository that can be accessed by everybody who needs to install RPM packages signed by us: otherwise they have no way to check the signature.
This mean that we must export the public key as follows:
gpg --export -a ci@grimoire.carcano.ch > ~/RPM-GPG-KEY-grimoire
and publish it on the repository.
Sign the RPM Package
The signing feature for the rpm command line utility is provided by the rpm-sign RPM package - let's install it as follows:
sudo dnf install -y rpm-sign
then we have to configure the RPM macros of the current user:
- since you can have several keys within your GPG keyring, we must specify which GPG key must be used to sign RPMs: here we use "ci@grimoire.carcano.ch", the GPG key we just created
- although this is optional, the GPG command we use to sign - here I'm doing this since I wanted to add the "--pinentry-mode loopback"
cat << \EOF >> ~/.rpmmacros
%_gpg_name <ci@grimoire.carcano.ch>
%__gpg_sign_cmd %{__gpg} gpg --force-v3-sigs --no-armor --pinentry-mode loopback --no-secmem-warning -u "%{_gpg_name}" -sbo %{__signature_filename} --digest-algo sha256 %{__plaintext_filename}'
EOF
now we have setup everything and we are eventually ready to sign the packages - the command is as follows:
rpm --addsign ~/rpmbuild/RPMS/noarch/python3-carcano_foolist*.rpm
after typing the password to unlock the secret key for the signature, the RPM packages get signed.
We can of course add a "sign" target to the Makefile:
sign:
$(info -> Makefile: digitally signing the RPM packages...)
rpm --addsign ~/rpmbuild/RPMS/noarch/python3-carcano_foolist*.rpm
if you are using a RPM repository such as Red Hat Network Satellite Server and you have a channel dedicated to the delivery of the software you develop, this target is also suitable for the purpose of pushing the signed RPM package to them too. That is: you sign your software and immediately publish it.
Just as an example, this is the command we'd use to push to Red Hat Network Satellite 6.x (or Katello):
hammer repository upload-content --organization Grimoire --product Carcano-Stuff --path ~/rpmbuild/RPMS/noarch/python3-carcano_foolist*.rpm --name my-amazing-software-repo
search the web for "configure passwordless login for hammer" if you don't want to interactively type the credentials to login to the Red Hat Network Satellite Server.
This is the command we'd use to push to Red Hat Network Satellite 5.x (or Spacewalk):
rhnpush --username=ciuser --channel "my-amazing-software-channel" --nosign --server=<FQDN> ~/rpmbuild/RPMS/noarch/python3-carcano_foolist*.rpm
to run the sign target of the Makefile, simply type:
export RELEASE="0.0.2"
make sign
again, right after typing the password to unlock the secret key for the signature, the RPM packages get signed.
Verify the Signature of the Package
Let's now pretend to be the ones that need to install the RPM package: since the delivered RPM package is signed, we must import the public key used to sign it into an RPM database:
sudo rpm --import ~/RPM-GPG-KEY-grimoire
we are now ready to install the RPM package using "yum", "dnf" or even the simple "rpm" command tool.
If we are not interested into installing it, but we anyway wot to check the signature, simply type:
rpm --checksig ~/rpmbuild/RPMS/noarch/python3-carcano_foolist*.rpm
the output is as follows:
/home/grimoire/rpmbuild/RPMS/noarch/python3-carcano_foolist-0.0.2-1.el8.noarch.rpm: digests signatures OK
/home/grimoire/rpmbuild/RPMS/noarch/python3-carcano_foolist-common-0.0.2-1.el8.noarch.rpm: digests signatures OK
the RPM packet is signed and the signature is valid if the word "signatures OK" appears at the end of the line.
Footnotes
Here it ends the third and last part of this trilogy of posts dedicated to Python: I hope you enjoyed it. You now also know how to create an RPM package. I hope you enjoyed the whole trilogy, and that it provided you with good hints about how to set up and manage your Python projects.
In this trilogy I wanted to share my style, ... probably there are things that can be improved a bit more, but at least setting things like so has proved to be a clean and easy way of doing, that promotes maintainability. I hope you appreciate the strive.
1 thought on “Packaging a Python Wheel as RPM”