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 buildshebang - You should never use any external libraries unless you rescue possible
LoadError - Base64 for emails
Base64.decode64("c3Rhc3RueUAxMDFpZGVhcy5jeg==\n") -
Gem::Specification#files=withgit ls-filesis 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