|
|
@@ -0,0 +1,878 @@
|
|
|
+defmodule Mix.Release do
|
|
|
+ @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 ->
|
|
|
+ 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,
|
|
|
+ options: options
|
|
|
+ } = release
|
|
|
+
|
|
|
+ skip_mode_validation_for =
|
|
|
+ options
|
|
|
+ |> Keyword.get(:skip_mode_validation_for, [])
|
|
|
+ |> MapSet.new()
|
|
|
+
|
|
|
+ rel_apps =
|
|
|
+ for {app, mode} <- modes do
|
|
|
+ properties = Map.get(apps, app) || throw({:error, "Unknown application #{inspect(app)}"})
|
|
|
+ children = Keyword.get(properties, :applications, [])
|
|
|
+ app in skip_mode_validation_for || 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
|