One of the things I enjoy is building tools that support my team and make us all more productive.

Our system has a mechanism for installing add-on features. These features are built into a CD image (ISO file) that can be installed directly from a USB drive or burned to a CD and installed that way. Yes, this sounds old-fashioned, but we build industrial equipment that is not necessarily connected to the Internet.

I recently built an internal Ruby gem to make it easier to create these installation images. We use Ruby and Rake for our automated builds, so I built this new tool on top of Rake.

Every add-on product must have certain files in known locations in order to work with our installation infrastructure. Some products have additional Debian packages that need to be installed.

For a simple installer (no Debian packages), all that is required is the following code:

Simple Installer
installer "myProject"

installer is a simple DSL (domain-specific language) function that hides all of the complexity of building one of our add-on installers. Under the hood, it defines Rake tasks that hook into our common Rake infrastructure. These tasks build up the directory structure for the ISO image, generate the PDF documentation, produce the ISO image and an md5 checksum for it, etc.

For an installer that also needs to install Debian packages, it gets slightly more complicated:

Installer With Debian Packages
installer "myProject" do
package_pool roots: %w{myAddOn otherPackage} repositories: %i{repo1 repo2}

installer takes a customization block. In that block, package_pool takes a list of Debian package names and a list of repository identifiers (recognized by more of our infrastructure) and generates Rake tasks that create a local Debian repository containing the root packages and all of their dependencies. This local repository is placed in the ISO image and can be used by the installer script to install the packages using apt-get.

(%w and %i are shortcuts for producing an array of strings and an array of symbols respectively. %i is new in Ruby 2).

As you can imagine, behind this very simple interface is a fair bit of code that makes all of the magic happen. Clients of the gem only need to know this very small surface area in order to access the magic.

In order for such a simple API to work well, there need to be some well-established conventions that are followed by the clients.

In this case, our system imposes constraints on what an add-on installer looks like.

We have conventions and a framework around how we use Rake. This gem plugs into that framework, so a lot of things “just work”.

We also have a well-known set of local Debian repositories, so we define a registry of them that can be looked up by symbols, making it easier to specify the repositories.

The more flexibility needed by clients, the more complicated the API will become. At some point, the nice abstractions created by the simple API will start to leak, and it’ll be time to re-think.

You might have heard the Rails catchphrase, “Convention Over Configuration”. There is a lot of power in that approach. As long as you can make the client code fit into the conventions, you can provide access to powerful tools (the iceberg) with a simple, small API (the tip).