Chapter 6. Advanced Topics

This chapter covers topics that are beyond the scope of the introductory tutorial but are often useful in real-world RPM packaging.

6.1. Signing Packages

Signing a package is a way to secure the package for an end user. Secure transport can be achieved with implementation of the HTTPS protocol, which can be done when the package is downloaded just before installing. However, the packages are often downloaded in advance and stored in local repositories before they are used. The packages are signed to make sure no third party can alter the content of a package.

There are three ways to sign a package:

6.1.1. Adding a Signature to a Package

In most cases packages are built without a signature. The signature is added just before the release of the package.

In order to add another signature to the package package, use the --addsign option. Having more than one signature makes it possible to record the package’s path of ownership from the package builder to the end-user.

As an example, a division of a company creates a package and signs it with the division’s key. The company’s headquarters then checks the package’s signature and adds the corporate signature to the package, stating that the signed package is authentic.

With two signatures, the package makes its way to a retailer. The retailer checks the signatures and, if they check out, adds their signature as well.

The package now makes its way to a company that wishes to deploy the package. After checking every signature on the package, they know that it is an authentic copy, unchanged since it was first created. Depending on the deploying company’s internal controls, they may choose to add their own signature, to reassure their employees that the package has received their corporate approval.

The output from the --addsign option:

$ rpm --addsign blather-7.9-1.i386.rpm
            Enter pass phrase:

Pass phrase is good.
blather-7.9-1.i386.rpm:

To check the signatures of a package with multiple signatures:

$ rpm --checksig blather-7.9-1.i386.rpm
blather-7.9-1.i386.rpm: size pgp pgp md5 OK

The two pgp strings in the output of the rpm --checksig command show that the package has been signed twice.

RPM makes it possible to add the same signature multiple times. The --addsign option does not check for multiple identical signatures.

$ rpm --addsig blather-7.9-1.i386.rpm
              Enter pass phrase:

Pass phrase is good.
blather-7.9-1.i386.rpm:
$ rpm --addsig blather-7.9-1.i386.rpm
              Enter pass phrase:

Pass phrase is good.
blather-7.9-1.i386.rpm:
$ rpm --addsig blather-7.9-1.i386.rpm
              Enter pass phrase:

Pass phrase is good.
blather-7.9-1.i386.rpm:
$ rpm --checksig blather-7.9-1.i386.rpm
blather-7.9-1.i386.rpm: size pgp pgp pgp pgp md5 OK

The output of the rpm --checksig command displays four signatures.

6.1.2. Replacing a Package Signature

To change the public key without having to rebuild each package, use the --resign option.

$ rpm --resign blather-7.9-1.i386.rpm
            Enter pass phrase:

Pass phrase is good.
blather-7.9-1.i386.rpm:

To use the --resign option on multiple package files:

$ rpm --resign b*.rpm
            Enter pass phrase:

Pass phrase is good.
blather-7.9-1.i386.rpm:
bother-3.5-1.i386.rpm:

6.1.3. Build-time Signing

To sign a package at build-time, use the rpmbuild command with the --sign option. This requires entering the PGP passphrase.

For example:

$ rpmbuild -ba --sign blather-7.9.spec
            Enter pass phrase:

Pass phrase is good.
* Package: blather
…
Binary Packaging: blather-7.9-1
Finding dependencies...
…
Generating signature: 1002
Wrote: /usr/src/redhat/RPMS/i386/blather-7.9-1.i386.rpm
…
Source Packaging: blather-7.9-1
…
Generating signature: 1002
Wrote: /usr/src/redhat/SRPMS/blather-7.9-1.src.rpm

The "Generating signature" message appears in both the binary and source packaging sections. The number following the message indicates that the signature added was created using PGP.

Note

When using the --sign option for rpmbuild, use only -bb or -ba options for package building.

To verify the signature of a package, use the rpm command with --checksig option. For example:

$ rpm --checksig blather-7.9-1.i386.rpm
blather-7.9-1.i386.rpm: size pgp md5 OK

6.1.3.1. Building Multiple Packages

When building multiple packages, use the following syntax to avoid entering the PGP passphrase multiple times. For example when building the blather and bother packages, sign them by using the following:

$ rpmbuild -ba --sign b*.spec
              Enter pass phrase:

Pass phrase is good.
* Package: blather
…
Binary Packaging: blather-7.9-1
…
Generating signature: 1002
Wrote: /usr/src/redhat/RPMS/i386/blather-7.9-1.i386.rpm
…
Source Packaging: blather-7.9-1
…
Generating signature: 1002
Wrote: /usr/src/redhat/SRPMS/blather-7.9-1.src.rpm
…
* Package: bother
…
Binary Packaging: bother-3.5-1
…
Generating signature: 1002
Wrote: /usr/src/redhat/RPMS/i386/bother-3.5-1.i386.rpm
…
Source Packaging: bother-3.5-1
…
Generating signature: 1002
Wrote: /usr/src/redhat/SRPMS/bother-3.5-1.src.rpm

6.2. More on Macros

There are many built-in RPM Macros and we will cover a few in the following section, however an exhaustive list can be found at the rpm.org rpm macro official documentation.

There are also macros that are provided by Red Hat Enterprise Linux, some of which we cover in this section. We also see how to inspect your system to learn about other macros.

6.2.1. Defining Your Own Macros

You can define your own Macros. Below is an excerpt from the RPM Official Documentation, which provides a comprehensive reference on macros capabilities.

To define a macro, use:

%global <name>[(opts)] <body>

All whitespace surrounding \ is removed. Name may be composed of alphanumeric characters, and the character _ and must be at least 3 characters in length. A macro without an (opts) field is “simple” in that only recursive macro expansion is performed. A parameterized macro contains an (opts) field. The opts (the string between parentheses) is passed exactly as is to getopt(3) for argc/argv processing at the beginning of a macro invocation.

Note

Older RPM SPEC files may use the %define <name> <body> macro pattern. The differences between %define and %global macros are as follows:

  • %define has local scope, which means that it applies only to a specified part of a SPEC file. In addition, the body of a %define macro is expanded when used—​it is lazily evaluated.
  • %global has global scope, which means that it applies to an entire SPEC file. In addition, the body of a %global macro is expanded at definition time.

Examples:

%global githash 0ec4e58
%global python_sitelib %(%{__python} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")
Note

Macros are always evaluated, even in comments. Sometimes it is harmless. But in the second example, we are executing python command to get the content of a macro. This command will be executed even when you comment out the macro. Or when you put the name of the macro into %changelog. To comment out macro, use %%. For example: %%global.

6.2.2. %setup

Macro %setup can be used to build the package with source code tarballs. Standard behavior of the %setup macro can be seen in the rpmbuild output. At the beginning of each phase macro outputs Executing(%something). For example:

Executing(%prep): /bin/sh -e /var/tmp/rpm-tmp.DhddsG

The shell output is set with set -x enabled. To see the content of /var/tmp/rpm-tmp.DhddsG use --debug option, since rpmbuild deletes temporary files after successful build. This displays the setup of environment variables followed by for example:

cd '/builddir/build/BUILD'
rm -rf 'cello-1.0'
/usr/bin/gzip -dc '/builddir/build/SOURCES/cello-1.0.tar.gz' | /usr/bin/tar -xof -
STATUS=$?
if [ $STATUS -ne 0 ]; then
  exit $STATUS
fi
cd 'cello-1.0'
/usr/bin/chmod -Rf a+rX,u+w,g-w,o-w .

The %setup ensures that we are working in the right directory, removes residues of previous builds, unpacks the source tarball, and sets up some default privileges. There are multiple options to adjust the behavior of the %setup macro.

6.2.2.1. %setup -q

Option -q limits verbosity of %setup macro. Only tar -xof is executed instead of tar -xvvof. This option has to be used as first.

6.2.2.2. %setup -n

In some cases, the directory from expanded tarball has a different name than expected %{name}-%{version}. This can lead to an error of the %setup macro. The name of a directory has to be specified by -n directory_name option.

For example, if the package name is cello, but the source code is archived in hello-1.0.tgz and contained hello/ directory, the spec file content needs to be:

Name: cello
Source0: https://example.com/%{name}/release/hello-%{version}.tar.gz
…
%prep
%setup -n hello

6.2.2.3. %setup -c

The -c option can be used if the source code tarball does not contain any subdirectories and after unpacking, files from an archive fill the current directory. The -c option creates the directory and steps into the archive expansion. An illustrative example:

/usr/bin/mkdir -p cello-1.0
cd 'cello-1.0'

The directory is not changed after archive expansion.

6.2.2.4. %setup -D and -T

-D option disables deleting of source code directory. This option is useful if %setup macro is used several times. Essentially, -D option means that following lines are not used:

rm -rf 'cello-1.0'

The -T option disables expansion of the source code tarball by removing the following line from the script:

/usr/bin/gzip -dc '/builddir/build/SOURCES/cello-1.0.tar.gz' | /usr/bin/tar -xvvof -

6.2.2.5. %setup -a and -b

Options -a and -b expand specific sources.

  • Option -b (which stands for before) expands specific sources before entering the working directory.
  • Option -a (which stands for after) expands those sources after entering. Their arguments are source numbers from the spec file preamble.

For example, let’s say the cello-1.0.tar.gz archive contains empty examples directory, and the examples are shipped in separate examples.tar.gz tarball and they expand into the directory of the same name. In this case use -a 1, as we want to expand Source1 after entering the working directory:

Source0: https://example.com/%{name}/release/%{name}-%{version}.tar.gz
Source0: examples.tar.gz
…
%prep
%setup -a 1

But if the examples were in the separate cello-1.0-examples.tar.gz tarball, which expands into cello-1.0/examples, use -b 1 options, since the Source1 should be expanded before entering the working directory:

Source0: https://example.com/%{name}/release/%{name}-%{version}.tar.gz
Source0: %{name}-%{version}-examples.tar.gz
…
%prep
%setup -b 1

You can also use a combination of all these options.

6.2.3. %files

Common “advanced” RPM Macros needed in the %files section are as follows:

Macro

Definition

%license

This identifies the file listed as a LICENSE file and it will be installed and labeled as such by RPM. Example: %license LICENSE

%doc

This identifies the file listed as documentation and it will be installed and labeled as such by RPM. This is often used not only for documentation about the software being packaged but also code examples and various items that should accompany documentation. In the event code examples are included, care should be taken to remove executable mode from the file. Example: %doc README

%dir

Identifies that the path is a directory that should be owned by this RPM. This is important so that the rpm file manifest accurately knows what directories to clean up on uninstall. Example: %dir %{_libdir}/%{name}

%config(noreplace)

Specifies that the following file is a configuration file and therefore should not be overwritten (or replaced) on a package install or update if the file has been modified from the original installation checksum. In the event that there is a change, the file will be created with .rpmnew appended to the end of the filename upon upgrade or install so that the pre-existing or modified file on the target system is not modified. Example: %config(noreplace) %{_sysconfdir}/%{name}/%{name}.conf

6.2.4. Built-In Macros

Your system has many built-in RPM Macros and the fastest way to view them all is to simply run the rpm --showrc command. Note that this will contain a lot of output so it is often used in combination with a pipe to grep.

You can also find information about the RPMs macros that come directly with your system’s version of RPM by looking at the output of the rpm -ql rpm taking note of the files titled macros in the directory structure.

6.2.5. RPM Distribution Macros

Different distributions will supply different sets of recommended RPM Macros based on the language implementation of the software being packaged or the specific Guidelines of the distribution in question.

These are often provided as RPM Packages themselves and can be installed with the distribution package manager, such as yum. The macro files themselves once installed can be found in /usr/lib/rpm/macros.d/ and will be included in the rpm --showrc output by default once installed.

One primary example of this is the Fedora Packaging Guidelines section pertaining specifically to Application Specific Guidelines which at the time of this writing has over 60 different sets of guidelines along with associated RPM Macro sets for subject matter specific RPM Packaging.

One example of this kind of RPMs would be for Python version 2.x and if we have the python2-rpm-macros package installed (available in EPEL for RHEL 7), we have a number of python2 specific macros available to us.

$ rpm -ql python2-rpm-macros
/usr/lib/rpm/macros.d/macros.python2

$ rpm --showrc | grep python2
-14: __python2  /usr/bin/python2
CFLAGS="%{optflags}" %{__python2} %{py_setup} %{?py_setup_args} build --executable="%{__python2} %{py2_shbang_opts}" %{?1}
CFLAGS="%{optflags}" %{__python2} %{py_setup} %{?py_setup_args} install -O1 --skip-build --root %{buildroot} %{?1}
-14: python2_sitearch   %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib(1))")
-14: python2_sitelib    %(%{__python2} -c "from distutils.sysconfig import get_python_lib; print(get_python_lib())")
-14: python2_version    %(%{__python2} -c "import sys; sys.stdout.write('{0.major}.{0.minor}'.format(sys.version_info))")
-14: python2_version_nodots     %(%{__python2} -c "import sys; sys.stdout.write('{0.major}{0.minor}'.format(sys.version_info))")

The above output displays the raw RPM Macro definitions, but we are likely more interested in what these will evaluate to which we can do with rpm --eval in order to determine what they do as well as how they may be helpful to us when packaging RPMs.

$ rpm --eval %{__python2}
/usr/bin/python2

$ rpm --eval %{python2_sitearch}
/usr/lib64/python2.7/site-packages

$ rpm --eval %{python2_sitelib}
/usr/lib/python2.7/site-packages

$ rpm --eval %{python2_version}
2.7

$ rpm --eval %{python2_version_nodots}
27

6.3. Custom Macros

You can override the distribution macros in the ~/.rpmmacros file. Any changes you make will affect every build on your machine.

There are several macros you can use to override:

%_topdir /opt/some/working/directory/rpmbuild
You can create this directory, including all subdirectories using the rpmdev-setuptree utility. The value of this macro is by default ~/rpmbuild.
%_smp_mflags -l3
This macro is often used to pass to Makefile, for example make %{?_smp_mflags}, and to set a number of concurrent processes during the build phase. By default, it is set to -jX, where X is a number of cores. If you alter the number of cores, you can speed up or slow down a build of packages.

While you can define any new macros in the ~/.rpmmacros file, this is discouraged, because those macros would not be present on other machines, where users may want to try to rebuild your package.

6.4. Epoch, Scriptlets, and Triggers

There are various topics in the world of RPM spec files that are considered advanced because they have implications on not only the spec file, how the package is built, but also on the end machine that the resulting RPM is installed upon. In this section we will cover the most common of these such as Epoch, Scriptlets, and Triggers.

6.4.1. Epoch

First on the list is Epoch, epoch is a way to define weighted dependencies based on version numbers. It’s default value is 0 and this is assumed if an Epoch directive is not listed in the RPM spec file. This was not covered in the spec File section of this guide because it is almost always a bad idea to introduce an Epoch value as it will skew what you would normally otherwise expect RPM to do when comparing versions of packages.

For example if a package foobar with Epoch: 1 and Version: 1.0 was installed and someone else packaged foobar with Version: 2.0 but simply omitted the Epoch directive either because they were unaware of it’s necessity or simply forgot, that new version would never be considered an update because the Epoch version would win out over the traditional Name-Version-Release marker that signifies versioning for RPM Packages.

This approach is generally only used when absolutely necessary (as a last resort) to resolve an upgrade ordering issue which can come up as a side effect of upstream software changing versioning number schemes or versions incorporating alphabetical characters that can not always be compared reliably based on encoding.

6.4.2. Triggers and Scriptlets

In RPM Packages, there are a series of directives that can be used to inflict necessary or desired change on a system during install time of the RPM. These are called scriptlets.

One primary example of when and why you’d want to do this is when a system service RPM is installed and it provides a systemd unit file. At install time we will need to notify systemd that there is a new unit so that the system administrator can run a command similar to systemctl start foo.service after the fictional RPM foo (which provides some service daemon in this example) has been installed. Similarly, we would need to inverse of this action upon uninstallation so that an administrator would not get errors due to the daemon’s binary no longer being installed but the unit file still existing in systemd’s running configuration.

There are a small handful of common scriptlet directives, they are similar to the “section headers” like %build or %install in that they are defined by multi-line segments of code, often written as standard POSIX shell script but can be a few different programming languages such that RPM for the target machine’s distribution is configured to allow them. An exhaustive list of these available languages can be found in the RPM Official Documentation.

Scriptlet directives are as follows:

Directive

Definition

%pre

Scriptlet that is executed just before the package is installed on the target system.

%post

Scriptlet that is executed just after the package is installed on the target system.

%preun

Scriptlet that is executed just before the package is uninstalled from the target system.

%postun

Scriptlet that is executed just after the package is uninstalled from the target system.

Is is also common for RPM Macros to exist for this function. In our previous example we discussed systemd needing to be notified about a new unit file, this is easily handled by the systemd scriptlet macros as we can see from the below example output. More information on this can be found in the Fedora systemd Packaging Guidelines.

$ rpm --showrc | grep systemd
-14: __transaction_systemd_inhibit      %{__plugindir}/systemd_inhibit.so
-14: _journalcatalogdir /usr/lib/systemd/catalog
-14: _presetdir /usr/lib/systemd/system-preset
-14: _unitdir   /usr/lib/systemd/system
-14: _userunitdir       /usr/lib/systemd/user
/usr/lib/systemd/systemd-binfmt %{?*} >/dev/null 2>&1 || :
/usr/lib/systemd/systemd-sysctl %{?*} >/dev/null 2>&1 || :
-14: systemd_post
-14: systemd_postun
-14: systemd_postun_with_restart
-14: systemd_preun
-14: systemd_requires
Requires(post): systemd
Requires(preun): systemd
Requires(postun): systemd
-14: systemd_user_post  %systemd_post --user --global %{?*}
-14: systemd_user_postun        %{nil}
-14: systemd_user_postun_with_restart   %{nil}
-14: systemd_user_preun
systemd-sysusers %{?*} >/dev/null 2>&1 || :
echo %{?*} | systemd-sysusers - >/dev/null 2>&1 || :
systemd-tmpfiles --create %{?*} >/dev/null 2>&1 || :

$ rpm --eval %{systemd_post}

if [ $1 -eq 1 ] ; then
        # Initial installation
        systemctl preset  >/dev/null 2>&1 || :
fi

$ rpm --eval %{systemd_postun}

systemctl daemon-reload >/dev/null 2>&1 || :

$ rpm --eval %{systemd_preun}

if [ $1 -eq 0 ] ; then
        # Package removal, not upgrade
        systemctl --no-reload disable  > /dev/null 2>&1 || :
        systemctl stop  > /dev/null 2>&1 || :
fi

Another item that provides even more fine grained control over the RPM Transaction as a whole is what is known as triggers. These are effectively the same thing as a scriptlet but are executed in a very specific order of operations during the RPM install or upgrade transaction allowing for a more fine grained control over the entire process.

The order in which each is executed and the details of which are provided below.

all-%pretrans
...
any-%triggerprein (%triggerprein from other packages set off by new install)
new-%triggerprein
new-%pre      for new version of package being installed
...           (all new files are installed)
new-%post     for new version of package being installed

any-%triggerin (%triggerin from other packages set off by new install)
new-%triggerin
old-%triggerun
any-%triggerun (%triggerun from other packages set off by old uninstall)

old-%preun    for old version of package being removed
...           (all old files are removed)
old-%postun   for old version of package being removed

old-%triggerpostun
any-%triggerpostun (%triggerpostun from other packages set off by old un
            install)
...
all-%posttrans

The above items are from the included RPM documentation found in /usr/share/doc/rpm-4.*/triggers.

6.4.2.1. Using Non-Shell Scripts in spec File

A scriptlet option, -p, in a spec file allows to invoke a specific interpreter instead of the default -p /bin/sh. An illustrative example is a script, which prints out a message after the installation of pello.py.

  1. Open the pello.spec file.
  2. Find the following line:

    install -m 0644 %{name}.py* %{buildroot}/usr/lib/%{name}/

    Under this line, insert the following code:

    %post -p /usr/bin/python3
    print("This is {} code".format("python"))
  3. Build your package according to the Section 5.2, “Building RPMS” chapter.
  4. Install your package:

    # yum install /home/<username>/rpmbuild/RPMS/noarch/pello-0.1.1-1.fc27.noarch.rpm

    The output of this command is the following message after the installation:

    Installing       : pello-0.1.1-1.fc27.noarch                              1/1
    Running scriptlet: pello-0.1.1-1.fc27.noarch                              1/1
    This is python code
Note
  • To use a Python 3 script: Write a line %post -p /usr/bin/python3 under the line install -m in a spec file.
  • To use a Lua script: Write a line %post -p <lua> under the line install -m in a spec file.
  • This way any interpreter can be specified in the spec file.

6.5. RPM Conditionals

RPM Conditionals enable the conditional inclusion of various sections of the spec file.

Most commonly, conditional inclusions deal with:

  • architecture-specific sections
  • operating system-specific sections
  • compatibility issues between various versions of operating systems
  • existence and definition of macros

6.5.1. RPM Conditionals Syntax

If expression is true, then do some action:

%if expression
...
%endif

If expression is true, then do some action, in other case, do another action:

%if expression
...
%else
...
%endif

6.5.2. RPM Conditionals Examples

6.5.2.1. The %if Conditional

%if 0%{?rhel} == 6
sed -i '/AS_FUNCTION_DESCRIBE/ s/^/#/' configure.in
sed -i '/AS_FUNCTION_DESCRIBE/ s/^/#/' acinclude.m4
%endif

This conditional handles compatibility between RHEL6 and other operating systems in terms of support of the AS_FUNCTION_DESCRIBE macro. When the package is build for RHEL, the %rhel macro is defined and it is expanded to RHEL version. If its value is 6, meaning the package is build for RHEL 6, then the references to AS_FUNCTION_DESCRIBE, which is not supported by RHEL6, are deleted from autoconfig scripts.

%define ruby_archive %{name}-%{ruby_version}
%if 0%{?milestone:1}%{?revision:1} != 0
%define ruby_archive %{ruby_archive}-%{?milestone}%{?!milestone:%{?revision:r%{revision}}}
%endif

This conditional handles definition of the macros. If the %milestone or the %revision macros are set, the %ruby_archive macro, which defines the name of the upstream tarball, is redefined.

6.5.2.2. Specialized variants of %if Conditional

The %ifarch conditional, %ifnarch conditional and %ifos conditional are specialized variants of the %if conditionals. These variants are commonly used, so they have their own macros.

6.5.2.2.1. The %ifarch Conditional

The %ifarch conditional is used to begin a block of the SPEC file that is architecture-specific. It is followed by one or more architecture specifiers, each separated by commas or whitespace.

%ifarch i386 sparc
...
%endif

All the contents of the spec file between %ifarch and %endif are processed only on the 32-bit AMD and Intel architectures or Sun SPARC-based systems.

6.5.2.2.2. The %ifnarch Conditional

The %ifnarch conditional has a reverse logic than %ifarch conditional.

%ifnarch alpha
...
%endif

All the contents of the spec file between %ifnarch and %endif are processed only if not being done on a Digital Alpha/AXP-based system.

6.5.2.2.3. The %ifos Conditional

The %ifos conditional is used to control processing based on the operating system of the build. It can be followed by one or more operating system names.

%ifos linux
...
%endif

All the contents of the spec file between %ifos and %endif are processed only if the build was done on a Linux system.