update_appup.escript 11 KB

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