Permalink
Browse files

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.
  • Loading branch information...
1 parent 7a1c882 commit 3b58935b8621a876afad2f649bbd00a12f7ab03f @hyperthunk hyperthunk committed with tuncer May 2, 2011
@@ -0,0 +1,2 @@
+{plugins, [bad_plugin]}.
+{plugin_dir, "bad_plugins"}.
@@ -0,0 +1,7 @@
+-module(bad_plugin).
+-compile(export_all).
+
+%% this plugin contains numerous DELIBERATE syntax errors
+
+fwibble(Config, _) >
+ file:delete("fwibble.test")
@@ -0,0 +1,5 @@
+-module(fish).
+
+-compile(export_all).
+
+fish() -> fish.
@@ -0,0 +1 @@
+{plugins, [test_plugin]}.
@@ -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").
@@ -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
@@ -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
@@ -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.

0 comments on commit 3b58935

Please sign in to comment.