Fork me on GitHub

The Truth About Gemspecs

This post has been updated at 16/08/2011.

There are still many folks who know shit about gemspecs, who misunderstand what gemspecs are good for or don't have a clue how to work with them. So let me explain to you and show some tricks how to use them effectively.

There are still many folks who know shit about gemspecs, who misunderstand what gemspecs are good for or don't have a clue how to work with them. So let me explain to you and show some tricks how to use them effectively.

How the Gem is Created?

RubyGems provides command build which takes your gemspec as an only argument, reads its content and evaluates it via Kernel#eval. Particularly important is that, your gemspec has to return Gem::Specification object, because in RubyGems there is something like spec = eval(File.read(gemspec)) which obviously can access just the last returned value and therefore if you put puts("Done") to the end of your gemspec, it won't work because RubyGems won't be able to get the specification object.

So then RubyGems just takes the specification and build the gem according the informations provided in it. Easy-peasy. However the crucial thing here is it will compile the gemspec and save it in the expanded form. Basically it means if you have Dir.glob("lib/**/*") in your gemspec, RubyGems expand it to the list of files.

Surprisingly some guys including these I consider a good developers like wycats and qrush argues that gemspecs shouldn't use relative paths, refer __FILE__ and so on. I don't think so, because this is what the RubyGems-generated specifications are good for. The gemspecs you write are for building gems and since RubyGems itself take care about the compilation, I don't see any reason why you should bother about it yourself.

Generated Gemspecs?

You should provide a way how to build your gem without installing any external libraries. The solution should be your gemspec, which I consider as a standard solution.

Many people use some stupid Rake tasks, hoe, jeweller or similar shits to generate a gemspec. I think it's a very bad idea, because:

  • Your Rakefile often use some external dependencies and therefore users who just clone your project and want to change something and rebuild the gem can have serious troubles to get it working.
  • If you don't regenerate your gemspec before each commit, there can be serious inconsistencies, because these tools use expanded list of files and if you simply remove a file in one commit and don't regenerate your gemspec, then the gemspec is just useless because it won't work. And if you do so, you'll have awful mess in your commits.

This is why I write gemspecs myself. It's simplier than remember some Hoe API anyway, so what's everyone's problem about it?

Tricks for Effective Working with Gemspecs

  • Gemspec can be an executable with #!/usr/bin/env gem build shebang
  • You should never use any external libraries unless you rescue possible LoadError
  • Base64 for emails Base64.decode64("c3Rhc3RueUAxMDFpZGVhcy5jeg==\n")
  • Gem::Specification#files= with git ls-files is nice way for adding everything in repository to the gem specification
  • You can have more gemspecs in one directory what is a very nice way how to handle prerelease versions (see example bellow).
#!/usr/bin/env gem build
# encoding: utf-8

require "base64"

Gem::Specification.new do |s|
  s.name = "example"
  s.version = "0.2.1"
  s.authors = ["Jakub Šťastný aka Botanicus"]
  s.homepage = "http://github.com/botanicus/example"
  s.summary = "This is just an example app"
  s.description = "#{s.summary}. You can find examples how to work with gemspecs here."
  s.cert_chain = nil
  s.email = Base64.decode64("c3Rhc3RueUAxMDFpZGVhcy5jeg==\n")
  s.has_rdoc = true

  # files
  s.files = `git ls-files`.split("\n")

  Dir["bin/*"].map(&File.method(:basename))
  s.default_executable = "example"
  s.require_paths = ["lib"]

  # Ruby version
  s.required_ruby_version = ::Gem::Requirement.new("~> 1.9")

  # dependencies
  # RubyGems has runtime dependencies (add_dependency) and
  # development dependencies (add_development_dependency)
  s.add_development_dependency "simple-templater", ">= 0.0.1.2"
  s.add_development_dependency "bundler"

  begin
    require "changelog"
  rescue LoadError
    warn "You have to have changelog gem installed for post install message"
  else
    s.post_install_message = CHANGELOG.new.version_changes
  end

  # RubyForge
  s.rubyforge_project = "example"
end

And now let's just reuse it for the prereleasing:

#!/usr/bin/env gem build
# encoding: utf-8

eval(File.read("example.gemspec")).tap do |specification|
  specification.version = "#{specification.version}.pre"
end

Very nice but RubyGems feature is Gem::Specification#post_install_message= which is useful for showing some informations about your software, typically changes in current version. And this is why I made Changelog gem, which know how to parse changelog (in a nice, functional way) and it has useful method CHANGELOG#version_changes, for pretty-printing these changes.

Again, you can find an example of the usage in the previous example and on the picture bellow is how it can look like:

blog comments powered by Disqus
About

RSS

All Posts IT Ruby RubyGems gemspec

Tags

GitHub projects

Twitter @botanicus

Recent Comments