Moon

Mining RubyGems from Ore

2010 / 10 / 25 — gemspec, ore, ruby, rubygems

Recently there has been some interesting discussion on the role of Gem builders and gemspecs. Jeff Kreeftmeijer wrote about how easy it is to build a RubyGem using a hand-written .gemspec file. With just a stand-alone .gemspec file one can:

  • Build RubyGems: gem build my-project.gemspec.
  • Publish RubyGems: gem push my-project-0.1.0.gem.

One thing worries me is the fact that you either have to specify everything explicitly in the gemspec, or use inline git commands to query the files of a project. Copying and pasting all this boilerplate code around seems like a potential future maintenance hassle; also not very DRY.

The argument for just a .gemspec file did get me thinking. I do find myself regenerating the gemspec with Jeweler, almost once per-day. Also, these stand-alone gemspecs are pretty succinct. So when in doubt, see how other languages solved the problem.

Code vs. Data

I asked one of my Haskell friends how Cabal (the Haskell packager of choice) solves this problem. He pointed me to the Cabal file of his Haskell webapp serialist.net. Cabal uses easy to read, easy to parse and easy to edit plain-text. This reminded me, Code is for describing logic and flat-files are for describing static data. The majority of the information in the .gemspec file, is static data.

Now I am starting to really question the whole reason for an explicit gemspec. The .gemspec file only exists to create a Gem::Specification object, which is then passed to Gem::Builder or loaded by Bundler. Why are we writing Ruby code that normally would only exist in-memory? The gemspec purists state this is to allow things such as, dynamically loading the VERSION constant from the project or dynamically listing files tracked by Git. Although, both of these tasks can easily be automated by a library.

So I wondered, why not have a small library that loads the project information from a YAML file, fills in any missing information, and then creates a new Gem::Specification object. Furthermore, If we can call a method and get a Gem::Specification object back, we could place this in the .gemspec file for both gem build and Bundler to make use of.

Introducing Ore

Ore allows you to define all project information for a RubyGem in a single YAML file (gemspec.yml).

name: ore
version: 0.1.2
summary: Cut raw RubyGems from YAML
description:
  Ore is a simple RubyGem building solution. Ore handles the
  creation of Gem::Specification objects as well as building '.gem'
  files. Ore allows the developer to keep all of the project information
  in a single YAML file.

license: MIT
authors: Postmodern
email: postmodern.mod3@gmail.com
homepage: http://github.com/postmodern/ore
has_yard: true

dependencies:
  thor: ~> 0.14.3

development_dependencies:
  yard: ~> 0.6.1
  rspec: ~> 2.0.0

With Ore, one can write their description as free-text, no more using with awkward Ruby %Q{...} syntax.

Dependencies are listed in a YAML Hash, no more repeating add_dependency or add_development_dependency.

Ore can also infer missing information. If the version is not specified, Ore will search for and parse any VERSION or VERSION.yml files. Ore can even slurp up any VERSION, MAJOR, MINOR, PATCH or BUILD constants from a version.rb file in the lib/ directory. Also, notice that files is not listed, this is because Ore can detect the project is using Git, and list all files tracked by Git.

For a complete reference of everything that may go into a gemspec.yml file, and how Ore infers missing data, please see GemspecYML.

Building gems with Ore is easy as:

$ ore
Successfully built RubyGem
Name: ore
Version: 0.1.2
File: ore-0.1.2.gem
$ ls pkg/
ore-0.1.2.gem

One can still get the traditional gemspec from Ore:

$ ore gemspec
# -*- encoding: utf-8 -*-

Gem::Specification.new do |s|
  s.name = %q{ore}
  s.version = "0.1.2"

  s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
  s.authors = ["Postmodern"]
  s.date = %q{2010-10-25}
  s.default_executable = %q{ore}
...

We can even use Ore in .gemspec files:

# -*- encoding: utf-8 -*-

begin
  Ore::Specification.new do |gemspec|
    # custom logic here
  end
rescue NameError
  begin
    require 'ore/specification'
    retry
  rescue LoadError
    STDERR.puts "The 'my-project.gemspec' file requires Ore."
    STDERR.puts "Run `gem install ore-core` to install Ore."
  end
end

Ore will still work with gem build and even Bundler.

Mining RubyGems from Ore

Ore also comes with an extendable generator, for creating new projects:

$ mine my-project
Generating /home/hal/my-project
  create  lib
  create  lib/my/project
  create  spec
  create  .rspec
  create  spec/my/project_spec.rb
  create  spec/spec_helper.rb
  create  .document
  create  .gitignore
  create  my-project.gemspec
  create  ChangeLog.rdoc
  create  LICENSE.txt
  create  README.rdoc
  create  Rakefile
  create  gemspec.yml
  create  lib/my/project/version.rb
  create  lib/my/project.rb
     run  git init from "."
     run  git add . from "."
     run  git commit -m "Initial commit." from "."

By default mine will generate an RDoc and test-unit project. mine can also generate very customized projects:

$ mine my-project --rspec --yard --markdown --bundler
Generating /home/hal/my-project
      create  lib
      create  lib/my/project
      create  spec
      create  .yardopts
      create  .rspec
      create  spec/my/project_spec.rb
      create  spec/spec_helper.rb
      create  Gemfile
      create  .document
      create  .gitignore
      create  my-project.gemspec
      create  ChangeLog.md
      create  LICENSE.txt
      create  README.md
      create  Rakefile
      create  gemspec.yml
      create  lib/my/project/version.rb
      create  lib/my/project.rb
         run  git init from "."
         run  git add . from "."
         run  git commit -m "Initial commit." from "."

mine simply renders Ore Templates. Unlike other generators which have their logic hard-coded in Ruby, Ore Templates are simply directories, containing static and ERb files. One can make their own Ore template by creating a directory, adding files and publishing a Git repository.

Users can install custom Ore templates from Git repositories:

$ ore install http://github.com/user/awesometest.git
$ ore list
Builtin templates:
  base
  rspec
  test_unit
  yard
  bundler
  jeweler_tasks
  rdoc
  ore_tasks
Installed templates:
  awesometest

Then use the -T option to specify additional custom templates:

$ mine my-project --yard --markdown -T awesometest

Workflow

By default Ore does not impose a workflow onto the developer. Even the mine utility does not add any additional Rake tasks to new projects. This allows the developer to use gem build and gem push, or even use Jeweler::Tasks with Ore.

For those just wanting simple Rake tasks to build, push and tag releases, there is ore-tasks. Ore::Tasks provides the following tasks:

rake build            # Only builds a Gem
rake console[script]  # Start IRB with all runtime dependencies loaded
rake gem              # Alias to the 'build' task
rake install          # Builds and installs a Gem
rake push             # Builds and pushes a Gem
rake release          # Builds and Pushes a new Gem / Build, Tags and Pushe...
rake tag              # Tags a release and pushes the tag
rake version          # Displays the current version

To generate a project with Ore::Tasks included:

$ mine my-project --ore-tasks

To generate a project with Jeweler::Tasks included:

$ mine my-project --jeweler-tasks

Dog Fooding

In order to do real-world testing with Ore, I created ore-example which uses Bundler, RSpec2, YARD and Ore::Tasks.

As of now, I have also migrated my chars and uri-query_params libraries to Ore.

Interested?

$ gem install ore
$ mine the-future

For questions or feedback, join #ruby-ore on irc.freenode.net.

All source-code is located on GitHub.

Ore is tested with RSpec2 and has extensive YARD documentation.

Comments

blog comments powered by Disqus