Packages

Upgrade tools for Erlang/OTP. Contains rebar3 plugin and escript.

Current section

Files

Jump to
erlup src erlup_appup.erl
Raw

src/erlup_appup.erl

%% @copyright 2016 Hinagiku Soranoba All Rights Reserved.
%%
%% @doc Automatically generate the .appup files from the beam file.
%%
%% ### Overview
%%
%% 1. Support the multiple versions upgrade and downgrade.
%% 2. You can define the extra and original code_change functions.
%%
%% ### Usage
%%
%% ```
%% $ rebar3 help erlup appup # using rebar3.
%% $ erlup appup -h # using escript.
%% '''
%%
-module(erlup_appup).
-behaviour(provider).
-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.
%% @private
-spec do([file:filename()], string(), string(), erlup_state:t()) -> ok.
do(Dirs, PreviousVsn, CurrentVsn, State0) ->
?INFO("previous = ~s, current = ~s", [PreviousVsn, CurrentVsn]),
State = lists:foldl(fun({Before, After}, Acc) -> erlup_state:set_sedargs(Before, After, Acc) end,
State0, [{'$from', PreviousVsn}, {'$to', CurrentVsn}]),
Fun = fun(Vsn) ->
case erlup_utils:find_rel(Dirs, Vsn) of
{ok, RelFile} ->
case erlup_utils:vsn_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;
{error, Reason} ->
?throw(Reason)
end
end,
rewrite_appups(Fun(CurrentVsn), Fun(PreviousVsn), State).
%%----------------------------------------------------------------------------------------------------------------------
%% '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, opts()},
{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) ->
{Opts, _} = rebar_state:command_parsed_args(State),
Name = case rebar_state:get(State, relx, undefined) of
undefined -> ?throw("Relx configuration is not found.");
RelxConf ->
case lists:keyfind(release, 1, RelxConf) of
{_, {Name0, _}, _} when is_atom(Name0) -> atom_to_list(Name0);
_ -> ?throw("Release name is unknown.")
end
end,
Dir = filename:join([rebar_dir:base_dir(State), "rel", Name]),
CurrentVsn = proplists:get_value(current, Opts ++ [{current, erlup_utils:default_current_vsn(Dir)}]),
PreviousVsn = proplists:get_value(previous, Opts ++ [{previous, erlup_utils:default_previous_vsn(Dir, CurrentVsn)}]),
CurrentVsn =:= [] andalso ?throw("Current vsn is unknown. Please run ./rebar3 release"),
PreviousVsn =:= [] andalso ?throw("Previous vsn is unknown"),
do([Dir], PreviousVsn, CurrentVsn, erlup_state:new(rebar_state:get(State, erlup, []))),
{ok, State}.
%% @private
-spec format_error(iodata()) -> iolist().
format_error(Reason) ->
io_lib:format("~s", [Reason]).
%%----------------------------------------------------------------------------------------------------------------------
%% Internal Functions
%%----------------------------------------------------------------------------------------------------------------------
-spec opts() -> [getopt:option_spec()].
opts() ->
[
{previous, $p, undefined, string, "Previous vsn"},
{current, $c, undefined, string, "Current vsn"}
].
rewrite_appups([], _, _) ->
ok;
rewrite_appups([{App, ToVsn, ToEbinDir} | ToRest], From, State0) ->
case lists:keyfind(App, 1, From) of
false ->
rewrite_appups(ToRest, From, State0);
{_, ToVsn, _} ->
rewrite_appups(ToRest, From, State0);
{_, FromVsn, FromEbinDir} ->
?DEBUG("to: ~s, from: ~s", [ToEbinDir, FromEbinDir]),
State = erlup_state:set_sedargs('$oldvsn', FromVsn, State0),
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) ->
%% TODO: match pattern. not override mode.
{proplists:delete(FromVsn, Up0), proplists:delete(FromVsn, Down0)};
{ok, _} ->
?WARN("the appup (~s) is invalid format. to overwrite.", [AppupPath]),
{[], []};
_ ->
{[], []}
end,
Up2 = [{FromVsn, Instructions = application_instructions(ToEbinDir, FromEbinDir, State)} | 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]),
rewrite_appups(ToRest, From, State)
end.
%% @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:applys(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_applys({Module, ChunkData}, State)).
%% @doc Get the functions that be called at the time of upgrade.
-spec get_applys({module(), [beam_lib:chunkdata()]}, erlup_state:t()) -> [{module(), Function :: atom(), Args :: term()}].
get_applys({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:applys(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([]) ->
[];
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].