diff --git a/apps/emqx/mix.exs b/apps/emqx/mix.exs index 2daf2cfb9..1ff0d23bb 100644 --- a/apps/emqx/mix.exs +++ b/apps/emqx/mix.exs @@ -1,5 +1,6 @@ defmodule EMQX.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -19,6 +20,7 @@ defmodule EMQX.MixProject do def application do [ mod: {:emqx_app, []}, + applications: EMQX.Mix.Common.erl_apps(:emqx), extra_applications: [:logger, :os_mon, :syntax_tools] ] end @@ -33,7 +35,7 @@ defmodule EMQX.MixProject do {:ekka, github: "emqx/ekka", tag: "0.11.1"}, # {:gen_rpc, github: "emqx/gen_rpc", tag: "2.5.1"}, # {:cuttlefish, github: "emqx/cuttlefish", tag: "v4.0.1"}, - {:hocon, github: "emqx/hocon", tag: "0.22.0"}, + {:hocon, github: "emqx/hocon", tag: "0.22.0", runtime: false}, # {:pbkdf2, github: "emqx/erlang-pbkdf2", tag: "2.0.4"}, # {:snabbkaffe, github: "kafka4beam/snabbkaffe", tag: "0.14.0"}, # {:jiffy, github: "emqx/jiffy", tag: "1.0.5"}, diff --git a/apps/emqx_authz/mix.exs b/apps/emqx_authz/mix.exs index 075691772..5395eaf8c 100644 --- a/apps/emqx_authz/mix.exs +++ b/apps/emqx_authz/mix.exs @@ -1,9 +1,12 @@ defmodule EMQXAuthz.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") + + @app :emqx_authz def project do [ - app: :emqx_authz, + app: @app, version: "0.1.0", build_path: "../../_build", config_path: "../../config/config.exs", @@ -17,6 +20,8 @@ defmodule EMQXAuthz.MixProject do def application do [ + mod: EMQX.Mix.Common.from_erl!(@app, :mod), + applications: EMQX.Mix.Common.from_erl!(@app, :applications), extra_applications: [:logger] ] end diff --git a/apps/emqx_bridge/mix.exs b/apps/emqx_bridge/mix.exs index f9a94ad47..228fdcfc5 100644 --- a/apps/emqx_bridge/mix.exs +++ b/apps/emqx_bridge/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXBridge.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -20,6 +21,7 @@ defmodule EMQXBridge.MixProject do [ registered: [], mod: {:emqx_bridge_app, []}, + applications: EMQX.Mix.Common.from_erl!(:emqx_bridge, :applications), extra_applications: [:logger] ] end diff --git a/apps/emqx_conf/mix.exs b/apps/emqx_conf/mix.exs index ca38717d5..f4bbf0eec 100644 --- a/apps/emqx_conf/mix.exs +++ b/apps/emqx_conf/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXConf.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -19,6 +20,8 @@ defmodule EMQXConf.MixProject do def application do [ mod: {:emqx_conf_app, []}, + # applications: EMQX.Mix.Common.erl_apps(:emqx_conf), + # included_applications: [:hocon], # extra_applications: [:logger, :os_mon, :syntax_tools] ] end @@ -26,7 +29,7 @@ defmodule EMQXConf.MixProject do defp deps do [ {:emqx, in_umbrella: true, runtime: false}, - # {:hocon, github: "emqx/hocon"} + {:hocon, github: "emqx/hocon", tag: "0.22.0"}, ] end end diff --git a/apps/emqx_connector/mix.exs b/apps/emqx_connector/mix.exs index ae6fb0b09..daf7dab84 100644 --- a/apps/emqx_connector/mix.exs +++ b/apps/emqx_connector/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXConnector.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -18,6 +19,7 @@ defmodule EMQXConnector.MixProject do def application do [ mod: {:emqx_connector_app, []}, + applications: EMQX.Mix.Common.from_erl!(:emqx_connector, :applications), extra_applications: [:logger] ] end @@ -28,6 +30,8 @@ defmodule EMQXConnector.MixProject do {:epgsql, github: "epgsql/epgsql", tag: "4.4.0"}, {:mysql, github: "emqx/mysql-otp", tag: "1.7.1"}, {:emqtt, github: "emqx/emqtt", tag: "1.4.3"}, + {:eredis_cluster, github: "emqx/eredis_cluster", tag: "0.6.7"}, + {:mongodb, github: "emqx/mongodb-erlang", tag: "v3.0.10"}, # {:ecpool, github: "emqx/ecpool", tag: "0.5.1"}, # {:emqtt, github: "emqx/emqtt", tag: "1.4.3"} ] diff --git a/apps/emqx_dashboard/mix.exs b/apps/emqx_dashboard/mix.exs index ff8f1eac6..97b1fb469 100644 --- a/apps/emqx_dashboard/mix.exs +++ b/apps/emqx_dashboard/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXDashboard.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -20,6 +21,7 @@ defmodule EMQXDashboard.MixProject do [ registered: [:emqx_dashboard_sup], mod: {:emqx_dashboard_app, []}, + applications: EMQX.Mix.Common.from_erl!(:emqx_dashboard, :applications), extra_applications: [:logger] ] end diff --git a/apps/emqx_exhook/mix.exs b/apps/emqx_exhook/mix.exs index 4a90501b0..e599ddec0 100644 --- a/apps/emqx_exhook/mix.exs +++ b/apps/emqx_exhook/mix.exs @@ -28,7 +28,7 @@ defmodule EMQXExhook.MixProject do defp deps do [ - {:emqx, in_umbrella: true, runtime: false}, + {:emqx, in_umbrella: true}, {:grpc, github: "emqx/grpc-erl", tag: "0.6.2"}, ] end diff --git a/apps/emqx_gateway/mix.exs b/apps/emqx_gateway/mix.exs index 32c3b856c..76f45b5cf 100644 --- a/apps/emqx_gateway/mix.exs +++ b/apps/emqx_gateway/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXGateway.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -22,6 +23,7 @@ defmodule EMQXGateway.MixProject do [ registered: [], mod: {:emqx_gateway_app, []}, + applications: EMQX.Mix.Common.from_erl!(:emqx_gateway, :applications), extra_applications: [:logger] ] end diff --git a/apps/emqx_machine/src/emqx_machine_boot.erl b/apps/emqx_machine/src/emqx_machine_boot.erl index d1347693f..bef68a319 100644 --- a/apps/emqx_machine/src/emqx_machine_boot.erl +++ b/apps/emqx_machine/src/emqx_machine_boot.erl @@ -110,14 +110,37 @@ reboot_apps() -> sorted_reboot_apps() -> Apps = [{App, app_deps(App)} || App <- reboot_apps()], + ?SLOG(warning, #{ msg => "sorted_reboot_apps/0 before /1" + , apps => Apps + }), Res = sorted_reboot_apps(Apps), io:format(user, "~n>>>>>>>>>>>>>> sorted_reboot_apps~n ~100p~n", [Res]), - Res. + Res, + %% FIXME!!!!! For some reason, emqx_conf appears in the + %% `applications` key in resource, but it is not there in the + %% .app.src... + Res0 = [emqx_conf,gproc,esockd,ranch,cowboy,emqx,emqx_prometheus,emqx_modules,emqx_dashboard, + emqx_gateway,emqx_management,emqx_retainer,emqx_statsd,emqx_resource,emqx_connector,emqx_bridge, + emqx_authn,emqx_authz,emqx_exhook], + case true of + true -> + Res; + false -> + Res0 + end. app_deps(App) -> case application:get_key(App, applications) of undefined -> []; - {ok, List} -> lists:filter(fun(A) -> lists:member(A, reboot_apps()) end, List) + {ok, List} -> + ?SLOG(warning, #{ msg => ">>>>>> machine_boot:add_deps" + , app => App + , apps => List + , included => application:get_key(App, included_applications) + }), + lists:filter(fun(A) -> + lists:member(A, reboot_apps()) + end, List) end. sorted_reboot_apps(Apps) -> diff --git a/apps/emqx_management/mix.exs b/apps/emqx_management/mix.exs index b9f8ec078..0838bf49a 100644 --- a/apps/emqx_management/mix.exs +++ b/apps/emqx_management/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXManagement.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -20,6 +21,7 @@ defmodule EMQXManagement.MixProject do [ registered: [:emqx_management_sup], mod: {:emqx_mgmt_app, []}, + applications: EMQX.Mix.Common.from_erl!(:emqx_management, :applications), extra_applications: [:logger, :syntax_tools] ] end diff --git a/apps/emqx_modules/mix.exs b/apps/emqx_modules/mix.exs index a842654d3..b931a823c 100644 --- a/apps/emqx_modules/mix.exs +++ b/apps/emqx_modules/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXModules.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -19,6 +20,7 @@ defmodule EMQXModules.MixProject do [ registered: [:emqx_mod_sup], mod: {:emqx_modules_app, []}, + applications: EMQX.Mix.Common.from_erl!(:emqx_modules, :applications), extra_applications: [:logger] ] end diff --git a/apps/emqx_prometheus/mix.exs b/apps/emqx_prometheus/mix.exs index caac0c13a..2e3a6b4e7 100644 --- a/apps/emqx_prometheus/mix.exs +++ b/apps/emqx_prometheus/mix.exs @@ -10,7 +10,7 @@ defmodule EMQXPrometheus.MixProject do deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.12", - start_permanent: Mix.env() == :prod, + # start_permanent: Mix.env() == :prod, deps: deps(), description: "Prometheus for EMQ X" ] @@ -26,7 +26,7 @@ defmodule EMQXPrometheus.MixProject do defp deps do [ - {:emqx, in_umbrella: true, runtime: false}, + {:emqx, in_umbrella: true}, {:prometheus, github: "emqx/prometheus.erl", tag: "v3.1.1"} ] end diff --git a/apps/emqx_resource/mix.exs b/apps/emqx_resource/mix.exs index 1b35204ad..fe3edbaa0 100644 --- a/apps/emqx_resource/mix.exs +++ b/apps/emqx_resource/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXResource.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -11,7 +12,7 @@ defmodule EMQXResource.MixProject do lockfile: "../../mix.lock", elixir: "~> 1.12", # start_permanent: Mix.env() == :prod, - start_permanent: false, + # start_permanent: false, deps: deps() ] end @@ -19,7 +20,8 @@ defmodule EMQXResource.MixProject do def application do [ mod: {:emqx_resource_app, []}, - extra_applications: [:logger, :syntax_tools] + applications: EMQX.Mix.Common.erl_apps(:emqx_resource), + # extra_applications: [:emqx, :emqx_conf], ] end @@ -28,7 +30,8 @@ defmodule EMQXResource.MixProject do # {:jsx, "3.1.0"}, # {:gproc, "0.9.0"}, {:hocon, github: "emqx/hocon", tag: "0.22.0", runtime: false}, - {:emqx, in_umbrella: true, runtime: false} + {:emqx, in_umbrella: true}, + {:emqx_conf, in_umbrella: true}, ] end end diff --git a/apps/emqx_resource/src/emqx_resource.app.src b/apps/emqx_resource/src/emqx_resource.app.src index 1b93aa0de..65e2a433c 100644 --- a/apps/emqx_resource/src/emqx_resource.app.src +++ b/apps/emqx_resource/src/emqx_resource.app.src @@ -8,9 +8,10 @@ [kernel, stdlib, gproc, - hocon, + %% hocon, jsx, - emqx + emqx, + emqx_conf ]}, {env,[]}, {modules, []}, diff --git a/apps/emqx_retainer/mix.exs b/apps/emqx_retainer/mix.exs index 730079ad4..7fd1b7077 100644 --- a/apps/emqx_retainer/mix.exs +++ b/apps/emqx_retainer/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXRetainer.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -10,23 +11,24 @@ defmodule EMQXRetainer.MixProject do deps_path: "../../deps", lockfile: "../../mix.lock", elixir: "~> 1.12", - start_permanent: Mix.env() == :prod, + # start_permanent: Mix.env() == :prod, deps: deps(), description: "EMQ X Retainer" ] end - # def application do - # [ - # registered: [:emqx_retainer_sup], - # mod: {:emqx_retainer_app, []}, - # # extra_applications: [:logger] - # ] - # end + def application do + [ + registered: [:emqx_retainer_sup], + mod: {:emqx_retainer_app, []}, + applications: EMQX.Mix.Common.erl_apps(:emqx_retainer), + # extra_applications: [:emqx], + ] + end defp deps do [ - {:emqx, in_umbrella: true, runtime: false} + {:emqx, in_umbrella: true} ] end end diff --git a/apps/emqx_statsd/mix.exs b/apps/emqx_statsd/mix.exs index 6823daefe..a2bd0ec64 100644 --- a/apps/emqx_statsd/mix.exs +++ b/apps/emqx_statsd/mix.exs @@ -1,5 +1,6 @@ defmodule EMQXStatsd.MixProject do use Mix.Project + Code.require_file("../../lib/emqx/mix/common.ex") def project do [ @@ -19,6 +20,7 @@ defmodule EMQXStatsd.MixProject do def application do [ mod: {:emqx_statsd_app, []}, + applications: EMQX.Mix.Common.from_erl!(:emqx_statsd, :applications), extra_applications: [:logger] ] end diff --git a/lib/emqx/mix/common.ex b/lib/emqx/mix/common.ex new file mode 100644 index 000000000..41b5da4dc --- /dev/null +++ b/lib/emqx/mix/common.ex @@ -0,0 +1,11 @@ +defmodule EMQX.Mix.Common do + def erl_apps(app) do + from_erl!(app, :applications) + end + + def from_erl!(app, key) do + path = Path.join("src", "#{app}.app.src") + {:ok, [{:application, ^app, props}]} = :file.consult(path) + Keyword.fetch!(props, key) + end +end diff --git a/lib/emqx/mix/helpers.exs b/lib/emqx/mix/helpers.exs new file mode 100644 index 000000000..b1742a4a8 --- /dev/null +++ b/lib/emqx/mix/helpers.exs @@ -0,0 +1,24 @@ +defmodule EMQX.Mix.Helpers do + def read_rebar_config!(filepath) do + {:ok, config} = read_rebar_config(filepath) + config + end + + def read_rebar_config(filepath) do + filepath + |> to_charlist() + |> :file.consult() + end + + def rebar_to_mix_dep({name, {:git, url, {:tag, tag}}}), + do: {name, git: to_string(url), tag: to_string(tag)} + + def rebar_to_mix_dep({name, {:git, url, {:ref, ref}}}), + do: {name, git: to_string(url), ref: to_string(ref)} + + def rebar_to_mix_dep({name, {:git, url, {:branch, branch}}}), + do: {name, git: to_string(url), branch: to_string(branch)} + + def rebar_to_mix_dep({name, vsn}) when is_list(vsn), + do: {name, to_string(vsn)} +end diff --git a/lib/mix/tasks/compile/render_config.exs b/lib/mix/tasks/compile/render_config.exs new file mode 100644 index 000000000..a7d18e016 --- /dev/null +++ b/lib/mix/tasks/compile/render_config.exs @@ -0,0 +1,9 @@ +defmodule Mix.Tasks.Compile.RenderConfig do + use Mix.Task.Compiler + + def run(_args) do + cmd = "./rebar3" + args = ["deps"] + System.cmd(cmd, args, env: [{"DEBUG", "1"}], into: IO.stream(:stdio, :line)) + end +end diff --git a/mix.exs b/mix.exs index 06b9eed1a..083e22c50 100644 --- a/mix.exs +++ b/mix.exs @@ -1,6 +1,8 @@ defmodule EMQXUmbrella.MixProject do use Mix.Project + Code.load_file("./mix_release.exs") + def project do [ apps_path: "apps", @@ -107,6 +109,7 @@ defmodule EMQXUmbrella.MixProject do compiler: :permanent, runtime_tools: :permanent, # emqx_conf: :permanent, + # hocon: :load, emqx: :load, emqx_conf: :load, # as per rebar.config.erl emqx_machine: :permanent, @@ -119,7 +122,7 @@ defmodule EMQXUmbrella.MixProject do emqx_connector: :permanent, emqx_authn: :permanent, emqx_authz: :permanent, - # emqx_auto_subscribe: :permanent, + emqx_auto_subscribe: :permanent, emqx_gateway: :permanent, emqx_exhook: :permanent, emqx_bridge: :permanent, @@ -128,13 +131,13 @@ defmodule EMQXUmbrella.MixProject do emqx_management: :permanent, emqx_dashboard: :permanent, emqx_statsd: :permanent, - # emqx_retainer: :permanent, - emqx_retainer: :none, + emqx_retainer: :permanent, + # emqx_retainer: :none, emqx_prometheus: :permanent, - # emqx_psk: :permanent, - emqx_psk: :none, - # emqx_limiter: :permanent, - emqx_limiter: :none, + emqx_psk: :permanent, + # emqx_psk: :none, + emqx_limiter: :permanent, + # emqx_limiter: :none, observer: :load, # emqx_mix: :none, ], diff --git a/mix.lock b/mix.lock index 916c6f6ed..7c03e7dc3 100644 --- a/mix.lock +++ b/mix.lock @@ -5,6 +5,8 @@ "cowboy_swagger": {:git, "https://github.com/inaka/cowboy_swagger", "bc441df7988da0f5c5d11ae0861c394dc30995c5", [tag: "2.5.0"]}, "cowlib": {:hex, :cowlib, "2.11.0", "0b9ff9c346629256c42ebe1eeb769a83c6cb771a6ee5960bd110ab0b9b872063", [:make, :rebar3], [], "hexpm", "2b3e9da0b21c4565751a6d4901c20d1b4cc25cbb7fd50d91d2ab6dd287bc86a9"}, "cuttlefish": {:git, "https://github.com/emqx/cuttlefish.git", "6c346563e89ebbd95dbc1c29017adaf9abf85ca1", []}, + "earmark": {:hex, :earmark, "1.4.19", "3854a17305c880cc46305af15fb1630568d23a709aba21aaa996ced082fc29d7", [:mix], [{:earmark_parser, ">= 1.4.18", [hex: :earmark_parser, repo: "hexpm", optional: false]}], "hexpm", "d5a8c9f9e37159a8fdd3ea8437fb4e229eaf56d5129b9a011dc4780a4872079d"}, + "earmark_parser": {:hex, :earmark_parser, "1.4.18", "e1b2be73eb08a49fb032a0208bf647380682374a725dfb5b9e510def8397f6f2", [:mix], [], "hexpm", "114a0e85ec3cf9e04b811009e73c206394ffecfcc313e0b346de0d557774ee97"}, "ecpool": {:git, "https://github.com/emqx/ecpool.git", "0516d2cebd14654ef8c583c347e4a0b01363b86d", [tag: "0.5.1"]}, "eetcd": {:hex, :eetcd, "0.3.4", "27e8b4775230c53a9ef602f62a1603591302b40b2eb195d567edffb35b6cf1a2", [:rebar3], [{:gun, "1.3.3", [hex: :gun, repo: "hexpm", optional: false]}], "hexpm", "b763c0e1a9741d39a62f5a19186a342863eacbc769151c4e81db5790efecefca"}, "ehttpc": {:git, "https://github.com/emqx/ehttpc.git", "7b1a76b2353b385725e62f948cd399c7040467f8", [tag: "0.1.12"]}, @@ -13,11 +15,12 @@ "emqtt": {:git, "https://github.com/emqx/emqtt.git", "25892ef48a979a9dfbd74d86133cb28cf11f3cf4", [tag: "1.4.3"]}, "emqx_http_lib": {:git, "https://github.com/emqx/emqx_http_lib.git", "9a1aafcbad1bb35392ebabc0cf102c7bce660432", [tag: "0.4.0"]}, "epgsql": {:git, "https://github.com/epgsql/epgsql.git", "895c8f9d53f08d09ec6a0301c56d3d6f270929f2", [tag: "4.4.0"]}, - "eredis": {:git, "https://github.com/emqx/eredis", "75f2b8eedbe631136326680225efbcd2684e93e7", [tag: "1.2.5"]}, - "eredis_cluster": {:git, "https://github.com/emqx/eredis_cluster", "624749b4aef25668e9c7a545427fdc663a04faef", [tag: "0.6.7"]}, + "eredis": {:hex, :eredis, "1.2.0", "0b8e9cfc2c00fa1374cd107ea63b49be08d933df2cf175e6a89b73dd9c380de4", [:rebar3], [], "hexpm", "d9b5abef2c2c8aba8f32aa018203e0b3dc8b1157773b254ab1d4c2002317f1e1"}, + "eredis_cluster": {:git, "https://github.com/emqx/eredis_cluster.git", "624749b4aef25668e9c7a545427fdc663a04faef", [tag: "0.6.7"]}, "esasl": {:git, "https://github.com/emqx/esasl.git", "96d7ac9f6c156017dd35b30df2dd722ae469c7f0", [tag: "0.2.0"]}, "esockd": {:git, "https://github.com/emqx/esockd.git", "9b959fc11a1c398a589892f335235be6c5b4a454", [tag: "5.8.0"]}, "estatsd": {:git, "https://github.com/emqx/estatsd.git", "5184d846b7ecb83509bd4d32695c60428c0198cd", [tag: "0.1.0"]}, + "ex_doc": {:hex, :ex_doc, "0.19.3", "3c7b0f02851f5fc13b040e8e925051452e41248f685e40250d7e40b07b9f8c10", [:mix], [{:earmark, "~> 1.2", [hex: :earmark, repo: "hexpm", optional: false]}, {:makeup_elixir, "~> 0.10", [hex: :makeup_elixir, repo: "hexpm", optional: false]}], "hexpm", "0e11d67e662142fc3945b0ee410c73c8c956717fbeae4ad954b418747c734973"}, "gen_coap": {:git, "https://github.com/emqx/gen_coap.git", "9bf5e7f795badf68e2fb4eb226f576308f5b1bb4", [tag: "v0.3.2"]}, "gen_rpc": {:git, "https://github.com/emqx/gen_rpc.git", "fb7418dc8cf7e97d153fba073bee0fac07dce753", [tag: "2.5.1"]}, "getopt": {:git, "https://github.com/emqx/getopt.git", "215f2083408e1fe562d441aea6062bf5d9e1fb67", [tag: "v1.0.2"]}, @@ -30,11 +33,14 @@ "jose": {:hex, :jose, "1.11.2", "f4c018ccf4fdce22c71e44d471f15f723cb3efab5d909ab2ba202b5bf35557b3", [:mix, :rebar3], [], "hexpm", "98143fbc48d55f3a18daba82d34fe48959d44538e9697c08f34200fa5f0947d2"}, "jsx": {:hex, :jsx, "3.1.0", "d12516baa0bb23a59bb35dccaf02a1bd08243fcbb9efe24f2d9d056ccff71268", [:rebar3], [], "hexpm", "0c5cc8fdc11b53cc25cf65ac6705ad39e54ecc56d1c22e4adb8f5a53fb9427f3"}, "lc": {:git, "https://github.com/qzhuyan/lc.git", "6f98d098e5aaf4fcd6afbbb2acca96855c474600", [tag: "0.1.2"]}, + "makeup": {:hex, :makeup, "1.0.5", "d5a830bc42c9800ce07dd97fa94669dfb93d3bf5fcf6ea7a0c67b2e0e4a7f26c", [:mix], [{:nimble_parsec, "~> 0.5 or ~> 1.0", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "cfa158c02d3f5c0c665d0af11512fed3fba0144cf1aadee0f2ce17747fba2ca9"}, + "makeup_elixir": {:hex, :makeup_elixir, "0.15.2", "dc72dfe17eb240552857465cc00cce390960d9a0c055c4ccd38b70629227e97c", [:mix], [{:makeup, "~> 1.0", [hex: :makeup, repo: "hexpm", optional: false]}, {:nimble_parsec, "~> 1.1", [hex: :nimble_parsec, repo: "hexpm", optional: false]}], "hexpm", "fd23ae48d09b32eff49d4ced2b43c9f086d402ee4fd4fcb2d7fad97fa8823e75"}, "minirest": {:git, "https://github.com/emqx/minirest.git", "f3f80b3e07295d8b6db22ed456318e0cc9dd167f", [tag: "1.2.7"]}, "mnesia_rocksdb": {:git, "https://github.com/k32/mnesia_rocksdb", "42715650a3b84fe13d23a1e7f65815ff36386b0f", [tag: "0.1.3-k32"]}, - "mongodb": {:git, "https://github.com/emqx/mongodb-erlang", "2ffe62f42dafb98eaafead9d340a674c5f9279a5", [tag: "v3.0.10"]}, + "mongodb": {:git, "https://github.com/emqx/mongodb-erlang.git", "2ffe62f42dafb98eaafead9d340a674c5f9279a5", [tag: "v3.0.10"]}, "mria": {:git, "https://github.com/emqx/mria.git", "0887b3fa2b3576175ac38d32e35177635f01621a", [tag: "0.1.4"]}, "mysql": {:git, "https://github.com/emqx/mysql-otp.git", "bdabac44cc8836a9e23897b7e1b77c7df7e04f70", [tag: "1.7.1"]}, + "nimble_parsec": {:hex, :nimble_parsec, "1.2.0", "b44d75e2a6542dcb6acf5d71c32c74ca88960421b6874777f79153bbbbd7dccc", [:mix], [], "hexpm", "52b2871a7515a5ac49b00f214e4165a40724cf99798d8e4a65e4fd64ebd002c1"}, "pbkdf2": {:git, "https://github.com/emqx/erlang-pbkdf2.git", "45d9981209ea07a83a58cf85aaf8236457da4342", [tag: "2.0.4"]}, "poolboy": {:git, "https://github.com/emqx/poolboy.git", "29be47db8c2be38b18c908e43a80ebb7b9b6116b", [tag: "1.5.2"]}, "prometheus": {:git, "https://github.com/emqx/prometheus.erl.git", "a41488df09472448057d264ef520cf2f71d925f8", [tag: "v3.1.1"]}, diff --git a/mix_release.exs b/mix_release.exs new file mode 100644 index 000000000..9a92a408e --- /dev/null +++ b/mix_release.exs @@ -0,0 +1,874 @@ +defmodule Mix.Release do + IO.puts( + IO.ANSI.red() + <> "I live!!" + <> IO.ANSI.reset() + ) + + @moduledoc """ + Defines the release structure and convenience for assembling releases. + """ + + @doc """ + The Mix.Release struct has the following read-only fields: + + * `:name` - the name of the release as an atom + * `:version` - the version of the release as a string or + `{:from_app, app_name}` + * `:path` - the path to the release root + * `:version_path` - the path to the release version inside the release + * `:applications` - a map of application with their definitions + * `:erts_source` - the ERTS source as a charlist (or nil) + * `:erts_version` - the ERTS version as a charlist + + The following fields may be modified as long as they keep their defined types: + + * `:boot_scripts` - a map of boot scripts with the boot script name + as key and a keyword list with **all** applications that are part of + it and their modes as value + * `:config_providers` - a list of `{config_provider, term}` tuples where the + first element is a module that implements the `Config.Provider` behaviour + and `term` is the value given to it on `c:Config.Provider.init/1` + * `:options` - a keyword list with all other user supplied release options + * `:overlays` - a list of extra files added to the release. If you have a custom + step adding extra files to a release, you can add these files to the `:overlays` + field so they are also considered on further commands, such as tar/zip. Each entry + in overlays is the relative path to the release root of each file + * `:steps` - a list of functions that receive the release and returns a release. + Must also contain the atom `:assemble` which is the internal assembling step. + May also contain the atom `:tar` to create a tarball of the release. + + """ + defstruct [ + :name, + :version, + :path, + :version_path, + :applications, + :boot_scripts, + :erts_source, + :erts_version, + :config_providers, + :options, + :overlays, + :steps + ] + + @type mode :: :permanent | :transient | :temporary | :load | :none + @type application :: atom() + @type t :: %__MODULE__{ + name: atom(), + version: String.t(), + path: String.t(), + version_path: String.t() | {:from_app, application()}, + applications: %{application() => keyword()}, + boot_scripts: %{atom() => [{application(), mode()}]}, + erts_version: charlist(), + erts_source: charlist() | nil, + config_providers: [{module, term}], + options: keyword(), + overlays: list(String.t()), + steps: [(t -> t) | :assemble, ...] + } + + @default_apps [kernel: :permanent, stdlib: :permanent, elixir: :permanent, sasl: :permanent] + @safe_modes [:permanent, :temporary, :transient] + @unsafe_modes [:load, :none] + @significant_chunks ~w(Atom AtU8 Attr Code StrT ImpT ExpT FunT LitT Line)c + @copy_app_dirs ["priv"] + + @doc false + @spec from_config!(atom, keyword, keyword) :: t + def from_config!(name, config, overrides) do + {name, apps, opts} = find_release(name, config) + + unless Atom.to_string(name) =~ ~r/^[a-z][a-z0-9_]*$/ do + Mix.raise( + "Invalid release name. A release name must start with a lowercase ASCII letter, " <> + "followed by lowercase ASCII letters, numbers, or underscores, got: #{inspect(name)}" + ) + end + + opts = + [overwrite: false, quiet: false, strip_beams: true] + |> Keyword.merge(opts) + |> Keyword.merge(overrides) + + {include_erts, opts} = Keyword.pop(opts, :include_erts, true) + {erts_source, erts_lib_dir, erts_version} = erts_data(include_erts) + + deps_apps = Mix.Project.deps_apps() + loaded_apps = apps |> Keyword.keys() |> load_apps(deps_apps, %{}, erts_lib_dir, [], :root) + + # Make sure IEx is either an active part of the release or add it as none. + {loaded_apps, apps} = + if Map.has_key?(loaded_apps, :iex) do + {loaded_apps, apps} + else + {load_apps([:iex], deps_apps, loaded_apps, erts_lib_dir, [], :root), apps ++ [iex: :none]} + end + + start_boot = build_start_boot(loaded_apps, apps) + start_clean_boot = build_start_clean_boot(start_boot) + + {path, opts} = + Keyword.pop_lazy(opts, :path, fn -> + Path.join([Mix.Project.build_path(config), "rel", Atom.to_string(name)]) + end) + + path = Path.absname(path) + + {version, opts} = + Keyword.pop_lazy(opts, :version, fn -> + config[:version] || + Mix.raise( + "No :version found. Please make sure a :version is set in your project definition " <> + "or inside the release the configuration" + ) + end) + + version = + case version do + {:from_app, app} -> + Application.load(app) + version = Application.spec(app, :vsn) + + if !version do + Mix.raise( + "Could not find version for #{inspect(app)}, please make sure the application exists" + ) + end + + to_string(version) + + "" -> + Mix.raise("The release :version cannot be an empty string") + + _ -> + version + end + + {config_providers, opts} = Keyword.pop(opts, :config_providers, []) + {steps, opts} = Keyword.pop(opts, :steps, [:assemble]) + validate_steps!(steps) + + %Mix.Release{ + name: name, + version: version, + path: path, + version_path: Path.join([path, "releases", version]), + erts_source: erts_source, + erts_version: erts_version, + applications: loaded_apps, + boot_scripts: %{start: start_boot, start_clean: start_clean_boot}, + config_providers: config_providers, + options: opts, + overlays: [], + steps: steps + } + end + + defp find_release(name, config) do + {name, opts_fun_or_list} = lookup_release(name, config) || infer_release(config) + opts = if is_function(opts_fun_or_list, 0), do: opts_fun_or_list.(), else: opts_fun_or_list + {apps, opts} = Keyword.pop(opts, :applications, []) + + if apps == [] and Mix.Project.umbrella?(config) do + bad_umbrella!() + end + + app = Keyword.get(config, :app) + apps = Keyword.merge(@default_apps, apps) + + if is_nil(app) or Keyword.has_key?(apps, app) do + {name, apps, opts} + else + {name, apps ++ [{app, :permanent}], opts} + end + end + + defp lookup_release(nil, config) do + case Keyword.get(config, :releases, []) do + [] -> + nil + + [{name, opts}] -> + {name, opts} + + [_ | _] -> + case Keyword.get(config, :default_release) do + nil -> + Mix.raise( + "\"mix release\" was invoked without a name but there are multiple releases. " <> + "Please call \"mix release NAME\" or set :default_release in your project configuration" + ) + + name -> + lookup_release(name, config) + end + end + end + + defp lookup_release(name, config) do + if opts = config[:releases][name] do + {name, opts} + else + found = Keyword.get(config, :releases, []) + + Mix.raise( + "Unknown release #{inspect(name)}. " <> + "The available releases are: #{inspect(Keyword.keys(found))}" + ) + end + end + + defp infer_release(config) do + if Mix.Project.umbrella?(config) do + bad_umbrella!() + else + {Keyword.fetch!(config, :app), []} + end + end + + defp bad_umbrella! do + Mix.raise(""" + Umbrella projects require releases to be explicitly defined with \ + a non-empty applications key that chooses which umbrella children \ + should be part of the releases: + + releases: [ + foo: [ + applications: [child_app_foo: :permanent] + ], + bar: [ + applications: [child_app_bar: :permanent] + ] + ] + + Alternatively you can perform the release from the children applications + """) + end + + defp erts_data(erts_data) when is_function(erts_data) do + erts_data(erts_data.()) + end + + defp erts_data(false) do + {nil, :code.lib_dir(), :erlang.system_info(:version)} + end + + defp erts_data(true) do + version = :erlang.system_info(:version) + {:filename.join(:code.root_dir(), 'erts-#{version}'), :code.lib_dir(), version} + end + + defp erts_data(erts_source) when is_binary(erts_source) do + if File.exists?(erts_source) do + [_, erts_version] = erts_source |> Path.basename() |> String.split("-") + erts_lib_dir = erts_source |> Path.dirname() |> Path.join("lib") |> to_charlist() + {to_charlist(erts_source), erts_lib_dir, to_charlist(erts_version)} + else + Mix.raise("Could not find ERTS system at #{inspect(erts_source)}") + end + end + + defp load_apps(apps, deps_apps, seen, otp_root, optional, type) do + for app <- apps, reduce: seen do + seen -> + # IO.inspect(app, label: ">>>>>> load_apps app") + if reentrant_seen = reentrant(seen, app, type) do + reentrant_seen + else + load_app(app, deps_apps, seen, otp_root, optional, type) + end + end + end + + defp reentrant(seen, app, type) do + properties = seen[app] + + cond do + is_nil(properties) -> + nil + + type != :root and properties[:type] != type -> + if properties[:type] == :root do + put_in(seen[app][:type], type) + else + Mix.raise( + "#{inspect(app)} is listed both as a regular application and as an included application" + ) + end + + true -> + seen + end + end + + defp load_app(app, deps_apps, seen, otp_root, optional, type) do + cond do + path = app not in deps_apps && otp_path(otp_root, app) -> + do_load_app(app, path, deps_apps, seen, otp_root, true, type) + + path = code_path(app) -> + do_load_app(app, path, deps_apps, seen, otp_root, false, type) + + app in optional -> + seen + + true -> + Mix.raise("Could not find application #{inspect(app)}") + end + end + + defp otp_path(otp_root, app) do + path = Path.join(otp_root, "#{app}-*") + + case Path.wildcard(path) do + [] -> nil + paths -> paths |> Enum.sort() |> List.last() |> to_charlist() + end + end + + defp code_path(app) do + case :code.lib_dir(app) do + {:error, :bad_name} -> nil + path -> path + end + end + + defp do_load_app(app, path, deps_apps, seen, otp_root, otp_app?, type) do + case :file.consult(Path.join(path, "ebin/#{app}.app")) do + {:ok, terms} -> + [{:application, ^app, properties}] = terms + value = [path: path, otp_app?: otp_app?, type: type] ++ properties + seen = Map.put(seen, app, value) + applications = Keyword.get(properties, :applications, []) + optional = Keyword.get(properties, :optional_applications, []) + seen = load_apps(applications, deps_apps, seen, otp_root, optional, :depended) + included_applications = Keyword.get(properties, :included_applications, []) + load_apps(included_applications, deps_apps, seen, otp_root, [], :included) + + {:error, reason} -> + Mix.raise("Could not load #{app}.app. Reason: #{inspect(reason)}") + end + end + + defp build_start_boot(all_apps, specified_apps) do + specified_apps ++ + Enum.sort( + for( + {app, props} <- all_apps, + not List.keymember?(specified_apps, app, 0), + do: {app, default_mode(props)} + ) + ) + end + + defp default_mode(props) do + if props[:type] == :included, do: :load, else: :permanent + end + + defp build_start_clean_boot(boot) do + for({app, _mode} <- boot, do: {app, :none}) + |> Keyword.put(:stdlib, :permanent) + |> Keyword.put(:kernel, :permanent) + end + + defp validate_steps!(steps) do + valid_atoms = [:assemble, :tar] + + if not is_list(steps) or Enum.any?(steps, &(&1 not in valid_atoms and not is_function(&1, 1))) do + Mix.raise(""" + The :steps option must be a list of: + + * anonymous function that receives one argument + * the atom :assemble or :tar + + Got: #{inspect(steps)} + """) + end + + if Enum.count(steps, &(&1 == :assemble)) != 1 do + Mix.raise("The :steps option must contain the atom :assemble once, got: #{inspect(steps)}") + end + + if :assemble in Enum.drop_while(steps, &(&1 != :tar)) do + Mix.raise("The :tar step must come after :assemble") + end + + if Enum.count(steps, &(&1 == :tar)) > 1 do + Mix.raise("The :steps option can only contain the atom :tar once") + end + + :ok + end + + @doc """ + Makes the `sys.config` structure. + + If there are config providers, then a value is injected into + the `:elixir` application configuration in `sys_config` to be + read during boot and trigger the providers. + + It uses the following release options to customize its behaviour: + + * `:reboot_system_after_config` + * `:start_distribution_during_config` + * `:prune_runtime_sys_config_after_boot` + + In case there are no config providers, it doesn't change `sys_config`. + """ + @spec make_sys_config(t, keyword(), Config.Provider.config_path()) :: + :ok | {:error, String.t()} + def make_sys_config(release, sys_config, config_provider_path) do + {sys_config, runtime_config?} = + merge_provider_config(release, sys_config, config_provider_path) + + path = Path.join(release.version_path, "sys.config") + + args = [runtime_config?, sys_config] + format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~tw.~n" + File.mkdir_p!(Path.dirname(path)) + File.write!(path, IO.chardata_to_string(:io_lib.format(format, args))) + + case :file.consult(path) do + {:ok, _} -> + :ok + + {:error, reason} -> + invalid = + for {app, kv} <- sys_config, + {key, value} <- kv, + not valid_config?(value), + do: """ + + Application: #{inspect(app)} + Key: #{inspect(key)} + Value: #{inspect(value)} + """ + + message = + case invalid do + [] -> + "Could not read configuration file. Reason: #{inspect(reason)}" + + _ -> + "Could not read configuration file. It has invalid configuration terms " <> + "such as functions, references, and pids. Please make sure your configuration " <> + "is made of numbers, atoms, strings, maps, tuples and lists. The following entries " <> + "are wrong:\n#{Enum.join(invalid)}" + end + + {:error, message} + end + end + + defp valid_config?(m) when is_map(m), + do: Enum.all?(Map.delete(m, :__struct__), &valid_config?/1) + + defp valid_config?(l) when is_list(l), do: Enum.all?(l, &valid_config?/1) + defp valid_config?(t) when is_tuple(t), do: Enum.all?(Tuple.to_list(t), &valid_config?/1) + defp valid_config?(o), do: is_number(o) or is_atom(o) or is_binary(o) + + defp merge_provider_config(%{config_providers: []}, sys_config, _), do: {sys_config, false} + + defp merge_provider_config(release, sys_config, config_path) do + {reboot?, extra_config, initial_config} = start_distribution(release) + + prune_runtime_sys_config_after_boot = + Keyword.get(release.options, :prune_runtime_sys_config_after_boot, false) + + opts = [ + extra_config: initial_config, + prune_runtime_sys_config_after_boot: prune_runtime_sys_config_after_boot, + reboot_system_after_config: reboot?, + validate_compile_env: validate_compile_env(release) + ] + + init_config = Config.Provider.init(release.config_providers, config_path, opts) + {Config.Reader.merge(sys_config, init_config ++ extra_config), reboot?} + end + + defp validate_compile_env(release) do + with true <- Keyword.get(release.options, :validate_compile_env, true), + [_ | _] = compile_env <- compile_env(release) do + compile_env + else + _ -> false + end + end + + defp compile_env(release) do + for {_, properties} <- release.applications, + triplet <- Keyword.get(properties, :compile_env, []), + do: triplet + end + + defp start_distribution(%{options: opts}) do + reboot? = Keyword.get(opts, :reboot_system_after_config, false) + early_distribution? = Keyword.get(opts, :start_distribution_during_config, false) + + if not reboot? or early_distribution? do + {reboot?, [], []} + else + {true, [kernel: [start_distribution: false]], [kernel: [start_distribution: true]]} + end + end + + @doc """ + Copies the cookie to the given path. + + If a cookie option was given, we compare it with + the contents of the file (if any), and ask the user + if they want to override. + + If there is no option, we generate a random one + the first time. + """ + @spec make_cookie(t, Path.t()) :: :ok + def make_cookie(release, path) do + cond do + cookie = release.options[:cookie] -> + Mix.Generator.create_file(path, cookie, quiet: true) + :ok + + File.exists?(path) -> + :ok + + true -> + File.write!(path, random_cookie()) + :ok + end + end + + defp random_cookie, do: Base.encode32(:crypto.strong_rand_bytes(32)) + + @doc """ + Makes the start_erl.data file with the + ERTS version and release versions. + """ + @spec make_start_erl(t, Path.t()) :: :ok + def make_start_erl(release, path) do + File.write!(path, "#{release.erts_version} #{release.version}") + :ok + end + + @doc """ + Makes boot scripts. + + It receives a path to the boot file, without extension, such as + `releases/0.1.0/start` and this command will write `start.rel`, + `start.boot`, and `start.script` to the given path, returning + `{:ok, rel_path}` or `{:error, message}`. + + The boot script uses the RELEASE_LIB environment variable, which must + be accordingly set with `--boot-var` and point to the release lib dir. + """ + @spec make_boot_script(t, Path.t(), [{application(), mode()}], [String.t()]) :: + :ok | {:error, String.t()} + def make_boot_script(release, path, modes, prepend_paths \\ []) do + with {:ok, rel_spec} <- build_release_spec(release, modes) do + File.write!(path <> ".rel", consultable(rel_spec)) + + sys_path = String.to_charlist(path) + + sys_options = [ + :silent, + :no_dot_erlang, + :no_warn_sasl, + variables: build_variables(release), + path: build_paths(release) + ] + + case :systools.make_script(sys_path, sys_options) do + {:ok, _module, _warnings} -> + script_path = sys_path ++ '.script' + {:ok, [{:script, rel_info, instructions}]} = :file.consult(script_path) + + instructions = + instructions + |> post_stdlib_applies(release) + |> prepend_paths_to_script(prepend_paths) + + script = {:script, rel_info, instructions} + File.write!(script_path, consultable(script)) + :ok = :systools.script2boot(sys_path) + + {:error, module, info} -> + message = module.format_error(info) |> to_string() |> String.trim() + {:error, message} + end + end + end + + defp build_variables(release) do + for {_, properties} <- release.applications, + not Keyword.fetch!(properties, :otp_app?), + uniq: true, + do: {'RELEASE_LIB', properties |> Keyword.fetch!(:path) |> :filename.dirname()} + end + + defp build_paths(release) do + for {_, properties} <- release.applications, + Keyword.fetch!(properties, :otp_app?), + do: properties |> Keyword.fetch!(:path) |> Path.join("ebin") |> to_charlist() + end + + defp build_release_spec(release, modes) do + %{name: name, version: version, erts_version: erts_version, applications: apps} = release + + rel_apps = + for {app, mode} <- modes do + properties = Map.get(apps, app) || throw({:error, "Unknown application #{inspect(app)}"}) + children = Keyword.get(properties, :applications, []) + # validate_mode!(app, mode, modes, children) + build_app_for_release(app, mode, properties) + end + + {:ok, {:release, {to_charlist(name), to_charlist(version)}, {:erts, erts_version}, rel_apps}} + catch + {:error, message} -> {:error, message} + end + + defp validate_mode!(app, mode, modes, children) do + safe_mode? = mode in @safe_modes + + if not safe_mode? and mode not in @unsafe_modes do + throw( + {:error, + "Unknown mode #{inspect(mode)} for #{inspect(app)}. " <> + "Valid modes are: #{inspect(@safe_modes ++ @unsafe_modes)}"} + ) + end + + for child <- children do + child_mode = Keyword.get(modes, child) + + cond do + is_nil(child_mode) -> + throw( + {:error, + "Application #{inspect(app)} is listed in the release boot, " <> + "but it depends on #{inspect(child)}, which isn't"} + ) + + safe_mode? and child_mode in @unsafe_modes -> + throw( + {:error, + """ + Application #{inspect(app)} has mode #{inspect(mode)} but it depends on \ + #{inspect(child)} which is set to #{inspect(child_mode)}. If you really want \ + to set such mode for #{inspect(child)} make sure that all applications that depend \ + on it are also set to :load or :none, otherwise your release will fail to boot + """} + ) + + true -> + :ok + end + end + end + + defp build_app_for_release(app, mode, properties) do + vsn = Keyword.fetch!(properties, :vsn) + + case Keyword.get(properties, :included_applications, []) do + [] -> {app, vsn, mode} + included_apps -> {app, vsn, mode, included_apps} + end + end + + defp post_stdlib_applies(instructions, release) do + {pre, [stdlib | post]} = + Enum.split_while( + instructions, + &(not match?({:apply, {:application, :start_boot, [:stdlib, _]}}, &1)) + ) + + pre ++ [stdlib] ++ config_provider_apply(release) ++ post + end + + defp config_provider_apply(%{config_providers: []}), + do: [] + + defp config_provider_apply(_), + do: [{:apply, {Config.Provider, :boot, []}}] + + defp prepend_paths_to_script(instructions, []), do: instructions + + defp prepend_paths_to_script(instructions, prepend_paths) do + prepend_paths = Enum.map(prepend_paths, &String.to_charlist/1) + + Enum.map(instructions, fn + {:path, paths} -> + if Enum.any?(paths, &List.starts_with?(&1, '$RELEASE_LIB')) do + {:path, prepend_paths ++ paths} + else + {:path, paths} + end + + other -> + other + end) + end + + defp consultable(term) do + IO.chardata_to_string(:io_lib.format("%% coding: utf-8~n~tp.~n", [term])) + end + + @doc """ + Finds a template path for the release. + """ + def rel_templates_path(release, path) do + Path.join(release.options[:rel_templates_path] || "rel", path) + end + + @doc """ + Copies ERTS if the release is configured to do so. + + Returns true if the release was copied, false otherwise. + """ + @spec copy_erts(t) :: boolean() + def copy_erts(%{erts_source: nil}) do + false + end + + def copy_erts(release) do + destination = Path.join(release.path, "erts-#{release.erts_version}/bin") + File.mkdir_p!(destination) + + release.erts_source + |> Path.join("bin") + |> File.cp_r!(destination, fn _, _ -> false end) + + _ = File.rm(Path.join(destination, "erl")) + _ = File.rm(Path.join(destination, "erl.ini")) + + destination + |> Path.join("erl") + |> File.write!(~S""" + #!/bin/sh + SELF=$(readlink "$0" || true) + if [ -z "$SELF" ]; then SELF="$0"; fi + BINDIR="$(cd "$(dirname "$SELF")" && pwd -P)" + ROOTDIR="${ERL_ROOTDIR:-"$(dirname "$(dirname "$BINDIR")")"}" + EMU=beam + PROGNAME=$(echo "$0" | sed 's/.*\///') + export EMU + export ROOTDIR + export BINDIR + export PROGNAME + exec "$BINDIR/erlexec" ${1+"$@"} + """) + + File.chmod!(Path.join(destination, "erl"), 0o755) + true + end + + @doc """ + Copies the given application specification into the release. + + It assumes the application exists in the release. + """ + @spec copy_app(t, application) :: boolean() + def copy_app(release, app) do + properties = Map.fetch!(release.applications, app) + vsn = Keyword.fetch!(properties, :vsn) + + source_app = Keyword.fetch!(properties, :path) + target_app = Path.join([release.path, "lib", "#{app}-#{vsn}"]) + + if is_nil(release.erts_source) and Keyword.fetch!(properties, :otp_app?) do + false + else + File.rm_rf!(target_app) + File.mkdir_p!(target_app) + + copy_ebin(release, Path.join(source_app, "ebin"), Path.join(target_app, "ebin")) + + for dir <- @copy_app_dirs do + source_dir = Path.join(source_app, dir) + target_dir = Path.join(target_app, dir) + + source_dir = + case File.read_link(source_dir) do + {:ok, link_target} -> Path.expand(link_target, source_app) + _ -> source_dir + end + + File.exists?(source_dir) && File.cp_r!(source_dir, target_dir) + end + + true + end + end + + @doc """ + Copies the ebin directory at `source` to `target` + respecting release options such a `:strip_beams`. + """ + @spec copy_ebin(t, Path.t(), Path.t()) :: boolean() + def copy_ebin(release, source, target) do + with {:ok, [_ | _] = files} <- File.ls(source) do + File.mkdir_p!(target) + + strip_options = + release.options + |> Keyword.get(:strip_beams, true) + |> parse_strip_beams_options() + + for file <- files do + source_file = Path.join(source, file) + target_file = Path.join(target, file) + + with true <- is_list(strip_options) and String.ends_with?(file, ".beam"), + {:ok, binary} <- strip_beam(File.read!(source_file), strip_options) do + File.write!(target_file, binary) + else + _ -> + # Use File.cp!/3 to preserve file mode for any executables stored + # in the ebin directory. + File.cp!(source_file, target_file) + end + end + + true + else + _ -> false + end + end + + @doc """ + Strips a beam file for a release. + + This keeps only significant chunks necessary for the VM operation, + discarding documentation, debug info, compile information and others. + + The exact chunks that are kept are not documented and may change in + future versions. + """ + @spec strip_beam(binary(), keyword()) :: {:ok, binary()} | {:error, :beam_lib, term()} + def strip_beam(binary, options \\ []) when is_list(options) do + chunks_to_keep = options[:keep] |> List.wrap() |> Enum.map(&String.to_charlist/1) + all_chunks = Enum.uniq(@significant_chunks ++ chunks_to_keep) + + case :beam_lib.chunks(binary, all_chunks, [:allow_missing_chunks]) do + {:ok, {_, chunks}} -> + chunks = for {name, chunk} <- chunks, is_binary(chunk), do: {name, chunk} + {:ok, binary} = :beam_lib.build_module(chunks) + {:ok, :zlib.gzip(binary)} + + {:error, _, _} = error -> + error + end + end + + defp parse_strip_beams_options(options) do + case options do + options when is_list(options) -> options + true -> [] + false -> nil + end + end +end diff --git a/mix_release.sh b/mix_release.sh index 24a572301..23bdab1b3 100755 --- a/mix_release.sh +++ b/mix_release.sh @@ -1,5 +1,7 @@ #!/bin/bash +set -e + mix release --overwrite cp -r apps/emqx/etc/certs _build/dev/rel/emqx/etc/certs cp temp/sys.config _build/dev/rel/emqx/releases/5.0.0-beta.2-3fdc075b/