Skip to content

AaronDMarasco/rpm-cookbook

Repository files navigation

Table of Contents

rpm-cookbook

Cookbook of RPM techniques

Overview

I (Aaron D. Marasco) have been creating RPM packages since the CentOS 4 timeframe(1, 2, 3). I decided to collate some of the things I've done before that I keep referencing in new projects, as well as answering some of the most common questions I come across. This should be considered a complement and not a replacement for the Fedora Packaging Guidelines. It's also not a generic "How To Make RPMs" guide, but more of a "shining a flashlight into a dusty corner to see if I can do this."

Each chapter is a separate directory, so any source code files are transcluded by Travis-CI using markdown-include. All files are available individually in the git repo — no need to copy and paste from your browser; clone the source!

Feel free to create a new chapter and submit a PR!

Quick Tips

In reviewing some of the most highly voted answers on Stack Overflow, I decided to gather a few here that don't require a full example to explain:

RPM Preprocessor is Really Dumb

Don't put single % in comments... this happens a lot. You need to double it with %%, or a multi-line macro will only have the first line commented out! Newer versions of rpmbuild will at least warn you now.

Extract All Files

Use rpm2cpio with cpio. This will extract all files treating the current directory as /:

$ rpm2cpio package-version.rpm | cpio -div

Extract a Single File

As noted here, use rpm2cpio with cpio --to-stdout:

$ rpm2cpio package-3.8.3.rpm | cpio -iv --to-stdout ./usr/share/doc/package-3.8.3/README > /tmp/README
./usr/share/doc/package-3.8.3/README
2173 blocks

Change Compression or No Compression

As noted here:

%define _source_payload w0.gzdio
%define _binary_payload w0.gzdio

These will send -0 to gzip to effectively not compress. The 0 can be 0-9 to change the level. The gz can be changed:

  • bz for bzip2
  • xz for XZ/LZMA (on some versions of RPM)

Set Output of a Shell Command Into Variable

As noted here:

%global your_var %(your commands)

Request User Input on Install

Don't. This breaks many things, for example automated configuration (KickStart or puppet).

Provide Output to User on Install

rpm will show all commands if told to be "extra verbose" with -vv. However, all output to stderr is shown to the user. Specific syntax can be found in many recipes, including an extreme example below.

Warn User if Wrong Distribution

On a previous project, we had people install the CentOS 7 RPMs on a CentOS 6 box. Normally, this would fail because things like your C libraries won't match. But it was a noarch package... This was helpful; figured I would include it here. Unfortunately, it is very CentOS-specific:

# Check if somebody is installing on the wrong platform
if [ -n "%{dist}" ]; then
  PKG_VER=`echo %{dist} | perl -ne '/el(\d)/ && print $1'`
  THIS_VER=`perl -ne '/release (\d)/ && print $1' /etc/redhat-release`
  if [ -n "${PKG_VER}" -a -n "${THIS_VER}" ]; then
    if [ ${PKG_VER} -ne ${THIS_VER} ]; then
      for i in `seq 20`; do echo ""; done
        echo "WARNING: This RPM is for CentOS${PKG_VER}, but you seem to be running CentOS${THIS_VER}" >&2
        echo "You might want to uninstall these RPMs immediately and get the CentOS${THIS_VER} version." >&2
      for i in `seq 5`; do echo "" >&2; done
    fi
  fi
fi

How to...

Define key parameters elsewhere

The techniques to define Version, Release, etc. in another file, environment variable, etc. are shown in most chapters, including Importing a Pre-Existing File Tree. As an alternative to using the rpmbuild command line's --define option, you can also pre-process the specfile using sed, autotools, etc. I've seen them all and done them all for various reasons.

Call rpmbuild from a Makefile

This is shown in most chapters, including Git Branch or Tag in Release.

Disable debug packaging

While not recommended, because debug packages are very useful, this is shown in most chapters as well.

Include Jenkins Job Number in Release

This is shown in the git chapter as Jenkins Build Number in Release

Provide Older Versions of Libraries

This is normally done using a compat package; an example is shown in Symlinks to Latest


Importing a Pre-Existing File Tree

Reasoning

This is probably one of the most common questions on Stack Overflow. It might be because you don't know enough about RPMs to "do it right" or you just want to "get it done."

I'm a little reluctant to include this, because doing it the "right way" isn't all that hard.

This is not recommended but sometimes inevitable:

  • The build system is too complicated (seldom)
  • You're packaging something already installed that...
    • ... you have no control over
    • ... was installed by a GUI installer and you want to repackage for local usage
    • ... you don't have source code for

How It Works

The Makefile takes various variables and generates a temporary tarball as well as a file listing that are used by the specfile. It uses that to build the %files directive and has an empty %build phase.

Variable Default Use Case
INPUT /opt/project Source Tree to Copy
OUTPUT /opt/project Destination on Target Machine
PROJECT myproject Base Name of the RPM
VERSION 0.1 Version of the RPM
RELEASE 1 Release/ Build of the RPM
EXTRA_SOURCES (n/a) Space-separated list of other files to add to source
OUTPUTSPECFILE project.spec RPM Specfile to use
TARBALL {PROJECT}.tar Temporary tarball used to build
RPM_TEMP {CWD}/rpmbuild-tmpdir Temporary directory to build RPM

Recipe

This recipe has two parts, a Makefile and a specfile. There is also an example .gitignore that might be useful as well.

If you have other files you want included, you can use EXTRA_SOURCES and then refer to them in your specfile, e.g. Source1: myfile. They are not in the tarball itself, so they are not automatically added to the file list. If you expect to package them, you can also add them to the %files stanza yourself, e.g. %{source1}. I personally used this feature to create a nice listing of plugins that were pre-baked into the RPM and then included them in the description.

Optional Usage 1

If you don't want to package an entire directory tree, but instead a subset, in the Makefile you can comment out filelist-$(PROJECT).txt from the .PHONY flag as well as its recipe. Then generate filelist-$(PROJECT).txt manually (e.g. by trimming the automatically created one). Only those files will then be packaged. Don't forget to force-add the file to your version control, because it is normally ephemeral and ignored.

Optional Usage 2

If your particular source tree may contain files that are identical and the user won't need to edit any of them, uncomment the two lines in the specfile referring to hardlink. This will cause any duplicate files within the RPM to be hardlinked to save space (e.g. older versions of python with identical .pyc and .pyo files). This utility is not available on all OSs.

Makefile:

# These can be overriden on the command line
PROJECT?=myproject
VERSION?=0.1
RELEASE?=1
OUTPUTSPECFILE?=project.spec

INPUT?=/opt/project
OUTPUT?=/opt/project

# End of configuration
$(info PROJECT:$(PROJECT): VERSION:$(VERSION): RELEASE:$(RELEASE))

# Make's realpath won't expand ~
REAL_INPUT=$(shell realpath -e $(INPUT))
TARBALL?=$(PROJECT).tar
RPM_TEMP?=$(CURDIR)/rpmbuild-tmpdir

ifeq ($(REAL_INPUT),)
  $(error Error parsing INPUT=$(INPUT))
endif

# Even real files are declared "phony" since dependencies otherwise broken
default: rpm
.PHONY: clean rpm $(TARBALL) filelist-$(PROJECT).txt
.SILENT: clean rpm $(TARBALL) filelist-$(PROJECT).txt

clean:
	rm -vrf $(RPM_TEMP) $(TARBALL) $(PROJECT)*.rpm filelist-$(PROJECT).txt

rpm: $(TARBALL) $(EXTRA_SOURCES)
	mkdir -p $(RPM_TEMP)/SOURCES
	cp --target-directory=$(RPM_TEMP)/SOURCES/ $^
	rpmbuild -ba \
		--define="_topdir $(RPM_TEMP)" \
		--define "outdir  $(OUTPUT)"   \
		--define "project_ $(PROJECT)" \
		--define "version_ $(VERSION)" \
		--define "release_ $(RELEASE)" \
		$(OUTPUTSPECFILE)
	cp -v --target-directory=. $(RPM_TEMP)/SRPMS/*.rpm $(RPM_TEMP)/RPMS/*/*.rpm

# The transform will replace the absolute path with a relative one with a new top-level of "proj-ver", which is what RPM prefers
$(TARBALL): filelist-$(PROJECT).txt
	echo "Building tarball of $(shell cat $< | wc -l) files"
	tar --files-from=$< --owner=0 --group=0 --absolute-names --transform 's|^$(REAL_INPUT)|$(PROJECT)-$(VERSION)|' -cf $@
	ls -halF $@

filelist-$(PROJECT).txt: $(REAL_INPUT)
	find $(REAL_INPUT) -type f -not -path '*/\.git/*' > $@

specfile:

Name: %{project_}
Version: %{version_}
Release: %{release_}%{?dist}
License: MIT
Summary: My Poorly Packaged Project
Source0: %{name}.tar
# Remove this line if you have executables with debug info in the source tree:
%global debug_package %{nil}
BuildRequires: sed tar
# BuildRequires: hardlink

%description
This RPM is effectively a fancy tarball.

%prep
set -o pipefail
# Default setup - extract the tarball
%setup -q
# Generate the file list with absolute target pathnames)
tar tf %{SOURCE0} | sed -e 's|^%{name}-%{version}|%{outdir}|' > parsed_filelist.txt
# Fix spaces in filename in manifest by putting into double quotes
sed -i -e 's/^.* .*$/"\0"/g' parsed_filelist.txt

%build
# Empty; rpmlint recommends it is present anyway

%install
%{__mkdir_p} %{buildroot}/%{outdir}/
echo "Hardlinking / copying %(wc -l parsed_filelist.txt | cut -f1 -d' ') files..."
%{__cp} --target-directory=%{buildroot}/%{outdir}/ -alR . || %{__cp} --target-directory=%{buildroot}/%{outdir}/ -aR .
%{__rm} %{buildroot}/%{outdir}/parsed_filelist.txt
# hardlink -cv %{buildroot}/%{outdir}

%clean
%{__rm} -rf --preserve-root %{buildroot}

%files -f parsed_filelist.txt

Git Problems and Tricks

Git Branch or Tag in Release

and

Monotonic Release Numbers

and

Embedding Source Hash in Description

and

Jenkins Build Number in Release

Overview

This chapter handles all of the above requirements and is very much intertwined, so you'll have to rip out the parts you want.

Reasoning

  • Branch or Tag in Release: When checking what version of your software is installed on a machine, it's nice to instantly be able to tell if it's one of your "release" versions or a development branch that somebody was working with. If it is a branch, which?
  • Monotonic Release Numbers: Git hashes aren't easily sorted, so there's no way for rpm/yum/dnf to know that 1.1-7289cc5 is actually newer than 1.1-dc650cc. By default, they would be incorrectly sorted lexicographically.
    • This allows your CI process to update a repository and yum upgrade does the right thing.
  • Embedding Source Hash: There's nothing better than "ground truth" when somebody asks for help and they can tell you exactly what RPMs they're dealing with thanks to rpm -qi yourpackage.
  • Build Number in Release: When testing RPMs, it's easier to go back and see what CI job created the RPMs.

How It Works

A little git command-line magic (along with some perl and sed regex) gets us what we want. It's not obviously straight-forward because it works around various problematic scenarios that I've experienced:

  • Detached HEAD build (e.g. Jenkins)
  • It's in both a branch and a tag
  • origin has moved forward since the checkout happened, but before our code is run, resulting in things like mybranch~2
  • The branch name is obnoxiously long because it has a prefix like bugfix--BUG13-Broken-CLI
    • Any prefix ending in -- is stripped
  • The branch has "." or other characters in it that are invalid for the RPM release field
  • A "release version" is in a specially named branch (not tag) of the format v1.0 or v.1.1.3

To compute the monotonic number, it counts the number of six-minute time periods that have passed since the last release (which requires a manual "bump" in the Makefile every version).

External information needed:

Variable Default Use Case
project myproject Base Name of the RPM
version 0.1 Version of the RPM
release snapshot<etc...> Release/ Build of the RPM
BUILD_NUMBER (n/a) Job number from Jenkins
RPM_TEMP {CWD}/rpmbuild-tmpdir Temporary directory to build RPM

Obviously, BUILD_NUMBER is Jenkins-specific. It could just as easily be CI_JOB_ID on GitLab or TRAVIS_BUILD_NUMBER for Travis-CI.

Recipe

This recipe has two parts, a Makefile and a specfile.

Makefile:

# These can be overriden on the command line
project?=myproject
version?=$(or $(git_version),0.1)
release?=snapshot$(tag)$(git_tag)
# End of configuration

RPM_TEMP?=$(CURDIR)/rpmbuild-tmpdir

##### Set variables that affect the naming of the packages
# The general package naming scheme is:
# <project>-<version>[-<release>][_<tag>][_J<job>][_<branch>][<dist>]
# where:
# <project> is our base project name
# <version> is a normal versioning scheme 1.2.3
# <release> is a label that defaults to "snapshot" if not overridden

# These are only applied if not a specific versioned release:
# <tag> is a monotonic sequence number/timestamp within a release cycle
# <job> is a Jenkins job reference if this process is run under Jenkins
# <branch> is a git branch reference if not "master" or cannot be determined
# <dist> added by RPM building, e.g. el7.x86_64

# This changes every 6 minutes which is enough for updated releases (snapshots).
# It is rebased after a release so it is relative within its release cycle.
timestamp := $(shell printf %05d $(shell expr `date -u +"%s"` / 360 - 4429931))
# Get the git branch and clean it up from various prefixes and suffixes tacked on
git_branch :=$(notdir $(shell git name-rev --name-only HEAD | \
                              perl -pe 's/~[^\d]*$//' | perl -pe 's/^.*?--//'))
git_version:=$(shell echo $(git_branch) | perl -ne '/^v[\.\d]+$/ && print')
git_hash   :=$(shell h=`(git tag --points-at HEAD | head -n1) 2>/dev/null`;\
                     [ -z "$h" ] && h=`git rev-list --max-count=1 HEAD`; echo $h)
# Any non alphanumeric (or .) strings converted to single _
git_tag    :=$(if $(git_version),,$(strip\
               $(if $(BUILD_NUMBER),_J$(BUILD_NUMBER)))$(strip\
               $(if $(filter-out undefined master,$(git_branch)),\
                    _$(shell echo $(git_branch) | sed -e 's/[^A-Za-z0-9.]\+/_/g'))))
tag:=$(if $(git_version),,_$(timestamp))

# $(info GIT_BRANCH:$(git_branch): GIT_VERSION:$(git_version): GIT_HASH:$(git_hash): GIT_TAG:$(git_tag): TAG:$(tag))
$(info PROJECT:$(project): VERSION:$(version): RELEASE:$(release) GIT_HASH:$(git_hash):)

default: rpm
.PHONY: clean rpm
.SILENT: clean rpm

clean:
	rm -vrf $(RPM_TEMP) $(project)*.rpm

rpm:
	rpmbuild -ba \
	  --define="_topdir   $(RPM_TEMP)" \
	  --define="project_  $(project)"  \
	  --define="version_  $(version)"  \
	  --define="release_  $(release)"  \
	  --define="hash_     $(git_hash)" \
	  project.spec
	cp -v --target-directory=. $(RPM_TEMP)/SRPMS/*.rpm $(RPM_TEMP)/RPMS/*/*.rpm

specfile:

Name: %{project_}
Version: %{version_}
Release: %{release_}%{?dist}
BuildArch: noarch
License: MIT
Summary: My Project That Likes Git

# Remove this line if you have executables with debug info in the source tree:
%global debug_package %{nil}

%description
Not much to say. Nothing in here. But, I know where I came from:

%{?hash_:ReleaseID: %{hash_}}

%prep
# Empty; rpmlint recommends it is present anyway

%build
# Empty; rpmlint recommends it is present anyway

%install
# Empty; rpmlint recommends it is present anyway

%clean
%{__rm} -rf --preserve-root %{buildroot}

%files
# None

Filtering Requires and Provides

Reasoning

The dependency management system is usually pretty good, but sometimes it's not perfect. For example, I had to package a version of python2 for some other software for a project. We didn't want to tell the RPM database that our libpython2.7.so was available to just anybody, but we don't want to just say "AutoReqProv: no" which is almost never the right answer and often downright overkill.

How It Works

For both Requires and Provides, RPM provides a hook before the processing, which gives you filenames it will analyze, and a hook after the processing, which gives you strings like liblua-5.3.so()(64bit).

The macros you use to add the filters are filter_(provides|requires)_in and filter_from_(provides|requires). The _in version allows the -P parameter to tell grep to use Perl-like regular expressions.

Lastly, once you use these macros to set internal variables, you call %filter_setup to read them in and create the full macros.

Recipe

Unlike other recipes, this chapter only provides blurbs; not a full working example. Again, these were from a custom packaging of python.

These all filter filenames that are fed into the /usr/lib/rpm/rpmdeps executable for analysis, which are given to grep so with my perl background, I use grep -P expressions:

%global short_version 2.7
# Don't announce to the rest of the system that they can use our python packages or shared library:
%filter_provides_in -P /site-packages/.*\.so
%filter_provides_in -P libpython%{short_version}*\.so

# This reduces the number of unimportant files looped over considerably (13K => <600, or 30min+ => 5min)
# (e.g. we know that python scripts included will need python; the no-suffix versions in bin/ should catch "our" python)
# The second backslash is important or nothing will be needed (all files excluded if they have 'c' or 'h' in them)
%global ignore_suffices .*\\.(pyc?o?|c|h|txt|rc|rst|mat|dat|decTest|pxd|html?|aiff|wav|xml|bmp|gif|jpe?g|pbm|pgm|png|ppm|ras|tiff|xbm)
%filter_requires_in -P %{ignore_suffices}
%filter_provides_in -P %{ignore_suffices}

# scipy/numpy require openblas and gfortran, which they they provide themselves:
%filter_requires_in -P .*/site-packages/(sci|num)py/.*

These filter after the analysis of the files and are sed commands, so it's usually a pattern with a d (delete) command:

# Ourselves (because we will not "provide" it) - but check OUR system library requirements first, so don't pre-filter them
%filter_from_requires /libypthon%{short_version}/d

I have found that generating the filter_from macros from bash is easy to iterate over things by concatenating strings starting with ; -- sed skips over the initial empty statement:

for f in FILES_TO_SKIP; do
  FILTER_STRING+=";/${f}/d"
done
...
rpmbuild --define="filterstring ${FILTER_STRING}" ...

Don't forget you need %filter_setup at the end to implement these filters.

One last thing that helped me with some debugging was modifying the default macros that filter_setup calls to see what the files were, for example why it was taking so long as noted above. I manually expanded what that macro did, while adding that single tee call to the __deploop macro. The other macros are what get set by the helpers above. Don't distribute spec files with these hacks in them!

%global _use_internal_dependency_generator 0
%global __deploop() while read FILE; do echo "${FILE}" | tee /dev/fd/2 | /usr/lib/rpm/rpmdeps -${1}; done | /bin/sort -u
%global __find_provides /bin/sh -c "${?__filter_prov_cmd} %{__deploop P} %{?__filter_from_prov}"
%global __find_requires /bin/sh -c "%{?__filter_req_cmd}  %{__deploop R} %{?__filter_from_req}"

Having Multiple Versions

Symlinks to Latest

Reasoning

This one is very specific to a workflow in an office I was in, and might not be "generically" useful. Their pre-RPM deployment method was to extract a tarball into a directory and then update a symlink that ended with -latest to point to it. For testing, you could simply manipulate that symlink to point to the versions you wanted to use.

This recipe allows you to build compat packages with the old libraries, similar to the official Fedora-recommended practice. However, the multiple version numbers in the RPM name can be confusing, so we replace the "." in the original version with "p", e.g. 1.0.1 => 1p0p1.

How It Works

The Makefile creates an array to generate 3 RPMs: myproject, myproject-compat1p0p1 (1.0.1 compat), and myproject-compat1p1 (1.1 compat). It will build the RPM(s) using the specfile.

Warning: The Makefile targets test and clean will use sudo to manipulate the demo RPMs to show the various effects of installation order, etc. It is recommended that you review the source before running it to ensure you are comfortable with the commands it is executing as root on your machine.

External information accepted:

Variable Default Use Case
rpm_names myproject myproject-compat1p0p1 myproject-compat1p1 RPMs to Build
version 1.2 Version of the CURRENT Project
release 1 Release/ Build of the RPM
RPM_TEMP {CWD}/rpmbuild-tmpdir Temporary directory to build RPM

The specfile is where the "real" magic is; it will:

  • Determine the "base project name" (%{base_project}) by removing anything that comes after a "-"
  • Generate the "latest" symlink name (%{target_link}) to be used in various places
  • Decide if this is the "base" RPM or one of the compatible ones
    • %if "%{name}" != "%{base_project}"
  • If it determines this is a "compat" RPM, it will:
    • Generate the version number it is compatible with (%{compat_version})
    • Automatically tell RPM that this RPM Obsoletes the original RPM with the same version numbers
    • Tells RPM that this RPM also Provides a specific exact version of the base RPM
      • This is needed if you have other RPMs that depend explicitly on certain versions (which is why you're going through this trouble in the first place)
    • Generate a %triggerpostun stanza that will "take over" the symlink if the base RPM is removed and this one left behind
      • Note: If you have multiple compat versions, which one will perform this is nondeterministic!
  • Generate a %post section that will:
    • If it is the base RPM, will always point the symlink to itself
    • If it is a compat RPM, will point a new symlink to itself iff one doesn't already exist
  • Generate a %postun section that will, if it's the last RPM uninstalled (so updates will not trigger):
    • Will remove the symlink iff it points to the RPM being removed
    • If possible alternatives still exist (/<orgdir>/<project>*), will warn if the symlink is now missing, and will present a list of candidates

Any "business logic" in %build, %install, etc. should use the same comparison above (%{name} vs. %{base_project}) to determine which files should be used (along with %{compat_version}).

What it doesn't do (or does "wrong"):

  • It does not declare the symlink as a %ghost file; this will cause it to always be removed on any package removal
    • Some more manipulations in %preun might be able to get around this, e.g. saving off a copy and then putting it back if it pointed elsewhere
  • It does not properly add the symlink to the RPM database:
    • No RPMs can depend on the exact path of the "latest" symlink
    • The RPM DB cannot be queried for it, e.g. rpm -q --whatprovides /path/to/symlink
  • It does not force the "compat" RPMs to Require the newest base; that is something you can choose to add

Recipe

This recipe has two parts, a Makefile and a specfile.

Makefile:

# These can be overriden on the command line (but will break test, sorry)
rpm_names?=myproject myproject-compat1p0p1 myproject-compat1p1
version?=1.2
release?=1
# End of configuration

RPM_TEMP?=$(CURDIR)/rpmbuild-tmpdir

default: rpms
.PHONY: clean rpms test
.SILENT: clean rpms test

clean:
	rm -vrf $(RPM_TEMP) $(foreach project_, $(rpm_names), $(project_)*.rpm)
	rpm -q --whatprovides myproject >/dev/null && rpm -q --whatprovides myproject | xargs -rt sudo rpm -ev || :

define do_build
	rpmbuild -ba \
	  --define="_topdir   $(RPM_TEMP)" \
	  --define="project_  $(project_)" \
	  --define="version_  $(version)"  \
	  --define="release_  $(release)"  \
	  project.spec
	cp -v --target-directory=. $(RPM_TEMP)/SRPMS/*.rpm $(RPM_TEMP)/RPMS/*/*.rpm
endef

rpms:
	$(foreach project_, $(rpm_names), $(do_build);)

test: clean rpms
	echo "Install main RPM:"
	sudo rpm -i myproject-1.2-1.noarch.rpm
	echo "Main RPM provides:"
	rpm -q --provides myproject
	tree -a /opt/my_org/
	echo "Install compat RPMs:"
	sudo rpm -i myproject-compat1p0p1-1.2-1.noarch.rpm myproject-compat1p1-1.2-1.noarch.rpm
	echo "Compat RPMs provide:"
	rpm -q --provides myproject-compat1p0p1 myproject-compat1p1 | sort
	echo "Who provides ANY 'myproject'?"
	rpm -q --whatprovides myproject
	echo "Compat RPMs do not take over symlink:"
	tree -a /opt/my_org/
	echo "Removing all (depending on order, a warning may occur):"
	sudo rpm -e myproject myproject-compat1p0p1 myproject-compat1p1
	echo "Now install compat 1.0.1 only:"
	sudo rpm -i myproject-compat1p0p1-1.2-1.noarch.rpm
	echo "Latest should be compat 1.0.1:"
	tree -a /opt/my_org/
	echo "Now install compat 1.1 only:"
	sudo rpm -i myproject-compat1p1-1.2-1.noarch.rpm
	echo "Latest should be compat 1.0.1 still - FIRST compat installed 'wins':"
	tree -a /opt/my_org/
	echo "Install regular; should overwrite symlink (will warn):"
	sudo rpm -i myproject-1.2-1.noarch.rpm
	tree -a /opt/my_org/
	echo "Removing compat 1.0.1 only (so no warning about broken link):"
	sudo rpm -e myproject-compat1p0p1
	tree -a /opt/my_org/
	echo "Now removing main package (should be told that 1.1 is a candidate; 1.1 should step up):"
	sudo rpm -e myproject
	tree -a /opt/my_org/
	echo "Removing compat 1.1 only (no warnings; removed symlink but no candidates remain):"
	sudo rpm -e myproject-compat1p1
	echo "Now install compat 1.1 only:"
	sudo rpm -i myproject-compat1p1-1.2-1.noarch.rpm
	echo "Symlink now 1.1:"
	tree -a /opt/my_org/
	echo "Now install compat 1.0.1 and immediately delete (it should leave symlink alone):"
	sudo rpm -i myproject-compat1p0p1-1.2-1.noarch.rpm
	sudo rpm -e myproject-compat1p0p1
	tree -a /opt/my_org/
	echo "Removing compat 1.1 only (should clean up the symlink):"
	sudo rpm -e myproject-compat1p1
	echo "What's left behind in /opt/my_org/:"
	tree -a /opt/my_org/

specfile:

Name: %{project_}
Version: %{version_}
Release: %{release_}%{?dist}
BuildArch: noarch
License: MIT
Summary: My Project That Likes Symlinks

# Remove this line if you have executables with debug info in the source tree:
%global debug_package %{nil}

%global orgdir /opt/my_org
%global outdir %{orgdir}/%{name}
# Compute the "base" project name if we are myproj-compatXpY
%global base_project %(echo %{name} | cut -f1 -d-)
%global target_link %{orgdir}/%{base_project}-latest

%if "%{name}" != "%{base_project}"
BuildRequires: /usr/bin/perl
# Convert that myproj-compatXpY to X.Y
%global compat_version %(echo %{name} | perl -ne '/-compat(.*)/ && print $1' | tr p .)
Obsoletes: %{base_project} = %{compat_version}
Provides: %{base_project} = %{compat_version}
# Take over symlink if needed (if main is removed)
%triggerpostun -- %{base_project}
[ $2 = 0 ] || exit 0
if [ ! -e %{target_link} ]; then
  >&2 echo "%{name}: %{target_link} was removed; pointing it at me instead"
  ln -s %{outdir} %{target_link}
fi
%endif

%description
Not much to say. Nothing in here.

But we share the symlink %{target_link} across two or more packages.

%prep
# Empty; rpmlint recommends it is present anyway

%build
# Empty; rpmlint recommends it is present anyway

%install
%{__mkdir_p} %{buildroot}/%{outdir}/
touch %{buildroot}/%{outdir}/myfile.txt

%post
%if "%{name}" != "%{base_project}"
# We are "compat" package - only write symlink if it doesn't already exist
if [ ! -e %{target_link} ]; then
  >&2 echo "%{name}: %{target_link} does not yet exist; setting it to point to compat-%{compat_version}"
  ln -s %{outdir} %{target_link}
fi
%else
# We are main package - always take over symlink
if [ -e %{target_link} ]; then
  >&2 echo "%{name}: %{target_link} being updated. Was `readlink -e %{target_link}`"
  rm -f %{target_link}
fi

# Add a symlink to "us"
ln -s %{outdir} %{target_link}
%endif

%postun
[ $1 = 0 ] || exit 0
# See if symlink points to us explicitly
if [ x"%{outdir}" == x"`readlink %{target_link}`" ]; then
  rm -f %{target_link}
fi

# All packages warn about missing symlink if there are potential candidates
if [ ! -e %{target_link} ]; then
  CANDIDATES=$(cd %{orgdir} && find . -maxdepth 1 -type d -name '%{base_project}*')
  if [ -n "${CANDIDATES}" ]; then
    >&2 echo "%{name}: %{target_link} is removed and may need to be manually updated; candidate(s): ${CANDIDATES}"
  fi
fi

%clean
%{__rm} -rf --preserve-root %{buildroot}

%files
%dir %{outdir}
%{outdir}/myfile.txt


Spoofing RPM Host Name

Reasoning

When distributing RPMs, you might not want people to know the build host. It shouldn't matter to the end user, and your security folks might not want internal hostnames or DNS information published for no good reason.

Newer versions of rpmbuild support defining _buildhost; I have not tested that capability myself.

How It Works

It sets LD_PRELOAD to intercept all 32- or 64-bit calls to gethostname() and gethostbyname() to replace them with the text you provide. Only later versions of rpmbuild call gethostbyname().

Recipe

This recipe requires you wrap your rpmbuild command with a script or Makefile. Using the Makefile below, you would have make call $(SPOOF_HOSTNAME) rpmbuild.

There is a default target testrpm that will build some RPMs with and without the hostname spoofing as an example; at its conclusion you should see:

Build hosts: (with spoof)
Build Host  : buildhost_x86_64.myprojectname.proj
Build Host  : buildhost_x86_64.myprojectname.proj

Scroll back in your terminal and compare this to the default output after "Build hosts: (without spoof)".

Edit the Makefile yourself where it says ".myprojectname.proj" - you can optionally not have it use the buildhost_<arch> prefix as well.

Other usage notes are at the top of the Makefile with an example at the bottom.

Makefile:

# This spoofs the build host for both 32- and 64-bit applications

default: testrpm

# To use:
# 1. Add libmyhostname as a target that calls rpmbuild
# 2. Add "myhostnameclean" as a target to your "clean"
# 3. Call rpmbuild or any other program with $(SPOOF_HOSTNAME) prefix

MYHOSTNAME_MNAME:=$(shell uname -m)
libmyhostname:=libmyhostname_$(MYHOSTNAME_MNAME).so
MYHOSTNAME_PWD:=$(shell pwd)
SPOOF_HOSTNAME:=LD_PRELOAD=$(MYHOSTNAME_PWD)/myhostname/\$LIB/$(libmyhostname)

.PHONY: myhostnameclean
.SILENT: myhostnameclean
.IGNORE: myhostnameclean
myhostnameclean:
	rm -rf myhostname

# Linux doesn't support explicit 32- vs. 64-bit LD paths like Solaris, but ld.so
# does accept a literal "$LIB" in the path to expand to lib vs lib64. So we need
# to make our own private library tree myhostname/lib{,64} to feed to rpmbuild.
.PHONY: libmyhostname
.SILENT: libmyhostname
libmyhostname: /usr/include/gnu/stubs-32.h /lib/libgcc_s.so.1
	mkdir -p myhostname/lib{,64}
	$(MAKE) -I $(MYHOSTNAME_PWD) -s --no-print-directory -C myhostname/lib   -f $(MYHOSTNAME_PWD)/Makefile $(libmyhostname) MYHOSTARCH=32
	$(MAKE) -I $(MYHOSTNAME_PWD) -s --no-print-directory -C myhostname/lib64 -f $(MYHOSTNAME_PWD)/Makefile $(libmyhostname) MYHOSTARCH=64

.SILENT: /usr/include/gnu/stubs-32.h /lib/libgcc_s.so.1
/usr/include/gnu/stubs-32.h:
	echo "You need to install the 'glibc-devel.i686' package."
	echo "'sudo yum install glibc-devel.i686' should do it for you."
	false

/lib/libgcc_s.so.1:
	echo "You need to install the 'libgcc.i686' package."
	echo "'sudo yum install libgcc.i686' should do it for you."
	false

.SILENT: libmyhostname $(libmyhostname) libmyhostname_$(MYHOSTNAME_MNAME).o libmyhostname_$(MYHOSTNAME_MNAME).c
$(libmyhostname): libmyhostname_$(MYHOSTNAME_MNAME).o
	echo "Building $(MYHOSTARCH)-bit version of hostname spoofing library."
	gcc -m$(MYHOSTARCH) -shared -o $@ $<

libmyhostname_$(MYHOSTNAME_MNAME).o: libmyhostname_$(MYHOSTNAME_MNAME).c
	gcc -m$(MYHOSTARCH) -fPIC -rdynamic -g -c -Wall $<

libmyhostname_$(MYHOSTNAME_MNAME).c:
	echo "$libmyhostname_body" > $@

define libmyhostname_body
#include <asm/errno.h>
#include <netdb.h>
#include <string.h>

int gethostname(char *name, size_t len) {
	const char *myhostname = "buildhost_$(MYHOSTNAME_MNAME).myprojectname.proj";
	if (len < strlen(myhostname))
		return EINVAL;
	strcpy(name, myhostname);
	return 0;
}

struct hostent *gethostbyname(const char *name) {
	return NULL;  /* Let it fail */
}

endef
export libmyhostname_body

## End of Recipe. Example Usage Code:
.PHONY: clean testrpm
.SILENT: clean testrpm

project?=myproject
version?=1.2
release?=3
RPM_TEMP?=$(CURDIR)/rpmbuild-tmpdir

clean: myhostnameclean

define do_build
	rpmbuild -ba \
	  --define="_topdir   $(RPM_TEMP)" \
	  --define="project_  $(project)"  \
	  --define="version_  $(version)"  \
	  --define="release_  $(release)"  \
	  project.spec
	cp -v --target-directory=. $(RPM_TEMP)/SRPMS/*.rpm $(RPM_TEMP)/RPMS/*/*.rpm
endef

testrpm: clean libmyhostname
	$(do_build)
	echo "Build hosts: (without spoof)"
	rpm -qip $(project)-$(version)-$(release).src.rpm $(project)-$(version)-$(release).noarch.rpm | grep "Build Host"
	$(SPOOF_HOSTNAME) $(do_build)
	echo "Build hosts: (with spoof)"
	rpm -qip $(project)-$(version)-$(release).src.rpm $(project)-$(version)-$(release).noarch.rpm | grep "Build Host"

About

Cookbook of RPM techniques

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published