Using RubyGems commands from Ruby

in #programming6 years ago (edited)

thumb.png

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.

Sort:  

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