Skip to content

Commit 9c791dd

Browse files
committed
build_support: reject Mix-managed deps/ paths as sibling checkouts
1 parent b9d90f7 commit 9c791dd

1 file changed

Lines changed: 49 additions & 3 deletions

File tree

build_support/dependency_sources.exs

Lines changed: 49 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
defmodule DependencySources do
22
@moduledoc false
33

4-
@helper_version 1
4+
@helper_version 2
55
@source_keys [:path, :github, :hex]
66
@github_option_keys [:branch, :ref, :tag, :subdir]
77

@@ -129,16 +129,62 @@ defmodule DependencySources do
129129
false
130130

131131
path when is_binary(path) ->
132-
File.exists?(Path.expand(path, repo_root))
132+
usable_sibling_path?(path, repo_root)
133133

134134
paths when is_list(paths) ->
135-
Enum.any?(paths, &File.exists?(Path.expand(&1, repo_root)))
135+
Enum.any?(paths, &usable_sibling_path?(&1, repo_root))
136136

137137
_other ->
138138
false
139139
end
140140
end
141141

142+
# A configured `:path` candidate only counts as a usable sibling
143+
# checkout when (1) it exists on disk and (2) it does not resolve to a
144+
# Mix-managed `deps/` directory.
145+
#
146+
# Without (2), running `mix deps.get` from a fresh clone could
147+
# materialize multiple sibling deps under the parent project's
148+
# `deps/`, and this helper -- invoked from within one of those
149+
# child deps with `repo_root = <parent>/deps/<child>` -- would then
150+
# mistake another Mix-fetched dep for a developer sibling checkout
151+
# and pick `:path`. That produces a divergent source vs. however a
152+
# peer dep already declared the same app, and Mix refuses to
153+
# resolve with the "overriding a child dependency" error.
154+
defp usable_sibling_path?(path, repo_root) do
155+
abs = Path.expand(path, repo_root)
156+
File.exists?(abs) and not under_mix_deps_dir?(repo_root, abs)
157+
end
158+
159+
# When `repo_root` itself sits under a path component named `deps`,
160+
# `mix_deps_ancestor/1` returns the absolute path to that ancestor
161+
# (the parent project's `deps/` directory). A candidate that resolves
162+
# to anything under that same ancestor is therefore another
163+
# Mix-fetched sibling, not a developer workspace checkout.
164+
defp under_mix_deps_dir?(repo_root, abs) do
165+
case mix_deps_ancestor(repo_root) do
166+
nil -> false
167+
deps_dir -> String.starts_with?(abs <> "/", deps_dir <> "/")
168+
end
169+
end
170+
171+
defp mix_deps_ancestor(repo_root) do
172+
segments = repo_root |> Path.expand() |> Path.split()
173+
174+
segments
175+
|> Enum.reverse()
176+
|> Enum.with_index()
177+
|> Enum.find(fn {seg, _idx} -> seg == "deps" end)
178+
|> case do
179+
nil ->
180+
nil
181+
182+
{"deps", reverse_index} ->
183+
forward_index = length(segments) - reverse_index
184+
segments |> Enum.take(forward_index) |> Path.join()
185+
end
186+
end
187+
142188
defp configured?(config, source),
143189
do: not is_nil(config[source] || config[Atom.to_string(source)])
144190

0 commit comments

Comments
 (0)