6 min read

Using Python to build RPM’s of non-python code

Anyone that I’ve ever talked to about RPMs knows that I can’t stand the ‘official’ rpmbuild tools.

For whatever reason, I’ve just never got on with them. I know loads of people who use them all the time without complaint, but the whole process has always seemed dysfunctional to me. That’s why, for the vast majority of my packaging needs, I turn to fpm. Using fpm allows me to quickly create RPMs and debs with a really simple, single command line call. And since so much of my work involves shell utilities, having the ability to quickly package something up for easy distribution and installation is a must.

There’s a problem though. Sadly, fpm is itself not available from the official Red Hat repos, nor from the EPEL. In many of the companies I works with, their servers are unable to access the internet. This is obviously great from a security perspective, but not so good when it comes to installing the Ruby gems required for fpm and its dependencies.

So does this mean that I have to fall back to using the official rpmbuild tools? It probably would, but I really don’t like those tools, and trying to shoehorn them into workflows that are already based around using fpm just causes headaches. So is there another way?

Since I’m typically working in non-compiled languages like bash and R, I often just want to drop some of those scripts into an RPM for easy distribution, update, and installation. The workflow with fpm is simple; just build out the directory structure in your project directory that you want your scripts to land in when they’re installed. You then use fpm to bundle that up into an RPM. Done, simple as that.

I spent some time with a customer recently trying to bend the official rpmbuild tools to my will and sadly it seems that even something this simple is not trivial to do. After a frustrating couple of hours trying to replicate the straightforward approach of fpm, I happened to read somewhere that python’s setuptools can build RPMs. It wasn’t long after that the RPM was built and working perfectly.

Before we take a look at how this works, I want to stress, that this approach is in no way comparable to fpm. The beauty of fpm lies fundamentally in its simplicity, but it is also highly configurable and extremely flexible and I’d always prefer to use fpm over this approach. Where fpm isn’t available however, this is a working method of last resort. It’s a hack, but it gets the job done.

With that out of the way, let’s take a look at how it works. Now, I can use fpm or this approach for a variety of things, but I most commonly use it for adding custom scripts in R, bash, or python or for dropping config files onto a system. Either approach works really well in a CI/CD pipeline and combines well with other DevOps approaches.

In my project directory I’ll have a layout similar to the following:

${PROJECTDIR}/src/etc/profile.d/
${PROJECTDIR}/src/opt/myproject/

This sort of layout is really necessary for the setuptools method, but works really nicely with fpm. You just drop your files in wherever they’re needed and point fpm at the src directory. Everything below src is the locations of your files on the target system after your RPM has been installed.

So, In the example above, I’d put my scripts and things into ${PROJECTDIR}/src/opt/myproject/, but I’d also put a little myproject.sh script in ${PROJECTDIR}/src/etc/profile.d/. That would look something like this:

# simple script to add myproject to the path
export PATH=/opt/myproject/:${PATH}

This is to ensure that your files that end up in /opt/myproject are on the system $PATH and can therefore, just be run using commandname, without having to specify the full path to the command.

That’s all fine for fpm, it’s really easy to point it at the src directory and have a package created a few seconds later, but what about when we can’t just use fpm. That’s where python’s setuptools comes in. This is in the standard repos, so it’s easy to install, but how do we use it to create an RPM?

Use the following script as a template for your own project.

from distutils.core import setup
setup(name = "myproject",
    version = "0.1",
    description = "small helper utilities for R",
    author = "Mark Sellors",
    author_email = "marksemail at example.com",
    url = "https://githuburl-or-internal-project-url.com",
    license = "MIT",
    long_description = """For more information see the Github repo referenced above""",
    # data files is the destination, followed by a list of source files to put there
    data_files=[('/etc/profile.d', ['src/etc/profile.d/myproject.sh']),
                ('/opt/myproject', ['src/opt/myproject/command1',
                                      'src/opt/myproject/command2',
                                      'src/opt/myproject/command3'])
               ]
) 

Most of it is straightforward config options, but the most interesting part from the point of view of creating an arbitrary RPM package is the data_files section. This is a list of file system destinations and the files that should go in them. In the example above, you can see that we’re taking the file from src/etc/profile.d/myproject.sh and telling setuptools to put it in /etc/profile.d. Next the 3 ‘command’ files from src/opt/myproject are dropped into /opt/myproject.

Obviously the above is only a template and it would need to be adjusted for your project. You can probably guess from the file the fact that we’ve specified the full paths to the files that it’s not necessary to maintain the directory layout that I described above, but I like to maintain compatibility with fpm.

In terms of what gets built by the above, the end result is an RPM, but it literally only contains exactly what is specified in that data_files section, so it’s essential you get that right, and remember to update it as the contents of your project changes.

In order to actually build your RPM, you’ll need to run the following one-liner:

python setup.py bdist_rpm

The first time I did this, I got an error because a README file was missing, which is interesting, since even when it does build the RPM, there’s no README in it! I took yet another shortcut here and copied the project’s README.md to just plain old README, and then it built without issue. You could probably even just touch README to get around it too.

Anyway, I hope you find this useful for those occasions when fpm is not available and you’re too lazy to learn to use rpmbuild properly! At the end of the day, it’s a misuse of the tool and a total hack, but it got me out of a jam, and it might help you too.