Creating pkgng packages without ports

Lately, I’ve been working on expanding the scope of OpenPAM to more than just a PAM library. Specifically, I’ve added support (in a separate library) for the OATH HOTP and TOTP one-time password algorithms. In the long term, I also intend to implement PSKC and OCRA, the ultimate goal being full compliance with the OATH client and server certification profiles. Part of the reason I’m doing this is that my employer needs it, which is why the University of Oslo holds the copyright on most of the OATH code, but it is also something I’ve been wanting to do for a long time, and which I believe will greatly benefit FreeBSD.

This is a large undertaking, though. I’m not comfortable rolling a new OpenPAM release with the OATH code at this time—and I probably won’t be for quite a while. I’ve created a “nooath” branch and may roll a release from that branch in order to get the many other OpenPAM improvements into FreeBSD 10.0, but that’s a different story.

In the meantime, I need a way to test my code; not just on a development machine, but also on semi-production systems such as my desktop and my home router. Once it’s tested, I also need a way to deploy it on mission-critical systems. All these systems have one thing in common: they are binary installations, maintained with freebsd-update rather than built from source. So I need a way to install a newer version of OpenPAM without disturbing the base version.

The easy answer is to install in /usr/local:

# ./configure --prefix=/usr/local
# gmake
# gmake install

We also need to make sure that everything that uses PAM uses the new version (which is 100% backward compatible with older applications and modules). Conveniently, for historical reasons, OpenPAM installs, whereas FreeBSD 8.x and newer install, so it’s a simple matter of mapping one to the other:

# echo "" >>/etc/libmap.conf

That doesn’t address deployment, though. I don’t want to have to compile OpenPAM on every machine, and I already have a mechanism for distributing and updating software across multiple machines: my two pkgng repositories. Let’s take advantage of them by creating pkgng packages for OpenPAM.

I could create an OpenPAM port and build packages from there. There is even precedent for creating a port that obtains sources directly from a repository rather than from a release tarball, so I could test individual revisions. I would however need a copy of the ports tree—it used to be possible to build a port independently of the ports tree, but that time is long gone. Another drawback is that I would have to jump through hoops to create packages from a modified source tree (for pre-commit testing). Finally, I would not be able to create a package of a specific version without first installing that version locally. So creating a port is a less-than-ideal solution.

What I did instead was write a script which installs OpenPAM into a temporary directory and creates the package from there. Well—nearly: there is a small hitch due to a bug in pkg which I expect will be fixed in the near future.

Let’s take a look at some of the juicier parts of the script.

First, we need to determine the package name and version. The name is taken directly from, and so is the version—at first. The thing is, I’d rather not have to continuously update, so @PACKAGE_VERSION@ is normally “trunk” (or “nooath”) until I roll a release. Therefore, if @PACKAGE_VERSION@ is a word rather than a number, I use Subversion’s svnversion utility to retrieve the current revision number. If I can successfully extract a number from the ouput, I append it to the original value.

if ! expr "$version" : "[0-9]{1,}$" >/dev/null ; then
    svnversion="$(svnversion 2>&1)"
    svnversion=$(expr "$svnversion" : '\([0-9][0-9]*\)[A-Z]\{0,1\}$')
    if [ -n "$svnversion" ] ; then

For reasons which will become clear later, we also need to know which version of pkg is installed, as well as the ABI:

pkgver=$(pkg query %v pkg)
[ -n "$pkgver" ] || error "Unable to determine pkgng version."
pkgabi=$(pkg -vv | awk '$1 == "ABI:" { print $2 }')
[ -n "$pkgabi" ] || error "Unable to determine package ABI."

Next, we create a temporary directory into which we will install the software, so we can create a package without touching the host system. The traps ensure that the temporary directory is deleted when the script exits or is interrupted (SIGINT). Two separate traps are needed, because if we install the same trap for both EXIT and INT, it will run twice in the SIGINT case: once due to the SIGINT itself and once because the script exits. Clearing the trap from within the trap handler doesn’t work, because traps are local to the block in which they were set.

info "Creating the temporary directory."
tmproot=$(mktemp -d "${TMPDIR:-/tmp}/$package-$version.XXXXXX")
[ -n "$tmproot" -a -d "$tmproot" ] || \
    error "Unable to create the temporary directory."
trap "exit 1" INT
trap "info Deleting the temporary directory. ; rm -rf '$tmproot'" EXIT
set -e

We can now install our software into the temporary directory ($make evaluates to either make or gmake with a few options to reduce the amount of noise GNU make generates):

info "Installing into the temporary directory."
$make install DESTDIR="$tmproot"

We need a manifest for the package. Most of it can be automatically generated from the information provided in; the only hardcoded OpenPAM-specific information in my script are the comment and description. The latter can easily be avoided: if no description is provided, pkg create will look for a +DESC file in the same directory as the manifest and use its contents instead. The former is not so easily avoided; there is no autoconf macro for a short description of the package, and while PACKAGE_COMMENT="foo"; AC_SUBST(PACKAGE_COMMENT) would work, it feels sort of dirty. I’ll probably end up writing a custom macro that does just that.

Anyway, we start out with a stub:

info "Generating the stub manifest."
cat >"$manifest" <<EOF
name: $package
version: $version
origin: local/$package
comment: [...]
arch: $pkgabi
prefix: @prefix@
categories: local, security

The rest of the manifest consists of a list of files to be included in the package. We generate it automatically from the contents of our temporary directory, which shouldn’t contain anything that we don’t want to include.

info "Generating the file list."
    echo "files:"
    find "$tmproot" -type f | while read file ; do
        [ "$file" = "$manifest" ] && continue
        mode=$(stat -f%p "$file" | cut -c 3-)
        echo "  $file: { uname: root, gname: wheel, perm: $mode }"

We hardcode the ownership as root:wheel, which is correct 99% of the time. This allows us to run the entire package creation process as an unprivileged user. Or it would, except that pkg contains some rather advanced logic to determine whether a package installs shared libraries or depends on shared libraries provided other packages, and that logic doesn’t take into account the case where the package root is not /. Setting LD_LIBRARY_PATH doesn’t help, since pkg doesn’t use the run-time linker, but reads and interprets the Elf headers itself. I haven’t yet managed to untangle that logic to the point where I can figure out where to insert the package root so it will find the correct libraries. The only workaround is to install the package to /, which requires root privileges.

info "Packaging."
if [ "$pkgver" \< "1.1.5" ] ; then
    info "pkg 1.1.4 or older detected."
    yesno "We must now install to /.  Proceed?" || error "Chicken."
    $make install
    pkg create -m "$tmproot" -o "$builddir"
    pkg create -r "$tmproot" -m "$tmproot" -o "$builddir"

Note that I have optimistically predicted that the bug will be fixed in pkg 1.1.5…

This script should be easily adaptable to any other project that uses GNU autotools and doesn’t deviate too far from automake’s standard installation procedure. As previously mentioned, the only part of the script specific to OpenPAM is the stub manifest, and that can easily be changed.

Leave a Reply

Your email address will not be published. Required fields are marked *

This site uses Akismet to reduce spam. Learn how your comment data is processed.