mix.exs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623
  1. defmodule EMQXUmbrella.MixProject do
  2. use Mix.Project
  3. @moduledoc """
  4. The purpose of this file is to configure the release of EMQX under
  5. Mix. Since EMQX uses its own configuration conventions and startup
  6. procedures, one cannot simply use `iex -S mix`. Instead, it's
  7. recommendd to build and use the release.
  8. ## Release Environment Variables
  9. The release build is controlled by a few environment variables.
  10. * `ELIXIR_MAKE_TAR` - If set to `yes`, will produce a `.tar.gz`
  11. tarball along with the release.
  12. * `EMQX_RELEASE_TYPE` - Must be one of `cloud | edge`. Controls a
  13. few dependencies and the `vm.args` to be used. Defaults to
  14. `cloud`.
  15. * `EMQX_PACKAGE_TYPE` - Must be one of `bin | pkg`. Controls
  16. whether the build is intended for direct usage or for packaging.
  17. Defaults to `bin`.
  18. * `EMQX_EDITION_TYPE` - Must be one of `community | enterprise`.
  19. Defaults to `community`.
  20. """
  21. def project() do
  22. [
  23. app: :emqx_mix,
  24. version: pkg_vsn(),
  25. deps: deps(),
  26. releases: releases()
  27. ]
  28. end
  29. defp deps() do
  30. # we need several overrides here because dependencies specify
  31. # other exact versions, and not ranges.
  32. [
  33. {:lc, github: "qzhuyan/lc", tag: "0.1.2"},
  34. {:redbug, "2.0.7"},
  35. {:typerefl, github: "k32/typerefl", tag: "0.8.6", override: true},
  36. {:ehttpc, github: "emqx/ehttpc", tag: "0.1.12"},
  37. {:gproc, github: "uwiger/gproc", tag: "0.8.0", override: true},
  38. {:jiffy, github: "emqx/jiffy", tag: "1.0.5", override: true},
  39. {:cowboy, github: "emqx/cowboy", tag: "2.9.0", override: true},
  40. {:esockd, github: "emqx/esockd", tag: "5.9.0", override: true},
  41. {:mria, github: "emqx/mria", tag: "0.1.5", override: true},
  42. {:ekka, github: "emqx/ekka", tag: "0.11.3", override: true},
  43. {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.0", override: true},
  44. {:minirest, github: "emqx/minirest", tag: "1.2.10", override: true},
  45. {:ecpool, github: "emqx/ecpool", tag: "0.5.2"},
  46. {:replayq, "0.3.3", override: true},
  47. {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true},
  48. {:emqtt, github: "emqx/emqtt", tag: "1.4.3", override: true},
  49. {:rulesql, github: "emqx/rulesql", tag: "0.1.4"},
  50. {:observer_cli, "1.7.1"},
  51. {:system_monitor, github: "k32/system_monitor", tag: "2.2.1"},
  52. # in conflict by emqtt and hocon
  53. {:getopt, "1.0.2", override: true},
  54. {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "0.16.0", override: true},
  55. {:hocon, github: "emqx/hocon", tag: "0.23.0", override: true},
  56. {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.4.1", override: true},
  57. {:esasl, github: "emqx/esasl", tag: "0.2.0"},
  58. {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"},
  59. # in conflict by ehttpc and emqtt
  60. {:gun, github: "emqx/gun", tag: "1.3.6", override: true},
  61. # in conflict by emqx_connectior and system_monitor
  62. {:epgsql, github: "emqx/epgsql", tag: "4.7-emqx.2", override: true},
  63. # in conflict by mongodb and eredis_cluster
  64. {:poolboy, github: "emqx/poolboy", tag: "1.5.2", override: true},
  65. # in conflict by emqx and observer_cli
  66. {:recon, github: "ferd/recon", tag: "2.5.1", override: true},
  67. {:jsx, github: "talentdeficit/jsx", tag: "v3.1.0", override: true},
  68. # dependencies of dependencies; we choose specific refs to match
  69. # what rebar3 chooses.
  70. # in conflict by gun and emqtt
  71. {:cowlib,
  72. github: "ninenines/cowlib", ref: "c6553f8308a2ca5dcd69d845f0a7d098c40c3363", override: true},
  73. # in conflict by cowboy_swagger and cowboy
  74. {:ranch,
  75. github: "ninenines/ranch", ref: "a692f44567034dacf5efcaa24a24183788594eb7", override: true},
  76. # in conflict by grpc and eetcd
  77. {:gpb, "4.11.2", override: true}
  78. ] ++ umbrella_apps() ++ bcrypt_dep() ++ quicer_dep()
  79. end
  80. defp umbrella_apps() do
  81. "apps/*"
  82. |> Path.wildcard()
  83. |> Enum.map(fn path ->
  84. app =
  85. path
  86. |> String.trim_leading("apps/")
  87. |> String.to_atom()
  88. {app, path: path, manager: :rebar3, override: true}
  89. end)
  90. end
  91. defp releases() do
  92. [
  93. emqx: fn ->
  94. %{
  95. release_type: release_type,
  96. package_type: package_type,
  97. edition_type: edition_type
  98. } = read_inputs()
  99. base_steps = [
  100. :assemble,
  101. &create_RELEASES/1,
  102. &copy_files(&1, release_type, package_type, edition_type),
  103. &copy_escript(&1, "nodetool"),
  104. &copy_escript(&1, "install_upgrade.escript")
  105. ]
  106. steps =
  107. if System.get_env("ELIXIR_MAKE_TAR") == "yes" do
  108. base_steps ++ [&prepare_tar_overlays/1, :tar]
  109. else
  110. base_steps
  111. end
  112. [
  113. applications: applications(release_type),
  114. skip_mode_validation_for: [
  115. :emqx_gateway,
  116. :emqx_dashboard,
  117. :emqx_resource,
  118. :emqx_connector,
  119. :emqx_exhook,
  120. :emqx_bridge,
  121. :emqx_modules,
  122. :emqx_management,
  123. :emqx_statsd,
  124. :emqx_retainer,
  125. :emqx_prometheus,
  126. :emqx_plugins
  127. ],
  128. steps: steps,
  129. strip_beams: false
  130. ]
  131. end
  132. ]
  133. end
  134. def applications(release_type) do
  135. [
  136. crypto: :permanent,
  137. public_key: :permanent,
  138. asn1: :permanent,
  139. syntax_tools: :permanent,
  140. ssl: :permanent,
  141. os_mon: :permanent,
  142. inets: :permanent,
  143. compiler: :permanent,
  144. runtime_tools: :permanent,
  145. redbug: :permanent,
  146. hocon: :load,
  147. emqx: :load,
  148. emqx_conf: :load,
  149. emqx_machine: :permanent,
  150. mria: :load,
  151. mnesia: :load,
  152. ekka: :load,
  153. emqx_plugin_libs: :load,
  154. esasl: :load,
  155. observer_cli: :permanent,
  156. system_monitor: :permanent,
  157. emqx_http_lib: :permanent,
  158. emqx_resource: :permanent,
  159. emqx_connector: :permanent,
  160. emqx_authn: :permanent,
  161. emqx_authz: :permanent,
  162. emqx_auto_subscribe: :permanent,
  163. emqx_gateway: :permanent,
  164. emqx_exhook: :permanent,
  165. emqx_bridge: :permanent,
  166. emqx_rule_engine: :permanent,
  167. emqx_modules: :permanent,
  168. emqx_management: :permanent,
  169. emqx_dashboard: :permanent,
  170. emqx_retainer: :permanent,
  171. emqx_statsd: :permanent,
  172. emqx_prometheus: :permanent,
  173. emqx_psk: :permanent,
  174. emqx_slow_subs: :permanent,
  175. emqx_plugins: :permanent,
  176. emqx_mix: :none
  177. ] ++
  178. if(enable_quicer?(), do: [quicer: :permanent], else: []) ++
  179. if(enable_bcrypt?(), do: [bcrypt: :permanent], else: []) ++
  180. if(release_type == :cloud,
  181. do: [xmerl: :permanent, observer: :load],
  182. else: []
  183. )
  184. end
  185. defp read_inputs() do
  186. release_type =
  187. read_enum_env_var(
  188. "EMQX_RELEASE_TYPE",
  189. [:cloud, :edge],
  190. :cloud
  191. )
  192. package_type =
  193. read_enum_env_var(
  194. "EMQX_PACKAGE_TYPE",
  195. [:bin, :pkg],
  196. :bin
  197. )
  198. edition_type =
  199. read_enum_env_var(
  200. "EMQX_EDITION_TYPE",
  201. [:community, :enterprise],
  202. :community
  203. )
  204. %{
  205. release_type: release_type,
  206. package_type: package_type,
  207. edition_type: edition_type
  208. }
  209. end
  210. #############################################################################
  211. # Custom Steps
  212. #############################################################################
  213. defp copy_files(release, release_type, package_type, edition_type) do
  214. overwrite? = Keyword.get(release.options, :overwrite, false)
  215. bin = Path.join(release.path, "bin")
  216. etc = Path.join(release.path, "etc")
  217. log = Path.join(release.path, "log")
  218. Mix.Generator.create_directory(bin)
  219. Mix.Generator.create_directory(etc)
  220. Mix.Generator.create_directory(log)
  221. Mix.Generator.create_directory(Path.join(etc, "certs"))
  222. Enum.each(
  223. ["mnesia", "configs", "patches", "scripts"],
  224. fn dir ->
  225. path = Path.join([release.path, "data", dir])
  226. Mix.Generator.create_directory(path)
  227. end
  228. )
  229. Mix.Generator.copy_file(
  230. "apps/emqx_authz/etc/acl.conf",
  231. Path.join(etc, "acl.conf"),
  232. force: overwrite?
  233. )
  234. # required by emqx_authz
  235. File.cp_r!(
  236. "apps/emqx/etc/certs",
  237. Path.join(etc, "certs")
  238. )
  239. # this is required by the produced escript / nodetool
  240. Mix.Generator.copy_file(
  241. Path.join(release.version_path, "start_clean.boot"),
  242. Path.join(bin, "no_dot_erlang.boot"),
  243. force: overwrite?
  244. )
  245. assigns = template_vars(release, release_type, package_type, edition_type)
  246. # This is generated by `scripts/merge-config.escript` or `make
  247. # conf-segs`. So, this should be run before the release.
  248. # TODO: run as a "compiler" step???
  249. conf_rendered =
  250. File.read!("apps/emqx_conf/etc/emqx.conf.all")
  251. |> from_rebar_to_eex_template()
  252. |> EEx.eval_string(assigns)
  253. File.write!(
  254. Path.join(etc, "emqx.conf"),
  255. conf_rendered
  256. )
  257. vars_rendered =
  258. File.read!("rel/emqx_vars")
  259. |> from_rebar_to_eex_template()
  260. |> EEx.eval_string(assigns)
  261. File.write!(
  262. Path.join([release.path, "releases", "emqx_vars"]),
  263. vars_rendered
  264. )
  265. vm_args_template_path =
  266. case release_type do
  267. :cloud ->
  268. "apps/emqx/etc/emqx_cloud/vm.args"
  269. :edge ->
  270. "apps/emqx/etc/emqx_edge/vm.args"
  271. end
  272. vm_args_rendered =
  273. File.read!(vm_args_template_path)
  274. |> from_rebar_to_eex_template()
  275. |> EEx.eval_string(assigns)
  276. File.write!(
  277. Path.join(etc, "vm.args"),
  278. vm_args_rendered
  279. )
  280. File.write!(
  281. Path.join(release.version_path, "vm.args"),
  282. vm_args_rendered
  283. )
  284. for name <- [
  285. "emqx",
  286. "emqx_ctl"
  287. ] do
  288. Mix.Generator.copy_file(
  289. "bin/#{name}",
  290. Path.join(bin, name),
  291. force: overwrite?
  292. )
  293. # Files with the version appended are expected by the release
  294. # upgrade script `install_upgrade.escript`
  295. Mix.Generator.copy_file(
  296. Path.join(bin, name),
  297. Path.join(bin, name <> "-#{release.version}"),
  298. force: overwrite?
  299. )
  300. end
  301. for base_name <- ["emqx", "emqx_ctl"],
  302. suffix <- ["", "-#{release.version}"] do
  303. name = base_name <> suffix
  304. File.chmod!(Path.join(bin, name), 0o755)
  305. end
  306. built_on_rendered =
  307. File.read!("rel/BUILT_ON")
  308. |> from_rebar_to_eex_template()
  309. |> EEx.eval_string(assigns)
  310. File.write!(
  311. Path.join([release.version_path, "BUILT_ON"]),
  312. built_on_rendered
  313. )
  314. release
  315. end
  316. # needed by nodetool and by release_handler
  317. defp create_RELEASES(release) do
  318. apps =
  319. Enum.map(release.applications, fn {app_name, app_props} ->
  320. app_vsn = Keyword.fetch!(app_props, :vsn)
  321. app_path =
  322. "./lib"
  323. |> Path.join("#{app_name}-#{app_vsn}")
  324. |> to_charlist()
  325. {app_name, app_vsn, app_path}
  326. end)
  327. release_entry = [
  328. {
  329. :release,
  330. to_charlist(release.name),
  331. to_charlist(release.version),
  332. release.erts_version,
  333. apps,
  334. :permanent
  335. }
  336. ]
  337. release.path
  338. |> Path.join("releases")
  339. |> Path.join("RELEASES")
  340. |> File.open!([:write, :utf8], fn handle ->
  341. IO.puts(handle, "%% coding: utf-8")
  342. :io.format(handle, '~tp.~n', [release_entry])
  343. end)
  344. release
  345. end
  346. defp copy_escript(release, escript_name) do
  347. [shebang, rest] =
  348. "bin/#{escript_name}"
  349. |> File.read!()
  350. |> String.split("\n", parts: 2)
  351. # the elixir version of escript + start.boot required the boot_var
  352. # RELEASE_LIB to be defined.
  353. boot_var = "%%!-boot_var RELEASE_LIB $RUNNER_ROOT_DIR/lib"
  354. # Files with the version appended are expected by the release
  355. # upgrade script `install_upgrade.escript`
  356. Enum.each(
  357. [escript_name, escript_name <> "-" <> release.version],
  358. fn name ->
  359. path = Path.join([release.path, "bin", name])
  360. File.write!(path, [shebang, "\n", boot_var, "\n", rest])
  361. end
  362. )
  363. release
  364. end
  365. # The `:tar` built-in step in Mix Release does not currently add the
  366. # `etc` directory into the resulting tarball. The workaround is to
  367. # add those to the `:overlays` key before running `:tar`.
  368. # See: https://hexdocs.pm/mix/1.13.2/Mix.Release.html#__struct__/0
  369. defp prepare_tar_overlays(release) do
  370. Map.update!(release, :overlays, &["etc", "data" | &1])
  371. end
  372. #############################################################################
  373. # Helper functions
  374. #############################################################################
  375. defp template_vars(release, release_type, :bin = _package_type, edition_type) do
  376. [
  377. platform_bin_dir: "bin",
  378. platform_data_dir: "data",
  379. platform_etc_dir: "etc",
  380. platform_lib_dir: "lib",
  381. platform_log_dir: "log",
  382. platform_plugins_dir: "plugins",
  383. runner_root_dir: "$(cd $(dirname $(readlink $0 || echo $0))/..; pwd -P)",
  384. runner_bin_dir: "$RUNNER_ROOT_DIR/bin",
  385. runner_etc_dir: "$RUNNER_ROOT_DIR/etc",
  386. runner_lib_dir: "$RUNNER_ROOT_DIR/lib",
  387. runner_log_dir: "$RUNNER_ROOT_DIR/log",
  388. runner_data_dir: "$RUNNER_ROOT_DIR/data",
  389. runner_user: "",
  390. release_version: release.version,
  391. erts_vsn: release.erts_version,
  392. # FIXME: this is empty in `make emqx` ???
  393. erl_opts: "",
  394. emqx_description: emqx_description(release_type, edition_type),
  395. built_on_arch: built_on(),
  396. is_elixir: "yes"
  397. ]
  398. end
  399. defp template_vars(release, release_type, :pkg = _package_type, edition_type) do
  400. [
  401. platform_bin_dir: "",
  402. platform_data_dir: "/var/lib/emqx",
  403. platform_etc_dir: "/etc/emqx",
  404. platform_lib_dir: "",
  405. platform_log_dir: "/var/log/emqx",
  406. platform_plugins_dir: "/var/lib/emqx/plugins",
  407. runner_root_dir: "/usr/lib/emqx",
  408. runner_bin_dir: "/usr/bin",
  409. runner_etc_dir: "/etc/emqx",
  410. runner_lib_dir: "$RUNNER_ROOT_DIR/lib",
  411. runner_log_dir: "/var/log/emqx",
  412. runner_data_dir: "/var/lib/emqx",
  413. runner_user: "emqx",
  414. release_version: release.version,
  415. erts_vsn: release.erts_version,
  416. # FIXME: this is empty in `make emqx` ???
  417. erl_opts: "",
  418. emqx_description: emqx_description(release_type, edition_type),
  419. built_on_arch: built_on(),
  420. is_elixir: "yes"
  421. ]
  422. end
  423. defp read_enum_env_var(env_var, allowed_values, default_value) do
  424. case System.fetch_env(env_var) do
  425. :error ->
  426. default_value
  427. {:ok, raw_value} ->
  428. value =
  429. raw_value
  430. |> String.downcase()
  431. |> String.to_atom()
  432. if value not in allowed_values do
  433. Mix.raise("""
  434. Invalid value #{raw_value} for variable #{env_var}.
  435. Allowed values are: #{inspect(allowed_values)}
  436. """)
  437. end
  438. value
  439. end
  440. end
  441. defp emqx_description(release_type, edition_type) do
  442. case {release_type, edition_type} do
  443. {:cloud, :enterprise} ->
  444. "EMQ X Enterprise Edition"
  445. {:cloud, :community} ->
  446. "EMQ X Community Edition"
  447. {:edge, :community} ->
  448. "EMQ X Edge Edition"
  449. end
  450. end
  451. defp bcrypt_dep() do
  452. if enable_bcrypt?(),
  453. do: [{:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.0", override: true}],
  454. else: []
  455. end
  456. defp quicer_dep() do
  457. if enable_quicer?(),
  458. # in conflict with emqx and emqtt
  459. do: [{:quicer, github: "emqx/quic", tag: "0.0.9", override: true}],
  460. else: []
  461. end
  462. defp enable_bcrypt?() do
  463. not win32?()
  464. end
  465. defp enable_quicer?() do
  466. not Enum.any?([
  467. build_without_quic?(),
  468. win32?(),
  469. centos6?()
  470. ])
  471. end
  472. defp pkg_vsn() do
  473. %{edition_type: edition_type} = read_inputs()
  474. basedir = Path.dirname(__ENV__.file)
  475. script = Path.join(basedir, "pkg-vsn.sh")
  476. {str_vsn, 0} = System.cmd(script, [Atom.to_string(edition_type)])
  477. String.trim(str_vsn)
  478. end
  479. defp win32?(),
  480. do: match?({:win_32, _}, :os.type())
  481. defp centos6?() do
  482. case File.read("/etc/centos-release") do
  483. {:ok, "CentOS release 6" <> _} ->
  484. true
  485. _ ->
  486. false
  487. end
  488. end
  489. defp build_without_quic?() do
  490. opt = System.get_env("BUILD_WITHOUT_QUIC", "false")
  491. String.downcase(opt) != "false"
  492. end
  493. defp from_rebar_to_eex_template(str) do
  494. # we must not consider surrounding space in the template var name
  495. # because some help strings contain informative variables that
  496. # should not be interpolated, and those have no spaces.
  497. Regex.replace(
  498. ~r/\{\{ ([a-zA-Z0-9_]+) \}\}/,
  499. str,
  500. "<%= \\g{1} %>"
  501. )
  502. end
  503. defp built_on() do
  504. system_architecture = to_string(:erlang.system_info(:system_architecture))
  505. elixir_version = System.version()
  506. words = wordsize()
  507. "#{elixir_version}-#{otp_release()}-#{system_architecture}-#{words}"
  508. end
  509. # https://github.com/erlang/rebar3/blob/e3108ac187b88fff01eca6001a856283a3e0ec87/src/rebar_utils.erl#L142
  510. defp wordsize() do
  511. size =
  512. try do
  513. :erlang.system_info({:wordsize, :external})
  514. rescue
  515. ErlangError ->
  516. :erlang.system_info(:wordsize)
  517. end
  518. to_string(8 * size)
  519. end
  520. # As from Erlang/OTP 17, the OTP release number corresponds to the
  521. # major OTP version number. No erlang:system_info() argument gives
  522. # the exact OTP version.
  523. # https://www.erlang.org/doc/man/erlang.html#system_info_otp_release
  524. # https://github.com/erlang/rebar3/blob/e3108ac187b88fff01eca6001a856283a3e0ec87/src/rebar_utils.erl#L572-L577
  525. defp otp_release() do
  526. major_version = System.otp_release()
  527. root_dir = to_string(:code.root_dir())
  528. [root_dir, "releases", major_version, "OTP_VERSION"]
  529. |> Path.join()
  530. |> File.read()
  531. |> case do
  532. {:error, _} ->
  533. major_version
  534. {:ok, version} ->
  535. version
  536. |> String.trim()
  537. |> String.split("**")
  538. |> List.first()
  539. end
  540. end
  541. end