Using RubyGems commands from Ruby
RubyGems, or just gem
, is Ruby's excellent package manager. It's got a pretty good command-line interface... but what if you want to interact with RubyGems from Ruby?
I ran into the need to do this when creating a RubyGems tool for my new universal build system, UltiTool, where each tool package is written in Ruby. I needed a method of building and installing gems from Ruby code.
The obvious solution is to invoke the gem
command line tool, but this isn't a great idea. For example, you're assuming that the user will have gem
on their path, but they might not if they're using a portable build of Ruby. In addition to this, throwing random strings at the command line is never a great idea.
Introducing rubygems/commands
Fortunately, RubyGems itself provides a rather elegant solution to this problem. RubyGems exposes a number of Command
objects, which allow each action to be invoked from Ruby almost as you would invoke it using the gem
command line tool.
Note: You might need to
require 'rubygems'
if you're using a really old version of Ruby.
For example, suppose I wanted to build a gem. I need to first require the relevant command and instantiate it:
require 'rubygems/commands/build_command'
build_cmd = Gem::Commands::BuildCommand.new
Once this is done, you can pass the Command
object arguments, then finally execute the command:
build_cmd.handle_options ['example.gemspec']
build_cmd.execute
This will do exactly the same as gem build example.gemspec
, while using a friendly Ruby API which is nice and portable. Neat!
Making RubyGems shut up
As just stated, this does exactly the same thing as gem build example.gemspec
. That includes printing information and status about the build. If you're trying to run the build as part of a different project, you might not want this output. Unfortunately, making RubyGems be quiet isn't as easy as one might first think.
The classic Ruby trick for silencing methods is to temporarily replace the $stdout
and $stderr
variables with a 'sinkhole' stream., but this doesn't work with RubyGems.
Instead, RubyGems uses an impressively complex UserInteraction
object heirarchy for handling command line operations. This allows the RubyGems interface to be modular.
What we need to do is create a UserInteraction
which just stores the output given in strings instead, rather than printing it. This is rather easy using the StringIO
class:
class CustomUI < Gem::StreamUI
def initialize
super(StringIO.new, StringIO.new, StringIO.new, false)
end
end
Once we've created this custom user interaction class, we can simply tell RubyGems to use that as the default one:
Gem::DefaultUserInteraction.ui = CustomUI.new
Now RubyGems will run silently! You can also retrieve the StringIO
values by calling CustomUI#outs
, CustomUI#errs
and CustomUI#ins
if you need to.
Making RubyGems not close your entire program unexpectedly
There's one more issue with using RubyGems like this. If the 'commands' portion of RubyGems encounters an error, then rather than throwing an exception like an ordinary library, it will just close your program. On the command line, this is usually what you want, but it makes developing applications which hook into RubyGems a real pain.
To rectify this, you can simply override RubyGems' terminate_interaction
method to throw an exception instead. Note that this method is passed an error code as a parameter, and if this code is zero, then everything went OK and you shouldn't throw an error.
Here's an example of how to do this by modifiying our CustomUI
class created earlier:
class CustomUI < Gem::StreamUI
def initialize
super(StringIO.new, StringIO.new, StringIO.new, false)
end
def terminate_interaction(exit_code = 0)
raise "Gem encountered fatal errors:\n#{errs.string}" if exit_code != 0
end
end
That's it!
You're all done! You can now start using RubyGems commands right from your project. In this example, I've just used the build
command, but there are loads more. It really helped me to skim through the source to find what commands were available and what I needed to require
to use them.
✅ @orangeflash81, I gave you an upvote on your post! Please give me a follow and I will give you a follow in return and possible future votes!
Thank you in advance!