1 /** 2 Dependency configuration/version resolution algorithm. 3 4 Copyright: © 2014 rejectedsoftware e.K. 5 License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file. 6 Authors: Sönke Ludwig 7 */ 8 module dub.dependencyresolver; 9 10 import dub.dependency; 11 import dub.internal.vibecompat.core.log; 12 13 import std.algorithm : all, canFind, filter, map, sort; 14 import std.array : appender, array, join; 15 import std.conv : to; 16 import std.exception : enforce; 17 import std.typecons : Nullable; 18 import std.string : format, indexOf, lastIndexOf; 19 20 21 class DependencyResolver(CONFIGS, CONFIG) { 22 static struct TreeNodes { 23 string pack; 24 CONFIGS configs; 25 DependencyType depType = DependencyType.required; 26 27 hash_t toHash() const nothrow @trusted { 28 size_t ret = typeid(string).getHash(&pack); 29 ret ^= typeid(CONFIGS).getHash(&configs); 30 return ret; 31 } 32 bool opEqual(in ref TreeNodes other) const { return pack == other.pack && configs == other.configs; } 33 int opCmp(in ref TreeNodes other) const { 34 if (pack != other.pack) return pack < other.pack ? -1 : 1; 35 if (configs != other.configs) return configs < other.configs ? -1 : 1; 36 return 0; 37 } 38 } 39 40 static struct TreeNode { 41 string pack; 42 CONFIG config; 43 44 hash_t toHash() const nothrow @trusted { 45 size_t ret = pack.hashOf(); 46 ret ^= typeid(CONFIG).getHash(&config); 47 return ret; 48 } 49 bool opEqual(in ref TreeNode other) const { return pack == other.pack && config == other.config; } 50 int opCmp(in ref TreeNode other) const { 51 if (pack != other.pack) return pack < other.pack ? -1 : 1; 52 if (config != other.config) return config < other.config ? -1 : 1; 53 return 0; 54 } 55 } 56 57 private static struct PackageConfigs 58 { 59 static struct Depender 60 { 61 TreeNode origin; 62 TreeNodes dependency; 63 } 64 65 // all possible configurations to test for this package 66 CONFIG[] allConfigs; 67 68 // determines whether this package has any dependencies, may be 69 // different from allConfigs.length > 0 after certain configurations 70 // have been filtered out 71 bool anyConfig; 72 73 Depender[] origins; 74 } 75 76 CONFIG[string] resolve(TreeNode root, bool throw_on_failure = true) 77 { 78 auto root_base_pack = basePackage(root.pack); 79 80 // find all possible configurations of each possible dependency 81 size_t[string] package_indices; 82 string[size_t] package_names; 83 PackageConfigs[] configs; 84 bool[string] maybe_optional_deps; 85 bool[TreeNode] visited; 86 87 void findConfigsRec(TreeNode parent, bool parent_unique) 88 { 89 if (parent in visited) return; 90 visited[parent] = true; 91 92 foreach (ch; getChildren(parent)) { 93 auto basepack = basePackage(ch.pack); 94 auto pidx = configs.length; 95 96 if (ch.depType != DependencyType.required) maybe_optional_deps[ch.pack] = true; 97 98 PackageConfigs config; 99 if (auto pi = basepack in package_indices) { 100 pidx = *pi; 101 config = configs[*pi]; 102 } else { 103 if (basepack == root_base_pack) config.allConfigs = [root.config]; 104 else config.allConfigs = getAllConfigs(basepack); 105 configs ~= config; 106 package_indices[basepack] = pidx; 107 package_names[pidx] = basepack; 108 } 109 110 foreach (c; getSpecificConfigs(basepack, ch)) 111 if (!config.allConfigs.canFind(c)) 112 config.allConfigs = c ~ config.allConfigs; 113 114 if (config.allConfigs.length > 0) 115 config.anyConfig = true; 116 117 // store package depending on this for better error messages 118 config.origins ~= PackageConfigs.Depender(parent, ch); 119 120 // eliminate configurations from which we know that they can't satisfy 121 // the uniquely defined root dependencies (==version or ~branch style dependencies) 122 if (parent_unique) config.allConfigs = config.allConfigs.filter!(c => matches(ch.configs, c)).array; 123 124 configs[pidx] = config; 125 126 foreach (v; config.allConfigs) 127 findConfigsRec(TreeNode(ch.pack, v), parent_unique && config.allConfigs.length == 1); 128 } 129 } 130 findConfigsRec(root, true); 131 132 // append an invalid configuration to denote an unchosen dependency 133 // this is used to properly support optional dependencies (when 134 // getChildren() returns no configurations for an optional dependency, 135 // but getAllConfigs() has already provided an existing list of configs) 136 foreach (i, ref cfgs; configs) 137 if (cfgs.allConfigs.length == 0 || package_names[i] in maybe_optional_deps) 138 cfgs.allConfigs = cfgs.allConfigs ~ CONFIG.invalid; 139 140 logDebug("Configurations used for dependency resolution:"); 141 foreach (n, i; package_indices) logDebug(" %s (%s%s): %s", n, i, n in maybe_optional_deps ? ", maybe optional" : ", required", configs[i]); 142 143 auto config_indices = new size_t[configs.length]; 144 config_indices[] = 0; 145 146 visited = null; 147 sizediff_t validateConfigs(TreeNode parent, ref ConflictError error) 148 { 149 import std.algorithm : max; 150 151 if (parent in visited) return -1; 152 153 visited[parent] = true; 154 sizediff_t maxcpi = -1; 155 sizediff_t parentidx = package_indices.get(basePackage(parent.pack), -1); 156 auto parentbase = basePackage(parent.pack); 157 158 // loop over all dependencies 159 foreach (ch; getChildren(parent)) { 160 auto basepack = basePackage(ch.pack); 161 assert(basepack in package_indices, format("%s not in packages %s", basepack, package_indices)); 162 163 // get the current config/version of the current dependency 164 sizediff_t childidx = package_indices[basepack]; 165 auto child = configs[childidx]; 166 167 if (child.allConfigs.length == 1 && child.allConfigs[0] == CONFIG.invalid) { 168 // ignore invalid optional dependencies 169 if (ch.depType != DependencyType.required) 170 continue; 171 172 if (parentbase == root_base_pack) { 173 import std.uni : toLower; 174 auto lp = ch.pack.toLower(); 175 if (lp != ch.pack) { 176 logError("Dependency \"%s\" of %s contains upper case letters, but must be lower case.", ch.pack, parent.pack); 177 if (getAllConfigs(lp).length) logError("Did you mean \"%s\"?", lp); 178 } 179 if (child.anyConfig) 180 throw new Exception(format("Root package %s reference %s %s cannot be satisfied.\nPackages causing the conflict:\n\t%s", 181 parent.pack, ch.pack, ch.configs, 182 child.origins.map!(a => a.origin.pack ~ " depends on " ~ a.dependency.configs.to!string).join("\n\t"))); 183 else 184 throw new Exception(format("Root package %s references unknown package %s", parent.pack, ch.pack)); 185 } 186 // choose another parent config to avoid the invalid child 187 if (parentidx > maxcpi) { 188 error = ConflictError(ConflictError.Kind.invalidDependency, parent, ch, CONFIG.invalid); 189 logDiagnostic("%s (ci=%s)", error, parentidx); 190 maxcpi = parentidx; 191 } 192 } else { 193 auto config = child.allConfigs[config_indices[childidx]]; 194 auto chnode = TreeNode(ch.pack, config); 195 196 if (config == CONFIG.invalid || !matches(ch.configs, config)) { 197 // ignore missing optional dependencies 198 if (config == CONFIG.invalid && ch.depType != DependencyType.required) 199 continue; 200 201 // if we are at the root level, we can safely skip the maxcpi computation and instead choose another childidx config 202 if (parentbase == root_base_pack) { 203 error = ConflictError(ConflictError.Kind.noRootMatch, parent, ch, config); 204 return childidx; 205 } 206 207 if (childidx > maxcpi) { 208 maxcpi = max(childidx, parentidx); 209 error = ConflictError(ConflictError.Kind.childMismatch, parent, ch, config); 210 logDebug("%s (ci=%s)", error, maxcpi); 211 } 212 213 // we know that either the child or the parent needs to be switched 214 // to another configuration, no need to continue with other children 215 if (config == CONFIG.invalid) break; 216 } 217 218 maxcpi = max(maxcpi, validateConfigs(chnode, error)); 219 } 220 } 221 return maxcpi; 222 } 223 224 Nullable!ConflictError first_error; 225 size_t loop_counter = 0; 226 227 // Leave the possibility to opt-out from the loop limit 228 import std.process : environment; 229 bool no_loop_limit = environment.get("DUB_NO_RESOLVE_LIMIT") !is null; 230 231 while (true) { 232 assert(no_loop_limit || loop_counter++ < 1_000_000, 233 "The dependency resolution process is taking too long. The" 234 ~ " dependency graph is likely hitting a pathological case in" 235 ~ " the resolution algorithm. Please file a bug report at" 236 ~ " https://github.com/dlang/dub/issues and mention the package" 237 ~ " recipe that reproduces this error."); 238 239 // check if the current combination of configurations works out 240 visited = null; 241 ConflictError error; 242 auto conflict_index = validateConfigs(root, error); 243 if (first_error.isNull) first_error = error; 244 245 // print out current iteration state 246 logDebug("Interation (ci=%s) %s", conflict_index, { 247 import std.array : join; 248 auto cs = new string[configs.length]; 249 foreach (p, i; package_indices) { 250 if (configs[i].allConfigs.length) 251 cs[i] = p~" "~configs[i].allConfigs[config_indices[i]].to!string~(i >= 0 && i >= conflict_index ? " (C)" : ""); 252 else cs[i] = p ~ " [no config]"; 253 } 254 return cs.join(", "); 255 }()); 256 257 if (conflict_index < 0) { 258 CONFIG[string] ret; 259 foreach (p, i; package_indices) 260 if (configs[i].allConfigs.length) { 261 auto cfg = configs[i].allConfigs[config_indices[i]]; 262 if (cfg != CONFIG.invalid) ret[p] = cfg; 263 } 264 logDebug("Resolved dependencies before optional-purge: %s", ret.byKey.map!(k => k~" "~ret[k].to!string)); 265 purgeOptionalDependencies(root, ret); 266 logDebug("Resolved dependencies after optional-purge: %s", ret.byKey.map!(k => k~" "~ret[k].to!string)); 267 return ret; 268 } 269 270 // find the next combination of configurations 271 foreach_reverse (pi, ref i; config_indices) { 272 if (pi > conflict_index) i = 0; 273 else if (++i >= configs[pi].allConfigs.length) i = 0; 274 else break; 275 } 276 if (config_indices.all!"a==0") { 277 if (throw_on_failure) throw new Exception(format("Could not find a valid dependency tree configuration: %s", first_error.get)); 278 else return null; 279 } 280 } 281 } 282 283 protected abstract CONFIG[] getAllConfigs(string pack); 284 protected abstract CONFIG[] getSpecificConfigs(string pack, TreeNodes nodes); 285 protected abstract TreeNodes[] getChildren(TreeNode node); 286 protected abstract bool matches(CONFIGS configs, CONFIG config); 287 288 private void purgeOptionalDependencies(TreeNode root, ref CONFIG[string] configs) 289 { 290 bool[string] required; 291 bool[string] visited; 292 293 void markRecursively(TreeNode node) 294 { 295 if (node.pack in visited) return; 296 visited[node.pack] = true; 297 required[basePackage(node.pack)] = true; 298 foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional)) 299 if (auto dp = basePackage(dep.pack) in configs) 300 markRecursively(TreeNode(dep.pack, *dp)); 301 } 302 303 // recursively mark all required dependencies of the concrete dependency tree 304 markRecursively(root); 305 306 // remove all un-marked configurations 307 foreach (p; configs.keys.dup) 308 if (p !in required) 309 configs.remove(p); 310 } 311 312 private struct ConflictError { 313 enum Kind { 314 none, 315 noRootMatch, 316 childMismatch, 317 invalidDependency 318 } 319 320 Kind kind; 321 TreeNode parent; 322 TreeNodes child; 323 CONFIG config; 324 325 string toString() 326 const { 327 final switch (kind) { 328 case Kind.none: return "no error"; 329 case Kind.noRootMatch: 330 return "No match for dependency %s %s of %s" 331 .format(child.pack, child.configs, parent.pack); 332 case Kind.childMismatch: 333 return "Dependency %s -> %s %s mismatches with selected version %s" 334 .format(parent.pack, child.pack, child.configs, config); 335 case Kind.invalidDependency: 336 return "Package %s contains invalid dependency %s (no version candidates)" 337 .format(parent.pack, child.pack); 338 } 339 } 340 } 341 } 342 343 enum DependencyType { 344 required, 345 optionalDefault, 346 optional 347 } 348 349 private string basePackage(string p) 350 { 351 auto idx = indexOf(p, ':'); 352 if (idx < 0) return p; 353 return p[0 .. idx]; 354 } 355 356 357 unittest { 358 static struct IntConfig { 359 int value; 360 alias value this; 361 enum invalid = IntConfig(-1); 362 } 363 static IntConfig ic(int v) { return IntConfig(v); } 364 static struct IntConfigs { 365 IntConfig[] configs; 366 alias configs this; 367 } 368 static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); } 369 370 static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) { 371 private TreeNodes[][string] m_children; 372 this(TreeNodes[][string] children) { m_children = children; } 373 protected override IntConfig[] getAllConfigs(string pack) { 374 auto ret = appender!(IntConfig[]); 375 foreach (p; m_children.byKey) { 376 if (p.length <= pack.length+1) continue; 377 if (p[0 .. pack.length] != pack || p[pack.length] != ':') continue; 378 auto didx = p.lastIndexOf(':'); 379 ret ~= ic(p[didx+1 .. $].to!uint); 380 } 381 ret.data.sort!"a>b"(); 382 return ret.data; 383 } 384 protected override IntConfig[] getSpecificConfigs(string pack, TreeNodes nodes) { return null; } 385 protected override TreeNodes[] getChildren(TreeNode node) { return m_children.get(node.pack ~ ":" ~ node.config.to!string(), null); } 386 protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); } 387 } 388 389 // properly back up if conflicts are detected along the way (d:2 vs d:1) 390 with (TestResolver) { 391 auto res = new TestResolver([ 392 "a:0": [TreeNodes("b", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)])), TreeNodes("e", ics([ic(2), ic(1)]))], 393 "b:1": [TreeNodes("c", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)]))], 394 "b:2": [TreeNodes("c", ics([ic(3), ic(2)])), TreeNodes("d", ics([ic(2), ic(1)]))], 395 "c:1": [], "c:2": [], "c:3": [], 396 "d:1": [], "d:2": [], 397 "e:1": [], "e:2": [], 398 ]); 399 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2), "c":ic(3), "d":ic(1), "e":ic(2)], format("%s", res.resolve(TreeNode("a", ic(0))))); 400 } 401 402 // handle cyclic dependencies gracefully 403 with (TestResolver) { 404 auto res = new TestResolver([ 405 "a:0": [TreeNodes("b", ics([ic(1)]))], 406 "b:1": [TreeNodes("b", ics([ic(1)]))] 407 ]); 408 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)]); 409 } 410 411 // don't choose optional dependencies by default 412 with (TestResolver) { 413 auto res = new TestResolver([ 414 "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)], 415 "b:1": [] 416 ]); 417 assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0))))); 418 } 419 420 // choose default optional dependencies by default 421 with (TestResolver) { 422 auto res = new TestResolver([ 423 "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optionalDefault)], 424 "b:1": [] 425 ]); 426 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)], to!string(res.resolve(TreeNode("a", ic(0))))); 427 } 428 429 // choose optional dependency if non-optional within the dependency tree 430 with (TestResolver) { 431 auto res = new TestResolver([ 432 "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional), TreeNodes("c", ics([ic(1)]))], 433 "b:1": [], 434 "c:1": [TreeNodes("b", ics([ic(1)]))] 435 ]); 436 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1), "c":ic(1)], to!string(res.resolve(TreeNode("a", ic(0))))); 437 } 438 439 // don't choose optional dependency if non-optional outside of final dependency tree 440 with (TestResolver) { 441 auto res = new TestResolver([ 442 "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)], 443 "b:1": [], 444 "preset:0": [TreeNodes("b", ics([ic(1)]))] 445 ]); 446 assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0))))); 447 } 448 449 // don't choose optional dependency if non-optional in a non-selected version 450 with (TestResolver) { 451 auto res = new TestResolver([ 452 "a:0": [TreeNodes("b", ics([ic(1), ic(2)]))], 453 "b:1": [TreeNodes("c", ics([ic(1)]))], 454 "b:2": [TreeNodes("c", ics([ic(1)]), DependencyType.optional)], 455 "c:1": [] 456 ]); 457 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0))))); 458 } 459 460 // make sure non-satisfyable dependencies are not a problem, even if non-optional in some dependencies 461 with (TestResolver) { 462 auto res = new TestResolver([ 463 "a:0": [TreeNodes("b", ics([ic(1), ic(2)]))], 464 "b:1": [TreeNodes("c", ics([ic(2)]))], 465 "b:2": [TreeNodes("c", ics([ic(2)]), DependencyType.optional)], 466 "c:1": [] 467 ]); 468 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0))))); 469 } 470 }