Here are my thoughts on adding plugins to M::B. Comments, anyone?
The big advantage of using subclassing to customize M::B is that any
method can be overridden, without needing any special code in each
method to call hooks.
But subclassing has problems:
* It's awkward to combine the behaviors of multiple subclasses.
* Any new methods or subroutines a subclass defines run the risk of
conflicts with new versions of M::B.
* There's no way for a user to customize M::B.
Goals for a plugin system:
* Each plugin should be a separate object. This will help prevent
conflicts with other plugins and M::B itself.
* A plugin should be able to customize any method in M::B.
Methods should not have to include special code to allow this.
* Multiple plugins should be able to customize the same method.
* Plugins should be able to define new actions. If multiple plugins
define the same action, they should normally all get run.
* It should be possible to have 2 M::B objects with different plugins
in one process. (I'm not sure if this is really useful. Abandoning
it might simplify the plugin system.)
Places to specify plugins to be loaded:
* Build.PL
(must distinguish between required plugins and optional ones)
* Module::Build subclass
(is this one necessary?)
* system-level config file
* user-level config file
* on the Build.PL command line
I don't see any need for optional plugins at the system or user
level. If you didn't install a plugin, then don't request it.
What plugins can do:
I'm going to borrow some ideas from Damian Conway's Hook::LexWrap, and
modify them as I see fit.
Plugins can install pre and post hooks on any method. Hooks get
called like this:
$plugin->pre_METHOD($builder, $parameters, $context, $return)
$plugin->post_METHOD($builder, $parameters, $context, $return)
METHOD is the name of the method being customized. $plugin is the
plugin object. $builder is the M::B object. $parameters is an
arrayref of the parameters passed to the method (not including
$builder). $context is the value of wantarray. $return is the return
value from the method.
In a pre hook:
* $return is always undef.
* You can modify the $parameters array to change what the method (and
any subsequent hooks) see.
* To completely override a method, set $_[-1] to an appropriate value
and return 'done'. No more pre hooks will be called. The normal
method will not be called. All post hooks will still be called.
(I don't think setting $_[-1] is sufficient, because it makes it
impossible to override a method and return undef.)
You must set $_[-1] to an arrayref if $context is 1. (I can't
decide if the wrapping code should auto-upgrade any non-arrayref
return value in this case, to simplify returning a scalar from a
method that might be called in list context or scalar context.)
* Otherwise, a pre-hook must return 'continue'. Returning anything
else is an error. (This is to prevent hooks that just happen to
return the value of their last expression from causing mysterious
problems.)
In a post hook:
* If $context is undef, $return is always undef.
If $context is false, $return is a scalar.
If $context is true, $return is an arrayref.
* Modifying $_[-1] changes the return value.
(Unless $context is undef, of course.)
* The return value of a post hook is ignored.
(Or does it make sense to have done/continue here also?)
* You can modify the $parameters array to change what any subsequent
hooks see, but this is HIGHLY discouraged.
To allow adding new actions:
When a plugin hooks ACTION_whatever, we check for
Module::Build::Base::ACTION_whatever. If it doesn't exist, we define
it as sub ACTION_whatever {}.
I don't think it should be an error to hook a method that doesn't
exist. (It might have been introduced in a newer version of M::B.)
It only becomes a runtime error if the non-existent method is ever
called (and it hasn't been completely overridden by a pre-hook).
The order in which plugins get called:
When you request a plugin, you specify a priority for it.
* Build.PL can specify priorities from -100 .. 100.
* The system config can use priorities from -1000 .. 1000.
* The user config can use priorities from -10000 .. 10000.
* Pre-hooks get called in decreasing order of priority.
* Post-hooks get called in increasing order of priority.
* Plugins do not get to pick their own priority.
The person loading the plugin does that.
* The default priority is 0.
(Assuming the method of specifying plugins allows for a default.)
* Plugins with equal priority are sorted by name (just to ensure
deterministic results).
* Do we need to allow per-method priorities, or is per-module sufficient?
Some implementation notes:
* Plugin classes are named Module::Build::Plugin::*. (The
Module::Build::Plugin:: prefix is assumed when specifying plugins to
load).
* Each plugin class has a constructor named new.
* It also has a method named get_hooks, which returns a list of hook
methods implemented by the plugin. (I thought of groveling around
in the plugin's symbol table, but this makes it easier to subclass a
plugin.)
For example, if you want to put a pre-hook on ACTION_distdir, and a
post-hook on ACTION_test, you'd say:
sub get_hooks { return qw(pre_ACTION_distdir post_ACTION_test) }
sub pre_ACTION_distdir { ... }
sub post_ACTION_test { ... }
Questions for further thought:
* How can configuration be passed to plugins?
* What format should the user & system config files be?
* Is this generic enough to be implemented in a separate module and
re-used in other classes?
* There's no clear way for pre and post hooks to communicate. You can
store data in the plugin object, but that may not work well for
recursive methods. Of course, most of the methods that plugins
would be likely to hook aren't recursive anyway.
--
Chris Madsen [EMAIL PROTECTED]
-------------------- http://www.cjmweb.net --------------------