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:
discover/source/unsource/sources
The plugin tool uses an ad-hoc method of finding plugins.
Rather than requiring you to specify the URL of a plugin
repository, script/plugin
tries to find it for you. One way it does this is by scraping
the "Plugins" page of the Rails wiki [24] for source URLs. This can be triggered with the
discover
command.
The source
and unsource
commands add and remove
source URLs, respectively. The sources
command lists all current
source URLs.
install/update/remove
These commands install, update, and uninstall plugins.
They can take an HTTP URL, a Subversion URL (svn://
or svn+ssh://
), or a bare plugin name, in
which case the list of sources is scanned.
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 search
command
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:
The remote server must be contacted on each update to determine whether any-thing has changed. This can incur quite a performance penalty if the project has many externals. In addition, it adds an unneeded dependency; problems can ensue if the remote server is down.
The project is generally at the mercy of code changes that happen at the remote branch; there is no easy way to cherry-pick or block changes that happen remotely. The only flexibility Subversion affords is the ability to lock to a certain remote revision.
Similarly, there is no way to maintain local modifications to a remote branch. Any needed modifications can only be kept in the working copy, where they are unversioned.
No history is kept of how external versions relate to the local repository. If you want to update your working copy to last month's version, nobody knows what version the external code was at.
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
Piston effectively creates one layer between a remote repository and the working copy. Decentralized version control systems take this model to its logical conclusion: every working copy is a repository, equally able to share changes with other repositories. This can be a much more flexible model than normal centralized version control systems. We examine decentralized version control systems in more detail in Chapter 10.
Plugins and other vendor code can be managed very well with a decentralized version control system. These systems afford much more flexibility, especially in complicated situations with multiple developers and vendors.
A tool is available, hgsvn
,[26] which will migrate changes from a SVN repository to a
Mercurial repository. This can be used to set up a system similar to
Piston, but with much more flexibility. One repository (the
"upstream" or "incoming") can mirror the remote repository, and
other projects can cherry-pick desired patches from the upstream and
ignore undesired ones. Local modifications suitable for the upstream
can be exported to patches and sent to the project
maintainer.