Building RHEL packages with Tito
Are you a Fedora packager and consider Tito to be a valuable asset in your toolbox? Do you know it can be used for maintaining RHEL packages as well? Or any downstream packaging? I didn’t. This article explains how it can be done.
Fedora vs RHEL packaging
Apart from different hostnames, service names, and a great focus on quality assurance, there is only one difference relevant to the topic at hand. That is, in the majority of cases (unless there is a good reason to do so), the package sources tarball is not being changed within an RHEL major release. While this may sound insignificant, it is the only reason for this whole article, so let me elaborate.
We have an imaginary upstream project
foo in version
project gets packaged into Fedora as
foo-1.0-1 (i.e. package name is
foo, its upstream version is
1.0 and this is the first release of
this version in Fedora). When this package gets included in RHEL, its
NVR is going to be the same,
foo-1.0-1. So far there is no
Updating this package is when it gets tricky. Upstream publishes
1.1. In Fedora, we take the new upstream sources as they
are, and build a package
foo-1.1-1 on top of them. In RHEL, we want
to avoid changing the sources. Instead, we create a patch (or series
of patches) that modifies the original sources into the newly
published ones. Therefore the new package in RHEL will be
(the version number remains the same, release is incremented).
We can choose to do all this patching labor manually or let Tito help us.
This initial setup needs to be done only once for each package. It’s a bit lengthy but the payoff is worth it.
Create an intermediate git repository
First, create an empty git repository on some internal forge (e.g. GitLab) and clone it to your computer.
git clone email@example.com:bar/foo.git ~/git/rhel/foo cd ~/git/rhel/foo
In case that you use a different email address for internal purposes, configure your git credentials.
git config user.name "John Doe" git config user.email "firstname.lastname@example.org"
Add an upstream repository as a new remote for this git project. We will use this remote only for pulling, so make sure to use its HTTPS variant instead of the SSH one. It will help us prevent accidental pushing of sensitive information out to the world.
git remote add upstream https://github.com/bar/foo.git
Pull everything from upstream.
git fetch --all
Go and see what is the version (ignore the release number) of our
package in RHEL, and point the
main branch to the Tito tag
associated with this version. For example, if the package name is
foo and its version is
1.5-3, run the following command.
git reset --hard foo-1.5-1
And finally, push everything to the internal repository.
git push --follow-tags
.tito/tito.props file and update the
builder = tito.distributionbuilder.DistributionBuilder tagger = tito.tagger.ReleaseTagger
DistributionBuilder handles the patches generation (from the
current RHEL version into the latest upstream version) when building a
ReleaseTagger increments release number instead of
a version number when tagging a new package.
.tito/releasers.conf and append the following releaser.
[rhel] releaser = tito.release.DistGitReleaser branches = rhel-8.5.0
In this example I am specifying
rhel-8.5.0 branch, please insert
the branch that you maintain your package in. In the case of multiple
branches, use spaces as separators.
Update the spec file
There may be some RHEL specific changes to our package spec file in
the internal DistGit, that we wouldn’t like to get lost. Let’s assume
that the latest upstream version is
1.7-1 and the latest RHEL
- Find the spec file for your package in the internal DistGit service
- Append (from the top) all the changelog entries recorded between
- Set the current RHEL release number. In this example
- Perform any additional changes that were made in the RHEL spec file
- In case the RHEL spec file contains some RHEL-specific patches, do
not copy the patch files and add
PatchN:records in the spec file. Instead, perform those changes directly in this repository and commit them.
- Commit all the changes that we made to the spec file and in the
Cherry-pick upstream changes
Now, we are going to cherry-pick the upstream changes that we would like to include in RHEL. Let’s see what changes were made between the version we have in RHEL and the upstream one.
git log master..upstream/master
For each commit that you want in RHEL, run
git cherry-pick <hash>
It is a good idea to avoid commits generated by Tito or any commits incrementing the version number or modifying the changelog.
Build and test the package locally
To make sure we made all changes correctly, we are going to build the package locally and test it works as expected before irreversibly pushing anything to DistGit and eventually embarrassing ourselves with amends.
tito build --srpm --test
Build the package locally in Mock, or in the internal Copr instance which provides an actual RHEL chroots.
mock -r rocky-8-x86_64 /tmp/tito/foo-1.5-3.git.0.da1346d.fc33.src.rpm
Examine the built package, try to install it (in Docker, Mock, etc) and test that it works as expected.
Push the changes
If you are sure, tag the package, and push the changes.
tito tag git push --follow-tags origin
By this point, we should be able to build a non-test SRPM package. We won’t need it but it is a good idea to make sure it works.
tito build --srpm
Push everything into the internal DistGit and submit builds for all predefined branches.
tito release rhel
When asked if you want to edit the commit message, proceed with yes. You must reference a ticket requesting this update, e.g.
Just make sure all the submitted builds succeeded and continue with the rest of the update process.
I haven’t done this part yet, thus it will be explained right after I get through it. The general idea is to run
git fetch --all
Diff contains binary files
If any of the patches that
DistributionBuilder generates should
contain binary files, you will end up with a fatal error (with a
rather nice wording).
ERROR: You are doomed. Diff contains binary files. You can not use this builder
In this case, I would suggest trying
builder = tito.builder.UpstreamBuilder
The difference between those two is that
generates one patch per upstream version and therefore if any of the
intermediate patches contains binary files, the build fails. Whereas
UpstreamBuilder always generates only one patch file at all times,
therefore any intermediate changes don’t matter, the patch will
simply contain changes for the resulting upstream state (i.e. if the
latest upstream release is alright, the build will succeed).