1defmodule Mix.Releases.Release do
2  @moduledoc """
3  Represents metadata about a release
4  """
5  alias Mix.Releases.{App, Profile, Overlays, Config, Utils, Environment, Logger}
6
7  defstruct name: nil,
8    version: "0.1.0",
9    applications: [
10      :elixir, # required for elixir apps
11      :iex, # included so the elixir shell works
12      :sasl # required for upgrades
13      # can also use `app_name: type`, as in `some_dep: load`,
14      # to only load the application, not start it
15    ],
16    output_dir: nil,
17    is_upgrade: false,
18    upgrade_from: :latest,
19    resolved_overlays: [],
20    profile: %Profile{
21      code_paths: [],
22      erl_opts: "",
23      run_erl_env: "",
24      exec_opts: [transient: false],
25      dev_mode: false,
26      include_erts: true,
27      include_src: false,
28      include_system_libs: true,
29      included_configs: [],
30      strip_debug_info: false,
31      plugins: [],
32      overlay_vars: [],
33      overlays: [],
34      commands: [],
35      overrides: []
36    }
37
38  @type t :: %__MODULE__{
39    name: atom(),
40    version: String.t,
41    applications: list(atom | {atom, App.start_type} | App.t),
42    is_upgrade: boolean,
43    upgrade_from: nil | String.t,
44    resolved_overlays: [Overlays.overlay],
45    profile: Profile.t
46  }
47
48  @doc """
49  Creates a new Release with the given name, version, and applications.
50  """
51  @spec new(atom(), String.t) :: __MODULE__.t
52  @spec new(atom(), String.t, [atom()]) :: __MODULE__.t
53  def new(name, version, apps \\ []) do
54    build_path = Mix.Project.build_path
55    output_dir = Path.relative_to_cwd(Path.join([build_path, "rel", "#{name}"]))
56    definition = %__MODULE__{name: name, version: version}
57    profile    = definition.profile
58    %{definition | :applications => definition.applications ++ apps,
59                   :output_dir => output_dir,
60                   :profile => %{profile | :output_dir => output_dir}}
61  end
62
63  @doc """
64  Load a fully configured Release object given a release name and environment name.
65  """
66  @spec get(atom()) :: {:ok, __MODULE__.t} | {:error, term()}
67  @spec get(atom(), atom()) :: {:ok, __MODULE__.t} | {:error, term()}
68  @spec get(atom(), atom(), Keyword.t) :: {:ok, __MODULE__.t} | {:error, term()}
69  def get(name, env \\ :default, opts \\ [])
70
71  def get(name, env, opts) when is_atom(name) and is_atom(env) do
72    # load release configuration
73    default_opts = [
74      selected_environment: env,
75      selected_release: name,
76      is_upgrade: Keyword.get(opts, :is_upgrade, false),
77      upgrade_from: Keyword.get(opts, :upgrade_from, false)
78    ]
79    case Config.get(Keyword.merge(default_opts, opts)) do
80      {:error, _} = err -> err
81      {:ok, config} ->
82        with {:ok, env} <- select_environment(config),
83             {:ok, rel} <- select_release(config),
84             rel        <- apply_environment(rel, env),
85          do: apply_configuration(rel, config, false)
86    end
87  end
88
89  @doc """
90  Get the path at which the release tarball will be output
91  """
92  @spec archive_path(__MODULE__.t) :: String.t
93  def archive_path(%__MODULE__{profile: %Profile{output_dir: output_dir} = p} = r) do
94    cond do
95      p.executable ->
96        Path.join([output_dir, "bin", "#{r.name}.run"])
97      :else ->
98        Path.join([output_dir, "releases", "#{r.version}", "#{r.name}.tar.gz"])
99    end
100  end
101
102  # Returns the environment that the provided Config has selected
103  @doc false
104  @spec select_environment(Config.t) :: {:ok, Environment.t} | {:error, :no_environments}
105  def select_environment(%Config{selected_environment: :default, default_environment: :default} = c),
106    do: select_environment(Map.fetch(c.environments, :default))
107  def select_environment(%Config{selected_environment: :default, default_environment: name} = c),
108    do: select_environment(Map.fetch(c.environments, name))
109  def select_environment(%Config{selected_environment: name} = c),
110    do: select_environment(Map.fetch(c.environments, name))
111  def select_environment({:ok, _} = e), do: e
112  def select_environment(_),            do: {:error, :missing_environment}
113
114  # Returns the release that the provided Config has selected
115  @doc false
116  @spec select_release(Config.t) :: {:ok, Release.t} | {:error, :no_releases}
117  def select_release(%Config{selected_release: :default, default_release: :default} = c),
118    do: {:ok, List.first(Map.values(c.releases))}
119  def select_release(%Config{selected_release: :default, default_release: name} = c),
120    do: select_release(Map.fetch(c.releases, name))
121  def select_release(%Config{selected_release: name} = c),
122    do: select_release(Map.fetch(c.releases, name))
123  def select_release({:ok, _} = r), do: r
124  def select_release(_),            do: {:error, :missing_release}
125
126  # Applies the environment settings to a release
127  @doc false
128  @spec apply_environment(__MODULE__.t, Environment.t) :: Release.t
129  def apply_environment(%__MODULE__{profile: rel_profile} = r, %Environment{profile: env_profile}) do
130    env_profile = Map.from_struct(env_profile)
131    profile = Enum.reduce(env_profile, rel_profile, fn {k, v}, acc ->
132      case v do
133        ignore when ignore in [nil, []] -> acc
134        _   -> Map.put(acc, k, v)
135      end
136    end)
137    %{r | :profile => profile}
138  end
139
140  @doc false
141  @spec validate_configuration(__MODULE__.t) :: :ok | {:error, term} | {:ok, warning :: String.t}
142  def validate_configuration(%__MODULE__{version: _, profile: profile}) do
143    with :ok <- Utils.validate_erts(profile.include_erts) do
144      # Warn if not including ERTS when not obviously running in a dev configuration
145      if profile.dev_mode == false and profile.include_erts == false do
146        {:ok, "IMPORTANT: You have opted to *not* include the Erlang runtime system (ERTS).\n" <>
147          "You must ensure that the version of Erlang this release is built with matches\n" <>
148          "the version the release will be run with once deployed. It will fail to run otherwise."}
149      else
150        :ok
151      end
152    end
153  end
154
155  # Applies global configuration options to the release profile
156  @doc false
157  @spec apply_configuration(__MODULE__.t, Config.t) :: {:ok, __MODULE__.t} | {:error, term}
158  @spec apply_configuration(__MODULE__.t, Config.t, log? :: boolean) :: {:ok, __MODULE__.t} | {:error, term}
159  def apply_configuration(%__MODULE__{version: current_version, profile: profile} = release, %Config{} = config, log? \\ false) do
160    config_path = case profile.config do
161                    p when is_binary(p) -> p
162                    _ -> Keyword.get(Mix.Project.config, :config_path)
163                  end
164    base_release = %{release | :profile => %{profile | :config => config_path}}
165    release = check_cookie(base_release, log?)
166    case Utils.get_apps(release) do
167      {:error, _} = err -> err
168      release_apps ->
169        release = %{release | :applications => release_apps}
170        case config.is_upgrade do
171          true ->
172            case config.upgrade_from do
173              :latest ->
174                upfrom = case Utils.get_release_versions(release.profile.output_dir) do
175                  [] -> :no_upfrom
176                  [^current_version, v|_] -> v
177                  [v|_] -> v
178                end
179                case upfrom do
180                  :no_upfrom ->
181                    if log? do
182                      Logger.warn "An upgrade was requested, but there are no " <>
183                        "releases to upgrade from, no upgrade will be performed."
184                    end
185                    {:ok, %{release | :is_upgrade => false, :upgrade_from => nil}}
186                  v ->
187                    {:ok, %{release | :is_upgrade => true, :upgrade_from => v}}
188                end
189              ^current_version ->
190                {:error, {:assembler, {:bad_upgrade_spec, :upfrom_is_current, current_version}}}
191              version when is_binary(version) ->
192                if log?, do: Logger.debug("Upgrading #{release.name} from #{version} to #{current_version}")
193                upfrom_path = Path.join([release.profile.output_dir, "releases", version])
194                case File.exists?(upfrom_path) do
195                  false ->
196                    {:error, {:assembler, {:bad_upgrade_spec, :doesnt_exist, version, upfrom_path}}}
197                  true ->
198                    {:ok, %{release | :is_upgrade => true, :upgrade_from => version}}
199                end
200            end
201          false ->
202            {:ok, release}
203        end
204    end
205  end
206
207  defp check_cookie(%__MODULE__{profile: %Profile{cookie: cookie} = profile} = release, log?) do
208    cond do
209      !cookie and log? ->
210        Logger.warn "Attention! You did not provide a cookie for the erlang distribution protocol in rel/config.exs\n" <>
211          "    For backwards compatibility, the release name will be used as a cookie, which is potentially a security risk!\n" <>
212          "    Please generate a secure cookie and use it with `set cookie: <cookie>` in rel/config.exs.\n" <>
213          "    This will be an error in a future release."
214        %{release | :profile => %{profile | :cookie => release.name}}
215      not is_atom(cookie) ->
216        %{release | :profile => %{profile | :cookie => :"#{cookie}"}}
217      log? and String.contains?(Atom.to_string(cookie), "insecure") ->
218        Logger.warn "Attention! You have an insecure cookie for the erlang distribution protocol in rel/config.exs\n" <>
219          "    This is probably because a secure cookie could not be auto-generated.\n" <>
220          "    Please generate a secure cookie and use it with `set cookie: <cookie>` in rel/config.exs." <>
221        release
222      :else ->
223        release
224    end
225  end
226
227  @doc """
228  Returns a list of all code_paths of all appliactions included in the release
229  """
230  @spec get_code_paths(__MODULE__.t) :: [charlist()]
231  def get_code_paths(%__MODULE__{profile: %Profile{output_dir: output_dir}} = release) do
232    release.applications
233    |> Enum.flat_map(fn %App{name: name, vsn: version, path: path} ->
234      lib_dir = Path.join([output_dir, "lib", "#{name}-#{version}", "ebin"])
235      [String.to_charlist(lib_dir), String.to_charlist(Path.join(path, "ebin"))]
236    end)
237  end
238end
239