%% -*- erlang-indent-level: 4;indent-tabs-mode: nil -*- %% ex: ts=4 sw=4 et %% ------------------------------------------------------------------- %% %% rebar: Erlang Build Tools %% %% Copyright (c) 2011 Joe Williams (joe@joetify.com) %% %% Permission is hereby granted, free of charge, to any person obtaining a copy %% of this software and associated documentation files (the "Software"), to deal %% in the Software without restriction, including without limitation the rights %% to use, copy, modify, merge, publish, distribute, sublicense, and/or sell %% copies of the Software, and to permit persons to whom the Software is %% furnished to do so, subject to the following conditions: %% %% The above copyright notice and this permission notice shall be included in %% all copies or substantial portions of the Software. %% %% THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR %% IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, %% FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE %% AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER %% LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, %% OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN %% THE SOFTWARE. %% ------------------------------------------------------------------ -module(rebar_appups). -include("rebar.hrl"). -export(['generate-appups'/2]). %% for internal use only -export([info/2]). -define(APPUPFILEFORMAT, "%% appup generated for ~p by rebar (~p)~n" "{~p, [{~p, ~p}], [{~p, []}]}.~n"). %% ==================================================================== %% Public API %% ==================================================================== 'generate-appups'(Config, ReltoolFile) -> %% Get the old release path {Config1, ReltoolConfig} = rebar_rel_utils:load_config(Config, ReltoolFile), TargetParentDir = rebar_rel_utils:get_target_parent_dir(Config, ReltoolConfig), PrevRelPath = rebar_rel_utils:get_previous_release_path(Config), OldVerPath = filename:join([TargetParentDir, PrevRelPath]), ModDeps = rebar_config:get(Config, module_deps, []), %% Get the new and old release name and versions {Name, _Ver} = rebar_rel_utils:get_reltool_release_info(ReltoolConfig), NewVerPath = filename:join([TargetParentDir, Name]), {NewName, NewVer} = rebar_rel_utils:get_rel_release_info(Name, NewVerPath), {OldName, OldVer} = rebar_rel_utils:get_rel_release_info(Name, OldVerPath), %% Run some simple checks true = rebar_utils:prop_check(NewVer =/= OldVer, "New and old .rel versions match~n", []), true = rebar_utils:prop_check( NewName == OldName, "Reltool and .rel release names do not match~n", []), %% Find all the apps that have been upgraded {_Added, _Removed, Upgraded} = get_apps(Name, OldVerPath, NewVerPath), %% Get a list of any appup files that exist in the new release NewAppUpFiles = rebar_utils:find_files( filename:join([NewVerPath, "lib"]), "^.*.appup$"), %% Convert the list of appup files into app names AppUpApps = [file_to_name(File) || File <- NewAppUpFiles], %% Create a list of apps that don't already have appups UpgradeApps = genappup_which_apps(Upgraded, AppUpApps), %% Generate appup files for upgraded apps generate_appup_files(NewVerPath, OldVerPath, ModDeps, UpgradeApps), {ok, Config1}. %% =================================================================== %% Internal functions %% =================================================================== info(help, 'generate-appups') -> ?CONSOLE("Generate appup files.~n" "~n" "Valid command line options:~n" " previous_release=path~n", []). get_apps(Name, OldVerPath, NewVerPath) -> OldApps = rebar_rel_utils:get_rel_apps(Name, OldVerPath), ?DEBUG("Old Version Apps: ~p~n", [OldApps]), NewApps = rebar_rel_utils:get_rel_apps(Name, NewVerPath), ?DEBUG("New Version Apps: ~p~n", [NewApps]), Added = app_list_diff(NewApps, OldApps), ?DEBUG("Added: ~p~n", [Added]), Removed = app_list_diff(OldApps, NewApps), ?DEBUG("Removed: ~p~n", [Removed]), PossiblyUpgraded = proplists:get_keys(NewApps), UpgradedApps = [upgraded_app(AppName, proplists:get_value(AppName, OldApps), proplists:get_value(AppName, NewApps)) || AppName <- PossiblyUpgraded], Upgraded = lists:dropwhile(fun(Elem) -> Elem == false end, lists:sort(UpgradedApps)), ?DEBUG("Upgraded: ~p~n", [Upgraded]), {Added, Removed, Upgraded}. upgraded_app(AppName, OldAppVer, NewAppVer) when OldAppVer /= NewAppVer -> {AppName, {OldAppVer, NewAppVer}}; upgraded_app(_, _, _) -> false. app_list_diff(List1, List2) -> List3 = lists:umerge(lists:sort(proplists:get_keys(List1)), lists:sort(proplists:get_keys(List2))), List3 -- proplists:get_keys(List2). file_to_name(File) -> filename:rootname(filename:basename(File)). genappup_which_apps(UpgradedApps, [First|Rest]) -> List = proplists:delete(list_to_atom(First), UpgradedApps), genappup_which_apps(List, Rest); genappup_which_apps(Apps, []) -> Apps. generate_appup_files(NewVerPath, OldVerPath, ModDeps, [{_App, {undefined, _}}|Rest]) -> generate_appup_files(NewVerPath, OldVerPath, ModDeps, Rest); generate_appup_files(NewVerPath, OldVerPath, ModDeps, [{App, {OldVer, NewVer}}|Rest]) -> OldEbinDir = filename:join([OldVerPath, "lib", atom_to_list(App) ++ "-" ++ OldVer, "ebin"]), NewEbinDir = filename:join([NewVerPath, "lib", atom_to_list(App) ++ "-" ++ NewVer, "ebin"]), {AddedFiles, DeletedFiles, ChangedFiles} = beam_lib:cmp_dirs(NewEbinDir, OldEbinDir), ChangedNames = [list_to_atom(file_to_name(F)) || {F, _} <- ChangedFiles], ModDeps1 = [{N, [M1 || M1 <- M, lists:member(M1, ChangedNames)]} || {N, M} <- ModDeps], Added = [generate_instruction(added, File) || File <- AddedFiles], Deleted = [generate_instruction(deleted, File) || File <- DeletedFiles], Changed = [generate_instruction(changed, ModDeps1, File) || File <- ChangedFiles], Inst = lists:append([Added, Deleted, Changed]), AppUpFile = filename:join([NewEbinDir, atom_to_list(App) ++ ".appup"]), ok = file:write_file(AppUpFile, io_lib:fwrite(?APPUPFILEFORMAT, [App, rebar_utils:now_str(), NewVer, OldVer, Inst, OldVer])), ?CONSOLE("Generated appup for ~p~n", [App]), generate_appup_files(NewVerPath, OldVerPath, ModDeps, Rest); generate_appup_files(_, _, _, []) -> ?CONSOLE("Appup generation complete~n", []). generate_instruction(added, File) -> Name = list_to_atom(file_to_name(File)), {add_module, Name}; generate_instruction(deleted, File) -> Name = list_to_atom(file_to_name(File)), {delete_module, Name}. generate_instruction(changed, ModDeps, {File, _}) -> {ok, {Name, List}} = beam_lib:chunks(File, [attributes, exports]), Behavior = get_behavior(List), CodeChange = is_code_change(List), Deps = proplists:get_value(Name, ModDeps, []), generate_instruction_advanced(Name, Behavior, CodeChange, Deps). generate_instruction_advanced(Name, undefined, undefined, Deps) -> %% Not a behavior or code change, assume purely functional {load_module, Name, Deps}; generate_instruction_advanced(Name, [supervisor], _, _) -> %% Supervisor {update, Name, supervisor}; generate_instruction_advanced(Name, _, code_change, Deps) -> %% Includes code_change export {update, Name, {advanced, []}, Deps}; generate_instruction_advanced(Name, _, _, Deps) -> %% Anything else {load_module, Name, Deps}. get_behavior(List) -> Attributes = proplists:get_value(attributes, List), case proplists:get_value(behavior, Attributes) of undefined -> proplists:get_value(behaviour, Attributes); Else -> Else end. is_code_change(List) -> Exports = proplists:get_value(exports, List), case proplists:is_defined(code_change, Exports) of true -> code_change; false -> undefined end.