mix.exs 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704
  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. ## Profiles
  9. To control the profile and edition to build, we case split on the
  10. MIX_ENV value.
  11. The following profiles are valid:
  12. * `emqx`
  13. * `emqx-enterprise`
  14. * `emqx-pkg`
  15. * `emqx-enterprise-pkg`
  16. * `dev` -> same as `emqx`, for convenience
  17. ## Release Environment Variables
  18. The release build is controlled by a few environment variables.
  19. * `ELIXIR_MAKE_TAR` - If set to `yes`, will produce a `.tar.gz`
  20. tarball along with the release.
  21. """
  22. def project() do
  23. check_profile!()
  24. [
  25. app: :emqx_mix,
  26. version: pkg_vsn(),
  27. deps: deps(),
  28. releases: releases()
  29. ]
  30. end
  31. defp deps() do
  32. # we need several overrides here because dependencies specify
  33. # other exact versions, and not ranges.
  34. [
  35. {:lc, github: "emqx/lc", tag: "0.3.1"},
  36. {:redbug, "2.0.7"},
  37. {:typerefl, github: "ieQu1/typerefl", tag: "0.9.1", override: true},
  38. {:ehttpc, github: "emqx/ehttpc", tag: "0.2.0"},
  39. {:gproc, github: "uwiger/gproc", tag: "0.8.0", override: true},
  40. {:jiffy, github: "emqx/jiffy", tag: "1.0.5", override: true},
  41. {:cowboy, github: "emqx/cowboy", tag: "2.9.0", override: true},
  42. {:esockd, github: "emqx/esockd", tag: "5.9.3", override: true},
  43. {:mria, github: "emqx/mria", tag: "0.2.7", override: true},
  44. {:ekka, github: "emqx/ekka", tag: "0.12.8", override: true},
  45. {:gen_rpc, github: "emqx/gen_rpc", tag: "2.8.1", override: true},
  46. {:minirest, github: "emqx/minirest", tag: "1.3.3", override: true},
  47. {:ecpool, github: "emqx/ecpool", tag: "0.5.2"},
  48. {:replayq, "0.3.4", override: true},
  49. {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4", override: true},
  50. {:emqtt, github: "emqx/emqtt", tag: "1.5.0", override: true},
  51. {:rulesql, github: "emqx/rulesql", tag: "0.1.4"},
  52. {:observer_cli, "1.7.1"},
  53. {:system_monitor, github: "ieQu1/system_monitor", tag: "3.0.3"},
  54. # in conflict by emqtt and hocon
  55. {:getopt, "1.0.2", override: true},
  56. {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "1.0.0", override: true},
  57. {:hocon, github: "emqx/hocon", tag: "0.28.1", override: true},
  58. {:emqx_http_lib, github: "emqx/emqx_http_lib", tag: "0.5.1", override: true},
  59. {:esasl, github: "emqx/esasl", tag: "0.2.0"},
  60. {:jose, github: "potatosalad/erlang-jose", tag: "1.11.2"},
  61. # in conflict by ehttpc and emqtt
  62. {:gun, github: "emqx/gun", tag: "1.3.7", override: true},
  63. # in conflict by emqx_connectior and system_monitor
  64. {:epgsql, github: "emqx/epgsql", tag: "4.7-emqx.2", override: true},
  65. # in conflict by mongodb and eredis_cluster
  66. {:poolboy, github: "emqx/poolboy", tag: "1.5.2", override: true},
  67. # in conflict by emqx and observer_cli
  68. {:recon, github: "ferd/recon", tag: "2.5.1", override: true},
  69. {:jsx, github: "talentdeficit/jsx", tag: "v3.1.0", override: true},
  70. # dependencies of dependencies; we choose specific refs to match
  71. # what rebar3 chooses.
  72. # in conflict by gun and emqtt
  73. {:cowlib,
  74. github: "ninenines/cowlib", ref: "c6553f8308a2ca5dcd69d845f0a7d098c40c3363", override: true},
  75. # in conflict by cowboy_swagger and cowboy
  76. {:ranch,
  77. github: "ninenines/ranch", ref: "a692f44567034dacf5efcaa24a24183788594eb7", override: true},
  78. # in conflict by grpc and eetcd
  79. {:gpb, "4.11.2", override: true, runtime: false}
  80. ] ++ umbrella_apps() ++ bcrypt_dep() ++ jq_dep() ++ quicer_dep()
  81. end
  82. defp umbrella_apps() do
  83. "apps/*"
  84. |> Path.wildcard()
  85. |> Enum.map(fn path ->
  86. app =
  87. path
  88. |> String.trim_leading("apps/")
  89. |> String.to_atom()
  90. {app, path: path, manager: :rebar3, override: true}
  91. end)
  92. end
  93. defp releases() do
  94. [
  95. emqx: fn ->
  96. %{
  97. release_type: release_type,
  98. package_type: package_type,
  99. edition_type: edition_type
  100. } = check_profile!()
  101. base_steps = [
  102. :assemble,
  103. &create_RELEASES/1,
  104. &copy_files(&1, release_type, package_type, edition_type),
  105. &copy_escript(&1, "nodetool"),
  106. &copy_escript(&1, "install_upgrade.escript")
  107. ]
  108. steps =
  109. if System.get_env("ELIXIR_MAKE_TAR") == "yes" do
  110. base_steps ++ [&prepare_tar_overlays/1, :tar]
  111. else
  112. base_steps
  113. end
  114. [
  115. applications: applications(edition_type),
  116. skip_mode_validation_for: [
  117. :emqx_gateway,
  118. :emqx_dashboard,
  119. :emqx_resource,
  120. :emqx_connector,
  121. :emqx_exhook,
  122. :emqx_bridge,
  123. :emqx_modules,
  124. :emqx_management,
  125. :emqx_statsd,
  126. :emqx_retainer,
  127. :emqx_prometheus,
  128. :emqx_auto_subscribe,
  129. :emqx_slow_subs,
  130. :emqx_plugins
  131. ],
  132. steps: steps,
  133. strip_beams: false
  134. ]
  135. end
  136. ]
  137. end
  138. def applications(edition_type) do
  139. [
  140. crypto: :permanent,
  141. public_key: :permanent,
  142. asn1: :permanent,
  143. syntax_tools: :permanent,
  144. ssl: :permanent,
  145. os_mon: :permanent,
  146. inets: :permanent,
  147. compiler: :permanent,
  148. runtime_tools: :permanent,
  149. redbug: :permanent,
  150. xmerl: :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: :load,
  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(enable_jq?(), do: [jq: :permanent], else: []) ++
  186. if(is_app(:observer),
  187. do: [observer: :load],
  188. else: []
  189. ) ++
  190. if(edition_type == :enterprise,
  191. do: [
  192. emqx_license: :permanent,
  193. emqx_enterprise_conf: :load
  194. ],
  195. else: []
  196. )
  197. end
  198. defp is_app(name) do
  199. case Application.load(name) do
  200. :ok ->
  201. true
  202. {:error, {:already_loaded, _}} ->
  203. true
  204. _ ->
  205. false
  206. end
  207. end
  208. def check_profile!() do
  209. valid_envs = [
  210. :dev,
  211. :emqx,
  212. :"emqx-pkg",
  213. :"emqx-enterprise",
  214. :"emqx-enterprise-pkg"
  215. ]
  216. if Mix.env() not in valid_envs do
  217. formatted_envs =
  218. valid_envs
  219. |> Enum.map(&" * #{&1}")
  220. |> Enum.join("\n")
  221. Mix.raise("""
  222. Invalid env #{Mix.env()}. Valid options are:
  223. #{formatted_envs}
  224. """)
  225. end
  226. {
  227. release_type,
  228. package_type,
  229. edition_type
  230. } =
  231. case Mix.env() do
  232. :dev ->
  233. {:cloud, :bin, :community}
  234. :emqx ->
  235. {:cloud, :bin, :community}
  236. :"emqx-enterprise" ->
  237. {:cloud, :bin, :enterprise}
  238. :"emqx-pkg" ->
  239. {:cloud, :pkg, :community}
  240. :"emqx-enterprise-pkg" ->
  241. {:cloud, :pkg, :enterprise}
  242. end
  243. normalize_env!()
  244. %{
  245. release_type: release_type,
  246. package_type: package_type,
  247. edition_type: edition_type
  248. }
  249. end
  250. #############################################################################
  251. # Custom Steps
  252. #############################################################################
  253. defp copy_files(release, release_type, package_type, edition_type) do
  254. overwrite? = Keyword.get(release.options, :overwrite, false)
  255. bin = Path.join(release.path, "bin")
  256. etc = Path.join(release.path, "etc")
  257. log = Path.join(release.path, "log")
  258. Mix.Generator.create_directory(bin)
  259. Mix.Generator.create_directory(etc)
  260. Mix.Generator.create_directory(log)
  261. Mix.Generator.create_directory(Path.join(etc, "certs"))
  262. Enum.each(
  263. ["mnesia", "configs", "patches", "scripts"],
  264. fn dir ->
  265. path = Path.join([release.path, "data", dir])
  266. Mix.Generator.create_directory(path)
  267. end
  268. )
  269. Mix.Generator.copy_file(
  270. "apps/emqx_authz/etc/acl.conf",
  271. Path.join(etc, "acl.conf"),
  272. force: overwrite?
  273. )
  274. # required by emqx_authz
  275. File.cp_r!(
  276. "apps/emqx/etc/certs",
  277. Path.join(etc, "certs")
  278. )
  279. # required by emqx_dashboard
  280. Mix.Generator.copy_file(
  281. "apps/emqx_dashboard/etc/i18n.conf.all",
  282. Path.join(etc, "i18n.conf"),
  283. force: overwrite?
  284. )
  285. # this is required by the produced escript / nodetool
  286. Mix.Generator.copy_file(
  287. Path.join(release.version_path, "start_clean.boot"),
  288. Path.join(bin, "no_dot_erlang.boot"),
  289. force: overwrite?
  290. )
  291. assigns = template_vars(release, release_type, package_type, edition_type)
  292. # This is generated by `scripts/merge-config.escript` or `make
  293. # conf-segs`. So, this should be run before the release.
  294. # TODO: run as a "compiler" step???
  295. render_template(
  296. "apps/emqx_conf/etc/emqx.conf.all",
  297. assigns,
  298. Path.join(etc, "emqx.conf")
  299. )
  300. if edition_type == :enterprise do
  301. render_template(
  302. "apps/emqx_conf/etc/emqx_enterprise.conf.all",
  303. assigns,
  304. Path.join(etc, "emqx_enterprise.conf")
  305. )
  306. end
  307. render_template(
  308. "rel/emqx_vars",
  309. assigns,
  310. Path.join([release.path, "releases", "emqx_vars"])
  311. )
  312. vm_args_template_path =
  313. case release_type do
  314. :cloud ->
  315. "apps/emqx/etc/emqx_cloud/vm.args"
  316. end
  317. render_template(
  318. vm_args_template_path,
  319. assigns,
  320. [
  321. Path.join(etc, "vm.args"),
  322. Path.join(release.version_path, "vm.args")
  323. ]
  324. )
  325. for name <- [
  326. "emqx",
  327. "emqx_ctl"
  328. ] do
  329. Mix.Generator.copy_file(
  330. "bin/#{name}",
  331. Path.join(bin, name),
  332. force: overwrite?
  333. )
  334. # Files with the version appended are expected by the release
  335. # upgrade script `install_upgrade.escript`
  336. Mix.Generator.copy_file(
  337. Path.join(bin, name),
  338. Path.join(bin, name <> "-#{release.version}"),
  339. force: overwrite?
  340. )
  341. end
  342. for base_name <- ["emqx", "emqx_ctl"],
  343. suffix <- ["", "-#{release.version}"] do
  344. name = base_name <> suffix
  345. File.chmod!(Path.join(bin, name), 0o755)
  346. end
  347. Mix.Generator.copy_file(
  348. "bin/node_dump",
  349. Path.join(bin, "node_dump"),
  350. force: overwrite?
  351. )
  352. File.chmod!(Path.join(bin, "node_dump"), 0o755)
  353. render_template(
  354. "rel/BUILD_INFO",
  355. assigns,
  356. Path.join(release.version_path, "BUILD_INFO")
  357. )
  358. release
  359. end
  360. defp render_template(template, assigns, target) when is_binary(target) do
  361. render_template(template, assigns, [target])
  362. end
  363. defp render_template(template, assigns, tartgets) when is_list(tartgets) do
  364. rendered =
  365. File.read!(template)
  366. |> from_rebar_to_eex_template()
  367. |> EEx.eval_string(assigns)
  368. for target <- tartgets do
  369. File.write!(target, rendered)
  370. end
  371. end
  372. # needed by nodetool and by release_handler
  373. defp create_RELEASES(release) do
  374. apps =
  375. Enum.map(release.applications, fn {app_name, app_props} ->
  376. app_vsn = Keyword.fetch!(app_props, :vsn)
  377. app_path =
  378. "./lib"
  379. |> Path.join("#{app_name}-#{app_vsn}")
  380. |> to_charlist()
  381. {app_name, app_vsn, app_path}
  382. end)
  383. release_entry = [
  384. {
  385. :release,
  386. to_charlist(release.name),
  387. to_charlist(release.version),
  388. release.erts_version,
  389. apps,
  390. :permanent
  391. }
  392. ]
  393. release.path
  394. |> Path.join("releases")
  395. |> Path.join("RELEASES")
  396. |> File.open!([:write, :utf8], fn handle ->
  397. IO.puts(handle, "%% coding: utf-8")
  398. :io.format(handle, '~tp.~n', [release_entry])
  399. end)
  400. release
  401. end
  402. defp copy_escript(release, escript_name) do
  403. [shebang, rest] =
  404. "bin/#{escript_name}"
  405. |> File.read!()
  406. |> String.split("\n", parts: 2)
  407. # the elixir version of escript + start.boot required the boot_var
  408. # RELEASE_LIB to be defined.
  409. boot_var = "%%!-boot_var RELEASE_LIB $RUNNER_ROOT_DIR/lib"
  410. # Files with the version appended are expected by the release
  411. # upgrade script `install_upgrade.escript`
  412. Enum.each(
  413. [escript_name, escript_name <> "-" <> release.version],
  414. fn name ->
  415. path = Path.join([release.path, "bin", name])
  416. File.write!(path, [shebang, "\n", boot_var, "\n", rest])
  417. end
  418. )
  419. release
  420. end
  421. # The `:tar` built-in step in Mix Release does not currently add the
  422. # `etc` directory into the resulting tarball. The workaround is to
  423. # add those to the `:overlays` key before running `:tar`.
  424. # See: https://hexdocs.pm/mix/1.13.4/Mix.Release.html#__struct__/0
  425. defp prepare_tar_overlays(release) do
  426. Map.update!(
  427. release,
  428. :overlays,
  429. &[
  430. "etc",
  431. "data",
  432. "bin/node_dump"
  433. | &1
  434. ]
  435. )
  436. end
  437. #############################################################################
  438. # Helper functions
  439. #############################################################################
  440. defp template_vars(release, release_type, :bin = _package_type, edition_type) do
  441. [
  442. platform_data_dir: "data",
  443. platform_etc_dir: "etc",
  444. platform_log_dir: "log",
  445. platform_plugins_dir: "plugins",
  446. runner_bin_dir: "$RUNNER_ROOT_DIR/bin",
  447. emqx_etc_dir: "$RUNNER_ROOT_DIR/etc",
  448. runner_lib_dir: "$RUNNER_ROOT_DIR/lib",
  449. runner_log_dir: "$RUNNER_ROOT_DIR/log",
  450. runner_user: "",
  451. release_version: release.version,
  452. erts_vsn: release.erts_version,
  453. # FIXME: this is empty in `make emqx` ???
  454. erl_opts: "",
  455. emqx_description: emqx_description(release_type, edition_type),
  456. emqx_schema_mod: emqx_schema_mod(edition_type),
  457. is_elixir: "yes",
  458. is_enterprise: if(edition_type == :enterprise, do: "yes", else: "no")
  459. ] ++ build_info()
  460. end
  461. defp template_vars(release, release_type, :pkg = _package_type, edition_type) do
  462. [
  463. platform_data_dir: "/var/lib/emqx",
  464. platform_etc_dir: "/etc/emqx",
  465. platform_log_dir: "/var/log/emqx",
  466. platform_plugins_dir: "/var/lib/emqx/plugins",
  467. runner_bin_dir: "/usr/bin",
  468. emqx_etc_dir: "/etc/emqx",
  469. runner_lib_dir: "$RUNNER_ROOT_DIR/lib",
  470. runner_log_dir: "/var/log/emqx",
  471. runner_user: "emqx",
  472. release_version: release.version,
  473. erts_vsn: release.erts_version,
  474. # FIXME: this is empty in `make emqx` ???
  475. erl_opts: "",
  476. emqx_description: emqx_description(release_type, edition_type),
  477. emqx_schema_mod: emqx_schema_mod(edition_type),
  478. is_elixir: "yes",
  479. is_enterprise: if(edition_type == :enterprise, do: "yes", else: "no")
  480. ] ++ build_info()
  481. end
  482. defp emqx_description(release_type, edition_type) do
  483. case {release_type, edition_type} do
  484. {:cloud, :enterprise} ->
  485. "EMQX Enterprise"
  486. {:cloud, :community} ->
  487. "EMQX"
  488. end
  489. end
  490. defp emqx_schema_mod(:enterprise), do: :emqx_enterprise_conf_schema
  491. defp emqx_schema_mod(:community), do: :emqx_conf_schema
  492. defp bcrypt_dep() do
  493. if enable_bcrypt?(),
  494. do: [{:bcrypt, github: "emqx/erlang-bcrypt", tag: "0.6.0", override: true}],
  495. else: []
  496. end
  497. defp jq_dep() do
  498. if enable_jq?(),
  499. do: [{:jq, github: "emqx/jq", tag: "v0.3.4", override: true}],
  500. else: []
  501. end
  502. defp quicer_dep() do
  503. if enable_quicer?(),
  504. # in conflict with emqx and emqtt
  505. do: [{:quicer, github: "emqx/quic", tag: "0.0.9", override: true}],
  506. else: []
  507. end
  508. defp enable_bcrypt?() do
  509. not win32?()
  510. end
  511. defp enable_jq?() do
  512. not win32?()
  513. end
  514. defp enable_quicer?() do
  515. not Enum.any?([
  516. build_without_quic?(),
  517. win32?(),
  518. centos6?()
  519. ])
  520. end
  521. defp pkg_vsn() do
  522. %{edition_type: edition_type} = check_profile!()
  523. basedir = Path.dirname(__ENV__.file)
  524. script = Path.join(basedir, "pkg-vsn.sh")
  525. os_cmd(script, [Atom.to_string(edition_type)])
  526. end
  527. defp os_cmd(script, args) do
  528. {str, 0} = System.cmd("bash", [script | args])
  529. String.trim(str)
  530. end
  531. defp win32?(),
  532. do: match?({:win_32, _}, :os.type())
  533. defp centos6?() do
  534. case File.read("/etc/centos-release") do
  535. {:ok, "CentOS release 6" <> _} ->
  536. true
  537. _ ->
  538. false
  539. end
  540. end
  541. defp build_without_quic?() do
  542. opt = System.get_env("BUILD_WITHOUT_QUIC", "false")
  543. String.downcase(opt) != "false"
  544. end
  545. defp from_rebar_to_eex_template(str) do
  546. # we must not consider surrounding space in the template var name
  547. # because some help strings contain informative variables that
  548. # should not be interpolated, and those have no spaces.
  549. Regex.replace(
  550. ~r/\{\{ ([a-zA-Z0-9_]+) \}\}/,
  551. str,
  552. "<%= \\g{1} %>"
  553. )
  554. end
  555. defp build_info() do
  556. [
  557. build_info_arch: to_string(:erlang.system_info(:system_architecture)),
  558. build_info_wordsize: wordsize(),
  559. build_info_os: os_cmd("./scripts/get-distro.sh", []),
  560. build_info_erlang: otp_release(),
  561. build_info_elixir: System.version(),
  562. build_info_relform: System.get_env("EMQX_REL_FORM", "tgz")
  563. ]
  564. end
  565. # https://github.com/erlang/rebar3/blob/e3108ac187b88fff01eca6001a856283a3e0ec87/src/rebar_utils.erl#L142
  566. defp wordsize() do
  567. size =
  568. try do
  569. :erlang.system_info({:wordsize, :external})
  570. rescue
  571. ErlangError ->
  572. :erlang.system_info(:wordsize)
  573. end
  574. to_string(8 * size)
  575. end
  576. defp normalize_env!() do
  577. env =
  578. case Mix.env() do
  579. :dev ->
  580. :emqx
  581. env ->
  582. env
  583. end
  584. Mix.env(env)
  585. end
  586. # As from Erlang/OTP 17, the OTP release number corresponds to the
  587. # major OTP version number. No erlang:system_info() argument gives
  588. # the exact OTP version.
  589. # https://www.erlang.org/doc/man/erlang.html#system_info_otp_release
  590. # https://github.com/erlang/rebar3/blob/e3108ac187b88fff01eca6001a856283a3e0ec87/src/rebar_utils.erl#L572-L577
  591. defp otp_release() do
  592. major_version = System.otp_release()
  593. root_dir = to_string(:code.root_dir())
  594. [root_dir, "releases", major_version, "OTP_VERSION"]
  595. |> Path.join()
  596. |> File.read()
  597. |> case do
  598. {:error, _} ->
  599. major_version
  600. {:ok, version} ->
  601. version
  602. |> String.trim()
  603. |> String.split("**")
  604. |> List.first()
  605. end
  606. end
  607. end