check-elixir-applications.exs 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. #!/usr/bin/env elixir
  2. defmodule CheckElixirApplications do
  3. @default_applications [:kernel, :stdlib, :sasl]
  4. def main() do
  5. {:ok, _} = Application.ensure_all_started(:mix)
  6. inputs = read_inputs()
  7. # produce `rebar.config.rendered` to consult
  8. profile = profile_of(inputs)
  9. File.cwd!()
  10. |> Path.join("rebar3")
  11. |> System.cmd(["as", to_string(profile)],
  12. env: [{"DEBUG", "1"}]
  13. )
  14. File.cwd!()
  15. |> Path.join("mix.exs")
  16. |> Code.compile_file()
  17. mix_apps = mix_applications(inputs.release_type)
  18. rebar_apps = rebar_applications(profile)
  19. results = diff_apps(mix_apps, rebar_apps)
  20. report_discrepancy(
  21. results[:missing_apps],
  22. "* There are missing applications in the Elixir release",
  23. fn %{app: app, mode: mode, after: last_app} ->
  24. IO.puts(" * #{app}: #{inspect(mode)} should be placed after #{inspect(last_app)}")
  25. end
  26. )
  27. report_discrepancy(
  28. results[:different_modes],
  29. "* There are applications with different application modes in the Elixir release",
  30. fn %{app: app, rebar_mode: rebar_mode, mix_mode: mix_mode} ->
  31. IO.puts(
  32. " * #{inspect(app)} should have mode #{inspect(rebar_mode)}, but it has mode #{inspect(mix_mode)}"
  33. )
  34. end
  35. )
  36. report_discrepancy(
  37. results[:different_positions],
  38. "* There are applications in the Elixir release in the wrong order",
  39. fn %{app: app, mode: mode, after: last_app} ->
  40. IO.puts(" * #{app}: #{inspect(mode)} should be placed after #{inspect(last_app)}")
  41. end
  42. )
  43. success? =
  44. results
  45. |> Map.take([:missing_apps, :different_modes, :different_positions])
  46. |> Map.values()
  47. |> Enum.concat()
  48. |> Enum.empty?()
  49. if not success? do
  50. System.halt(1)
  51. else
  52. IO.puts(
  53. IO.ANSI.green() <>
  54. "Mix and Rebar applications OK!" <>
  55. IO.ANSI.reset()
  56. )
  57. end
  58. end
  59. defp mix_applications(release_type) do
  60. EMQXUmbrella.MixProject.applications(release_type)
  61. end
  62. defp rebar_applications(profile) do
  63. {:ok, props} =
  64. File.cwd!()
  65. |> Path.join("rebar.config.rendered")
  66. |> :file.consult()
  67. props[:profiles][profile][:relx]
  68. |> Enum.find(&(elem(&1, 0) == :release))
  69. |> elem(2)
  70. |> Enum.map(fn
  71. app when is_atom(app) ->
  72. {app, :permanent}
  73. {app, mode} ->
  74. {app, mode}
  75. end)
  76. |> Enum.reject(fn {app, _mode} ->
  77. # Elixir already includes those implicitly
  78. app in @default_applications
  79. end)
  80. end
  81. defp profile_of(%{
  82. release_type: release_type,
  83. package_type: package_type,
  84. edition_type: edition_type
  85. }) do
  86. case {release_type, package_type, edition_type} do
  87. {:cloud, :bin, :community} ->
  88. :emqx
  89. {:cloud, :pkg, :community} ->
  90. :"emqx-pkg"
  91. {:cloud, :bin, :enterprise} ->
  92. :"emqx-enterprise"
  93. {:cloud, :pkg, :enterprise} ->
  94. :"emqx-enterprise-pkg"
  95. {:edge, :bin, :community} ->
  96. :"emqx-edge"
  97. {:edge, :pkg, :community} ->
  98. :"emqx-edge-pkg"
  99. end
  100. end
  101. defp read_inputs() do
  102. release_type =
  103. read_enum_env_var(
  104. "EMQX_RELEASE_TYPE",
  105. [:cloud, :edge],
  106. :cloud
  107. )
  108. package_type =
  109. read_enum_env_var(
  110. "EMQX_PACKAGE_TYPE",
  111. [:bin, :pkg],
  112. :bin
  113. )
  114. edition_type =
  115. read_enum_env_var(
  116. "EMQX_EDITION_TYPE",
  117. [:community, :enterprise],
  118. :community
  119. )
  120. %{
  121. release_type: release_type,
  122. package_type: package_type,
  123. edition_type: edition_type
  124. }
  125. end
  126. defp read_enum_env_var(env_var, allowed_values, default_value) do
  127. case System.fetch_env(env_var) do
  128. :error ->
  129. default_value
  130. {:ok, raw_value} ->
  131. value =
  132. raw_value
  133. |> String.downcase()
  134. |> String.to_atom()
  135. if value not in allowed_values do
  136. Mix.raise("""
  137. Invalid value #{raw_value} for variable #{env_var}.
  138. Allowed values are: #{inspect(allowed_values)}
  139. """)
  140. end
  141. value
  142. end
  143. end
  144. defp diff_apps(mix_apps, rebar_apps) do
  145. app_names = Keyword.keys(rebar_apps)
  146. mix_apps = Keyword.filter(mix_apps, fn {app, _mode} -> app in app_names end)
  147. acc = %{
  148. mix_apps: mix_apps,
  149. missing_apps: [],
  150. different_positions: [],
  151. different_modes: [],
  152. last_app: nil
  153. }
  154. Enum.reduce(
  155. rebar_apps,
  156. acc,
  157. fn
  158. {rebar_app, rebar_mode}, acc = %{mix_apps: [], last_app: last_app} ->
  159. missing_app = %{
  160. app: rebar_app,
  161. mode: rebar_mode,
  162. after: last_app
  163. }
  164. acc
  165. |> Map.update!(:missing_apps, &[missing_app | &1])
  166. |> Map.put(:last_app, rebar_app)
  167. {rebar_app, rebar_mode},
  168. acc = %{mix_apps: [{mix_app, mix_mode} | rest], last_app: last_app} ->
  169. case {rebar_app, rebar_mode} do
  170. {^mix_app, ^mix_mode} ->
  171. acc
  172. |> Map.put(:mix_apps, rest)
  173. |> Map.put(:last_app, rebar_app)
  174. {^mix_app, _mode} ->
  175. different_mode = %{
  176. app: rebar_app,
  177. rebar_mode: rebar_mode,
  178. mix_mode: mix_mode
  179. }
  180. acc
  181. |> Map.put(:mix_apps, rest)
  182. |> Map.update!(:different_modes, &[different_mode | &1])
  183. |> Map.put(:last_app, rebar_app)
  184. {_app, _mode} ->
  185. case Keyword.pop(rest, rebar_app) do
  186. {nil, _} ->
  187. missing_app = %{
  188. app: rebar_app,
  189. mode: rebar_mode,
  190. after: last_app
  191. }
  192. acc
  193. |> Map.update!(:missing_apps, &[missing_app | &1])
  194. |> Map.put(:last_app, rebar_app)
  195. {^rebar_mode, rest} ->
  196. different_position = %{
  197. app: rebar_app,
  198. mode: rebar_mode,
  199. after: last_app
  200. }
  201. acc
  202. |> Map.update!(:different_positions, &[different_position | &1])
  203. |> Map.put(:last_app, rebar_app)
  204. |> Map.put(:mix_apps, [{mix_app, mix_mode} | rest])
  205. {mode, rest} ->
  206. different_mode = %{
  207. app: rebar_app,
  208. rebar_mode: rebar_mode,
  209. mix_mode: mode
  210. }
  211. different_position = %{
  212. app: rebar_app,
  213. mode: rebar_mode,
  214. after: last_app
  215. }
  216. acc
  217. |> Map.put(:mix_apps, [{mix_app, mix_mode} | rest])
  218. |> Map.update!(:different_modes, &[different_mode | &1])
  219. |> Map.update!(:different_positions, &[different_position | &1])
  220. |> Map.put(:last_app, rebar_app)
  221. end
  222. end
  223. end
  224. )
  225. end
  226. defp report_discrepancy(diffs, header, line_fn) do
  227. unless Enum.empty?(diffs) do
  228. IO.puts(IO.ANSI.red() <> header)
  229. diffs
  230. |> Enum.reverse()
  231. |> Enum.each(line_fn)
  232. IO.puts(IO.ANSI.reset())
  233. end
  234. end
  235. end
  236. CheckElixirApplications.main()