Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add mix deps.add task #10291

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
4 changes: 4 additions & 0 deletions lib/mix/lib/mix/dep.ex
Expand Up @@ -589,4 +589,8 @@ defmodule Mix.Dep do
if is_binary(app), do: String.to_atom(app), else: app
end)
end

def add(app, opts, mix_exs) do
Mix.Dep.Adder.add(app, opts, mix_exs)
end
end
107 changes: 107 additions & 0 deletions lib/mix/lib/mix/dep/adder.ex
@@ -0,0 +1,107 @@
defmodule Mix.Dep.Adder do
def add(app, opts, mix_exs) do
lines = String.split(mix_exs, "\n")

check_for_app!(lines, app)

dep_opts = get_version_str(opts, app) <> get_only_string(opts) <> get_runtime_string(opts)
new_line = "{:#{app}, #{dep_opts}}"

new_lines = add_dep_line(lines, [], new_line)

if lines == new_lines, do: Mix.raise("Unable to identify deps function in mix.exs")

Mix.shell().info([:green, "Added #{app} to mix.exs: #{new_line}"])

Enum.join(new_lines, "\n")
end

defp get_runtime_string(opts) do
if Keyword.has_key?(opts, :runtime) and !opts[:runtime] do
", runtime: false"
else
""
end
end

defp get_only_string(opts) do
case opts[:only] do
[] -> ""
[env] -> ", only: :#{env}"
envs -> ", only: [" <> (envs |> Enum.map(&(":" <> &1)) |> Enum.join(", ")) <> "]"
end
end

defp get_version_str(opts, app) do
case opts[:path] do
nil ->
case opts[:version] || latest_hex_version(app) do
"~> " <> _ = version -> ~s|"#{version}"|
">= " <> _ = version -> ~s|"#{version}"|
"0.0.0" -> ~s|">= 0.0.0"|
version -> ~s|"~> #{version}"|
end

path ->
~s|path: "#{path}"|
end
end

defp check_for_app!(lines, app) do
lines
|> Enum.find(fn line ->
line
|> String.trim()
|> String.starts_with?("{:" <> app)
end)
|> case do
nil ->
:ok

found ->
Mix.raise("#{app} already exists in mix.exs as #{String.trim(found)}")
end
end

defp latest_hex_version(_app) do
# TODO: Get latest version from Hex
"0.0.0"
end

defp add_dep_line([line1, line2 | rest], acc, dep_line) do
cond do
String.trim(line1) == "defp deps do" && String.trim(line2) == "[" ->
dep_line =
if additional_deps?(rest) do
dep_line <> ","
else
dep_line
end

Enum.reverse(acc) ++ [line1, line2, indent(dep_line, line2) | rest]

String.trim(line1) == "defp deps do" && String.trim(line2) == "[]" ->
Enum.reverse(acc) ++
[line1, indent("[", line1), indent(dep_line, line2), indent("]", line1) | rest]

true ->
add_dep_line([line2 | rest], [line1 | acc], dep_line)
end
end

defp indent(line, parent_line) do
String.duplicate(" ", indented(parent_line) + 2) <> line
end

defp indented(line, count \\ 0)
defp indented(" " <> rest, count), do: indented(rest, count + 1)
defp indented(_, count), do: count

defp additional_deps?([head | tail]) do
case String.trim(head) do
"{:" <> _ -> true
"]" -> false
_ -> additional_deps?(tail)
end
end
end
98 changes: 98 additions & 0 deletions lib/mix/lib/mix/tasks/deps.add.ex
@@ -0,0 +1,98 @@
defmodule Mix.Tasks.Deps.Add do
use Mix.Task

@shortdoc "Adds the specified dependency"

@moduledoc """
Adds the given dependency to mix.exs.

The given dependency will be added to mix.exs at the given version, or the
latest version available on hex if no version is specified. Alternatively a
path can be specified instead of a version. `runtime: false` and `only: :env`
can also be specified by the below options.

## Command line options

* `--version` - version to add (defaults to latest available on hex)
* `--only` - Adds the only: :env option to the dependency
* `--no-runtime - Adds the runtime: false option to the dependency
* `--path` - Adds the `path: "path"` option to the dependency instead of `version:`

## Notes

--version will automatically add "~> " to the front of the version unless the
version is `0.0.0`, in which case it will use `>= 0.0.0`

--only` supports multiple values in this format:

`mix deps.add foo --only test --only dev

`mix deps.add` requires the standard deps function in your mix.exs file in one of these formats:

defp deps do
[
...

or
defp deps do
[]
...

"""

@snake_case_regex ~r/^[a-z|_]+$/

@impl true
def run(args) do
mix_exs = add(args, File.read!("mix.exs"))
File.write!("mix.exs", mix_exs)
end

def add(args, mix_exs) do
{opts, rest, invalid} =
OptionParser.parse(args,
strict: [version: :string, only: :keep, runtime: :boolean, path: :string]
)

app =
case rest do
[binary] when is_binary(binary) ->
binary

_ ->
Mix.raise("Invalid options: #{inspect(rest)}")
end

if invalid != [], do: Mix.raise("Invalid options: #{inspect(invalid)}")

if opts[:version] && opts[:path],
do: Mix.raise("Cannot specify both version and path")

app = normalize_atom(app, "package")

only =
case Keyword.get_values(opts, :only) do
nil -> []
env when is_binary(env) -> [normalize_atom(env, "only")]
envs when is_list(envs) -> Enum.map(envs, &normalize_atom(&1, "only"))
end

opts = Keyword.put(opts, :only, only)

Mix.Dep.add(app, opts, mix_exs)
end

defp normalize_atom(atom, type) do
atom =
case atom do
":" <> atom -> atom
atom -> atom
end

if !Regex.match?(@snake_case_regex, atom) do
Mix.raise("Invalid #{type}: #{inspect(atom)}")
end

atom
end
end