Current section

Files

Jump to
igniter lib igniter util info.ex
Raw

lib/igniter/util/info.ex

defmodule Igniter.Util.Info do
@moduledoc false
# This is an *extremely* unfortunate hack.
# For dependencies that depend on other common dev dependencies (like `sourceror`)
# our installation process has an issue. Specifically, we first install the dep
# into all environments, and then set it to the right env.
# This causes our conflict resolution logic to set the dependents (like `sourceror`)
# to be for all envs, but then never set back to only specific envs.
# I don't have time to fix this right now, nor can I think of a good way to actually
# do it. So for now, this ridiculous hack will have to do :)
@known_only_options [
smokestack: [:test],
mishka_chelekom: [:dev],
mneme: [:dev, :test],
usage_rules: [:dev],
git_ops: [:dev],
live_debugger: [:dev],
tidewave: [:dev]
]
@known_only_keys Keyword.keys(@known_only_options)
require Logger
alias Igniter.Mix.Task.Info
def compose_install_and_validate!(
igniter \\ Igniter.new(),
argv,
schema,
task_name,
opts,
return \\ :options,
insert_before \\ nil,
acc \\ []
) do
schema = %{
schema
| flag_conflicts: flag_groups(schema, task_name),
alias_conflicts: alias_groups(schema, task_name)
}
schema = recursively_compose_schema(schema, argv, task_name, opts)
case schema.installs do
[] ->
names = Enum.map_join(List.wrap(schema.adds_deps), ", ", &elem(&1, 0))
igniter =
igniter
|> add_deps(
List.wrap(schema.adds_deps),
opts
)
|> Igniter.apply_and_fetch_dependencies(
Keyword.put(opts, :operation, "compiling #{names}")
)
if return == :options do
raise_on_conflicts!(schema, argv)
{igniter, Enum.uniq(acc), validate!(argv, schema, task_name)}
else
{igniter, acc, schema}
end
installs ->
schema = %{schema | installs: []}
install_names = Enum.map(installs, &elem(&1, 0))
count = Enum.count(install_names)
names_message =
if count >= 4 do
"#{count} packages"
else
Enum.join(
Enum.uniq(Enum.map(List.wrap(schema.adds_deps), &elem(&1, 0)) ++ install_names),
", "
)
end
installs =
Enum.map(installs, fn
{dep, val} when is_binary(val) ->
if dep in @known_only_keys || opts[:only] do
{dep, val, only: opts[:only] || @known_only_options[dep]}
else
{dep, val}
end
{dep, dep_opts} when is_list(dep_opts) ->
if dep in @known_only_keys || opts[:only] do
if opts[:only] do
{dep, Keyword.put(dep_opts, :only, opts[:only])}
else
{dep, Keyword.put_new(dep_opts, :only, @known_only_options[dep])}
end
else
{dep, dep_opts}
end
{dep, val, dep_opts} when dep in @known_only_keys ->
if dep in @known_only_keys || opts[:only] do
if opts[:only] do
{dep, val, Keyword.put(dep_opts, :only, opts[:only])}
else
{dep, val, Keyword.put_new(dep_opts, :only, @known_only_options[dep])}
end
else
{dep, val, dep_opts}
end
other ->
other
end)
igniter =
igniter
|> add_deps(
List.wrap(installs),
opts
)
|> Igniter.apply_and_fetch_dependencies(
Keyword.put(opts, :operation, "compiling #{names_message}")
)
|> maybe_set_dep_options(install_names, argv, task_name, opts)
acc = insert_installs(acc, install_names, insert_before)
adds_deps = schema.adds_deps
{igniter, acc, schema} =
installs
|> Enum.reduce({igniter, acc, schema}, fn install, {igniter, acc, schema} ->
name =
if is_tuple(install) do
elem(install, 0)
else
install
end
compose_install_and_validate!(
igniter,
argv,
%{
schema
| composes: ["#{name}.install"],
installs: [],
adds_deps: []
},
task_name,
Keyword.delete(opts, :only),
:schema,
name,
acc
)
end)
compose_install_and_validate!(
igniter,
argv,
%{
schema
| adds_deps: schema.adds_deps ++ adds_deps
},
task_name,
Keyword.delete(opts, :only),
return,
insert_before,
acc
)
end
end
defp insert_installs(acc, install_names, insert_before) do
if insert_before in acc do
Enum.flat_map(acc, fn item ->
if item == insert_before do
install_names ++ [item]
else
[item]
end
end)
else
install_names ++ acc
end
end
defp raise_on_conflicts!(schema, argv) do
flag_conflicts =
schema.flag_conflicts
|> Map.drop(Keyword.keys(Igniter.Mix.Task.Info.global_options()[:switches]))
|> Enum.filter(fn {_, list} ->
Enum.count(list) > 1
end)
|> Map.new(fn {k, v} ->
{"--" <> String.replace(to_string(k), "_", "-"), v}
end)
alias_conflicts =
schema.alias_conflicts
|> Map.drop(Keyword.keys(Igniter.Mix.Task.Info.global_options()[:aliases]))
|> Enum.flat_map(fn {k, list} ->
case Enum.uniq_by(list, &elem(&1, 0)) do
[] -> []
[_] -> []
list -> [{k, Enum.uniq(Enum.map(list, &elem(&1, 1)))}]
end
end)
|> Map.new(fn {k, v} ->
{"-" <> String.replace(to_string(k), "_", "-"), v}
end)
if Enum.empty?(flag_conflicts) and Enum.empty?(alias_conflicts) do
:ok
else
Enum.each(argv, fn arg ->
cond do
conflicting = flag_conflicts[arg] ->
Mix.shell().error("""
Ambiguous flag provided: `#{arg}`
The tasks or task groups `#{Enum.join(conflicting, ", ")}` all define the flag `#{arg}`.
To disambiguate, provide the arg as `--<prefix>.#{String.trim_leading(arg, "--")}`,
where `<prefix>` is the task or task group name.
For example:
`--#{Enum.at(conflicting, 0)}.#{String.trim_leading(arg, "--")}`
""")
exit({:shutdown, 2})
conflicting = alias_conflicts[arg] ->
Mix.shell().error("""
Ambiguous flag provided: `#{arg}`
The tasks or task groups `#{Enum.join(conflicting, ", ")}` all define the flag `#{arg}`.
To disambiguate, provide the arg as `-<prefix>.#{String.trim_leading(arg, "-")}`,
where `<prefix>` is the task or task group name.
For example:
`--#{Enum.at(conflicting, 0)}.#{String.trim_leading(arg, "-")}`
""")
exit({:shutdown, 2})
true ->
:ok
end
end)
end
end
defp maybe_set_dep_options(igniter, install_names, argv, parent, opts) do
Enum.reduce(install_names, igniter, fn install, igniter ->
composing_task = "#{install}.install"
with composing_task when not is_nil(composing_task) <- Mix.Task.get(composing_task),
true <- function_exported?(composing_task, :info, 2),
composing_schema when not is_nil(composing_schema) <-
composing_task.info(argv, parent) do
options =
if composing_schema.only && !opts[:only] do
Keyword.put(composing_schema.dep_opts, :only, composing_schema.only)
else
composing_schema.dep_opts
end
if options == [] do
igniter
else
Enum.reduce(options, igniter, fn {key, val}, igniter ->
val =
if key == :only do
List.wrap(val)
else
val
end
Igniter.Project.Deps.set_dep_option(igniter, install, key, val)
end)
end
else
_ ->
igniter
end
end)
end
defp add_deps(igniter, add_deps, opts) do
Enum.reduce(add_deps, igniter, fn dependency, igniter ->
with {name, _, dep_opts} <- dependency,
only when not is_nil(only) <- dep_opts[:only],
false <- Mix.env() in only do
igniter
|> Igniter.assign(
:failed_to_add_deps,
[name | igniter.assigns[:failed_to_add_deps] || []]
)
|> Igniter.add_warning("""
Dependency #{inspect(dependency)} could not be installed,
because it is configured to be installed with `only: #{inspect(only)}`.
Please install it manually, for example:
`MIX_ENV=#{Enum.at(only, 0)} mix igniter.install #{dependency}`.
""")
else
_ ->
new_igniter =
Igniter.Project.Deps.add_dep(
igniter,
dependency,
Keyword.merge(opts, error?: true, notify_on_present?: true, yes?: !!opts[:yes])
)
name =
case dependency do
{name, _} -> name
{name, _, _} -> name
end
if Enum.count(igniter.issues) != Enum.count(new_igniter.issues) ||
Enum.count(igniter.warnings) != Enum.count(new_igniter.warnings) do
Igniter.assign(
new_igniter,
:failed_to_add_deps,
[name | igniter.assigns[:failed_to_add_deps] || []]
)
else
new_igniter
end
end
end)
end
def validate!(argv, schema, task_name)
def validate!(_argv, nil, _task_name), do: {[], []}
def validate!(argv, schema, task_name) do
group = group(schema, task_name)
argv = args_for_group(argv, group)
merged_schema = recursively_compose_schema(schema, argv, task_name, [])
options_key =
if merged_schema.extra_args? do
:switches
else
:strict
end
OptionParser.parse!(
argv,
[
{options_key, clean_csv(merged_schema.schema || [])},
{:aliases, merged_schema.aliases || []}
]
)
end
defp clean_csv(schema) do
Enum.map(schema, fn
{k, :csv} ->
{k, :keep}
{k, v} ->
{k, v}
end)
end
@doc false
def args_for_group(argv, group) do
case argv do
["--" <> arg, "-" <> v2 | rest] ->
with true <- String.starts_with?(arg, "#{group}."),
[_, actual_arg] <- String.split(arg, "#{group}.", parts: 2) do
["--" <> actual_arg] ++ args_for_group(["-" <> v2 | rest], group)
else
_ ->
rest_args = args_for_group(["-" <> v2 | rest], group)
if String.contains?(arg, ".") do
rest_args
else
["--#{arg}"] ++ rest_args
end
end
["-" <> arg, "-" <> v2 | rest] ->
with true <- String.starts_with?(arg, "#{group}."),
[_, actual_arg] <- String.split(arg, "#{group}.", parts: 2) do
["-" <> actual_arg] ++ args_for_group(["-" <> v2 | rest], group)
else
_ ->
rest_args = args_for_group(["-" <> v2 | rest], group)
if String.contains?(arg, ".") do
rest_args
else
["-#{arg}"] ++ rest_args
end
end
["--" <> arg | rest] ->
with true <- String.starts_with?(arg, "#{group}."),
[_, actual_arg] <- String.split(arg, "#{group}.", parts: 2) do
["--" <> actual_arg] ++ args_for_group(rest, group)
else
_ ->
rest_args = args_for_group(rest, group)
if String.contains?(arg, ".") do
rest_args
else
["--#{arg}"] ++ rest_args
end
end
["-" <> arg | rest] ->
with true <- String.starts_with?(arg, "#{group}."),
[_, actual_arg] <- String.split(arg, "#{group}.", parts: 2) do
["-" <> actual_arg] ++ args_for_group(rest, group)
else
_ ->
rest_args = args_for_group(rest, group)
if String.contains?(arg, ".") do
rest_args
else
["-#{arg}"] ++ rest_args
end
end
[v1 | rest] ->
[v1] ++ args_for_group(rest, group)
[] ->
[]
end
end
defp recursively_compose_schema(%Info{composes: []} = schema, _argv, _parent, _opts), do: schema
defp recursively_compose_schema(%Info{composes: [compose | rest]} = schema, argv, parent, opts) do
with composing_task when not is_nil(composing_task) <- Mix.Task.get(compose),
true <- function_exported?(composing_task, :info, 2),
composing_schema when not is_nil(composing_schema) <- composing_task.info(argv, parent) do
composing_task_name = Mix.Task.task_name(composing_task)
composing_schema =
if opts[:only] do
%{composing_schema | only: opts[:only]}
else
composing_schema
end
recursively_compose_schema(
%{
schema
| schema:
merge_schemas(
schema.schema,
composing_schema.schema
),
aliases:
merge_aliases(
schema.aliases,
composing_schema.aliases
),
flag_conflicts: flag_conflicts(schema, composing_schema, composing_task_name),
alias_conflicts: alias_conflicts(schema, composing_schema, composing_task_name),
composes: rest,
extra_args?: schema.extra_args? || composing_schema.extra_args?,
installs: Keyword.merge(composing_schema.installs, schema.installs),
adds_deps: Keyword.merge(composing_schema.adds_deps, schema.adds_deps)
},
argv,
parent,
Keyword.delete(opts, :only)
)
|> Map.put(:composes, List.wrap(composing_schema.composes))
|> recursively_compose_schema(argv, composing_task_name, Keyword.delete(opts, :only))
else
_ ->
recursively_compose_schema(
%{schema | composes: rest},
argv,
parent,
Keyword.delete(opts, :only)
)
end
end
defp flag_conflicts(schema, composing_schema, composing_task_name) do
Map.merge(
schema.flag_conflicts,
flag_groups(composing_schema, composing_task_name),
fn _key, schema_value, composing_value ->
Enum.uniq(schema_value ++ composing_value)
end
)
end
defp alias_conflicts(schema, composing_schema, composing_task_name) do
Map.merge(
schema.alias_conflicts,
alias_groups(composing_schema, composing_task_name),
fn _key, schema_value, composing_value ->
Enum.uniq(schema_value ++ composing_value)
end
)
end
@doc false
def group(%{group: group}, _task_name) when not is_nil(group),
do: String.replace(to_string(group), "_", "-")
def group(_, task_name), do: String.replace(task_name, "_", "-")
defp merge_schemas(schema, composing_schema) do
schema = schema || []
composing_schema = composing_schema || []
Keyword.merge(composing_schema, schema)
end
defp merge_aliases(aliases, composing_aliases) do
aliases = aliases || []
composing_aliases = composing_aliases || []
Keyword.merge(composing_aliases, aliases)
end
defp flag_groups(schema, task_name) do
Map.new(schema.schema, fn {k, _} ->
{k, [group(schema, task_name)]}
end)
end
defp alias_groups(schema, task_name) do
Map.new(schema.aliases, fn {k, v} ->
{k, [{v, group(schema, task_name)}]}
end)
end
end