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 // remove any non-default optional dependencies 114 purgeOptionalDependencies(root, context.result); 115 116 // the root package is implied by the `root` argument and will not be 117 // returned explicitly 118 context.result.remove(rootbase); 119 120 logDiagnostic("Dependency resolution result:"); 121 foreach (d; context.result.keys.sort()) 122 logDiagnostic(" %s: %s", d, context.result[d]); 123 124 return context.result; 125 } 126 127 protected abstract CONFIG[] getAllConfigs(in PackageName pack); 128 protected abstract CONFIG[] getSpecificConfigs(in PackageName pack, TreeNodes nodes); 129 protected abstract TreeNodes[] getChildren(TreeNode node); 130 protected abstract bool matches(CONFIGS configs, CONFIG config); 131 132 private static struct ResolveConfig { 133 CONFIG config; 134 bool included; 135 } 136 137 private static struct ResolveContext { 138 /** Contains all packages visited by the resolution process so far. 139 140 The key is the qualified name of the package (base + sub) 141 */ 142 void[0][PackageName] visited; 143 144 /// The finally chosen configurations for each package 145 CONFIG[PackageName] result; 146 147 /// The set of available configurations for each package 148 ResolveConfig[][PackageName] configs; 149 150 /// Determines if a certain package has already been processed 151 bool isVisited(in PackageName package_) const { return (package_ in visited) !is null; } 152 153 /// Marks a package as processed 154 void setVisited(in PackageName package_) { visited[package_] = (void[0]).init; } 155 156 /// Returns a deep clone 157 ResolveContext clone() 158 { 159 ResolveContext ret; 160 ret.visited = this.visited.dup; 161 ret.result = this.result.dup; 162 foreach (pack, cfgs; this.configs) { 163 ret.configs[pack] = cfgs.dup; 164 } 165 return ret; 166 } 167 } 168 169 170 /** Starting with a single node, fills `context` with a minimized set of 171 configurations that form valid solutions. 172 */ 173 private void constrain(TreeNode n, ref ResolveContext context, ref ulong max_iterations) 174 { 175 auto base = n.pack.main; 176 assert(base in context.configs); 177 if (context.isVisited(n.pack)) return; 178 context.setVisited(n.pack); 179 context.result[base] = n.config; 180 foreach (j, ref sc; context.configs[base]) 181 sc.included = sc.config == n.config; 182 183 auto dependencies = getChildren(n); 184 185 foreach (dep; dependencies) { 186 // lazily load all dependency configurations 187 auto depbase = dep.pack.main; 188 auto di = depbase in context.configs; 189 if (!di) { 190 context.configs[depbase] = 191 getAllConfigs(depbase) 192 .map!(c => ResolveConfig(c, true)) 193 .array; 194 di = depbase in context.configs; 195 } 196 197 // add any dependee defined dependency configurations 198 foreach (sc; getSpecificConfigs(n.pack, dep)) 199 if (!(*di).canFind!(c => c.config == sc)) 200 *di = ResolveConfig(sc, true) ~ *di; 201 202 // restrain the configurations to the current dependency spec 203 bool any_config = false; 204 foreach (i, ref c; *di) 205 if (c.included) { 206 if (!matches(dep.configs, c.config)) 207 c.included = false; 208 else any_config = true; 209 } 210 211 if (!any_config && dep.depType == DependencyType.required) { 212 if ((*di).length) 213 throw new ResolveException(n, dep, context); 214 else throw new DependencyLoadException(n, dep); 215 } 216 } 217 218 constrainDependencies(n, dependencies, 0, context, max_iterations); 219 } 220 221 /** Recurses back into `constrain` while recursively going through `n`'s 222 dependencies. 223 224 This attempts to constrain each dependency, while keeping each of them 225 in a nested stack frame. This allows any errors to properly back 226 propagate. 227 */ 228 private void constrainDependencies(TreeNode n, TreeNodes[] dependencies, size_t depidx, 229 ref ResolveContext context, ref ulong max_iterations) 230 { 231 if (depidx >= dependencies.length) return; 232 233 assert (--max_iterations > 0, 234 "The dependency resolution process is taking too long. The" 235 ~ " dependency graph is likely hitting a pathological case in" 236 ~ " the resolution algorithm. Please file a bug report at" 237 ~ " https://github.com/dlang/dub/issues and mention the package" 238 ~ " recipe that reproduces this error."); 239 240 auto dep = &dependencies[depidx]; 241 auto depbase = dep.pack.main; 242 auto depconfigs = context.configs[depbase]; 243 244 Exception first_err; 245 246 // try each configuration/version of the current dependency 247 foreach (i, c; depconfigs) { 248 if (c.included) { 249 try { 250 // try the configuration on a cloned context 251 auto subcontext = context.clone; 252 constrain(TreeNode(dep.pack, c.config), subcontext, max_iterations); 253 constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations); 254 // if a branch succeeded, replace the current context 255 // with the one from the branch and return 256 context = subcontext; 257 return; 258 } catch (Exception e) { 259 if (!first_err) first_err = e; 260 } 261 } 262 } 263 264 // ignore unsatisfiable optional dependencies 265 if (dep.depType != DependencyType.required) { 266 auto subcontext = context.clone; 267 constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations); 268 context = subcontext; 269 return; 270 } 271 272 // report the first error encountered to the user 273 if (first_err) throw first_err; 274 275 // should have thrown in constrainRec before reaching this 276 assert(false, format("Got no configuration for dependency %s %s of %s %s!?", 277 dep.pack, dep.configs, n.pack, n.config)); 278 } 279 280 private void purgeOptionalDependencies(TreeNode root, ref CONFIG[PackageName] configs) 281 { 282 bool[PackageName] required; 283 bool[PackageName] visited; 284 285 void markRecursively(TreeNode node) 286 { 287 if (node.pack in visited) return; 288 visited[node.pack] = true; 289 required[node.pack.main] = true; 290 foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional)) 291 if (auto dp = dep.pack.main in configs) 292 markRecursively(TreeNode(dep.pack, *dp)); 293 } 294 295 // recursively mark all required dependencies of the concrete dependency tree 296 markRecursively(root); 297 298 // remove all unmarked configurations 299 foreach (p; configs.keys.dup) 300 if (p !in required) 301 configs.remove(p); 302 } 303 304 final class ResolveException : Exception { 305 import std.range : chain, only; 306 import std.typecons : tuple; 307 308 PackageName failedNode; 309 310 this(TreeNode parent, TreeNodes dep, const scope ref ResolveContext context, string file = __FILE__, size_t line = __LINE__) 311 { 312 auto m = format("Unresolvable dependencies to package %s:", dep.pack.main); 313 super(m, file, line); 314 315 this.failedNode = dep.pack; 316 317 auto failbase = failedNode.main; 318 319 // get the list of all dependencies to the failed package 320 auto deps = context.visited.byKey 321 .filter!(p => !!(p.main in context.result)) 322 .map!(p => TreeNode(p, context.result[p.main])) 323 .map!(n => getChildren(n) 324 .filter!(d => d.pack.main == failbase) 325 .map!(d => tuple(n, d)) 326 ) 327 .join 328 .sort!((a, b) => a[0].pack < b[0].pack); 329 330 foreach (d; deps) { 331 // filter out trivial self-dependencies 332 if (d[0].pack.main == failbase 333 && matches(d[1].configs, d[0].config)) 334 continue; 335 msg ~= format("\n %s %s depends on %s %s", d[0].pack, d[0].config, d[1].pack, d[1].configs); 336 } 337 } 338 } 339 340 final class DependencyLoadException : Exception { 341 TreeNode parent; 342 TreeNodes dependency; 343 344 this(TreeNode parent, TreeNodes dep) 345 { 346 auto m = format("Failed to find any versions for package %s, referenced by %s %s", 347 dep.pack, parent.pack, parent.config); 348 super(m, file, line); 349 350 this.parent = parent; 351 this.dependency = dep; 352 } 353 } 354 } 355 356 enum DependencyType { 357 required, 358 optionalDefault, 359 optional 360 } 361 362 unittest { 363 static struct IntConfig { 364 int value; 365 alias value this; 366 enum invalid = IntConfig(-1); 367 } 368 static IntConfig ic(int v) { return IntConfig(v); } 369 static struct IntConfigs { 370 IntConfig[] configs; 371 alias configs this; 372 } 373 static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); } 374 static PackageName pn(string name) { return PackageName(name); } 375 376 static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) { 377 private TreeNodes[][string] m_children; 378 this(TreeNodes[][string] children) { super(ulong.max); m_children = children; } 379 protected override IntConfig[] getAllConfigs(in PackageName pack) { 380 auto ret = appender!(IntConfig[]); 381 foreach (p_; m_children.byKey) { 382 // Note: We abuse subpackage notation to store configs 383 const p = PackageName(p_); 384 if (p.main != pack.main) continue; 385 ret ~= ic(p.sub.to!uint); 386 } 387 ret.data.sort!"a>b"(); 388 return ret.data; 389 } 390 protected override IntConfig[] getSpecificConfigs(in PackageName pack, TreeNodes nodes) { return null; } 391 protected override TreeNodes[] getChildren(TreeNode node) { 392 assert(node.pack.sub.length == 0); 393 return m_children.get(node.pack.toString() ~ ":" ~ node.config.to!string(), null); 394 } 395 protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); } 396 } 397 398 // properly back up if conflicts are detected along the way (d:2 vs d:1) 399 with (TestResolver) { 400 auto res = new TestResolver([ 401 "a:0": [TreeNodes(pn("b"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)])), TreeNodes(pn("e"), ics([ic(2), ic(1)]))], 402 "b:1": [TreeNodes(pn("c"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)]))], 403 "b:2": [TreeNodes(pn("c"), ics([ic(3), ic(2)])), TreeNodes(pn("d"), ics([ic(2), ic(1)]))], 404 "c:1": [], "c:2": [], "c:3": [], 405 "d:1": [], "d:2": [], 406 "e:1": [], "e:2": [], 407 ]); 408 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2), pn("c"):ic(3), pn("d"):ic(1), pn("e"):ic(2)], 409 format("%s", res.resolve(TreeNode(pn("a"), ic(0))))); 410 } 411 412 // handle cyclic dependencies gracefully 413 with (TestResolver) { 414 auto res = new TestResolver([ 415 "a:0": [TreeNodes(pn("b"), ics([ic(1)]))], 416 "b:1": [TreeNodes(pn("b"), ics([ic(1)]))] 417 ]); 418 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)]); 419 } 420 421 // don't choose optional dependencies by default 422 with (TestResolver) { 423 auto res = new TestResolver([ 424 "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)], 425 "b:1": [] 426 ]); 427 assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0, 428 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 429 } 430 431 // choose default optional dependencies by default 432 with (TestResolver) { 433 auto res = new TestResolver([ 434 "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optionalDefault)], 435 "b:1": [] 436 ]); 437 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)], 438 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 439 } 440 441 // choose optional dependency if non-optional within the dependency tree 442 with (TestResolver) { 443 auto res = new TestResolver([ 444 "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional), TreeNodes(pn("c"), ics([ic(1)]))], 445 "b:1": [], 446 "c:1": [TreeNodes(pn("b"), ics([ic(1)]))] 447 ]); 448 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1), pn("c"):ic(1)], 449 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 450 } 451 452 // don't choose optional dependency if non-optional outside of final dependency tree 453 with (TestResolver) { 454 auto res = new TestResolver([ 455 "a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)], 456 "b:1": [], 457 "preset:0": [TreeNodes(pn("b"), ics([ic(1)]))] 458 ]); 459 assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0, 460 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 461 } 462 463 // don't choose optional dependency if non-optional in a non-selected version 464 with (TestResolver) { 465 auto res = new TestResolver([ 466 "a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))], 467 "b:1": [TreeNodes(pn("c"), ics([ic(1)]))], 468 "b:2": [TreeNodes(pn("c"), ics([ic(1)]), DependencyType.optional)], 469 "c:1": [] 470 ]); 471 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)], 472 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 473 } 474 475 // make sure non-satisfiable dependencies are not a problem, even if non-optional in some dependencies 476 with (TestResolver) { 477 auto res = new TestResolver([ 478 "a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))], 479 "b:1": [TreeNodes(pn("c"), ics([ic(2)]))], 480 "b:2": [TreeNodes(pn("c"), ics([ic(2)]), DependencyType.optional)], 481 "c:1": [] 482 ]); 483 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)], 484 to!string(res.resolve(TreeNode(pn("a"), ic(0))))); 485 } 486 487 // check error message for multiple conflicting dependencies 488 with (TestResolver) { 489 auto res = new TestResolver([ 490 "a:0": [TreeNodes(pn("b"), ics([ic(1)])), TreeNodes(pn("c"), ics([ic(1)]))], 491 "b:1": [TreeNodes(pn("d"), ics([ic(1)]))], 492 "c:1": [TreeNodes(pn("d"), ics([ic(2)]))], 493 "d:1": [], 494 "d:2": [] 495 ]); 496 try { 497 res.resolve(TreeNode(pn("a"), ic(0))); 498 assert(false, "Expected resolve to throw."); 499 } catch (ResolveException e) { 500 assert(e.msg == 501 "Unresolvable dependencies to package d:" 502 ~ "\n b 1 depends on d [1]" 503 ~ "\n c 1 depends on d [2]"); 504 } 505 } 506 507 // check error message for invalid dependency 508 with (TestResolver) { 509 auto res = new TestResolver([ 510 "a:0": [TreeNodes(pn("b"), ics([ic(1)]))] 511 ]); 512 try { 513 res.resolve(TreeNode(pn("a"), ic(0))); 514 assert(false, "Expected resolve to throw."); 515 } catch (DependencyLoadException e) { 516 assert(e.msg == "Failed to find any versions for package b, referenced by a 0"); 517 } 518 } 519 520 // regression: unresolvable optional dependency skips the remaining dependencies 521 with (TestResolver) { 522 auto res = new TestResolver([ 523 "a:0": [ 524 TreeNodes(pn("b"), ics([ic(2)]), DependencyType.optional), 525 TreeNodes(pn("c"), ics([ic(1)])) 526 ], 527 "b:1": [], 528 "c:1": [] 529 ]); 530 assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("c"):ic(1)]); 531 } 532 }