Skip to content

Commit d574535

Browse files
authored
Merge pull request #2697 from ferd/support-project-local-plugins
Add support for project-local plugins
2 parents 55e3c41 + 2af0af1 commit d574535

10 files changed

+328
-27
lines changed

.github/workflows/shelltests.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929
- name: Install and run shelltestrunner
3030
run: |
3131
sudo apt-get update
32-
sudo apt-get install -y shelltestrunner build-essential
32+
sudo apt-get install -y shelltestrunner build-essential cmake liblz4-dev
3333
cd rebar3_tests
3434
mix local.hex --force
3535
./run_tests.sh

src/rebar.hrl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
-define(DEFAULT_BASE_DIR, "_build").
1616
-define(DEFAULT_ROOT_DIR, ".").
1717
-define(DEFAULT_PROJECT_APP_DIRS, ["apps/*", "lib/*", "."]).
18+
-define(DEFAULT_PROJECT_PLUGIN_DIRS, ["plugins/*"]).
1819
-define(DEFAULT_CHECKOUTS_DIR, "_checkouts").
1920
-define(DEFAULT_CHECKOUTS_OUT_DIR, "checkouts").
2021
-define(DEFAULT_DEPS_DIR, "lib").

src/rebar_app_discover.erl

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,8 +64,8 @@ do(State, LibDirs) ->
6464
OutDir = filename:join(DepsDir, Name),
6565
AppInfo2 = rebar_app_info:out_dir(AppInfo1, OutDir),
6666
ProjectDeps1 = lists:delete(Name, ProjectDeps),
67-
rebar_state:project_apps(StateAcc1
68-
,rebar_app_info:deps(AppInfo2, ProjectDeps1));
67+
rebar_state:project_apps(StateAcc1,
68+
rebar_app_info:deps(AppInfo2, ProjectDeps1));
6969
false ->
7070
?INFO("Ignoring ~ts", [Name]),
7171
StateAcc

src/rebar_app_info.erl

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -638,7 +638,8 @@ valid(#app_info_t{valid=Valid}) ->
638638

639639
%% @doc sets whether the app is valid (built) or not. If left unset,
640640
%% rebar3 will do the detection of the status itself.
641-
-spec valid(t(), boolean()) -> t().
641+
%% Explicitly setting the value to `undefined' can force a re-evaluation.
642+
-spec valid(t(), boolean() | undefined) -> t().
642643
valid(AppInfo=#app_info_t{}, Valid) ->
643644
AppInfo#app_info_t{valid=Valid}.
644645

src/rebar_dir.erl

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
checkouts_out_dir/2,
1414
plugins_dir/1,
1515
lib_dirs/1,
16+
project_plugin_dirs/1,
1617
home_dir/0,
1718
global_config_dir/1,
1819
global_config/1,
@@ -127,6 +128,12 @@ plugins_dir(State) ->
127128
lib_dirs(State) ->
128129
rebar_state:get(State, project_app_dirs, ?DEFAULT_PROJECT_APP_DIRS).
129130

131+
%% @doc returns the list of relative path where the project plugins can
132+
%% be located.
133+
-spec project_plugin_dirs(rebar_state:t()) -> [file:filename_all()].
134+
project_plugin_dirs(State) ->
135+
rebar_state:get(State, project_plugin_dirs, ?DEFAULT_PROJECT_PLUGIN_DIRS).
136+
130137
%% @doc returns the user's home directory.
131138
-spec home_dir() -> file:filename_all().
132139
home_dir() ->

src/rebar_plugins.erl

Lines changed: 113 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@
88
,project_apps_install/1
99
,install/2
1010
,handle_plugins/3
11-
,handle_plugins/4]).
11+
,handle_plugins/4
12+
,discover_plugins/1]).
1213

1314
-include("rebar.hrl").
1415

@@ -94,10 +95,11 @@ handle_plugins(Profile, Plugins, State, Upgrade) ->
9495
Locks = rebar_state:lock(State),
9596
DepsDir = rebar_state:get(State, deps_dir, ?DEFAULT_DEPS_DIR),
9697
State1 = rebar_state:set(State, deps_dir, ?DEFAULT_PLUGINS_DIR),
98+
SrcPlugins = discover_plugins(Plugins, State),
9799
%% Install each plugin individually so if one fails to install it doesn't effect the others
98100
{_PluginProviders, State2} =
99101
lists:foldl(fun(Plugin, {PluginAcc, StateAcc}) ->
100-
{NewPlugins, NewState} = handle_plugin(Profile, Plugin, StateAcc, Upgrade),
102+
{NewPlugins, NewState} = handle_plugin(Profile, Plugin, StateAcc, SrcPlugins, Upgrade),
101103
NewState1 = rebar_state:create_logic_providers(NewPlugins, NewState),
102104
{PluginAcc++NewPlugins, NewState1}
103105
end, {[], State1}, Plugins),
@@ -106,24 +108,34 @@ handle_plugins(Profile, Plugins, State, Upgrade) ->
106108
State3 = rebar_state:set(State2, deps_dir, DepsDir),
107109
rebar_state:lock(State3, Locks).
108110

109-
handle_plugin(Profile, Plugin, State, Upgrade) ->
111+
handle_plugin(Profile, Plugin, State, SrcPlugins, Upgrade) ->
110112
try
111-
{Apps, State2} = rebar_prv_install_deps:handle_deps_as_profile(Profile, State, [Plugin], Upgrade),
112-
{no_cycle, Sorted} = rebar_prv_install_deps:find_cycles(Apps),
113+
%% Inject top-level src plugins as project apps, so that they get skipped
114+
%% by the installation as already seen
115+
ProjectApps = rebar_state:project_apps(State),
116+
State0 = rebar_state:project_apps(State, SrcPlugins),
117+
%% We however have to pick the deps of top-level apps and promote them
118+
%% directly to make sure they are installed if they were not also at the top level
119+
TopDeps = top_level_deps(State, SrcPlugins),
120+
%% Install the plugins
121+
{Apps, State1} = rebar_prv_install_deps:handle_deps_as_profile(Profile, State0, [Plugin|TopDeps], Upgrade),
122+
{no_cycle, Sorted} = rebar_prv_install_deps:find_cycles(SrcPlugins++Apps),
113123
ToBuild = rebar_prv_install_deps:cull_compile(Sorted, []),
124+
%% Return things to normal
125+
State2 = rebar_state:project_apps(State1, ProjectApps),
114126

115127
%% Add already built plugin deps to the code path
116128
ToBuildPaths = [rebar_app_info:ebin_dir(A) || A <- ToBuild],
117-
PreBuiltPaths = [Ebin || A <- Apps,
129+
PreBuiltPaths = [Ebin || A <- Sorted,
118130
Ebin <- [rebar_app_info:ebin_dir(A)],
119131
not lists:member(Ebin, ToBuildPaths)],
120132
code:add_pathsa(PreBuiltPaths),
121133

122134
%% Build plugin and its deps
123-
build_plugins(ToBuild, Apps, State2),
135+
build_plugins(ToBuild, Sorted, State2),
124136

125137
%% Add newly built deps and plugin to code path
126-
State3 = rebar_state:update_all_plugin_deps(State2, Apps),
138+
State3 = rebar_state:update_all_plugin_deps(State2, Sorted),
127139
NewCodePaths = [rebar_app_info:ebin_dir(A) || A <- ToBuild],
128140

129141
%% Store plugin code paths so we can remove them when compiling project apps
@@ -172,3 +184,96 @@ validate_plugin(Plugin) ->
172184
end
173185
end.
174186

187+
discover_plugins([], _) ->
188+
%% don't search if nothing is declared
189+
[];
190+
discover_plugins(_, State) ->
191+
discover_plugins(State).
192+
193+
discover_plugins(State) ->
194+
%% only support this mode in an umbrella project to avoid cases where
195+
%% this is used in a project intended to be an installed dependency and accidentally
196+
%% relies on vendoring when not intended. Also skip for global plugins, this would
197+
%% make no sense.
198+
case lists:member(global, rebar_state:current_profiles(State)) orelse not is_umbrella(State) of
199+
true ->
200+
[];
201+
false ->
202+
%% Inject source paths for plugins to allow vendoring and umbrella
203+
%% top-level declarations
204+
BaseDir = rebar_state:dir(State),
205+
LibDirs = rebar_dir:project_plugin_dirs(State),
206+
Dirs = [filename:join(BaseDir, LibDir) || LibDir <- LibDirs],
207+
RebarOpts = rebar_state:opts(State),
208+
SrcDirs = rebar_dir:src_dirs(RebarOpts, ["src"]),
209+
Found = rebar_app_discover:find_apps(Dirs, SrcDirs, all, State),
210+
?DEBUG("Found local plugins: ~p~n"
211+
"\tusing config: {project_plugin_dirs, ~p}",
212+
[[rebar_utils:to_atom(rebar_app_info:name(F)) || F <- Found],
213+
LibDirs]),
214+
PluginsDir = rebar_dir:plugins_dir(State),
215+
SetUp = lists:map(fun(App) ->
216+
Name = rebar_app_info:name(App),
217+
OutDir = filename:join(PluginsDir, Name),
218+
prepare_plugin(rebar_app_info:out_dir(App, OutDir))
219+
end, Found),
220+
rebar_utils:sort_deps(SetUp)
221+
end.
222+
223+
is_umbrella(State) ->
224+
%% We can't know if this is an umbrella project before running app discovery,
225+
%% but plugins are installed before app discovery. So we do a heuristic.
226+
%% The lib dirs we search contain things such as apps/, lib/, etc.
227+
%% which contain sub-applications. Then there's a final search for the
228+
%% local directory ("."), which finds the top-level app in a non-umbrella
229+
%% project.
230+
%%
231+
%% So what we do here is look for the library directories without the ".",
232+
%% and if none of these paths exist but one of the src_dirs exist, then
233+
%% we know this is not an umbrella application.
234+
Root = rebar_dir:root_dir(State),
235+
LibPaths = lists:usort(rebar_dir:lib_dirs(State)) -- ["."],
236+
SrcPaths = rebar_dir:src_dirs(rebar_state:opts(State), ["src"]),
237+
lists:any(fun(Dir) -> [] == filelib:wildcard(filename:join(Root, Dir)) end, LibPaths)
238+
andalso
239+
lists:all(fun(Dir) -> not filelib:is_dir(filename:join(Root, Dir)) end, SrcPaths).
240+
241+
prepare_plugin(AppInfo) ->
242+
%% We need to handle plugins as dependencies to avoid re-building them
243+
%% continuously. So here we copy the app directories to the dep location
244+
%% and then change the AppInfo record to be redirected to the dep location.
245+
AppDir = rebar_app_info:dir(AppInfo),
246+
OutDir = rebar_app_info:out_dir(AppInfo),
247+
rebar_prv_compile:copy_app_dirs(AppInfo, AppDir, OutDir),
248+
Relocated = rebar_app_info:dir(AppInfo, OutDir),
249+
case needs_rebuild(AppInfo) of
250+
true -> rebar_app_info:valid(Relocated, false); % force recompilation
251+
false -> rebar_app_info:valid(Relocated, undefined) % force revalidation
252+
end.
253+
254+
top_level_deps(State, Apps) ->
255+
CurrentProfiles = rebar_state:current_profiles(State),
256+
Keys = lists:append([[{plugins, P}, {deps, P}] || P <- CurrentProfiles]),
257+
RawDeps = lists:foldl(fun(App, Acc) ->
258+
%% Only support the profiles we would with regular plugins?
259+
lists:append([rebar_app_info:get(App, Key, []) || Key <- Keys]) ++ Acc
260+
end, [], Apps),
261+
rebar_utils:tup_dedup(RawDeps).
262+
263+
needs_rebuild(AppInfo) ->
264+
%% if source files are newer than built files then the code was edited
265+
%% and can't be considered valid -- force a rebuild.
266+
%%
267+
%% we do this by reusing the compiler code for Erlang as a heuristic for
268+
%% files to check. The actual compiler provider will do an in-depth
269+
%% validation of each module that may or may not need recompiling.
270+
#{src_dirs := SrcD, include_dirs := InclD,
271+
out_mappings := List} = rebar_compiler_erl:context(AppInfo),
272+
SrcDirs = SrcD++InclD,
273+
OutDirs = [Dir || {_Ext, Dir} <- List],
274+
newest_stamp(OutDirs) < newest_stamp(SrcDirs).
275+
276+
newest_stamp(DirList) ->
277+
lists:max([0] ++
278+
[filelib:last_modified(F)
279+
|| F <- rebar_utils:find_files_in_dirs(DirList, ".+", true)]).

src/rebar_prv_compile.erl

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
format_error/1]).
88

99
-export([compile/2, compile/3, compile/4]).
10+
-export([copy_app_dirs/3]).
1011

1112
-include_lib("providers/include/providers.hrl").
1213
-include("rebar.hrl").

src/rebar_prv_plugins_upgrade.erl

Lines changed: 23 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -69,27 +69,37 @@ upgrade(Plugin, State) ->
6969
Dep ->
7070
Dep
7171
end,
72-
72+
LocalPlugins = [rebar_utils:to_atom(rebar_app_info:name(App))
73+
|| App <- rebar_plugins:discover_plugins(State)],
7374
case Dep of
7475
not_found ->
7576
?PRV_ERROR({not_found, Plugin});
7677
{ok, P, Profile} ->
77-
State1 = rebar_state:set(State, deps_dir, ?DEFAULT_PLUGINS_DIR),
78-
maybe_update_pkg(P, State1),
79-
{Apps, State2} = rebar_prv_install_deps:handle_deps_as_profile(Profile, State1, [P], true),
78+
case lists:member(P, LocalPlugins) of
79+
true ->
80+
?INFO("Plugin ~p is defined locally and does not need upgrading", [P]),
81+
{ok, State};
82+
false ->
83+
do_upgrade(State, P, Profile)
84+
end
85+
end.
8086

81-
{no_cycle, Sorted} = rebar_prv_install_deps:find_cycles(Apps),
82-
ToBuild = rebar_prv_install_deps:cull_compile(Sorted, []),
87+
do_upgrade(State, P, Profile) ->
88+
State1 = rebar_state:set(State, deps_dir, ?DEFAULT_PLUGINS_DIR),
89+
maybe_update_pkg(P, State1),
90+
{Apps, State2} = rebar_prv_install_deps:handle_deps_as_profile(Profile, State1, [P], true),
8391

84-
%% Add already built plugin deps to the code path
85-
CodePaths = [rebar_app_info:ebin_dir(A) || A <- Apps -- ToBuild],
86-
code:add_pathsa(CodePaths),
92+
{no_cycle, Sorted} = rebar_prv_install_deps:find_cycles(Apps),
93+
ToBuild = rebar_prv_install_deps:cull_compile(Sorted, []),
8794

88-
%% Build plugin and its deps
89-
_ = build_plugin(ToBuild, State2),
95+
%% Add already built plugin deps to the code path
96+
CodePaths = [rebar_app_info:ebin_dir(A) || A <- Apps -- ToBuild],
97+
code:add_pathsa(CodePaths),
9098

91-
{ok, State}
92-
end.
99+
%% Build plugin and its deps
100+
_ = build_plugin(ToBuild, State2),
101+
102+
{ok, State}.
93103

94104
find_plugin(Plugin, Profiles, State) ->
95105
ec_lists:search(fun(Profile) ->

0 commit comments

Comments
 (0)