Skip to content

Commit 7aa5a89

Browse files
author
José Valim
committed
Add support for optional dependencies
1 parent 3ede5ff commit 7aa5a89

File tree

7 files changed

+164
-48
lines changed

7 files changed

+164
-48
lines changed

lib/mix/lib/mix/deps.ex

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,7 @@ defmodule Mix.Deps do
4949
* `:env` - The environment to run the dependency on, defaults to :prod
5050
* `:compile` - A command to compile the dependency, defaults to a mix,
5151
rebar or make command
52+
* `:optional` - The dependency is optional and used only to specify requirements
5253
5354
## Git options (`:git`)
5455
@@ -230,7 +231,7 @@ defmodule Mix.Deps do
230231
end
231232

232233
def format_status(Mix.Dep[app: app, status: { :diverged, other }] = dep) do
233-
"different specs were given for the #{inspect app} app:\n" <>
234+
"different specs were given for the #{app} app:\n" <>
234235
"#{dep_status(dep)}#{dep_status(other)}" <>
235236
"\n Ensure they match or specify one of the above in your #{inspect Mix.Project.get} deps and set `override: true`"
236237
end
@@ -251,7 +252,7 @@ defmodule Mix.Deps do
251252
do: "the dependency requires Elixir #{req} but you are running on v#{System.version}"
252253
253254
defp dep_status(Mix.Dep[app: app, requirement: req, opts: opts, from: from]) do
254-
info = { app, req, Dict.drop(opts, [:dest, :lock, :env]) }
255+
info = { app, req, Dict.drop(opts, [:dest, :lock, :env, :drop]) }
255256
"\n > In #{Path.relative_to_cwd(from)}:\n #{inspect info}\n"
256257
end
257258

lib/mix/lib/mix/deps/converger.ex

Lines changed: 18 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,8 @@ defmodule Mix.Deps.Converger do
5050
config = [ deps_path: Path.expand(Mix.project[:deps_path]),
5151
root_lockfile: Path.expand(Mix.project[:lockfile]) ]
5252
main = Mix.Deps.Retriever.children
53-
all(main, [], [], main, config, callback, rest)
53+
apps = Enum.map(main, &(&1.app))
54+
all(main, [], [], apps, config, callback, rest)
5455
end
5556

5657
# We traverse the tree of dependencies in a breadth-
@@ -105,8 +106,11 @@ defmodule Mix.Deps.Converger do
105106
# dependency), we fetch the dependency including its latest info
106107
# and children information.
107108
{ dep, children } = Mix.Deps.Retriever.fetch(dep, config)
109+
children = reject_non_fullfilled_optional(children, current_breadths)
110+
dep = dep.deps(Enum.map(children, &(&1.app)))
111+
108112
{ acc, rest } = all(t, [dep|acc], upper_breadths, current_breadths, config, callback, rest)
109-
all(children, acc, current_breadths, children ++ current_breadths, config, callback, rest)
113+
all(children, acc, current_breadths, dep.deps ++ current_breadths, config, callback, rest)
110114
end
111115
end
112116

@@ -120,11 +124,7 @@ defmodule Mix.Deps.Converger do
120124
# overrider is moved to the front of the accumulator to
121125
# preserve the position of the removed dep.
122126
defp overriden_deps(acc, upper_breadths, dep) do
123-
overriden = Enum.any?(upper_breadths, fn(other) ->
124-
other.app == dep.app
125-
end)
126-
127-
if overriden do
127+
if dep.app in upper_breadths do
128128
Mix.Dep[app: app] = dep
129129

130130
{ overrider, acc } =
@@ -169,13 +169,23 @@ defmodule Mix.Deps.Converger do
169169
if match, do: acc
170170
end
171171

172+
defp converge?(_, Mix.Dep[scm: Mix.SCM.Optional]) do
173+
true
174+
end
175+
172176
defp converge?(Mix.Dep[scm: scm, opts: opts1], Mix.Dep[scm: scm, opts: opts2]) do
173177
scm.equal?(opts1, opts2)
174178
end
175179

176180
defp converge?(_, _), do: false
177181

178-
def with_matching_req(Mix.Dep[] = other, Mix.Dep[] = dep) do
182+
defp reject_non_fullfilled_optional(children, upper_breadths) do
183+
Enum.reject children, fn Mix.Dep[app: app, opts: opts] ->
184+
opts[:optional] && not(app in upper_breadths)
185+
end
186+
end
187+
188+
defp with_matching_req(Mix.Dep[] = other, Mix.Dep[] = dep) do
179189
case other.status do
180190
{ :ok, vsn } when not nil?(vsn) ->
181191
if Mix.Deps.Retriever.vsn_match?(dep.requirement, vsn) do

lib/mix/lib/mix/deps/retriever.ex

Lines changed: 13 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,12 @@ defmodule Mix.Deps.Retriever do
88
Gets all direct children of the current `Mix.Project`
99
as a `Mix.Dep` record. Umbrella project dependencies
1010
are included as children.
11+
12+
Optional dependencies are filtered out as they only
13+
matter as a dependency children.
1114
"""
1215
def children do
13-
scms = Mix.SCM.available
14-
from = Path.absname("mix.exs")
15-
Enum.map(Mix.project[:deps] || [], &to_dep(&1, scms, from)) ++
16-
Mix.Deps.Umbrella.unfetched
16+
mix_children |> Enum.reject(fn Mix.Dep[opts: opts] -> opts[:optional] end)
1717
end
1818

1919
@doc """
@@ -46,7 +46,6 @@ defmodule Mix.Deps.Retriever do
4646
mix_dep(dep.manager(:mix), config)
4747
end
4848

49-
dep = dep.deps(Enum.map(children, &(&1.app)))
5049
{ validate_app(dep), children }
5150
end
5251

@@ -97,7 +96,7 @@ defmodule Mix.Deps.Retriever do
9796
]
9897
else
9998
raise Mix.Error, message: "#{inspect Mix.Project.get} did not specify a supported scm " <>
100-
"for app #{inspect app}, expected one of :git, :path or :in_umbrella"
99+
"for app #{inspect app}, expected one of :git, :path, :in_umbrella or :optional"
101100
end
102101
end
103102

@@ -154,10 +153,17 @@ defmodule Mix.Deps.Retriever do
154153
true -> status
155154
end
156155

157-
{ dep.manager(:mix).opts(opts).status(stat), children }
156+
{ dep.manager(:mix).opts(opts).status(stat), mix_children }
158157
end)
159158
end
160159

160+
defp mix_children do
161+
scms = Mix.SCM.available
162+
from = Path.absname("mix.exs")
163+
Enum.map(Mix.project[:deps] || [], &to_dep(&1, scms, from)) ++
164+
Mix.Deps.Umbrella.unfetched
165+
end
166+
161167
defp rebar_dep(Mix.Dep[opts: opts] = dep, _config) do
162168
File.cd!(opts[:dest], fn ->
163169
config = Mix.Rebar.load_config(".")

lib/mix/lib/mix/scm.ex

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,5 +125,6 @@ defmodule Mix.SCM do
125125
def register_builtin do
126126
register Mix.SCM.Git
127127
register Mix.SCM.Path
128+
register Mix.SCM.Optional
128129
end
129130
end

lib/mix/lib/mix/scm/optional.ex

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
defmodule Mix.SCM.Optional do
2+
@behavior Mix.SCM
3+
@moduledoc false
4+
5+
def format(_opts) do
6+
"[optional]"
7+
end
8+
9+
def format_lock(_lock) do
10+
nil
11+
end
12+
13+
def accepts_options(_app, opts) do
14+
if opts[:optional], do: opts
15+
end
16+
17+
def checked_out?(_opts) do
18+
true
19+
end
20+
21+
def lock_status(_opts) do
22+
:ok
23+
end
24+
25+
def equal?(_opts1, _opts2) do
26+
true
27+
end
28+
29+
def checkout(_opts) do
30+
raise Mix.Error, message: "Cannot checkout optional dependency"
31+
end
32+
33+
def update(_opts) do
34+
raise Mix.Error, message: "Cannot update optional dependency"
35+
end
36+
37+
def clean(_opts) do
38+
raise Mix.Error, message: "Cannot clean optional dependency"
39+
end
40+
end

lib/mix/test/mix/deps_test.exs

Lines changed: 57 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ defmodule Mix.DepsTest do
1111
{ :invalidvsn, "0.2.0", path: "deps/invalidvsn" },
1212
{ :invalidapp, "0.1.0", path: "deps/invalidapp" },
1313
{ :noappfile, "0.1.0", path: "deps/noappfile" },
14-
{ :uncloned, git: "https://github.com/elixir-lang/uncloned.git" }
14+
{ :uncloned, git: "https://github.com/elixir-lang/uncloned.git" },
15+
{ :optional, optional: true },
16+
{ :optional_git, optional: true, git: "https://github.com/elixir-lang/optinal.git" }
1517
]
1618
]
1719
end
@@ -34,6 +36,7 @@ defmodule Mix.DepsTest do
3436

3537
in_fixture "deps_status", fn ->
3638
deps = Mix.Deps.fetched
39+
assert length(deps) == 5
3740
assert Enum.find deps, &match?(Mix.Dep[app: :ok, status: { :ok, _ }], &1)
3841
assert Enum.find deps, &match?(Mix.Dep[app: :invalidvsn, status: { :invalidvsn, :ok }], &1)
3942
assert Enum.find deps, &match?(Mix.Dep[app: :invalidapp, status: { :invalidapp, _ }], &1)
@@ -60,7 +63,7 @@ defmodule Mix.DepsTest do
6063

6164
in_fixture "deps_status", fn ->
6265
msg = "Mix.DepsTest.NoSCMApp did not specify a supported scm for app :ok, " <>
63-
"expected one of :git, :path or :in_umbrella"
66+
"expected one of :git, :path, :in_umbrella or :optional"
6467
assert_raise Mix.Error, msg, fn -> Mix.Deps.fetched end
6568
end
6669
after
@@ -104,6 +107,32 @@ defmodule Mix.DepsTest do
104107
Mix.Project.pop
105108
end
106109

110+
test "nested optional deps are never added" do
111+
Mix.Project.push NestedDepsApp
112+
113+
in_fixture "deps_status", fn ->
114+
File.write!("custom/deps_repo/mix.exs", """)
115+
defmodule DepsRepo do
116+
use Mix.Project
117+
118+
def project do
119+
[
120+
app: :deps_repo,
121+
version: "0.1.0",
122+
deps: [
123+
{ :git_repo, "0.2.0", optional: true }
124+
]
125+
]
126+
end
127+
end
128+
"""
129+
130+
assert Enum.map(Mix.Deps.fetched, &(&1.app)) == [:deps_repo]
131+
end
132+
after
133+
Mix.Project.pop
134+
end
135+
107136
defmodule ConvergedDepsApp do
108137
def project do
109138
[
@@ -126,4 +155,30 @@ defmodule Mix.DepsTest do
126155
after
127156
Mix.Project.pop
128157
end
158+
159+
test "correctly order converged deps even with optional dependencies" do
160+
Mix.Project.push ConvergedDepsApp
161+
162+
in_fixture "deps_status", fn ->
163+
File.write!("custom/deps_repo/mix.exs", """)
164+
defmodule DepsRepo do
165+
use Mix.Project
166+
167+
def project do
168+
[
169+
app: :deps_repo,
170+
version: "0.1.0",
171+
deps: [
172+
{ :git_repo, "0.2.0", optional: true }
173+
]
174+
]
175+
end
176+
end
177+
"""
178+
179+
assert Enum.map(Mix.Deps.fetched, &(&1.app)) == [:git_repo, :deps_repo]
180+
end
181+
after
182+
Mix.Project.pop
183+
end
129184
end

lib/mix/test/mix/tasks/deps_test.exs

Lines changed: 32 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -27,17 +27,6 @@ defmodule Mix.Tasks.DepsTest do
2727
end
2828
end
2929

30-
defmodule OutOfDateDepsApp do
31-
def project do
32-
[
33-
deps: [
34-
{ :ok, "0.1.0", git: "https://github.com/elixir-lang/ok.git" },
35-
{ :uncloned, "0.1.0", git: "https://github.com/elixir-lang/uncloned.git" }
36-
]
37-
]
38-
end
39-
end
40-
4130
defmodule ReqDepsApp do
4231
def project do
4332
[
@@ -132,7 +121,7 @@ defmodule Mix.Tasks.DepsTest do
132121
Mix.Project.pop
133122
end
134123

135-
test "check list of dependencies and their status with success" do
124+
test "checks list of dependencies and their status with success" do
136125
Mix.Project.push SuccessfulDepsApp
137126

138127
in_fixture "deps_status", fn ->
@@ -142,20 +131,6 @@ defmodule Mix.Tasks.DepsTest do
142131
Mix.Project.pop
143132
end
144133

145-
test "check slist of dependencies and their status with failure" do
146-
Mix.Project.push OutOfDateDepsApp
147-
148-
in_fixture "deps_status", fn ->
149-
assert_raise Mix.Error, fn ->
150-
Mix.Tasks.Deps.Check.run []
151-
end
152-
153-
assert_received { :mix_shell, :error, ["* uncloned (https://github.com/elixir-lang/uncloned.git)"] }
154-
end
155-
after
156-
Mix.Project.pop
157-
end
158-
159134
test "checks list of dependencies and their status on failure" do
160135
Mix.Project.push DepsApp
161136

@@ -359,7 +334,7 @@ defmodule Mix.Tasks.DepsTest do
359334
end
360335

361336
receive do
362-
{ :mix_shell, :error, [" different specs were given for the :git_repo app:" <> _ = msg] } ->
337+
{ :mix_shell, :error, [" different specs were given for the git_repo app:" <> _ = msg] } ->
363338
assert msg =~ "In custom/deps_repo/mix.exs:"
364339
assert msg =~ "{:git_repo, \"0.1.0\", [git: #{inspect fixture_path("git_repo")}]}"
365340
after
@@ -406,6 +381,36 @@ defmodule Mix.Tasks.DepsTest do
406381
Mix.Project.pop
407382
end
408383

384+
test "fails on diverged dependencies even when optional" do
385+
Mix.Project.push ConvergedDepsApp
386+
387+
in_fixture "deps_status", fn ->
388+
File.write!("custom/deps_repo/mix.exs", """)
389+
defmodule DepsRepo do
390+
use Mix.Project
391+
392+
def project do
393+
[
394+
app: :deps_repo,
395+
version: "0.1.0",
396+
deps: [
397+
{ :git_repo, git: MixTest.Case.fixture_path("bad_git_repo"), branch: "omg" }
398+
]
399+
]
400+
end
401+
end
402+
"""
403+
404+
assert_raise Mix.Error, fn ->
405+
Mix.Tasks.Deps.Get.run []
406+
end
407+
408+
assert_received { :mix_shell, :error, [" the dependency git_repo in mix.exs is overriding" <> _] }
409+
end
410+
after
411+
Mix.Project.pop
412+
end
413+
409414
test "works with converged dependencies" do
410415
Mix.Project.push ConvergedDepsApp
411416

@@ -454,8 +459,6 @@ defmodule Mix.Tasks.DepsTest do
454459
assert_received { :mix_shell, :info, [^message] }
455460
message = "* Updating git_repo (#{fixture_path("git_repo")})"
456461
assert_received { :mix_shell, :info, [^message] }
457-
458-
Mix.Tasks.Deps.Check.run []
459462
end
460463
after
461464
purge [GitRepo, GitRepo.Mix]

0 commit comments

Comments
 (0)