programmatically determine if a certain gem is installed, then install it if not - ruby

This doesn't seem like it would be a hard thing to do.
I want a ruby script that figures out if a certain gem (any version, and/or a certain version spec) is currently installed on the system (wherever gem list will look), and if not, installs it.
Yes, I know in some cases bundler is great at that. Trust me that I have a case where for weird reasons I don't want to use bundler. I really do want to programmatically interrogate the local gem repo.
One way to programmatially install is just to shell out to gem install, sure.
But I can't manage to figure out a reliable way to programmatically interrogate to see if a particular gem is installed, not with shell out, not with rubygem api, not anything.
Am I missing it?

# The version requirements are optional.
# You can also specify multiple version requirements, just append more at the end
gem_name, *gem_ver_reqs = 'json', '~> 1.8.0'
gdep = Gem::Dependency.new(gem_name, *gem_ver_reqs)
# find latest that satisifies
found_gspec = gdep.matching_specs.max_by(&:version)
# instead of using Gem::Dependency, you can also do:
# Gem::Specification.find_all_by_name(gem_name, *gem_ver_reqs)
if found_gspec
puts "Requirement '#{gdep}' already satisfied by #{found_gspec.name}-#{found_gspec.version}"
else
puts "Requirement '#{gdep}' not satisfied; installing..."
# reqs_string will be in the format: "> 1.0, < 1.2"
reqs_string = gdep.requirements_list.join(', ')
# multi-arg is safer, to avoid injection attacks
system('gem', 'install', gem_name, '-v', reqs_string)
end
More recent rubygems versions provide an installer API, so instead of shelling out to the gem command you could also use:
# using the same "gdep" variable as above
Gem.install gem_name, gdep.requirement
However, I'm not sure if Gem.install respects your .gemrc file.
There are a lot of useful methods for querying your installed gems (see rdocs). Some that might be helpful:
Gem::Specification.find_all_by_name
Gem::Requirement#satisfied_by?(gem_version_instance)
Gem::Specification#satisfies_requirement?(gem_dependency_instance)
Gem.loaded_specs - hash of the gems you've actually loaded via the gem method, or by require

Last answer was good, but this is a little more precise:
`gem install redis` unless `gem list`.lines.grep(/^redis \(.*\)/)
Matches only the gem named redis, and not other gems like redis-native_hash, or something else.
Another way I've seen this done is to try requiring the gem.
begin
require 'some_crazy_gem'
rescue LoadError
`gem install some_crazy_gem`
#...
end

Check that there is installed a gem newer than a particular version.
PROMPT> gem list nakamoto -i -v ">=1.2.3"
true

I have no idea if this is a best practice or not but:
list = `gem list`
`gem install builder` unless list.include? "builder"
... worked for me in irb.

Related

build a ruby gem and conditionally specify dependencies

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.

How do I get a list of gems that are installed that have native extensions?

I'm on windows, and have updated from ruby 1.8.x to 1.9.x, and am now getting error popups that complain ruby-mssomethingrt.1.8.x.dll is missing.
I would like to find out which gems have native extensions, so I can uninstall them and force a rebuild of the native extensions locally during installation again, to make the error go away.
Based on this answer, here is a solution that finds and offers to reinstall gems with native extensions that works with recent rubies (>=1.9).
native_gems = []
Gem::Specification.each do |spec|
native_gems << "#{spec.name}:#{spec.version}" unless spec.extensions.empty?
end
install_cmd = "gem install #{native_gems.join ' '}"
puts "Found #{native_gems.length} gem(s) with native extensions:"
puts "\n> " + install_cmd, "\nReinstall gems with above command? (yn)"
exec insall_cmd if gets.downcase[0] == 'y'
Example Output:
Found 36 gem(s) with native extensions:
> gem install atomic:1.1.13 bcrypt-ruby:3.0.1 bigdecimal:1.2.0 eventmachine:1.0.3 eventmachine:1.0.0 eventmachine:0.12.10 ffi:1.9.3 ffi:1.9.0 ffi:1.7.0 hiredis:0.4.5 hpricot:0.8.6 io-console:0.4.2 json:1.8.1 json:1.8.0 json:1.7.6 nokogiri:1.6.0 nokogiri:1.5.9 pg:0.17.1 pg:0.17.0 pg:0.16.0 pg:0.15.1 pg:0.13.2 psych:2.0.0 puma:2.7.1 puma:2.6.0 puma:2.4.0 puma:1.6.3 sqlite3:1.3.8 sqlite3:1.3.7 sqlite3:1.3.5 therubyracer:0.12.0 thin:1.5.1 thin:1.5.0 thin:1.4.1 websocket-driver:0.2.3 websocket-driver:0.1.0
Reinstall gems with above command? (yn)
…
A good start would be to look at the gem specification for each gem and see if it has the extensions field set. That should leave you with a short-list of gems to re-install. They don't necessarily all use native extensions, but if you look at the corresponding extconf.rb files, this should be pretty easy to find out.
Update: Here is a short ruby script to list those gems:
require 'rubygems'
Gem.source_index.each do |gem|
spec = Gem.source_index.specification(gem[0])
ext = spec.extensions
puts "#{gem[0]} has extensions: #{ext}" unless ext.empty?
end
You can rebuild (and restore to a pristine state) all installed gems with:
gem pristine --all
--all --no-extensions will restore gems without extensions, but despite being documented, --extensions appears to have no effect (at least on rubygems 1.8.23 on Ubuntu 12.10).
In Cygwin you could try gem list --all -d | grep --before-context=1 --after-context=4 Platform.
gem list
the part after the version next to the gem should indicate whether it's running native code:
e.g. json (1.4.6 x86-mingw32)
The error you are seeing is because one of the gems you are using expects the 1.8 ruby interpreter to be present which it no longer is (as you have upgraded to 1.9).
I would have thought that just running 'gem update' would fix your problem. If it doesn't, then you might need to seek an alternative gem for the one that is expecting the ruby 1.8 interpreter to be present.
Onliner for bundler + rails:
rails r "puts Gem::Specification.select{|i| i.extensions.any?}.map(&:name)"
prints out all native code gems (which usually need updates when updating major Ruby versions)

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.

Best way to install mulitple gems onto a computer?

Is there an easy way, when running a Ruby script, to get it to automatically install gems required for script?
For example, consider these require statements at the top of a ruby script:
require 'net/http'
require 'fileutils'
require 'archive/zip'
Now, I know (as a human and programmer) that for this script to run on a given PC with Ruby, the 'gem install archive-zip' command needs to be run first, before the script will work. But if this script needs to run on dozens of PCs, is there anything that can save me from having to ensure ALL the gem dependancies are installed first?
Furthermore, what if there are several gems required?
Not sure if this is exactly what you are after but when I have a server set up how I want I dump a list of my gems to somewhere safe...
gem list > my_gems.txt
If I need to rebuild the box or build another machine I use this script to install the gems...
bulk_gems.rb
#! /usr/local/bin/ruby
STDIN.readlines.each do |l|
m = l.match /^(\S+) \((.*)\)/
unless m.nil?
gem_name, versions = m[1], m[2].split(',')
versions.each do |v|
system "gem install #{gem_name} --version #{v} --ignore-dependencies"
end
end
end
more my_gems.txt | bulk_gems.rb
By using gem unpack you can unpack the gems into a directory. From there, you can include them in your script. For example, randomly picking the gem morse (a gem that encodes/decodes morse code), let's say I use gem unpack morse to put it in a directory /gems/. It unpacks to the directory morse-0.0.2, since that's the version.
$LOAD_PATH << './gems/morse-0.0.2/lib'
require 'morse'
# The gem is included, and Morse is now defined.
Shoes has a really slick way of doing this. See this blog post by _why.
You could port some of that to standard ruby (without the fancy UI)

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