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, 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 25 hash_t toHash() const nothrow @trusted { 26 size_t ret = typeid(string).getHash(&pack); 27 ret ^= typeid(CONFIGS).getHash(&configs); 28 return ret; 29 } 30 bool opEqual(in ref TreeNodes other) const { return pack == other.pack && configs == other.configs; } 31 int opCmp(in ref TreeNodes other) const { 32 if (pack != other.pack) return pack < other.pack ? -1 : 1; 33 if (configs != other.configs) return configs < other.configs ? -1 : 1; 34 return 0; 35 } 36 } 37 38 static struct TreeNode { 39 string pack; 40 CONFIG config; 41 42 hash_t toHash() const nothrow @trusted { 43 size_t ret = typeid(string).getHash(&pack); 44 ret ^= typeid(CONFIG).getHash(&config); 45 return ret; 46 } 47 bool opEqual(in ref TreeNode other) const { return pack == other.pack && config == other.config; } 48 int opCmp(in ref TreeNode other) const { 49 if (pack != other.pack) return pack < other.pack ? -1 : 1; 50 if (config != other.config) return config < other.config ? -1 : 1; 51 return 0; 52 } 53 } 54 55 CONFIG[string] resolve(TreeNode root, bool throw_on_failure = true) 56 { 57 static string rootPackage(string p) { 58 auto idx = indexOf(p, ":"); 59 if (idx < 0) return p; 60 return p[0 .. idx]; 61 } 62 63 auto root_base_pack = rootPackage(root.pack); 64 65 // find all possible configurations of each possible dependency 66 size_t[string] package_indices; 67 CONFIG[][] all_configs; 68 bool[TreeNode] visited; 69 void findConfigsRec(TreeNode parent, bool parent_unique) 70 { 71 if (parent in visited) return; 72 visited[parent] = true; 73 74 foreach (ch; getChildren(parent)) { 75 auto basepack = rootPackage(ch.pack); 76 auto pidx = all_configs.length; 77 CONFIG[] configs; 78 if (auto pi = basepack in package_indices) { 79 pidx = *pi; 80 configs = all_configs[*pi]; 81 } else { 82 if (basepack == root_base_pack) configs = [root.config]; 83 else configs = getAllConfigs(basepack); 84 all_configs ~= configs; 85 package_indices[basepack] = pidx; 86 } 87 88 configs = getSpecificConfigs(ch) ~ configs; 89 90 // eliminate configurations from which we know that they can't satisfy 91 // the uniquely defined root dependencies (==version or ~branch style dependencies) 92 if (parent_unique) configs = configs.filter!(c => matches(ch.configs, c)).array; 93 94 all_configs[pidx] = configs; 95 96 foreach (v; configs) 97 findConfigsRec(TreeNode(ch.pack, v), parent_unique && configs.length == 1); 98 } 99 } 100 findConfigsRec(root, true); 101 102 // prepend an invalid configuration to denote an unchosen dependency 103 // this is used to properly support optional dependencies (when 104 // getChildren() returns no configurations for an optional dependency, 105 // but getAllConfigs() has already provided an existing list of configs) 106 foreach (ref cfgs; all_configs) cfgs = CONFIG.invalid ~ cfgs; 107 108 logDebug("Configurations used for dependency resolution:"); 109 foreach (n, i; package_indices) logDebug(" %s (%s): %s", n, i, all_configs[i]); 110 111 auto config_indices = new size_t[all_configs.length]; 112 config_indices[] = 0; 113 114 string last_error; 115 116 visited = null; 117 sizediff_t validateConfigs(TreeNode parent, ref string error) 118 { 119 import std.algorithm : max; 120 121 if (parent in visited) return -1; 122 123 visited[parent] = true; 124 sizediff_t maxcpi = -1; 125 sizediff_t parentidx = package_indices.get(rootPackage(parent.pack), -1); 126 auto parentbase = rootPackage(parent.pack); 127 128 // loop over all dependencies 129 foreach (ch; getChildren(parent)) { 130 auto basepack = rootPackage(ch.pack); 131 assert(basepack in package_indices, format("%s not in packages %s", basepack, package_indices)); 132 133 // get the current config/version of the current dependency 134 sizediff_t childidx = package_indices[basepack]; 135 if (all_configs[childidx] == [CONFIG.invalid]) { 136 enforce(parentbase != root_base_pack, format("Root package %s contains reference to invalid package %s", parent.pack, ch.pack)); 137 // choose another parent config to avoid the invalid child 138 if (parentidx > maxcpi) { 139 error = format("Package %s contains invalid dependency %s", parent.pack, ch.pack); 140 logDiagnostic("%s (ci=%s)", error, parentidx); 141 maxcpi = parentidx; 142 } 143 } else { 144 auto config = all_configs[childidx][config_indices[childidx]]; 145 auto chnode = TreeNode(ch.pack, config); 146 147 if (config == CONFIG.invalid || !matches(ch.configs, config)) { 148 // if we are at the root level, we can safely skip the maxcpi computation and instead choose another childidx config 149 if (parentbase == root_base_pack) { 150 error = format("No match for dependency %s %s of %s", ch.pack, ch.configs, parent.pack); 151 return childidx; 152 } 153 154 if (childidx > maxcpi) { 155 maxcpi = max(childidx, parentidx); 156 error = format("Dependency %s -> %s %s mismatches with selected version %s", parent.pack, ch.pack, ch.configs, config); 157 logDebug("%s (ci=%s)", error, maxcpi); 158 } 159 160 // we know that either the child or the parent needs to be switched 161 // to another configuration, no need to continue with other children 162 if (config == CONFIG.invalid) break; 163 } 164 165 maxcpi = max(maxcpi, validateConfigs(chnode, error)); 166 } 167 } 168 return maxcpi; 169 } 170 171 string first_error; 172 173 while (true) { 174 // check if the current combination of configurations works out 175 visited = null; 176 string error; 177 auto conflict_index = validateConfigs(root, error); 178 if (!first_error) first_error = error; 179 180 // print out current iteration state 181 logDebug("Interation (ci=%s) %s", conflict_index, { 182 import std.array : join; 183 auto cs = new string[all_configs.length]; 184 foreach (p, i; package_indices) { 185 if (all_configs[i].length) 186 cs[i] = p~" "~all_configs[i][config_indices[i]].to!string~(i >= 0 && i >= conflict_index ? " (C)" : ""); 187 else cs[i] = p ~ " [no config]"; 188 } 189 return cs.join(", "); 190 }()); 191 192 if (conflict_index < 0) { 193 CONFIG[string] ret; 194 foreach (p, i; package_indices) 195 if (all_configs[i].length) { 196 auto cfg = all_configs[i][config_indices[i]]; 197 if (cfg != CONFIG.invalid) ret[p] = cfg; 198 } 199 return ret; 200 } 201 202 // find the next combination of configurations 203 foreach_reverse (pi, ref i; config_indices) { 204 if (pi > conflict_index) i = 0; 205 else if (++i >= all_configs[pi].length) i = 0; 206 else break; 207 } 208 if (config_indices.all!"a==0") { 209 if (throw_on_failure) throw new Exception("Could not find a valid dependency tree configuration: "~first_error); 210 else return null; 211 } 212 } 213 } 214 215 protected abstract CONFIG[] getAllConfigs(string pack); 216 protected abstract CONFIG[] getSpecificConfigs(TreeNodes nodes); 217 protected abstract TreeNodes[] getChildren(TreeNode node); 218 protected abstract bool matches(CONFIGS configs, CONFIG config); 219 } 220 221 222 unittest { 223 static struct IntConfig { 224 int value; 225 alias value this; 226 enum invalid = IntConfig(-1); 227 } 228 static IntConfig ic(int v) { return IntConfig(v); } 229 230 static class TestResolver : DependencyResolver!(IntConfig[], IntConfig) { 231 private TreeNodes[][string] m_children; 232 this(TreeNodes[][string] children) { m_children = children; } 233 protected override IntConfig[] getAllConfigs(string pack) { 234 auto ret = appender!(IntConfig[]); 235 foreach (p; m_children.byKey) { 236 if (p.length <= pack.length+1) continue; 237 if (p[0 .. pack.length] != pack || p[pack.length] != ':') continue; 238 auto didx = p.lastIndexOf(':'); 239 ret ~= ic(p[didx+1 .. $].to!uint); 240 } 241 ret.data.sort!"a>b"(); 242 return ret.data; 243 } 244 protected override IntConfig[] getSpecificConfigs(TreeNodes nodes) { return null; } 245 protected override TreeNodes[] getChildren(TreeNode node) { return m_children.get(node.pack ~ ":" ~ node.config.to!string(), null); } 246 protected override bool matches(IntConfig[] configs, IntConfig config) { return configs.canFind(config); } 247 } 248 249 // properly back up if conflicts are detected along the way (d:2 vs d:1) 250 with (TestResolver) { 251 auto res = new TestResolver([ 252 "a:0": [TreeNodes("b", [ic(2), ic(1)]), TreeNodes("d", [ic(1)]), TreeNodes("e", [ic(2), ic(1)])], 253 "b:1": [TreeNodes("c", [ic(2), ic(1)]), TreeNodes("d", [ic(1)])], 254 "b:2": [TreeNodes("c", [ic(3), ic(2)]), TreeNodes("d", [ic(2), ic(1)])], 255 "c:1": [], "c:2": [], "c:3": [], 256 "d:1": [], "d:2": [], 257 "e:1": [], "e:2": [], 258 ]); 259 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))))); 260 } 261 262 // handle cyclic dependencies gracefully 263 with (TestResolver) { 264 auto res = new TestResolver([ 265 "a:0": [TreeNodes("b", [ic(1)])], 266 "b:1": [TreeNodes("b", [ic(1)])] 267 ]); 268 assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)]); 269 } 270 }