defmodule MishkaInstaller.Installer.DepHandler do @event "on_change_dependency" alias MishkaInstaller.{ Dependency, Installer.MixCreator, Installer.DepChangesProtector, Installer.RunTimeSourcing } alias MishkaInstaller.Helper.Extra require Logger defstruct [ :app, :version, :type, :url, :git_tag, :custom_command, :dependency_type, dependencies: [] ] @moduledoc """ A module that holds new dependencies' information, and add them into database or validating to implement in runtime ### Responsibilities of this module * Create `Json` - it helps developer to implement theire plugin/commponent into the project. * Add dependencies into database - for validating, stroing and keeping backup of runtime dependencies * Get dependencies information in several different ways ### For example, these are output from `Json` file ```elixir [ %{ app: :mishka_installer, version: "0.0.2", type: :git, # :hex, if user upload elixir libraries (path), we should keep them in a temporary folder, and Docker should make it valume url: "https://github.com/mishka-group/mishka_installer", # if it is hex: https://hex.pm/packages/mishka_installer git_tag: "0.0.2", # we consider it when it is a git, and if does not exist we get master, custom_command: "ecto.migrate", # you can write nil or you task file like ecto.migrate dependency_type: :none, # :force_update, When you use this, the RunTime sourcing check what dependencies you use in your program have a higher version #compared to the old source. it just notice admin there is a update, it does not force the source to be updated dependencies: [ # this part let mishka_installer to know can update or not dependencies of a app, we should consider a backup file %{app: :mishka_developer_tools, max: "0.0.2", min: "0.0.1"}, %{app: :mishka_social, max: "0.0.2", min: "0.0.1"} ] } ] ``` OR ```elixir %MishkaInstaller.Installer.DepHandler{ app: "mishka_social", version: "0.0.2 ", type: "hex", url: "https://hex.pm/packages/mishka_social", git_tag: nil, custom_command: nil, dependency_type: "force_update", dependencies: [ %{app: :phoenix, min: "1.6"}, %{app: :phoenix_live_view, max: "0.17.7", min: "0.17.7"}, %{app: :ueberauth, max: "0.17.7", min: "0.17.7"}, %{app: :ueberauth_github, min: "0.8.1"}, %{app: :ueberauth_google, min: "0.10.1"}, ] } ``` """ @type app_info() :: String.t() | atom() | map() | list() @type run() :: :hex | :git | :upload @type installed_apps() :: {atom, description :: charlist(), vsn :: charlist()} @type t() :: %__MODULE__{ app: String.t() | nil, version: String.t() | nil, type: String.t() | nil, url: String.t() | nil, git_tag: String.t() | nil, custom_command: String.t() | nil, dependency_type: String.t() | nil, dependencies: [map()] } @doc """ ## Run and Get a dependency ```elixir Application.spec(:timex, :vsn) MishkaInstaller.Installer.DepHandler.run(:hex, "faker") app = %{url: "https://github.com/bitwalker/timex", tag: "3.7.5"} MishkaInstaller.Installer.DepHandler.run(:git, app) app = %{url: "https://github.com/bitwalker/timex", tag: "3.7.6"} MishkaInstaller.Installer.DepHandler.run(:git, app) app = %{url: "https://github.com/bitwalker/timex", tag: "3.7.8"} MishkaInstaller.Installer.DepHandler.run(:git, app) app = %{url: "https://github.com/elixirs/faker", tag: "v0.17.0"} MishkaInstaller.Installer.DepHandler.run(:git, app) app = %{url: "https://github.com/martinsvalin/html_entities", tag: "v0.5.1"} MishkaInstaller.Installer.DepHandler.run(:git, app) app = %{url: "https://github.com/beatrichartz/csv", tag: "v2.3.0"} MishkaInstaller.Installer.DepHandler.run(:git, app) MishkaInstaller.Installer.DepHandler.run(:upload, ["../mishka_installer/deployment/extensions/timex-3.7.8.zip"]) ``` """ # ref: fix phoenix reload issue when a dep is compiled (https://github.com/phoenixframework/phoenix/issues/4278) # this ref should be in the document https://hexdocs.pm/phoenix/Phoenix.CodeReloader.html#reload/1 @spec run(:git | :hex | :upload, app_info(), atom()) :: map() def run(type, app, output_type \\ :cmd) def run(:hex = type, app, output_type) do MishkaInstaller.Helper.Sender.package("hex", %{"app" => app}) |> check_app_status(type, nil) |> run_request_handler(type, output_type) end def run(:git = type, app, output_type) do MishkaInstaller.Helper.Sender.package("github", %{"url" => app.url, "tag" => app.tag}) |> check_app_status(type, nil) |> run_request_handler(type, output_type) end def run(:upload = type, [file_path], output_type) do unzip_dep_folder(file_path) |> check_mix_file_and_get_ast(file_path) |> check_app_status(:upload, file_path) |> run_request_handler(type, output_type) end @spec create_mix_file_and_start_compile(String.t() | atom(), atom()) :: :ok def create_mix_file_and_start_compile(app_name, output_type) do mix_path = MishkaInstaller.get_config(:mix_path) MixCreator.backup_mix(mix_path) create_deps_json_file(MishkaInstaller.get_config(:project_path)) list_json_dpes = Enum.map(mix_read_from_json(), fn {key, _v} -> String.contains?(File.read!(mix_path), "#{key}") end) |> Enum.any?(&(!&1)) MixCreator.create_mix(MishkaInstaller.get_config(:mix).project[:deps], mix_path) if list_json_dpes do Logger.warn("Try to re-create Mix file") create_mix_file_and_start_compile(app_name, output_type) else DepChangesProtector.deps(app_name, output_type) end end @spec create_mix_file :: :ok def create_mix_file() do mix_path = MishkaInstaller.get_config(:mix_path) create_deps_json_file(MishkaInstaller.get_config(:project_path)) list_json_dpes = Enum.map(mix_read_from_json(), fn {key, _v} -> String.contains?(File.read!(mix_path), "#{key}") end) |> Enum.any?(&(!&1)) MixCreator.create_mix(MishkaInstaller.get_config(:mix).project[:deps], mix_path) if list_json_dpes do Logger.warn("Try to re-create Mix file") create_mix_file() else :ok end end # This function helps developer to decide what they should do when an app is going to be updated. # For example, each of the extensions maybe have states or necessary jobs, hence they can register their app for `on_change_dependency` event. @spec add_new_app(MishkaInstaller.Installer.DepHandler.t()) :: {:ok, :add_new_app, any} | {:error, :add_new_app, :changeset | :file, any} def add_new_app(%__MODULE__{} = app_info) do case check_or_create_deps_json() do {:ok, :check_or_create_deps_json, exist_json} -> insert_new_ap( {:open_file, File.open(extensions_json_path(), [:read, :write])}, app_info, exist_json ) {:error, :check_or_create_deps_json, msg} -> {:error, :add_new_app, :file, msg} end end @spec read_dep_json(any) :: {:error, :read_dep_json, String.t()} | {:ok, :read_dep_json, list()} def read_dep_json(json \\ File.read!(extensions_json_path())) do {:ok, :read_dep_json, json |> Jason.decode!()} rescue _e -> {:error, :read_dep_json, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "You do not have access to read this file or maybe the file does not exist or even has syntax error" )} end @spec mix_read_from_json :: list def mix_read_from_json() do case read_dep_json() do {:ok, :read_dep_json, data} -> Enum.map(data, fn item -> mix_creator(item["type"], item) end) {:error, :read_dep_json, msg} -> raise msg <> ". To make sure, re-create the JSON file from scratch." end end @spec append_mix([tuple()]) :: list def append_mix(list) do new_list = Enum.map(list, &(&1 |> Tuple.to_list() |> List.first())) json_mix = Enum.map(mix_read_from_json(), &mix_item(&1, new_list, list)) |> Enum.reject(&is_nil(&1)) Enum.reject(list, fn item -> List.first(Tuple.to_list(item)) in Enum.map(json_mix, fn item -> List.first(Tuple.to_list(item)) end) end) ++ json_mix rescue _e -> list end @spec compare_dependencies_with_json(installed_apps() | any()) :: list | {:error, :compare_dependencies_with_json, String.t()} def compare_dependencies_with_json(installed_apps \\ Application.loaded_applications()) do with {:ok, :check_or_create_deps_json, exist_json} <- check_or_create_deps_json(), {:ok, :read_dep_json, json_data} <- read_dep_json(exist_json) do installed_apps = Map.new(installed_apps, fn {app, _des, _ver} = item -> {Atom.to_string(app), item} end) Enum.map(json_data, fn app -> case Map.fetch(installed_apps, app["app"]) do :error -> nil {:ok, {installed_app, _des, installed_version}} -> %{ app: installed_app, json_version: app["version"], installed_version: installed_version, status: Version.compare(String.trim(app["version"]), "#{installed_version}") } end end) |> Enum.reject(&is_nil(&1)) else {:error, :check_or_create_deps_json, msg} -> {:error, :compare_dependencies_with_json, msg} _ -> {:error, :compare_dependencies_with_json, "invalid Json file"} end end @spec compare_sub_dependencies_with_json(any) :: list | {:error, :compare_sub_dependencies_with_json, String.t()} def compare_sub_dependencies_with_json(installed_apps \\ Application.loaded_applications()) do with {:ok, :check_or_create_deps_json, exist_json} <- check_or_create_deps_json(), {:ok, :read_dep_json, json_data} <- read_dep_json(exist_json) do installed_apps = Map.new(installed_apps, fn {app, _des, _ver} = item -> {Atom.to_string(app), item} end) Enum.map(merge_json_by_min_version(json_data), fn app -> case Map.fetch(installed_apps, app["app"]) do :error -> nil {:ok, {installed_app, _des, installed_version}} -> %{ app: installed_app, json_min_version: app["min"], json_max_version: app["max"], installed_version: installed_version, min_status: if(!is_nil(app["min"]), do: Version.compare(String.trim(app["min"]), "#{installed_version}"), else: nil ), max_status: if(!is_nil(app["max"]), do: Version.compare(String.trim(app["max"]), "#{installed_version}"), else: nil ) } end end) |> Enum.reject(&is_nil(&1)) else {:error, :check_or_create_deps_json, msg} -> {:error, :compare_sub_dependencies_with_json, msg} _ -> {:error, :compare_sub_dependencies_with_json, "invalid Json file"} end end @spec check_or_create_deps_json(binary) :: {:ok, :check_or_create_deps_json, String.t()} | {:error, :check_or_create_deps_json, String.t()} def check_or_create_deps_json(project_path \\ MishkaInstaller.get_config(:project_path)) do with {:deployment_path, true} <- {:deployment_path, File.exists?(Path.join(project_path, ["deployment"]))}, {:extensions_path, true} <- {:extensions_path, File.exists?(Path.join(project_path, ["deployment/", "extensions"]))}, {:json_file, true} <- {:json_file, File.exists?(extensions_json_path())}, {:empty_json, true, json_data} <- {:empty_json, File.read!(extensions_json_path()) != "", File.read!(extensions_json_path())}, {:ok, :read_dep_json, _converted_json} <- read_dep_json(json_data) do {:ok, :check_or_create_deps_json, json_data} else {:deployment_path, false} -> create_deps_json_directory(project_path, "deployment") {:extensions_path, false} -> create_deps_json_directory(project_path, "deployment/extensions") {:json_file, false} -> create_deps_json_file(project_path) {:empty_json, false, _data} -> File.rm_rf(extensions_json_path()) create_deps_json_file(project_path) {:error, :read_dep_json, _msg} -> File.rm_rf(extensions_json_path()) create_deps_json_file(project_path) end end @spec get_deps_from_mix(module()) :: list def get_deps_from_mix(mix_module) do [{:deps, app_info} | _t] = Keyword.filter(mix_module.project, fn {key, _value} -> key == :deps end) Enum.map(app_info, fn app_info -> [app, version] = Tuple.to_list(app_info) |> Enum.take(2) %{app: app, version: version} end) end @spec get_deps_from_mix_lock :: list def get_deps_from_mix_lock() do Mix.Dep.Lock.read() |> Map.to_list() |> Enum.map(fn {key, list} -> [_h | [_app, version]] = Tuple.to_list(list) |> Enum.take(3) %{app: key, version: version} end) end @spec extensions_json_path :: binary() def extensions_json_path() do path = MishkaInstaller.get_config(:project_path) Path.join(path, ["deployment/", "extensions/", "extensions.json"]) end @spec create_deps_json_file(binary()) :: {:error, :check_or_create_deps_json, binary} | {:ok, :check_or_create_deps_json, binary} def create_deps_json_file(project_path) do case File.open(extensions_json_path(), [:write]) do {:ok, file} -> IO.binwrite(file, Jason.encode!(MishkaInstaller.Dependency.dependencies())) File.close(file) check_or_create_deps_json(project_path) _error -> {:error, :check_or_create_deps_json, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "You do not have sufficient access to create this file. Please add it manually." )} end end @spec compare_installed_deps_with_app_file(String.t()) :: {:error, :compare_installed_deps_with_app_file, String.t()} | {:ok, :compare_installed_deps_with_app_file, list()} def compare_installed_deps_with_app_file(app) do new_app_path = Path.join(MishkaInstaller.get_config(:project_path), ["deps/", "#{app}"]) if File.dir?(new_app_path) and File.dir?(new_app_path <> "/_build/#{Mix.env()}/lib") do apps_list = File.ls!(new_app_path <> "/_build/#{Mix.env()}/lib") |> Enum.map(fn sub_app -> with {:ok, bin} <- RunTimeSourcing.read_app(new_app_path, sub_app), {:ok, {:application, _, properties}} <- RunTimeSourcing.consult_app_file(bin), true <- compare_version_with_installed_app(sub_app, properties[:vsn]) do {sub_app, new_app_path <> "/_build/#{Mix.env()}/lib/#{sub_app}"} else _ -> nil end end) |> Enum.reject(&is_nil(&1)) {:ok, :compare_installed_deps_with_app_file, apps_list} else {:error, :compare_installed_deps_with_app_file, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "App folder or its _build does not exist" )} end end def move_and_replace_compiled_app_build(app_list) do Enum.map(app_list, fn {app, build_path} -> MishkaInstaller.Installer.RunTimeSourcing.do_runtime(String.to_atom(app), :uninstall) File.cp_r(build_path, Path.join(RunTimeSourcing.get_build_path(), "#{app}")) end) end def compare_version_with_installed_app(app, version) do ver = Application.spec(String.to_atom(app), :vsn) if !is_nil(ver), do: Version.compare("#{version}", "#{ver}") == :gt, else: true end defp create_deps_json_directory(project_path, folder_path) do case File.mkdir(Path.join(project_path, folder_path)) do :ok -> check_or_create_deps_json(project_path) {:error, :eacces} -> {:error, :check_or_create_deps_json, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "You do not have sufficient access to create this directory. Please add it manually." )} {:error, :enospc} -> {:error, :check_or_create_deps_json, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "there is no space left on the device." )} {:error, e} when e in [:eexist, :enoent, :enotdir] -> {:error, :check_or_create_deps_json, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "Please contact plugin support when you encounter this error." )} end end # Ref: https://elixirforum.com/t/how-to-improve-sort-of-maps-in-a-list-which-have-duplicate-key/47486 defp merge_json_by_min_version(json_data) do Enum.flat_map(json_data, & &1["dependencies"]) |> Enum.group_by(& &1["app"]) |> Enum.map(fn {_key, list} -> Enum.max_by(list, & &1["min"], Version) end) end defp insert_new_ap({:open_file, {:ok, _file}}, app_info, exist_json) do with {:decode, {:ok, exist_json_data}} <- {:decode, Jason.decode(exist_json)}, {:duplicate_app, false} <- {:duplicate_app, Enum.any?( exist_json_data, &(&1["app"] == Map.get(app_info, :app) || Map.get(app_info, "app")) )}, map_app_info <- [Map.from_struct(app_info)], {:encode, {:ok, _new_apps}} <- {:encode, Jason.encode(exist_json_data ++ map_app_info)}, {:ok, :add, :dependency, repo_data} <- MishkaInstaller.Dependency.create(Map.from_struct(app_info)) do # after the new app added into the database, the DepChangesProtector module remove the json file and re-create it {:ok, :add_new_app, repo_data} else {:decode, {:error, _error}} -> {:error, :add_new_app, :file, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "We can not decode the JSON file, because this file has syntax problems. Please delete this file or fix it" )} {:duplicate_app, true} -> {:error, :add_new_app, :file, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "You can not insert new app which is duplicate, if you want to update it please use another function." )} {:encode, {:error, _error}} -> {:error, :add_new_app, :file, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "We can not encode your new app data, please check your data." )} {:error, :add, :dependency, repo_error} -> {:error, :add_new_app, :changeset, repo_error} end end defp insert_new_ap({:open_file, {:error, _posix}}, _app_info, _exist_json), do: {:error, :add_new_app, :file, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "Unfortunately, the JSON concerned file either does not exist or we do not have access to it. You can delete or create it in your panel, but before that please check you have enough access to edit it." )} defp mix_creator("hex", data) do {String.to_atom(data["app"]), "~> #{String.trim(data["version"])}"} end defp mix_creator("path", data) do uploaded_extension = Path.join(MishkaInstaller.get_config(:project_path), [ "deployment/", "extensions/", "#{data["app"]}" ]) {String.to_atom(data["app"]), path: uploaded_extension} end defp mix_creator("git", data) do case data["git_tag"] do nil -> {String.to_atom(data["app"]), git: data["url"]} _ -> {String.to_atom(data["app"]), git: data["url"], tag: data["git_tag"]} end end # The priority of path and git master/main are always higher the `mix` dependencies list. defp mix_item({_app, path: _uploaded_extension} = value, _app_list, _deps_list), do: value defp mix_item({_app, git: _url} = value, _app_list, _deps_list), do: value defp mix_item({app, git: _url, tag: imporetd_version} = value, app_list, deps_list) do if app in app_list, do: check_same_app_version(app, deps_list, imporetd_version, value), else: value end defp mix_item({app, imporetd_version} = value, app_list, deps_list) do if app in app_list, do: check_same_app_version(app, deps_list, imporetd_version, value), else: value end defp check_same_app_version(app, deps_list, imporetd_version, value) do founded_app = Enum.find(deps_list, fn item -> Enum.take(Tuple.to_list(item), 1) |> List.first() == app end) case Enum.take(Tuple.to_list(founded_app), 3) |> List.to_tuple() do {_founded_app_name, git: _url} -> nil {_founded_app_name, git: _url, tag: tag} -> if Version.compare(clean_mix_version(imporetd_version), clean_mix_version(tag)) in [ :eq, :lt ], do: nil, else: value {_founded_app_name, version} -> if Version.compare(clean_mix_version(imporetd_version), clean_mix_version(version)) in [ :eq, :lt ], do: nil, else: value end rescue _e -> nil end defp clean_mix_version(version) do version |> String.replace("~>", "") |> String.replace(">=", "") |> String.replace("v", "") |> String.trim() end defp check_app_status({:ok, :package, pkg}, :hex, _) do create_app_info(pkg, :hex) |> Map.from_struct() |> sync_app_with_database() end defp check_app_status(result, type, file_path) when type in [:git, :upload] do case result do {:error, :package, result} when result in [:mix_file, :not_found, :not_tag, :unhandled] -> {:error, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "Unfortunately, an error occurred while we were comparing your mix.exs file. The flag of erorr is %{result}", result: result )} data -> if Enum.any?(data, &(&1 == {:error, :package, :convert_ast_output})) do {:error, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "Your mix.exs file must contain the app, version and source_url parameters" )} else create_app_info(data, if(type == :git, do: type, else: :path)) |> Map.from_struct() |> rename_folder_copy_to_deps(file_path) |> sync_app_with_database() end end end defp check_app_status({:error, :package, status}, type, _) do msg = if status == :not_found do Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "Are you sure you have entered the package name or url correctly?" ) else Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "Unfortunately, we cannot connect to %{type} server now, please try other time! or make it correct", type: type ) end {:error, msg} end defp rename_folder_copy_to_deps(data, file_path) when not is_nil(file_path) and is_binary(file_path) do file = Path.join(MishkaInstaller.get_config(:project_path), [ "deployment/", "extensions/", "#{Path.basename(file_path, ".zip")}" ]) new_name = Path.join(MishkaInstaller.get_config(:project_path), [ "deployment/", "extensions/", "#{data.app}" ]) File.rename!(file, new_name) File.cp_r!( new_name, Path.join(MishkaInstaller.get_config(:project_path), ["deps/", "#{data.app}"]) ) data end defp rename_folder_copy_to_deps(data, _file_path), do: data defp sync_app_with_database(data) do if compare_version_with_installed_app(data.app, data.version) do case Dependency.create_or_update(data) do {:ok, :add, :dependency, repo_data} -> {:ok, :no_state, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "We could not find any registered-app that has important state, hence you can update safely. It should be noted if you send multi apps before finishing previous app, other new apps are saved in a queue." ), repo_data.app} {:ok, :edit, :dependency, repo_data} -> if MishkaInstaller.PluginETS.get_all(event: @event) == [] do {:ok, :no_state, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "We could not find any registered-app that has important state, hence you can update safely. It should be noted if you send multi apps before finishing previous app, other new apps are saved in a queue." ), repo_data.app} else {:ok, :registered_app, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "There is an important state for an app at least, so we sent a notification to them and put your request in the update queue. After their response, we will change the %{app} dependency and let you know about its latest news.", app: repo_data.app ), repo_data.app} end {:error, action, :dependency, _repo_error} when action in [:add, :edit] -> # TODO: save it in activities {:error, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "Unfortunately, an error occurred while storing the data in the database. To check for errors, see the Activities section, and if this error persists, report it to support." )} {:error, action, :uuid, _error_tag} when action in [:uuid, :get_record_by_id] -> # TODO: save it in activities {:error, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "Unfortunately, an error occurred while storing the data in the database. To check for errors, see the Activities section, and if this error persists, report it to support." )} {:error, :update_app_version, :older_version} -> {:error, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "You have already installed this library and the installed version is the same as the latest version of the Hex site. Please take action when a new version of this app is released" )} end else {:error, Gettext.dgettext( MishkaInstaller.gettext(), "mishka_installer", "You have already installed this library and the installed version is the same as the latest version of the Hex site. Please take action when a new version of this app is released" )} end end defp run_request_handler(status, type, output_type) do case status do {:ok, :no_state, msg, app_name} -> MishkaInstaller.DepCompileJob.add_job(app_name, output_type) %{ "app_name" => app_name, "status_message_type" => :success, "message" => msg, "selected_form" => type } {:ok, :registered_app, msg, app_name} -> %{ "app_name" => app_name, "status_message_type" => :info, "message" => msg, "selected_form" => :registered_app } {:error, msg} -> %{ "app_name" => nil, "status_message_type" => :danger, "message" => msg, "selected_form" => type } end end defp unzip_dep_folder(file_path) do {:unzip, :zip.unzip(~c'#{file_path}', [ {:cwd, ~c'#{Path.join(MishkaInstaller.get_config(:project_path), ["deployment/", "extensions"])}'} ])} end defp check_mix_file_and_get_ast({:unzip, {:ok, _content}}, file_path) do mix_file = Path.join(MishkaInstaller.get_config(:project_path), [ "deployment/", "extensions/", "#{Path.basename(file_path, ".zip")}", "/mix.exs" ]) with {:mix_file, {:ok, body}} <- {:mix_file, File.read(mix_file)}, {:code, {:ok, ast}} <- {:code, Code.string_to_quoted(body)} do Extra.ast_mix_file_basic_information(ast, [:app, :version, :source_url]) else {:mix_file, {:error, _error}} -> {:error, :package, :mix_file} {:code, {:error, _error}} -> {:error, :package, :string_mix_file} end end defp check_mix_file_and_get_ast({:unzip, {:error, _error}}, _file_path), do: {:error, :package, :unzip} defp create_app_info(pkg, :hex) do %__MODULE__{ app: pkg["name"], version: pkg["latest_stable_version"], type: "hex", url: pkg["html_url"], dependency_type: "force_update", dependencies: [] } end defp create_app_info([app: app, version: version, source_url: source_url, tag: tag], :git) do %__MODULE__{ app: "#{app}", version: "#{version}", type: "git", git_tag: tag, url: "#{source_url}", dependency_type: "force_update", dependencies: [] } end defp create_app_info([app: app, version: version, source_url: source_url], :path) do %__MODULE__{ app: "#{app}", version: "#{version}", type: "path", url: "#{source_url}", dependency_type: "force_update", dependencies: [] } end end