Chapter 3. Rails Plugins

Civilization advances by extending the number of important operations which we can perform without thinking of them.

—Alfred North Whitehead

Ruby on Rails is very powerful, but it cannot do everything. There are many features that are too experimental, out of scope of the Rails core, or even blatantly contrary to the way Rails was designed (it is opinionated software, after all). The core team cannot and would not include everything that anybody wants in Rails.

Luckily, Rails comes with a very flexible extension system. Rails plugins allow developers to extend or override nearly any part of the Rails framework, and share these modifications with others in an encapsulated and reusable manner.

By default, plugins are loaded from directories under vendor/plugins in the Rails application root. Should you need to change or add to these paths, the plugin_paths configuration item contains the plugin load paths:

	config.plugin_paths += [File.join(RAILS_ROOT, 'vendor', 'other_plugins')]

By default, plugins are loaded in alphabetical order; attachment_fu is loaded before http_authentication. If the plugins have dependencies on each other, a manual loading order can be specified with the plugins configuration element:

	config.plugins = %w(prerequisite_plugin actual_plugin)

Any plugins not specified in config.plugins will not be loaded. However, if the last plugin specified is the symbol :all, Rails will load all remaining plugins at that point. Rails accepts either symbols or strings as plugin names here.

	config.plugins = [ :prerequisite_plugin, :actual_plugin, :all ]

The plugin locator searches for plugins under the configured paths, recursively. Because a recursive search is performed, you can organize plugins into directories; for example, vendor/plugins/active_record_acts and vendor/plugins/view_extensions.

The actual plugin locating and loading system is extensible, and you can write your own strategies. The locator (which by default is Rails::Plugin::FileSystemLocator) searches for plugins; the loader (by default Rails::Plugin::Loader) determines whether a directory contains a plugin and does the work of loading it.

To write your own locators and loaders, examine railties/lib/rails/plugin/locator.rb and railties/lib/rails/plugin/loader.rb. The locators (more than one locator can be used) and loader can be changed with configuration directives:

	config.plugin_locators += [MyPluginLocator]
	config.plugin_loader = MyPluginLoader

Plugins are most often installed with the built-in Rails plugin tool, script/plugin. This plugin tool has several commands:

script/plugin install takes an option, -x, that directs it to manage plugins as Subversion externals. This has the advantage that the directory is still linked to the external repository. However, it is a bit inflexible—you cannot cherry-pick changes from the upstream repository. We will examine some better options later.

RaPT (http://rapt.rubyforge.org/) is a replacement for the standard Rails plugin installer, script/plugin. It can be installed with gem install rapt.

The first advantage that RaPT has is that it can search for plugins from the command line. (The second advantage is that it is extremely fast, because it caches everything.)

The rapt searchcommand looks for plugins matching a specified keyword. To search for plugins that add calendar features to Rails, change to the root directory of a Rails application and execute:

	$ rapt search calendar
	Calendar Helper
	  Info: http://agilewebdevelopment.com/plugins/show/98
	  Install: http://topfunky.net/svn/plugins/calendar_helper
	Calendariffic 0.1.0
	  Info: http://agilewebdevelopment.com/plugins/show/743
	  Install: http://opensvn.csie.org/calendariffic/calendariffic/
	Google Calendar Generator
	  Info: http://agilewebdevelopment.com/plugins/show/277
	  Install: svn://rubyforge.org//var/svn/googlecalendar/plugins/googlecalendar
	dhtml_calendar
	  Info: http://agilewebdevelopment.com/plugins/show/333
	  Install: svn://rubyforge.org//var/svn/dhtmlcalendar/dhtml_calendar
	Bundled Resource
	  Info: http://agilewebdevelopment.com/plugins/show/166
	  Install: svn://syncid.textdriven.com/svn/opensource/bundled_resource/trunk
	DatebocksEngine
	  Info: http://agilewebdevelopment.com/plugins/show/356
	  Install: http://svn.toolbocks.com/plugins/datebocks_engine/       
	datepicker_engine       
	  Info: http://agilewebdevelopment.com/plugins/show/409
	  Install: http://svn.mamatux.dk/rails-engines/datepicker_engine

One of these could then be installed with, for example, rapt install datepicker_engine.

In Rails, plugins are perhaps the most common use of code supplied by an external vendor (other than the Rails framework itself). This requires some special care where version control is concerned. Managing Rails plugins as Subversion externals has several disadvantages:

To solve these problems, François Beausoleil created Piston, [25] a program to manage vendor branches in Subversion. Piston imports the remote branch into the local repository, only synchronizing when requested. As a full copy of the code exists inside the project's repository, it can be modified as needed. Any changes made to the local copy will be merged when the project is updated from the remote server.

Piston is available as a gem; install it with sudo gem install --include-dependencies piston.

To install a plugin using Piston, you need to manually find the Subversion URL of the repository. Then, simply import it with Piston, specifying the repository URL and the destination path in your working copy:

	$ piston import http://svn.rubyonrails.org/rails/plugins/deadlock_retry \
	    vendor/plugins/deadlock_retry
	Exported r7144 from 'http://svn.rubyonrails.org/rails/plugins/deadlock_retry/'
	to 'vendor/plugins/deadlock_retry'

	$ svn ci

The svn ci is necessary because Piston adds the code to your working copy. To Subversion, it is as if you wrote the code yourself—it is versioned alongside the rest of your application. This makes it very simple to patch the vendor branch for local use; simply make modifications to the working copy and check them in.

When the time comes to update the vendor branch, piston update vendor/plugins/ deadlock_retry will fetch all changes from the remote repository and merge them in. Any local modifications will be preserved in the merge. piston update can be called without an argument; in that case, it will recursively update any Piston-controlled directories under the current one.

Piston-controlled directories can be locked to their current version with piston lock and unlocked with piston unlock. And for current svn:externals users, existing directories managed with svn:externals can be converted to Piston all at once with piston convert.

Piston is also good for managing edge Rails, along with any patches you may apply. To import Rails from the edge, with all of the features of Piston:

	$ piston import http://svn.rubyonrails.org/rails/trunk vendor/rails