mix.exs 17 KB

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