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 }