Packages
igniter
0.6.10
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/project/deps.ex
defmodule Igniter.Project.Deps do
@moduledoc "Codemods and utilities for managing dependencies declared in mix.exs"
require Igniter.Code.Common
alias Igniter.Code.Common
alias Sourceror.Zipper
@doc """
Adds a dependency to the mix.exs file.
```elixir
|> Igniter.Project.Deps.add_dep({:my_dependency, "~> X.Y.Z"})
```
# Options
- `:yes?` - Automatically answer yes to any prompts.
- `:append?` - Append to the dependency list instead of prepending.
- `:error?` - Returns an error instead of a notice on failure.
"""
def add_dep(igniter, dep, opts \\ []) do
case dep do
{name, version} ->
add_dependency(igniter, name, version, opts)
{name, version, version_opts} ->
new_igniter =
if Keyword.keyword?(version) do
add_dependency(igniter, name, version ++ version_opts, opts)
else
add_dependency(igniter, name, version, Keyword.put(opts, :dep_opts, version_opts))
end
new_igniter =
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
new_igniter
other ->
raise ArgumentError, "Invalid dependency: #{inspect(other)}"
end
end
@deprecated "Use `add_dep/2` or `add_dep/3` instead."
def add_dependency(igniter, name, version, opts \\ []) do
case get_dep(igniter, name) do
{:ok, nil} ->
do_add_dependency(igniter, name, version, opts)
{:error, error} ->
if opts[:error?] do
Igniter.add_issue(igniter, error)
else
Igniter.add_warning(igniter, error)
end
{:ok, current} ->
desired =
if opts[:dep_opts] do
Code.eval_string(
"{#{inspect(name)}, #{inspect(version)}, #{inspect(opts[:dep_opts])}}"
)
|> elem(0)
else
Code.eval_string("{#{inspect(name)}, #{inspect(version)}}") |> elem(0)
end
current = Code.eval_string(current) |> elem(0)
{desired, current} =
case {desired, current} do
{{da, db}, {ca, cb, []}} ->
{{da, db}, {ca, cb}}
{{da, db, []}, {ca, cb}} ->
{{da, db}, {ca, cb}}
{desired, current} ->
{desired, current}
end
if desired == current do
if opts[:notify_on_present?] do
Mix.shell().info(
"Dependency #{name} is already in mix.exs with the desired version. Skipping."
)
end
igniter
else
if opts[:yes?] ||
Igniter.Util.IO.yes?("""
Dependency #{name} is already in mix.exs. Should we replace it?
Desired: `#{inspect(desired)}`
Found: `#{inspect(current)}`
""") do
do_add_dependency(igniter, name, version, opts)
else
igniter
end
end
end
end
@doc "Sets a dependency option for an existing dependency"
@spec set_dep_option(Igniter.t(), atom(), atom(), quoted :: term) :: Igniter.t()
def set_dep_option(igniter, name, key, quoted) do
Igniter.update_elixir_file(igniter, "mix.exs", fn zipper ->
with {:ok, zipper} <- Igniter.Code.Module.move_to_module_using(zipper, Mix.Project),
{:ok, zipper} <- Igniter.Code.Function.move_to_defp(zipper, :deps, 0),
true <- Igniter.Code.List.list?(zipper),
{:ok, zipper} <-
Igniter.Code.List.move_to_list_item(zipper, fn zipper ->
if Igniter.Code.Tuple.tuple?(zipper) do
case Igniter.Code.Tuple.tuple_elem(zipper, 0) do
{:ok, first_elem} ->
Common.nodes_equal?(first_elem, name)
:error ->
false
end
end
end) do
case Igniter.Code.Tuple.tuple_elem(zipper, 2) do
{:ok, zipper} ->
Igniter.Code.Keyword.set_keyword_key(zipper, key, quoted, fn zipper ->
{:ok,
Igniter.Code.Common.replace_code(
zipper,
quoted
)}
end)
:error ->
with {:ok, zipper} <- Igniter.Code.Tuple.tuple_elem(zipper, 1),
true <- Igniter.Code.List.list?(zipper) do
Igniter.Code.Keyword.set_keyword_key(
zipper,
key,
quoted,
fn zipper ->
{:ok,
Igniter.Code.Common.replace_code(
zipper,
quoted
)}
end
)
else
_ ->
Igniter.Code.Tuple.append_elem(zipper, [{key, quoted}])
end
end
else
_ ->
{:ok, zipper}
end
end)
end
@doc "Returns true if the given dependency is in `mix.exs`"
def has_dep?(igniter, name) do
!!get_dependency_declaration(igniter, name)
end
@doc "Gets the current dependency declaration in mix.exs for a given dependency."
@deprecated "use `get_dep/2` instead, which can return ok/error tuple"
def get_dependency_declaration(igniter, name) do
case get_dep(igniter, name) do
{:ok, dep} -> dep
_ -> nil
end
end
@doc "Gets the current dependency declaration in mix.exs for a given dependency."
@spec get_dep(Igniter.t(), name :: atom()) :: {:ok, nil | String.t()} | {:error, String.t()}
def get_dep(igniter, name) do
zipper =
igniter
|> Igniter.include_existing_file("mix.exs")
|> Map.get(:rewrite)
|> Rewrite.source!("mix.exs")
|> Rewrite.Source.get(:quoted)
|> Zipper.zip()
with {:ok, zipper} <- Igniter.Code.Module.move_to_module_using(zipper, Mix.Project),
{:ok, zipper} <- Igniter.Code.Function.move_to_defp(zipper, :deps, 0) do
case igniter.assigns[:igniter_exs][:deps_location] || :last_list_literal do
{m, f, a} ->
case apply(m, f, [a] ++ [igniter, zipper]) do
{:ok, zipper} ->
get_dep_declaration(zipper, name)
:error ->
{:error,
"""
Could not get dependency #{inspect(name)}
#{inspect(m)}.#{f}/#{Enum.count(a) + 2} did not find a deps location
Please remove the dependency manually.
"""}
end
{:variable, name} ->
with {:ok, zipper} <-
Igniter.Code.Common.move_to(
zipper,
&Igniter.Code.Common.variable_assignment?(&1, name)
),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
true <- Igniter.Code.List.list?(zipper) do
get_dep_declaration(zipper, name)
else
_ ->
{:error,
"""
Could not get dependency #{inspect(name)}
`deps/0` does not contain an assignment of the `#{name}` variable to a literal list
Please remove the dependency manually.
"""}
end
:last_list_literal ->
zipper = Zipper.rightmost(zipper)
if Igniter.Code.List.list?(zipper) do
get_dep_declaration(zipper, name)
else
{:error,
"""
Could not get dependency #{inspect(name)}
`deps/0` does not end in a list literal that can be read.
"""}
end
end
else
_ ->
nil
end
end
defp get_dep_declaration(zipper, name) do
case Igniter.Code.List.move_to_list_item(zipper, fn item ->
if Igniter.Code.Tuple.tuple?(item) do
case Igniter.Code.Tuple.tuple_elem(item, 0) do
{:ok, first_elem} ->
Common.nodes_equal?(first_elem, name)
:error ->
false
end
end
end) do
{:ok, current_declaration} ->
{:ok,
current_declaration
|> Zipper.node()
|> Sourceror.to_string()}
_ ->
{:ok, nil}
end
end
@doc "Removes a dependency from mix.exs"
def remove_dep(igniter, name) do
igniter
|> Igniter.update_elixir_file("mix.exs", fn zipper ->
with {:ok, zipper} <- Igniter.Code.Module.move_to_module_using(zipper, Mix.Project),
{:ok, zipper} <- Igniter.Code.Function.move_to_defp(zipper, :deps, 0) do
case igniter.assigns[:igniter_exs][:deps_location] || :last_list_literal do
{m, f, a} ->
case apply(m, f, [a] ++ [igniter, zipper]) do
{:ok, zipper} ->
remove_from_deps_list(zipper, name)
:error ->
{:warning,
"""
Could not remove dependency #{inspect(name)}
#{inspect(m)}.#{f}/#{Enum.count(a) + 2} did not find a deps location
Please remove the dependency manually.
"""}
end
{:variable, name} ->
with {:ok, zipper} <-
Igniter.Code.Common.move_to(
zipper,
&Igniter.Code.Common.variable_assignment?(&1, name)
),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
true <- Igniter.Code.List.list?(zipper) do
remove_from_deps_list(zipper, name)
else
_ ->
{:warning,
"""
Could not remove dependency #{inspect(name)}
`deps/0` does not contain an assignment of the `#{name}` variable to a literal list
Please remove the dependency manually.
"""}
end
:last_list_literal ->
zipper = Zipper.rightmost(zipper)
if Igniter.Code.List.list?(zipper) do
remove_from_deps_list(zipper, name)
else
{:warning,
"""
Could not remove dependency #{inspect(name)}
`deps/0` does not end in a list literal that can be removed from.
Please remove the dependency manually.
"""}
end
end
else
_ ->
{:warning,
"""
Failed to remove dependency #{inspect(name)} from `mix.exs`.
Please remove the dependency manually.
"""}
end
end)
end
defp remove_from_deps_list(zipper, name) do
with current_declaration_index when not is_nil(current_declaration_index) <-
Igniter.Code.List.find_list_item_index(zipper, fn item ->
if Igniter.Code.Tuple.tuple?(item) do
case Igniter.Code.Tuple.tuple_elem(item, 0) do
{:ok, first_elem} ->
Common.nodes_equal?(first_elem, name)
:error ->
false
end
end
end),
{:ok, zipper} <- Igniter.Code.List.remove_index(zipper, current_declaration_index) do
{:ok, zipper}
else
_ ->
{:warning,
"""
Failed to remove dependency #{inspect(name)} from `mix.exs`.
Please remove the old dependency manually.
"""}
end
end
defp do_add_dependency(igniter, name, version, opts) do
error_tag =
if opts[:error?] do
:error
else
:warning
end
quoted =
if opts[:dep_opts] do
quote do
{unquote(name), unquote(version), unquote(opts[:dep_opts])}
end
else
{:__block__, [],
[
{{:__block__, [], [name]}, {:__block__, [], [version]}}
]}
end
igniter
|> Igniter.update_elixir_file("mix.exs", fn zipper ->
with {:ok, zipper} <- Igniter.Code.Module.move_to_module_using(zipper, Mix.Project),
{:ok, zipper} <- Igniter.Code.Function.move_to_defp(zipper, :deps, 0) do
case igniter.assigns[:igniter_exs][:deps_location] || :last_list_literal do
{m, f, a} ->
case apply(m, f, [a] ++ [igniter, zipper]) do
{:ok, zipper} ->
add_to_deps_list(zipper, name, quoted, opts)
:error ->
{error_tag,
"""
Could not add dependency #{inspect({name, version})}
#{inspect(m)}.#{f}/#{Enum.count(a) + 2} did not find a deps location
Please add the dependency manually.
"""}
end
{:variable, name} ->
with {:ok, zipper} <-
Igniter.Code.Common.move_to(
zipper,
&Igniter.Code.Common.variable_assignment?(&1, name)
),
{:ok, zipper} <- Igniter.Code.Function.move_to_nth_argument(zipper, 1),
true <- Igniter.Code.List.list?(zipper) do
add_to_deps_list(zipper, name, quoted, opts)
else
_ ->
{error_tag,
"""
Could not add dependency #{inspect({name, version})}
`deps/0` does not contain an assignment of the `#{name}` variable to a literal list
Please add the dependency manually.
"""}
end
:last_list_literal ->
zipper = Zipper.rightmost(zipper)
if Igniter.Code.List.list?(zipper) do
add_to_deps_list(zipper, name, quoted, opts)
else
{error_tag,
"""
Could not add dependency #{inspect({name, version})}
`deps/0` does not end in a list literal that can be added to.
Please add the dependency manually.
"""}
end
end
else
_ ->
{error_tag,
"""
Could not add dependency #{inspect({name, version})}
`mix.exs` file does not contain a `deps/0` function.
Please add the dependency manually.
"""}
end
end)
end
defp add_to_deps_list(zipper, name, quoted, opts) do
match =
Igniter.Code.List.move_to_list_item(zipper, fn zipper ->
if Igniter.Code.Tuple.tuple?(zipper) do
case Igniter.Code.Tuple.tuple_elem(zipper, 0) do
{:ok, first_elem} ->
Common.nodes_equal?(first_elem, name)
:error ->
false
end
end
end)
case match do
{:ok, zipper} ->
Igniter.Code.Common.replace_code(zipper, quoted)
_ ->
if Keyword.get(opts, :append?, false) do
Igniter.Code.List.append_to_list(zipper, quoted)
else
Igniter.Code.List.prepend_to_list(zipper, quoted)
end
end
end
@doc false
def determine_dep_type_and_version!(requirement) do
[package | maybe_version] = String.split(requirement, "@", trim: true, parts: 2)
{package, opts} =
case String.split(package, "/", parts: 2) do
[org, package] -> {package, organization: org}
[package] -> {package, []}
end
{package, opts} =
case String.split(package, ".", parts: 2) do
[repo, package] -> {package, Keyword.put(opts, :repo, repo)}
[package] -> {package, opts}
end
case maybe_version do
[] ->
with {:ok, version} <- fetch_latest_version(package, opts) do
{version, []}
end
["git:" <> requirement] ->
{nil, git_dep_opts(requirement, :git)}
["github:" <> requirement] ->
{nil, git_dep_opts(requirement, :github)}
["path:" <> requirement] ->
{nil, path: requirement, override: true}
[version] ->
case Version.parse(version) do
{:ok, version} ->
{"== #{version}", []}
_ ->
case Igniter.Util.Version.version_string_to_general_requirement(version) do
{:ok, requirement} ->
{requirement, []}
_ ->
:error
end
end
_ ->
:error
end
|> case do
{version, additional_opts} ->
to_dependency_spec(package, version, additional_opts ++ opts)
:error ->
if opts[:repo] do
Mix.raise("""
Failed to automatically determine latest version for `#{requirement}`
Private repositories (like `#{opts[:repo]}`) do not always expose APIs that allow
us to determine the latest verison. This means you may need to specify a version manually.
For example:
mix igniter.install #{requirement}@1.0
Please see the documentation of the package you are trying to install for more information.
""")
else
if opts[:organization] do
Mix.raise("""
Failed to automatically determine latest version for `#{requirement}`.
You may need to specify a version manually. For example:
mix igniter.install #{requirement}@1.0
Please see the documentation of the package you are trying to install for more information.
""")
else
Mix.raise("""
Failed to automatically determine latest version for `#{requirement}`
This may mean that the package `#{package}` is not available on hex, or that you are missing some extra information.
You may need to specify a version manually. For example:
mix igniter.install #{requirement}@1.0
If it is a private package from a hexpm organization, include that organization.
mix igniter.install org/#{requirement}
Or if it is a package from a private repository, include the repository name.
mix igniter.install repo.#{requirement}
""")
end
end
end
end
defp to_dependency_spec(package, version, opts)
defp to_dependency_spec(package, version, []), do: {String.to_atom(package), version}
defp to_dependency_spec(package, nil, opts), do: {String.to_atom(package), opts}
defp to_dependency_spec(package, version, opts), do: {String.to_atom(package), version, opts}
defp git_dep_opts(string, kind) do
case String.split(string, "@", trim: true, parts: 2) do
[git_dep, ref] ->
[{kind, git_dep}, {:ref, ref}, {:override, true}]
[git_dep] ->
[{kind, git_dep}, {:override, true}]
end
end
defp fetch_latest_version(package, opts) do
if Regex.match?(~r/^[a-z][a-z0-9_]*$/, package) do
{:ok, _} = Application.ensure_all_started(:req)
with {:ok, url, headers} <-
fetch_hex_api_url_and_headers(package, opts),
{:ok, %{body: %{"releases" => releases} = body}} <-
Req.get(url,
headers: headers
),
%{"version" => version} <- first_non_rc_version_or_first_version(releases, body) do
{:ok, Igniter.Util.Version.version_string_to_general_requirement!(version)}
else
_ -> :error
end
else
:error
end
rescue
_ ->
:error
end
def fetch_hex_api_url_and_headers(package, opts) do
default_headers = [
{"User-Agent", "igniter-installer"},
{
"Accept",
"application/json"
}
]
cond do
repo = opts[:repo] ->
repo =
:repos
|> fetch_hex_state()
|> Map.get(repo)
|> Kernel.||(
raise """
No repository found for #{opts[:repo]}. Perhaps you missed a setup step, like below?
mix hex.repo add ...
"""
)
fetch_public_package_url(
repo.url,
package,
default_headers ++ auth_headers(key: repo.auth_key)
)
org = opts[:organization] ->
fetch_org_package_url("https://hex.pm/api", package, org, default_headers)
true ->
fetch_public_package_url(
"https://hex.pm/api",
package,
default_headers
)
end
end
defp fetch_public_package_url(api_url, package, default_headers) do
{:ok, "#{api_url}/packages/#{package}", default_headers}
end
defp fetch_org_package_url(api_url, package, org, default_headers) do
auth = get_hex_auth()
{:ok, "#{api_url}/repos/#{org}/packages/#{package}", auth_headers(auth) ++ default_headers}
end
defp get_hex_auth do
api_key_write_unencrypted = fetch_hex_state(:api_key_write_unencrypted)
api_key_read = fetch_hex_state(:api_key_read)
cond do
api_key_write_unencrypted ->
[key: api_key_write_unencrypted]
api_key_read ->
[key: api_key_read]
true ->
raise """
No authentication key found for api:read.
Please run `mix hex.user auth` to authenticate with Hex and ensure that the user is a member of the organization.
"""
end
end
defp auth_headers(opts) do
cond do
opts[:key] ->
[{"authorization", opts[:key]}]
opts[:user] && opts[:pass] ->
base64 = :base64.encode(opts[:user] <> ":" <> opts[:pass])
[{"authorization", "Basic " <> base64}]
true ->
[]
end
end
defp first_non_rc_version_or_first_version(releases, body) do
releases = Enum.reject(releases, &body["retirements"][&1["version"]])
Enum.find(releases, Enum.at(releases, 0), fn release ->
!rc?(release["version"])
end)
end
# This just actually checks if there is any pre-release metadata
defp rc?(version) do
version
|> Version.parse!()
|> Map.get(:pre)
|> Enum.any?()
end
# This is a hack. I've done this because actually talking to hex was causing
# errors w/ the type system on 1.16, and including `:hex` in the apps list
# caused its own kind of strange compilation errors.
defp fetch_hex_state(key) do
Agent.get(Hex.State, fn state ->
case Map.fetch(state, key) do
{:ok, {_source, value}} ->
value
_ ->
nil
end
end)
end
end