Let’s say you have a Rubygem named “blorf”. You want to enable other developers to write plugins in the forms of Rubygems of their own. For the end user, loading the plugins should be as simple as writing:

require 'blorf'
require 'blorf-git'
require 'blorf-twitter'
require 'blorf-cowsay'
# ...
Blorf.activate! # activate any loaded plugins

In order to make this work, blorf needs to look for files matching a certain path pattern inside other loaded gems. If a gem contains a file matching ‘lib/blorf/plugins/**/*/rb’, it should be loaded when blorf is loaded.

Rubygems provides a method which almost fits the bill. The only problem with Gem#find_files is that it will find files matching that pattern in all installed gems–not just in the gems that have been loaded. If you have multiple versions of a Gem installed it will find paths in all the installed versions. We want the end user to control gem versions, so this method doesn’t help us.

Here’s some code which will look in only loaded gems for files matching a given globbing pattern. It’s complicated by the fact that the Rubygems API for this changed (for the better) at version 1.8.0. This code has been tried on Rubygems versions 1.8.7 and 1.6.2.

  def find_files_in_active_gems(glob)
    rubygems_version = Gem::Version.new(Gem::VERSION)
    if rubygems_version >= Gem::Version.new("1.8.0")
      # somewhat messy new version
      Gem.loaded_specs.values.inject([]){|ps,s|
        ps.concat(s.matches_for_glob(glob))
      }
     else
      # even messier old version
      Gem.loaded_specs.values.  # get the gemspecs for active gems
        map{|s|                 # find the lib glob pattern for each gem
          Gem.searcher.lib_dirs_for(s)
        }.flatten.map{|d|       # append the glob pattern and look for matches
          Dir.glob(File.join(d, glob))
        }.flatten
    end
  end

Yes, this is messy first-working-version exploratory code. It could stand to be cleaned up.

At Eric Hodel’s encouragement I’ve submitted a ticket which, if accepted, would reduce this to a one-liner.

Published by Avdi Grimm

11 Comments

  1. Why not applying inversion of control here? I would be tempted to look for a solution where each plugin has to explicitly register for being activated later. 

    Doesn’t your solution involve strong coupling with rubygems, which could be avoided?

    Just my 5-cents critical reading, btw. I’m not sure to catch the context of use, that is.

    Reply
  2. Here, I fixed your code:

      $:.map { |lib| Dir.glob File.join(lib, plugin_glob) }.flatten

    Yes, I agree with Bernard. Because the gems you’re interested in are activated, it means their “lib” dirs are in $LOAD_PATH. You shouldn’t need RubyGems.

    Reply
  3. How do you determine which files to require in the first place?

    Reply
    • I’m not sure I understand the question, can you elaborate?

      I’m talking about systems where the person writing the pluggable gem; the person writing a plugin; and the person deciding which plugins to use (and which versions of the plugins to use) are three different people. If that’s any help.

      Reply
      • When writing plugins/pluggable gems I let the person deciding which plugins to use manage that through gem install blorf-some_plugin instead of gem install blorf-some_plugin plus require ‘blorf-some_plugin’

        Why have the user take the extra step?

        Reply
        • As the plugin user, I may have two dozen plugins installed but only want to use three of them for a given project. Guard is an example that comes to mind. A better example might be Firetower: I don’t want the workflow for disabling a plugin to be “uninstall it”. Especially if they happen to be in the process of developing that plugin but have decided to temporarily disable it.

          For development-oriented gems it may be reasonable to assume the end-user will be constraining their gem set with Bundler or RVM, but for general userland command-line utilities and the like that’s not a safe assumption.

          Reply
  4. Might I suggest you take a look at the “little-plugger” project

    https://github.com/TwP/little-plugger

    It is a single ruby module you include somewhere in your code. The Logging framework uses it, and there are some examples in the class documentation

    https://github.com/TwP/little-plugger/blob/master/lib/little-plugger.rb

    Specifically, it will search through all your gem files for potential plugins. You can then restrict which plugins are loaded by simply listing the ones you want …

    Logging.plugin :foo, :bar, :baz

    Anyway, take a read through the source code. It was fun to write, and hopefully someone other than myself might find it useful.

    Blessings,
    TwP

    Reply

Leave a Reply

Your email address will not be published. Required fields are marked *