1 /** 2 Dependency configuration/version resolution algorithm. 3 4 Copyright: © 2014-2018 Sönke Ludwig 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.logging; 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.string : format, lastIndexOf; 18 19 20 /** Resolves dependency graph with multiple configurations per package. 21 22 The term "configuration" can mean any kind of alternative dependency 23 configuration of a package. In particular, it can mean different 24 versions of a package. 25 26 `CONFIG` is an abstract type denoting a single configuration of a certain 27 package, whereas `CONFIGS` denotes a set of configurations. The 28 representation of both can be freely chosen, so that `CONFIGS` for example 29 can be defined in terms of a version range. 30 */ 31 class DependencyResolver(CONFIGS, CONFIG) { 32 /// Maximum number of loop rounds to do 33 protected ulong loop_limit; 34 35 /** 36 * Construct an instance of this class 37 * 38 * Params: 39 * limit = Maximum number of loop rounds to do 40 */ 41 public this (ulong limit) inout scope @safe pure nothrow @nogc 42 { 43 this.loop_limit = limit; 44 } 45 46 /// Compatibility overload 47 deprecated("Use the overload that accepts a `ulong limit` argument") 48 public this () scope @safe 49 { 50 // Leave the possibility to opt-out from the loop limit 51 import std.process : environment; 52 if (environment.get("DUB_NO_RESOLVE_LIMIT") !is null) 53 this(ulong.max); 54 else 55 this(1_000_000); 56 } 57 58 /** Encapsulates a list of outgoing edges in the dependency graph. 59 60 A value of this type represents a single dependency with multiple 61 possible configurations for the target package. 62 */ 63 static struct TreeNodes { 64 PackageName pack; 65 CONFIGS configs; 66 DependencyType depType = DependencyType.required; 67 68 size_t toHash() const nothrow @trusted { 69 size_t ret = typeid(string).getHash(&pack); 70 ret ^= typeid(CONFIGS).getHash(&configs); 71 return ret; 72 } 73 bool opEqual(const scope ref TreeNodes other) const { return pack == other.pack && configs == other.configs; } 74 int opCmp(const scope ref TreeNodes other) const { 75 if (pack != other.pack) return pack < other.pack ? -1 : 1; 76 if (configs != other.configs) return configs < other.configs ? -1 : 1; 77 return 0; 78 } 79 } 80 81 /** A single node in the dependency graph. 82 83 Nodes are a combination of a package and a single package configuration. 84 */ 85 static struct TreeNode { 86 PackageName pack; 87 CONFIG config; 88 89 size_t toHash() const nothrow @trusted { 90 size_t ret = pack.hashOf(); 91 ret ^= typeid(CONFIG).getHash(&config); 92 return ret; 93 } 94 bool opEqual(const scope ref TreeNode other) const { return pack == other.pack && config == other.config; } 95 int opCmp(const scope ref TreeNode other) const { 96 if (pack != other.pack) return pack < other.pack ? -1 : 1; 97 if (config != other.config) return config < other.config ? -1 : 1; 98 return 0; 99 } 100 } 101 102 CONFIG[PackageName] resolve(TreeNode root, bool throw_on_failure = true) 103 { 104 auto rootbase = root.pack.main; 105 106 // build up the dependency graph, eliminating as many configurations/ 107 // versions as possible 108 ResolveContext context; 109 context.configs[rootbase] = [ResolveConfig(root.config, true)]; 110 ulong loop_counter = this.loop_limit; 111 constrain(root, context, loop_counter); 112 113 // Get best available results 114 foreach (base; context.configs.keys) 115 foreach (j, ref sc; context.configs[base]) 116 if (sc.included){ 117 context.result[base] = sc.config; 118 break; 119 } 120 121 // remove any non-default optional dependencies 122 purgeOptionalDependencies(root, context.result); 123 124 // the root package is implied by the `root` argument and will not be 125 // returned explicitly 126 context.result.remove(rootbase); 127 128 logDiagnostic("Dependency resolution result:"); 129 foreach (d; context.result.keys.sort()) 130 logDiagnostic(" %s: %s", d, context.result[d]); 131 132 return context.result; 133 } 134 135 protected abstract CONFIG[] getAllConfigs(in PackageName pack); 136 protected abstract CONFIG[] getSpecificConfigs(in PackageName pack, TreeNodes nodes); 137 protected abstract TreeNodes[] getChildren(TreeNode node); 138 protected abstract bool matches(CONFIGS configs, CONFIG config); 139 140 private static struct ResolveConfig { 141 CONFIG config; 142 bool included; 143 } 144 145 private static struct ResolveContext { 146 /** Contains all packages visited by the resolution process so far. 147 148 The key is the qualified name of the package (base + sub) 149 */ 150 void[0][PackageName] visited; 151 152 /// The finally chosen configurations for each package 153 CONFIG[PackageName] result; 154 155 /// The set of available configurations for each package 156 ResolveConfig[][PackageName] configs; 157 158 /// Determines if a certain package has already been processed 159 bool isVisited(in PackageName package_) const { return (package_ in visited) !is null; } 160 161 /// Marks a package as processed 162 void setVisited(in PackageName package_) { visited[package_] = (void[0]).init; } 163 164 /// Returns a deep clone 165 ResolveContext clone() 166 { 167 ResolveContext ret; 168 ret.visited = this.visited.dup; 169 ret.result = this.result.dup; 170 foreach (pack, cfgs; this.configs) { 171 ret.configs[pack] = cfgs.dup; 172 } 173 return ret; 174 } 175 } 176 177 178 /** Starting with a single node, fills `context` with a minimized set of 179 configurations that form valid solutions. 180 */ 181 private void constrain(TreeNode n, ref ResolveContext context, ref ulong max_iterations) 182 { 183 auto base = n.pack.main; 184 assert(base in context.configs); 185 if (context.isVisited(n.pack)) return; 186 context.setVisited(n.pack); 187 188 auto dependencies = getChildren(n); 189 190 foreach (dep; dependencies) { 191 // lazily load all dependency configurations 192 auto depbase = dep.pack.main; 193 auto di = depbase in context.configs; 194 if (!di) { 195 context.configs[depbase] = 196 getAllConfigs(depbase) 197 .map!(c => ResolveConfig(c, true)) 198 .array; 199 di = depbase in context.configs; 200 } 201 202 // add any dependee defined dependency configurations 203 foreach (sc; getSpecificConfigs(n.pack, dep)) 204 if (!(*di).canFind!(c => c.config == sc)) 205 *di = ResolveConfig(sc, true) ~ *di; 206 207 // restrain the configurations to the current dependency spec 208 bool any_config = false; 209 foreach (i, ref c; *di) 210 if (c.included) { 211 if (!matches(dep.configs, c.config)) 212 c.included = false; 213 else any_config = true; 214 } 215 216 if (!any_config && dep.depType == DependencyType.required) { 217 if ((*di).length) 218 throw new ResolveException(n, dep, context); 219 else throw new DependencyLoadException(n, dep); 220 } 221 } 222 223 constrainDependencies(n, dependencies, 0, context, max_iterations); 224 } 225 226 /** Recurses back into `constrain` while recursively going through `n`'s 227 dependencies. 228 229 This attempts to constrain each dependency, while keeping each of them 230 in a nested stack frame. This allows any errors to properly back 231 propagate. 232 */ 233 private void constrainDependencies(TreeNode n, TreeNodes[] dependencies, size_t depidx, 234 ref ResolveContext context, ref ulong max_iterations) 235 { 236 if (depidx >= dependencies.length) return; 237 238 assert (--max_iterations > 0, 239 "The dependency resolution process is taking too long. The" 240 ~ " dependency graph is likely hitting a pathological case in" 241 ~ " the resolution algorithm. Please file a bug report at" 242 ~ " https://github.com/dlang/dub/issues and mention the package" 243 ~ " recipe that reproduces this error."); 244 245 auto dep = &dependencies[depidx]; 246 auto depbase = dep.pack.main; 247 auto depconfigs = context.configs[depbase]; 248 249 Exception first_err; 250 251 // try each configuration/version of the current dependency 252 foreach (i, c; depconfigs) { 253 if (c.included) { 254 try { 255 // try the configuration on a cloned context 256 auto subcontext = context.clone; 257 constrain(TreeNode(dep.pack, c.config), subcontext, max_iterations); 258 constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations); 259 // if a branch succeeded, replace the current context 260 // with the one from the branch and return 261 context = subcontext; 262 return; 263 } catch (Exception e) { 264 if (!first_err) first_err = e; 265 } 266 } 267 } 268 269 // ignore unsatisfiable optional dependencies 270 if (dep.depType != DependencyType.required) { 271 auto subcontext = context.clone; 272 constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations); 273 context = subcontext; 274 return; 275 } 276 277 // report the first error encountered to the user 278 if (first_err) throw first_err; 279 280 // should have thrown in constrainRec before reaching this 281 assert(false, format("Got no configuration for dependency %s %s of %s %s!?", 282 dep.pack, dep.configs, n.pack, n.config)); 283 } 284 285 private void purgeOptionalDependencies(TreeNode root, ref CONFIG[PackageName] configs) 286 { 287 bool[PackageName] required; 288 bool[PackageName] visited; 289 290 void markRecursively(TreeNode node) 291 { 292 if (node.pack in visited) return; 293 visited[node.pack] = true; 294 required[node.pack.main] = true; 295 foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional)) 296 if (auto dp = dep.pack.main in configs) 297 markRecursively(TreeNode(dep.pack, *dp)); 298 } 299 300 // recursively mark all required dependencies of the concrete dependency tree 301 markRecursively(root); 302 303 // remove all unmarked configurations 304 foreach (p; configs.keys.dup) 305 if (p !in required) 306 configs.remove(p); 307 } 308 309 final class ResolveException : Exception { 310 import std.range : chain, only; 311 import std.typecons : tuple; 312 313 PackageName failedNode; 314 315 this(TreeNode parent, TreeNodes dep, const scope ref ResolveContext context, string file = __FILE__, size_t line = __LINE__) 316 { 317 auto m = format("Unresolvable dependencies to package %s:", dep.pack.main); 318 super(m, file, line); 319 320 this.failedNode = dep.pack; 321 322 auto failbase = failedNode.main; 323 324 // Get partial results 325 CONFIG[PackageName] partial_result; 326 foreach (base; context.configs.keys) 327 foreach (j, ref sc; context.configs[base]) 328 if (sc.included){ 329 partial_result[base] = sc.config; 330 break; 331 } 332 333 // get the list of all dependencies to the failed package 334 auto deps = context.visited.byKey 335 .filter!(p => !!(p.main in partial_result)) 336 .map!(p => TreeNode(p, partial_result[p.main])) 337 .map!(n => getChildren(n) 338 .filter!(d => d.pack.main == failbase) 339 .map!(d => tuple(n, d)) 340 ) 341 .join 342 .sort!((a, b) => a[0].pack < b[0].pack); 343 344 foreach (d; deps) { 345 // filter out trivial self-dependencies 346 if (d[0].pack.main == failbase 347 && matches(d[1].configs, d[0].config)) 348 continue; 349 msg ~= format("\n %s %s depends on %s %s", d[0].pack, d[0].config, d[1].pack, d[1].configs); 350 } 351 } 352 } 353 354 final class DependencyLoadException : Exception { 355 TreeNode parent; 356 TreeNodes dependency; 357 358 this(TreeNode parent, TreeNodes dep) 359 { 360 auto m = format("Failed to find any versions for package %s, referenced by %s %s", 361 dep.pack, parent.pack, parent.config); 362 super(m, file, line); 363 364 this.parent = parent; 365 this.dependency = dep; 366 } 367 } 368 } 369 370 enum DependencyType { 371 required, 372 optionalDefault, 373 optional 374 } 375 376 unittest { 377 static struct IntConfig { 378 int value; 379 alias value this; 380 enum invalid = IntConfig(-1); 381 } 382 static IntConfig ic(int v) { return IntConfig(v); } 383 static struct IntConfigs { 384 IntConfig[] configs; 385 alias configs this; 386 } 387 static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); } 388 static PackageName pn(string name) { return PackageName(name); } 389 390 static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) { 391 private TreeNodes[][string] m_children; 392 this(TreeNodes[][string] children) { super(ulong.max); m_children = children; } 393 protected override IntConfig[] getAllConfigs(in PackageName pack) { 394 auto ret = appender!(IntConfig[]); 395 foreach (p_; m_children.byKey) { 396 // Note: We abuse subpackage notation to store configs 397 const p = PackageName(p_); 398 if (p.main != pack.main) continue; 399 ret ~= ic(p.sub.to!uint); 400 } 401 ret.data.sort!"a>b"(); 402 return ret.data; 403 } 404 protected override IntConfig[] getSpecificConfigs(in PackageName pack, TreeNodes nodes) { return null; } 405 protected override TreeNodes[] getChildren(TreeNode node) { 406 assert(node.pack.sub.length == 0); 407 return m_children.get(node.pack.toString() ~ ":" ~ node.config.to!string(), null); 408 } 409 protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); } 410 } 411 412 // properly back up if conflicts are detected along the way (d:2 vs d:1) 413 with (TestResolver) { 414 auto res = new TestResolver([ 415 "a:0": [TreeNodes(pn("b"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)])), TreeNodes(pn("e"), ics([ic(2), ic(1)]))], 416 "b:1": [TreeNodes(pn("c"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)]))], 417 "b:2": [TreeNodes(pn("c"), ics([ic(3), ic(2)])), TreeNodes(pn("d"), ics([ic(2), ic(1)]))], 418 "c:1": [], "c:2": [], "c:3": [], 419 "d:1": [], "d:2": [], 420 "e:1": [], "e:2": [], 421 ]); 422 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2), pn("c"):ic(3), pn("d"):ic(1), pn("e"):ic(2)], 423 format("%s", res.resolve(TreeNode(pn("a"), ic(0))))); 424 } 425 426 // handle cyclic dependencies gracefully 427 with (TestResolver) { 428 auto res = new TestResolver([ 429 "a:0": [TreeNodes(pn("b"), ics([ic(1)]))], 430 "b:1": [TreeNodes(pn("b"), ics([ic(1)]))] 431 ]); 432 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)]); 433 } 434 435 // don't choose optional dependencies by default 436 with (TestResolver) { 437 auto res = new TestResolver([ 438 "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)], 439 "b:1": [] 440 ]); 441 assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0, 442 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 443 } 444 445 // choose default optional dependencies by default 446 with (TestResolver) { 447 auto res = new TestResolver([ 448 "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optionalDefault)], 449 "b:1": [] 450 ]); 451 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)], 452 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 453 } 454 455 // choose optional dependency if non-optional within the dependency tree 456 with (TestResolver) { 457 auto res = new TestResolver([ 458 "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional), TreeNodes(pn("c"), ics([ic(1)]))], 459 "b:1": [], 460 "c:1": [TreeNodes(pn("b"), ics([ic(1)]))] 461 ]); 462 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1), pn("c"):ic(1)], 463 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 464 } 465 466 // don't choose optional dependency if non-optional outside of final dependency tree 467 with (TestResolver) { 468 auto res = new TestResolver([ 469 "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)], 470 "b:1": [], 471 "preset:0": [TreeNodes(pn("b"), ics([ic(1)]))] 472 ]); 473 assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0, 474 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 475 } 476 477 // don't choose optional dependency if non-optional in a non-selected version 478 with (TestResolver) { 479 auto res = new TestResolver([ 480 "a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))], 481 "b:1": [TreeNodes(pn("c"), ics([ic(1)]))], 482 "b:2": [TreeNodes(pn("c"), ics([ic(1)]), DependencyType.optional)], 483 "c:1": [] 484 ]); 485 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)], 486 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 487 } 488 489 // make sure non-satisfiable dependencies are not a problem, even if non-optional in some dependencies 490 with (TestResolver) { 491 auto res = new TestResolver([ 492 "a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))], 493 "b:1": [TreeNodes(pn("c"), ics([ic(2)]))], 494 "b:2": [TreeNodes(pn("c"), ics([ic(2)]), DependencyType.optional)], 495 "c:1": [] 496 ]); 497 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)], 498 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 499 } 500 501 // check error message for multiple conflicting dependencies 502 with (TestResolver) { 503 auto res = new TestResolver([ 504 "a:0": [TreeNodes(pn("b"), ics([ic(1)])), TreeNodes(pn("c"), ics([ic(1)]))], 505 "b:1": [TreeNodes(pn("d"), ics([ic(1)]))], 506 "c:1": [TreeNodes(pn("d"), ics([ic(2)]))], 507 "d:1": [], 508 "d:2": [] 509 ]); 510 try { 511 res.resolve(TreeNode(pn("a"), ic(0))); 512 assert(false, "Expected resolve to throw."); 513 } catch (ResolveException e) { 514 assert(e.msg == 515 "Unresolvable dependencies to package d:" 516 ~ "\n b 1 depends on d [1]" 517 ~ "\n c 1 depends on d [2]"); 518 } 519 } 520 521 // check error message for invalid dependency 522 with (TestResolver) { 523 auto res = new TestResolver([ 524 "a:0": [TreeNodes(pn("b"), ics([ic(1)]))] 525 ]); 526 try { 527 res.resolve(TreeNode(pn("a"), ic(0))); 528 assert(false, "Expected resolve to throw."); 529 } catch (DependencyLoadException e) { 530 assert(e.msg == "Failed to find any versions for package b, referenced by a 0"); 531 } 532 } 533 534 // regression: unresolvable optional dependency skips the remaining dependencies 535 with (TestResolver) { 536 auto res = new TestResolver([ 537 "a:0": [ 538 TreeNodes(pn("b"), ics([ic(2)]), DependencyType.optional), 539 TreeNodes(pn("c"), ics([ic(1)])) 540 ], 541 "b:1": [], 542 "c:1": [] 543 ]); 544 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("c"):ic(1)]); 545 } 546 }