update_appup.escript 8.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215
  1. #!/usr/bin/env -S escript -c
  2. %% -*- erlang-indent-level:4 -*-
  3. %% A script that adds changed modules to the corresponding appup files
  4. main(Args) ->
  5. #{check := Check, current_release := CurrentRelease, prepare := Prepare} =
  6. parse_args(Args, #{check => false, prepare => true}),
  7. case find_pred_tag(CurrentRelease) of
  8. {ok, Baseline} ->
  9. {CurrDir, PredDir} = prepare(Baseline, Prepare),
  10. Changed = diff_releases(CurrDir, PredDir),
  11. _ = maps:map(fun(App, Changes) -> process_app(Baseline, Check, App, Changes) end, Changed),
  12. ok;
  13. undefined ->
  14. io:format(standard_error, "No appup update is needed for this release, done~n", []),
  15. ok
  16. end.
  17. parse_args([CurrentRelease = [A|_]], State) when A =/= $- ->
  18. State#{current_release => CurrentRelease};
  19. parse_args(["--check"|Rest], State) ->
  20. parse_args(Rest, State#{check => true});
  21. parse_args(["--skip-build"|Rest], State) ->
  22. parse_args(Rest, State#{prepare => false});
  23. parse_args([], _) ->
  24. fail("Usage:~n update_appup.escript [--check] [--skip-build] <current_release_tag>
  25. --check Don't update the appup files, just check that they are complete
  26. --skip-build Don't rebuild the releases. May produce wrong appup files.
  27. ").
  28. process_app(PredVersion, _Check, App, Changes) ->
  29. AppupFiles = filelib:wildcard(lists:concat(["{src,apps,lib-*}/**/", App, ".appup.src"])),
  30. case AppupFiles of
  31. [AppupFile] ->
  32. update_appup(PredVersion, AppupFile, Changes);
  33. [] ->
  34. io:format("~nWARNING: Please create an stub appup src file for ~p~n", [App])
  35. end.
  36. group_modules(L) ->
  37. lists:foldl(fun({App, Mod}, Acc) ->
  38. maps:update_with(App, fun(Tl) -> [Mod|Tl] end, [Mod], Acc)
  39. end, #{}, L).
  40. update_appup(_, File, {[], [], []}) ->
  41. %% No changes in the app. Just check syntax of the existing appup:
  42. _ = read_appup(File);
  43. update_appup(PredVersion, File, Changes) ->
  44. io:format("~nUpdating appup: ~p~n", [File]),
  45. {_, Upgrade0, Downgrade0} = read_appup(File),
  46. Upgrade = update_actions(PredVersion, Changes, Upgrade0),
  47. Downgrade = update_actions(PredVersion, Changes, Downgrade0),
  48. IOList = io_lib:format("%% -*- mode: erlang -*-
  49. {VSN,~n ~p,~n ~p}.~n", [Upgrade, Downgrade]),
  50. ok = file:write_file(File, IOList),
  51. %% Check appup syntax:
  52. _ = read_appup(File).
  53. update_actions(PredVersion, Changes, Versions) ->
  54. lists:map(fun(L) -> do_update_actions(Changes, L) end, ensure_pred_version(PredVersion, Versions)).
  55. do_update_actions(_, Ret = {<<".*">>, _}) ->
  56. Ret;
  57. do_update_actions(Changes, {Vsn, Actions}) ->
  58. {Vsn, process_changes(Changes, Actions)}.
  59. process_changes({New0, Changed0, Deleted0}, OldActions) ->
  60. AlreadyHandled = lists:map(fun(It) -> element(2, It) end, OldActions),
  61. New = New0 -- AlreadyHandled,
  62. Changed = Changed0 -- AlreadyHandled,
  63. Deleted = Deleted0 -- AlreadyHandled,
  64. OldActions ++ [{load_module, M, brutal_purge, soft_purge, []} || M <- Changed ++ New]
  65. ++ [{delete_module, M} || M <- Deleted].
  66. ensure_pred_version(PredVersion, Versions) ->
  67. case lists:keyfind(PredVersion, 1, Versions) of
  68. false ->
  69. [{PredVersion, []}|Versions];
  70. _ ->
  71. Versions
  72. end.
  73. read_appup(File) ->
  74. case file:script(File, [{'VSN', "VSN"}]) of
  75. {ok, Terms} ->
  76. Terms;
  77. Error ->
  78. fail("Failed to parse appup file ~s: ~p", [File, Error])
  79. end.
  80. diff_releases(CurrDir, OldDir) ->
  81. Curr = hashsums(find_beams(CurrDir)),
  82. Old = hashsums(find_beams(OldDir)),
  83. Fun = fun(App, Modules, Acc) ->
  84. OldModules = maps:get(App, Old, #{}),
  85. Acc#{App => diff_app_modules(Modules, OldModules)}
  86. end,
  87. maps:fold(Fun, #{}, Curr).
  88. diff_app_modules(Modules, OldModules) ->
  89. {New, Changed} =
  90. maps:fold( fun(Mod, MD5, {New, Changed}) ->
  91. case OldModules of
  92. #{Mod := OldMD5} when MD5 =:= OldMD5 ->
  93. {New, Changed};
  94. #{Mod := _} ->
  95. {[Mod|New], Changed};
  96. _ -> {New, [Mod|Changed]}
  97. end
  98. end
  99. , {[], []}
  100. , Modules
  101. ),
  102. Deleted = maps:keys(maps:without(maps:keys(Modules), OldModules)),
  103. {New, Changed, Deleted}.
  104. find_beams(Dir) ->
  105. [filename:join(Dir, I) || I <- filelib:wildcard("**/ebin/*.beam", Dir)].
  106. prepare(Baseline, Prepare) ->
  107. io:format("~n===================================~n"
  108. "Baseline: ~s"
  109. "~n===================================~n", [Baseline]),
  110. io:format("Building the current version...~n"),
  111. Prepare andalso success(cmd("make", #{args => ["emqx-rel"]}), "Failed to build HEAD"),
  112. io:format("Downloading the preceding release...~n"),
  113. {ok, PredRootDir} = build_pred_release(Baseline, Prepare),
  114. BeamDir = "_build/emqx/rel/emqx/lib/",
  115. {BeamDir, filename:join(PredRootDir, BeamDir)}.
  116. build_pred_release(Baseline, Prepare) ->
  117. Repo = find_upstream_repo(),
  118. BaseDir = "/tmp/emqx-baseline/",
  119. Dir = filename:basename(Repo, ".git") ++ [$-|Baseline],
  120. %% TODO: shallow clone
  121. Script = "mkdir -p ${BASEDIR} && cd ${BASEDIR} && { git clone --branch ${TAG} ${REPO} ${DIR} || true; } && cd ${DIR} && make emqx-rel",
  122. Env = [{"REPO", Repo}, {"TAG", Baseline}, {"BASEDIR", BaseDir}, {"DIR", Dir}],
  123. Prepare andalso
  124. success( cmd("bash", #{ args => ["-c", Script]
  125. , env => Env
  126. })
  127. , "Failed to build the baseline release"
  128. ),
  129. {ok, filename:join(BaseDir, Dir)}.
  130. %% @doc Find whether we are in emqx or emqx-ee
  131. find_upstream_repo() ->
  132. Str = os:cmd("git remote get-url origin"),
  133. case re:run(Str, "/([^/]+).git$", [{capture, all_but_first, list}]) of
  134. {match, ["emqx"]} -> "git@github.com:emqx/emqx.git";
  135. {match, ["emqx-ee"]} -> "git@github.com:emqx/emqx-ee.git";
  136. Ret -> fail("Cannot detect the correct upstream repo: ~p", [Ret])
  137. end.
  138. find_pred_tag(CurrentRelease) ->
  139. case re:run(CurrentRelease, "^([0-9]+)\.([0-9]+)\.([0-9]+)$", [{capture, all_but_first, list}]) of
  140. {match, [Maj, Min, Patch]} ->
  141. case list_to_integer(Patch) of
  142. 0 -> undefined;
  143. P -> {ok, lists:flatten(io_lib:format("~s.~s.~p", [Maj, Min, P - 1]))}
  144. end;
  145. Err ->
  146. fail("The current release tag doesn't follow semver pattern: ~p", [Err])
  147. end.
  148. -spec hashsums(file:filename()) -> #{App => #{module() => binary()}}
  149. when App :: atom().
  150. hashsums(Files) ->
  151. hashsums(Files, #{}).
  152. hashsums([], Acc) ->
  153. Acc;
  154. hashsums([File|Rest], Acc0) ->
  155. [_, "ebin", Dir|_] = lists:reverse(filename:split(File)),
  156. {match, [AppStr]} = re:run(Dir, "^(.*)-[^-]+$", [{capture, all_but_first, list}]),
  157. App = list_to_atom(AppStr),
  158. {ok, {Module, MD5}} = beam_lib:md5(File),
  159. Acc = maps:update_with( App
  160. , fun(Old) -> Old #{Module => MD5} end
  161. , #{Module => MD5}
  162. , Acc0
  163. ),
  164. hashsums(Rest, Acc).
  165. %% Spawn an executable and return the exit status
  166. cmd(Exec, Params) ->
  167. case os:find_executable(Exec) of
  168. false ->
  169. fail("Executable not found in $PATH: ~s", [Exec]);
  170. Path ->
  171. Params1 = maps:to_list(maps:with([env, args, cd], Params)),
  172. Port = erlang:open_port( {spawn_executable, Path}
  173. , [ exit_status
  174. , nouse_stdio
  175. | Params1
  176. ]
  177. ),
  178. receive
  179. {Port, {exit_status, Status}} ->
  180. Status
  181. end
  182. end.
  183. success(0, _) ->
  184. true;
  185. success(_, Msg) ->
  186. fail(Msg).
  187. fail(Str) ->
  188. fail(Str, []).
  189. fail(Str, Args) ->
  190. io:format(standard_error, Str ++ "~n", Args),
  191. halt(1).