update_appup.escript 9.0 KB

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