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