update_appup.escript 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724
  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 Upsteam 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. --prev-release-dir Previous version's release dir (if already built/extracted)
  26. --release-dir Release directory
  27. --src-dirs Directories where source code is found. Defaults to '{src,apps,lib-*}/**/'
  28. ".
  29. -record(app,
  30. { modules :: #{module() => binary()}
  31. , version :: string()
  32. }).
  33. default_options() ->
  34. #{ clone_url => find_upstream_repo("origin")
  35. , make_command => "make emqx-rel"
  36. , beams_dir => "_build/emqx/rel/emqx/lib/"
  37. , check => false
  38. , prev_tag => undefined
  39. , src_dirs => "{src,apps,lib-*}/**/"
  40. , prev_beams_dir => undefined
  41. }.
  42. %% App-specific actions that should be added unconditionally to any update/downgrade:
  43. app_specific_actions(_) ->
  44. [].
  45. ignored_apps() ->
  46. [gpb %% only a build tool
  47. ] ++ 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 => undefined});
  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(["--prev-release-dir", Dir|Rest], State) ->
  67. parse_args(Rest, State#{prev_beams_dir => Dir});
  68. parse_args(["--src-dirs", Pattern|Rest], State) ->
  69. parse_args(Rest, State#{src_dirs => Pattern});
  70. parse_args(_, _) ->
  71. fail(usage()).
  72. main(Options, Baseline) ->
  73. {CurrRelDir, PrevRelDir} = prepare(Baseline, Options),
  74. putopt(prev_beams_dir, PrevRelDir),
  75. log("~n===================================~n"
  76. "Processing changes..."
  77. "~n===================================~n"),
  78. CurrAppsIdx = index_apps(CurrRelDir),
  79. PrevAppsIdx = index_apps(PrevRelDir),
  80. %% log("Curr: ~p~nPrev: ~p~n", [CurrAppsIdx, PrevAppsIdx]),
  81. AppupChanges = find_appup_actions(CurrAppsIdx, PrevAppsIdx),
  82. ok = update_appups(AppupChanges),
  83. ok = check_appup_files(),
  84. ok = warn_and_exit(is_valid()).
  85. warn_and_exit(true) ->
  86. log("
  87. NOTE: Please review the changes manually. This script does not know about NIF
  88. changes, supervisor changes, process restarts and so on. Also the load order of
  89. the beam files might need updating.~n"),
  90. halt(0);
  91. warn_and_exit(false) ->
  92. log("~nERROR: Incomplete appups found. Please inspect the output for more details.~n"),
  93. halt(1).
  94. prepare(Baseline, Options = #{make_command := MakeCommand, beams_dir := BeamDir}) ->
  95. log("~n===================================~n"
  96. "Baseline: ~s"
  97. "~n===================================~n", [Baseline]),
  98. log("Building the current version...~n"),
  99. ok = bash(MakeCommand),
  100. PrevRelDir =
  101. case maps:get(prev_beams_dir, Options, undefined) of
  102. undefined ->
  103. log("Building the previous release...~n"),
  104. {ok, PrevRootDir} = build_prev_release(Baseline, Options),
  105. filename:join(PrevRootDir, BeamDir);
  106. Dir ->
  107. %% already built
  108. Dir
  109. end,
  110. {BeamDir, PrevRelDir}.
  111. build_prev_release(Baseline, #{clone_url := Repo, make_command := MakeCommand}) ->
  112. BaseDir = "/tmp/emqx-appup-base/",
  113. Dir = filename:basename(Repo, ".git") ++ [$-|Baseline],
  114. Script = "mkdir -p ${BASEDIR} &&
  115. cd ${BASEDIR} &&
  116. { [ -d ${DIR} ] || git clone --depth 1 --branch ${TAG} ${REPO} ${DIR}; } &&
  117. cd ${DIR} &&" ++ MakeCommand,
  118. Env = [{"REPO", Repo}, {"TAG", Baseline}, {"BASEDIR", BaseDir}, {"DIR", Dir}],
  119. ok = bash(Script, Env),
  120. {ok, filename:join([BaseDir, Dir, "_build/*/lib"])}.
  121. find_upstream_repo(Remote) ->
  122. string:trim(os:cmd("git remote get-url " ++ Remote)).
  123. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  124. %% Appup action creation and updating
  125. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  126. find_appup_actions(CurrApps, PrevApps) ->
  127. maps:fold(
  128. fun(App, CurrAppIdx, Acc) ->
  129. case PrevApps of
  130. #{App := PrevAppIdx} ->
  131. find_appup_actions(App, CurrAppIdx, PrevAppIdx) ++ Acc;
  132. _ ->
  133. %% New app, nothing to upgrade here.
  134. Acc
  135. end
  136. end,
  137. [],
  138. CurrApps).
  139. find_appup_actions(_App, AppIdx, AppIdx) ->
  140. %% No changes to the app, ignore:
  141. [];
  142. find_appup_actions(App,
  143. CurrAppIdx = #app{version = CurrVersion},
  144. PrevAppIdx = #app{version = PrevVersion}) ->
  145. {OldUpgrade0, OldDowngrade0} = find_base_appup_actions(App, PrevVersion),
  146. OldUpgrade = ensure_all_patch_versions(App, CurrVersion, OldUpgrade0),
  147. OldDowngrade = ensure_all_patch_versions(App, CurrVersion, OldDowngrade0),
  148. UpDiff = diff_app(up, App, CurrAppIdx, PrevAppIdx),
  149. DownDiff = diff_app(down, App, PrevAppIdx, CurrAppIdx),
  150. Upgrade = merge_update_actions(App, UpDiff, OldUpgrade, PrevVersion),
  151. Downgrade = merge_update_actions(App, DownDiff, OldDowngrade, PrevVersion),
  152. case OldUpgrade =:= Upgrade andalso OldDowngrade =:= Downgrade of
  153. true -> [];
  154. false -> [{App, {Upgrade, Downgrade, OldUpgrade, OldDowngrade}}]
  155. end.
  156. %% To avoid missing one patch version when upgrading, we try to
  157. %% optimistically generate the list of expected versions that should
  158. %% be covered by the upgrade.
  159. ensure_all_patch_versions(App, CurrVsn, OldActions) ->
  160. case is_app_external(App) of
  161. true ->
  162. %% we do not attempt to predict the version list for
  163. %% external dependencies, as those may not follow our
  164. %% conventions.
  165. OldActions;
  166. false ->
  167. do_ensure_all_patch_versions(App, CurrVsn, OldActions)
  168. end.
  169. do_ensure_all_patch_versions(App, CurrVsn, OldActions) ->
  170. case enumerate_past_versions(CurrVsn) of
  171. {ok, ExpectedVsns} ->
  172. CoveredVsns = [V || {V, _} <- OldActions, V =/= <<".*">>],
  173. ExpectedVsnStrs = [vsn_number_to_string(V) || V <- ExpectedVsns],
  174. MissingActions = [{V, []} || V <- ExpectedVsnStrs, not contains_version(V, CoveredVsns)],
  175. MissingActions ++ OldActions;
  176. {error, bad_version} ->
  177. log("WARN: Could not infer expected versions to upgrade from for ~p~n", [App]),
  178. OldActions
  179. end.
  180. %% For external dependencies, show only the changes that are missing
  181. %% in their current appup.
  182. diff_appup_instructions(ComputedChanges, PresentChanges) ->
  183. lists:foldr(
  184. fun({VsnOrRegex, ComputedActions}, Acc) ->
  185. case find_matching_version(VsnOrRegex, PresentChanges) of
  186. undefined ->
  187. [{VsnOrRegex, ComputedActions} | Acc];
  188. PresentActions ->
  189. DiffActions = ComputedActions -- PresentActions,
  190. case DiffActions of
  191. [] ->
  192. %% no diff
  193. Acc;
  194. _ ->
  195. [{VsnOrRegex, DiffActions} | Acc]
  196. end
  197. end
  198. end,
  199. [],
  200. ComputedChanges).
  201. %% checks if any missing diffs are present
  202. %% and groups them by `up' and `down' types.
  203. parse_appup_diffs(Upgrade, OldUpgrade, Downgrade, OldDowngrade) ->
  204. DiffUp = diff_appup_instructions(Upgrade, OldUpgrade),
  205. DiffDown = diff_appup_instructions(Downgrade, OldDowngrade),
  206. case {DiffUp, DiffDown} of
  207. {[], []} ->
  208. %% no diff for external dependency; ignore
  209. ok;
  210. _ ->
  211. Diffs = #{ up => DiffUp
  212. , down => DiffDown
  213. },
  214. {diffs, Diffs}
  215. end.
  216. %% TODO: handle regexes
  217. %% Since the first argument may be a regex itself, we would need to
  218. %% check if it is "contained" within other regexes inside list of
  219. %% versions in the second argument.
  220. find_matching_version(VsnOrRegex, PresentChanges) ->
  221. proplists:get_value(VsnOrRegex, PresentChanges).
  222. find_base_appup_actions(App, PrevVersion) ->
  223. {Upgrade, Downgrade} =
  224. case locate_appup(App) of
  225. {ok, AppupSrcFile} ->
  226. log("INFO: Using ~s as a source of previous update actions~n", [AppupSrcFile]),
  227. read_appup(AppupSrcFile);
  228. undefined ->
  229. log("INFO: no appup base found for ~p~n", [App]),
  230. {[], []}
  231. end,
  232. {ensure_version(PrevVersion, Upgrade), ensure_version(PrevVersion, Downgrade)}.
  233. merge_update_actions(App, Changes, Vsns, PrevVersion) ->
  234. lists:map(fun(Ret = {<<".*">>, _}) ->
  235. Ret;
  236. ({Vsn, Actions}) ->
  237. case is_skipped_version(App, Vsn, PrevVersion) of
  238. true ->
  239. log("WARN: ~p has version ~s skipped over?~n", [App, Vsn]),
  240. {Vsn, Actions};
  241. false ->
  242. {Vsn, do_merge_update_actions(App, Changes, Actions)}
  243. end
  244. end,
  245. Vsns).
  246. %% say current version is 1.1.3, and the compare base is version 1.1.1,
  247. %% but there is a 1.1.2 in appup we may skip merging instructions for
  248. %% 1.1.2 because it's not used and no way to know what has been changed
  249. is_skipped_version(App, Vsn, PrevVersion) when is_list(Vsn) andalso is_list(PrevVersion) ->
  250. case is_app_external(App) andalso parse_version_number(Vsn) of
  251. {ok, VsnTuple} ->
  252. case parse_version_number(PrevVersion) of
  253. {ok, PrevVsnTuple} ->
  254. VsnTuple > PrevVsnTuple;
  255. _ ->
  256. false
  257. end;
  258. _ ->
  259. false
  260. end;
  261. is_skipped_version(_App, _Vsn, _PrevVersion) ->
  262. %% if app version is a regexp, we don't know for sure
  263. %% return 'false' to be on the safe side
  264. false.
  265. do_merge_update_actions(App, {New0, Changed0, Deleted0}, OldActions) ->
  266. AppSpecific = app_specific_actions(App) -- OldActions,
  267. AlreadyHandled = lists:flatten(lists:map(fun process_old_action/1, OldActions)),
  268. New = New0 -- AlreadyHandled,
  269. Changed = Changed0 -- AlreadyHandled,
  270. Deleted = Deleted0 -- AlreadyHandled,
  271. HasRestart = contains_restart_application(App, OldActions),
  272. Actions =
  273. case HasRestart of
  274. true ->
  275. [];
  276. false ->
  277. [{load_module, M, brutal_purge, soft_purge, []} || M <- Changed] ++
  278. [{add_module, M} || M <- New]
  279. end,
  280. {OldActionsWithStop, OldActionsAfterStop} =
  281. find_application_stop_instruction(App, OldActions),
  282. OldActionsWithStop ++
  283. Actions ++
  284. OldActionsAfterStop ++
  285. case HasRestart of
  286. true ->
  287. [];
  288. false ->
  289. [{delete_module, M} || M <- Deleted]
  290. end ++
  291. AppSpecific.
  292. %% If an entry restarts an application, there's no need to use
  293. %% `load_module' instructions.
  294. contains_restart_application(Application, Actions) ->
  295. lists:member({restart_application, Application}, Actions).
  296. %% If there is an `application:stop(Application)' call in the
  297. %% instructions, we insert `load_module' instructions after it.
  298. find_application_stop_instruction(Application, Actions) ->
  299. {Before, After0} =
  300. lists:splitwith(
  301. fun({apply, {application, stop, [App]}}) when App =:= Application ->
  302. false;
  303. (_) ->
  304. true
  305. end, Actions),
  306. case After0 of
  307. [StopInst | After] ->
  308. {Before ++ [StopInst], After};
  309. [] ->
  310. {[], Before}
  311. end.
  312. %% @doc Process the existing actions to exclude modules that are
  313. %% already handled
  314. process_old_action({purge, Modules}) ->
  315. Modules;
  316. process_old_action({add_module, Module}) ->
  317. [Module];
  318. process_old_action({delete_module, Module}) ->
  319. [Module];
  320. process_old_action({update, Module, _Change}) ->
  321. [Module];
  322. process_old_action(LoadModule) when is_tuple(LoadModule) andalso
  323. element(1, LoadModule) =:= load_module ->
  324. element(2, LoadModule);
  325. process_old_action(_) ->
  326. [].
  327. ensure_version(Version, OldInstructions) ->
  328. OldVersions = [element(1, I) || I <- OldInstructions],
  329. case contains_version(Version, OldVersions) of
  330. false ->
  331. [{Version, []} | OldInstructions];
  332. true ->
  333. OldInstructions
  334. end.
  335. contains_version(Needle, Haystack) when is_list(Needle) ->
  336. lists:any(
  337. fun(Regex) when is_binary(Regex) ->
  338. case re:run(Needle, Regex) of
  339. {match, _} ->
  340. true;
  341. nomatch ->
  342. false
  343. end;
  344. (Vsn) ->
  345. Vsn =:= Needle
  346. end,
  347. Haystack).
  348. %% As a best effort approach, we assume that we only bump patch
  349. %% version numbers between release upgrades for our dependencies and
  350. %% that we deal only with 3-part version schemas
  351. %% (`Major.Minor.Patch'). Using those assumptions, we enumerate the
  352. %% past versions that should be covered by regexes in .appup file
  353. %% instructions.
  354. enumerate_past_versions(Vsn) when is_list(Vsn) ->
  355. case parse_version_number(Vsn) of
  356. {ok, ParsedVsn} ->
  357. {ok, enumerate_past_versions(ParsedVsn)};
  358. Error ->
  359. Error
  360. end;
  361. enumerate_past_versions({Major, Minor, Patch}) ->
  362. [{Major, Minor, P} || P <- lists:seq(Patch - 1, 0, -1)].
  363. parse_version_number(Vsn) when is_list(Vsn) ->
  364. Nums = string:split(Vsn, ".", all),
  365. Results = lists:map(fun string:to_integer/1, Nums),
  366. case Results of
  367. [{Major, []}, {Minor, []}, {Patch, []}] ->
  368. {ok, {Major, Minor, Patch}};
  369. _ ->
  370. {error, bad_version}
  371. end.
  372. vsn_number_to_string({Major, Minor, Patch}) ->
  373. io_lib:format("~b.~b.~b", [Major, Minor, Patch]).
  374. read_appup(File) ->
  375. %% NOTE: appup file is a script, it may contain variables or functions.
  376. case do_read_appup(File) of
  377. {ok, {U, D}} -> {U, D};
  378. {error, Reason} -> fail("Failed to parse appup file ~p~n~p", [File, Reason])
  379. end.
  380. do_read_appup(File) ->
  381. case file:script(File, [{'VSN', "VSN"}]) of
  382. {ok, {_, U, D}} ->
  383. {ok, {U, D}};
  384. {ok, Other} ->
  385. {error, {bad_appup_format, Other}};
  386. {error, Reason} ->
  387. {error, Reason}
  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_current_src(App, ".appup.src") of
  400. {ok, AppupFile} ->
  401. case contains_contents(AppupFile, Upgrade, Downgrade) of
  402. true ->
  403. ok;
  404. false ->
  405. render_appup(App, AppupFile, Upgrade, Downgrade)
  406. end;
  407. undefined ->
  408. maybe_create_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade)
  409. end.
  410. maybe_create_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade) ->
  411. case create_stub(App) of
  412. {ok, AppupFile} ->
  413. render_appup(App, AppupFile, Upgrade, Downgrade);
  414. external ->
  415. %% for external appup, the best we can do is to validate it
  416. _ = check_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade),
  417. ok
  418. end.
  419. check_appup(App, Upgrade, Downgrade, OldUpgrade, OldDowngrade) ->
  420. case parse_appup_diffs(Upgrade, OldUpgrade, Downgrade, OldDowngrade) of
  421. ok ->
  422. %% no diff for external dependency; ignore
  423. ok;
  424. {diffs, Diffs} ->
  425. set_invalid(),
  426. log("ERROR: Appup file for '~p' is not complete.~n"
  427. "Missing:~100p~n", [App, Diffs]),
  428. notok
  429. end.
  430. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  431. %% Appup file creation
  432. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  433. render_appup(App, File, Up, Down) ->
  434. case getopt(check) of
  435. true ->
  436. case do_read_appup(File) of
  437. {ok, {U, D}} when U =:= Up andalso D =:= Down ->
  438. ok;
  439. {ok, {OldU, OldD}} ->
  440. check_appup(App, Up, Down, OldU, OldD);
  441. {error, enoent} ->
  442. %% failed to read old file, exit
  443. log("ERROR: ~s is missing", [File]),
  444. set_invalid()
  445. end;
  446. false ->
  447. do_render_appup(File, Up, Down)
  448. end.
  449. do_render_appup(File, Up, Down) ->
  450. IOList = io_lib:format("%% -*- mode: erlang -*-~n"
  451. "%% Unless you know what you are doing, DO NOT edit manually!!~n"
  452. "{VSN,~n ~p,~n ~p}.~n", [Up, Down]),
  453. ok = file:write_file(File, IOList).
  454. create_stub(App) ->
  455. Ext = ".app.src",
  456. case locate_current_src(App, Ext) of
  457. {ok, AppSrc} ->
  458. DirName = filename:dirname(AppSrc),
  459. AppupFile = filename:basename(AppSrc, Ext) ++ ".appup.src",
  460. Default = {<<".*">>, []},
  461. AppupFileFullpath = filename:join(DirName, AppupFile),
  462. render_appup(App, AppupFileFullpath, [Default], [Default]),
  463. {ok, AppupFileFullpath};
  464. undefined ->
  465. external
  466. end.
  467. %% we check whether the destination file already has the contents we
  468. %% want to write to avoid writing and losing indentation and comments.
  469. contains_contents(File, Upgrade, Downgrade) ->
  470. %% the file may contain the VSN variable, so it's a script
  471. case file:script(File, [{'VSN', 'VSN'}]) of
  472. {ok, {_, Upgrade, Downgrade}} ->
  473. true;
  474. _ ->
  475. false
  476. end.
  477. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  478. %% application and release indexing
  479. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  480. index_apps(ReleaseDir) ->
  481. log("INFO: indexing apps in ~s~n", [ReleaseDir]),
  482. AppFiles0 = filelib:wildcard("**/ebin/*.app", ReleaseDir),
  483. %% everything in _build sub-dir e.g. cuttlefish/_build should be ignored
  484. AppFiles = lists:filter(fun(File) -> re:run(File, "_build") =:= nomatch end, AppFiles0),
  485. Apps0 = maps:from_list([index_app(filename:join(ReleaseDir, AppFile)) || AppFile <- AppFiles]),
  486. maps:without(ignored_apps(), Apps0).
  487. index_app(AppFile) ->
  488. {ok, [{application, App, Properties}]} = file:consult(AppFile),
  489. Vsn = proplists:get_value(vsn, Properties),
  490. %% Note: assuming that beams are always located in the same directory where app file is:
  491. EbinDir = filename:dirname(AppFile),
  492. Modules = hashsums(EbinDir),
  493. {App, #app{ version = Vsn
  494. , modules = Modules
  495. }}.
  496. diff_app(UpOrDown, App,
  497. #app{version = NewVersion, modules = NewModules},
  498. #app{version = OldVersion, modules = OldModules}) ->
  499. {New, Changed} =
  500. maps:fold( fun(Mod, MD5, {New, Changed}) ->
  501. case OldModules of
  502. #{Mod := OldMD5} when MD5 =:= OldMD5 ->
  503. {New, Changed};
  504. #{Mod := _} ->
  505. {New, [Mod | Changed]};
  506. _ ->
  507. {[Mod | New], Changed}
  508. end
  509. end
  510. , {[], []}
  511. , NewModules
  512. ),
  513. Deleted = maps:keys(maps:without(maps:keys(NewModules), OldModules)),
  514. Changes = lists:filter(fun({_T, L}) -> length(L) > 0 end,
  515. [{added, New}, {changed, Changed}, {deleted, Deleted}]),
  516. case NewVersion =:= OldVersion of
  517. true when Changes =:= [] ->
  518. %% no change
  519. ok;
  520. true ->
  521. set_invalid(),
  522. case UpOrDown =:= up of
  523. true ->
  524. %% only log for the upgrade case because it would be the same result
  525. log("ERROR: Application '~p' contains changes, but its version is not updated. ~s",
  526. [App, format_changes(Changes)]);
  527. false ->
  528. ok
  529. end;
  530. false ->
  531. log("INFO: Application '~p' has been updated: ~p --[~p]--> ~p~n", [App, OldVersion, UpOrDown, NewVersion]),
  532. log("INFO: changes [~p]: ~p~n", [UpOrDown, Changes]),
  533. ok
  534. end,
  535. {New, Changed, Deleted}.
  536. format_changes(Changes) ->
  537. lists:map(fun({Tag, List}) -> io_lib:format("~p: ~p~n", [Tag, List]) end, Changes).
  538. -spec hashsums(file:filename()) -> #{module() => binary()}.
  539. hashsums(EbinDir) ->
  540. maps:from_list(lists:map(
  541. fun(Beam) ->
  542. File = filename:join(EbinDir, Beam),
  543. {ok, Ret = {_Module, _MD5}} = beam_lib:md5(File),
  544. Ret
  545. end,
  546. filelib:wildcard("*.beam", EbinDir)
  547. )).
  548. is_app_external(App) ->
  549. Ext = ".app.src",
  550. case locate_current_src(App, Ext) of
  551. {ok, _} ->
  552. false;
  553. undefined ->
  554. true
  555. end.
  556. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  557. %% Global state
  558. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  559. init_globals(Options) ->
  560. ets:new(globals, [named_table, set, public]),
  561. ets:insert(globals, {valid, true}),
  562. ets:insert(globals, {options, Options}).
  563. putopt(Option, Value) ->
  564. ets:insert(globals, {{option, Option}, Value}).
  565. getopt(Option) ->
  566. case ets:lookup(globals, {option, Option}) of
  567. [] ->
  568. maps:get(Option, ets:lookup_element(globals, options, 2));
  569. [{_, V}] ->
  570. V
  571. end.
  572. %% Set a global flag that something about the appfiles is invalid
  573. set_invalid() ->
  574. ets:insert(globals, {valid, false}).
  575. is_valid() ->
  576. ets:lookup_element(globals, valid, 2).
  577. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  578. %% Utility functions
  579. %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
  580. locate_appup(App) ->
  581. case locate_current_rel(App, ".appup.src") of
  582. {ok, File} ->
  583. {ok, File};
  584. undefined ->
  585. %% fallback to .appup
  586. locate_current_rel(App, ".appup")
  587. end.
  588. locate_current_rel(App, Suffix) ->
  589. CurDir = getopt(beams_dir),
  590. do_locate(filename:join([CurDir, "**"]), App, Suffix).
  591. %% Locate a file in a specified application
  592. locate_current_src(App, Suffix) ->
  593. SrcDirs = getopt(src_dirs),
  594. do_locate(SrcDirs, App, Suffix).
  595. do_locate(Dir, App, Suffix) ->
  596. AppStr = atom_to_list(App),
  597. Pattern = filename:join(Dir, AppStr ++ Suffix),
  598. case find_app(Pattern) of
  599. [File] ->
  600. {ok, File};
  601. [] ->
  602. undefined;
  603. Files ->
  604. error({more_than_one_app_found, Files})
  605. end.
  606. find_app(Pattern) ->
  607. lists:filter(fun(D) -> re:run(D, "apps/.*/_build") =:= nomatch end,
  608. filelib:wildcard(Pattern)).
  609. bash(undefined) -> ok;
  610. bash(Script) ->
  611. bash(Script, []).
  612. bash(Script, Env) ->
  613. log("+ ~s~n+ Env: ~p~n", [Script, Env]),
  614. case cmd("bash", #{args => ["-c", Script], env => Env}) of
  615. 0 -> ok;
  616. _ -> fail("Failed to run command: ~s", [Script])
  617. end.
  618. %% Spawn an executable and return the exit status
  619. cmd(Exec, Params) ->
  620. case os:find_executable(Exec) of
  621. false ->
  622. fail("Executable not found in $PATH: ~s", [Exec]);
  623. Path ->
  624. Params1 = maps:to_list(maps:with([env, args, cd], Params)),
  625. Port = erlang:open_port( {spawn_executable, Path}
  626. , [ exit_status
  627. , nouse_stdio
  628. | Params1
  629. ]
  630. ),
  631. receive
  632. {Port, {exit_status, Status}} ->
  633. Status
  634. end
  635. end.
  636. fail(Str) ->
  637. fail(Str, []).
  638. fail(Str, Args) ->
  639. log(Str ++ "~n", Args),
  640. halt(1).
  641. log(Msg) ->
  642. log(Msg, []).
  643. log(Msg, Args) ->
  644. io:format(standard_error, Msg, Args).
  645. otp_standard_apps() ->
  646. [ssl, mnesia, kernel, asn1, stdlib].