Newer
Older
dub_jkp / source / dub / dependencyresolver.d
/**
	Dependency configuration/version resolution algorithm.

	Copyright: © 2014-2018 Sönke Ludwig
	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
	Authors: Sönke Ludwig
*/
module dub.dependencyresolver;

import dub.dependency;
import dub.internal.logging;

import std.algorithm : all, canFind, filter, map, sort;
import std.array : appender, array, join;
import std.conv : to;
import std.exception : enforce;
import std.string : format, lastIndexOf;


/** Resolves dependency graph with multiple configurations per package.

	The term "configuration" can mean any kind of alternative dependency
	configuration of a package. In particular, it can mean different
	versions of a package.

	`CONFIG` is an abstract type denoting a single configuration of a certain
	package, whereas `CONFIGS` denotes a set of configurations. The
	representation of both can be freely chosen, so that `CONFIGS` for example
	can be defined in terms of a version range.
*/
class DependencyResolver(CONFIGS, CONFIG) {
	/// Maximum number of loop rounds to do
	protected ulong loop_limit;

	/**
	 * Construct an instance of this class
	 *
	 * Params:
	 *	 limit = Maximum number of loop rounds to do
	 */
	public this (ulong limit) inout scope @safe pure nothrow @nogc
	{
		this.loop_limit = limit;
	}

	/// Compatibility overload
	deprecated("Use the overload that accepts a `ulong limit` argument")
	public this () scope @safe
	{
		// Leave the possibility to opt-out from the loop limit
		import std.process : environment;
		if (environment.get("DUB_NO_RESOLVE_LIMIT") !is null)
			this(ulong.max);
		else
			this(1_000_000);
	}

	/** Encapsulates a list of outgoing edges in the dependency graph.

		A value of this type represents a single dependency with multiple
		possible configurations for the target package.
	*/
	static struct TreeNodes {
		PackageName pack;
		CONFIGS configs;
		DependencyType depType = DependencyType.required;

		size_t toHash() const nothrow @trusted {
			size_t ret = typeid(string).getHash(&pack);
			ret ^= typeid(CONFIGS).getHash(&configs);
			return ret;
		}
		bool opEqual(const scope ref TreeNodes other) const { return pack == other.pack && configs == other.configs; }
		int opCmp(const scope ref TreeNodes other) const {
			if (pack != other.pack) return pack < other.pack ? -1 : 1;
			if (configs != other.configs) return configs < other.configs ? -1 : 1;
			return 0;
		}
	}

	/** A single node in the dependency graph.

		Nodes are a combination of a package and a single package configuration.
	*/
	static struct TreeNode {
		PackageName pack;
		CONFIG config;

		size_t toHash() const nothrow @trusted {
			size_t ret = pack.hashOf();
			ret ^= typeid(CONFIG).getHash(&config);
			return ret;
		}
		bool opEqual(const scope ref TreeNode other) const { return pack == other.pack && config == other.config; }
		int opCmp(const scope ref TreeNode other) const {
			if (pack != other.pack) return pack < other.pack ? -1 : 1;
			if (config != other.config) return config < other.config ? -1 : 1;
			return 0;
		}
	}

	CONFIG[PackageName] resolve(TreeNode root, bool throw_on_failure = true)
	{
		auto rootbase = root.pack.main;

		// build up the dependency graph, eliminating as many configurations/
		// versions as possible
		ResolveContext context;
		context.configs[rootbase] = [ResolveConfig(root.config, true)];
		ulong loop_counter = this.loop_limit;
		constrain(root, context, loop_counter);

		// remove any non-default optional dependencies
		purgeOptionalDependencies(root, context.result);

		// the root package is implied by the `root` argument and will not be
		// returned explicitly
		context.result.remove(rootbase);

		logDiagnostic("Dependency resolution result:");
		foreach (d; context.result.keys.sort())
			logDiagnostic("  %s: %s", d, context.result[d]);

		return context.result;
	}

	protected abstract CONFIG[] getAllConfigs(in PackageName pack);
	protected abstract CONFIG[] getSpecificConfigs(in PackageName pack, TreeNodes nodes);
	protected abstract TreeNodes[] getChildren(TreeNode node);
	protected abstract bool matches(CONFIGS configs, CONFIG config);

	private static struct ResolveConfig {
		CONFIG config;
		bool included;
	}

	private static struct ResolveContext {
		/** Contains all packages visited by the resolution process so far.

			The key is the qualified name of the package (base + sub)
		*/
		void[0][PackageName] visited;

		/// The finally chosen configurations for each package
		CONFIG[PackageName] result;

		/// The set of available configurations for each package
		ResolveConfig[][PackageName] configs;

		/// Determines if a certain package has already been processed
		bool isVisited(in PackageName package_) const { return (package_ in visited) !is null; }

		/// Marks a package as processed
		void setVisited(in PackageName package_) { visited[package_] = (void[0]).init; }

		/// Returns a deep clone
		ResolveContext clone()
		{
			ResolveContext ret;
			ret.visited = this.visited.dup;
			ret.result = this.result.dup;
			foreach (pack, cfgs; this.configs) {
				ret.configs[pack] = cfgs.dup;
			}
			return ret;
		}
	}


	/** Starting with a single node, fills `context` with a minimized set of
		configurations that form valid solutions.
	*/
	private void constrain(TreeNode n, ref ResolveContext context, ref ulong max_iterations)
	{
		auto base = n.pack.main;
		assert(base in context.configs);
		if (context.isVisited(n.pack)) return;
		context.setVisited(n.pack);
		context.result[base] = n.config;
		foreach (j, ref sc; context.configs[base])
			sc.included = sc.config == n.config;

		auto dependencies = getChildren(n);

		foreach (dep; dependencies) {
			// lazily load all dependency configurations
			auto depbase = dep.pack.main;
			auto di = depbase in context.configs;
			if (!di) {
				context.configs[depbase] =
					getAllConfigs(depbase)
					.map!(c => ResolveConfig(c, true))
					.array;
				di = depbase in context.configs;
			}

			// add any dependee defined dependency configurations
			foreach (sc; getSpecificConfigs(n.pack, dep))
				if (!(*di).canFind!(c => c.config == sc))
					*di = ResolveConfig(sc, true) ~ *di;

			// restrain the configurations to the current dependency spec
			bool any_config = false;
			foreach (i, ref c; *di)
				if (c.included) {
					if (!matches(dep.configs, c.config))
						c.included = false;
					else any_config = true;
				}

			if (!any_config && dep.depType == DependencyType.required) {
				if ((*di).length)
					throw new ResolveException(n, dep, context);
				else throw new DependencyLoadException(n, dep);
			}
		}

		constrainDependencies(n, dependencies, 0, context, max_iterations);
	}

	/** Recurses back into `constrain` while recursively going through `n`'s
		dependencies.

		This attempts to constrain each dependency, while keeping each of them
		in a nested stack frame. This allows any errors to properly back
		propagate.
	*/
	private void constrainDependencies(TreeNode n, TreeNodes[] dependencies, size_t depidx,
		ref ResolveContext context, ref ulong max_iterations)
	{
		if (depidx >= dependencies.length) return;

		assert (--max_iterations > 0,
			"The dependency resolution process is taking too long. The"
			~ " dependency graph is likely hitting a pathological case in"
			~ " the resolution algorithm. Please file a bug report at"
			~ " https://github.com/dlang/dub/issues and mention the package"
			~ " recipe that reproduces this error.");

		auto dep = &dependencies[depidx];
		auto depbase = dep.pack.main;
		auto depconfigs = context.configs[depbase];

		Exception first_err;

		// try each configuration/version of the current dependency
		foreach (i, c; depconfigs) {
			if (c.included) {
				try {
					// try the configuration on a cloned context
					auto subcontext = context.clone;
					constrain(TreeNode(dep.pack, c.config), subcontext, max_iterations);
					constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations);
					// if a branch succeeded, replace the current context
					// with the one from the branch and return
					context = subcontext;
					return;
				} catch (Exception e) {
					if (!first_err) first_err = e;
				}
			}
		}

		// ignore unsatisfiable optional dependencies
		if (dep.depType != DependencyType.required) {
			auto subcontext = context.clone;
			constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations);
			context = subcontext;
			return;
		}

		// report the first error encountered to the user
		if (first_err) throw first_err;

		// should have thrown in constrainRec before reaching this
		assert(false, format("Got no configuration for dependency %s %s of %s %s!?",
			dep.pack, dep.configs, n.pack, n.config));
	}

	private void purgeOptionalDependencies(TreeNode root, ref CONFIG[PackageName] configs)
	{
		bool[PackageName] required;
		bool[PackageName] visited;

		void markRecursively(TreeNode node)
		{
			if (node.pack in visited) return;
			visited[node.pack] = true;
			required[node.pack.main] = true;
			foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional))
				if (auto dp = dep.pack.main in configs)
					markRecursively(TreeNode(dep.pack, *dp));
		}

		// recursively mark all required dependencies of the concrete dependency tree
		markRecursively(root);

		// remove all unmarked configurations
		foreach (p; configs.keys.dup)
			if (p !in required)
				configs.remove(p);
	}

	final class ResolveException : Exception {
		import std.range : chain, only;
		import std.typecons : tuple;

		PackageName failedNode;

		this(TreeNode parent, TreeNodes dep, const scope ref ResolveContext context, string file = __FILE__, size_t line = __LINE__)
		{
			auto m = format("Unresolvable dependencies to package %s:", dep.pack.main);
			super(m, file, line);

			this.failedNode = dep.pack;

			auto failbase = failedNode.main;

			// get the list of all dependencies to the failed package
			auto deps = context.visited.byKey
				.filter!(p => !!(p.main in context.result))
				.map!(p => TreeNode(p, context.result[p.main]))
				.map!(n => getChildren(n)
					.filter!(d => d.pack.main == failbase)
					.map!(d => tuple(n, d))
				)
				.join
				.sort!((a, b) => a[0].pack < b[0].pack);

			foreach (d; deps) {
				// filter out trivial self-dependencies
				if (d[0].pack.main == failbase
					&& matches(d[1].configs, d[0].config))
					continue;
				msg ~= format("\n  %s %s depends on %s %s", d[0].pack, d[0].config, d[1].pack, d[1].configs);
			}
		}
	}

	final class DependencyLoadException : Exception {
		TreeNode parent;
		TreeNodes dependency;

		this(TreeNode parent, TreeNodes dep)
		{
			auto m = format("Failed to find any versions for package %s, referenced by %s %s",
				dep.pack, parent.pack, parent.config);
			super(m, file, line);

			this.parent = parent;
			this.dependency = dep;
		}
	}
}

enum DependencyType {
	required,
	optionalDefault,
	optional
}

unittest {
	static struct IntConfig {
		int value;
		alias value this;
		enum invalid = IntConfig(-1);
	}
	static IntConfig ic(int v) { return IntConfig(v); }
	static struct IntConfigs {
		IntConfig[] configs;
		alias configs this;
	}
	static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); }
	static PackageName pn(string name)		{ return PackageName(name); }

	static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) {
		private TreeNodes[][string] m_children;
		this(TreeNodes[][string] children) { super(ulong.max); m_children = children; }
		protected override IntConfig[] getAllConfigs(in PackageName pack) {
			auto ret = appender!(IntConfig[]);
			foreach (p_; m_children.byKey) {
				// Note: We abuse subpackage notation to store configs
				const p = PackageName(p_);
				if (p.main != pack.main) continue;
				ret ~= ic(p.sub.to!uint);
			}
			ret.data.sort!"a>b"();
			return ret.data;
		}
		protected override IntConfig[] getSpecificConfigs(in PackageName pack, TreeNodes nodes) { return null; }
		protected override TreeNodes[] getChildren(TreeNode node) {
			assert(node.pack.sub.length == 0);
			return m_children.get(node.pack.toString() ~ ":" ~ node.config.to!string(), null);
		}
		protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); }
	}

	// properly back up if conflicts are detected along the way (d:2 vs d:1)
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)])), TreeNodes(pn("e"), ics([ic(2), ic(1)]))],
			"b:1": [TreeNodes(pn("c"), ics([ic(2), ic(1)])), TreeNodes(pn("d"), ics([ic(1)]))],
			"b:2": [TreeNodes(pn("c"), ics([ic(3), ic(2)])), TreeNodes(pn("d"), ics([ic(2), ic(1)]))],
			"c:1": [], "c:2": [], "c:3": [],
			"d:1": [], "d:2": [],
			"e:1": [], "e:2": [],
		]);
		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2), pn("c"):ic(3), pn("d"):ic(1), pn("e"):ic(2)],
			format("%s", res.resolve(TreeNode(pn("a"), ic(0)))));
	}

	// handle cyclic dependencies gracefully
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(1)]))],
			"b:1": [TreeNodes(pn("b"), ics([ic(1)]))]
		]);
		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)]);
	}

	// don't choose optional dependencies by default
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)],
			"b:1": []
		]);
		assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0,
			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
	}

	// choose default optional dependencies by default
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optionalDefault)],
			"b:1": []
		]);
		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1)],
			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
	}

	// choose optional dependency if non-optional within the dependency tree
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional), TreeNodes(pn("c"), ics([ic(1)]))],
			"b:1": [],
			"c:1": [TreeNodes(pn("b"), ics([ic(1)]))]
		]);
		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(1), pn("c"):ic(1)],
			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
	}

	// don't choose optional dependency if non-optional outside of final dependency tree
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(1)]), DependencyType.optional)],
			"b:1": [],
			"preset:0": [TreeNodes(pn("b"), ics([ic(1)]))]
		]);
		assert(res.resolve(TreeNode(pn("a"), ic(0))).length == 0,
			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
	}

	// don't choose optional dependency if non-optional in a non-selected version
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))],
			"b:1": [TreeNodes(pn("c"), ics([ic(1)]))],
			"b:2": [TreeNodes(pn("c"), ics([ic(1)]), DependencyType.optional)],
			"c:1": []
		]);
		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)],
			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
	}

	// make sure non-satisfiable dependencies are not a problem, even if non-optional in some dependencies
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(1), ic(2)]))],
			"b:1": [TreeNodes(pn("c"), ics([ic(2)]))],
			"b:2": [TreeNodes(pn("c"), ics([ic(2)]), DependencyType.optional)],
			"c:1": []
		]);
		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("b"):ic(2)],
			to!string(res.resolve(TreeNode(pn("a"), ic(0)))));
	}

	// check error message for multiple conflicting dependencies
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(1)])), TreeNodes(pn("c"), ics([ic(1)]))],
			"b:1": [TreeNodes(pn("d"), ics([ic(1)]))],
			"c:1": [TreeNodes(pn("d"), ics([ic(2)]))],
			"d:1": [],
			"d:2": []
		]);
		try {
			res.resolve(TreeNode(pn("a"), ic(0)));
			assert(false, "Expected resolve to throw.");
		} catch (ResolveException e) {
			assert(e.msg ==
				"Unresolvable dependencies to package d:"
				~ "\n  b 1 depends on d [1]"
				~ "\n  c 1 depends on d [2]");
		}
	}

	// check error message for invalid dependency
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [TreeNodes(pn("b"), ics([ic(1)]))]
		]);
		try {
			res.resolve(TreeNode(pn("a"), ic(0)));
			assert(false, "Expected resolve to throw.");
		} catch (DependencyLoadException e) {
			assert(e.msg == "Failed to find any versions for package b, referenced by a 0");
		}
	}

	// regression: unresolvable optional dependency skips the remaining dependencies
	with (TestResolver) {
		auto res = new TestResolver([
			"a:0": [
				TreeNodes(pn("b"), ics([ic(2)]), DependencyType.optional),
				TreeNodes(pn("c"), ics([ic(1)]))
			],
			"b:1": [],
			"c:1": []
		]);
		assert(res.resolve(TreeNode(pn("a"), ic(0))) == [pn("c"):ic(1)]);
	}
}