Sunday, February 17, 2008

Shared Ruby Code

It's no secret that I think plugins are unnecessary. Unfortunately, they are also popular, largely due to to how easy it is to create a plugin and put it in your Rails project.

It's also easy to create a new gem. In 2006 Dr. Nic released the newgem gem. This was a great step in 2006, but it's 2008 now and it doesn't feel like we've moved much in the way of shared ruby code.

We're all to blame.

The vast majority of Ruby work is working with Rails. I do believe the next step for RubyGems is to play seamlessly with Rails. Since RubyGems are great for any Ruby code, it doesn't make sense to add Rails specifics to RubyGems. However, RubyGems could be more opinionated about how gems are structured. Right now, RubyGems library code can be structured in almost any way that you'd like. If the structure followed a stricter convention then auto gem loading code could be written for Rails.

I like newgem, but I think the features provided by newgem should actually be part of RubyGems itself. If the gem command could create a project skeleton it could be opinionated about the file structure without creating a larger barrier to entry.

There is another change that needs to happen. Everyone knows that you should vendor everything for your Rails projects. But, there's no single step to go from uninstalled gem to vendored gem. The path is generally to install it locally and them unpack it in whatever folder you've created that gets autoloaded.

I'm pretty sure a remote unpack is fairly simple, which is why we are all to blame. RubyGems is an open source project. Anyone can contribute patches to make it better.

Lastly, if gems become the standard way to share Ruby/Rails code then we'll need a few rake tasks added to Rails. For example, who wouldn't want a rake command to upgrade to a later version of a gem.

I'm hoping 2008 is the year that shared ruby code becomes synonymous with RubyGems and the year Rails Plugins become unnecessary.

9 comments:

  1. Grr. Stupid openid login lost my original comment, which was longer and more complete.

    The short comment is: Rubygems, and most of the Ruby practice around using them, is really only appropriate for applications that are only used by developers. For applications like mine (Puppet), which are packaged in multiple distros and used almost exclusively by non-developers, gems are an unnecessary, additional packaging system, and they are difficult to automatically manage (until recently, they couldn't tell whether they should install the Windows or Unix version of a gem).

    I personally can't stand gems, as I find myself always working around them rather than with them. For instance, I use RDoc::Usage, but gems breaks it entirely because they replace my executables.

    ReplyDelete
  2. Luke, sorry about OpenID, I guess blogger is still figuring that feature out.

    I wouldn't want to deploy an application that required non-devs to gem install either. It seems like the right thing to do is unpack your gem dependencies and package them with your application.

    Of course, that probably doesn't work if you have gems that differ for Unix and Windows.

    I'm not downplaying your pain at all. On the contrary, the point of the post was that hopefully we can all contribute and make the whole process easier.

    Thanks for the comment,

    Cheers, Jay

    ReplyDelete
  3. A patch was created by Marcel to allow gems dumped into vendor/gems to be unpacked into vendor/gems/home, which was an automatic extension of your gems cache (so dependencies worked etc) - http://dev.rubyonrails.org/ticket/8511

    There is also http://gemsonrails.rubyforge.org for unpacking a gem in vendor/gems and creating an init.rb. The gem is then loaded automatically like a plugin.

    "Death of plugins" has been an interest of mine for a while, but recently I haven't felt the pain as much as I used to.

    But I recommend ppl check out Marcel's patch and add any useful comments,

    ReplyDelete
  4. My point is that it doesn't usually make sense to include every gem my application needs in my package, and, in fact, it's directly against the packaging guidelines for nearly every distribution.

    Imagine getting a gem that itself included a bunch of other gems -- it would be inane.

    It's just inane to image a .deb or an rpm that included a bunch of other gems, and it's a problem that Rubyists think this is an acceptable practice.

    ReplyDelete
  5. In my new project, I use http://geminstaller.rubyforge.org/
    which uses a simple YAML config file to:

    * Automatically install the correct versions of all required gems wherever your app runs.
    * Automatically ensure installed gems and versions are consistent across multiple applications, machines, platforms, and environments
    * Automatically add correct versions of gems to the ruby load path when your app runs (‘require_gem’/’gem’)
    * Automatically reinstall missing dependency gems (built in to RubyGems > 1.0)
    * Automatically detect correct platform to install for multi-platform gems (built in to RubyGems > 1.0)
    * Print YAML for “rogue gems” which are not specified in the current config, to easily bootstrap your config file, or find gems that were manually installed without GemInstaller.
    * Allow for common configs to be reused across projects or environments by supporting multiple config files, including common config file snippets, and defaults with overrides.
    * Allow for dynamic selection of gems, versions, and platforms to be used based on environment vars or any other logic.
    * Avoid the “works on demo, breaks on production” syndrome

    Chad Wooley used bdd and rspec, his test coverage is *very^good and he has cruisecontrol running for testing his library with all versions of rubygems!

    I predict geminstaller will become a standard in rails projects such as maven is for java projects

    ReplyDelete
  6. Gems has gotten a little too opinionated, in my opinion. It complains if you don't provide RDoc, and if you don't provide a RubyForge project, and if you don't provide a homepage. (Remember the concept of the homepage? Gems still does.) In practical terms many many gems will have a rubyforge "home page" which consists of the gem's RDoc, so as long as you've got a RubyForge project it should be easy enough to skip the rest of it.

    Personally I haven't felt the gems vs plugins pain, and I've worked on a lot of different projects. I'm not closed-minded, but I am unconvinced. To a certain extent distinguishing between gems and plugins plays a kind of namespacing role. I think doing away with plugins would be overkill, but accomodating people who want a smoother, cleaner, more automatic way to handle gems in Rails is a very good idea.

    And the auto-unpack stuff would probably be easy to write. PDI ;-)

    ReplyDelete
  7. Anonymous5:09 AM

    The other problem with gems is that they are not scoped to the project. With a plugin I can simply install it in the project I want and it won't affect other projects. With a Gem based approach either all Rails apps on the machine would automatically pickup the Gem (dangerous) or I would have some second step to enable a specific gem (and specific version) within the target rails app.

    ReplyDelete
  8. randy wilson6:38 AM

    Great point Jay...

    I've recently been using merb for some stuff and the biggest pain is trying to use my Rails activerecord models "as is" ... why? Because they depend on a variety of plugins -- acts_as_state_machine, attachment_fu, acts_as_paranoid, etc.

    Merb has done away with the concept of plugins in favor of gems, and I now hope Rails does the same.

    Tools like piston, braid, etc., that help manage plugin versions just add complexity... One should be able to specify the project's dependencies and let the package manager do the rest.

    ReplyDelete
  9. Anonymous8:26 AM

    If you want versioned gems private to your application:

    env GEM_HOME=vendor/gems gem install foo bar baz

    ...
    gems = Dir[File.join(RAILS_ROOT, "vendor/gems/gems/*")]
    if gems.any?
    gems.each do |dir|
    lib = File.join(dir, 'lib')
    $LOAD_PATH.unshift(lib) if File.directory?(lib)
    end
    end

    This is basically the same as gemsonrails, except keeping the whole gem subdir structure under vendor/gems (so you have vendor/gems/bin/... and so on)

    ReplyDelete

Note: Only a member of this blog may post a comment.