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 		PackageName 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 		PackageName 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[PackageName] resolve(TreeNode root, bool throw_on_failure = true)
103 	{
104 		auto rootbase = root.pack.main;
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 		// Get best available results
114 		foreach (base; context.configs.keys)
115 			foreach (j, ref sc; context.configs[base])
116 				if (sc.included){
117 					context.result[base] = sc.config;
118 					break;
119 				}
120 
121 		// remove any non-default optional dependencies
122 		purgeOptionalDependencies(root, context.result);
123 
124 		// the root package is implied by the `root` argument and will not be
125 		// returned explicitly
126 		context.result.remove(rootbase);
127 
128 		logDiagnostic("Dependency resolution result:");
129 		foreach (d; context.result.keys.sort())
130 			logDiagnostic("  %s: %s", d, context.result[d]);
131 
132 		return context.result;
133 	}
134 
135 	protected abstract CONFIG[] getAllConfigs(in PackageName pack);
136 	protected abstract CONFIG[] getSpecificConfigs(in PackageName pack, TreeNodes nodes);
137 	protected abstract TreeNodes[] getChildren(TreeNode node);
138 	protected abstract bool matches(CONFIGS configs, CONFIG config);
139 
140 	private static struct ResolveConfig {
141 		CONFIG config;
142 		bool included;
143 	}
144 
145 	private static struct ResolveContext {
146 		/** Contains all packages visited by the resolution process so far.
147 
148 			The key is the qualified name of the package (base + sub)
149 		*/
150 		void[0][PackageName] visited;
151 
152 		/// The finally chosen configurations for each package
153 		CONFIG[PackageName] result;
154 
155 		/// The set of available configurations for each package
156 		ResolveConfig[][PackageName] configs;
157 
158 		/// Determines if a certain package has already been processed
159 		bool isVisited(in PackageName package_) const { return (package_ in visited) !is null; }
160 
161 		/// Marks a package as processed
162 		void setVisited(in PackageName package_) { visited[package_] = (void[0]).init; }
163 
164 		/// Returns a deep clone
165 		ResolveContext clone()
166 		{
167 			ResolveContext ret;
168 			ret.visited = this.visited.dup;
169 			ret.result = this.result.dup;
170 			foreach (pack, cfgs; this.configs) {
171 				ret.configs[pack] = cfgs.dup;
172 			}
173 			return ret;
174 		}
175 	}
176 
177 
178 	/** Starting with a single node, fills `context` with a minimized set of
179 		configurations that form valid solutions.
180 	*/
181 	private void constrain(TreeNode n, ref ResolveContext context, ref ulong max_iterations)
182 	{
183 		auto base = n.pack.main;
184 		assert(base in context.configs);
185 		if (context.isVisited(n.pack)) return;
186 		context.setVisited(n.pack);
187 
188 		auto dependencies = getChildren(n);
189 
190 		foreach (dep; dependencies) {
191 			// lazily load all dependency configurations
192 			auto depbase = dep.pack.main;
193 			auto di = depbase in context.configs;
194 			if (!di) {
195 				context.configs[depbase] =
196 					getAllConfigs(depbase)
197 					.map!(c => ResolveConfig(c, true))
198 					.array;
199 				di = depbase in context.configs;
200 			}
201 
202 			// add any dependee defined dependency configurations
203 			foreach (sc; getSpecificConfigs(n.pack, dep))
204 				if (!(*di).canFind!(c => c.config == sc))
205 					*di = ResolveConfig(sc, true) ~ *di;
206 
207 			// restrain the configurations to the current dependency spec
208 			bool any_config = false;
209 			foreach (i, ref c; *di)
210 				if (c.included) {
211 					if (!matches(dep.configs, c.config))
212 						c.included = false;
213 					else any_config = true;
214 				}
215 
216 			if (!any_config && dep.depType == DependencyType.required) {
217 				if ((*di).length)
218 					throw new ResolveException(n, dep, context);
219 				else throw new DependencyLoadException(n, dep);
220 			}
221 		}
222 
223 		constrainDependencies(n, dependencies, 0, context, max_iterations);
224 	}
225 
226 	/** Recurses back into `constrain` while recursively going through `n`'s
227 		dependencies.
228 
229 		This attempts to constrain each dependency, while keeping each of them
230 		in a nested stack frame. This allows any errors to properly back
231 		propagate.
232 	*/
233 	private void constrainDependencies(TreeNode n, TreeNodes[] dependencies, size_t depidx,
234 		ref ResolveContext context, ref ulong max_iterations)
235 	{
236 		if (depidx >= dependencies.length) return;
237 
238 		assert (--max_iterations > 0,
239 			"The dependency resolution process is taking too long. The"
240 			~ " dependency graph is likely hitting a pathological case in"
241 			~ " the resolution algorithm. Please file a bug report at"
242 			~ " https://github.com/dlang/dub/issues and mention the package"
243 			~ " recipe that reproduces this error.");
244 
245 		auto dep = &dependencies[depidx];
246 		auto depbase = dep.pack.main;
247 		auto depconfigs = context.configs[depbase];
248 
249 		Exception first_err;
250 
251 		// try each configuration/version of the current dependency
252 		foreach (i, c; depconfigs) {
253 			if (c.included) {
254 				try {
255 					// try the configuration on a cloned context
256 					auto subcontext = context.clone;
257 					constrain(TreeNode(dep.pack, c.config), subcontext, max_iterations);
258 					constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations);
259 					// if a branch succeeded, replace the current context
260 					// with the one from the branch and return
261 					context = subcontext;
262 					return;
263 				} catch (Exception e) {
264 					if (!first_err) first_err = e;
265 				}
266 			}
267 		}
268 
269 		// ignore unsatisfiable optional dependencies
270 		if (dep.depType != DependencyType.required) {
271 			auto subcontext = context.clone;
272 			constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations);
273 			context = subcontext;
274 			return;
275 		}
276 
277 		// report the first error encountered to the user
278 		if (first_err) throw first_err;
279 
280 		// should have thrown in constrainRec before reaching this
281 		assert(false, format("Got no configuration for dependency %s %s of %s %s!?",
282 			dep.pack, dep.configs, n.pack, n.config));
283 	}
284 
285 	private void purgeOptionalDependencies(TreeNode root, ref CONFIG[PackageName] configs)
286 	{
287 		bool[PackageName] required;
288 		bool[PackageName] visited;
289 
290 		void markRecursively(TreeNode node)
291 		{
292 			if (node.pack in visited) return;
293 			visited[node.pack] = true;
294 			required[node.pack.main] = true;
295 			foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional))
296 				if (auto dp = dep.pack.main in configs)
297 					markRecursively(TreeNode(dep.pack, *dp));
298 		}
299 
300 		// recursively mark all required dependencies of the concrete dependency tree
301 		markRecursively(root);
302 
303 		// remove all unmarked configurations
304 		foreach (p; configs.keys.dup)
305 			if (p !in required)
306 				configs.remove(p);
307 	}
308 
309 	final class ResolveException : Exception {
310 		import std.range : chain, only;
311 		import std.typecons : tuple;
312 
313 		PackageName failedNode;
314 
315 		this(TreeNode parent, TreeNodes dep, const scope ref ResolveContext context, string file = __FILE__, size_t line = __LINE__)
316 		{
317 			auto m = format("Unresolvable dependencies to package %s:", dep.pack.main);
318 			super(m, file, line);
319 
320 			this.failedNode = dep.pack;
321 
322 			auto failbase = failedNode.main;
323 
324 			// Get partial results
325 			CONFIG[PackageName] partial_result;
326 			foreach (base; context.configs.keys)
327 				foreach (j, ref sc; context.configs[base])
328 					if (sc.included){
329 						partial_result[base] = sc.config;
330 						break;
331 					}
332 
333 			// get the list of all dependencies to the failed package
334 			auto deps = context.visited.byKey
335 				.filter!(p => !!(p.main in partial_result))
336 				.map!(p => TreeNode(p, partial_result[p.main]))
337 				.map!(n => getChildren(n)
338 					.filter!(d => d.pack.main == failbase)
339 					.map!(d => tuple(n, d))
340 				)
341 				.join
342 				.sort!((a, b) => a[0].pack < b[0].pack);
343 
344 			foreach (d; deps) {
345 				// filter out trivial self-dependencies
346 				if (d[0].pack.main == failbase
347 					&& matches(d[1].configs, d[0].config))
348 					continue;
349 				msg ~= format("\n  %s %s depends on %s %s", d[0].pack, d[0].config, d[1].pack, d[1].configs);
350 			}
351 		}
352 	}
353 
354 	final class DependencyLoadException : Exception {
355 		TreeNode parent;
356 		TreeNodes dependency;
357 
358 		this(TreeNode parent, TreeNodes dep)
359 		{
360 			auto m = format("Failed to find any versions for package %s, referenced by %s %s",
361 				dep.pack, parent.pack, parent.config);
362 			super(m, file, line);
363 
364 			this.parent = parent;
365 			this.dependency = dep;
366 		}
367 	}
368 }
369 
370 enum DependencyType {
371 	required,
372 	optionalDefault,
373 	optional
374 }
375 
376 unittest {
377 	static struct IntConfig {
378 		int value;
379 		alias value this;
380 		enum invalid = IntConfig(-1);
381 	}
382 	static IntConfig ic(int v) { return IntConfig(v); }
383 	static struct IntConfigs {
384 		IntConfig[] configs;
385 		alias configs this;
386 	}
387 	static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); }
388 	static PackageName pn(string name)		{ return PackageName(name); }
389 
390 	static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) {
391 		private TreeNodes[][string] m_children;
392 		this(TreeNodes[][string] children) { super(ulong.max); m_children = children; }
393 		protected override IntConfig[] getAllConfigs(in PackageName pack) {
394 			auto ret = appender!(IntConfig[]);
395 			foreach (p_; m_children.byKey) {
396 				// Note: We abuse subpackage notation to store configs
397 				const p = PackageName(p_);
398 				if (p.main != pack.main) continue;
399 				ret ~= ic(p.sub.to!uint);
400 			}
401 			ret.data.sort!"a>b"();
402 			return ret.data;
403 		}
404 		protected override IntConfig[] getSpecificConfigs(in PackageName pack, TreeNodes nodes) { return null; }
405 		protected override TreeNodes[] getChildren(TreeNode node) {
406 			assert(node.pack.sub.length == 0);
407 			return m_children.get(node.pack.toString() ~ ":" ~ node.config.to!string(), null);
408 		}
409 		protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); }
410 	}
411 
412 	// properly back up if conflicts are detected along the way (d:2 vs d:1)
413 	with (TestResolver) {
414 		auto res = new TestResolver([
415 			"a:0": [TreeNodes(pn("b"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)])), TreeNodes(pn("e"), ics([ic(2), ic(1)]))],
416 			"b:1": [TreeNodes(pn("c"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)]))],
417 			"b:2": [TreeNodes(pn("c"), ics([ic(3), ic(2)])), TreeNodes(pn("d"), ics([ic(2), ic(1)]))],
418 			"c:1": [], "c:2": [], "c:3": [],
419 			"d:1": [], "d:2": [],
420 			"e:1": [], "e:2": [],
421 		]);
422 		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2), pn("c"):ic(3), pn("d"):ic(1), pn("e"):ic(2)],
423 			format("%s", res.resolve(TreeNode(pn("a"), ic(0)))));
424 	}
425 
426 	// handle cyclic dependencies gracefully
427 	with (TestResolver) {
428 		auto res = new TestResolver([
429 			"a:0": [TreeNodes(pn("b"), ics([ic(1)]))],
430 			"b:1": [TreeNodes(pn("b"), ics([ic(1)]))]
431 		]);
432 		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)]);
433 	}
434 
435 	// don't choose optional dependencies by default
436 	with (TestResolver) {
437 		auto res = new TestResolver([
438 			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)],
439 			"b:1": []
440 		]);
441 		assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0,
442 			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
443 	}
444 
445 	// choose default optional dependencies by default
446 	with (TestResolver) {
447 		auto res = new TestResolver([
448 			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optionalDefault)],
449 			"b:1": []
450 		]);
451 		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)],
452 			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
453 	}
454 
455 	// choose optional dependency if non-optional within the dependency tree
456 	with (TestResolver) {
457 		auto res = new TestResolver([
458 			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional), TreeNodes(pn("c"), ics([ic(1)]))],
459 			"b:1": [],
460 			"c:1": [TreeNodes(pn("b"), ics([ic(1)]))]
461 		]);
462 		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1), pn("c"):ic(1)],
463 			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
464 	}
465 
466 	// don't choose optional dependency if non-optional outside of final dependency tree
467 	with (TestResolver) {
468 		auto res = new TestResolver([
469 			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)],
470 			"b:1": [],
471 			"preset:0": [TreeNodes(pn("b"), ics([ic(1)]))]
472 		]);
473 		assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0,
474 			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
475 	}
476 
477 	// don't choose optional dependency if non-optional in a non-selected version
478 	with (TestResolver) {
479 		auto res = new TestResolver([
480 			"a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))],
481 			"b:1": [TreeNodes(pn("c"), ics([ic(1)]))],
482 			"b:2": [TreeNodes(pn("c"), ics([ic(1)]), DependencyType.optional)],
483 			"c:1": []
484 		]);
485 		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)],
486 			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
487 	}
488 
489 	// make sure non-satisfiable dependencies are not a problem, even if non-optional in some dependencies
490 	with (TestResolver) {
491 		auto res = new TestResolver([
492 			"a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))],
493 			"b:1": [TreeNodes(pn("c"), ics([ic(2)]))],
494 			"b:2": [TreeNodes(pn("c"), ics([ic(2)]), DependencyType.optional)],
495 			"c:1": []
496 		]);
497 		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)],
498 			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
499 	}
500 
501 	// check error message for multiple conflicting dependencies
502 	with (TestResolver) {
503 		auto res = new TestResolver([
504 			"a:0": [TreeNodes(pn("b"), ics([ic(1)])), TreeNodes(pn("c"), ics([ic(1)]))],
505 			"b:1": [TreeNodes(pn("d"), ics([ic(1)]))],
506 			"c:1": [TreeNodes(pn("d"), ics([ic(2)]))],
507 			"d:1": [],
508 			"d:2": []
509 		]);
510 		try {
511 			res.resolve(TreeNode(pn("a"), ic(0)));
512 			assert(false, "Expected resolve to throw.");
513 		} catch (ResolveException e) {
514 			assert(e.msg ==
515 				"Unresolvable dependencies to package d:"
516 				~ "\n  b 1 depends on d [1]"
517 				~ "\n  c 1 depends on d [2]");
518 		}
519 	}
520 
521 	// check error message for invalid dependency
522 	with (TestResolver) {
523 		auto res = new TestResolver([
524 			"a:0": [TreeNodes(pn("b"), ics([ic(1)]))]
525 		]);
526 		try {
527 			res.resolve(TreeNode(pn("a"), ic(0)));
528 			assert(false, "Expected resolve to throw.");
529 		} catch (DependencyLoadException e) {
530 			assert(e.msg == "Failed to find any versions for package b, referenced by a 0");
531 		}
532 	}
533 
534 	// regression: unresolvable optional dependency skips the remaining dependencies
535 	with (TestResolver) {
536 		auto res = new TestResolver([
537 			"a:0": [
538 				TreeNodes(pn("b"), ics([ic(2)]), DependencyType.optional),
539 				TreeNodes(pn("c"), ics([ic(1)]))
540 			],
541 			"b:1": [],
542 			"c:1": []
543 		]);
544 		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("c"):ic(1)]);
545 	}
546 }