inject-deps.escript 4.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141
  1. #!/usr/bin/env escript
  2. %% This script injects implicit relup dependencies for emqx applications.
  3. %%
  4. %% By 'implicit', it means that it is not feasible to define application
  5. %% dependencies in .app.src files.
  6. %%
  7. %% For instance, during upgrade/downgrade, emqx_dashboard usually requires
  8. %% a restart after (but not before) all plugins are upgraded (and maybe
  9. %% restarted), however, the dependencies are not resolvable at build time
  10. %% when relup is generated.
  11. %%
  12. %% This script is to be executed after compile, with the profile given as the
  13. %% first argument. For each dependency, it modifies the .app file to
  14. %% have the `relup_deps` list extended to application attributes.
  15. %%
  16. %% The `relup_deps` application attribute is then picked up by (EMQ's fork of)
  17. %% `relx` when top-sorting apps to generate relup instructions
  18. -mode(compile).
  19. usage() ->
  20. "Usage: " ++ escript:script_name() ++ " emqx|emqx-edge".
  21. -type app() :: atom().
  22. -type deps_overlay() :: {re, string()} | app().
  23. %% deps/0 returns the dependency overlays.
  24. %% {re, Pattern} to match application names using regexp pattern
  25. -spec deps(string()) -> [{app(), [deps_overlay()]}].
  26. deps("emqx-edge" ++ _) ->
  27. %% special case for edge
  28. base_deps() ++ [{{re, ".+"}, [{exclude, emqx_reloader}]}];
  29. deps(_Profile) ->
  30. base_deps().
  31. base_deps() ->
  32. [ {emqx_dashboard, [{re, "emqx_.*"}]}
  33. , {emqx_management, [{re, "emqx_.*"}, {exclude, emqx_dashboard}]}
  34. , {{re, "emqx_.*"}, [emqx]}
  35. ].
  36. main([Profile | _]) ->
  37. ok = inject(Profile);
  38. main(_Args) ->
  39. io:format(standard_error, "~s", [usage()]),
  40. erlang:halt(1).
  41. expand_names({Name, Deps}, AppNames) ->
  42. Names = match_pattern(Name, AppNames),
  43. [{N, Deps} || N <- Names].
  44. %% merge k-v pairs with v1 ++ v2
  45. merge([], Acc) -> Acc;
  46. merge([{K, V0} | Rest], Acc) ->
  47. V = case lists:keyfind(K, 1, Acc) of
  48. {K, V1} -> V1 ++ V0;
  49. false -> V0
  50. end,
  51. NewAcc = lists:keystore(K, 1, Acc, {K, V}),
  52. merge(Rest, NewAcc).
  53. expand_deps([], _AppNames, Acc) -> Acc;
  54. expand_deps([{exclude, Dep} | Deps], AppNames, Acc) ->
  55. Matches = expand_deps([Dep], AppNames, []),
  56. expand_deps(Deps, AppNames, Acc -- Matches);
  57. expand_deps([Dep | Deps], AppNames, Acc) ->
  58. NewAcc = add_to_list(Acc, match_pattern(Dep, AppNames)),
  59. expand_deps(Deps, AppNames, NewAcc).
  60. inject(Profile) ->
  61. LibDir = lib_dir(Profile),
  62. AppNames = list_apps(LibDir),
  63. Deps0 = lists:flatmap(fun(Dep) -> expand_names(Dep, AppNames) end, deps(Profile)),
  64. Deps1 = merge(Deps0, []),
  65. Deps2 = lists:map(fun({Name, DepsX}) ->
  66. NewDeps = expand_deps(DepsX, AppNames, []),
  67. {Name, NewDeps}
  68. end, Deps1),
  69. lists:foreach(fun({App, Deps}) -> inject(App, Deps, LibDir) end, Deps2).
  70. %% list the profile/lib dir to get all apps
  71. list_apps(LibDir) ->
  72. Apps = filelib:wildcard("*", LibDir),
  73. lists:foldl(fun(App, Acc) -> [App || is_app(LibDir, App)] ++ Acc end, [], Apps).
  74. is_app(_LibDir, "." ++ _) -> false; %% ignore hidden dir
  75. is_app(LibDir, AppName) ->
  76. filelib:is_regular(filename:join([ebin_dir(LibDir, AppName), AppName ++ ".app"])) orelse
  77. error({unknown_app, AppName}). %% wtf
  78. lib_dir(Profile) ->
  79. filename:join(["_build", Profile, lib]).
  80. ebin_dir(LibDir, AppName) -> filename:join([LibDir, AppName, "ebin"]).
  81. inject(App0, DepsToAdd, LibDir) ->
  82. App = str(App0),
  83. AppEbinDir = ebin_dir(LibDir, App),
  84. [AppFile0] = filelib:wildcard("*.app", AppEbinDir),
  85. AppFile = filename:join(AppEbinDir, AppFile0),
  86. {ok, [{application, AppName, Props}]} = file:consult(AppFile),
  87. Deps0 = case lists:keyfind(relup_deps, 1, Props) of
  88. {_, X} -> X;
  89. false -> []
  90. end,
  91. %% merge extra deps, but do not self-include
  92. Deps = add_to_list(Deps0, DepsToAdd) -- [App0],
  93. case Deps =:= [] of
  94. true -> ok;
  95. _ ->
  96. NewProps = lists:keystore(relup_deps, 1, Props, {relup_deps, Deps}),
  97. AppSpec = {application, AppName, NewProps},
  98. AppSpecIoData = io_lib:format("~p.", [AppSpec]),
  99. io:format(user, "updated_relup_deps for ~p~n", [App]),
  100. file:write_file(AppFile, AppSpecIoData)
  101. end.
  102. str(A) when is_atom(A) -> atom_to_list(A).
  103. match_pattern({re, Re}, AppNames) ->
  104. Match = fun(AppName) -> re:run(AppName, Re) =/= nomatch end,
  105. AppNamesToAdd = lists:filter(Match, AppNames),
  106. AppsToAdd = lists:map(fun(N) -> list_to_atom(N) end, AppNamesToAdd),
  107. case AppsToAdd =:= [] of
  108. true -> error({nomatch, Re});
  109. false -> AppsToAdd
  110. end;
  111. match_pattern(NameAtom, AppNames) ->
  112. case lists:member(str(NameAtom), AppNames) of
  113. true -> [NameAtom];
  114. false -> error({notfound, NameAtom})
  115. end.
  116. %% Append elements to list without duplication. No reordering.
  117. add_to_list(List, []) -> List;
  118. add_to_list(List, [H | T]) ->
  119. case lists:member(H, List) of
  120. true -> add_to_list(List, T);
  121. false -> add_to_list(List ++ [H], T)
  122. end.