Lock gem dependency versions for different rubies - ruby

Rubygems doesn't provide a proper way to specify different dependencies for different ruby versions. If we put a conditional in the gemspec, it can switch what gets installed:
if RUBY_VERSION < "1.9"
# ruby 1.8 only supports nokogiri < 1.6
s.runtime_dependency "nokogiri", ">= 1.4.0", "< 1.6"
else
s.runtime_dependency "nokogiri", ">= 1.4.0"
end
But, this doesn't control what gets loaded. If there's a newer version in the load path, that would get loaded even if it's incompatible with ruby 1.8.
I'm not sure if this is actually a problem: If you're using rbenv/rvm etc. then you have different gem paths for each ruby, so hopefully you'd never have both installed in the same place. I think even the standard gem paths are divided by compatibility versions (1.8 / 1.9.1). Is that intended to take care of this, or could you ever get into a situation where both versions are installed together?
The other approach would be to leave the gemspec open-ended (without the conditional), and warn users to set the correct version constraint in their Gemfile if they need it.
Which way is preferable?

I believe this is just the nature of how bundler and requiring gems works. It's one of the great benefits to tools such as rvm.

Related

What does it mean when a Ruby script starts with Gem?

I am working on learning Ruby and one thing that I have seen in several instances and cannot understand are scripts that start with the keyword gem.
An example can been seen in the Sensu code.
gem "amqp", "1.3.0"
require "amqp"
require File.join(File.dirname(__FILE__), "base")
I understand the require statement for accessing another gem, but what does the exact gem "amqp", "1.3.0" mean?
This is the gem method that Rubygems adds to Kernel (Rubygems is required by default in current Ruby versions). It activates a specific version of a gem (version 1.3.0 of the amqp gem in this case), meaning that the gems lib dir (or whatever dirs the gem specifies) is added to your LOAD_PATH, as are the lib dirs of any dependent gems it has.
It also checks that there are no version incompatibilities with any already activated gems.
All gems are activated when you use them. This normally happens when you call require. Calling gem activates the gem but doesn’t require any files from it, hence the line require "amqp" below (note the difference between amqp the gem, which the gem method refers to, and amqp the file, which is contained in the amqp gem and is what the require method is refering to).
This method is used to ensure you are using a specific version of a gem, without needing to use Bundler (or something similar). Bundler also has a gem method used in Gemfiles, but this is a different (but similar) method.
It’s also used to specify that you want to use the gem version of a library that is also part of the standard library (say if you want to use a more recent version). For example the Yaml library distributed with Ruby is Psych which is also available as a gem.

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

using bundler to load different versions of gems for different platforms

So I am developing a Sinatra for both windows and linux. The problem is I'm using Thin instead of Webrick and eventmachine for windows only works with a pre-release version while linux uses the latest stable. in the gemfile you of course cannot include the same gem with different versions like so:
gem "eventmachine", "~> 1.0.0.beta.4.1", :group => :development_win
gem "eventmachine", group => :development_linux
gem "thin
I was wondering if there was a way to work around this, maybe using one gemfile for windows and one gemfile for linux, what would the command be to load one or the other.
Alternatively is there a way to perhaps in git manage just the gemfile for two different platforms, perhaps through a branch for just the file (don't know if that's possible from what I've read of git branches).
You can do it like that:
# Windows
gem "eventmachine", "~> 1.0.0.beta.4.1", :platform => [:mswin, :mingw]
# C Ruby (MRI) or Rubinius, but NOT Windows
gem "eventmachine", :platform => :ruby
Full list of available platforms:
ruby C Ruby (MRI) or Rubinius, but NOT Windows
ruby_18 ruby AND version 1.8
ruby_19 ruby AND version 1.9
ruby_20 ruby AND version 2.0
mri Same as ruby, but not Rubinius
mri_18 mri AND version 1.8
mri_19 mri AND version 1.9
mri_20 mri AND version 2.0
rbx Same as ruby, but only Rubinius (not MRI)
jruby JRuby
mswin Windows
mingw Windows 'mingw32' platform (aka RubyInstaller)
mingw_18 mingw AND version 1.8
mingw_19 mingw AND version 1.9
mingw_20 mingw AND version 2.0
You can find more information in Gemfile(5) man page here (see 'Platforms' section).
Another approach is to use RUBY_PLATFORM constant:
if RUBY_PLATFORM =~ /win32/
gem "eventmachine", "~> 1.0.0.beta.4.1"
else
gem "eventmachine"
end
I haven't seen full list of available values for RUBY_PLATFORM but you can run
ruby -e 'puts RUBY_PLATFORM'
on both your platforms and see the difference.
You can use the --gemfile option to use different gemfiles for different platforms. See the documentation here
http://gembundler.com/man/bundle-config.1.html
You need multiple versions (all with the same name) of a gem. Therefore, currently with Bundler, you need multiple, simultaneous Bundler dependency snapshot 'lock' files. This is possible, if your developers make use of Bundler's gemfile configuration setting. They might do this either:
By making use of environment variable BUNDLE_GEMFILE (on the command line or in .bash_profile); or
(Probably less desirably) in .bundle/config (globally, in their home directories).
Thus, safely, Bundler can create (and presumably automatically later use, given the same configuration settings) e.g. Gemfile-linux.lock and Gemfile-windows.lock.
Although this basic approach seems workable, it's not very DRY. However, this improves if, e.g., both Gemfile-linux and Gemfile-windows automatically incorporate whatever Gemfile statements they share in common: i.e., if they include the statement:
::Kernel.eval(File.open('Gemfile-common','r'){|f| f.read},::Kernel.binding)

Managing conflicting versions of ruby gems

I am building a framework that loads user provided ruby code. It is basically a plugin mechanism. I want the user provide ruby code to be able to require gems of its own. I intend to have the "plugin" package include a vendor directory with the gems.
How can I load gems that are required by the plugin without having them conflict with my framework's gems? For example, if my framework uses treetop version 1.3.0, and a plugin uses treetop 1.4.2 I want each to work with their specified version.
Likewise, is there a way to prevent plugins from conflicting with each other?
I have looked at gem_plugin, _why's sandbox, and some other tools. But I don't see any library that specifically handles this case - I assume its been done before.
I have also looked at the internals of Bundler to see how it manages gem versions. I am prepared to do some pretty complex stuff if need be. But I am still uncertain of how to go about it.
I also have a lot of freedom in how I implement this. So if you think I am barking up the wrong tree, please say so.
Thanks for any advice.
SIDE NOTE: It occurred to me while writing this that I need something similar to the Classloaders in a Java servlet container. A WAR file can include jar files, and the web application's class loader will prefer those over the jars that are on the global classpath. Is there any way in ruby to segment the ruby "classpath" (i.e. load_path, require, etc)?
To be blunt, you can't have two versions of the same gem loaded at the same time.
Bundler does a good (ish) job of looking through all of the required gems and finding a solution to the various overlapping dependencies, but even so it is limited to only loading one version of a gem at a time.
This leads to plugin developers constantly having to update to support any changes that are made in dependent gems in order to avoid just the situation you describe.
(Don't get me started on the screw up that results from the various competing JSON implementations and the pain you have to go through when you have several gem dependencies all requiring different ones.)
Respectfully disagree with the answer above. Here is how I do it:
ruby -S gem list my_gem
`*** LOCAL GEMS ***
my_gem (1.0.1, 1.0.0, 0.0.2)
`
ruby -S gem lock my_gem-1.0.0 > locklist.rb
which generates list of dependencies for a specific version into locklist
require 'rubygems'
gem 'my_gem', '= 1.0.0'
gem 'gem_base', '= 1.0.0'
gem 'rest-client', '= 1.7.2'
gem 'savon', '= 1.1.0'
gem 'addressable', '= 2.3.6'
gem 'mime-types', '= 1.25.1'
gem 'netrc', '= 0.11.0'
now you can do load('locklist.rb') which will load a specific version of a gem along with its dependencies. Look ma, no Bundler.

Do you have to do duplicate gem installs for JRuby & MRI?

I have JRuby and Ruby (MRI) installed. It seems that I need to install gems twice - once for each of these platforms. Is this necessary or am I doing it wrong? After I installed the rails gem for MRI, should I have pointed JRuby to it, or was it necessary for me to also call: "jruby -S gem install rails"
You need to install gems for each different install of ruby that you have.
If you set GEM_HOME you can share your gem installations.
Some gems target specific platforms, e.g. Mongrel (there's a MRI one and a JRuby one). Also, JRuby cannot use gems that have native extensions (i.e. C code) unless they use the FFI (which most do not - yet).
Personally I have separate gem repos for MRI and JRuby. The little bit of extra hassle is worth the peace of mind when trying to track down a problem.
It's pretty easy to see what each repo has installed:
jruby -S gem list --local
vs.
gem list --local
You could even write a ruby script to sync one gem list to the other, but you'd have to be careful about platform specific gems....
I hit this problem when creating my gem, jimmy_jukebox, but made my gem work with both.
First, JRuby doesn't handle fork...exec (and even replies incorrectly to Process.respond_to?(:fork)), so I had to rescue NotImplementedError and use Spoon.spawnp instead.
I then created (in my gem's /bin directory) paired executables -- play_jukebox and jplay_jukebox; and load_jukebox and jload_jukebox -- each with the correct shebang line (/usr/bin/env ruby or /usr/bin/env jruby).
I'd love to know a better way. But I'd rather handle everything within a single gem than maintain and distribute multiple gems.

Resources