compile.grpc.ex 6.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230
  1. defmodule Mix.Tasks.Compile.Grpc do
  2. use Mix.Task.Compiler
  3. alias EMQXUmbrella.MixProject, as: UMP
  4. @recursive true
  5. @manifest_vsn 1
  6. @manifest "compile.grpc"
  7. # TODO: use manifest to track generated files?
  8. @impl true
  9. def manifests(), do: [manifest()]
  10. defp manifest(), do: Path.join(Mix.Project.manifest_path(), @manifest)
  11. @impl true
  12. def run(_args) do
  13. Mix.Project.get!()
  14. config = Mix.Project.config()
  15. %{
  16. gpb_opts: gpb_opts,
  17. proto_dirs: proto_dirs,
  18. out_dir: out_dir
  19. } = config[:grpc_opts]
  20. add_to_path_and_cache(:syntax_tools)
  21. :ok = Application.ensure_loaded(:syntax_tools)
  22. :ok = Application.ensure_loaded(:gpb)
  23. app_root = File.cwd!()
  24. app_build_path = Mix.Project.app_path(config)
  25. proto_srcs =
  26. proto_dirs
  27. |> Enum.map(& Path.join([app_root, &1]))
  28. |> Mix.Utils.extract_files([:proto])
  29. manifest_data = read_manifest(manifest())
  30. context = %{
  31. manifest_data: manifest_data,
  32. app_root: app_root,
  33. app_build_path: app_build_path,
  34. out_dir: out_dir,
  35. gpb_opts: gpb_opts,
  36. }
  37. Enum.each(proto_srcs, & compile_pb(&1, context))
  38. write_manifest(manifest(), manifest_data)
  39. {:noop, []}
  40. after
  41. Application.unload(:gpb)
  42. Application.unload(:syntax_tools)
  43. end
  44. defp compile_pb(proto_src, context) do
  45. %{
  46. app_root: app_root,
  47. app_build_path: app_build_path,
  48. out_dir: out_dir,
  49. gpb_opts: gpb_opts,
  50. } = context
  51. manifest_modified_time = Mix.Utils.last_modified(manifest())
  52. ebin_path = Path.join([app_build_path, "ebin"])
  53. basename = proto_src |> Path.basename(".proto") |> to_charlist()
  54. prefix = Keyword.get(gpb_opts, :module_name_prefix, '')
  55. suffix = Keyword.get(gpb_opts, :module_name_suffix, '')
  56. mod_name = '#{prefix}#{basename}#{suffix}'
  57. opts = [
  58. :use_packages,
  59. :maps,
  60. :strings_as_binaries,
  61. i: '.',
  62. o: out_dir,
  63. report_errors: false,
  64. rename: {:msg_name, :snake_case},
  65. rename: {:msg_fqname, :base_name},
  66. ]
  67. if stale?(proto_src, manifest_modified_time) do
  68. Mix.shell().info("compiling proto file: #{proto_src}")
  69. File.mkdir_p!(out_dir)
  70. # TODO: better error logging...
  71. :ok = :gpb_compile.file(
  72. to_charlist(proto_src),
  73. opts ++ gpb_opts
  74. )
  75. else
  76. Mix.shell().info("proto file up to date, not compiling: #{proto_src}")
  77. end
  78. generated_src = Path.join([app_root, out_dir, "#{mod_name}.erl"])
  79. gpb_include_dir = :code.lib_dir(:gpb, :include)
  80. if stale?(generated_src, manifest_modified_time) do
  81. Mix.shell().info("compiling proto module: #{generated_src}")
  82. compile_res = :compile.file(
  83. to_charlist(generated_src),
  84. [
  85. :return_errors,
  86. i: to_charlist(gpb_include_dir),
  87. outdir: to_charlist(ebin_path)
  88. ] ++ UMP.erlc_options()
  89. )
  90. # todo: error handling & logging
  91. case compile_res do
  92. {:ok, _} ->
  93. :ok
  94. {:ok, _, _warnings} ->
  95. :ok
  96. end
  97. else
  98. Mix.shell().info("file up to date, not compiling: #{generated_src}")
  99. end
  100. mod_name
  101. |> List.to_atom()
  102. |> :code.purge()
  103. {:module, _mod} =
  104. ebin_path
  105. |> Path.join(mod_name)
  106. |> to_charlist()
  107. |> :code.load_abs()
  108. mod_name = List.to_atom(mod_name)
  109. service_quoted =
  110. [__DIR__, "../../", "emqx/grpc/template/service.eex"]
  111. |> Path.join()
  112. |> Path.expand()
  113. |> EEx.compile_file()
  114. client_quoted =
  115. [__DIR__, "../../", "emqx/grpc/template/client.eex"]
  116. |> Path.join()
  117. |> Path.expand()
  118. |> EEx.compile_file()
  119. mod_name.get_service_names()
  120. |> Enum.each(fn service ->
  121. service
  122. |> mod_name.get_service_def()
  123. |> then(fn {{:service, service_name}, methods} ->
  124. methods = Enum.map(methods, fn method ->
  125. snake_case = method.name |> to_string() |> Macro.underscore()
  126. message_type = mod_name.msg_name_to_fqbin(method.input)
  127. method
  128. |> Map.put(:message_type, message_type)
  129. |> Map.put(:snake_case, snake_case)
  130. |> Map.put(:pb_module, mod_name)
  131. |> Map.put(:unmodified_method, method.name)
  132. end)
  133. snake_service =
  134. service_name
  135. |> to_string()
  136. |> Macro.underscore()
  137. |> String.replace("/", "_")
  138. |> String.replace(~r/(.)([0-9]+)/, "\\1_\\2")
  139. bindings = [
  140. methods: methods,
  141. pb_module: mod_name,
  142. module_name: snake_service,
  143. unmodified_service_name: service_name
  144. ]
  145. bhvr_output_src = Path.join([app_root, out_dir, "#{snake_service}_bhvr.erl"])
  146. if stale?(bhvr_output_src, manifest_modified_time) do
  147. render_and_write(service_quoted, bhvr_output_src, bindings)
  148. else
  149. Mix.shell().info("file up to date, not compiling: #{bhvr_output_src}")
  150. end
  151. client_output_src = Path.join([app_root, out_dir, "#{snake_service}_client.erl"])
  152. if stale?(client_output_src, manifest_modified_time) do
  153. render_and_write(client_quoted, client_output_src, bindings)
  154. else
  155. Mix.shell().info("file up to date, not compiling: #{client_output_src}")
  156. end
  157. :ok
  158. end)
  159. end)
  160. :ok
  161. end
  162. defp stale?(file, manifest_modified_time) do
  163. with true <- File.exists?(file),
  164. false <- Mix.Utils.stale?([file], [manifest_modified_time]) do
  165. false
  166. else
  167. _ -> true
  168. end
  169. end
  170. defp read_manifest(file) do
  171. try do
  172. file |> File.read!() |> :erlang.binary_to_term()
  173. rescue
  174. _ -> %{}
  175. else
  176. {@manifest_vsn, data} when is_map(data) -> data
  177. _ -> %{}
  178. end
  179. end
  180. defp write_manifest(file, data) do
  181. Mix.shell().info("writing manifest #{file}")
  182. File.mkdir_p!(Path.dirname(file))
  183. File.write!(file, :erlang.term_to_binary({@manifest_vsn, data}))
  184. end
  185. defp render_and_write(quoted_file, output_src, bindings) do
  186. {result, _bindings} = Code.eval_quoted(quoted_file, bindings)
  187. result = String.replace(result, ~r/\n\n\n+/, "\n\n\n")
  188. File.write!(output_src, result)
  189. end
  190. def add_to_path_and_cache(lib_name) do
  191. :code.lib_dir()
  192. |> Path.join("#{lib_name}-*")
  193. |> Path.wildcard()
  194. |> hd()
  195. |> Path.join("ebin")
  196. |> to_charlist()
  197. |> :code.add_path(:cache)
  198. end
  199. end