update_appup.escript 9.9 KB

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