|
1 | 1 | defmodule DependencySources do |
2 | 2 | @moduledoc false |
3 | 3 |
|
4 | | - @helper_version 1 |
| 4 | + @helper_version 2 |
5 | 5 | @source_keys [:path, :github, :hex] |
6 | 6 | @github_option_keys [:branch, :ref, :tag, :subdir] |
7 | 7 |
|
@@ -129,16 +129,62 @@ defmodule DependencySources do |
129 | 129 | false |
130 | 130 |
|
131 | 131 | path when is_binary(path) -> |
132 | | - File.exists?(Path.expand(path, repo_root)) |
| 132 | + usable_sibling_path?(path, repo_root) |
133 | 133 |
|
134 | 134 | 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)) |
136 | 136 |
|
137 | 137 | _other -> |
138 | 138 | false |
139 | 139 | end |
140 | 140 | end |
141 | 141 |
|
| 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 | + |
142 | 188 | defp configured?(config, source), |
143 | 189 | do: not is_nil(config[source] || config[Atom.to_string(source)]) |
144 | 190 |
|
|
0 commit comments