update_appup.escript 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  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. log("No appup update is needed for this release, nothing to be 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: 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 if changes are made.
  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. log("Updating 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_versions(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_versions(PredVersion, Versions) ->
  89. {Maj, Min, Patch} = parse_semver(PredVersion),
  90. PredVersions = [semver(Maj, Min, P) || P <- lists:seq(0, Patch)],
  91. lists:foldl(fun ensure_version/2, Versions, PredVersions).
  92. ensure_version(Version, Versions) ->
  93. case lists:keyfind(Version, 1, Versions) of
  94. false ->
  95. [{Version, []}|Versions];
  96. _ ->
  97. Versions
  98. end.
  99. read_appup(File) ->
  100. case file:script(File, [{'VSN', "VSN"}]) of
  101. {ok, Terms} ->
  102. Terms;
  103. Error ->
  104. fail("Failed to parse appup file ~s: ~p", [File, Error])
  105. end.
  106. diff_releases(CurrDir, OldDir) ->
  107. Curr = hashsums(find_beams(CurrDir)),
  108. Old = hashsums(find_beams(OldDir)),
  109. Fun = fun(App, Modules, Acc) ->
  110. OldModules = maps:get(App, Old, #{}),
  111. Acc#{App => diff_app_modules(Modules, OldModules)}
  112. end,
  113. maps:fold(Fun, #{}, Curr).
  114. diff_app_modules(Modules, OldModules) ->
  115. {New, Changed} =
  116. maps:fold( fun(Mod, MD5, {New, Changed}) ->
  117. case OldModules of
  118. #{Mod := OldMD5} when MD5 =:= OldMD5 ->
  119. {New, Changed};
  120. #{Mod := _} ->
  121. {[Mod|New], Changed};
  122. _ -> {New, [Mod|Changed]}
  123. end
  124. end
  125. , {[], []}
  126. , Modules
  127. ),
  128. Deleted = maps:keys(maps:without(maps:keys(Modules), OldModules)),
  129. {New, Changed, Deleted}.
  130. find_beams(Dir) ->
  131. [filename:join(Dir, I) || I <- filelib:wildcard("**/ebin/*.beam", Dir)].
  132. prepare(Baseline, Prepare) ->
  133. log("~n===================================~n"
  134. "Baseline: ~s"
  135. "~n===================================~n", [Baseline]),
  136. log("Building the current version...~n"),
  137. Prepare andalso bash("make emqx-rel"),
  138. log("Downloading the preceding release...~n"),
  139. {ok, PredRootDir} = build_pred_release(Baseline, Prepare),
  140. BeamDir = "_build/emqx/rel/emqx/lib/",
  141. {BeamDir, filename:join(PredRootDir, BeamDir)}.
  142. build_pred_release(Baseline, Prepare) ->
  143. Repo = find_upstream_repo(),
  144. BaseDir = "/tmp/emqx-baseline/",
  145. Dir = filename:basename(Repo, ".git") ++ [$-|Baseline],
  146. %% TODO: shallow clone
  147. Script = "mkdir -p ${BASEDIR} &&
  148. cd ${BASEDIR} &&
  149. { git clone --branch ${TAG} ${REPO} ${DIR} || true; } &&
  150. cd ${DIR} &&
  151. make emqx-rel",
  152. Env = [{"REPO", Repo}, {"TAG", Baseline}, {"BASEDIR", BaseDir}, {"DIR", Dir}],
  153. Prepare andalso bash(Script, Env),
  154. {ok, filename:join(BaseDir, Dir)}.
  155. %% @doc Find whether we are in emqx or emqx-ee
  156. find_upstream_repo() ->
  157. Str = os:cmd("git remote get-url origin"),
  158. case re(Str, "/([^/]+).git$") of
  159. {match, ["emqx"]} -> "git@github.com:emqx/emqx.git";
  160. {match, ["emqx-ee"]} -> "git@github.com:emqx/emqx-ee.git";
  161. Ret -> fail("Cannot detect the correct upstream repo: ~p", [Ret])
  162. end.
  163. find_pred_tag(CurrentRelease) ->
  164. {Maj, Min, Patch} = parse_semver(CurrentRelease),
  165. case Patch of
  166. 0 -> undefined;
  167. _ -> {ok, semver(Maj, Min, Patch - 1)}
  168. end.
  169. -spec hashsums(file:filename()) -> #{App => #{module() => binary()}}
  170. when App :: atom().
  171. hashsums(Files) ->
  172. hashsums(Files, #{}).
  173. hashsums([], Acc) ->
  174. Acc;
  175. hashsums([File|Rest], Acc0) ->
  176. [_, "ebin", Dir|_] = lists:reverse(filename:split(File)),
  177. {match, [AppStr]} = re(Dir, "^(.*)-[^-]+$"),
  178. App = list_to_atom(AppStr),
  179. {ok, {Module, MD5}} = beam_lib:md5(File),
  180. Acc = maps:update_with( App
  181. , fun(Old) -> Old #{Module => MD5} end
  182. , #{Module => MD5}
  183. , Acc0
  184. ),
  185. hashsums(Rest, Acc).
  186. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  187. %% Utility functions
  188. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  189. parse_semver(Version) ->
  190. case re(Version, "^([0-9]+)\.([0-9]+)\.([0-9]+)$") of
  191. {match, [Maj, Min, Patch]} ->
  192. {list_to_integer(Maj), list_to_integer(Min), list_to_integer(Patch)};
  193. Err ->
  194. error({not_a_semver, Version})
  195. end.
  196. semver(Maj, Min, Patch) ->
  197. lists:flatten(io_lib:format("~p.~p.~p", [Maj, Min, Patch])).
  198. %% Locate a file in a specified application
  199. locate(App, Suffix) ->
  200. AppStr = atom_to_list(App),
  201. case filelib:wildcard("{src,apps,lib-*}/**/" ++ AppStr ++ Suffix) of
  202. [File] ->
  203. {ok, File};
  204. [] ->
  205. undefined
  206. end.
  207. bash(Script) ->
  208. bash(Script, []).
  209. bash(Script, Env) ->
  210. case cmd("bash", #{args => ["-c", Script], env => Env}) of
  211. 0 -> true;
  212. _ -> fail("Failed to run command: ~s", [Script])
  213. end.
  214. %% Spawn an executable and return the exit status
  215. cmd(Exec, Params) ->
  216. case os:find_executable(Exec) of
  217. false ->
  218. fail("Executable not found in $PATH: ~s", [Exec]);
  219. Path ->
  220. Params1 = maps:to_list(maps:with([env, args, cd], Params)),
  221. Port = erlang:open_port( {spawn_executable, Path}
  222. , [ exit_status
  223. , nouse_stdio
  224. | Params1
  225. ]
  226. ),
  227. receive
  228. {Port, {exit_status, Status}} ->
  229. Status
  230. end
  231. end.
  232. fail(Str) ->
  233. fail(Str, []).
  234. fail(Str, Args) ->
  235. log(Str ++ "~n", Args),
  236. halt(1).
  237. re(Subject, RE) ->
  238. re:run(Subject, RE, [{capture, all_but_first, list}]).
  239. log(Msg) ->
  240. log(Msg, []).
  241. log(Msg, Args) ->
  242. io:format(standard_error, Msg, Args).