build a ruby gem and conditionally specify dependencies - ruby

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!!

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!

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=

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.

Related

Ruby gem - Requires depending on Runtime Options

I'm developing a ruby gem, and my requires are a mess.
The gem is a web Scraper, which depending on the given options, uses different methods to access the web, and thus needs to require different gems. Some users may never need some gems, or any of them.
My question is, what do I put in my .gemspec:
s.add_runtime_dependency #do I require all the gems here?
Where do I actually require the necessary gems in my code, and how do I do testing?
I don't know the conventions on this. Thanks.
*code: https://github.com/ZirconCode/Scrapah
RubyGems provides two main “types” of dependencies: runtime and development.
Runtime dependencies are what your gem needs to work (such as rails needing activesupport).
Development dependencies are useful for when someone wants to make modifications to your gem. When you specify development dependencies, another developer can run gem install --dev your_gem and RubyGems will grab both sets of dependencies (runtime and development). Typical development dependencies include test frameworks and build systems.
So in Gem Specification file you can add those gems that your gems need to work
Gem::Specification.new do |s|
s.name = "gem name"
s.version = "2.0.0"
s.add_runtime_dependency "daemons",
["= 1.1.0"]
s.add_development_dependency "bourne",
[">= 0"]

ruby gem statement - what does it do?

I think I have a basic understanding of what require/include statements at the top of a ruby script are doing, like
require 'rspec'
These statements are easy to google and find relevant results. But sometimes I have seen a gem statement like
gem 'rspec'
What does this line do?
In ruby code, gem(gem_name, *requirements) defined in Kernel tells Ruby to load a specific version of gem_name. That's useful when you have installed more than one version of the same gem.
For example, if you have installed two versions of rspec, say 2.12.0 and 2.13.0, you can call gem before require to use specific version. Note that gem should come before the require call.
gem 'rspec', '=2.12.0'
require 'rspec'
A gem 'gem_name' without version uses the latest version on your machine, and that's unnecessary. You can call require without gem to get the same behavior.
And besides, in Bundler::Dsl, gem is used to tell bundler to prepare/install specific version of ruby gems. You'll see that in Gemfile
The original behaviour of require, before Rubygems, was to search all the directories listed in the $LOAD_FILES variable for the file, and to load the first one it finds that matches. If no matching file was found, require would raise a LoadError.
Rubygems changes this process. With Rubygems, require will search the existing $LOAD_PATH as before, but if there is no matching file found then Rubygems will search the installed gems on your machine for a match. If a gem is found that contains a matching file, that gem is activated, and then the $LOAD_PATH search is repeated. The main effect of activating a gem is that the gems lib directory is added to your load path. In this way the second search of the load path will find the file being required.
Normally this will mean that the latest version of a gem that you have installed gets activated. Sometimes you will want to use a different version of a gem, and to do that you can use the gem method. The gem method activates a gem, and you can specify the version you want, but doesn’t require any files. When you later require the files you want, you’ll get them from the gem version you specified.
In Ruby, gems are packages with functionality that can be used out of the box (as libraries in other Programming languages).
The gems that you use with your Ruby Project can easily be managed with a tool called "bundler", just google it. The snippet of code you posted is part of the spec file that bundler uses to install and update all the libraries that you specify for your project.
If you are developing a Ruby on Rails, using gems an managing them with bundler is very common and so to say best practice.
Gems are just great because there are so many useful libraries that extend default functionality, eg of rails, and that you can use out of the box!
For a list of gems, visit rubygems.org

Writing a Jeweler Rakefile that adds dependencies depending on RUBY_ENGINE (ruby or jruby)

I have a Rakefile that includes this:
Jeweler::Tasks.new do |gem|
# ...
gem.add_dependency('json')
end
The gemspec that this generates builds a gem that can't be installed on jruby because the 'json' gem is native.
For jruby, this would have to be:
Jeweler::Tasks.new do |gem|
# ...
gem.add_dependency('json-jruby')
end
How do I conditionally add the dependency for 'json-jruby' when RUBY_ENGINE == 'java'?
It seems like my only option is to manually edit the gemspec file that jeweler generates to add the RUBY_ENGINE check. But I'd rather avoid this, since it kind of defeats the purpose of using jeweler in the first place.
Any ideas?
After some more digging around, I've come to the conclusion that the problem here isn't so much the limitation in Jeweler as it is a problem with how the json-jruby gem is named.
In a perfect world, the json-jruby gem should be named just json. That way when the gem file is built under JRuby (i.e. jgem build json-jruby.gem), the resulting gem file would be named json-1.4.3-universal-java-1.6.gem (RubyGems names the file based on the 'platform' property in the gemspec). When installed with the appropriate platform in the gem name, everything works transparently. There's no need to explicitly require json-jruby. RubyGems uses the appropriate version automatically.
I've contacted the maintainer of the json gem to see if he'd be willing to get json-1.4.3-universal-java-1.6.gem added to his gemcutter repo, but in the meantime you can download the drop-in jruby json gem here: http://roughest.net/json-1.4.3-universal-java-1.6.gem

How do I add conditional rubygem requirements to a gem specification?

Is it possible to add a gem dependency only if the person is using a certain version of ruby?
Background: I'm working on a fork of a project that uses Test::Unit::Autorunner and the like. They are part of the standard library in ruby 1.8, but aren't part of the standard library in 1.9.1, and is instead in the "test-unit" gem. I want to add a dependency that says that if someone's using ruby 1.9.1 or later, install the "test-unit" gem, but if they're using 1.8 or earlier, they don't need to install anything.
If you look at the gemspec documentation for add_dependency, there isn't an option for a ruby version. Perhaps you could use the post_install_message attribute to tell the user to install the gem if they're using ruby 1.9.
I did this exact thing for a project. The trick is to add the script as an extension, which will then get executed at install time on the user's machine.
Here are code snippets and links to our github:
First, when in the gemspec (we're actually using a Rakefile to generate it, but the result ends up the same) source
# This file *needs* to be named this, there are other files
# for other extension processors
s.extensions << 'ext/mkrf_conf.rb'
And then the relevant lines in that mkrf_conf.rb source
require 'rubygems/dependency_installer.rb'
inst = Gem::DependencyInstaller.new
inst.install "test-unit" if RUBY_VERSION > "1.9"
Gem doesn't support conditional dependencies (except on gem builder's environment -as noted above), and bundler is not a viable option to solve this either - see https://github.com/carlhuda/bundler/issues/1281
hay ... i'm kind of a ruby newbie ... if this is the best way to do it.
any way ... i wish i can do that using only Ruby .... though u can use your operating system shell to do that, by using this in your program installer, just execute (works for Linux based operating systems):
$ruby --version
(u can execute that from a ruby installer file, just like: ruby --version)
and put a possibility according to output, if it's 1.9.1 add an extra line to execute:
$ sudo gem install gem_name
else, just leave it be.
Checkout this tutorial in the Ruby Programming wikibook.
Tt shows how to install different versions of dependencies depending on what version of ruby the installee is using.
(short answer--it ain't as easy as it should be)
You can't. You need to build two gems, one with
spec.required_ruby_version = '~> 1.8.6'
and one with
spec.required_ruby_version = '~> 1.9.1'
spec.add_dependency 'test-unit', '~> 2.0.5'
Gemspecs are just ruby files anyway, so you can execute any ruby code inside them, so:
spec.add_dependency = 'test-unit', '>= 2.0' if RUBY_VERSION =~ '1.9'
EDIT: Specs run only on the builders machine.

How to quickly initialize ruby project development environment?

How to specify gem dependencies in a way that user with only ruby, rake and rubygems installed could issue a single rake command to install all the dependencies required? Is it possible to use the same dependency specification when building gem with GemBuildTask?
It's actually pretty easy to set up a rake task that installs a bunch of gems:
task :install_gems do
require "rubygems"
require "rubygems/dependency_installer"
installer = Gem::DependencyInstaller.new
[["rack"], ["merb-core", "1.0.12"]].each do |args|
installer.install(*args)
end
end
Of course, you could extract this into a method and write a prettier way to specify your dependencies, but this should work great.
I think currently you'd have to write a custom rake task that talked to the Gem library.
It's possible that rip, the (very) new kid on the block, will make it all easier, but it's very early days.
But someone else may have a better way...
If your app is packaged as a gem, you could add the dependencies to the gemspec and rubygems will attempt to install them for you when you install the gem.
There are a bunch of ways to make a gem out of some ruby code. Recently I have taken to using jeweler.
With it, you can install a project as a gem by running rake install. There are some instructions on how to do dependencies on its github wiki.

Resources