mix.exs 17 KB

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