From 3b58935b8621a876afad2f649bbd00a12f7ab03f Mon Sep 17 00:00:00 2001 From: Tim Watson Date: Tue, 3 May 2011 00:15:19 +0100 Subject: [PATCH] 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. --- inttest/tplugins/bad.config | 2 ++ inttest/tplugins/bad_plugin.erl | 7 +++++ inttest/tplugins/fish.erl | 5 +++ inttest/tplugins/rebar.config | 1 + inttest/tplugins/test_plugin.erl | 8 +++++ inttest/tplugins/tplugins_rt.erl | 40 +++++++++++++++++++++++ rebar.config.sample | 13 ++++++++ src/rebar_core.erl | 54 +++++++++++++++++++++++++++++--- 8 files changed, 125 insertions(+), 5 deletions(-) create mode 100644 inttest/tplugins/bad.config create mode 100644 inttest/tplugins/bad_plugin.erl create mode 100644 inttest/tplugins/fish.erl create mode 100644 inttest/tplugins/rebar.config create mode 100644 inttest/tplugins/test_plugin.erl create mode 100644 inttest/tplugins/tplugins_rt.erl diff --git a/inttest/tplugins/bad.config b/inttest/tplugins/bad.config new file mode 100644 index 0000000..23069b8 --- /dev/null +++ b/inttest/tplugins/bad.config @@ -0,0 +1,2 @@ +{plugins, [bad_plugin]}. +{plugin_dir, "bad_plugins"}. diff --git a/inttest/tplugins/bad_plugin.erl b/inttest/tplugins/bad_plugin.erl new file mode 100644 index 0000000..77ec01b --- /dev/null +++ b/inttest/tplugins/bad_plugin.erl @@ -0,0 +1,7 @@ +-module(bad_plugin). +-compile(export_all). + +%% this plugin contains numerous DELIBERATE syntax errors + +fwibble(Config, _) > + file:delete("fwibble.test") diff --git a/inttest/tplugins/fish.erl b/inttest/tplugins/fish.erl new file mode 100644 index 0000000..739cb94 --- /dev/null +++ b/inttest/tplugins/fish.erl @@ -0,0 +1,5 @@ +-module(fish). + +-compile(export_all). + +fish() -> fish. diff --git a/inttest/tplugins/rebar.config b/inttest/tplugins/rebar.config new file mode 100644 index 0000000..0b9c887 --- /dev/null +++ b/inttest/tplugins/rebar.config @@ -0,0 +1 @@ +{plugins, [test_plugin]}. diff --git a/inttest/tplugins/test_plugin.erl b/inttest/tplugins/test_plugin.erl new file mode 100644 index 0000000..461247c --- /dev/null +++ b/inttest/tplugins/test_plugin.erl @@ -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"). diff --git a/inttest/tplugins/tplugins_rt.erl b/inttest/tplugins/tplugins_rt.erl new file mode 100644 index 0000000..d2ef382 --- /dev/null +++ b/inttest/tplugins/tplugins_rt.erl @@ -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]). diff --git a/rebar.config.sample b/rebar.config.sample index 2053b8b..513daf4 100644 --- a/rebar.config.sample +++ b/rebar.config.sample @@ -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"}, diff --git a/src/rebar_core.erl b/src/rebar_core.erl index 75569b4..f30eb35 100644 --- a/src/rebar_core.erl +++ b/src/rebar_core.erl @@ -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.