Distribute Painlessly - Build Awesome Command-Line Applications in Ruby 2: Control Your Computer, Simplify Your Life (2013)

Build Awesome Command-Line Applications in Ruby 2: Control Your Computer, Simplify Your Life (2013)

Chapter 7. Distribute Painlessly

For our apps to achieve their full potential, we need to get them into the hands of users. In this chapter, we’ll learn how to distribute our apps to both users and developers.

We’ll start by learning about RubyGems, Ruby’s standard deployment mechanism. We’ll see how to use RubyGems for public and private distribution. After that, we’ll learn how to deploy our apps in environments that are more tightly controlled, where RubyGems is not available. Finally, we’ll see how to set up our codebase so that we can distribute our code to other developers and work more collaboratively on our apps.

7.1 Distributing with RubyGems

RubyGems is the standard way to distribute Ruby apps and libraries; if you have written a Rails app or done any nontrivial Ruby programming, you’ve likely used the gem command to install third-party code onto your system. We used it earlier to install libraries like gli and ronn. RubyGems is designed to make it very simple to install Ruby code, and this includes command-line applications. For example, rake, the standard Ruby build tool, can be installed via RubyGems like so:

$ sudo gem install rake

Installing rake 10.1.0.......

$ rake -V

rake, version 10.1.0

In addition to painlessly installing and updating Ruby applications, RubyGems is also used to install an app’s dependencies. Suppose, for example, our app parses XML; we might use the popular Nokogiri library to handle it. We can indicate to RubyGems that our app requires Nokogiri, and RubyGems will install it when someone installs our app (unless it’s already installed).

To configure our application for installation by RubyGems, we need to do three things: create a gem specification, package our code as a gem file, and make it available via a gem server.

Creating a Gem Specification

A gem’s specification is a piece of Ruby code called a gemspec . This file needs to be created only once and needs to be changed only if we change things about our app, such as the files or list of authors.

A gemspec is exactly what it sounds like: a spec for creating a gem. Essentially, it’s a chunk of Ruby code that the gem command will read and execute to understand our app, such as its name, files, and dependencies. Your gemspec should be named YOUR_APP.gemspec where YOUR_APP is the name of your app’s executable (which should be all lowercase). The file should be located in the root directory of your project. Here’s a sample gemspec we might use for todo, our task-management app:

Gem::Specification.new do |s|

s.name = "todo"

s.version = "0.0.1"

s.platform = Gem::Platform::RUBY

s.authors = ["David Copeland"]

s.email = ["davec at naildrivin5.com"]

s.homepage = "http://www.naildrivin5.com/todo"

s.summary = %q{A lightweight task-management app}

s.description = %q{Todo allows you to manage and prioritize

tasks on the command line}

s.rubyforge_project = "todo"

s.files = ["bin/todo"]

s.executables = ["bin/todo"]

s.add_dependency("gli")

end

Ideally, the fields in the gemspec are self-explanatory, but it’s worth calling out a few of the fields that are most important.

files

This is a list of the files that will be part of your gem. It’s important that this list is correct, or your gem won’t work when installed.

executables

This is the name of your app’s executable. By specifying this, RubyGems will set up your app in the user’s path when it’s installed.

add_dependency

Each call to add_dependency will indicate a dependency on a third-party gem. For example, in the gemspec for todo, we’ve indicated that our app needs GLI. When the user installs todo, RubyGems will see the dependency on GLI and install it.

Much of this information, such as the name of our app and its description, is static; it won’t change very often. Some of the information, however, might change more frequently, such as the filename and the version number. We’d like this to be derived. For example, we might store our app’s version in a constant so it can be printed in the help text. Since our gemspec is just Ruby code, we can access this constant and use it, rather than duplicating the value.

Let’s assume that a file lib/todo/version.rb exists and contains the version number of our app:

install_remove/todo/lib/todo/version.rb

module Todo

VERSION = '0.0.1'

end

Since our gemspec is Ruby code, we can use require to import that file and access the VERSION constant:

*

$LOAD_PATH.push File.expand_path("../lib", __FILE__)

*

require "todo/version"

Gem::Specification.new do |s|

s.name = "todo"

*

s.version = Todo::VERSION

s.platform = Gem::Platform::RUBY

$LOAD_PATH is the variable that holds Ruby’s load path, which is where Ruby looks for files when we require them. We add the path to the lib directory within our project and then require todo/version.rb.

Now, when we change the version of our app, our gemspec will automatically know about the new version number. If we wanted to get fancy, we could extract the summary information we use in our banner into a similar constant and use that value for the gemspec’s summary .

Now that we have a gemspec, we’re ready to package our code as a gem file so we can distribute it.

Packaging Code in a Gem File

Gems are created with the gem command, which we can run directly on the command line. Entering the command can become tiresome, so we’re going to use rake to script it. rake is Ruby’s build-automation tool, and it’s standard practice to automate any build and maintenance tasks in a Rakefile.

The Rakefile should be stored in the root directory of your project, which is also where the gemspec is located. RubyGems provides a rake task you can use in your Rakefile that will package your gem for you, as follows:

require 'rubygems/package_task'

spec = eval(File.read('todo.gemspec'))

Gem::PackageTask.new(spec) do |pkg|

end

This is all that’s needed. If we ask rake to show us the available tasks via rake -T, we can see that we have a task named package available:

$ rake -T

rake clobber_package # Remove package products

rake gem # Build the gem file todo-0.0.1.gem

rake package # Build all the packages

rake repackage # Force a rebuild of the package files

To build our gem, we use the package task:

$ rake package

mkdir -p pkg

mkdir -p pkg

mkdir -p pkg/todo-0.0.1/bin

rm -f pkg/todo-0.0.1/bin/todo

ln bin/todo pkg/todo-0.0.1/bin/todo

mkdir -p pkg/todo-0.0.1/lib/todo

rm -f pkg/todo-0.0.1/lib/todo/version.rb

ln lib/todo/version.rb pkg/todo-0.0.1/lib/todo/version.rb

rm -f pkg/todo-0.0.1/README.rdoc

ln README.rdoc pkg/todo-0.0.1/README.rdoc

rm -f pkg/todo-0.0.1/todo.rdoc

ln todo.rdoc pkg/todo-0.0.1/todo.rdoc

cd pkg/todo-0.0.1

WARNING: licenses is empty

WARNING: no description specified

Successfully built RubyGem

Name: todo

Version: 0.0.1

File: todo-0.0.1.gem

mv todo-0.0.1.gem ..

cd -

We can now install our gem locally via the gem command:

$ gem install pkg/todo-0.0.1.gem

Successfully installed todo-0.0.1

Parsing documentation for todo-0.0.1

Installing ri documentation for todo-0.0.1

1 gem installed

$ todo help

NAME

todo -

SYNOPSIS

todo [global options] command [command options] [arguments...]

VERSION

0.0.1

GLOBAL OPTIONS

-f, --filename=todo_file - Path to the todo file (default: ~/.todo.txt)

--[no-]force-tty -

--help - Show this message

--version - Display the program version

COMMANDS

done - Complete a task

help - Shows a list of commands or help for one command

initconfig - Initialize the config file using current global options

list - List tasks

new - Create a new task in the task list

Notice how we can run todo directly without having to explicitly run it out of bin (or with bundler). todo is now an app installed in our path just like any other application.

Installing a gem from a gem file is not typical behavior. Instead, users will generally install gems served from a remote gem server. Despite this, it’s still a good idea to install your gem manually before distributing it, as a last check to make sure things are working. Once you’ve done that, you’ll need to push your gem to a gem server so others can install it.

Pushing a Gem to a Gem Server

For open source applications, the simplest gem server to use for your apps is the canonical one provided at http://www.rubygems.org . This is where most users will look and is where gem will also look by default when you issue a gem install command. If you are writing internal applications that you don’t want distributed as open source, you can run a gem server internally and make a one-time configuration to your local environment to find gems from your internal server. Let’s look at the two options.

Distributing Gems via RubyGems.org

To distribute to your app through http://www.rubygems.org , you’ll need to create an account there. This is a one-time-only task, and you’ll need to provide your login credentials the first time you push a gem to the server.

Next, you’ll need to make sure that the name of your gem isn’t in use already. Since RubyGems.org is the central repository for almost all open source Ruby code, you may find that the name of your gem is already taken. If your name is taken, you should do the following:

· Change your executable’s name.

· Change the name of your gemspec file.

· Change the name of your gem in the gemspec.

· Change any modules in your code and rename your source files to match the new name.

This might sound onerous, but we’ve taken several steps throughout our journey to minimize the impact of such renames (you’ll recall in Chapter 3, Be Helpful that we derived our executable name in our help strings; this is one reason why). Once you’re sure your gem name is available, you can use the gem push command to push your gem to the server:

$ gem push pkg/todo-0.0.1.gem

Pushing gem to rubygems.org

Successfully registered gem: todo (0.0.1.gem)

The first time you push your gem, you’ll be asked for your RubyGems.org name and password; gem should store this information for future pushes. Now, you can go onto any other machine where RubyGems is installed and install your app:

$ gem install todo

Successfully installed todo-0.0.1

1 gem installed

Installing ri documentation for todo-0.0.1...

Installing RDoc documentation for todo-0.0.1...

RubyGems.org is great for open source apps, but for internal apps that you don’t want to make available to the rest of the world, you’ll need to set up a gem server internally.

Setting Up and Using an Internal Gem Server

Although the code that powers http://www.rubygems.org is open source and you could deploy it on your company’s intranet, this is not recommended, because the app is very complex and likely overpowered for your needs. Instead, you can use the much simpler geminabox.[40]

geminabox is incredibly easy to set up and use and allows you to push gems to it for internal distribution, via the inabox command it adds to gem. It also provides a rudimentary web interface to let you browse the installed gems. The first thing you’ll need to do is install it:

$ gem install geminabox

Fetching: geminabox-0.11.0.gem (100%)

Successfully installed geminabox-0.11.0

Parsing documentation for geminabox-0.11.0

Installing ri documentation for geminabox-0.11.0

1 gem installed

Next, you’ll need to make a directory where your gems will be stored. When you set this up on a real server, you’ll want to choose a more appropriate location, but to see how things work, you can serve them right out of your current directory:

$ mkdir gems

Now, create a small configuration file called config.ru:

require "rubygems"

require "geminabox"

Geminabox.data = "gems"

run Geminabox

Then, start up the server:

$ rackup

[2011-09-18 05:47:33] INFO WEBrick 1.3.1

[2011-09-18 05:47:33] INFO ruby 2.0.0

[2011-09-18 05:47:33] INFO WEBrick::HTTPServer#start: pid=34947 port=9292

If you navigate to http://localhost:9292 , you’ll see the web interface for geminabox. Although you can upload gems from this interface, we won’t use it; we work from the command line, and we want to be able to script it. Open a new shell window and navigate to the todo project. We can push a gem to the server via the inabox command that was added to gem by installing gem inabox. You can build and push your gem as follows:

$ rake package

Successfully built RubyGem

Name: todo

Version: 0.0.1

File: todo-0.0.1.gem

mv todo-0.0.1.gem pkg/todo-0.0.1.gem

$ gem inabox pkg/todo-0.0.1.gem

Pushing todo-0.0.1.gem to http://localhost:9292...

The first time you execute gem inabox, gem will ask for the host where the server is running; just enter http://localhost:9292, and it will do the rest. If you navigate to http://localhost:9292 in your browser, you can see that your gem has been received and is ready for installation. To install it, open a new shell window and type the following:

$ gem install --clear-sources --source http://localhost:9292 todo

Successfully installed todo-0.0.1

1 gem installed

The reason for --clear-sources is that there is aready a gem named todo on http://rubygems.org . By default, gem will look on that site before looking for gems stored at additional sources. Since we want to only install the files from our local gem server in this example, we tell gem to ignore all pre-configured sources.

You can alleviate the need to use --source every time by executing the one-time command gem sources -a http://localhost:9292. As a final note, geminabox doesn’t provide any authentication. Make sure you deploy your server somewhere secure or password-protect it.

We’ve seen how easy it is use RubyGems for open source as well as internal distribution of our apps, but managing your apps with RubyGems is not always an option. In the next section, we’ll see how to achieve a similar style of distribution using tools that might be easier to deploy inside a more controlled environment.

7.2 Distributing Without RubyGems

Many servers are tightly managed, and installing gems via RubyGems is not an option. System administrators of such systems prefer to have a single way of getting code onto the server, using the packaging system provided by the operating system. This complicates our job of making our apps easily deployable. Distributing gems in such environments is hard but not impossible.

To demonstrate how you might do this, we’ll walk through packaging our gem as an RPM, which can be used with Red Hat’s yum distribution system.

RPM is the package management system used by many Linux distributions such as Red Hat, Fedora, and CentOS. Creating an RPM can be done using gem2rpm.[41] First, install it (this can be done anywhere, not necessarily on the machine where you’ll install the RPM):

$ gem install gem2rpm

Next, get a copy of your gem file. If you have it available in a RubyGems repository, you can simply do the following:

$ gem fetch todo

This will download the latest version of your gem to the directory where you ran the command. The next step is to create a spec file. This isn’t a gemspec but another file that RPM uses to describe and install your gem. gem2rpm will create this spec file for us by running it as follows:

$ gem2rpm -o rubygem-todo.spec todo-1.0.0.gem

gem2rpm should create the file rubygem-todo.spec in the current directory. Note that the format of the filename is idiomatic; all RubyGem packages for RPM should be named rubygem-GEMNAME without the version information.

If your gem doesn’t include native code (such as C extensions), this should be all you need to do. Your next step is to create the actual RPM package using rpmbuild, which should be available wherever RPM is installed. RPM expects a certain directory structure, and if you see the following error message, it’s most likely because you don’t have it set up correctly.

$ rpmbuild -ba rubygem-todo.spec

ERROR: could not find gem /usr/src/redhat/SOURCES/todo-1.0.0.gem

locally or in a repository

To correct the error, simply copy your .gem file to /usr/src/redhat/SOURCES and rerun the command:

> rpmbuild -ba rubygem-todo.spec

# Some output omitted...

Wrote: /usr/src/redhat/RPMS/noarch/rubygem-todo-1.0.0-1.noarch.rpm

The RPM for your gem is now located in /usr/src/redhat/RPMS. It can be installed using whatever means your system administrator prefers (e.g., a local RPM server). Note that the RPM created this way is not signed; signing is outside the scope of this book, but for internal distribution, your system administrator shouldn’t have a problem installing this RPM package.

Now that we know how to get our apps into the hands of users, it’s worth talking about getting them into the hands of other developers. For an open source project, you might want to solicit contributions and bug fixes from the community. For an internal application, you’ll likely need to work with other developers, or others might need to maintain your code. In either case, we want other developers to get up to speed with our code as easily as users can install it.

7.3 Collaborating with Other Developers

We’ve talked a lot about making an awesome command-line app that is easy to use. We also want our application to be easy to develop and maintain. This reduces the burden on us and others for fixing bugs and adding new features (which, in turn, helps our users). This section is all about making sure the initial setup for developers is painless.

To allow developers to get up and running quickly, we need to provide two things: a way to manage development-time dependencies and a way to provide and manage developer documentation. We’ll also talk briefly about setting up an open source project on GitHub, which is a great way to allow other developers to contribute to your open source project.

Managing Development Dependencies

Unlike runtime dependencies, which are needed by users of an app, development dependencies are needed only by developers. Such dependencies range from testing libraries like RSpec to rake itself. Developers should be able to install all the gems they need with one command and get up and running as easily as possible. This will save you time in documenting the setup and make it easier for developers to contribute.

We can specify these dependencies in the gemspec and manage their installation with Bundler.[42] Bundler was created to help manage the dependencies for Ruby on Rails projects, but it’s a general-purpose tool and will work great for us. It’s also what most seasoned Rubyists will expect.

First, let’s specify our development dependencies. Although rake and RDoc are typically installed with Ruby, their inclusion is not guaranteed, and users might not necessarily have the versions of these tools that we require. Let’s add them explicitly as development dependencies to our gemspec by way of add_development_dependency :

Gem::Specification.new do |s|

s.name = "todo"

s.version = "0.0.1"

# Rest of the gemspec...

s.add_dependency("gli")

*

s.add_development_dependency("rake","~> 10.1.0")

*

s.add_development_dependency("rdoc","~> 3.9")

end

Now, we need a way for developers to install these dependencies automatically. Bundler does exactly this; will examine our gemspec for the gems and versions and install the correct dependencies. Although we have these dependencies installed already, we want to configure Bundler so other developers can get them installed simply. First we install it with Bundler:

$ gem install bundler

Successfully installed bundler-1.3.5

1 gem installed

Installing ri documentation for bundler-1.3.5...

Installing RDoc documentation for bundler-1.3.5...

Bundler uses a file named Gemfile to control what it does. For a Rails app, this file would contain all of the gems needed to run the app. Since we’ve specified them in our gemspec, we can tell Bundler to look there, instead of repeating them in Gemfile. Our Gemfile looks like so:

source 'https://www.rubygems.org'

gemspec

This tells Bundler to search the common RubyGems repository for needed gems and to look in the gemspec for the list of which gems are needed and what versions to fetch. If you’ve set up an internal gem server to host private gems, you can add additional source lines, giving the URL to your server as the argument.

Even though we have all these gems installed already, it’s still important to run Bundler, because it will generate an additional file that other developers will need. Let’s run it first and then see what it did. This is the same command developers will run to get started working on your app.

$ bundle install

Resolving dependencies...

Using rake (10.1.0)

Using gli (2.8.0)

Using rdoc (3.9.4)

Using todo (0.0.1) from source at .

Using bundler (1.3.5)

Your bundle is complete!

Use `bundle show [gemname]` to see where a bundled gem is installed.

If you were paying close attention, you’ll notice that we specified the version 3.9 of rdoc as a development dependency, but Bundler installed 3.9.4. What if version 3.9.5 of rdoc is released? When a new developer starts on our app, which version should be installed?

Bundler is designed to allow you to tightly control what versions are installed. When we ran bundle install earlier, it created a file called Gemfile.lock. This file contains the exact versions of each gem that was installed. We used the ~> symbol (a tilde, followed by the greater-than symbol) to specify our dependence on rdoc, meaning that any version number with the format 3.9.x would be acceptable. Bundler used the latest version that satisfied this dependency.

Suppose rdoc 3.9.5 is released. If we were to check Gemfile.lock into version control and a new developer checked it out and ran bundle install, Bundler would install 3.9.4, even though 3.9.5 is the latest version that satisfies our development dependency. This allows us to ensure that everyone is using the exact same gem versions that we are.

Now that we have a way to get new developers up and running quickly, we need a way to document anything additional to help them collaborate.

Writing and Generating Documentation

The documentation you provide with your project should be aimed mostly at developers. The in-app help and man page are where you can document things for your users, so you can use RDoc and a README file to get developers up to speed (although you should include a pointer to would-be users in your README as well).

The README is particularly important, since it’s the first thing a developer will see when examining your source code. It’s the ideal place to include instructions for developers to help understand what your app does and how to work with it.

Your README should be in RDoc format so you can include it when you generate and publish your RDoc. This allows you to connect your overview documentation to your code and API documentation. If you aren’t familiar with RDoc, it’s a plain-text format, similar to Markdown but geared toward documenting Ruby code. To use it effectively, you need to create a README.rdoc and then add the RDoc task to your Rakefile to automate document generation.

Your README.rdoc should have the following format:

1. Application name and brief description

2. Author list, copyright notice, and license

3. Installation and basic usage instructions for users

4. Instructions for developers

Although the README is primarily developer documentation, it’s a good idea to include some brief pointers for users who happen to come across the source code for your app.

Everything else in the file should be about helping developers get up to speed. Here’s an example of a README.rdoc that we might write for our database backup app, db_backup.rb:

install_remove/db_backup/README.rdoc

= `db_backup` - Iteration-aware MySQL database backups

Author:: David Copeland (mailto:dave@example.com)

Copyright:: Copyright (c) 2011 by David Copeland

License:: Distributes under the Apache License,

see LICENSE.txt in the source distro

This application provides an easy interface to backing up MySQL databases,

using a canonical naming scheme for both daily backups and

"end-of-iteration" backups.

== Install

Install:

gem install db_backup

== Use

Backup a database:

db_backup.rb my_database

For more help:

db_backup.rb --help

gem man db_backup.rb

== Developing for `db_backup`

First, install bundler:

gem install bundler

Get the development dependencies

bundle install

Most of the code is in `bin/db_backup.rb`.

The RDoc format provides most of what you’ll need to write good documentation. See RDoc’s RDoc[43] for a complete description of the format.

To publish the documentation, we want to create HTML versions of it, which can be done via the RDoc rake task.

install_remove/db_backup/Rakefile

require 'rdoc/task'

RDoc::Task.new do |rdoc|

rdoc.main = "README.rdoc"

rdoc.rdoc_files.include("README.rdoc","lib/**/*.rb","bin/**/*")

rdoc.title = 'db_backup - Backup MySQL Databases'

end

Now, you can generate RDoc via rake as follows:

$ rake rdoc

(in /Users/davec/Projects/db_backup)

rm -r html

Generating HTML...

Files: 3

Classes: 0

Modules: 1

Methods: 0

Elapsed: 0.105s

You can then view the RDoc by opening html/index.html in your browser. For an open source app, you’ll want to use a service like RDoc.info to publish your RDoc (we’ll see how to set that up in the next section). For internal apps, you can simply copy the HTML to a local intranet server.

Managing an Open Source App on GitHub

Although there are many free services to manage open source code, GitHub[44] has become one of the most popular, especially among members of the Ruby community. Hosting your open source project here will expose it to a large audience of active developers.

Although GitHub has many resources of its own to get you started, we’ll go over a few points here that will help you successfully manage your open source app. To get started, first go to GitHub and perform the following steps:

1. Create your account on GitHub.

2. Create a new open source project under your account (this is completely free).

3. Import your repository (GitHub includes very clear and specific instructions on doing this, which we won’t re-create here).

Make sure that the name you choose for your repository matches the name of your app (which should match the gem name you chose as well). Once you’ve imported your project, you’ll note that GitHub helpfully renders your README.rdoc as HTML. This is another great reason to have a README.

GitHub includes an issue-tracking system and a wiki. If these tools are helpful to you, by all means use them, but the feature you’ll definitely want to set up is called service hooks . These hooks allow you to connect web services to your commits. Of greatest interest to us is the hook for RDocinfo. http://rdoc.info is a free service that will generate and host your project’s RDoc. The HTML version of your RDoc will be generated every time you commit. This is a great alternative to hosting the files yourself, and it’s very easy to set up in GitHub. Here are the steps:

1. Navigate in your browser to your project’s GitHub page.

2. Click the Admin button, and then click the Service Hooks link.

3. Scroll down until you see the link for RDocinfo and click it.

4. Scroll back up and check the box to activate it, followed by clicking the Update Settings button.

5. Now, make a change to your project’s code, commit that change, and push it up to GitHub.

The service hook should kick in, and your project’s RDoc will be available at http://rubydoc.info/github/your-name/your-project, where “your-name” is your GitHub username and “your-project” is the name of your project. If you set your project’s URL to this one, developers will have a very easy time finding your documentation and development instructions, making it a snap for them to contribute!

7.4 Moving On

You now know everything you need to get your app into the hands of both users and developers. Whether you are working on an open source project to share with the world or an internal app for the exclusive use of your company, RubyGems, and various tools related to it, have you covered.

At this point, we can conceive, build, and distribute an awesome command-line application. But, what happens now? Once users start using your app, they’re sure to find bugs, and they’ll definitely want new features. We’ve taken a lot of steps to make it easy on ourselves to respond to our users, but there’s more that we can be doing, especially as our apps become more complex and featureful.

The best way to quickly respond to our users and maintain the level of quality and polish we’ve put into our app is to have a test suite—a way to verify that our app does what we say it does and to check that it doesn’t break when we make changes. Ideally, we would’ve started off this book with a discussion of tests, but we wanted to focus first on the principles and tools you need to build awesome apps. Now it’s time to look at testing, which we’ll do in the next chapter. When it comes to testing, command-line apps present their own challenges, but, as usual, the Ruby ecosystem of libraries and open source projects will help us overcome them.

Footnotes

[40]

https://github.com/cwninja/geminabox

[41]

https://github.com/lutter/gem2rpm

[42]

http://gembundler.com

[43]

http://rdoc.sourceforge.net/doc/index.html

[44]

http://www.github.com