RPM Packaging with the OBS – Part 2
A while back I wrote a post about how to get started packaging software using the openSUSE Build Service by setting up a home project and branching an existing package. This post will go into more detail on how to create a new package from scratch. Keep in mind that I’m just learning this too, so take everything I say with a grain of salt. =)
I assume that you already have osc installed, which is the build service’s command-line client. If you’re using openSUSE, you should be able to get it from the default repos; if you’re using another distribution, you may be able to find a package here, or if that fails, you can always get it from source. Most of the actions described here, such as creating a package, can also be done from the build service’s web interface. Whether you use the web interface or the command-line client is entirely up to you, but I’ll focus primarily on osc usage in this post. The web interface is great, but it’s also far easier to learn on your own, plus I always like encouraging people to use the command-line whenever they can.
Note: my intent here is to not only create a software package, but to do it using best practices. Why bother doing something if you’re not going to do it well?
Creating a Package
First, if you haven’t yet, you’ll need to initialize a local copy of your home project. Create a folder somewhere to store your OBS packages, then cd into it and run osc init home:<username>. If this is your first run, it will prompt you for your username and password.
From your local project, you can create a new package with osc mkpac <packagename>. This method of creating a package will only create a local copy in a new folder; you will need to add sources and commit the changes before it’s available on the server.
The Specfile
The specfile is where all the magic happens, and it is by far the most complicated part of RPM packaging. Fortunately, there is a handy tool for generating new specfiles in the rpmdevtools package. openSUSE users can install it from here, and Fedora users should be able to find it included in the main repos. The basic usage is
$ rpmdev-newspec -t <TYPE> <NAME>
The available types are dummy, lib, minimal, and some language-specific ones for OCaml, Perl, PHP, Python, R, and Ruby. Running it will create a new file called <NAME>.spec. For users who don’t have the tool installed, this is what the minimal template looks like, which I’ll be using in a later example.
At the top of the specfile are several different pieces of information that you will need to fill in about your package.
- Name – the name of your package (should be filled in)
- Version – the version number for your package’s source
- Release – current release number (defaults to 1)
- License – the license used by the packaged software
- Summary – short description of the package
- URL – where the user can find more information about this package’s software
- Source0 – name of the tarball containing the software’s sources
- BuildRequires / Requires – list of build / runtime dependencies respectively
Of the above fields, name, version, source, and dependencies (if any) must be filled in; everything else is not strictly required in order to get the package to build, but information like the license and summary will be required if you plan on submitting your package to a repository. In general it’s just good practice to make sure you don’t leave any fields blank.
You’ll notice that there’s one field commented out, and that’s BuildArch. When using the openSUSE build service, the available architectures for a package are specified in its metadata, not the specfile, so there’s no need to fill it in here.
Below these fields you’ll find several different sections, each with a different header.
%description – basically an extended summary. The description is specified in its own section in order to allow the use of newlines. Detailed information about what your package is should go here.
%prep – commands to prepare for the build. The default implementation uses a macro called %setup, which should be all you need in this section for the majority of use cases. The %setup macro expects the source to be specified as a tarball with one root directory called <name>-<version>. It will then take care of extracting the archive and cd’ing into the root folder.
%build – this is where the project source is compiled. The default implementation simply calls make with %{?_smp_mflags}, which will usually contain parameters like -j16 that add support for parallel building. Note: any line of code in the sections %prep, %build, %install, and %clean not starting with a percent sign (which denotes an RPM macro) is interpreted as simple bash. This gives a lot of control over exactly how the software is built. Macros exist to serve two purposes: variable substitution (like %{?_smp_mflags}) and automation of common tasks (like %setup); when neither one of those is needed, you can treat these sections as you would a shellscript.
%install – install the compiled software into the filesystem. The default implementation calls make again, this time with the install target. Since RPM building is done in an isolated environment, we have to make sure that the software is installed into that environment and not our computer’s filesystem, so we pass in a parameter DESTDIR that is set to that environment’s root. Note: any time the filesystem needs to be accessed with an absolute path, make sure you use macros to do it. For example, if you need to manually copy a file to /usr/bin, use cp file/to/copy %{buildroot}%{_bindir}. This has the added advantage of making cross-platform and cross-architecture builds easier since there are fewer hard-coded paths.
%clean – cleans up after the build. Defaults to deleting the entire build environment, which is also the default behavior if the %clean section isn’t specified at all. Unless you have very specific cleaning requirements, then this shouldn’t need to be changed.
%files – a list of files that your package is expected to install. The default implementation simply includes the %doc macro, which tells it to include the folder %{_docdir}/%{name} (the default value is /usr/share/doc/packages/<name>). Appending additional file names to the end of %doc will specify individual files contained in that folder. Note: it is considered an error to install a file that isn’t specified in this section and vice versa. This is mostly for security reasons, though admittedly it can be a pain in the ass, especially if you’re not intimately familiar with the software you’re packaging. Using wildcards can help, though; for example, if your package installs several files in /usr/bin, then you can take care of all of them with the line %{_bindir}/*. Second Note: the %{buildroot} macro isn’t necessary, since it’s just a list of files that are expected to be there on the target system. No actual system modification takes place here.
%changelog – a list of recent changes. This won’t become important until you have to start maintaining your package for a large userbase, and it can remain blank for the time being.
Let’s Write Some Code
To go through a full demonstration of packaging, we’ll first need something to package. We’ll start with the ever-popular hello world example. =)
First, if you haven’t yet, create a new package with osc mkpac and cd into its folder (I’m going to assume a package name of “hello” for this example). Before we create the specfile, though, we need to create our project’s sources. Here’s an example project structure:
hello-1.0
| - Makefile
| - src
| - hello.c
The file hello.c is just your standard everyday hello world:
#include <stdio.h>
int main(void)
{
printf("hello world\n");
return 0;
}
And the Makefile is pretty standard too:
CC := clang
CFLAGS := -g -Wall
TARGET := hello
SOURCES := $(shell find src -type f -name *.c)
OBJECTS := $(patsubst src/%,build/%,$(SOURCES:.c=.o))
DEPS := $(OBJECTS:.o=.deps)
PREFIX = /usr/local
BINDIR = $(PREFIX)/bin
all: $(TARGET)
$(TARGET): $(OBJECTS)
@echo " Linking..."; $(CC) $^ -o $(TARGET)
build/%.o: src/%.c
@mkdir -p build
@echo " CC $<"; $(CC) $(CFLAGS) -MD -MF $(@:.o=.deps) -c -o $@ $<
install: all
@echo " Installing..."; install -D $(TARGET) $(DESTDIR)$(BINDIR)/$(TARGET)
uninstall:
@echo " Uninstalling..."; $(RM) $(DESTDIR)$(BINDIR)/$(TARGET)
clean:
@echo " Cleaning..."; $(RM) -r build $(TARGET)
-include $(DEPS)
.PHONY: all install uninstall clean
Note that this Makefile includes an install target and uses the DESTDIR variable to determine where files should be installed. Both of these are there in order to make it easy to use in packaging, as these are both expected by the specfile.
Now that our source is written, we need to prepare it for packaging. Run the following commands from your package’s folder (which should be the parent of hello-1.0):
$ tar -cvzf hello-1.0.tar.gz hello-1.0/ $ rm -rf hello-1.0
Now we have our source packaged up into a tarball. We can now create the specfile:
$ rpmdev-newspec -t minimal hello
Open the specfile in an editor and make sure you have the following fields specified:
- Name: hello
- Version: 1.0
- Source0: %{name}-%{version}.tar.gz
- BuildRequires: clang
You should also put in values for summary, license, and URL, but those aren’t necessary for building. Note that I put clang as a build requirement, since it’s the compiler specified in the Makefile, but no runtime requirements.
The minimal default specfile is almost good enough to work without additional changes; however, there are a couple more things we need to add:
- In the %install section, at the end of the make install command, add
PREFIX=%{_usr}. By convention, Makefile installations default to /usr/local, but packages should install files to /usr. - In the %files section, add a line with
%{_bindir}/*. The only file we want to install will be put in /usr/bin, and this wildcard will catch it for us.
One last thing to check is that your home project has the desired repositories enabled. You can run osc meta prj -e to edit your project’s metadata. Here’s an example if you want to build your packages against openSUSE 12.1:
<project name="home:kog13">
<repository name="openSUSE_12.1">
<path project="openSUSE:12.1" repository="standard"/>
<arch>i586</arch>
<arch>x86_64</arch>
</repository>
</project>
After that, you can try a local build with osc build. It will first check the dependencies and cache any that haven’t been cached yet (the first run can take a little while), you’ll be prompted for the root password, and then the build starts. If all goes well, you should get no errors. Assuming you didn’t provide a custom build root, the resulting RPM file can be found somewhere in the vicinity of /var/tmp/build-root/home/abuild/rpmbuild/RPMS.
If you want to make the package available on the server, then run osc add * followed by osc commit. This will push your files to the server and schedule a remote build, whose status you can check either through the web interface or via osc results.
rpmlint
You may not have gotten any errors, but odds are that rpmlint spat back a whole bunch of warnings at you. rpmlint is a tool that analyzes your specfile and makes sure you didn’t do anything stupid. The warnings are very descriptive and usually pretty self-explanatory, and there are too many of them to cover in this post. You can find a list of common checks and what they mean here.
Sometimes you’ll want to tell rpmlint to interpret a check differently, or to skip one entirely. In cases like these, you can define custom rpmlint behaviors using an rc file. The file should be named <name>.rpmlintrc and included as a source, so if you have Source0 set to your tarball, then you can add another value called Source1 set to your rc file. The next time you run a build, rpmlint will take your rc file into account when deciding what errors and warnings to show you.
Hello, World!
And there you have it, your first RPM package created from scratch! In a future post I will cover the process of taking an existing software project and creating a package for it, which is where the real fun comes in.
Until then, happy packaging.





