defmodule MishkaInstaller.Installer.MixCreator do @moduledoc """ In version 0.0.2, the MishkaInstaller library was used to download a dependency from the project's own `mix.exs` file, so this module was written to edit this file. In fact, this module uses the Sourceror library to change the mentioned `mix.exs` file (with `AST`). Another use of this module is reading information from the programmer's Git or custom link. - Warning: in the next versions of MishkaInstaller, instead of `mix.exs`, the client project will be downloaded directly from `Git` or **hex.pm** site. If this update is executed, the original project will not be changed. - This module is not going to be deleted in new versions. """ @doc """ With the help of this function, you can make a backup copy of `mix.exs` and `mix.lock` of your project and keep it in the `deployment/extensions` path. If this function is used with one input, it targets the `mix.exs` file, and if the second input is `:lock` atom, it targets the `mix.lock` file to keep a backup copy. With the help of this function, you can make a backup copy of `mix.exs` and `mix.lock` of your project and keep it in the `deployment/extensions` path. If this function is used with one input, it targets the `mix.exs` file, and if the second input is `:lock` atom, it targets the `mix.lock` file to keep a backup copy. ## Examples ```elixir MishkaInstaller.Installer.MixCreator.backup_mix("mix.exs") MishkaInstaller.Installer.MixCreator.backup_mix("mix.lock", :lock) ``` """ @spec backup_mix(binary) :: {:error, atom} | {:ok, non_neg_integer} def backup_mix(mix_path), do: File.copy(mix_path, backup_path()) @doc """ Read `backup_mix/1` description. """ @spec backup_mix(binary(), :lock) :: {:error, atom} | {:ok, non_neg_integer} def backup_mix(lock_path, :lock), do: File.copy(lock_path, backup_path("original_mix.lock")) @doc """ This function is also the same as the `backup_mix/1` function, with the difference that it returns the backed-up version to the project path. Both functions use the `File.copy/2` function just to improve the naming and also to warn the programmer that it has been replaced in this file. ## Examples ```elixir MishkaInstaller.Installer.MixCreator.restore_mix("mix.exs") MishkaInstaller.Installer.MixCreator.restore_mix("mix.lock", :lock) ``` """ @spec restore_mix(binary) :: {:error, atom} | {:ok, non_neg_integer} def restore_mix(mix_path), do: File.copy(backup_path(), mix_path) @doc """ Read `restore_mix/1` description. """ @spec restore_mix(binary(), :lock) :: {:error, atom} | {:ok, non_neg_integer} def restore_mix(lock_path, :lock), do: File.copy(backup_path("original_mix.lock"), lock_path) @doc """ This function receives a list of libraries stored in the `extensions.json` file along with the `mix.exs` path of the file that needs to be changed, and after that, it changes the `deps` function in `mix.exs` and overwrites it with the new libraries. ## Examples ```elixir mix_path = MishkaInstaller.get_config(:mix) MixCreator.create_mix(mix_path.project[:deps], "mix_path") ``` As you see, we pass the current dependencies to let this function merge it with `extensions.json` """ @spec create_mix([tuple()], binary()) :: :ok | {:error, atom} def create_mix(list_of_deps, mix_path) do content = File.read!(mix_path) |> Sourceror.parse_string!() |> Sourceror.postwalk(fn {:defp, meta, [{:deps, _, _} = fun, [{{_, _, [:do]}, {:__block__, block_meta, [deps]}}]]}, state -> deps = MishkaInstaller.Installer.DepHandler.append_mix(list_of_deps) |> Enum.map(fn item -> [app_name | options] = Tuple.to_list(item) create_mix_postwalk(app_name, options, dep_line(deps, block_meta)) end) {{:defp, meta, [fun, [do: {:__block__, block_meta, [deps]}]]}, state} other, state -> {other, state} end) |> Sourceror.to_string() File.write(mix_path, content) end defp dep(:git, name, url, dep_line, other_options) do {:__block__, [closing: [line: dep_line], line: dep_line], [ {{:__block__, [line: dep_line], [name]}, [ {{:__block__, [format: :keyword, line: dep_line], [:git]}, {:__block__, [delimiter: "\"", line: dep_line], [url]}} ] ++ implement_other_options(other_options, dep_line)} ]} end defp dep(:path, name, path, dep_line, other_options) do {:__block__, [closing: [line: dep_line], line: dep_line], [ {{:__block__, [line: dep_line], [name]}, [ {{:__block__, [format: :keyword, line: dep_line], [:path]}, {:__block__, [delimiter: "\"", line: dep_line], [path]}} ] ++ implement_other_options(other_options, dep_line)} ]} end defp dep(:in_umbrella, name, value, dep_line) do {:__block__, [closing: [line: dep_line], line: dep_line], [ {{:__block__, [line: dep_line], [name]}, [ {{:__block__, [format: :keyword, line: dep_line], [:in_umbrella]}, {:__block__, [delimiter: "\"", line: dep_line], [value]}} ]} ]} end defp dep(name, full_version, dep_line, other_options) do {:{}, [trailing_comments: [], leading_comments: [], closing: [line: dep_line], line: dep_line], [ {:__block__, [trailing_comments: [], leading_comments: [], line: dep_line], [name]}, {:__block__, [trailing_comments: [], leading_comments: [], delimiter: "\"", line: dep_line], clean_mix_version(full_version)}, implement_other_options(other_options, dep_line, :nested) ]} end defp dep(name, full_version, dep_line) do {:__block__, [line: dep_line], [{name, {:__block__, [line: dep_line, delimiter: "\""], clean_mix_version(full_version)}}]} end defp create_mix_postwalk(app_name, [[{:path, value} | other_options] = h | _t], dep_line) when is_list(h) do dep(:path, app_name, value, dep_line, other_options) end defp create_mix_postwalk(app_name, [[{:git, value} | other_options] = h | _t], dep_line) when is_list(h) do dep(:git, app_name, value, dep_line, other_options) end defp create_mix_postwalk(app_name, [[in_umbrella: value]], dep_line) do dep(:in_umbrella, app_name, value, dep_line) end defp create_mix_postwalk(app_name, [version | t], dep_line) when is_binary(version) and t == [] do dep(app_name, version, dep_line) end defp create_mix_postwalk(app_name, [version | [other_options]], dep_line) when is_binary(version) and is_list(other_options) do dep(app_name, version, dep_line, other_options) end defp implement_other_options(other_options, dep_line) do Enum.map(other_options, fn {key, value} -> {{:__block__, [format: :keyword, line: dep_line], [key]}, {:__block__, [delimiter: "\"", line: dep_line], [value]}} end) end defp implement_other_options(other_options, dep_line, :nested) do Enum.map(other_options, fn {key, value} -> {{:__block__, [trailing_comments: [], leading_comments: [], format: :keyword, line: dep_line], [key]}, {:__block__, [trailing_comments: [], leading_comments: [], line: dep_line], [value]}} end) end defp dep_line(deps, block_meta) do case List.last(deps) do {_, meta, _} -> meta[:line] || block_meta[:line] _ -> block_meta[:line] end + 1 end defp clean_mix_version(full_version) do case String.trim(full_version) |> String.split(" ") do [type, version] -> ["#{type} " <> version] _ -> full_version = full_version |> String.replace("~>", "") |> String.replace(">=", "") |> String.trim() ["~> " <> full_version] end end defp backup_path(file_name \\ "original_mix.exs") do path = MishkaInstaller.get_config(:project_path) Path.join(path, ["deployment/", "extensions/", "#{file_name}"]) end end