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