Table of Contents
- rpm-cookbook
Cookbook of RPM techniques
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!
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:
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.
Use rpm2cpio
with cpio
. This will extract all files treating the current directory as /
:
$ rpm2cpio package-version.rpm | cpio -div
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
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 bzip2xz
for XZ/LZMA (on some versions of RPM)
As noted here:
%global your_var %(your commands)
Don't. This breaks many things, for example automated configuration (KickStart or puppet).
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.
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
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.
This is shown in most chapters, including Git Branch or Tag in Release.
While not recommended, because debug packages are very useful, this is shown in most chapters as well.
This is shown in the git
chapter as Jenkins Build Number in Release
This is normally done using a compat
package; an example is shown in Symlinks to Latest
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
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 |
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.
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.
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.
# 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/*' > $@
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
and
and
and
This chapter handles all of the above requirements and is very much intertwined, so you'll have to rip out the parts you want.
- 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 that1.1-7289cc5
is actually newer than1.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.
- This allows your CI process to update a repository and
- 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.
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 likemybranch~2
- The branch name is obnoxiously long because it has a prefix like
bugfix--BUG13-Broken-CLI
- Any prefix ending in
--
is stripped
- Any prefix ending in
- 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
orv.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.
This recipe has two parts, a Makefile
and a specfile.
# 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
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
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.
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.
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}"
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
.
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 the version number it is compatible with (
- 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
- Some more manipulations in
- 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
This recipe has two parts, a Makefile
and a specfile.
# 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/
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
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.
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()
.
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.
# 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"