Packaging Tutorial: GNU Hello

This tutorial demonstrates RPM packaging by packaging the GNU Hello program. While the program itself is simple, it also comes with most of the usual peripheral components of a FOSS project: configuration/build/install environment, documentation, internationalization, etc. However, it does not include RPM packaging information, therefore it is a reasonable vehicle to practice building RPMs on.

For comprehensive information on how to create RPM files, refer to RPM Reference Manual. If you plan to create an RPM package for the Fedora repository, follow the process for Joining the Package Maintainers, including following the various Fedora guidance.

This tutorial is intended to be run on a Fedora 36 system. It should, however, work also for other releases. Just replace strings like f36 with your release number.

Installing Packager Tools

Building the RPM

We need the source code of the project we are packaging, often referred to as the upstream source. We will download it from the project’s website into a directory we create for packaging GNU Hello. We are getting the compressed tarball archive, which happens to be the preferred distribution form for most FOSS projects.

$ mkdir hello && cd hello
$ wget http://ftp.gnu.org/gnu/hello/hello-2.10.tar.gz

The RPM package is configured by .spec files. We will create a template file hello.spec:

$ rpmdev-newspec hello

Inside a Spec File

The fields in our .spec file need slight editing. Please follow RPM Reference Manual’s section Spec file format for these fields. In our case, the file might start as follows:

Name:     hello
Version:  2.10
Release:  1%{?dist}
Summary:  Produces a familiar, friendly greeting
License:  GPLv3+
URL:      https://www.gnu.org/software/hello/
Source0:  https://ftp.gnu.org/gnu/hello/hello-%{version}.tar.gz

%description
The GNU Hello program produces a familiar, friendly greeting. Yes, this is
another implementation of the classic program that prints “Hello, world!” when
you run it.

%prep
%autosetup

%build
%configure
%make_build

%install
%make_install

%files

%changelog
* Sat Oct 23 2021 The Coon of Ty <Ty@coon.org> - 2.10-1
- Initial version of the package

The Version should mirror the upstream, while Release numbers our work within Fedora.

The first letter of the Summary should be uppercase to avoid rpmlint complaints.

Often, Summary and %description can be copied from the upstream README.

It is your responsibility to check the License status of the software. Inspect the source files and their LICENSE files, and talk to the authors as needed.

The %changelog should document the work on preparing the RPM, especially if there are security and bug patches included on top of the base upstream source. Changelog data can be displayed by rpm --changelog -q PACKAGE_NAME, which is very useful, for instance, to find out if specific bug and security patches were included in the installed software, thanks to the diligent Fedora packagers who include this info with the relevant CVE numbers.

The %changelog entry should include the version string to avoid rpmlint complaints.

Multi-line sections like %changelog or %description start on a line under the directive, and end when the next section starts or the file ends.

Lines which are not needed (e.g. BuildRequires and Requires) can be commented out with the hash # for now.

In many cases, many lines in the template do not need to be changed at all, at least for the initial attempt.

Building the Package

We are ready for the first run to build source, binary and debugging packages. This, and many other tasks, are done with the fedpkg tool. The production builds for Fedora are built in the Koji build system, which in turn uses Mock to manage isolated build environments. To get as close to a production build as is locally possible, we use the fedpkg mockbuild command which also invokes Mock:

$ fedpkg --release f36 mockbuild

The build environment created by Mock is very basic. It does not include a C compiler by default, so the build will fail. The reason is explained in the output:

checking whether the C compiler works... no
configure: error: in `/builddir/build/BUILD/hello-2.10':
configure: error: C compiler cannot create executables
See `config.log' for more details
error: Bad exit status from /var/tmp/rpm-tmp.D2nN0w (%build)
    Bad exit status from /var/tmp/rpm-tmp.D2nN0w (%build)

Additional build tools are defined by adding BuildRequires: rows to the specfile. In Fedora, GCC is the standard compiler, so we need to add a row for gcc. GNU Hello also uses make, so a row should be added for it, too. Add these lines after Source0:

BuildRequires:   gcc
BuildRequires:   make

Run a mockbuild again. The earlier error should be gone.

Installing files

The next thing rpm will complain about are unpackaged files, i.e. the files that would be installed in the system, but were not declared as belonging to the package. We need to declare them in the %files section. Fixing these errors is an iterative process. After declaring a missing file in the .spec file, run fedpkg again, then declare the next missing file and so on.

We will go through the file list one by one.

Executable

Installed (but unpackaged) file(s) found:
/usr/bin/hello

This is the executable binary program. /usr/bin, like many other system directories, have a default rpm macro defined. The macros should always be used when available, so the executable is listed in %files as follows:

%files
%{_bindir}/hello

Man pages

Installed (but unpackaged) file(s) found:
/usr/share/man/man1/hello.1.gz

The Packaging Guidelines have dedicated section for Manpages. Following its instructions, manpages are list as follows:

%{_mandir}/man1/hello.1.*

Texinfo pages

Installed (but unpackaged) file(s) found:
/usr/share/info/dir
/usr/share/info/hello.info.gz

Texinfo pages are handled much in the same way as man pages. The directory is defined by the default macro {_infodir}, so the Texinfo manual can be added as follows:

%{_infodir}/hello.info.*

The texinfo package has rpm triggers that automatically generate the Texinfo dir file from all the texinfo pages in the system. Thus, the dir generated by GNU Hello build script must not be installed. This is done by calling rm in %install. However, note that files are installed in the buildroot directory, and thus the removal is done like this:

%install
rm %{buildroot}/%{_infodir}/dir

Translations

Installed (but unpackaged) file(s) found:
/usr/share/locale/bg/LC_MESSAGES/hello.mo
/usr/share/locale/ca/LC_MESSAGES/hello.mo
/usr/share/locale/da/LC_MESSAGES/hello.mo
...

Since our program uses translations and internationalization, we are seeing a lot of undeclared i18n files. The recommended method to declare them is:

  1. Add the required build dependency with BuildRequires: gettext.

  2. Find the filenames in the %install step with %find_lang %{name}.

  3. Install the files with %files -f %{name}.lang.

License file

Every package must install its license, tagged with %license directive. In GNU Hello’s case, as well as for many other projects, the license file is located the source tarball’s top level, and perhaps not copied to the buildroot during installation at all. Regardless, it can be installed to the standard license directory by using a relative path:

%license COPYING

Additional documentation

Often, package sources contain documentation that could be useful for the end users as well. These can be installed and marked as documentation with the %doc directive. Similarly to %license, relative paths can be used to include files directly from the source tarball rather than from the buildroot:

%doc AUTHORS ChangeLog NEWS README THANKS TODO

Running tests

GNU Hello, like many other projects, includes an automated test suite in the sources. If at all possible, the test suite should be run during the rpm build. This helps ensuring that a working build was produced. This is done by adding the test suite invocation to specfile %check% section, which comes after %install in order. In GNU Hello’s case:

%check
make check

Run a mockbuild again and check the output to ensure that the tests were actually run. Something like this should be somewhere in the output:

============================================================================
Testsuite summary for GNU Hello 2.10
============================================================================
# TOTAL: 5
# PASS:  4
# SKIP:  1
# XFAIL: 0
# FAIL:  0
# XPASS: 0
# ERROR: 0
============================================================================

Checking the result with rpmlint

Next you should check them for conformance with RPM design rules, by running rpmlint on specfile, source rpm and binary rpm. Command fedpkg lint should do this, but as of version 1.41, it suffers from a bug causing it not to find the rpms created by fedpkg mockbuild. So instead, rpmlint needs to be called directly. Pass files to check as arguments:

$ rpmlint hello.spec results_hello/2.10/1.fc36/hello-2.10*.{x86_64,src}.rpm

If all is good, there should be no warnings or errors. In the GNU Hello case, one warning can be expected:

hello.x86_64: W: file-not-utf8 /usr/share/doc/hello/THANKS

Descriptions of various error codes can be queried with rpmlint -e <error_code>. In this case, in order to ensure a pure utf-8 installation, the file needs to be converted in %prep. This can be done with the iconv utility:

mv THANKS THANKS.old
iconv --from-code=ISO-8859-1 --to-code=UTF-8 --output=THANKS THANKS.old

A Complete hello.spec File

Here is the initial version of hello.spec:

Name:           hello
Version:        2.10
Release:        1%{?dist}
Summary:        Produces a familiar, friendly greeting

License:        GPLv3+
URL:            http://ftp.gnu.org/gnu/%{name}
Source0:        http://ftp.gnu.org/gnu/%{name}/%{name}-%{version}.tar.gz

BuildRequires:  gcc
BuildRequires:  gettext
BuildRequires:  make

%description
The GNU Hello program produces a familiar, friendly greeting. Yes, this is
another implementation of the classic program that prints “Hello, world!” when
you run it.

%prep
%autosetup
mv THANKS THANKS.old
iconv --from-code=ISO-8859-1 --to-code=UTF-8 --output=THANKS THANKS.old

%build
%configure
%make_build

%install
%make_install
rm %{buildroot}/%{_infodir}/dir
%find_lang %{name}

%check
make check

%files -f %{name}.lang
%{_mandir}/man1/hello.1.*
%{_infodir}/hello.info.*
%{_bindir}/hello
%doc AUTHORS ChangeLog NEWS README THANKS TODO
%license COPYING

%changelog
* Sat Oct 23 2021 The Coon of Ty <Ty@coon.org> 2.10-1
- Initial version of the package

With this .spec file, you should be able to successfully complete the build process, and create the source and binary RPM packages.

Checking the result

Having a working specfile and rpms built from it, the result can be checked. Before checking the result by installing the package, let us do some simple checks.

Files

List the files contained in the package:

$ dnf -C repoquery --list ./results_hello/2.10/1.fc36/hello-2.10-1.fc36.x86_64.rpm
/usr/bin/hello
/usr/lib/.build-id
/usr/lib/.build-id/39
/usr/lib/.build-id/39/c97ecb15c6292ce23e8b00e15e6e72a61e5072
/usr/share/doc/hello
/usr/share/doc/hello/AUTHORS
/usr/share/doc/hello/ChangeLog
/usr/share/doc/hello/NEWS
/usr/share/doc/hello/README
/usr/share/doc/hello/THANKS
/usr/share/doc/hello/TODO
/usr/share/info/hello.info.gz
/usr/share/licenses/hello
/usr/share/licenses/hello/COPYING
/usr/share/locale/bg/LC_MESSAGES/hello.mo
...
/usr/share/locale/zh_TW/LC_MESSAGES/hello.mo
/usr/share/man/man1/hello.1.gz

You can see that all the files listed in the specfile %files section are included, including the automatically processed locale files. Also, under /usr/lib/.build-id, there is an automatically generated file. It is actually a symlink, mapping an build id to the hello binary for debugging purposes.

Requires

List the package’s runtime dependencies with the following command:

$ dnf -C repoquery --requires ./results_hello/2.10/1.fc36/hello-2.10-1.fc36.x86_64.rpm
libc.so.6()(64bit)
libc.so.6(GLIBC_2.14)(64bit)
libc.so.6(GLIBC_2.2.5)(64bit)
libc.so.6(GLIBC_2.3)(64bit)
libc.so.6(GLIBC_2.3.4)(64bit)
libc.so.6(GLIBC_2.34)(64bit)
libc.so.6(GLIBC_2.4)(64bit)
libc.so.6(GLIBC_2.7)(64bit)
rpmlib(CompressedFileNames) <= 3.0.4-1
rpmlib(FileDigests) <= 4.6.0-1
rpmlib(PayloadFilesHavePrefix) <= 4.0-1
rpmlib(PayloadIsZstd) <= 5.4.18-1
rtld(GNU_HASH)

You can check what packages provide these dependencies as follows:

$ dnf -C repoquery --whatprovides 'libc.so.6()(64bit)'
glibc-0:2.34-11.fc36.x86_64
glibc-0:2.34-7.fc36.x86_64

You will see that the only dependency of GNU Hello is glibc, which provides symbols in libc.so.6 as well as rtld(GNU_HASH).

The rpmlib requires are special. These specify various rpm features used in the rpm package itself, constraining the version of rpm that can be used to install the package.

Provides

Conversely, to check what capabilities the package provides, you can do:

$ dnf -C repoquery --provides ./results_hello/2.10/1.fc36/hello-2.10-1.fc36.x86_64.rpm
hello = 2.10-1.fc36
hello(x86-64) = 2.10-1.fc36

The provides of this package are very simple. It simply provides its own name, in plain and architecture specific forms.

Installing

As a final check, the package can be installed and ran:

$ sudo dnf -C -y install ./results_hello/2.10/1.fc36/hello-2.10-1.fc36.x86_64.rpm
$ hello --greeting="Hello, rpm!"
Hello, rpm!

To clean up your system, undo the installation:

$ sudo dnf -C -y history undo last