update_appup.escript 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656
  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. Please note that it only compares the current release with its
  13. predecessor, assuming that the upgrade actions for the older releases
  14. are correct.
  15. Note: The defaults are set up for emqx, but they can be tuned to
  16. support other repos too.
  17. Usage:
  18. update_appup.escript [--check] [--repo URL] [--remote NAME] [--skip-build] [--make-commad SCRIPT] [--release-dir DIR] <previous_release_tag>
  19. Options:
  20. --check Don't update the appfile, just check that they are complete
  21. --repo Upstream git repo URL
  22. --remote Get upstream repo URL from the specified git remote
  23. --skip-build Don't rebuild the releases. May produce wrong results
  24. --make-command A command used to assemble the release
  25. --release-dir Release directory
  26. --src-dirs Directories where source code is found. Defaults to '{src,apps,lib-*}/**/'
  27. --binary-rel-url Binary release URL pattern. %VSN% variable is substituted with the version in release tag.
  28. E.g. \"https://github.com/emqx/emqx/releases/download/v%VSN%/emqx-%VSN%-otp-24.1.5-3-centos7-amd64.tar.gz\"
  29. ".
  30. -record(app,
  31. { modules :: #{module() => binary()}
  32. , version :: string()
  33. }).
  34. default_options() ->
  35. #{ clone_url => find_upstream_repo("origin")
  36. , make_command => "make emqx-rel"
  37. , beams_dir => "_build/emqx/rel/emqx/lib/"
  38. , check => false
  39. , prev_tag => undefined
  40. , src_dirs => "{src,apps,lib-*}/**/"
  41. , binary_rel_url => undefined
  42. }.
  43. %% App-specific actions that should be added unconditionally to any update/downgrade:
  44. app_specific_actions(_) ->
  45. [].
  46. ignored_apps() ->
  47. [emqx_dashboard, emqx_management] ++ otp_standard_apps().
  48. main(Args) ->
  49. #{prev_tag := Baseline} = Options = parse_args(Args, default_options()),
  50. init_globals(Options),
  51. main(Options, Baseline).
  52. parse_args([PrevTag = [A|_]], State) when A =/= $- ->
  53. State#{prev_tag => PrevTag};
  54. parse_args(["--check"|Rest], State) ->
  55. parse_args(Rest, State#{check => true});
  56. parse_args(["--skip-build"|Rest], State) ->
  57. parse_args(Rest, State#{make_command => "true"});
  58. parse_args(["--repo", Repo|Rest], State) ->
  59. parse_args(Rest, State#{clone_url => Repo});
  60. parse_args(["--remote", Remote|Rest], State) ->
  61. parse_args(Rest, State#{clone_url => find_upstream_repo(Remote)});
  62. parse_args(["--make-command", Command|Rest], State) ->
  63. parse_args(Rest, State#{make_command => Command});
  64. parse_args(["--release-dir", Dir|Rest], State) ->
  65. parse_args(Rest, State#{beams_dir => Dir});
  66. parse_args(["--src-dirs", Pattern|Rest], State) ->
  67. parse_args(Rest, State#{src_dirs => Pattern});
  68. parse_args(["--binary-rel-url", URL|Rest], State) ->
  69. parse_args(Rest, State#{binary_rel_url => {ok, URL}});
  70. parse_args(_, _) ->
  71. fail(usage()).
  72. main(Options, Baseline) ->
  73. {CurrRelDir, PrevRelDir} = prepare(Baseline, Options),
  74. log("~n===================================~n"
  75. "Processing changes..."
  76. "~n===================================~n"),
  77. CurrAppsIdx = index_apps(CurrRelDir),
  78. PrevAppsIdx = index_apps(PrevRelDir),
  79. %% log("Curr: ~p~nPrev: ~p~n", [CurrAppsIdx, PrevAppsIdx]),
  80. AppupChanges = find_appup_actions(CurrAppsIdx, PrevAppsIdx),
  81. case getopt(check) of
  82. true ->
  83. case AppupChanges of
  84. [] ->
  85. ok;
  86. _ ->
  87. Diffs =
  88. lists:filtermap(
  89. fun({App, {Upgrade, Downgrade, OldUpgrade, OldDowngrade}}) ->
  90. case parse_appup_diffs(Upgrade, OldUpgrade,
  91. Downgrade, OldDowngrade) of
  92. ok ->
  93. false;
  94. {diffs, Diffs} ->
  95. {true, {App, Diffs}}
  96. end
  97. end,
  98. AppupChanges),
  99. case Diffs =:= [] of
  100. true ->
  101. ok;
  102. false ->
  103. set_invalid(),
  104. log("ERROR: The appup files are incomplete. Missing changes:~n ~p",
  105. [Diffs])
  106. end
  107. end;
  108. false ->
  109. update_appups(AppupChanges)
  110. end,
  111. check_appup_files(),
  112. warn_and_exit(is_valid()).
  113. warn_and_exit(true) ->
  114. log("
  115. NOTE: Please review the changes manually. This script does not know about NIF
  116. changes, supervisor changes, process restarts and so on. Also the load order of
  117. the beam files might need updating.~n"),
  118. halt(0);
  119. warn_and_exit(false) ->
  120. log("~nERROR: Incomplete appups found. Please inspect the output for more details.~n"),
  121. halt(1).
  122. prepare(Baseline, Options = #{make_command := MakeCommand, beams_dir := BeamDir, binary_rel_url := BinRel}) ->
  123. log("~n===================================~n"
  124. "Baseline: ~s"
  125. "~n===================================~n", [Baseline]),
  126. log("Building the current version...~n"),
  127. bash(MakeCommand),
  128. log("Downloading and building the previous release...~n"),
  129. PrevRelDir =
  130. case BinRel of
  131. undefined ->
  132. {ok, PrevRootDir} = build_prev_release(Baseline, Options),
  133. filename:join(PrevRootDir, BeamDir);
  134. {ok, _URL} ->
  135. {ok, PrevRootDir} = download_prev_release(Baseline, Options),
  136. PrevRootDir
  137. end,
  138. {BeamDir, PrevRelDir}.
  139. build_prev_release(Baseline, #{clone_url := Repo, make_command := MakeCommand}) ->
  140. BaseDir = "/tmp/emqx-baseline/",
  141. Dir = filename:basename(Repo, ".git") ++ [$-|Baseline],
  142. %% TODO: shallow clone
  143. Script = "mkdir -p ${BASEDIR} &&
  144. cd ${BASEDIR} &&
  145. { [ -d ${DIR} ] || git clone --branch ${TAG} ${REPO} ${DIR}; } &&
  146. cd ${DIR} &&" ++ MakeCommand,
  147. Env = [{"REPO", Repo}, {"TAG", Baseline}, {"BASEDIR", BaseDir}, {"DIR", Dir}],
  148. bash(Script, Env),
  149. {ok, filename:join(BaseDir, Dir)}.
  150. download_prev_release(Tag, #{binary_rel_url := {ok, URL0}, clone_url := Repo}) ->
  151. URL = string:replace(URL0, "%TAG%", Tag, all),
  152. BaseDir = "/tmp/emqx-baseline-bin/",
  153. Dir = filename:basename(Repo, ".git") ++ [$-|Tag],
  154. Filename = filename:join(BaseDir, Dir),
  155. Script = "mkdir -p ${OUTFILE} &&
  156. wget -c -O ${OUTFILE}.tar.gz ${URL} &&
  157. tar -zxf ${OUTFILE} ${OUTFILE}.tar.gz",
  158. Env = [{"TAG", Tag}, {"OUTFILE", Filename}, {"URL", URL}],
  159. bash(Script, Env),
  160. {ok, Filename}.
  161. find_upstream_repo(Remote) ->
  162. string:trim(os:cmd("git remote get-url " ++ Remote)).
  163. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  164. %% Appup action creation and updating
  165. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  166. find_appup_actions(CurrApps, PrevApps) ->
  167. maps:fold(
  168. fun(App, CurrAppIdx, Acc) ->
  169. case PrevApps of
  170. #{App := PrevAppIdx} ->
  171. find_appup_actions(App, CurrAppIdx, PrevAppIdx) ++ Acc;
  172. _ ->
  173. %% New app, nothing to upgrade here.
  174. Acc
  175. end
  176. end,
  177. [],
  178. CurrApps).
  179. find_appup_actions(_App, AppIdx, AppIdx) ->
  180. %% No changes to the app, ignore:
  181. [];
  182. find_appup_actions(App,
  183. CurrAppIdx = #app{version = CurrVersion},
  184. PrevAppIdx = #app{version = PrevVersion}) ->
  185. {OldUpgrade0, OldDowngrade0} = find_old_appup_actions(App, PrevVersion),
  186. OldUpgrade = ensure_all_patch_versions(App, CurrVersion, OldUpgrade0),
  187. OldDowngrade = ensure_all_patch_versions(App, CurrVersion, OldDowngrade0),
  188. Upgrade = merge_update_actions(App, diff_app(App, CurrAppIdx, PrevAppIdx), OldUpgrade),
  189. Downgrade = merge_update_actions(App, diff_app(App, PrevAppIdx, CurrAppIdx), OldDowngrade),
  190. if OldUpgrade =:= Upgrade andalso OldDowngrade =:= Downgrade ->
  191. %% The appup file has been already updated:
  192. [];
  193. true ->
  194. [{App, {Upgrade, Downgrade, OldUpgrade, OldDowngrade}}]
  195. end.
  196. %% To avoid missing one patch version when upgrading, we try to
  197. %% optimistically generate the list of expected versions that should
  198. %% be covered by the upgrade.
  199. ensure_all_patch_versions(App, CurrVsn, OldActions) ->
  200. case is_app_external(App) of
  201. true ->
  202. %% we do not attempt to predict the version list for
  203. %% external dependencies, as those may not follow our
  204. %% conventions.
  205. OldActions;
  206. false ->
  207. do_ensure_all_patch_versions(App, CurrVsn, OldActions)
  208. end.
  209. do_ensure_all_patch_versions(App, CurrVsn, OldActions) ->
  210. case enumerate_past_versions(CurrVsn) of
  211. {ok, ExpectedVsns} ->
  212. CoveredVsns = [V || {V, _} <- OldActions, V =/= <<".*">>],
  213. ExpectedVsnStrs = [vsn_number_to_string(V) || V <- ExpectedVsns],
  214. MissingActions = [{V, []} || V <- ExpectedVsnStrs, not contains_version(V, CoveredVsns)],
  215. MissingActions ++ OldActions;
  216. {error, bad_version} ->
  217. log("WARN: Could not infer expected versions to upgrade from for ~p~n", [App]),
  218. OldActions
  219. end.
  220. %% For external dependencies, show only the changes that are missing
  221. %% in their current appup.
  222. diff_appup_instructions(ComputedChanges, PresentChanges) ->
  223. lists:foldr(
  224. fun({VsnOrRegex, ComputedActions}, Acc) ->
  225. case find_matching_version(VsnOrRegex, PresentChanges) of
  226. undefined ->
  227. [{VsnOrRegex, ComputedActions} | Acc];
  228. PresentActions ->
  229. DiffActions = ComputedActions -- PresentActions,
  230. case DiffActions of
  231. [] ->
  232. %% no diff
  233. Acc;
  234. _ ->
  235. [{VsnOrRegex, DiffActions} | Acc]
  236. end
  237. end
  238. end,
  239. [],
  240. ComputedChanges).
  241. %% For external dependencies, checks if any missing diffs are present
  242. %% and groups them by `up' and `down' types.
  243. parse_appup_diffs(Upgrade, OldUpgrade, Downgrade, OldDowngrade) ->
  244. DiffUp = diff_appup_instructions(Upgrade, OldUpgrade),
  245. DiffDown = diff_appup_instructions(Downgrade, OldDowngrade),
  246. case {DiffUp, DiffDown} of
  247. {[], []} ->
  248. %% no diff for external dependency; ignore
  249. ok;
  250. _ ->
  251. set_invalid(),
  252. Diffs = #{ up => DiffUp
  253. , down => DiffDown
  254. },
  255. {diffs, Diffs}
  256. end.
  257. find_matching_version(VsnOrRegex, PresentChanges) ->
  258. proplists:get_value(VsnOrRegex, PresentChanges).
  259. find_old_appup_actions(App, PrevVersion) ->
  260. {Upgrade0, Downgrade0} =
  261. case locate(ebin_current, App, ".appup") of
  262. {ok, AppupFile} ->
  263. log("Found the previous appup file: ~s~n", [AppupFile]),
  264. {_, U, D} = read_appup(AppupFile),
  265. {U, D};
  266. undefined ->
  267. %% Fallback to the app.src file, in case the
  268. %% application doesn't have a release (useful for the
  269. %% apps that live outside the EMQX monorepo):
  270. case locate(src, App, ".appup.src") of
  271. {ok, AppupSrcFile} ->
  272. log("Using ~s as a source of previous update actions~n", [AppupSrcFile]),
  273. {_, U, D} = read_appup(AppupSrcFile),
  274. {U, D};
  275. undefined ->
  276. {[], []}
  277. end
  278. end,
  279. {ensure_version(PrevVersion, Upgrade0), ensure_version(PrevVersion, Downgrade0)}.
  280. merge_update_actions(App, Changes, Vsns) ->
  281. lists:map(fun(Ret = {<<".*">>, _}) ->
  282. Ret;
  283. ({Vsn, Actions}) ->
  284. {Vsn, do_merge_update_actions(App, Changes, Actions)}
  285. end,
  286. Vsns).
  287. do_merge_update_actions(App, {New0, Changed0, Deleted0}, OldActions) ->
  288. AppSpecific = app_specific_actions(App) -- OldActions,
  289. AlreadyHandled = lists:flatten(lists:map(fun process_old_action/1, OldActions)),
  290. New = New0 -- AlreadyHandled,
  291. Changed = Changed0 -- AlreadyHandled,
  292. Deleted = Deleted0 -- AlreadyHandled,
  293. Reloads = [{load_module, M, brutal_purge, soft_purge, []}
  294. || not contains_restart_application(App, OldActions),
  295. M <- Changed ++ New],
  296. {OldActionsWithStop, OldActionsAfterStop} =
  297. find_application_stop_instruction(App, OldActions),
  298. OldActionsWithStop ++
  299. Reloads ++
  300. OldActionsAfterStop ++
  301. [{delete_module, M} || M <- Deleted] ++
  302. AppSpecific.
  303. %% If an entry restarts an application, there's no need to use
  304. %% `load_module' instructions.
  305. contains_restart_application(Application, Actions) ->
  306. lists:member({restart_application, Application}, Actions).
  307. %% If there is an `application:stop(Application)' call in the
  308. %% instructions, we insert `load_module' instructions after it.
  309. find_application_stop_instruction(Application, Actions) ->
  310. {Before, After0} =
  311. lists:splitwith(
  312. fun({apply, {application, stop, [App]}}) when App =:= Application ->
  313. false;
  314. (_) ->
  315. true
  316. end, Actions),
  317. case After0 of
  318. [StopInst | After] ->
  319. {Before ++ [StopInst], After};
  320. [] ->
  321. {[], Before}
  322. end.
  323. %% @doc Process the existing actions to exclude modules that are
  324. %% already handled
  325. process_old_action({purge, Modules}) ->
  326. Modules;
  327. process_old_action({delete_module, Module}) ->
  328. [Module];
  329. process_old_action(LoadModule) when is_tuple(LoadModule) andalso
  330. element(1, LoadModule) =:= load_module ->
  331. element(2, LoadModule);
  332. process_old_action(_) ->
  333. [].
  334. ensure_version(Version, OldInstructions) ->
  335. OldVersions = [element(1, I) || I <- OldInstructions],
  336. case contains_version(Version, OldVersions) of
  337. false ->
  338. [{Version, []} | OldInstructions];
  339. true ->
  340. OldInstructions
  341. end.
  342. contains_version(Needle, Haystack) when is_list(Needle) ->
  343. lists:any(
  344. fun(Regex) when is_binary(Regex) ->
  345. case re:run(Needle, Regex) of
  346. {match, _} ->
  347. true;
  348. nomatch ->
  349. false
  350. end;
  351. (Vsn) ->
  352. Vsn =:= Needle
  353. end,
  354. Haystack).
  355. %% As a best effort approach, we assume that we only bump patch
  356. %% version numbers between release upgrades for our dependencies and
  357. %% that we deal only with 3-part version schemas
  358. %% (`Major.Minor.Patch'). Using those assumptions, we enumerate the
  359. %% past versions that should be covered by regexes in .appup file
  360. %% instructions.
  361. enumerate_past_versions(Vsn) when is_list(Vsn) ->
  362. case parse_version_number(Vsn) of
  363. {ok, ParsedVsn} ->
  364. {ok, enumerate_past_versions(ParsedVsn)};
  365. Error ->
  366. Error
  367. end;
  368. enumerate_past_versions({Major, Minor, Patch}) ->
  369. [{Major, Minor, P} || P <- lists:seq(Patch - 1, 0, -1)].
  370. parse_version_number(Vsn) when is_list(Vsn) ->
  371. Nums = string:split(Vsn, ".", all),
  372. Results = lists:map(fun string:to_integer/1, Nums),
  373. case Results of
  374. [{Major, []}, {Minor, []}, {Patch, []}] ->
  375. {ok, {Major, Minor, Patch}};
  376. _ ->
  377. {error, bad_version}
  378. end.
  379. vsn_number_to_string({Major, Minor, Patch}) ->
  380. io_lib:format("~b.~b.~b", [Major, Minor, Patch]).
  381. read_appup(File) ->
  382. %% NOTE: appup file is a script, it may contain variables or functions.
  383. case file:script(File, [{'VSN', "VSN"}]) of
  384. {ok, Terms} ->
  385. Terms;
  386. Error ->
  387. fail("Failed to parse appup file ~s: ~p", [File, Error])
  388. end.
  389. check_appup_files() ->
  390. AppupFiles = filelib:wildcard(getopt(src_dirs) ++ "/*.appup.src"),
  391. lists:foreach(fun read_appup/1, AppupFiles).
  392. update_appups(Changes) ->
  393. lists:foreach(
  394. fun({App, {Upgrade, Downgrade, OldUpgrade, OldDowngrade}}) ->
  395. do_update_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade)
  396. end,
  397. Changes).
  398. do_update_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade) ->
  399. case locate(src, App, ".appup.src") of
  400. {ok, AppupFile} ->
  401. case contains_contents(AppupFile, Upgrade, Downgrade) of
  402. true ->
  403. ok;
  404. false ->
  405. render_appfile(AppupFile, Upgrade, Downgrade)
  406. end;
  407. undefined ->
  408. case create_stub(App) of
  409. {ok, AppupFile} ->
  410. render_appfile(AppupFile, Upgrade, Downgrade);
  411. false ->
  412. case parse_appup_diffs(Upgrade, OldUpgrade,
  413. Downgrade, OldDowngrade) of
  414. ok ->
  415. %% no diff for external dependency; ignore
  416. ok;
  417. {diffs, Diffs} ->
  418. set_invalid(),
  419. log("ERROR: Appup file for the external dependency '~p' is not complete.~n Missing changes: ~100p~n", [App, Diffs]),
  420. log("NOTE: Some changes above might be already covered by regexes.~n")
  421. end
  422. end
  423. end.
  424. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  425. %% Appup file creation
  426. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  427. render_appfile(File, Upgrade, Downgrade) ->
  428. IOList = io_lib:format("%% -*- mode: erlang -*-\n{VSN,~n ~p,~n ~p}.~n", [Upgrade, Downgrade]),
  429. ok = file:write_file(File, IOList).
  430. create_stub(App) ->
  431. Ext = ".app.src",
  432. case locate(src, App, Ext) of
  433. {ok, AppSrc} ->
  434. DirName = filename:dirname(AppSrc),
  435. AppupFile = filename:basename(AppSrc, Ext) ++ ".appup.src",
  436. Default = {<<".*">>, []},
  437. AppupFileFullpath = filename:join(DirName, AppupFile),
  438. render_appfile(AppupFileFullpath, [Default], [Default]),
  439. {ok, AppupFileFullpath};
  440. undefined ->
  441. false
  442. end.
  443. %% we check whether the destination file already has the contents we
  444. %% want to write to avoid writing and losing indentation and comments.
  445. contains_contents(File, Upgrade, Downgrade) ->
  446. %% the file may contain the VSN variable, so it's a script
  447. case file:script(File, [{'VSN', 'VSN'}]) of
  448. {ok, {_, Upgrade, Downgrade}} ->
  449. true;
  450. _ ->
  451. false
  452. end.
  453. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  454. %% application and release indexing
  455. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  456. index_apps(ReleaseDir) ->
  457. Apps0 = maps:from_list([index_app(filename:join(ReleaseDir, AppFile)) ||
  458. AppFile <- filelib:wildcard("**/ebin/*.app", ReleaseDir)]),
  459. maps:without(ignored_apps(), Apps0).
  460. index_app(AppFile) ->
  461. {ok, [{application, App, Properties}]} = file:consult(AppFile),
  462. Vsn = proplists:get_value(vsn, Properties),
  463. %% Note: assuming that beams are always located in the same directory where app file is:
  464. EbinDir = filename:dirname(AppFile),
  465. Modules = hashsums(EbinDir),
  466. {App, #app{ version = Vsn
  467. , modules = Modules
  468. }}.
  469. diff_app(App,
  470. #app{version = NewVersion, modules = NewModules},
  471. #app{version = OldVersion, modules = OldModules}) ->
  472. {New, Changed} =
  473. maps:fold( fun(Mod, MD5, {New, Changed}) ->
  474. case OldModules of
  475. #{Mod := OldMD5} when MD5 =:= OldMD5 ->
  476. {New, Changed};
  477. #{Mod := _} ->
  478. {New, [Mod | Changed]};
  479. _ ->
  480. {[Mod | New], Changed}
  481. end
  482. end
  483. , {[], []}
  484. , NewModules
  485. ),
  486. Deleted = maps:keys(maps:without(maps:keys(NewModules), OldModules)),
  487. NChanges = length(New) + length(Changed) + length(Deleted),
  488. if NewVersion =:= OldVersion andalso NChanges > 0 ->
  489. set_invalid(),
  490. log("ERROR: Application '~p' contains changes, but its version is not updated~n", [App]);
  491. NewVersion > OldVersion ->
  492. log("INFO: Application '~p' has been updated: ~p -> ~p~n", [App, OldVersion, NewVersion]),
  493. ok;
  494. true ->
  495. ok
  496. end,
  497. {New, Changed, Deleted}.
  498. -spec hashsums(file:filename()) -> #{module() => binary()}.
  499. hashsums(EbinDir) ->
  500. maps:from_list(lists:map(
  501. fun(Beam) ->
  502. File = filename:join(EbinDir, Beam),
  503. {ok, Ret = {_Module, _MD5}} = beam_lib:md5(File),
  504. Ret
  505. end,
  506. filelib:wildcard("*.beam", EbinDir)
  507. )).
  508. is_app_external(App) ->
  509. Ext = ".app.src",
  510. case locate(src, App, Ext) of
  511. {ok, _} ->
  512. false;
  513. undefined ->
  514. true
  515. end.
  516. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  517. %% Global state
  518. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  519. init_globals(Options) ->
  520. ets:new(globals, [named_table, set, public]),
  521. ets:insert(globals, {valid, true}),
  522. ets:insert(globals, {options, Options}).
  523. getopt(Option) ->
  524. maps:get(Option, ets:lookup_element(globals, options, 2)).
  525. %% Set a global flag that something about the appfiles is invalid
  526. set_invalid() ->
  527. ets:insert(globals, {valid, false}).
  528. is_valid() ->
  529. ets:lookup_element(globals, valid, 2).
  530. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  531. %% Utility functions
  532. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  533. %% Locate a file in a specified application
  534. locate(ebin_current, App, Suffix) ->
  535. ReleaseDir = getopt(beams_dir),
  536. AppStr = atom_to_list(App),
  537. case filelib:wildcard(ReleaseDir ++ "/**/ebin/" ++ AppStr ++ Suffix) of
  538. [File] ->
  539. {ok, File};
  540. [] ->
  541. undefined
  542. end;
  543. locate(src, App, Suffix) ->
  544. AppStr = atom_to_list(App),
  545. SrcDirs = getopt(src_dirs),
  546. case filelib:wildcard(SrcDirs ++ AppStr ++ Suffix) of
  547. [File] ->
  548. {ok, File};
  549. [] ->
  550. undefined
  551. end.
  552. bash(Script) ->
  553. bash(Script, []).
  554. bash(Script, Env) ->
  555. log("+ ~s~n+ Env: ~p~n", [Script, Env]),
  556. case cmd("bash", #{args => ["-c", Script], env => Env}) of
  557. 0 -> true;
  558. _ -> fail("Failed to run command: ~s", [Script])
  559. end.
  560. %% Spawn an executable and return the exit status
  561. cmd(Exec, Params) ->
  562. case os:find_executable(Exec) of
  563. false ->
  564. fail("Executable not found in $PATH: ~s", [Exec]);
  565. Path ->
  566. Params1 = maps:to_list(maps:with([env, args, cd], Params)),
  567. Port = erlang:open_port( {spawn_executable, Path}
  568. , [ exit_status
  569. , nouse_stdio
  570. | Params1
  571. ]
  572. ),
  573. receive
  574. {Port, {exit_status, Status}} ->
  575. Status
  576. end
  577. end.
  578. fail(Str) ->
  579. fail(Str, []).
  580. fail(Str, Args) ->
  581. log(Str ++ "~n", Args),
  582. halt(1).
  583. log(Msg) ->
  584. log(Msg, []).
  585. log(Msg, Args) ->
  586. io:format(standard_error, Msg, Args).
  587. otp_standard_apps() ->
  588. [ssl, mnesia, kernel, asn1, stdlib].