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