One of the first hurdles new ruby programmers run into is creating a project as a gem. Today there are some great tools that trivialize gem creation and publication: jeweler, bundler, github, gemcutter. In this article I’ll show the creation of an example application with a command line interface all packaged in a gem.
Example code available at: http://github.com/royw/creating-gem-example
∴ git clone git://github.com/royw/creating-gem-example.git
First, I really like Ruby Version Manager so I highly recommend installing it and the versions of ruby you prefer. Keeping it simple for this article:
∴ rvm install 1.8.7 ∴ rvm 1.8.7
Second, install the prerequisites Jeweler and Bundler :
∴ gem install jeweler ∴ gem install bundler
Now it’s time to code generate the project. Let’s see what jeweler can do for us:
∴ jeweler --help
Usage: jeweler [options] reponame
e.g. jeweler the-perfect-gem
--directory [DIRECTORY] specify the directory to generate into
--rspec generate rspec code examples
--shoulda generate shoulda tests
--testunit generate test/unit tests
--bacon generate bacon specifications
--testspec generate test/spec tests
--minitest generate minitest tests
--micronaut generate micronaut examples
--riot generate riot tests
--cucumber generate cucumber stories in addition to the other tests
--reek generate rake task for reek
--roodi generate rake task for roodi
--[no-]gemcutter setup project for gemcutter
--rubyforge setup project for rubyforge
--summary [SUMMARY] specify the summary of the project
--description [DESCRIPTION] specify a description of the project
--user-name [USER_NAME] the user's name, ie that is credited in the LICENSE
--user-email [USER_EMAIL] the user's email, ie that is credited in the Gem specification
--github-username [GITHUB_USERNAME]
name of the user on GitHub to set the project up under
--github-token [GITHUB_TOKEN]
GitHub token to use for interacting with the GitHub API
--git-remote [GIT_REMOTE] URI to set the git origin remote to
--homepage [HOMEPAGE] the homepage for your project (defaults to the GitHub repo)
--create-repo create the repository on GitHub
--yard use yard for documentation
--rdoc use rdoc for documentation
-h, --help display this help and exit
Wow! Pretty impressive. As you can see, jeweler let’s you define your project your way. For this article:
∴ jeweler --rspec --no-gemcutter --summary "example gem for blog article" --description "This is an example gem for my blog article showing how to create a gem" --rdoc --user-name "Roy Wright" --user-email "roy@wright.org" example
create .gitignore
create Rakefile
create LICENSE
create README.rdoc
create .document
create lib
create lib/example.rb
create spec
create spec/spec_helper.rb
create spec/example_spec.rb
create spec/spec.opts
Jeweler has prepared your gem in example
∴ cd example
∴ ls
LICENSE README.rdoc Rakefile lib spec
∴ rake -T
(in /Users/royw/views/example)
rake build # Build gem
rake check_dependencies # Check that runtime and development dependencies are installed
rake check_dependencies:development # Check that development dependencies are installed
rake check_dependencies:runtime # Check that runtime dependencies are installed
rake clobber_rcov # Remove rcov products for rcov
rake clobber_rdoc # Remove rdoc products
rake gemspec # Generate and validates gemspec
rake gemspec:debug # Display the gemspec for debugging purposes
rake gemspec:generate # Generates the gemspec, using version from VERSION
rake gemspec:validate # Validates the gemspec
rake git:release # Tag a release in Git
rake github:release # Release Gem to GitHub
rake install # Install gem using sudo
rake rcov # Run specs using RCov
rake rdoc # Build the rdoc HTML Files
rake release # Release gem
rake rerdoc # Force a rebuild of the RDOC files
rake spec # Run specs
rake version # Displays the current version
rake version:bump:major # Bump the gemspec by a major version.
rake version:bump:minor # Bump the gemspec by a minor version.
rake version:bump:patch # Bump the gemspec by a patch version.
rake version:write # Writes out an explicit version.
As you can see, most of the infrastructure is ready to use. Ah, you caught the “most” did you? OK, let’s complete it.
First, let’s get a VERSION file installed:
∴ rake version:write (in /Users/royw/views/example) Updated version: 0.0.0 ∴ ln -s ../VERSION VERSION
This puts a copy of the version in our gem so we can have a –version feature.
If your gem is going to furnish an executable, then do something like:
∴ mkdir bin ∴ mate bin/example # add the code below, changing "example" to your project name ∴ chmod +x bin/example ∴ cat bin/example
#!/usr/bin/env ruby require File.expand_path(File.dirname(__FILE__) + "/../lib/example") exit CLI.execute
Now let’s switch over to the lib directory and do a little infrastructure improvements.
∴ mkdir lib/example
And this directory is where our code goes. To access our code we modify the lib/example.rb to handle our requires:
∴ cat lib/example.rb
# not the best solution, probably should use require_relative in ruby 1.9 or the backported # version from Programming Ruby v3 for ruby 1.8. $:.unshift(File.dirname(__FILE__)) unless $:.include?(File.dirname(__FILE__)) || $:.include?(File.expand_path(File.dirname(__FILE__))) # a few of the gems I use in most projects require 'rubygems' require 'extlib' require 'versionomy' require 'log4r' require 'configliere' require 'singleton' require 'pp' # require all of the library files # note, you may need to specify these explicitly if there are any load order dependencies Dir['example/**/*.rb'].each do |name| require name end
And we will toss the start of a command line interface object (with support for command line arguments, environment variables, config files, and logging) into lib/example/cli.rb:
∴ cat lib/example/cli.rb
# Command Line Interface to the Example application
#
# Usage:
#
# exit CLI.execute
#
# Note, this is a Singleton class, only use CLI.execute to access this class.
#
class CLI
include Singleton
# The application can monitor this attribute to know if an interrupt (^c) has been signalled.
attr_reader :interrupt_signalled
APP_NAME = 'example'
# The main entry point for the CLI
# returns the exit code
def self.execute
instance.execute
end
# protected by the Singleton. I.e., do not attempt CLI.new
def initialize
@interrupt_signalled = false
end
# execute the app from the CLI
# returns the exit code
def execute
exit_code = 0
# ^c handler
Signal.trap("INT") { @interrupt_signalled = true }
begin
setup_settings
Settings[:app_name] = APP_NAME
if Settings[:version]
puts Example.version.to_s
else
Settings[:logger] = init_logger
app = Example.new
app.execute
end
rescue SystemExit
# exit() calls from within the block actually raise SystemExit
rescue Exception => e
Settings[:logger].error e.to_s if Settings[:logger]
exit_code = 1
end
exit_code
end
# log4r setup
def init_logger
# Initial setup of logger
logger = Log4r::Logger.new(APP_NAME)
level_map = {'DEBUG' => Log4r::DEBUG, 'INFO' => Log4r::INFO, 'WARN' => Log4r::WARN}
# console messages
logger.outputters = Log4r::StdoutOutputter.new(:console)
Log4r::Outputter[:console].formatter = Log4r::PatternFormatter.new(:pattern => "%m")
Log4r::Outputter[:console].level = Log4r::INFO
Log4r::Outputter[:console].level = Log4r::WARN if Settings[:quiet]
Log4r::Outputter[:console].level = Log4r::DEBUG if Settings[:debug]
Log4r::Outputter[:console].formatter = Log4r::PatternFormatter.new(:pattern => "%m")
logger.trace = true if Settings[:trace]
if Settings[:logfile]
logfile_outputter = Log4r::RollingFileOutputter.new(:logfile, :filename => Settings[:logfile], :maxsize => 1000000 )
logger.add logfile_outputter
Settings[:loglevel] ||= 'INFO'
Log4r::Outputter[:logfile].formatter = Log4r::PatternFormatter.new(:pattern => "[%l] %d :: %M")
Log4r::Outputter[:logfile].level = level_map[Settings[:loglevel].upcase] || Log4r::INFO
end
logger
end
# Configliere setup
def setup_settings
Settings.use :all
Settings.define :logfile, :type => String, :description => 'Log filename, default is: media2nfo.log', :required => false
Settings.define :loglevel, :type => String, :description => 'Logfile logging level, must be one of: DEBUG, INFO, WARN, ERROR', :required => false
Settings.define :debug, :type => :boolean, :description => 'Log debug messages to console', :required => false
Settings.define :quiet, :type => :boolean, :description => 'Only log errors to console', :required => false
Settings.define :trace, :type => :boolean, :description => 'Trace logging', :required => false
Settings.define :version, :type => :boolean, :description => 'Application Version', :required => false
Settings({
:logfile => "#{APP_NAME}.log",
:loglevel => 'debug'
})
Settings.read("#{APP_NAME}.yaml") if File.exist?(File.expand_path("~/.configliere/#{APP_NAME}.yaml"))
Settings.resolve!
end
end
And a really basic application:
∴ cat lib/example/example.rb
class Example
def self.version
ver = Versionomy.parse(IO.read(File.expand_path(File.dirname(__FILE__) + "/../VERSION")).strip)
ver
end
def execute
Settings[:logger].debug "Example.execute"
end
end
And to be good, an rspec:
∴ cat spec/example_spec.rb
require File.expand_path(File.dirname(__FILE__) + '/spec_helper')
describe "Example" do
it "should return a Versionomy object as the version" do
ver = Example.version
ver.instance_of?(Versionomy::Value).should be_true
end
it "should return the version stored in the VERSION file" do
v1 = Example.version.to_s.strip
v2 = IO.read(File.expand_path(File.dirname(__FILE__) + "/../VERSION")).strip
v1.should == v2
end
end
And the last change to the infrastructure is to bundle the gems for two reasons, first to ease deployment, but second, more importantly to resolve dependencies of all gems together instead of individually (which can lead to dependency problems). So here’s a real simple Gemfile to get started with:
∴ cat Gemfile
source :gemcutter gem 'configliere', '>=0.0.9' gem 'extlib', '>=0.9.15' gem 'versionomy', '>=0.4.0' gem 'log4r', '>=1.1.7' gem 'jeweler', '>=1.4.0' gem 'rake', '>=0.8.7' gem 'rspec', '>=1.3.0', :require => 'spec'
Then to install the gems:
∴ bundler install
Try it
∴ ls *.log ls: *.log: No such file or directory ∴ bin/example ∴ ls *.log example000001.log ∴ cat example000001.log [DEBUG] 2010-06-07 01:41:12 :: Example.execute ∴ bin/example --version 0.1.0 ∴ bin/example --help usage: example command [...--param=val...] Params: --debug: Log debug messages to console --logfile: Log filename, default is: media2nfo.log --loglevel: Logfile logging level, must be one of: DEBUG, INFO, WARN, ERROR --quiet: Only log errors to console --trace: Trace logging --version: Application Version Environment Variables can be used to set: Commands:
Workflow
And now the work flow for building and publishing our gem:
∴ rake spec (in /Users/royw/views/example) /Users/royw/.rvm/gems/ruby-1.8.7-p174/gems/jeweler-1.4.0/lib/jeweler/commands/check_dependencies.rb:13:Warning: Gem::Dependency#version_requirements is deprecated and will be removed on or after August 2010. Use #requirement All dependencies seem to be installed. .. Finished in 0.004048 seconds 2 examples, 0 failures ∴ rake version:bump:patch (in /Users/royw/views/example) Current version: 0.0.0 Updated version: 0.0.1 ∴ echo "*.log" >> .gitignore ∴ git status # On branch master # Changed but not updated: # (use "git add ..." to update what will be committed) # # modified: .gitignore # modified: lib/example.rb # modified: spec/example_spec.rb # # Untracked files: # (use "git add ..." to include in what will be committed) # # Gemfile # bin/ # example.gemspec # lib/VERSION # lib/example/ no changes added to commit (use "git add" and/or "git commit -a") ∴ git add Gemfile bin example.gemspec lib .gitignore spec ∴ git status # On branch master # Changes to be committed: # (use "git reset HEAD ..." to unstage) # # modified: .gitignore # new file: Gemfile # new file: bin/example # new file: example.gemspec # new file: lib/VERSION # modified: lib/example.rb # new file: lib/example/cli.rb # new file: lib/example/example.rb # modified: spec/example_spec.rb # ∴ git commit -m "initial coding" Created commit 3aa37db: initial coding 9 files changed, 208 insertions(+), 2 deletions(-) create mode 100644 Gemfile create mode 100755 bin/example create mode 100644 example.gemspec create mode 120000 lib/VERSION create mode 100644 lib/example/cli.rb create mode 100644 lib/example/example.rb ∴ rake build (in /Users/royw/views/example) Generated: example.gemspec example.gemspec is valid. WARNING: no rubyforge_project specified Successfully built RubyGem Name: example Version: 0.0.1 File: example-0.0.1.gem
And optionally if you are using github as your master and/or releasing to gemcutter:
∴ rake release
Conclusion
While we have just touched the surface of these powerful tools, hopefully I have shown that creating your project as a gem is pretty easy. Just a few minor infrastructure enhancements and you’ve run out of excuses not to gemify your next project.
Have fun,
Roy
Tags: gem, Programming, Ruby