Thursday, January 25, 2007


Part of the Gem-based Development Process series





We started with a small application. A few controllers, a few models. Soon the company grew, the development team grew, and with that the application grew. Luckily, we namespaced applications early on, so it was mostly clear where one application lived from another, but the rails project became too big. As one coworker (Eddie) put it, "My TextMate file find is getting too slow!". It was time to split applications. But for a portal with a single look and feel, how do you share things across applications? Here is how we do it (this week).

We wanted to use gems, as described in Part I, but without gems being true 1st class citizens in rails, there was no way to make it happen. So what was important to share and more importantly version?

I. Code
Thats obvious, rubygems already does (most of) this for us.

II. Views
With a common header, or commonly shared assets, we'd like to leverage those.

III. Rake Tasks

We have lots of rake tasks to do all sorts of things, we need to share those too.

IV. Assets

We'll cover this later. This gets complicated, and needs deployment input.

V. Plugems as Plugins

Using gems, as plugins, in your plugin dir, and the A/B/C problem.

I. Code
We didnt do (much of) anything to get this. Thanks guys who wrote RubyGems!

II. Views
We needed to load views from a few places, and it took some diving into rails internals to figure out how. There were a couple things we needed to figure out.

  1. Where to look for views.
  2. The order in which we look.
  3. Caching. Globbing for files is slow... especially across many directories. Cant do this a lot.

1. So we first overwrote the ActionView::Base's implementation of finding files. That was straightforward. We also fixed ActionController::Layout to allow layouts to live in gems as well. Once we provided ActionView and ActionController with a bigger list of partials, it could then load the partial from anywhere. Now you'll see the path to a file in a gem in certain stack traces, just to prove that where to find it.

2. So why is the order important and powerful? Lets say you decide to use our base_ui gem and its default layout.

<%= render :partial => "header"%>
<%= @content_for_layout -%>
<%= render :partial => "footer"%>

In our gem,

You've been told to build the FooBar application, whos pages dont have the same header. So in your application, add a
/app/views/layouts/_header.rhtml, and put whatever you want there. It just works.

So the dependency order is: app ==> plugins ==> gems. There is almost always true. I'll explain the exception later when "developing" things that are gems.

Note: The compiled template method names get kinda ugly, but who cares.. You only see those in some stack traces.

3. Caching was quickly important. Disclaimer: We only look for views in gems and plugins at startup. After that, bounce your app to pick up new files. Thanks to Zed, mongrel_rails restart is wicked fast.

III. Gem Rake Tasks
So the easiest way to include rake tasks from gems was to add a bootstrap.rake into /lib/tasks/bookstrap. We grab the manifest file for the application, and for each of the gems defined, 'load' the rake files. This was a quick fix and could probably be more elegant.

IV. Assets
This conversation gets complicated quick. We'll cover this later.

V. Plugems ( Plugin + Gem = Plugem!)
So now you've taken all your favorite plugins and made them gems. In the next article, I'll cover developing gems as plugins, and the A --> B --> C dependency loading problem, where A and C are gems, and B is a plugin.


Anonymous said...

Very very interesting. Please keep on!
Can you please show a small example of linking a gem as plugin to application?

Anonymous said...

I'm interested in seeing some of the code shared as well. I'm currently using rails-engines for this, but am worried that it's causing some memory leaks.

Any chance of talking about why you didn't use engines? And also, how much memory rss/vsize does your 'big' app use? Mine's up to 210M/140M RSS, which just doesn't seem right, but I'm having a bad time trying to track down the problem.

Anonymous said...

These comments have been invaluable to me as is this whole site. I thank you for your comment.

Anonymous said...

good post