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 string 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 string 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[string] resolve(TreeNode root, bool throw_on_failure = true) 103 { 104 auto rootbase = root.pack.basePackageName; 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(string pack); 128 protected abstract CONFIG[] getSpecificConfigs(string 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][string] visited; 143 144 /// The finally chosen configurations for each package 145 CONFIG[string] result; 146 147 /// The set of available configurations for each package 148 ResolveConfig[][string] configs; 149 150 /// Determines if a certain package has already been processed 151 bool isVisited(string package_) const { return (package_ in visited) !is null; } 152 153 /// Marks a package as processed 154 void setVisited(string 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.basePackageName; 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.basePackageName; 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.basePackageName; 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[string] configs) 281 { 282 bool[string] required; 283 bool[string] visited; 284 285 void markRecursively(TreeNode node) 286 { 287 if (node.pack in visited) return; 288 visited[node.pack] = true; 289 required[node.pack.basePackageName] = true; 290 foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional)) 291 if (auto dp = dep.pack.basePackageName 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 un-marked 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 string 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.basePackageName); 313 super(m, file, line); 314 315 this.failedNode = dep.pack; 316 317 auto failbase = failedNode.basePackageName; 318 319 // get the list of all dependencies to the failed package 320 auto deps = context.visited.byKey 321 .filter!(p => p.basePackageName in context.result) 322 .map!(p => TreeNode(p, context.result[p.basePackageName])) 323 .map!(n => getChildren(n) 324 .filter!(d => d.pack.basePackageName == 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.basePackageName == 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 private string basePackageName(string p) 363 { 364 import std.algorithm.searching : findSplit; 365 return p.findSplit(":")[0]; 366 } 367 368 unittest { 369 static struct IntConfig { 370 int value; 371 alias value this; 372 enum invalid = IntConfig(-1); 373 } 374 static IntConfig ic(int v) { return IntConfig(v); } 375 static struct IntConfigs { 376 IntConfig[] configs; 377 alias configs this; 378 } 379 static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); } 380 381 static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) { 382 private TreeNodes[][string] m_children; 383 this(TreeNodes[][string] children) { super(ulong.max); m_children = children; } 384 protected override IntConfig[] getAllConfigs(string pack) { 385 auto ret = appender!(IntConfig[]); 386 foreach (p; m_children.byKey) { 387 if (p.length <= pack.length+1) continue; 388 if (p[0 .. pack.length] != pack || p[pack.length] != ':') continue; 389 auto didx = p.lastIndexOf(':'); 390 ret ~= ic(p[didx+1 .. $].to!uint); 391 } 392 ret.data.sort!"a>b"(); 393 return ret.data; 394 } 395 protected override IntConfig[] getSpecificConfigs(string pack, TreeNodes nodes) { return null; } 396 protected override TreeNodes[] getChildren(TreeNode node) { return m_children.get(node.pack ~ ":" ~ node.config.to!string(), null); } 397 protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); } 398 } 399 400 // properly back up if conflicts are detected along the way (d:2 vs d:1) 401 with (TestResolver) { 402 auto res = new TestResolver([ 403 "a:0": [TreeNodes("b", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)])), TreeNodes("e", ics([ic(2), ic(1)]))], 404 "b:1": [TreeNodes("c", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)]))], 405 "b:2": [TreeNodes("c", ics([ic(3), ic(2)])), TreeNodes("d", ics([ic(2), ic(1)]))], 406 "c:1": [], "c:2": [], "c:3": [], 407 "d:1": [], "d:2": [], 408 "e:1": [], "e:2": [], 409 ]); 410 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))))); 411 } 412 413 // handle cyclic dependencies gracefully 414 with (TestResolver) { 415 auto res = new TestResolver([ 416 "a:0": [TreeNodes("b", ics([ic(1)]))], 417 "b:1": [TreeNodes("b", ics([ic(1)]))] 418 ]); 419 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)]); 420 } 421 422 // don't choose optional dependencies by default 423 with (TestResolver) { 424 auto res = new TestResolver([ 425 "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)], 426 "b:1": [] 427 ]); 428 assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0))))); 429 } 430 431 // choose default optional dependencies by default 432 with (TestResolver) { 433 auto res = new TestResolver([ 434 "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optionalDefault)], 435 "b:1": [] 436 ]); 437 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)], to!string(res.resolve(TreeNode("a", ic(0))))); 438 } 439 440 // choose optional dependency if non-optional within the dependency tree 441 with (TestResolver) { 442 auto res = new TestResolver([ 443 "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional), TreeNodes("c", ics([ic(1)]))], 444 "b:1": [], 445 "c:1": [TreeNodes("b", ics([ic(1)]))] 446 ]); 447 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1), "c":ic(1)], to!string(res.resolve(TreeNode("a", ic(0))))); 448 } 449 450 // don't choose optional dependency if non-optional outside of final dependency tree 451 with (TestResolver) { 452 auto res = new TestResolver([ 453 "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)], 454 "b:1": [], 455 "preset:0": [TreeNodes("b", ics([ic(1)]))] 456 ]); 457 assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0))))); 458 } 459 460 // don't choose optional dependency if non-optional in a non-selected version 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(1)]))], 465 "b:2": [TreeNodes("c", ics([ic(1)]), 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 471 // make sure non-satisfiable dependencies are not a problem, even if non-optional in some dependencies 472 with (TestResolver) { 473 auto res = new TestResolver([ 474 "a:0": [TreeNodes("b", ics([ic(1), ic(2)]))], 475 "b:1": [TreeNodes("c", ics([ic(2)]))], 476 "b:2": [TreeNodes("c", ics([ic(2)]), DependencyType.optional)], 477 "c:1": [] 478 ]); 479 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0))))); 480 } 481 482 // check error message for multiple conflicting dependencies 483 with (TestResolver) { 484 auto res = new TestResolver([ 485 "a:0": [TreeNodes("b", ics([ic(1)])), TreeNodes("c", ics([ic(1)]))], 486 "b:1": [TreeNodes("d", ics([ic(1)]))], 487 "c:1": [TreeNodes("d", ics([ic(2)]))], 488 "d:1": [], 489 "d:2": [] 490 ]); 491 try { 492 res.resolve(TreeNode("a", ic(0))); 493 assert(false, "Expected resolve to throw."); 494 } catch (ResolveException e) { 495 assert(e.msg == 496 "Unresolvable dependencies to package d:" 497 ~ "\n b 1 depends on d [1]" 498 ~ "\n c 1 depends on d [2]"); 499 } 500 } 501 502 // check error message for invalid dependency 503 with (TestResolver) { 504 auto res = new TestResolver([ 505 "a:0": [TreeNodes("b", ics([ic(1)]))] 506 ]); 507 try { 508 res.resolve(TreeNode("a", ic(0))); 509 assert(false, "Expected resolve to throw."); 510 } catch (DependencyLoadException e) { 511 assert(e.msg == "Failed to find any versions for package b, referenced by a 0"); 512 } 513 } 514 515 // regression: unresolvable optional dependency skips the remaining dependencies 516 with (TestResolver) { 517 auto res = new TestResolver([ 518 "a:0": [ 519 TreeNodes("b", ics([ic(2)]), DependencyType.optional), 520 TreeNodes("c", ics([ic(1)])) 521 ], 522 "b:1": [], 523 "c:1": [] 524 ]); 525 assert(res.resolve(TreeNode("a", ic(0))) == ["c":ic(1)]); 526 } 527 }