Current section
Files
Jump to
Current section
Files
src/erlup_appup.erl
%% @copyright 2016 Hinagiku Soranoba All Rights Reserved.
%%
%% @doc Automatically generate the .appup files from the beam file.
%%
-module(erlup_appup).
-include("erlup.hrl").
%%----------------------------------------------------------------------------------------------------------------------
%% Exported API
%%----------------------------------------------------------------------------------------------------------------------
-export([
do/4
]).
%%----------------------------------------------------------------------------------------------------------------------
%% 'provider' Callback API
%%----------------------------------------------------------------------------------------------------------------------
-export([init/1, do/1, format_error/1]).
%%----------------------------------------------------------------------------------------------------------------------
%% Macros and Types
%%----------------------------------------------------------------------------------------------------------------------
-type instruction() :: tuple().
%%----------------------------------------------------------------------------------------------------------------------
%% Exported Functions
%%----------------------------------------------------------------------------------------------------------------------
%% @doc Automatically generate the .appup files from the beam file.
-spec do([file:filename()], string() | [string()], string(), erlup_state:t()) -> ok.
do(Dirs, [X | _] = PreviousVsns, CurrentVsn, State0) when is_list(X) ->
lists:foreach(fun(P) -> do(Dirs, P, CurrentVsn, State0) end, PreviousVsns);
do(Dirs, PreviousVsn, CurrentVsn, State0) ->
?INFO("previous = ~p, current = ~p", [PreviousVsn, CurrentVsn]),
State1 = lists:foldl(fun({Before, After}, Acc) -> erlup_state:set_sedargs(Before, After, Acc) end,
State0, [{'$from', PreviousVsn}, {'$to', CurrentVsn}]),
State2 = case erlup_state:get(rels, State1, undefined) of
undefined -> erlup_state:put(rels, Rels = erlup_utils:find_rels(Dirs), State1);
Rels -> State1
end,
Fun = fun(Vsn) ->
case lists:keyfind(Vsn, 2, Rels) of
false -> ?throw("Can not find a rel file. (" ++ Vsn ++ ")");
{_, _, RelFile} ->
case erlup_utils:lookup_include_libs(RelFile) of
{ok, AppVsns} ->
[begin
EbinDir = filename:join([erlup_utils:base_dir(RelFile), "lib",
atom_to_list(App) ++ "-" ++ AppVsn, "ebin"]),
{App, AppVsn, EbinDir}
end || {App, AppVsn} <- AppVsns];
{error, Reason} ->
?throw(Reason)
end
end
end,
rewrite_appups(Fun(CurrentVsn), Fun(PreviousVsn), State2).
%%----------------------------------------------------------------------------------------------------------------------
%% 'provider' Callback Functions
%%----------------------------------------------------------------------------------------------------------------------
%% @private
-spec init(rebar_state:t()) -> {ok, rebar_state:t()}.
init(State) ->
Provider = providers:create([
{name, appup},
{namespace, erlup},
{module, ?MODULE},
{bare, true},
{deps, []},
{opts, erlup_rebar3:opts("appup")},
{short_desc, "Generate the .appup file"}
]),
{ok, rebar_state:add_provider(State, Provider)}.
%% @private
-spec do(rebar_state:t()) -> {ok, rebar_state:t()} | {error, string()}.
do(State) ->
erlup_rebar3:do("appup", State).
%% @private
-spec format_error(iodata()) -> iolist().
format_error(Reason) ->
io_lib:format("~s", [Reason]).
%%----------------------------------------------------------------------------------------------------------------------
%% Internal Functions
%%----------------------------------------------------------------------------------------------------------------------
%% @doc Rewrite the appup files.
-spec rewrite_appups(To, From, erlup_state:t()) -> ok when
To :: AppInfos,
From :: AppInfos,
AppInfos :: [{App :: atom(), Vsn :: string(), EbinDir :: file:filename_all()}].
rewrite_appups([], _, _) ->
ok;
rewrite_appups([{App, ToVsn, ToEbinDir} | ToRest], From, State0) ->
case lists:keyfind(App, 1, From) of
false ->
?DEBUG("~s (~p) is added application.", [App, ToVsn]),
rewrite_appups(ToRest, From, State0);
{_, ToVsn, _} ->
?DEBUG("~s (~p) is not changed.", [App, ToVsn]),
rewrite_appups(ToRest, From, State0);
{_, FromVsn, FromEbinDir} ->
?DEBUG("~s (~p => ~p) is changed. ebin : ~s => ~s", [App, FromVsn, ToVsn, FromEbinDir, ToEbinDir]),
State = lists:foldl(fun({Before, After}, Acc) -> erlup_state:set_sedargs(Before, After, Acc) end,
State0, [{'$from_vsn', FromVsn}, {'$to_vsn', ToVsn}]),
AppupPath = filename:join(ToEbinDir, atom_to_list(App) ++ ".appup"),
{Up, Down} = case file:consult(AppupPath) of
{ok, [{ToVsn, Up0, Down0}]} when is_list(Up0), is_list(Down0) ->
%% NOTE: If an exact match, delete here, because want to overwrite.
{proplists:delete(FromVsn, Up0), proplists:delete(FromVsn, Down0)};
{ok, _} ->
?WARN("the appup (~s) is invalid format. to overwrite.", [AppupPath]),
{[], []};
_ ->
{[], []}
end,
case {find_appup_instructions(FromVsn, Up), find_appup_instructions(FromVsn, Down)} of
{Res1, Res2} when Res1 =/= error, Res2 =/= error ->
?INFO("Skip that rewrite the appup (~s), because ~s is already exists.", [AppupPath, FromVsn]);
{Res1, Res2} ->
Instructions = case Res1 of
{ok, Instructions0} -> Instructions0;
error ->
?IF(Res2 =/= error,
?WARN("Down instructions exist, but up instructions doesn't exist. (~s)",
[AppupPath])),
application_instructions(ToEbinDir, FromEbinDir, State)
end,
Up2 = [{FromVsn, Instructions} | Up],
Down2 = [{FromVsn, revert_instructions(Instructions, State)} | Down],
case file:write_file(AppupPath, format_appup(ToVsn, Up2, Down2)) of
ok -> ok;
{error, Reason} -> ?throw(file:format_error(Reason) ++ " " ++ AppupPath)
end,
?INFO("rewrite the appup (~s).", [AppupPath])
end,
rewrite_appups(ToRest, From, State)
end.
%% @doc Find the instructions that is matching vsn pattern.
-spec find_appup_instructions(Vsn, [{VsnPattern, [instruction()]}]) -> {ok, [instruction()]} | error when
Vsn :: string(),
VsnPattern :: binary() | string().
find_appup_instructions(_, []) ->
error;
find_appup_instructions(Vsn, [{VsnPattern, Instructions} | Rest]) when is_binary(VsnPattern) ->
case re:run(Vsn, VsnPattern) of
nomatch -> find_appup_instructions(Vsn, Rest);
{match, _} -> {ok, Instructions}
end;
find_appup_instructions(Vsn, [{VsnPattern, Instructions} | _]) when Vsn =:= VsnPattern ->
{ok, Instructions};
find_appup_instructions(Vsn, [_ | Rest]) ->
find_appup_instructions(Vsn, Rest).
%% @doc Given instructions at the time of upgrade, returns instructions at the time of downgrade.
-spec revert_instructions([instruction()], erlup_state:t()) -> [instruction()].
revert_instructions(Insts, State) ->
revert_instructions(Insts, State, []).
%% @see revert_instructions/2
-spec revert_instructions([instruction()], erlup_state:t(), [instruction()]) -> [instruction()].
revert_instructions([], _, Acc) ->
Acc; % keep the reversing list
revert_instructions([Inst | RestInsts], State, Acc) when element(1, Inst) =:= add_module ->
%% {add_module, Mod}, {add_module, Mod, DepMods} -> {delete_module, ...}
revert_instructions(RestInsts, State, [setelement(1, Inst, delete_module) | Acc]);
revert_instructions([Inst | RestInsts], State, Acc) when element(1, Inst) =:= delete_module ->
%% {delete_module, Mod}, {delete_module, Mod, DepMods} -> {add_module, ...}
revert_instructions(RestInsts, State, [setelement(1, Inst, add_module) | Acc]);
revert_instructions([{apply, {M, F, _}} | RestInsts], State, Acc) ->
%% {apply, {M, F, A}}: Args changed.
A = proplists:get_value(F, erlup_state:applies(down, State)),
revert_instructions(RestInsts, State, [{apply, {M, F, A}} | Acc]);
revert_instructions([Inst | RestInsts], State, Acc) when
element(1, Inst) =:= update, element(1, element(3, Inst)) =:= advanced ->
%% {update, Mod, {advanced, Extra}, ...}: Extra changed.
Extra = erlup_state:extra(down, State),
revert_instructions(RestInsts, State, [setelement(3, Inst, {advanced, Extra}) | Acc]);
revert_instructions([Inst | RestInsts], State, Acc) ->
revert_instructions(RestInsts, State, [Inst | Acc]).
%% @doc Returns the instructions of the application at the time of upgrade.
-spec application_instructions(file:filename(), file:filename(), erlup_state:t()) -> [instruction()].
application_instructions(ToEbinDir, FromEbinDir, State) ->
{Added, Deleted, Differents} = beam_lib:cmp_dirs(ToEbinDir, FromEbinDir),
CmpFiles = lists:map(fun(X) -> {added, X} end, Added)
++ lists:map(fun({X, _}) -> {changed, X} end, Differents)
++ lists:map(fun(X) -> {deleted, X} end, Deleted),
%% TODO: ordering
application_instructions_1(CmpFiles, State, []).
%% @see application_instructions/3
-spec application_instructions_1([{Cmp, file:filename()}], erlup_state:t(), _) -> [instruction()] when
Cmp :: added | deleted | changed.
application_instructions_1([], _, Acc) ->
lists:flatten(Acc);
application_instructions_1([{Cmp, File} | Rest], State, Acc) ->
{ok, Chunks} = beam_lib:chunks(File, [attributes, exports]),
application_instructions_1(Rest, State, [module_instructions(Cmp, Chunks, State) | Acc]).
%% @doc Returns the instructions of the module at the time of upgrade.
-spec module_instructions(Cmp, {module(), beam_lib:chunkdata()}, erlup_state:t()) -> [instruction()] when
Cmp :: added | deleted | changed.
module_instructions(added, {Module, _}, _) ->
[{add_module, Module}];
module_instructions(deleted, {Module, _}, _) ->
[{delete_module, Module}];
module_instructions(changed, {Module, ChunkData}, State) ->
ModDeps = proplists:get_value(Module, erlup_state:mod_deps(State), []),
Insts = ?IIF(is_supervisor(ChunkData), [{update, Module, supervisor}], [])
++ ?IIF(is_special_process(ChunkData),
[{update, Module, {advanced, erlup_state:extra(up, State)}, ModDeps}], []),
?IIF(Insts =:= [], [{load_module, Module, ModDeps}], Insts)
++ lists:map(fun(X) -> {apply, X} end, get_applies({Module, ChunkData}, State)).
%% @doc Get the functions that be called at the time of upgrade.
-spec get_applies({module(), [beam_lib:chunkdata()]}, erlup_state:t()) -> [{module(), Function :: atom(), Args :: term()}].
get_applies({Module, ChunkData}, State) ->
Exports = proplists:get_value(exports, ChunkData, []),
lists:filter(fun({Function, Args}) when is_list(Args) ->
case lists:member({Function, length(Args)}, Exports) of
true -> {true, {Module, Function, Args}};
false -> false
end;
(_) -> false
end, erlup_state:applies(up, State)).
%% @doc Return the behaviour modules
-spec get_behaviour([beam_lib:chunkdata()]) -> [module()].
get_behaviour(ChunkData) ->
Attributes = proplists:get_value(attributes, ChunkData),
proplists:get_value(behavior, Attributes, []) ++ proplists:get_value(behaviour, Attributes, []).
%% @doc Return true, if it is special process. Otherwise false.
-spec is_special_process([beam_lib:chunkdata()]) -> boolean().
is_special_process(ChunkData) ->
Exports = proplists:get_value(exports, ChunkData, []),
proplists:is_defined(code_change, Exports) orelse proplists:is_defined(system_code_change, Exports).
%% @doc Return true, if it is supervisor. Otherwise, false.
-spec is_supervisor([beam_lib:chunkdata()]) -> boolean().
is_supervisor(ChunkData) ->
lists:member(supervisor, get_behaviour(ChunkData)).
%% @doc Returns the data of appup formats in human readable format.
-spec format_appup(string(), [{FromVsn, [instruction()]}], [{FromVsn, [instruction()]}]) -> iodata() when
FromVsn :: string().
format_appup(ToVsn, Up, Down) ->
io_lib:format("{~p,~n [~n~s ],~n [~n~s ]}.~n",
[ToVsn, format_vsn_instructions(Up), format_vsn_instructions(Down)]).
%% @see format_appup/3
-spec format_vsn_instructions([{FromVsn, [instruction()]}]) -> iodata() when
FromVsn :: string().
format_vsn_instructions(VsnInstructions) ->
string:join(lists:map(fun({FromVsn, Instructions}) ->
io_lib:format(" {~p,~n [~n~s ]}",
[FromVsn, format_instructions(Instructions)])
end, VsnInstructions), [$,,$\n]) ++ [$\n].
%% @see format_appup/3
-spec format_instructions([instruction()]) -> iodata().
format_instructions([]) ->
[];
format_instructions(Instructions) ->
string:join(lists:map(fun(X) -> io_lib:format(" ~p", [X]) end, Instructions), [$,,$\n]) ++ [$\n].