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