update_appup.escript 26 KB

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