Basics of RPM packaging
2017-10-27One of the core concepts to Linux Distributions is that of the package manager: instead of forcing the user to compile everything from source – or even worse, downloading untrusted binaries from shady third party websites – the distro provides the user with a wide assortment of pre-compiled software that can be easily installed. Package management comes with heaps of various benefits, such as dependency resolution: the package manager automatically installs any libraries or programs that might be required for the user's desired program to work correctly.
In this blog post, I'm going to describe the basics of creating an RPM package. The article assumes you're running Fedora, but save for the parts governing the use of a package manager, any other RPM-based distro should also work.
Installing the packaging tools
user $ su
root # dnf install fedora-packager @development-tools
Well, this one's rather obvious. Where we're going, we need proper tooling.
Creating a user for packaging
This step is optional, but highly recommended – when you get started with packaging, you may want
to purge some directories, or include a "remove bundled stuff" command in the package build process.
A badly written rm -rf
or find -exec rm '{}'
call might end in a disaster, so in case it ever
happens, it's better for the apocalypse to happen on a different user account than the one you use
daily.
user $ su
root # useradd rpmbuilder
root # passwd rpmbuilder
Setting up the environment
This step is fairly simple, as it involves only a single command.
user $ su --login rpmbuilder
rpmbuilder $ rpmdev-setuptree
rpmbuilder $ ls -p
rpmbuild/
rpmbuilder $ ls -p rpmbuild/
BUILD/
BUILDROOT/
RPMS/
SOURCES/
SPECS/
SRPMS/
The names of the directories in the resulting tree are quite self-explanatory, but either way, some commentary might be useful.
-
BUILD/
is a "container" directory for software builds. Each time we're going a build a package, a sub-directory will be created insideBUILD/
, and the software build itself –make
and all that stuff – will happen in said sub-directory. -
BUILDROOT/
won't actually be created byrpmdev-setuptree
... but it will appear when we build our first package. It performs a similar role toBUILD/
, in that every package getting built gets its own sub-directory withinBUILDROOT/
. During the package build, files belonging to the package get copied to said sub-directory, which, at the end, gets zipped (or, to be technically accurate, cpio'ed) and put in the package itself. And yes, to answer your question – when you install a package, the contents of this archive are simply dumped in/
. -
RPMS/
is rather self explanatory – this is the directory built packages land in. Or rather, architecture-depicting subdirectories get created here (x86_64/
,noarch/
, et cetera) and the built packages go in said subdirs. -
SOURCES/
is the directory software sources – tarballs and all that – used for building the packages are taken from. -
SPECS/
is where our package specification files (we'll get to these in a moment) go. SRPMS/
is where source RPMs – meta-packages containing all the sources and the spec file used to build a particular package – go. Since SRPMs contain source only, they're platform agnostic, and provide an easy way to "replay" a build performed on a different machine.
Writing a spec file
So, we've set up our build environment and we feel ready to build our first package. In order to do that, we need to write a description of our package – akin to a compiler generating a program based on the code we write, the RPM toolchain needs a description of our package that'll be used to generate it. The package description – or should we say, specification – is provided by a spec file.
Since there's quite a bit of stuff to cover regarding spec files, we'll go through it bit by bit.
Metadata
The spec file usually begins with a metadata section, which describes the package.
Each part of this metadata is called a tag.
Name: helloworld
Version: 1.0
Release: 2%{?dist}
Summary: Display a "Hello World!" message
License: zlib
URL: https://blog.svgames.pl
Source0: %{URL}/snippets/basics-of-rpm-packaging:helloworld-%{version}.zip
# Patch0:
BuildRequires: gcc make
# Requires:
Though the tag names are rather self-explanatory, it won't hurt to tell a little about each of them.
-
Name: is the name of the package. You'll most likely want it to reflect the name of the software being packaged. Sometimes some changes are made, e.g. lower-casing the name, or adding a suffix to distinguish between two programs using the same name (e.g.
awful-wm
for the window manager andawful-lang
for the language interpreter). -
Version: this is most often a period-separated string consisting of one or more numbers (sometimes also letters), but if you need that for whatever reason, it can be any string composed of printable ASCII characters. This number should reflect the version of the software you're packaging. The rule governing comparing versions is a simple alphabetic sort; if
strcmp()
says B goes after A, then version B is an update to version A. -
Release: is an additional field used to distinguish between the package versions. Suppose you corrected a mistake in the spec file, or you added a man page. You're still packaging the same version of upstream software, so the value of the Version: tag shouldn't change. The Release: tag allows you to specify that this is a new version of the package. Release: is usually numeric, increased after each build of the package, and reset back to 1 after changing the value of the Version: tag.
-
Summary: is the place to put a brief, one-line description of the package.
-
License: should describe the licence of the software being packaged. In Fedora, this should not hold the full licence name, but rather a short name (as described by this Fedora Wiki page). In case the software uses multiple licences, all of them should be listed, and the words "and" and "or" (possibly with parentheses) should be used to accurately describe the licensing.
-
URL: points to some kind of homepage for the software being packaged. If the software doesn't have a dedicated page, it's common to link either to the author's website, or to the landing page of the repository (e.g. in case of stuff hosted on GitHub).
-
Source0: points to the source being used build the software. This will most often be a tarball or zip file containing the source archive of the software being packaged. Source0: should ideally be a downloadable URL. In case there are many sources in use, or you want to add some files to the package (say, upstream doesn't provide a man page, or an icon), you can use multiple source tags – Source1:, Source2:, and so on.
-
Patch0: names a patch file. Sometimes there's a need to make some changes to the software being packaged – e.g. by default it comes with some telemetry features that we want to disable out of concern for our users' privacy. Similarly to the sources, there can be many patch tags.
-
BuildRequires: is a list of packages required to build our package. Depending on our style preferences, this can be either a single tag containing a space- or comma-separated list of packages, or we can repeat the tag multiple times, listing only a single package on each line. We can also specify version requirements for our dependencies, e.g.
libsomething-devel >= 1.2.0
; this will ensure that our package will refuse to build with older versions of the library. - Requires: is similar to the previous tag, but lists run-time and install-time dependencies. We don't need to put
libraries here, as the RPM toolchain is smart enough to look at the list of
.so
s our program is dependent on and figure library dependencies out by itself (at least most of the time). What we'll usually be putting in here is other programs that our software will be calling, or packages containing some non-executable data (e.g. for games and such, it's common to separate them in two: one package containing the executable itself, and the other containing all the assets: graphics, music, et cetera). As with BuildRequires:, we can specify version requirements.
Macros
You probably noticed the weird %{something}
constructs used in the above snippet. What they are is RPM macros.
Macros can generally be divided into two groups:
-
"Const-like macros" – these behave similarly to C preprocessor macros, or a simple find-and-replace. They can be used to avoid repetitions in the spec file (the way we used
%{URL}
as part of Source0:), or drag some external info into the package – e.g. the%{dist}
macro expands to a short name of the distribution we're running, like.f27
for Fedora 27. We will use them extensively later, when listing the files belonging to the package. - "Executable-like macros" – there are some macros which accept parameters. Perhaps the most commonly seen one
is
%{setup}
, which we're going to use in a while. It's worth to note that by convention, macros which take arguments are usually written without the brackets – which means%setup
instead of%{setup}
.
Build instructions
Following the metadata is the description and the build instructions.
%description
Display a "Hello world!" message.
%prep
%setup -q -n %{name}
%build
make %{?_smp_mflags}
The %description section is the place where we can put a longer, multi-line (even multi-paragraph!) description of the package. Basically everything until the next %section is considered part of the description (excessive whitespace at the ends gets removed, obviously).
The %prep section lists steps required to prepare the software for building. Most often this will only unpack
the archives and apply the patch files, though we can also perform other steps here, such as removing some
unnecessary files or extracting a licence from the source file. In this example, we employ the %setup
macro,
which is a commonly-used shorthand that extracts all the archives and cd
s into the proper directory.
The %build section describes how to build the software. This will usually just be a serie of ./configure
,
make
, cmake
and other build scripts called in order.
Installing and specifying the list of files
After our software is properly built, we need to specify which files go where.
%install
install -m 755 -d %{buildroot}%{_bindir}
install -m 755 -d %{buildroot}%{_mandir}/man1/
install -m 755 -p ./helloworld %{buildroot}%{_bindir}/%{name}
install -m 644 -p ./helloworld.man %{buildroot}%{_mandir}/man1/%{name}.1
%files
%{_bindir}/%{name}
%{_mandir}/man1/%{name}.1*
%license LICENCE.txt
The %install section contains the script used to install our software inside the buildroot (remember? the directory that gets zipped). If we're lucky and upstream provides an install script, we can use that. Here we wrote our own script – we created the _bindir and _mandir inside the build root, and then copied the executable and the man page to said directories.
The %files section lists the files belonging to the package. You may be wondering "why not just take all the files from the buildroot?" – and that's a fair question! The simple answer is: stray files. You don't want to package more than what's needed. If a file remains in the buildroot, but doesn't appear in %files, it's either superfluous and should be removed at the end of %install, or you forgot about it and should add it to %files. It also serves as a basic defence mechanism against possible mistakes when upgrading the package to a new upstream version – if the program now installs more files (or removes some of them), the mismatch between the contents of buildroot and %files will cause a build failure, alerting you of the issue.
It's recommended to use macros for directories, instead of specifying absolute paths, as that makes
managing a large package collection easier. Say, if the distro you're using decides one day that
program files should no longer go inside /usr/share
, but rather /data
– it's a matter of changing
a single macro definition, instead of modifying thousands of spec files. And if you think this is a bogus
example, then consider that some distros keep a separate /bin
and /usr/bin
, while some have fused
these two directories together. Again, if paths are specified as macros, you can use the same spec
in both cases and it should work okay.
That being said, a list of commonly used directory macros could be useful.
-
%{buildroot}
– as seen before, specifies the build (packaging) root directory. -
%{_prefix}
– a common prefix for the directories. Usually evaluates to/usr
. Most often used during %build or %install, if the makefile allows to specify the program prefix. -
%{_bindir}
– the executable directory. Usually evaluates to/usr/bin
. -
%{_libdir}
– the library directory. Usually evaluates to/usr/lib
or/usr/lib64
. -
%{_includedir}
– the includes (C header files) directory. Usually evaluates to/usr/include
. -
%{_datadir}
– the program data directory. Usually evaluates to/usr/share
. -
%{_mandir}
– the man page catalogue directory. Usually evaluates to/usr/share/man
. %{_sysconfdir}
– the system configuration directory. Usually evaluates to/etc
.
Changelog
We end the file with a changelog. Remember that this is a changelog of the package, not the upstream software! Don't copy the upstream changes here.
%changelog
* Wed Nov 01 2017 suve <veg@svgames.pl> 1.0-2
- Fix typo in description
- Install manpage
* Wed Nov 01 2017 suve <veg@svgames.pl> 1.0-1
- Initial packaging
...and we're finally done with the spec file. Obviously, there's quite a lot more that can be said about packaging, as the spec file presented here was rather simple. I'll get to that in a future article.
Validating the spec file
One more thing that we can do before building our package is making sure the spec file is top-notch.
To do that, we can use the rpmlint
utility.
rpmbuilder $ rpmlint ~/rpmbuild/SPECS/helloworld.spec
0 packages and 1 specfiles checked; 0 errors, 0 warnings.
Building the package
It looks like we're finally getting somewhere! Now that our spec file is ready, let's try building the package. Remember to put the sources and the spec file in the proper directories, first.
rpmbuilder $ cp ~/downloads/helloworld.zip ~/rpmbuild/SOURCES/
rpmbuilder $ cp ~/downloads/helloworld.spec ~/rpmbuild/SPECS/
rpmbuilder $ rpmbuild -ba ~/rpmbuild/SPECS/helloworld.spec
If anything failed along the way, we should get an error message somewhere among the output of rpmbuild
.
If everything went correct, then hurray! Our package is built. We can take a look in the RPMS/
directory.
rpmbuilder $ ls -p ~/rpmbuild/RPMS/
x86_64/
rpmbuilder $ ls -p ~/rpmbuild/RPMS/x86_64/
helloworld-1.0-2.x86_64.rpm
There's our baby. We can go ahead and redistribute that to whoever we want to.
Comments
Do you have some interesting thoughts to share? You can comment by sending an e-mail to blog-comments@svgames.pl.