Load plugins dynamically from source

This patch updates rebar_core to look for missing plugins (i.e. those
that aren't found on the code path at runtime) in a configurable
plugin directory, and dynamically compile and load them at runtime.

By default, the directory "plugins" is searched, although this can be
overriden by setting the plugin_dir in your rebar.config.
This commit is contained in:
Tim Watson 2011-05-03 00:15:19 +01:00 committed by Tuncer Ayaz
parent 7a1c88228b
commit 3b58935b86
8 changed files with 125 additions and 5 deletions

View file

@ -0,0 +1,2 @@
{plugins, [bad_plugin]}.
{plugin_dir, "bad_plugins"}.

View file

@ -0,0 +1,7 @@
-module(bad_plugin).
-compile(export_all).
%% this plugin contains numerous DELIBERATE syntax errors
fwibble(Config, _) >
file:delete("fwibble.test")

View file

@ -0,0 +1,5 @@
-module(fish).
-compile(export_all).
fish() -> fish.

View file

@ -0,0 +1 @@
{plugins, [test_plugin]}.

View file

@ -0,0 +1,8 @@
-module(test_plugin).
-compile(export_all).
fwibble(Config, _) ->
Pwd = rebar_utils:get_cwd(),
Ok = filelib:is_regular(filename:join(Pwd, "fwibble.test")),
rebar_log:log(info, "~p:~p in ~s :: ~p~n", [test_plugin, clean, Pwd, Ok]),
ok = file:delete("fwibble.test").

View file

@ -0,0 +1,40 @@
-module(tplugins_rt).
-compile(export_all).
-include_lib("eunit/include/eunit.hrl").
-define(COMPILE_ERROR,
"ERROR: Plugin bad_plugin contains compilation errors:").
files() ->
[
{copy, "../../rebar", "rebar"},
{copy, "rebar.config", "rebar.config"},
{copy, "bad.config", "bad.config"},
{copy, "fish.erl", "src/fish.erl"},
{copy, "test_plugin.erl", "plugins/test_plugin.erl"},
{copy, "bad_plugin.erl", "bad_plugins/bad_plugin.erl"},
{create, "fwibble.test", <<"fwibble">>},
{create, "ebin/fish.app", app(fish, [fish])}
].
run(Dir) ->
?assertMatch({ok, _}, retest_sh:run("./rebar fwibble -v", [])),
?assertEqual(false, filelib:is_regular("fwibble.test")),
Ref = retest:sh("./rebar -C bad.config -v clean", [{async, true}]),
{ok, _} = retest:sh_expect(Ref, "ERROR: Plugin .*bad_plugin.erl "
"contains compilation errors:.*",
[{newline, any}]),
ok.
%%
%% Generate the contents of a simple .app file
%%
app(Name, Modules) ->
App = {application, Name,
[{description, atom_to_list(Name)},
{vsn, "1"},
{modules, Modules},
{registered, []},
{applications, [kernel, stdlib]}]},
io_lib:format("~p.\n", [App]).

View file

@ -123,6 +123,19 @@
%% Subdirectories?
{sub_dirs, ["dir1", "dir2"]}.
%% == Plugins ==
%% Plugins you wish to include.
%% These can include any module on the code path, including deps.
%% Alternatively, plugins can be placed as source files in the plugin_dir, in
%% which case they will be compiled and loaded dynamically at runtime.
{plugins, [plugin1, plugin2]}.
%% Override the directory in which plugin sources can be found.
%% Defaults to ./plugins
{plugin_dir, "some_other_directory"}.
%% == Pre/Post Command Hooks ==
{pre_hooks, [{clean, "./prepare_package_files.sh"},

View file

@ -380,13 +380,57 @@ ulist([H | T], Acc) ->
plugin_modules(_Config, []) ->
{ok, []};
plugin_modules(_Config, Modules) ->
plugin_modules(Config, Modules) ->
FoundModules = [M || M <- Modules, code:which(M) =/= non_existing],
case (Modules =:= FoundModules) of
plugin_modules(Config, FoundModules, Modules -- FoundModules).
plugin_modules(_Config, FoundModules, []) ->
{ok, FoundModules};
plugin_modules(Config, FoundModules, MissingModules) ->
{Loaded, NotLoaded} = load_plugin_modules(Config, MissingModules),
AllViablePlugins = FoundModules ++ Loaded,
case length(NotLoaded) > 0 of
true ->
ok;
%% NB: we continue to ignore this situation, as did the original code
?WARN("Missing plugins: ~p\n", NotLoaded);
false ->
?WARN("Missing plugins: ~p\n", [Modules -- FoundModules]),
?DEBUG("Loaded plugins: ~p~n", [AllViablePlugins]),
ok
end,
{ok, FoundModules}.
{ok, AllViablePlugins}.
load_plugin_modules(Config, Modules) ->
PluginDir = case rebar_config:get_local(Config, plugin_dir, undefined) of
undefined ->
filename:join(rebar_utils:get_cwd(), "plugins");
Dir ->
Dir
end,
Sources = rebar_utils:find_files(PluginDir, ".*\.erl\$"),
Loaded = [load_plugin(Src) || Src <- Sources],
FilterMissing = is_missing_plugin(Loaded),
NotLoaded = [V || V <- Modules, FilterMissing(V)],
{Loaded, NotLoaded}.
is_missing_plugin(Loaded) ->
fun(Mod) -> not lists:member(Mod, Loaded) end.
load_plugin(Src) ->
case compile:file(Src, [binary, return_errors]) of
{ok, Mod, Bin} ->
load_plugin_module(Mod, Bin, Src);
{error, Errors, _Warnings} ->
?ABORT("Plugin ~s contains compilation errors: ~p~n",
[Src, Errors])
end.
load_plugin_module(Mod, Bin, Src) ->
case code:is_loaded(Mod) of
{file, Loaded} ->
?ABORT("Plugin ~p clashes with previously loaded module ~p~n",
[Mod, Loaded]);
false ->
?INFO("Loading plugin ~p from ~s~n", [Mod, Src]),
{module, Mod} = code:load_binary(Mod, Src, Bin),
Mod
end.