Packages
igniter
0.6.8
0.8.2
0.8.1
0.8.0
0.7.9
0.7.8
0.7.7
0.7.6
0.7.5
0.7.4
0.7.3
0.7.2
0.7.1
0.7.0
0.6.30
0.6.29
0.6.28
0.6.27
0.6.26
0.6.25
0.6.24
0.6.23
0.6.22
0.6.21
0.6.20
0.6.19
0.6.18
0.6.17
0.6.16
0.6.15
0.6.14
0.6.13
0.6.12
0.6.11
0.6.10
0.6.9
0.6.8
0.6.7
0.6.6
0.6.5
0.6.4
0.6.3
0.6.2
0.6.1
0.6.0
0.5.52
0.5.51
0.5.50
0.5.49
0.5.48
0.5.47
0.5.46
0.5.45
0.5.44
0.5.43
0.5.42
0.5.41
0.5.40
0.5.39
0.5.38
0.5.37
0.5.36
0.5.35
0.5.34
0.5.33
0.5.32
0.5.31
0.5.30
0.5.29
0.5.28
0.5.27
0.5.26
0.5.25
0.5.24
0.5.23
0.5.22
0.5.21
0.5.20
0.5.19
0.5.18
0.5.17
0.5.16
0.5.15
0.5.14
0.5.13
0.5.12
0.5.11
0.5.10
0.5.9
0.5.8
0.5.7
0.5.6
0.5.5
0.5.4
0.5.3
0.5.2
0.5.1
0.5.0
0.4.8
0.4.7
0.4.6
0.4.5
0.4.4
0.4.3
0.4.2
0.4.1
0.4.0
0.3.78
0.3.77
0.3.76
0.3.75
0.3.74
0.3.73
0.3.72
0.3.71
0.3.70
0.3.69
0.3.68
0.3.67
0.3.66
0.3.65
0.3.64
0.3.63
0.3.62
0.3.61
0.3.60
0.3.59
0.3.58
0.3.57
0.3.56
0.3.55
0.3.54
0.3.53
0.3.52
0.3.51
0.3.50
0.3.49
0.3.48
0.3.47
0.3.46
0.3.45
0.3.44
0.3.43
0.3.42
0.3.41
0.3.40
0.3.39
0.3.38
0.3.37
0.3.36
0.3.35
0.3.34
0.3.33
0.3.32
0.3.31
0.3.30
0.3.29
0.3.28
0.3.27
0.3.26
0.3.25
0.3.24
0.3.23
0.3.22
0.3.21
0.3.20
0.3.19
0.3.18
0.3.17
0.3.16
0.3.15
0.3.14
0.3.13
0.3.12
0.3.11
0.3.10
0.3.9
0.3.8
0.3.7
0.3.6
0.3.5
0.3.4
0.3.3
0.3.2
0.3.1
0.3.0
0.2.13
0.2.12
0.2.11
0.2.10
0.2.9
0.2.8
0.2.7
0.2.6
0.2.5
0.2.4
0.2.3
0.2.2
0.2.1
0.2.0
0.1.8
0.1.7
0.1.6
0.1.5
0.1.4
0.1.3
0.1.2
0.1.1
0.1.0
A code generation and project patching framework
Current section
Files
Jump to
Current section
Files
lib/igniter/util/install.ex
defmodule Igniter.Util.Install do
@moduledoc """
Tools for installing packages and running their associated
installers, if present.
[!NOTE]
The functions in this module are not composable, and are primarily meant to
be used internally and to support building custom tooling on top of Igniter,
such as [Fireside](https://github.com/ibarakaiev/fireside).
"""
@doc """
Installs the provided list of dependencies. `deps` can be either:
- a string like `"ash,ash_postgres"`
- a list of strings like `["ash", "ash_postgres", ...]`
- a list of tuples like `[{:ash, "~> 3.0"}, {:ash_postgres, "~> 2.0"}]`
"""
def install(deps, argv, igniter \\ Igniter.new(), opts \\ [])
def install(deps, argv, igniter, opts) when is_binary(deps) do
deps = String.split(deps, ",")
install(deps, argv, igniter, opts)
end
def install([head | _] = deps, argv, igniter, opts) when is_binary(head) do
deps =
Enum.map(deps, fn dep ->
Igniter.Project.Deps.determine_dep_type_and_version!(dep)
end)
install(deps, argv, igniter, opts)
end
def install([head | _] = deps, argv, igniter, opts) when is_tuple(head) do
if Enum.any?(deps, &(elem(&1, 0) == :igniter)) do
raise ArgumentError,
"cannot install the igniter package with `mix igniter.install`. Please use `mix igniter.setup` instead."
end
global_options =
Keyword.update!(
Igniter.Mix.Task.Info.global_options(),
:switches,
&Keyword.put(&1, :example, :boolean)
)
{installed_with, argv} = remove_installed_with(argv)
igniter =
if installed_with == "phx.new" do
igniter
|> Igniter.compose_task("igniter.add_extension", ["phoenix"])
else
igniter
end
only =
argv
|> OptionParser.parse!(switches: [only: :keep])
|> elem(0)
|> Keyword.get_values(:only)
|> Enum.join(",")
|> String.split(",", trim: true)
|> Enum.map(&String.to_atom/1)
|> case do
[] -> nil
value -> value
end
if only && Mix.env() not in only do
raise """
The `--only` option can only be used when running `mix igniter.install` in an environment
that matches one of the environments in `--only`. For example:
MIX_ENV=#{Enum.at(only, 0)} mix igniter.install --only #{Enum.join(only, ",")}
"""
end
{igniter, installing, {options, _}} =
Igniter.Util.Info.compose_install_and_validate!(
igniter,
argv,
%Igniter.Mix.Task.Info{
schema: global_options[:switches],
aliases: [],
installs: deps
},
"igniter.install",
yes: "--yes" in argv,
yes_to_deps: "--yes-to-deps" in argv,
only: only,
append?: Keyword.get(opts, :append?, false)
)
if igniter.assigns[:failed_to_add_deps] do
Mix.shell().error("""
Failed to add dependencies to the `mix.exs` file.
Igniter may not be able to find where to modify your `deps/0` function.
To address this, modify your `.igniter.exs` file's `deps_location` option.
If you don't yet have a `.igniter.exs`, run `mix igniter.setup`.
For more information, see:
https://hexdocs.pm/igniter/Igniter.Project.IgniterConfig.html
""")
exit({:shutdown, 1})
end
installing_names = Enum.join(installing, ", ")
igniter =
Igniter.apply_and_fetch_dependencies(
igniter,
Keyword.put(options, :operation, "compiling #{installing_names}")
)
available_tasks =
Enum.zip(installing, Enum.map(installing, &Mix.Task.get("#{&1}.install")))
|> Enum.filter(fn {_desired_task, source_task} -> source_task end)
case igniter.issues do
[] ->
case available_tasks do
[] ->
:ok
[{name, _}] = tasks ->
run_installers(
igniter,
tasks,
"The following installer was found and executed: `#{name}.install`",
argv,
options
)
tasks ->
run_installers(
igniter,
tasks,
"The following installers were found and executed: #{Enum.map_join(tasks, ", ", &"`#{elem(&1, 0)}.install`")}",
argv,
options
)
end
IO.puts("\nSuccessfully installed:\n\n#{Enum.map_join(installing, "\n", &"* #{&1}")}")
_issues ->
Igniter.display_issues(igniter)
exit({:shutdown, 1})
end
end
defp run_installers(igniter, igniter_task_sources, title, argv, options) do
igniter_task_sources
|> Enum.reduce(igniter, fn {name, task}, igniter ->
igniter = Igniter.compose_task(igniter, task, argv)
Mix.shell().info("`#{name}.install` #{IO.ANSI.green()}✔#{IO.ANSI.reset()}")
igniter
end)
|> Igniter.do_or_dry_run(Keyword.put(options, :title, title))
:ok
end
def get_deps!(igniter, opts) do
case System.cmd("mix", ["deps.get"], stderr_to_stdout: true) do
{_output, 0} ->
Igniter.Util.Loading.with_spinner(
opts[:operation] || "building deps",
fn ->
igniter =
case List.wrap(opts[:update_deps]) do
[] ->
igniter
[:all] ->
System.cmd("mix", ["deps.update", "--all" | opts[:update_deps_args] || []],
stderr_to_stdout: true
)
%{igniter | rewrite: Rewrite.drop(igniter.rewrite, ["mix.lock"])}
to_update ->
System.cmd(
"mix",
["deps.update" | to_update] ++ (opts[:update_deps_args] || []),
stderr_to_stdout: true
)
%{igniter | rewrite: Rewrite.drop(igniter.rewrite, ["mix.lock"])}
end
Mix.Project.pop()
old_undefined = Code.get_compiler_option(:no_warn_undefined)
old_relative_paths = Code.get_compiler_option(:relative_paths)
old_ignore_module_conflict = Code.get_compiler_option(:ignore_module_conflict)
try do
Code.compiler_options(
relative_paths: false,
no_warn_undefined: :all,
ignore_module_conflict: true
)
igniter =
if Keyword.get(opts, :compile_deps?, true) do
System.cmd("mix", ["deps.get"], stderr_to_stdout: true)
System.cmd("mix", ["deps.compile"], stderr_to_stdout: true)
%{igniter | rewrite: Rewrite.drop(igniter.rewrite, ["mix.lock"])}
else
igniter
end
_ = Code.compile_file("mix.exs")
if Keyword.get(opts, :compile_deps?, true) do
Mix.Task.reenable("deps.loadpaths")
Mix.Task.run("deps.loadpaths", ["--no-deps-check"])
end
igniter
after
Code.compiler_options(
relative_paths: old_relative_paths,
no_warn_undefined: old_undefined,
ignore_module_conflict: old_ignore_module_conflict
)
end
end
)
{output, exit_code} ->
case handle_error(output, exit_code, igniter, opts) do
{:ok, igniter} ->
get_deps!(igniter, Keyword.put(opts, :name, "applying dependency conflict changes"))
:error ->
Mix.shell().info("""
mix deps.get returned exited with code: `#{exit_code}`
""")
raise output
end
end
end
defp handle_error(output, _exit_code, igniter, opts) do
if String.contains?(output, "Dependencies have diverged") do
handle_diverged_dependencies(output, igniter, opts)
else
:error
end
end
defp handle_diverged_dependencies(rest, igniter, opts) do
with [_, dep] <-
String.split(rest, "the :only option for dependency ", parts: 2, trim: true),
[dep, rest] <- String.split(dep, ["\n", " "], parts: 2, trim: true),
[_, source1] <- String.split(rest, "> In ", parts: 2, trim: true),
[source1, rest] <- String.split(source1, ":", parts: 2, trim: true),
[declaration1, rest] <-
String.split(rest, "does not match the :only option calculated for",
parts: 2,
trim: true
),
[_, source2] <- String.split(rest, "> In ", parts: 2, trim: true),
[source2, rest] <- String.split(source2, ":", parts: 2, trim: true),
[declaration2, _] <-
String.split(rest, "\n\n", parts: 2, trim: true) do
dep = String.to_atom(dep)
source1 = parse_source(source1)
source2 = parse_source(source2)
# This is hacky :(
{declaration1, _} = Code.eval_string(String.replace(declaration1, ", ...", ""))
{declaration2, _} = Code.eval_string(String.replace(declaration2, ", ...", ""))
with {^dep, req, opts1} <- declaration1,
{^dep, _, opts2} <- declaration2 do
opts1 = Keyword.put_new(opts1, :only, [:dev, :test, :prod])
opts2 = Keyword.put_new(opts2, :only, [:dev, :test, :prod])
only = List.wrap(opts1[:only]) ++ List.wrap(opts2[:only])
igniter =
case Igniter.Project.Deps.get_dep(igniter, dep) do
{:error, error} ->
Mix.shell().error("""
Failed to add dependencies to the `mix.exs` file.
#{error}
Igniter may not be able to find where to modify your `deps/0` function.
To address this, modify your `.igniter.exs` file's `deps_location` option.
If you don't yet have a `.igniter.exs`, run `mix igniter.setup`.
For more information, see:
https://hexdocs.pm/igniter/Igniter.Project.IgniterConfig.html
""")
{:ok, nil} ->
Igniter.Project.Deps.add_dep(igniter, {dep, req, Keyword.put(opts1, :only, only)},
yes?: true
)
{:ok, string} ->
{existing_statement, _} = Code.eval_string(string)
case existing_statement do
{dep, requirement} when is_binary(requirement) ->
if only == [:dev, :test, :prod] do
Igniter.Project.Deps.add_dep(igniter, {dep, requirement}, yes?: true)
else
Igniter.Project.Deps.add_dep(igniter, {dep, requirement, [only: only]},
yes?: true
)
end
{dep, opts} when is_list(opts) ->
if only == [:dev, :test, :prod] do
Igniter.Project.Deps.add_dep(igniter, {dep, Keyword.delete(opts, :only)},
yes?: true
)
else
Igniter.Project.Deps.add_dep(igniter, {dep, Keyword.put(opts, :only, only)},
yes?: true
)
end
{dep, requirement, opts} ->
if only == [:dev, :test, :prod] do
Igniter.Project.Deps.add_dep(
igniter,
{dep, requirement, Keyword.put(opts, :only, only)},
yes?: true
)
else
case Keyword.delete(opts, :only) do
[] ->
Igniter.Project.Deps.add_dep(
igniter,
{dep, requirement},
yes?: true
)
new_opts ->
Igniter.Project.Deps.add_dep(
igniter,
{dep, requirement, new_opts},
yes?: true
)
end
end
_ ->
:error
end
end
case igniter do
:error ->
:error
igniter ->
message = """
Conflict in `only` option for dependency #{inspect(dep)}.
Between #{source1} and #{source2}.
We must update the `only` option as shown to continue.
"""
{:ok,
Igniter.apply_and_fetch_dependencies(
igniter,
Keyword.merge(opts,
compile_deps?: false,
operation: "recompiling conflicts",
message: message,
error_on_abort?: true
)
)}
end
else
_ ->
:error
end
else
_ ->
:error
end
rescue
_e ->
:error
end
defp parse_source("mix.exs"), do: "your application"
defp parse_source("deps/" <> dep) do
case String.split(dep, "/", parts: 2, trim: true) |> Enum.at(0) do
nil ->
"deps/#{dep}"
dep ->
"the :#{dep} dependency"
end
end
defp parse_source(dep), do: "\"#{dep}\""
defp remove_installed_with(argv, acc \\ {nil, []})
defp remove_installed_with([], {installed_with, trail}) do
{installed_with, Enum.reverse(trail)}
end
defp remove_installed_with(["--new-with", installed_with | rest], {_, trail}) do
{installed_with, Enum.reverse(trail, rest)}
end
defp remove_installed_with([other | rest], {installed_with, trail}) do
remove_installed_with(rest, {installed_with, [other | trail]})
end
end