1defmodule Hex.Resolver.Backtracks do 2 @moduledoc false 3 4 require Record 5 6 Record.defrecordp(:parent, [:name, :repo, :version, :requirement, :repo_requirement]) 7 8 @ets :hex_backtracks 9 10 def start() do 11 :ets.new(@ets, []) 12 end 13 14 def stop(ets) do 15 :ets.delete(ets) 16 end 17 18 def add(ets, repo, name, version, parents) do 19 parents = order_parents(parents) 20 21 case :ets.lookup(ets, {repo, name, parents}) do 22 [{_, versions}] -> 23 unless version in versions do 24 :ets.insert(ets, {{repo, name, parents}, [version | versions]}) 25 end 26 27 [] -> 28 :ets.insert(ets, {{repo, name, parents}, [version]}) 29 end 30 end 31 32 def collect(ets) do 33 :ets.tab2list(ets) 34 |> normalize() 35 |> merge_versions() 36 |> expand_merged() 37 |> sort_backtracks() 38 end 39 40 defp order_parents(parents) do 41 parents 42 |> Enum.uniq() 43 |> Enum.sort() 44 end 45 46 defp normalize(backtracks) do 47 # Expand versions into individual backtracks again 48 Enum.flat_map(backtracks, fn 49 {{repo, name, parents}, []} -> 50 [{{repo, name}, nil, parents}] 51 52 {{repo, name, parents}, versions} -> 53 Enum.map(versions, &{{repo, name}, &1, parents}) 54 end) 55 end 56 57 defp sort_backtracks(backtracks) do 58 Enum.map(backtracks, fn {repo, name, versions, parents} -> 59 # If nil is in versions it means any version of the dependency 60 # will fail with the given parents. nil is from when the resolver 61 # failed to activate ANY version of the dependency. 62 versions = if nil in versions, do: [], else: Enum.sort(versions, &version_cmp/2) 63 parents = sort_parents(parents) 64 {name, versions, repo, parents} 65 end) 66 |> Enum.sort() 67 end 68 69 defp sort_parents(parents) do 70 Enum.map(parents, fn parent(version: versions) = parent -> 71 versions = 72 versions 73 |> List.wrap() 74 |> Enum.reject(&is_nil/1) 75 |> Enum.uniq() 76 |> Enum.sort(&version_cmp/2) 77 78 parent(parent, version: versions) 79 end) 80 |> Enum.sort() 81 end 82 83 defp expand_merged(merged) do 84 Enum.flat_map(merged, fn {{repo, name}, list} -> 85 Enum.map(list, fn {versions, parents} -> 86 {repo, name, versions, parents} 87 end) 88 end) 89 end 90 91 defp merge_versions(backtracks) do 92 # For each package try to merge the similar backtrack messages 93 # from conflicts in the resolver. 94 # Complete duplicates are skipped when inserting into the ets table. 95 # If two backtracks have similar parents (only differing in the 96 # parents versions) we merge them. Finally if a package has the exact 97 # same conflicts for two different versions we also merge those. 98 Enum.reduce(backtracks, %{}, fn {name, version, parents}, map -> 99 parents = Enum.sort(parents) 100 101 case Map.fetch(map, name) do 102 {:ok, list} -> 103 case merge_package_version(list, version, parents) do 104 {:ok, list} -> 105 Map.put(map, name, list) 106 107 :error -> 108 Map.put(map, name, [{[version], parents} | list]) 109 end 110 111 :error -> 112 Map.put(map, name, [{[version], parents}]) 113 end 114 end) 115 end 116 117 # For a given a package try to merge this version and parents 118 # into the existing ones 119 defp merge_package_version(list, version, parents) do 120 update_one(list, [], fn {versions, existing_parents} -> 121 case merge_parents(existing_parents, parents, []) do 122 {:ok, parents} -> 123 {:ok, {insert_new(versions, version), parents}} 124 125 :error -> 126 :error 127 end 128 end) 129 end 130 131 # If two lists of parents match (in package name and requirement) 132 # merge them and append the versions 133 defp merge_parents([], [], acc), do: {:ok, Enum.reverse(acc)} 134 135 defp merge_parents( 136 [parent(repo: repo, name: name, requirement: req, version: versions) | existing], 137 [parent(repo: repo, name: name, requirement: req, version: version) | new], 138 acc 139 ) do 140 versions = [version | List.wrap(versions)] 141 parent = parent(repo: repo, name: name, requirement: req, version: versions) 142 merge_parents(existing, new, [parent | acc]) 143 end 144 145 defp merge_parents(_, _, _), do: :error 146 147 # Update a single element in a list 148 defp update_one([], _acc, _fun), do: :error 149 150 defp update_one([head | tail], acc, fun) do 151 case fun.(head) do 152 {:ok, new} -> {:ok, [new | acc] ++ tail} 153 :error -> update_one(tail, [head | acc], fun) 154 end 155 end 156 157 defp insert_new(list, elem) do 158 if elem in list, do: list, else: [elem | list] 159 end 160 161 def unzip(list), do: do_unzip(list, []) 162 163 def do_unzip([head | tail], acc) do 164 acc = inner_unzip(head, acc) 165 do_unzip(tail, acc) 166 end 167 168 def do_unzip([], acc), do: acc 169 170 defp inner_unzip([x | xs], [y | ys]), do: [[x | y] | inner_unzip(xs, ys)] 171 defp inner_unzip([x | xs], []), do: [[x] | inner_unzip(xs, [])] 172 defp inner_unzip([], []), do: [] 173 174 defp version_cmp(a, b), do: Hex.Version.compare(a, b) != :gt 175 176 def message({name, versions, repo, parents}, registry) do 177 if parent_messages = parent_messages(registry, parents, repo, name, versions) do 178 Hex.Shell.format([ 179 [:underline, "Failed to use \"", name, "\""], 180 versions_message(registry, repo, name, versions), 181 [" because", single_parent_message(parents), :reset, "\n"], 182 parent_messages 183 ]) 184 |> IO.iodata_to_binary() 185 end 186 end 187 188 defp single_parent_message(parents) when length(parents) < 2 do 189 " there are no packages that matches the requirement" 190 end 191 192 defp single_parent_message(_parents) do 193 "" 194 end 195 196 defp parent_messages(registry, parents, child_repo, child, child_versions) do 197 {mix, parents} = partition_mix(parents) 198 199 parent_reasons = 200 Enum.map(parents, &{&1, parent_reason(registry, &1, child_repo, child, child_versions)}) 201 202 mix_reason = {mix, parent_reason(registry, mix, child_repo, child, child_versions)} 203 messages = Enum.map(parent_reasons, &parent_message(&1, registry)) 204 all_reasons = [mix_reason | parent_reasons] 205 206 unless all_green?(all_reasons) do 207 messages = 208 if mix do 209 messages ++ [parent_message(mix_reason, registry)] 210 else 211 messages 212 end 213 214 Enum.map(messages, &[" ", &1, "\n"]) 215 end 216 end 217 218 defp parent_message( 219 {parent(name: "mix.lock", version: [], requirement: req), {color, pre_failed?}}, 220 _registry 221 ) do 222 [ 223 [:bright, "mix.lock", :reset, " specifies ", color, requirement(req), :reset], 224 pre_message(pre_failed?) 225 ] 226 end 227 228 defp parent_message( 229 {parent(name: "mix.exs", version: [], requirement: req), {color, pre_failed?}}, 230 _registry 231 ) do 232 [ 233 [:bright, "mix.exs", :reset, " specifies ", color, requirement(req), :reset], 234 pre_message(pre_failed?) 235 ] 236 end 237 238 defp parent_message( 239 {parent(repo: repo, name: name, version: versions, requirement: req), 240 {color, pre_failed?}}, 241 registry 242 ) do 243 [ 244 [:bright, name, versions_message(registry, repo, name, versions), :reset], 245 [" requires ", color, requirement(req), :reset], 246 pre_message(pre_failed?) 247 ] 248 end 249 250 defp parent_message({nil, nil}, _registry), do: [] 251 252 defp pre_message(true), do: " *" 253 defp pre_message(false), do: "" 254 255 defp all_green?(reasons) do 256 Enum.all?(reasons, fn 257 {nil, nil} -> false 258 {_parent, {color, _pre}} -> color == :green 259 end) 260 end 261 262 defp partition_mix(parents) do 263 map = 264 Enum.into(parents, %{}, fn parent -> 265 {parent(parent, :name), parent} 266 end) 267 268 parents = 269 Enum.reject(parents, fn parent(name: name) -> 270 name in ~w(mix.exs mix.lock) 271 end) 272 273 {map["mix.lock"] || map["mix.exs"], parents} 274 end 275 276 defp parent_reason(_registry, nil, _child_repo, _child, _versions), do: nil 277 278 defp parent_reason(registry, parent, child_repo, child, []) do 279 versions = registry.versions(child_repo, child) 280 parent_reason(registry, parent, child_repo, child, versions) 281 end 282 283 defp parent_reason(_registry, parent(requirement: req), _child_repo, _child, versions) do 284 num_failures = Enum.count(versions, &(not version_match?(&1, req, []))) 285 num_versions = length(versions) 286 pre_failures = Enum.count(versions, &(not version_match?(&1, req, allow_pre: true))) 287 pre_failed? = pre_failures < num_failures 288 289 {parent_color(num_versions, num_failures), pre_failed?} 290 end 291 292 defp parent_color(_versions, 0), do: :green 293 defp parent_color(versions, versions), do: :red 294 defp parent_color(versions, failures) when failures < versions, do: :yellow 295 296 defp version_match?(_version, nil, _opts) do 297 true 298 end 299 300 defp version_match?(version, req, opts) do 301 Hex.Version.match?(version, req, opts) 302 end 303 304 # Try converting lists of versions to a version range if the list is 305 # a complete range of versions of the package. 306 # For example ["0.1.0", "0.1.1", "0.2.0", "0.3.0"] can be converted 307 # to "to 0.1.0 from 0.3.0" if there are no other releases of the package 308 # between 0.1.0 and 0.3.0. 309 defp versions_message(registry, repo, package, versions) do 310 case {versions, merge_versions?(registry, repo, package, versions)} do 311 {[], _} -> 312 "" 313 314 {[x], _} -> 315 [" (version ", to_string(x), ")"] 316 317 {[x, y], _} -> 318 [" (versions ", to_string(x), " and ", to_string(y), ")"] 319 320 {_, true} when length(versions) > 2 -> 321 first = versions |> List.first() |> to_string() 322 last = versions |> List.last() |> to_string() 323 [" (versions ", first, " to ", last, ")"] 324 325 _ -> 326 versions = Enum.map(versions, &to_string/1) 327 [" (versions ", Enum.join(versions, ", "), ")"] 328 end 329 end 330 331 defp merge_versions?(_registry, _repo, _package, []) do 332 false 333 end 334 335 defp merge_versions?(registry, repo, package, versions) do 336 # Filter pre-releases because they were likely not considered during resolution 337 # This can give false-positive merged version ranges, but it's likely fine 338 all_versions = filter_pre_releases(registry.versions(repo, package)) 339 versions = filter_pre_releases(versions) 340 sub_range?(all_versions, versions) 341 end 342 343 defp filter_pre_releases(versions) do 344 Enum.filter(versions, &String.contains?(&1, "-")) 345 end 346 347 # Assumes lists are sorted 348 defp sub_range?(outer, inner), do: do_sub_range?(outer, inner, false) 349 350 defp do_sub_range?(_, [], _), do: true 351 defp do_sub_range?([], _, _), do: false 352 defp do_sub_range?([x | outer], [x | inner], _), do: do_sub_range?(outer, inner, true) 353 defp do_sub_range?(_, _, true), do: false 354 defp do_sub_range?([_ | outer], inner, false), do: do_sub_range?(outer, inner, false) 355 356 defp requirement(nil), do: ">= 0.0.0" 357 defp requirement(req), do: req 358end 359