defmodule MishkaInstaller.Installer.RunTimeSourcing do
@moduledoc """
This module is created just for compiling and sourcing, hence if you want to work with Json file and the other compiling dependencies
please see the `MishkaInstaller.Installer.DepHandler` module.
"""
use Agent
@module "run_time_sourcing"
@type ensure() :: :bad_directory | :load | :no_directory | :sure_all_started
@type do_runtime() :: :application_ensure | :prepend_compiled_apps
@type app_name() :: String.t() | atom()
@spec do_runtime(atom(), atom()) ::
{:ok, :application_ensure} | {:error, do_runtime(), ensure(), any}
def do_runtime(app, :add) when is_atom(app) do
get_build_path()
|> File.ls!()
|> Enum.reject(&(&1 == ".DS_Store"))
|> compare_dependencies()
|> prepend_compiled_apps()
|> application_ensure(app, :add)
end
def do_runtime(app, :force_update) when is_atom(app) do
if(Atom.to_string(app) in File.ls!(get_build_path()), do: ["#{app}"], else: false)
|> prepend_compiled_apps()
|> application_ensure(app, :force_update)
end
def do_runtime(app, :uninstall) when is_atom(app) do
Application.stop(app)
Application.unload(app)
if(Atom.to_string(app) in File.ls!(get_build_path()), do: "#{app}", else: false)
|> delete_app_dir()
end
@spec subscribe :: :ok | {:error, {:already_registered, pid}}
def subscribe do
Phoenix.PubSub.subscribe(MishkaInstaller.PubSub, @module)
end
@spec compare_dependencies([tuple()], [String.t()]) :: [String.t()]
def compare_dependencies(installed_apps \\ Application.loaded_applications(), files_list) do
installed_apps =
Map.new(installed_apps, fn {app, _des, _ver} = item -> {Atom.to_string(app), item} end)
Enum.map(files_list, fn app_name ->
case Map.fetch(installed_apps, app_name) do
:error ->
app_name
_ ->
nil
end
end)
|> Enum.reject(&is_nil(&1))
end
@spec do_deps_compile(String.t() | :cmd | :port) ::
{:ok, :do_deps_compile, String.t()}
| {:error, :do_deps_compile, String.t(), [{:operation, String.t()} | {:output, any}]}
def do_deps_compile(app, type \\ :cmd) do
with _cd_path <- File.cd(MishkaInstaller.get_config(:project_path)),
%{operation: "deps.get", output: _stream, status: 0} <- exec("deps.get", type),
deps_path <- Path.join(MishkaInstaller.get_config(:project_path), ["deps/", "#{app}"]),
{:change_dir, :ok} <- {:change_dir, File.cd(deps_path)},
{:inside_app, %{operation: "deps.get", output: _stream, status: 0}} <-
{:inside_app, exec("deps.get", type)},
%{operation: "deps.compile", output: _stream, status: 0} <- exec("deps.compile", type),
{:compile_main_app, %{operation: "compile", output: _stream, status: 0}} <-
{:compile_main_app, exec("compile", type)} do
{:ok, :do_deps_compile, app}
else
%{operation: "deps.get", output: stream, status: 1} ->
{:error, :do_deps_compile, app, operation: "deps.get", output: stream}
{:inside_app, %{operation: "deps.get", output: stream, status: 1}} ->
{:error, :do_deps_compile, app, operation: "deps.get", output: stream}
{:compile_main_app, %{operation: "compile", output: stream, status: 1}} ->
{:error, :do_deps_compile, app, operation: "compile", output: stream}
{:change_dir, file_error} ->
{:error, :do_deps_compile, app, operation: "File.cd", output: file_error}
%{operation: "deps.compile", output: stream, status: 1} ->
{:error, :do_deps_compile, app, operation: "deps.compile", output: stream}
_ ->
{:error, :do_deps_compile, app, operation: "File.cd", output: "Wrong path"}
end
after
# Maybe a developer does not consider changed-path, so for preventing issues we back to the project path after each compiling
File.cd(MishkaInstaller.get_config(:project_path))
end
@spec prepend_compiled_apps(any) ::
{:ok, :prepend_compiled_apps} | {:error, do_runtime(), ensure(), list}
def prepend_compiled_apps(false), do: {:error, :prepend_compiled_apps, :no_directory, []}
def prepend_compiled_apps([]), do: {:error, :prepend_compiled_apps, :no_directory, []}
def prepend_compiled_apps(files_list) do
files_list
|> Enum.map(
&{String.to_atom(&1),
Path.join(get_build_path() <> "/" <> &1, "ebin") |> Code.prepend_path()}
)
|> Enum.filter(fn {_app, status} -> status == {:error, :bad_directory} end)
|> case do
[] -> {:ok, :prepend_compiled_apps}
list -> {:error, :prepend_compiled_apps, :bad_directory, list}
end
end
@spec get_build_path(atom()) :: binary
def get_build_path(mode \\ Mix.env()) do
Path.join(MishkaInstaller.get_config(:project_path), [
"_build/",
"#{mode}/",
"lib"
])
end
# Ref: https://elixirforum.com/t/how-to-get-vsn-from-app-file/48132/2
# Ref: https://github.com/elixir-lang/elixir/blob/main/lib/mix/lib/mix/tasks/compile.all.ex#L153-L154
@spec read_app(binary(), app_name()) :: {:error, atom} | {:ok, binary}
def read_app(lib_path, sub_app) do
File.read("#{lib_path}/_build/#{Mix.env()}/lib/#{sub_app}/ebin/#{sub_app}.app")
end
@spec consult_app_file(binary) ::
{:error, {non_neg_integer | {non_neg_integer, pos_integer}, atom, any}}
| {:ok, any}
| {:error, {non_neg_integer | {non_neg_integer, pos_integer}, atom, any},
non_neg_integer | {non_neg_integer, pos_integer}}
def consult_app_file(bin) do
# The path could be located in an .ez archive, so we use the prim loader.
with {:ok, tokens, _} <- :erl_scan.string(String.to_charlist(bin)) do
:erl_parse.parse_term(tokens)
end
end
defp exec(command, type, operation \\ "mix")
defp exec(command, :cmd, operation) do
{stream, status} =
System.cmd(operation, [command],
into: IO.stream(),
stderr_to_stdout: true,
env: [{"MIX_ENV", "#{Mix.env()}"}]
)
%{operation: command, output: stream, status: status}
end
# Ref: https://hexdocs.pm/elixir/Port.html#module-spawn_executable
# Ref: https://elixirforum.com/t/how-to-send-line-by-line-of-system-cmd-to-liveview-when-a-task-is-running/48336/
defp exec(command, :port, operation) do
path = System.find_executable("#{operation}")
port =
Port.open({:spawn_executable, path}, [
:binary,
:exit_status,
args: [command],
line: 1000,
env: [{'MIX_ENV', '#{Mix.env()}'}]
])
start_exec_satet([])
loop(port, command)
end
defp application_ensure({:ok, :prepend_compiled_apps}, app, :add) do
with {:load, :ok} <- {:load, Application.load(app)},
{:sure_all_started, {:ok, _apps}} <-
{:sure_all_started, Application.ensure_all_started(app)} do
{:ok, :application_ensure}
else
{:load, {:error, term}} ->
{:error, :application_ensure, :load, term}
{:sure_all_started, {:error, {app, term}}} ->
{:error, :application_ensure, :sure_all_started, {app, term}}
end
end
defp application_ensure({:ok, :prepend_compiled_apps}, app, :force_update) do
Application.stop(app)
with {:unload, :ok} <- {:unload, Application.unload(app)},
{:load, :ok} <- {:load, Application.load(app)},
{:sure_all_started, {:ok, _apps}} <-
{:sure_all_started, Application.ensure_all_started(app)} do
{:ok, :application_ensure}
else
{:unload, {:error, term}} ->
{:error, :application_ensure, :unload, term}
{:load, {:error, term}} ->
{:error, :application_ensure, :load, term}
{:sure_all_started, {:error, {app, term}}} ->
{:error, :application_ensure, :sure_all_started, {app, term}}
end
end
defp application_ensure(error, _app, _status), do: error
defp loop(port, command) do
receive do
{^port, {:data, {:eol, msg}}} when is_binary(msg) ->
update_exec_satet([msg])
notify_subscribers(msg)
loop(port, command)
{^port, {:data, data}} ->
update_exec_satet([data])
notify_subscribers(data)
loop(port, command)
{^port, {:exit_status, exit_status}} ->
output = get_exec_state()
stop_exec_state()
%{operation: command, output: output, status: exit_status}
end
end
defp start_exec_satet(initial_value),
do: Agent.start_link(fn -> initial_value end, name: __MODULE__)
defp update_exec_satet(new_value),
do: Agent.get_and_update(__MODULE__, fn state -> {state, state ++ new_value} end)
defp get_exec_state(), do: Agent.get(__MODULE__, & &1)
defp stop_exec_state(), do: Agent.stop(__MODULE__)
defp notify_subscribers(answer) do
Phoenix.PubSub.broadcast(MishkaInstaller.PubSub, @module, {String.to_atom(@module), answer})
end
defp delete_app_dir(false), do: {:error, :prepend_compiled_apps, :no_directory, []}
defp delete_app_dir(dir) do
Path.join(get_build_path(), ["#{dir}"])
|> File.rm_rf()
|> case do
{:ok, files_and_directories} -> {:ok, :delete_app_dir, files_and_directories}
{:error, reason, file} -> {:error, :delete_app_dir, reason, file}
end
end
end