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 		string 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 		string 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[string] resolve(TreeNode root, bool throw_on_failure = true)
103 	{
104 		auto rootbase = root.pack.basePackageName;
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(string pack);
128 	protected abstract CONFIG[] getSpecificConfigs(string 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][string] visited;
143 
144 		/// The finally chosen configurations for each package
145 		CONFIG[string] result;
146 
147 		/// The set of available configurations for each package
148 		ResolveConfig[][string] configs;
149 
150 		/// Determines if a certain package has already been processed
151 		bool isVisited(string package_) const { return (package_ in visited) !is null; }
152 
153 		/// Marks a package as processed
154 		void setVisited(string 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.basePackageName;
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.basePackageName;
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.basePackageName;
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[string] configs)
281 	{
282 		bool[string] required;
283 		bool[string] visited;
284 
285 		void markRecursively(TreeNode node)
286 		{
287 			if (node.pack in visited) return;
288 			visited[node.pack] = true;
289 			required[node.pack.basePackageName] = true;
290 			foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional))
291 				if (auto dp = dep.pack.basePackageName 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 		string 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.basePackageName);
313 			super(m, file, line);
314 
315 			this.failedNode = dep.pack;
316 
317 			auto failbase = failedNode.basePackageName;
318 
319 			// get the list of all dependencies to the failed package
320 			auto deps = context.visited.byKey
321 				.filter!(p => p.basePackageName in context.result)
322 				.map!(p => TreeNode(p, context.result[p.basePackageName]))
323 				.map!(n => getChildren(n)
324 					.filter!(d => d.pack.basePackageName == 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.basePackageName == 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 private string basePackageName(string p)
363 {
364 	import std.algorithm.searching : findSplit;
365 	return p.findSplit(":")[0];
366 }
367 
368 unittest {
369 	static struct IntConfig {
370 		int value;
371 		alias value this;
372 		enum invalid = IntConfig(-1);
373 	}
374 	static IntConfig ic(int v) { return IntConfig(v); }
375 	static struct IntConfigs {
376 		IntConfig[] configs;
377 		alias configs this;
378 	}
379 	static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); }
380 
381 	static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) {
382 		private TreeNodes[][string] m_children;
383 		this(TreeNodes[][string] children) { super(ulong.max); m_children = children; }
384 		protected override IntConfig[] getAllConfigs(string pack) {
385 			auto ret = appender!(IntConfig[]);
386 			foreach (p; m_children.byKey) {
387 				if (p.length <= pack.length+1) continue;
388 				if (p[0 .. pack.length] != pack || p[pack.length] != ':') continue;
389 				auto didx = p.lastIndexOf(':');
390 				ret ~= ic(p[didx+1 .. $].to!uint);
391 			}
392 			ret.data.sort!"a>b"();
393 			return ret.data;
394 		}
395 		protected override IntConfig[] getSpecificConfigs(string pack, TreeNodes nodes) { return null; }
396 		protected override TreeNodes[] getChildren(TreeNode node) { return m_children.get(node.pack ~ ":" ~ node.config.to!string(), null); }
397 		protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); }
398 	}
399 
400 	// properly back up if conflicts are detected along the way (d:2 vs d:1)
401 	with (TestResolver) {
402 		auto res = new TestResolver([
403 			"a:0": [TreeNodes("b", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)])), TreeNodes("e", ics([ic(2), ic(1)]))],
404 			"b:1": [TreeNodes("c", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)]))],
405 			"b:2": [TreeNodes("c", ics([ic(3), ic(2)])), TreeNodes("d", ics([ic(2), ic(1)]))],
406 			"c:1": [], "c:2": [], "c:3": [],
407 			"d:1": [], "d:2": [],
408 			"e:1": [], "e:2": [],
409 		]);
410 		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)))));
411 	}
412 
413 	// handle cyclic dependencies gracefully
414 	with (TestResolver) {
415 		auto res = new TestResolver([
416 			"a:0": [TreeNodes("b", ics([ic(1)]))],
417 			"b:1": [TreeNodes("b", ics([ic(1)]))]
418 		]);
419 		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)]);
420 	}
421 
422 	// don't choose optional dependencies by default
423 	with (TestResolver) {
424 		auto res = new TestResolver([
425 			"a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)],
426 			"b:1": []
427 		]);
428 		assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0)))));
429 	}
430 
431 	// choose default optional dependencies by default
432 	with (TestResolver) {
433 		auto res = new TestResolver([
434 			"a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optionalDefault)],
435 			"b:1": []
436 		]);
437 		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)], to!string(res.resolve(TreeNode("a", ic(0)))));
438 	}
439 
440 	// choose optional dependency if non-optional within the dependency tree
441 	with (TestResolver) {
442 		auto res = new TestResolver([
443 			"a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional), TreeNodes("c", ics([ic(1)]))],
444 			"b:1": [],
445 			"c:1": [TreeNodes("b", ics([ic(1)]))]
446 		]);
447 		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1), "c":ic(1)], to!string(res.resolve(TreeNode("a", ic(0)))));
448 	}
449 
450 	// don't choose optional dependency if non-optional outside of final dependency tree
451 	with (TestResolver) {
452 		auto res = new TestResolver([
453 			"a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)],
454 			"b:1": [],
455 			"preset:0": [TreeNodes("b", ics([ic(1)]))]
456 		]);
457 		assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0)))));
458 	}
459 
460 	// don't choose optional dependency if non-optional in a non-selected version
461 	with (TestResolver) {
462 		auto res = new TestResolver([
463 			"a:0": [TreeNodes("b", ics([ic(1), ic(2)]))],
464 			"b:1": [TreeNodes("c", ics([ic(1)]))],
465 			"b:2": [TreeNodes("c", ics([ic(1)]), DependencyType.optional)],
466 			"c:1": []
467 		]);
468 		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0)))));
469 	}
470 
471 	// make sure non-satisfiable dependencies are not a problem, even if non-optional in some dependencies
472 	with (TestResolver) {
473 		auto res = new TestResolver([
474 			"a:0": [TreeNodes("b", ics([ic(1), ic(2)]))],
475 			"b:1": [TreeNodes("c", ics([ic(2)]))],
476 			"b:2": [TreeNodes("c", ics([ic(2)]), DependencyType.optional)],
477 			"c:1": []
478 		]);
479 		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0)))));
480 	}
481 
482 	// check error message for multiple conflicting dependencies
483 	with (TestResolver) {
484 		auto res = new TestResolver([
485 			"a:0": [TreeNodes("b", ics([ic(1)])), TreeNodes("c", ics([ic(1)]))],
486 			"b:1": [TreeNodes("d", ics([ic(1)]))],
487 			"c:1": [TreeNodes("d", ics([ic(2)]))],
488 			"d:1": [],
489 			"d:2": []
490 		]);
491 		try {
492 			res.resolve(TreeNode("a", ic(0)));
493 			assert(false, "Expected resolve to throw.");
494 		} catch (ResolveException e) {
495 			assert(e.msg ==
496 				"Unresolvable dependencies to package d:"
497 				~ "\n  b 1 depends on d [1]"
498 				~ "\n  c 1 depends on d [2]");
499 		}
500 	}
501 
502 	// check error message for invalid dependency
503 	with (TestResolver) {
504 		auto res = new TestResolver([
505 			"a:0": [TreeNodes("b", ics([ic(1)]))]
506 		]);
507 		try {
508 			res.resolve(TreeNode("a", ic(0)));
509 			assert(false, "Expected resolve to throw.");
510 		} catch (DependencyLoadException e) {
511 			assert(e.msg == "Failed to find any versions for package b, referenced by a 0");
512 		}
513 	}
514 
515 	// regression: unresolvable optional dependency skips the remaining dependencies
516 	with (TestResolver) {
517 		auto res = new TestResolver([
518 			"a:0": [
519 				TreeNodes("b", ics([ic(2)]), DependencyType.optional),
520 				TreeNodes("c", ics([ic(1)]))
521 			],
522 			"b:1": [],
523 			"c:1": []
524 		]);
525 		assert(res.resolve(TreeNode("a", ic(0))) == ["c":ic(1)]);
526 	}
527 }