update_appup.escript 10 KB

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