release.exs 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878
  1. defmodule Mix.Release do
  2. @moduledoc """
  3. Defines the release structure and convenience for assembling releases.
  4. """
  5. @doc """
  6. The Mix.Release struct has the following read-only fields:
  7. * `:name` - the name of the release as an atom
  8. * `:version` - the version of the release as a string or
  9. `{:from_app, app_name}`
  10. * `:path` - the path to the release root
  11. * `:version_path` - the path to the release version inside the release
  12. * `:applications` - a map of application with their definitions
  13. * `:erts_source` - the ERTS source as a charlist (or nil)
  14. * `:erts_version` - the ERTS version as a charlist
  15. The following fields may be modified as long as they keep their defined types:
  16. * `:boot_scripts` - a map of boot scripts with the boot script name
  17. as key and a keyword list with **all** applications that are part of
  18. it and their modes as value
  19. * `:config_providers` - a list of `{config_provider, term}` tuples where the
  20. first element is a module that implements the `Config.Provider` behaviour
  21. and `term` is the value given to it on `c:Config.Provider.init/1`
  22. * `:options` - a keyword list with all other user supplied release options
  23. * `:overlays` - a list of extra files added to the release. If you have a custom
  24. step adding extra files to a release, you can add these files to the `:overlays`
  25. field so they are also considered on further commands, such as tar/zip. Each entry
  26. in overlays is the relative path to the release root of each file
  27. * `:steps` - a list of functions that receive the release and returns a release.
  28. Must also contain the atom `:assemble` which is the internal assembling step.
  29. May also contain the atom `:tar` to create a tarball of the release.
  30. """
  31. defstruct [
  32. :name,
  33. :version,
  34. :path,
  35. :version_path,
  36. :applications,
  37. :boot_scripts,
  38. :erts_source,
  39. :erts_version,
  40. :config_providers,
  41. :options,
  42. :overlays,
  43. :steps
  44. ]
  45. @type mode :: :permanent | :transient | :temporary | :load | :none
  46. @type application :: atom()
  47. @type t :: %__MODULE__{
  48. name: atom(),
  49. version: String.t(),
  50. path: String.t(),
  51. version_path: String.t() | {:from_app, application()},
  52. applications: %{application() => keyword()},
  53. boot_scripts: %{atom() => [{application(), mode()}]},
  54. erts_version: charlist(),
  55. erts_source: charlist() | nil,
  56. config_providers: [{module, term}],
  57. options: keyword(),
  58. overlays: list(String.t()),
  59. steps: [(t -> t) | :assemble, ...]
  60. }
  61. @default_apps [kernel: :permanent, stdlib: :permanent, elixir: :permanent, sasl: :permanent]
  62. @safe_modes [:permanent, :temporary, :transient]
  63. @unsafe_modes [:load, :none]
  64. @significant_chunks ~w(Atom AtU8 Attr Code StrT ImpT ExpT FunT LitT Line)c
  65. @copy_app_dirs ["priv"]
  66. @doc false
  67. @spec from_config!(atom, keyword, keyword) :: t
  68. def from_config!(name, config, overrides) do
  69. {name, apps, opts} = find_release(name, config)
  70. unless Atom.to_string(name) =~ ~r/^[a-z][a-z0-9_]*$/ do
  71. Mix.raise(
  72. "Invalid release name. A release name must start with a lowercase ASCII letter, " <>
  73. "followed by lowercase ASCII letters, numbers, or underscores, got: #{inspect(name)}"
  74. )
  75. end
  76. opts =
  77. [overwrite: false, quiet: false, strip_beams: true]
  78. |> Keyword.merge(opts)
  79. |> Keyword.merge(overrides)
  80. {include_erts, opts} = Keyword.pop(opts, :include_erts, true)
  81. {erts_source, erts_lib_dir, erts_version} = erts_data(include_erts)
  82. deps_apps = Mix.Project.deps_apps()
  83. loaded_apps = apps |> Keyword.keys() |> load_apps(deps_apps, %{}, erts_lib_dir, [], :root)
  84. # Make sure IEx is either an active part of the release or add it as none.
  85. {loaded_apps, apps} =
  86. if Map.has_key?(loaded_apps, :iex) do
  87. {loaded_apps, apps}
  88. else
  89. {load_apps([:iex], deps_apps, loaded_apps, erts_lib_dir, [], :root), apps ++ [iex: :none]}
  90. end
  91. start_boot = build_start_boot(loaded_apps, apps)
  92. start_clean_boot = build_start_clean_boot(start_boot)
  93. {path, opts} =
  94. Keyword.pop_lazy(opts, :path, fn ->
  95. Path.join([Mix.Project.build_path(config), "rel", Atom.to_string(name)])
  96. end)
  97. path = Path.absname(path)
  98. {version, opts} =
  99. Keyword.pop_lazy(opts, :version, fn ->
  100. config[:version] ||
  101. Mix.raise(
  102. "No :version found. Please make sure a :version is set in your project definition " <>
  103. "or inside the release the configuration"
  104. )
  105. end)
  106. version =
  107. case version do
  108. {:from_app, app} ->
  109. Application.load(app)
  110. version = Application.spec(app, :vsn)
  111. if !version do
  112. Mix.raise(
  113. "Could not find version for #{inspect(app)}, please make sure the application exists"
  114. )
  115. end
  116. to_string(version)
  117. "" ->
  118. Mix.raise("The release :version cannot be an empty string")
  119. _ ->
  120. version
  121. end
  122. {config_providers, opts} = Keyword.pop(opts, :config_providers, [])
  123. {steps, opts} = Keyword.pop(opts, :steps, [:assemble])
  124. validate_steps!(steps)
  125. %Mix.Release{
  126. name: name,
  127. version: version,
  128. path: path,
  129. version_path: Path.join([path, "releases", version]),
  130. erts_source: erts_source,
  131. erts_version: erts_version,
  132. applications: loaded_apps,
  133. boot_scripts: %{start: start_boot, start_clean: start_clean_boot},
  134. config_providers: config_providers,
  135. options: opts,
  136. overlays: [],
  137. steps: steps
  138. }
  139. end
  140. defp find_release(name, config) do
  141. {name, opts_fun_or_list} = lookup_release(name, config) || infer_release(config)
  142. opts = if is_function(opts_fun_or_list, 0), do: opts_fun_or_list.(), else: opts_fun_or_list
  143. {apps, opts} = Keyword.pop(opts, :applications, [])
  144. if apps == [] and Mix.Project.umbrella?(config) do
  145. bad_umbrella!()
  146. end
  147. app = Keyword.get(config, :app)
  148. apps = Keyword.merge(@default_apps, apps)
  149. if is_nil(app) or Keyword.has_key?(apps, app) do
  150. {name, apps, opts}
  151. else
  152. {name, apps ++ [{app, :permanent}], opts}
  153. end
  154. end
  155. defp lookup_release(nil, config) do
  156. case Keyword.get(config, :releases, []) do
  157. [] ->
  158. nil
  159. [{name, opts}] ->
  160. {name, opts}
  161. [_ | _] ->
  162. case Keyword.get(config, :default_release) do
  163. nil ->
  164. Mix.raise(
  165. "\"mix release\" was invoked without a name but there are multiple releases. " <>
  166. "Please call \"mix release NAME\" or set :default_release in your project configuration"
  167. )
  168. name ->
  169. lookup_release(name, config)
  170. end
  171. end
  172. end
  173. defp lookup_release(name, config) do
  174. if opts = config[:releases][name] do
  175. {name, opts}
  176. else
  177. found = Keyword.get(config, :releases, [])
  178. Mix.raise(
  179. "Unknown release #{inspect(name)}. " <>
  180. "The available releases are: #{inspect(Keyword.keys(found))}"
  181. )
  182. end
  183. end
  184. defp infer_release(config) do
  185. if Mix.Project.umbrella?(config) do
  186. bad_umbrella!()
  187. else
  188. {Keyword.fetch!(config, :app), []}
  189. end
  190. end
  191. defp bad_umbrella! do
  192. Mix.raise("""
  193. Umbrella projects require releases to be explicitly defined with \
  194. a non-empty applications key that chooses which umbrella children \
  195. should be part of the releases:
  196. releases: [
  197. foo: [
  198. applications: [child_app_foo: :permanent]
  199. ],
  200. bar: [
  201. applications: [child_app_bar: :permanent]
  202. ]
  203. ]
  204. Alternatively you can perform the release from the children applications
  205. """)
  206. end
  207. defp erts_data(erts_data) when is_function(erts_data) do
  208. erts_data(erts_data.())
  209. end
  210. defp erts_data(false) do
  211. {nil, :code.lib_dir(), :erlang.system_info(:version)}
  212. end
  213. defp erts_data(true) do
  214. version = :erlang.system_info(:version)
  215. {:filename.join(:code.root_dir(), 'erts-#{version}'), :code.lib_dir(), version}
  216. end
  217. defp erts_data(erts_source) when is_binary(erts_source) do
  218. if File.exists?(erts_source) do
  219. [_, erts_version] = erts_source |> Path.basename() |> String.split("-")
  220. erts_lib_dir = erts_source |> Path.dirname() |> Path.join("lib") |> to_charlist()
  221. {to_charlist(erts_source), erts_lib_dir, to_charlist(erts_version)}
  222. else
  223. Mix.raise("Could not find ERTS system at #{inspect(erts_source)}")
  224. end
  225. end
  226. defp load_apps(apps, deps_apps, seen, otp_root, optional, type) do
  227. for app <- apps, reduce: seen do
  228. seen ->
  229. if reentrant_seen = reentrant(seen, app, type) do
  230. reentrant_seen
  231. else
  232. load_app(app, deps_apps, seen, otp_root, optional, type)
  233. end
  234. end
  235. end
  236. defp reentrant(seen, app, type) do
  237. properties = seen[app]
  238. cond do
  239. is_nil(properties) ->
  240. nil
  241. type != :root and properties[:type] != type ->
  242. if properties[:type] == :root do
  243. put_in(seen[app][:type], type)
  244. else
  245. Mix.raise(
  246. "#{inspect(app)} is listed both as a regular application and as an included application"
  247. )
  248. end
  249. true ->
  250. seen
  251. end
  252. end
  253. defp load_app(app, deps_apps, seen, otp_root, optional, type) do
  254. cond do
  255. path = app not in deps_apps && otp_path(otp_root, app) ->
  256. do_load_app(app, path, deps_apps, seen, otp_root, true, type)
  257. path = code_path(app) ->
  258. do_load_app(app, path, deps_apps, seen, otp_root, false, type)
  259. app in optional ->
  260. seen
  261. true ->
  262. Mix.raise("Could not find application #{inspect(app)}")
  263. end
  264. end
  265. defp otp_path(otp_root, app) do
  266. path = Path.join(otp_root, "#{app}-*")
  267. case Path.wildcard(path) do
  268. [] -> nil
  269. paths -> paths |> Enum.sort() |> List.last() |> to_charlist()
  270. end
  271. end
  272. defp code_path(app) do
  273. case :code.lib_dir(app) do
  274. {:error, :bad_name} -> nil
  275. path -> path
  276. end
  277. end
  278. defp do_load_app(app, path, deps_apps, seen, otp_root, otp_app?, type) do
  279. case :file.consult(Path.join(path, "ebin/#{app}.app")) do
  280. {:ok, terms} ->
  281. [{:application, ^app, properties}] = terms
  282. value = [path: path, otp_app?: otp_app?, type: type] ++ properties
  283. seen = Map.put(seen, app, value)
  284. applications = Keyword.get(properties, :applications, [])
  285. optional = Keyword.get(properties, :optional_applications, [])
  286. seen = load_apps(applications, deps_apps, seen, otp_root, optional, :depended)
  287. included_applications = Keyword.get(properties, :included_applications, [])
  288. load_apps(included_applications, deps_apps, seen, otp_root, [], :included)
  289. {:error, reason} ->
  290. Mix.raise("Could not load #{app}.app. Reason: #{inspect(reason)}")
  291. end
  292. end
  293. defp build_start_boot(all_apps, specified_apps) do
  294. specified_apps ++
  295. Enum.sort(
  296. for(
  297. {app, props} <- all_apps,
  298. not List.keymember?(specified_apps, app, 0),
  299. do: {app, default_mode(props)}
  300. )
  301. )
  302. end
  303. defp default_mode(props) do
  304. if props[:type] == :included, do: :load, else: :permanent
  305. end
  306. defp build_start_clean_boot(boot) do
  307. for({app, _mode} <- boot, do: {app, :none})
  308. |> Keyword.put(:stdlib, :permanent)
  309. |> Keyword.put(:kernel, :permanent)
  310. end
  311. defp validate_steps!(steps) do
  312. valid_atoms = [:assemble, :tar]
  313. if not is_list(steps) or Enum.any?(steps, &(&1 not in valid_atoms and not is_function(&1, 1))) do
  314. Mix.raise("""
  315. The :steps option must be a list of:
  316. * anonymous function that receives one argument
  317. * the atom :assemble or :tar
  318. Got: #{inspect(steps)}
  319. """)
  320. end
  321. if Enum.count(steps, &(&1 == :assemble)) != 1 do
  322. Mix.raise("The :steps option must contain the atom :assemble once, got: #{inspect(steps)}")
  323. end
  324. if :assemble in Enum.drop_while(steps, &(&1 != :tar)) do
  325. Mix.raise("The :tar step must come after :assemble")
  326. end
  327. if Enum.count(steps, &(&1 == :tar)) > 1 do
  328. Mix.raise("The :steps option can only contain the atom :tar once")
  329. end
  330. :ok
  331. end
  332. @doc """
  333. Makes the `sys.config` structure.
  334. If there are config providers, then a value is injected into
  335. the `:elixir` application configuration in `sys_config` to be
  336. read during boot and trigger the providers.
  337. It uses the following release options to customize its behaviour:
  338. * `:reboot_system_after_config`
  339. * `:start_distribution_during_config`
  340. * `:prune_runtime_sys_config_after_boot`
  341. In case there are no config providers, it doesn't change `sys_config`.
  342. """
  343. @spec make_sys_config(t, keyword(), Config.Provider.config_path()) ::
  344. :ok | {:error, String.t()}
  345. def make_sys_config(release, sys_config, config_provider_path) do
  346. {sys_config, runtime_config?} =
  347. merge_provider_config(release, sys_config, config_provider_path)
  348. path = Path.join(release.version_path, "sys.config")
  349. args = [runtime_config?, sys_config]
  350. format = "%% coding: utf-8~n%% RUNTIME_CONFIG=~s~n~tw.~n"
  351. File.mkdir_p!(Path.dirname(path))
  352. File.write!(path, IO.chardata_to_string(:io_lib.format(format, args)))
  353. case :file.consult(path) do
  354. {:ok, _} ->
  355. :ok
  356. {:error, reason} ->
  357. invalid =
  358. for {app, kv} <- sys_config,
  359. {key, value} <- kv,
  360. not valid_config?(value),
  361. do: """
  362. Application: #{inspect(app)}
  363. Key: #{inspect(key)}
  364. Value: #{inspect(value)}
  365. """
  366. message =
  367. case invalid do
  368. [] ->
  369. "Could not read configuration file. Reason: #{inspect(reason)}"
  370. _ ->
  371. "Could not read configuration file. It has invalid configuration terms " <>
  372. "such as functions, references, and pids. Please make sure your configuration " <>
  373. "is made of numbers, atoms, strings, maps, tuples and lists. The following entries " <>
  374. "are wrong:\n#{Enum.join(invalid)}"
  375. end
  376. {:error, message}
  377. end
  378. end
  379. defp valid_config?(m) when is_map(m),
  380. do: Enum.all?(Map.delete(m, :__struct__), &valid_config?/1)
  381. defp valid_config?(l) when is_list(l), do: Enum.all?(l, &valid_config?/1)
  382. defp valid_config?(t) when is_tuple(t), do: Enum.all?(Tuple.to_list(t), &valid_config?/1)
  383. defp valid_config?(o), do: is_number(o) or is_atom(o) or is_binary(o)
  384. defp merge_provider_config(%{config_providers: []}, sys_config, _), do: {sys_config, false}
  385. defp merge_provider_config(release, sys_config, config_path) do
  386. {reboot?, extra_config, initial_config} = start_distribution(release)
  387. prune_runtime_sys_config_after_boot =
  388. Keyword.get(release.options, :prune_runtime_sys_config_after_boot, false)
  389. opts = [
  390. extra_config: initial_config,
  391. prune_runtime_sys_config_after_boot: prune_runtime_sys_config_after_boot,
  392. reboot_system_after_config: reboot?,
  393. validate_compile_env: validate_compile_env(release)
  394. ]
  395. init_config = Config.Provider.init(release.config_providers, config_path, opts)
  396. {Config.Reader.merge(sys_config, init_config ++ extra_config), reboot?}
  397. end
  398. defp validate_compile_env(release) do
  399. with true <- Keyword.get(release.options, :validate_compile_env, true),
  400. [_ | _] = compile_env <- compile_env(release) do
  401. compile_env
  402. else
  403. _ -> false
  404. end
  405. end
  406. defp compile_env(release) do
  407. for {_, properties} <- release.applications,
  408. triplet <- Keyword.get(properties, :compile_env, []),
  409. do: triplet
  410. end
  411. defp start_distribution(%{options: opts}) do
  412. reboot? = Keyword.get(opts, :reboot_system_after_config, false)
  413. early_distribution? = Keyword.get(opts, :start_distribution_during_config, false)
  414. if not reboot? or early_distribution? do
  415. {reboot?, [], []}
  416. else
  417. {true, [kernel: [start_distribution: false]], [kernel: [start_distribution: true]]}
  418. end
  419. end
  420. @doc """
  421. Copies the cookie to the given path.
  422. If a cookie option was given, we compare it with
  423. the contents of the file (if any), and ask the user
  424. if they want to override.
  425. If there is no option, we generate a random one
  426. the first time.
  427. """
  428. @spec make_cookie(t, Path.t()) :: :ok
  429. def make_cookie(release, path) do
  430. cond do
  431. cookie = release.options[:cookie] ->
  432. Mix.Generator.create_file(path, cookie, quiet: true)
  433. :ok
  434. File.exists?(path) ->
  435. :ok
  436. true ->
  437. File.write!(path, random_cookie())
  438. :ok
  439. end
  440. end
  441. defp random_cookie, do: Base.encode32(:crypto.strong_rand_bytes(32))
  442. @doc """
  443. Makes the start_erl.data file with the
  444. ERTS version and release versions.
  445. """
  446. @spec make_start_erl(t, Path.t()) :: :ok
  447. def make_start_erl(release, path) do
  448. File.write!(path, "#{release.erts_version} #{release.version}")
  449. :ok
  450. end
  451. @doc """
  452. Makes boot scripts.
  453. It receives a path to the boot file, without extension, such as
  454. `releases/0.1.0/start` and this command will write `start.rel`,
  455. `start.boot`, and `start.script` to the given path, returning
  456. `{:ok, rel_path}` or `{:error, message}`.
  457. The boot script uses the RELEASE_LIB environment variable, which must
  458. be accordingly set with `--boot-var` and point to the release lib dir.
  459. """
  460. @spec make_boot_script(t, Path.t(), [{application(), mode()}], [String.t()]) ::
  461. :ok | {:error, String.t()}
  462. def make_boot_script(release, path, modes, prepend_paths \\ []) do
  463. with {:ok, rel_spec} <- build_release_spec(release, modes) do
  464. File.write!(path <> ".rel", consultable(rel_spec))
  465. sys_path = String.to_charlist(path)
  466. sys_options = [
  467. :silent,
  468. :no_dot_erlang,
  469. :no_warn_sasl,
  470. variables: build_variables(release),
  471. path: build_paths(release)
  472. ]
  473. case :systools.make_script(sys_path, sys_options) do
  474. {:ok, _module, _warnings} ->
  475. script_path = sys_path ++ '.script'
  476. {:ok, [{:script, rel_info, instructions}]} = :file.consult(script_path)
  477. instructions =
  478. instructions
  479. |> post_stdlib_applies(release)
  480. |> prepend_paths_to_script(prepend_paths)
  481. script = {:script, rel_info, instructions}
  482. File.write!(script_path, consultable(script))
  483. :ok = :systools.script2boot(sys_path)
  484. {:error, module, info} ->
  485. message = module.format_error(info) |> to_string() |> String.trim()
  486. {:error, message}
  487. end
  488. end
  489. end
  490. defp build_variables(release) do
  491. for {_, properties} <- release.applications,
  492. not Keyword.fetch!(properties, :otp_app?),
  493. uniq: true,
  494. do: {'RELEASE_LIB', properties |> Keyword.fetch!(:path) |> :filename.dirname()}
  495. end
  496. defp build_paths(release) do
  497. for {_, properties} <- release.applications,
  498. Keyword.fetch!(properties, :otp_app?),
  499. do: properties |> Keyword.fetch!(:path) |> Path.join("ebin") |> to_charlist()
  500. end
  501. defp build_release_spec(release, modes) do
  502. %{
  503. name: name,
  504. version: version,
  505. erts_version: erts_version,
  506. applications: apps,
  507. options: options
  508. } = release
  509. skip_mode_validation_for =
  510. options
  511. |> Keyword.get(:skip_mode_validation_for, [])
  512. |> MapSet.new()
  513. rel_apps =
  514. for {app, mode} <- modes do
  515. properties = Map.get(apps, app) || throw({:error, "Unknown application #{inspect(app)}"})
  516. children = Keyword.get(properties, :applications, [])
  517. app in skip_mode_validation_for || validate_mode!(app, mode, modes, children)
  518. build_app_for_release(app, mode, properties)
  519. end
  520. {:ok, {:release, {to_charlist(name), to_charlist(version)}, {:erts, erts_version}, rel_apps}}
  521. catch
  522. {:error, message} -> {:error, message}
  523. end
  524. defp validate_mode!(app, mode, modes, children) do
  525. safe_mode? = mode in @safe_modes
  526. if not safe_mode? and mode not in @unsafe_modes do
  527. throw(
  528. {:error,
  529. "Unknown mode #{inspect(mode)} for #{inspect(app)}. " <>
  530. "Valid modes are: #{inspect(@safe_modes ++ @unsafe_modes)}"}
  531. )
  532. end
  533. for child <- children do
  534. child_mode = Keyword.get(modes, child)
  535. cond do
  536. is_nil(child_mode) ->
  537. throw(
  538. {:error,
  539. "Application #{inspect(app)} is listed in the release boot, " <>
  540. "but it depends on #{inspect(child)}, which isn't"}
  541. )
  542. safe_mode? and child_mode in @unsafe_modes ->
  543. throw(
  544. {:error,
  545. """
  546. Application #{inspect(app)} has mode #{inspect(mode)} but it depends on \
  547. #{inspect(child)} which is set to #{inspect(child_mode)}. If you really want \
  548. to set such mode for #{inspect(child)} make sure that all applications that depend \
  549. on it are also set to :load or :none, otherwise your release will fail to boot
  550. """}
  551. )
  552. true ->
  553. :ok
  554. end
  555. end
  556. end
  557. defp build_app_for_release(app, mode, properties) do
  558. vsn = Keyword.fetch!(properties, :vsn)
  559. case Keyword.get(properties, :included_applications, []) do
  560. [] -> {app, vsn, mode}
  561. included_apps -> {app, vsn, mode, included_apps}
  562. end
  563. end
  564. defp post_stdlib_applies(instructions, release) do
  565. {pre, [stdlib | post]} =
  566. Enum.split_while(
  567. instructions,
  568. &(not match?({:apply, {:application, :start_boot, [:stdlib, _]}}, &1))
  569. )
  570. pre ++ [stdlib] ++ config_provider_apply(release) ++ post
  571. end
  572. defp config_provider_apply(%{config_providers: []}),
  573. do: []
  574. defp config_provider_apply(_),
  575. do: [{:apply, {Config.Provider, :boot, []}}]
  576. defp prepend_paths_to_script(instructions, []), do: instructions
  577. defp prepend_paths_to_script(instructions, prepend_paths) do
  578. prepend_paths = Enum.map(prepend_paths, &String.to_charlist/1)
  579. Enum.map(instructions, fn
  580. {:path, paths} ->
  581. if Enum.any?(paths, &List.starts_with?(&1, '$RELEASE_LIB')) do
  582. {:path, prepend_paths ++ paths}
  583. else
  584. {:path, paths}
  585. end
  586. other ->
  587. other
  588. end)
  589. end
  590. defp consultable(term) do
  591. IO.chardata_to_string(:io_lib.format("%% coding: utf-8~n~tp.~n", [term]))
  592. end
  593. @doc """
  594. Finds a template path for the release.
  595. """
  596. def rel_templates_path(release, path) do
  597. Path.join(release.options[:rel_templates_path] || "rel", path)
  598. end
  599. @doc """
  600. Copies ERTS if the release is configured to do so.
  601. Returns true if the release was copied, false otherwise.
  602. """
  603. @spec copy_erts(t) :: boolean()
  604. def copy_erts(%{erts_source: nil}) do
  605. false
  606. end
  607. def copy_erts(release) do
  608. destination = Path.join(release.path, "erts-#{release.erts_version}/bin")
  609. File.mkdir_p!(destination)
  610. release.erts_source
  611. |> Path.join("bin")
  612. |> File.cp_r!(destination, fn _, _ -> false end)
  613. _ = File.rm(Path.join(destination, "erl"))
  614. _ = File.rm(Path.join(destination, "erl.ini"))
  615. destination
  616. |> Path.join("erl")
  617. |> File.write!(~S"""
  618. #!/bin/sh
  619. SELF=$(readlink "$0" || true)
  620. if [ -z "$SELF" ]; then SELF="$0"; fi
  621. BINDIR="$(cd "$(dirname "$SELF")" && pwd -P)"
  622. ROOTDIR="${ERL_ROOTDIR:-"$(dirname "$(dirname "$BINDIR")")"}"
  623. EMU=beam
  624. PROGNAME=$(echo "$0" | sed 's/.*\///')
  625. export EMU
  626. export ROOTDIR
  627. export BINDIR
  628. export PROGNAME
  629. exec "$BINDIR/erlexec" ${1+"$@"}
  630. """)
  631. File.chmod!(Path.join(destination, "erl"), 0o755)
  632. true
  633. end
  634. @doc """
  635. Copies the given application specification into the release.
  636. It assumes the application exists in the release.
  637. """
  638. @spec copy_app(t, application) :: boolean()
  639. def copy_app(release, app) do
  640. properties = Map.fetch!(release.applications, app)
  641. vsn = Keyword.fetch!(properties, :vsn)
  642. source_app = Keyword.fetch!(properties, :path)
  643. target_app = Path.join([release.path, "lib", "#{app}-#{vsn}"])
  644. if is_nil(release.erts_source) and Keyword.fetch!(properties, :otp_app?) do
  645. false
  646. else
  647. File.rm_rf!(target_app)
  648. File.mkdir_p!(target_app)
  649. copy_ebin(release, Path.join(source_app, "ebin"), Path.join(target_app, "ebin"))
  650. for dir <- @copy_app_dirs do
  651. source_dir = Path.join(source_app, dir)
  652. target_dir = Path.join(target_app, dir)
  653. source_dir =
  654. case File.read_link(source_dir) do
  655. {:ok, link_target} -> Path.expand(link_target, source_app)
  656. _ -> source_dir
  657. end
  658. File.exists?(source_dir) && File.cp_r!(source_dir, target_dir)
  659. end
  660. true
  661. end
  662. end
  663. @doc """
  664. Copies the ebin directory at `source` to `target`
  665. respecting release options such a `:strip_beams`.
  666. """
  667. @spec copy_ebin(t, Path.t(), Path.t()) :: boolean()
  668. def copy_ebin(release, source, target) do
  669. with {:ok, [_ | _] = files} <- File.ls(source) do
  670. File.mkdir_p!(target)
  671. strip_options =
  672. release.options
  673. |> Keyword.get(:strip_beams, true)
  674. |> parse_strip_beams_options()
  675. for file <- files do
  676. source_file = Path.join(source, file)
  677. target_file = Path.join(target, file)
  678. with true <- is_list(strip_options) and String.ends_with?(file, ".beam"),
  679. {:ok, binary} <- strip_beam(File.read!(source_file), strip_options) do
  680. File.write!(target_file, binary)
  681. else
  682. _ ->
  683. # Use File.cp!/3 to preserve file mode for any executables stored
  684. # in the ebin directory.
  685. File.cp!(source_file, target_file)
  686. end
  687. end
  688. true
  689. else
  690. _ -> false
  691. end
  692. end
  693. @doc """
  694. Strips a beam file for a release.
  695. This keeps only significant chunks necessary for the VM operation,
  696. discarding documentation, debug info, compile information and others.
  697. The exact chunks that are kept are not documented and may change in
  698. future versions.
  699. """
  700. @spec strip_beam(binary(), keyword()) :: {:ok, binary()} | {:error, :beam_lib, term()}
  701. def strip_beam(binary, options \\ []) when is_list(options) do
  702. chunks_to_keep = options[:keep] |> List.wrap() |> Enum.map(&String.to_charlist/1)
  703. all_chunks = Enum.uniq(@significant_chunks ++ chunks_to_keep)
  704. case :beam_lib.chunks(binary, all_chunks, [:allow_missing_chunks]) do
  705. {:ok, {_, chunks}} ->
  706. chunks = for {name, chunk} <- chunks, is_binary(chunk), do: {name, chunk}
  707. {:ok, binary} = :beam_lib.build_module(chunks)
  708. {:ok, :zlib.gzip(binary)}
  709. {:error, _, _} = error ->
  710. error
  711. end
  712. end
  713. defp parse_strip_beams_options(options) do
  714. case options do
  715. options when is_list(options) -> options
  716. true -> []
  717. false -> nil
  718. end
  719. end
  720. end