Richard Crowley’s blog

Operable Ruby

Ruby today is a moving target.  The reference implementation is releasing rapidly on several branches and at least three alternatives to MRI are in wide use.  The RubyGems package manager is releasing nearly every week and the dizzying array of variously compatible gems are releasing whenever they feel like it.  Exciting times, to be sure, but also an operator’s nightmare.  At every level of this dependency tree, robust packaging brings order to the chaos.  Given that, it surprises me to read folks recommending RVM for production use.

An operable production environment relies on packages to account for most files on the system.  Think of it as a paper trail: dpkg-query -S pathname will tell you the package that owns a particular file.  With the mapping of package names to sets of pathnames, packages become uninstallable.  Naturally, anything that can be uninstalled entirely can be upgraded.  Do you trust that shell script to measure up?

Hopefully we’ve settled that packages are not optional in production.  By my philosophy, development systems should faithfully mimic production systems in every affordable way, so I extend the policy to my development virtual machines, too.  Reliable development environments are just as important to me as reliable production environments.

There’s been an interesting discussion on the devops-toolchain mailing list this week about how best to package the Ruby environment.  Unsurprisingly, RVM came up as well as several better options.  I’d like to offer executable proof that building packages from arbitrary Ruby versions is easy.

The remainder of this article will walk through building up a shell script that uses debra(1) to package arbitrary versions of Ruby for Debian-based Linux distributions.

We’re building a system package here: the prefix is /usr and the files should be owned by root.  It isn’t debra’s business who owns the files in the package, so we will run the entire build process as root to ensure this is true.

First things first, the version numbers we’re using:

VERSION="1.9.2"
PATCH="180"
DPKG_BUILD_ARCH="$(dpkg --print-architecture)"

Note well that set -e should really appear in every single shell script you write so they will exit on failure.

set -e

Next, use debra-create(1) to start building up a Debian package in a temporary directory.  The first argument to all debra commands is the directory being used for the build.  This block uses one of my favorite shell scripting idioms: using mktemp(1) to create a temporary directory and trap to clean up on EXIT (using debra-destroy(1) in this case).

DESTDIR="$(mktemp -d)"
debra create "$DESTDIR"
trap "debra destroy \"$DESTDIR\"" EXIT

The extensions we’re going to enable in the Ruby build require a few development libraries:

apt-get -y install libssl-dev libreadline5-dev zlib1g-dev

Now for the meat of the Ruby build.  Using debra-sourceinstall(1) makes this very easy.  It downloads, unpacks, and builds from the tarball given by the second argument.  -b provides a “bootstrapping” command that is run immediately after unpacking to setup the extensions we want to enable.  -p relays its argument as --prefix to the ./configure program and -f provides other arbitrary arguments to ./configure.  After configuring, sourceinstall(1) runs make and make install to install the package in DESTDIR.

debra sourceinstall "$DESTDIR" \
	"ftp://ftp.ruby-lang.org/pub/ruby/1.9/ruby-$VERSION-p$PATCH.tar.gz" \
	-b"sh -c 'echo fcntl\\\nopenssl\\\nreadline\\\nzlib >ext/Setup'" \
	-p/usr -f"--program-suffix=-$VERSION"

(See sourceinstall(1) for more options affecting the build process.)

The last artifact a Debian package needs is a control file for version and dependency metadata.  Other metadata like the MD5 sum of each file are handled automatically by debra.

cat >$DESTDIR/DEBIAN/control <<EOF
Package: ruby-$VERSION
Version: $VERSION-p$PATCH
Section: devel
Priority: optional
Essential: no
Architecture: $DPKG_BUILD_ARCH
Depends: libc6, libssl0.9.8, libreadline5, zlib1g
Maintainer: Richard Crowley <r@rcrowley.org>
Description: Ruby $VERSION.
EOF

With that, we can build the package in a file that follows the Debian convention of package_version_architecture.deb.

debra build "$DESTDIR" "ruby-${VERSION}_${VERSION}-p${PATCH}_${DPKG_BUILD_ARCH}.deb"

This package is useful in development and production, can be uninstalled or upgraded with ease, coexists peacefully with the system’s Ruby plus other Rubies built this way, and isn’t gated by anyone else’s feature freezes or patch schedules.  This whole example is available at https://gist.github.com/891447.