19

I am working on a gem that has needs to set dependencies conditionally when the gem is installed. I've done some digging around

and it looks like i'm not alone in this need.

Rubygems: How do I add platform-specific dependency?

this is a long thread

http://www.ruby-forum.com/topic/957999

The only way I can see to add dependencies to a gem is to use add_dependency method within a Gem::Specifiction block in a .gemspec file

Gem::Specification.new do |s|

  # ... standard setup stuff

  # conditionally set dependencies
  s.add_dependency "rb-inotify", "~> 0.8.8" if RUBY_PLATFORM =~ /linux/i
  s.add_dependency "rb-fsevent", "~> 0.4.3.1" if RUBY_PLATFORM =~ /darwin/i
  s.add_dependency "rb-fchange", "~> 0.0.5" if RUBY_PLATFORM =~ /mswin|mingw/i

end

Based on all of the docs and threads I found on the net, I would have expected that if you install the gem on

  • Linux, then, rb-inotify would be a dependency and auto-installed
  • Mac - rb-fsevent would be installed
  • Windows - rb-fchange would be installed

However, it seems that is not the case. The "if" statements within the block are evaluated at the time the gem is built and packaged. Therefore, if you build and package the gem on Linux, then, rb-inotify is added as a dependency, Mac, then, rb-fsevent, Windows - rb-fchange.

Still needing a solution, I dug around in the rubygems code and it seems the following is a broad stoke of what happens.

  • build all of your code for your gem: foo.gem
  • create a foo.gemspec file
  • build, package, and release the gem to a gem server such as rubygems.org
  • let everyone know
  • developers install it locally via: gem install foo
  • the foo.gem file is downloaded, unpacked, and installed. all dependencies are installed as well.
  • everything should be set and we can beging using the gem.

It seems that when the gem is built and released the foo.gemspec file is loaded and the Gem::Specification block is evaluated and converted to YAML, compressed as metadata.gz, and included in foo.gem. The ruby code is compressed into data.tar.gz and included as well. When the gem is installed on the local developer machine, the YAML is extracted from metadata.gz and converted back into a Gem::Specification block, however, it is not converted back to the original block.

instead, you will see something like the following:

Gem::Specification.new do |s|

  if s.respond_to? :specification_version then
    s.specification_version = 3

    if Gem::Version.new(Gem::VERSION) >= Gem::Version.new('1.2.0') then
      s.add_runtime_dependency(%q<rb-inotify>, ["~> 0.8.8"])
    else
      s.add_dependency(%q<rb-inotify>, ["~> 0.8.8"])
    end
  else
    s.add_dependency(%q<rb-inotify>, ["~> 0.8.8"])
  end

end

Ok. So, I have a bird's eye view of the process, however, that does not change my desire to build a single gem and conditionally specify dependencies for a range of OS targets.

If anyone has a solution other than building multiple .gemspec files for each target OS... I'm all ears!!

Community
  • 1
  • 1
worthspending
  • 301
  • 4
  • 11

3 Answers3

2

I have also stumbled upon this problem in the past. The only workaround I could find was to create a Rake task for installing the dependencies. Of course, at that stage, you might just want to let the user figure out on his own which gem he is missing based on the error message he is receiving. In my case, there were several platform-dependent dependencies to be installed, so that wasn't an option.

Rakefile:

task :install do |t|
  require './lib/library/installer'
  Library::Installer.install
end

Installer:

module Library::Installer

  require 'rubygems/dependency_installer'

  def self.install
    installer = Gem::DependencyInstaller.new
    dependency = case RUBY_PLATFORM
      when /darwin/i then ["rb-fsevent", "~> 0.4.3.1"]
      when /linux/i then ["rb-inotify", "~> 0.8.8"]
      when /mswin|mingw/i then ["rb-fchange", "~> 0.0.5"]
    end
    installer.install(*dependency)        
end

Then, the user can use rake install to get install appropriate dependencies.

Conditional dependency install (not just based on platform, but based on user input, for example) is cruelly missing to RubyGems. Let's hope it'll get implemented in the future!

user2398029
  • 6,699
  • 8
  • 48
  • 80
1

i have never done this myself, but there are some gems that are available in platform specific versions: http://rubygems.org/gems/libv8/versions

from what i understand it's just a naming thing, which can be configured by setting the platform option of your gemspec. have a look at the doc: http://guides.rubygems.org/specification-reference/#platform=

phoet
  • 18,688
  • 4
  • 46
  • 74
0

I have looked into this as well and have come to the conclusion that is not possible by design. Having a single 'mega gem' for all platforms causes the problem of not knowing if a platform is supported until the gem is downloaded and installed. A Gem would have to be smart enough to determine what is correct way to install depending on the platform. If a platform is not supported at all, the gem may fail horribly, opening a big can of worms. There use to be a callback after a gem was installed that was removed for the same reason, no magic to get a gem to install correctly. Some people have hacked around this using mkmf, but I suggest following the worn path of a gem per platform as the better solution.

Based on this, in a project that builds a gem for ruby and jruby, I have to manually create each gem and upload them to RubyGem. Using Jeweler this is as simple as specifing the Gemfile, but I have to rebuild the gem spec each time I package a gem. Fairly trivial when supporting only 2 platforms, but the build process is straight forward enough that it could be automated to provide support multiple platform gems.

mguymon
  • 8,946
  • 2
  • 39
  • 61