Skip to content

Commit bc7af21

Browse files
authored
Allow overriding specific dependencies in override (#15193)
Closes #14082. Closes #14156.
1 parent 7945446 commit bc7af21

4 files changed

Lines changed: 229 additions & 62 deletions

File tree

lib/mix/lib/mix/dep.ex

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -342,12 +342,14 @@ defmodule Mix.Dep do
342342
"the dependency compile environment is outdated, please run \"#{mix_env_var()}mix deps.compile\""
343343
end
344344

345-
def format_status(%Mix.Dep{app: app, status: {:divergedreq, vsn, other}} = dep) do
345+
def format_status(%Mix.Dep{app: app, status: {:divergedreq, vsn, parent, other}} = dep) do
346+
override = if parent, do: [parent], else: true
347+
346348
"the dependency #{app} #{vsn}\n" <>
347349
dep_status(dep) <>
348350
"\n does not match the requirement specified\n" <>
349351
dep_status(other) <>
350-
"\n Ensure they match or specify one of the above in your deps and set \"override: true\""
352+
"\n Ensure they match or specify one of the above in your deps and set \"override: #{inspect(override)}\""
351353
end
352354

353355
def format_status(%Mix.Dep{app: app, status: {:divergedonly, other}} = dep) do
@@ -378,14 +380,16 @@ defmodule Mix.Dep do
378380
dep_status(other) <> "\n #{recommendation}"
379381
end
380382

381-
def format_status(%Mix.Dep{app: app, status: {:diverged, other}} = dep) do
383+
def format_status(%Mix.Dep{app: app, status: {:diverged, parent, other}} = dep) do
382384
"different specs were given for the #{app} app:\n" <>
383-
"#{dep_status(dep)}#{dep_status(other)}\n " <> override_diverge_recommendation(dep, other)
385+
"#{dep_status(dep)}#{dep_status(other)}\n " <>
386+
override_diverge_recommendation(dep, parent, other)
384387
end
385388

386-
def format_status(%Mix.Dep{app: app, status: {:overridden, other}} = dep) do
389+
def format_status(%Mix.Dep{app: app, status: {:overridden, parent, other}} = dep) do
387390
"the dependency #{app} in #{Path.relative_to_cwd(dep.from)} is overriding a child dependency:\n" <>
388-
"#{dep_status(dep)}#{dep_status(other)}\n " <> override_diverge_recommendation(dep, other)
391+
"#{dep_status(dep)}#{dep_status(other)}\n " <>
392+
override_diverge_recommendation(dep, parent, other)
389393
end
390394

391395
def format_status(%Mix.Dep{status: {:unavailable, _}, scm: scm}) do
@@ -404,11 +408,14 @@ defmodule Mix.Dep do
404408
"the dependency was built with another SCM, run \"#{mix_env_var()}mix deps.compile\""
405409
end
406410

407-
defp override_diverge_recommendation(dep, other) do
411+
defp override_diverge_recommendation(dep, parent, other) do
408412
if dep.opts[:from_umbrella] || other.opts[:from_umbrella] do
409413
"Please remove the conflicting options from your definition"
410414
else
411-
"Ensure they match or specify one of the above in your deps and set \"override: true\""
415+
override = if parent, do: [parent], else: true
416+
417+
"Ensure they match or specify one of the above in your deps " <>
418+
"and set \"override: #{inspect(override)}\""
412419
end
413420
end
414421

@@ -499,9 +506,9 @@ defmodule Mix.Dep do
499506
@doc """
500507
Checks if a dependency has diverged.
501508
"""
502-
def diverged?(%Mix.Dep{status: {:overridden, _}}), do: true
503-
def diverged?(%Mix.Dep{status: {:diverged, _}}), do: true
504-
def diverged?(%Mix.Dep{status: {:divergedreq, _, _}}), do: true
509+
def diverged?(%Mix.Dep{status: {:overridden, _, _}}), do: true
510+
def diverged?(%Mix.Dep{status: {:diverged, _, _}}), do: true
511+
def diverged?(%Mix.Dep{status: {:divergedreq, _, _, _}}), do: true
505512
def diverged?(%Mix.Dep{status: {:divergedonly, _}}), do: true
506513
def diverged?(%Mix.Dep{status: {:divergedtargets, _}}), do: true
507514
def diverged?(%Mix.Dep{}), do: false

lib/mix/lib/mix/dep/converger.ex

Lines changed: 100 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,31 @@ defmodule Mix.Dep.Converger do
7373
def converge(acc, lock, opts, callback) do
7474
{deps, acc, lock} = all(acc, lock, opts, callback)
7575
if remote = Mix.RemoteConverger.get(), do: remote.post_converge()
76-
{topological_sort(deps), acc, lock}
76+
sorted_deps = topological_sort(deps)
77+
warn_on_unneeded_deps_overrides(sorted_deps)
78+
{sorted_deps, acc, lock}
79+
end
80+
81+
defp warn_on_unneeded_deps_overrides([%{app: app, opts: opts} = dep | rest]) do
82+
override = Keyword.get(opts, :override, false)
83+
84+
if is_list(override) do
85+
Enum.each(override, fn overridden_app ->
86+
# Since we used a topological sort, if someone depends on dep, it will come after dep
87+
with %{deps: deps} <- Enum.find(rest, &(&1.app == overridden_app)),
88+
true <- Enum.any?(deps, &(&1.app == app)) do
89+
:ok
90+
else
91+
_ -> warn_uneeded_override(dep, overridden_app)
92+
end
93+
end)
94+
end
95+
96+
warn_on_unneeded_deps_overrides(rest)
97+
end
98+
99+
defp warn_on_unneeded_deps_overrides([]) do
100+
:ok
77101
end
78102

79103
defp all(acc, lock, opts, callback) do
@@ -152,11 +176,10 @@ defmodule Mix.Dep.Converger do
152176

153177
defp init_all(main, apps, rest, lock, callback, locked?, env_target, cache) do
154178
state = %{locked?: locked?, env_target: env_target, cache: cache, callback: callback}
155-
{deps, _kept, _optional, rest, lock} = all(main, [], [], [], apps, [], rest, lock, state)
156-
deps = Enum.reverse(deps)
179+
{deps, _kept, _optional, rest, lock} = all(main, nil, [], [], [], apps, [], rest, lock, state)
157180
# When traversing dependencies, we keep skipped ones to
158181
# find conflicts. We remove them now after traversal.
159-
{deps, _} = Mix.Dep.Loader.split_by_env_and_target(deps, env_target)
182+
{deps, _} = deps |> Enum.reverse() |> Mix.Dep.Loader.split_by_env_and_target(env_target)
160183
{deps, rest, lock}
161184
end
162185

@@ -199,19 +222,19 @@ defmodule Mix.Dep.Converger do
199222
# Now, since "d" was specified in a parent project, no
200223
# exception is going to be raised since d is considered
201224
# to be the authoritative source.
202-
defp all([dep | t], acc, kept, upper, breadths, optional, rest, lock, state) do
203-
case match_deps(acc, upper, dep, state.env_target) do
225+
defp all([dep | t], parent, acc, kept, upper, breadths, optional, rest, lock, state) do
226+
case match_deps(parent, acc, upper, dep, state.env_target) do
204227
{:replace, dep, acc} ->
205-
all([dep | t], acc, kept, upper, breadths, optional, rest, lock, state)
228+
all([dep | t], parent, acc, kept, upper, breadths, optional, rest, lock, state)
206229

207230
{:match, acc} ->
208-
all(t, acc, kept, upper, breadths, optional, rest, lock, state)
231+
all(t, parent, acc, kept, upper, breadths, optional, rest, lock, state)
209232

210233
:skip ->
211234
# We still keep skipped dependencies around to detect conflicts.
212235
# They must be rejected after every all iteration but they are not
213236
# included in the list of kept dependencies.
214-
all(t, [dep | acc], kept, upper, breadths, optional, rest, lock, state)
237+
all(t, parent, [dep | acc], kept, upper, breadths, optional, rest, lock, state)
215238

216239
:nomatch ->
217240
{%{app: app, deps: deps, opts: opts} = dep, rest, lock} =
@@ -234,19 +257,21 @@ defmodule Mix.Dep.Converger do
234257
# no longer a dependency. Add it back for traversal.
235258
{no_longer_optional, optional} = Enum.split_with(optional, &(&1.app == app))
236259
t = no_longer_optional ++ t
260+
acc = [dep | acc]
237261

238262
{acc, kept, optional, rest, lock} =
239-
all(t, [dep | acc], [dep.app | kept], upper, breadths, optional, rest, lock, state)
263+
all(t, parent, acc, [dep.app | kept], upper, breadths, optional, rest, lock, state)
240264

241265
# Now traverse all parent dependencies and see if we have any optional dependency.
242266
{discarded, deps} = split_non_fulfilled_optional(deps, kept, opts[:from_umbrella])
243267

244268
new_breadths = Enum.map(deps, & &1.app) ++ breadths
245-
all(deps, acc, kept, breadths, new_breadths, discarded ++ optional, rest, lock, state)
269+
new_optional = discarded ++ optional
270+
all(deps, app, acc, kept, breadths, new_breadths, new_optional, rest, lock, state)
246271
end
247272
end
248273

249-
defp all([], acc, kept, _upper, _current, optional, rest, lock, _state) do
274+
defp all([], _parent, acc, kept, _upper, _current, optional, rest, lock, _state) do
250275
{acc, kept, optional, rest, lock}
251276
end
252277

@@ -264,7 +289,7 @@ defmodule Mix.Dep.Converger do
264289
# diverges is in the upper breadth, in those cases we
265290
# also check for the override option and mark the dependency
266291
# as overridden instead of diverged.
267-
defp match_deps(list, upper_breadths, %Mix.Dep{app: app} = dep, env_target) do
292+
defp match_deps(parent, list, upper_breadths, %Mix.Dep{app: app} = dep, env_target) do
268293
case Enum.split_while(list, &(&1.app != app)) do
269294
{_, []} ->
270295
if Mix.Dep.Loader.skip?(dep, env_target) do
@@ -283,51 +308,84 @@ defmodule Mix.Dep.Converger do
283308
)
284309
end
285310

311+
override = Keyword.get(other_opts, :override, false)
312+
286313
cond do
287-
in_upper? && other_opts[:override] ->
314+
in_upper? && override == true ->
288315
{:match, list}
289316

290317
not converge?(other, dep) ->
291-
tag = if in_upper?, do: :overridden, else: :diverged
292-
other = %{other | status: {tag, dep}}
293-
{:match, pre ++ [other | pos]}
318+
if parent_overriden?(override, parent) do
319+
{:match, list}
320+
else
321+
tag = if in_upper?, do: :overridden, else: :diverged
322+
other = %{other | status: {tag, parent, dep}}
323+
{:match, pre ++ [other | pos]}
324+
end
294325

295326
vsn = req_mismatch(other, dep) ->
296-
other = %{other | status: {:divergedreq, vsn, dep}}
297-
{:match, pre ++ [other | pos]}
327+
if parent_overriden?(override, parent) do
328+
{:match, list}
329+
else
330+
other = %{other | status: {:divergedreq, vsn, parent, dep}}
331+
{:match, pre ++ [other | pos]}
332+
end
298333

299334
not in_upper? and Mix.Dep.Loader.skip?(other, env_target) and
300335
not Mix.Dep.Loader.skip?(dep, env_target) ->
301-
dep =
302-
dep
303-
|> with_matching_only_and_targets(other, in_upper?)
304-
|> merge_manager(other, in_upper?)
305-
306-
{:replace, dep, pre ++ pos}
336+
dep
337+
|> merge_manager(other, in_upper?)
338+
|> with_matching_only_and_targets(other, in_upper?, override, parent, list, fn dep ->
339+
{:replace, dep, pre ++ pos}
340+
end)
307341

308342
true ->
309-
other =
310-
other
311-
|> with_matching_only_and_targets(dep, in_upper?)
312-
|> merge_manager(dep, in_upper?)
313-
314-
{:match, pre ++ [other | pos]}
343+
other
344+
|> merge_manager(dep, in_upper?)
345+
|> with_matching_only_and_targets(dep, in_upper?, override, parent, list, fn other ->
346+
{:match, pre ++ [other | pos]}
347+
end)
315348
end
316349
end
317350
end
318351

319-
defp with_matching_only_and_targets(other, dep, in_upper?) do
352+
defp parent_overriden?(list, app) when is_list(list), do: app in list
353+
defp parent_overriden?(_list, _app), do: false
354+
355+
defp with_matching_only_and_targets(other, dep, in_upper?, override, parent, list, callback) do
320356
%{opts: opts} = dep
321357

322358
if opts[:optional] do
323-
other
359+
maybe_warn_uneeded_override(dep, override, parent)
360+
callback.(other)
324361
else
325-
other
326-
|> with_matching(:only, dep, opts, in_upper?)
327-
|> with_matching(:targets, dep, opts, in_upper?)
362+
with {:ok, other} <- with_matching(other, :only, dep, opts, in_upper?),
363+
{:ok, other} <- with_matching(other, :targets, dep, opts, in_upper?) do
364+
maybe_warn_uneeded_override(dep, override, parent)
365+
callback.(other)
366+
else
367+
{:error, other} ->
368+
if parent_overriden?(override, parent) do
369+
{:match, list}
370+
else
371+
callback.(other)
372+
end
373+
end
374+
end
375+
end
376+
377+
defp maybe_warn_uneeded_override(dep, override, parent) do
378+
if parent_overriden?(override, parent) do
379+
warn_uneeded_override(dep, parent)
328380
end
329381
end
330382

383+
defp warn_uneeded_override(dep, parent) do
384+
Mix.shell().error(
385+
"Dependency #{Mix.Dep.format_dep(dep)} no longer requires :override on #{inspect(parent)}"
386+
)
387+
end
388+
331389
# When in_upper is true
332390
#
333391
# When a parent dependency specifies :only/:targets that is a
@@ -345,16 +403,16 @@ defmodule Mix.Dep.Converger do
345403
case Keyword.fetch(opts, key) do
346404
{:ok, value} ->
347405
case List.wrap(value) -- List.wrap(other_value) do
348-
[] -> other
349-
_ -> %{other | status: {:"diverged#{key}", dep}}
406+
[] -> {:ok, other}
407+
_ -> {:error, %{other | status: {:"diverged#{key}", dep}}}
350408
end
351409

352410
:error ->
353-
%{other | status: {:"diverged#{key}", dep}}
411+
{:error, %{other | status: {:"diverged#{key}", dep}}}
354412
end
355413

356414
:error ->
357-
other
415+
{:ok, other}
358416
end
359417
end
360418

@@ -369,9 +427,9 @@ defmodule Mix.Dep.Converger do
369427
value = Keyword.get(opts, key)
370428

371429
if other_value && value do
372-
put_in(other.opts[key], Enum.uniq(List.wrap(other_value) ++ List.wrap(value)))
430+
{:ok, put_in(other.opts[key], Enum.uniq(List.wrap(other_value) ++ List.wrap(value)))}
373431
else
374-
%{other | opts: Keyword.delete(other_opts, key)}
432+
{:ok, %{other | opts: Keyword.delete(other_opts, key)}}
375433
end
376434
end
377435

lib/mix/lib/mix/tasks/deps.ex

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,7 +99,11 @@ defmodule Mix.Tasks.Deps do
9999
targets (like `[:host, :rpi3]`)
100100
101101
* `:override` - if set to `true` the dependency will override any other
102-
definitions of itself by other dependencies
102+
definitions of itself by other dependencies. From Elixir v1.20.0,
103+
this option can also be a list of dependency names, which allows you to
104+
override the definition of specific dependencies. If a dependency is not
105+
included in the list and an override is required, it will still fail.
106+
If a dependency is in the list and no longer necessary, then an error is emitted
103107
104108
* `:manager` - Mix can also compile Rebar3 and makefile projects
105109
and can fetch sub dependencies of Rebar3 projects. Mix will

0 commit comments

Comments
 (0)