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, 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 
25 		hash_t toHash() const nothrow @trusted {
26 			size_t ret = typeid(string).getHash(&pack);
27 			ret ^= typeid(CONFIGS).getHash(&configs);
28 			return ret;
29 		}
30 		bool opEqual(in ref TreeNodes other) const { return pack == other.pack && configs == other.configs; }
31 		int opCmp(in ref TreeNodes other) const {
32 			if (pack != other.pack) return pack < other.pack ? -1 : 1;
33 			if (configs != other.configs) return configs < other.configs ? -1 : 1;
34 			return 0;
35 		}
36 	}
37 
38 	static struct TreeNode {
39 		string pack;
40 		CONFIG config;
41 
42 		hash_t toHash() const nothrow @trusted {
43 			size_t ret = typeid(string).getHash(&pack);
44 			ret ^= typeid(CONFIG).getHash(&config);
45 			return ret;
46 		}
47 		bool opEqual(in ref TreeNode other) const { return pack == other.pack && config == other.config; }
48 		int opCmp(in ref TreeNode other) const {
49 			if (pack != other.pack) return pack < other.pack ? -1 : 1;
50 			if (config != other.config) return config < other.config ? -1 : 1;
51 			return 0;
52 		}
53 	}
54 
55 	CONFIG[string] resolve(TreeNode root, bool throw_on_failure = true)
56 	{
57 		static string rootPackage(string p) {
58 			auto idx = indexOf(p, ":");
59 			if (idx < 0) return p;
60 			return p[0 .. idx];
61 		}
62 
63 		auto root_base_pack = rootPackage(root.pack);
64 
65 		// find all possible configurations of each possible dependency
66 		size_t[string] package_indices;
67 		CONFIG[][] all_configs;
68 		bool[TreeNode] visited;
69 		void findConfigsRec(TreeNode parent, bool parent_unique)
70 		{
71 			if (parent in visited) return;
72 			visited[parent] = true;
73 
74 			foreach (ch; getChildren(parent)) {
75 				auto basepack = rootPackage(ch.pack);
76 				auto pidx = all_configs.length;
77 				CONFIG[] configs;
78 				if (auto pi = basepack in package_indices) {
79 					pidx = *pi;
80 					configs = all_configs[*pi];
81 				} else {
82 					if (basepack == root_base_pack) configs = [root.config];
83 					else configs = getAllConfigs(basepack);
84 					all_configs ~= configs;
85 					package_indices[basepack] = pidx;
86 				}
87 
88 				configs = getSpecificConfigs(ch) ~ configs;
89 
90 				// eliminate configurations from which we know that they can't satisfy
91 				// the uniquely defined root dependencies (==version or ~branch style dependencies)
92 				if (parent_unique) configs = configs.filter!(c => matches(ch.configs, c)).array;
93 
94 				all_configs[pidx] = configs;
95 
96 				foreach (v; configs)
97 					findConfigsRec(TreeNode(ch.pack, v), parent_unique && configs.length == 1);
98 			}
99 		}
100 		findConfigsRec(root, true);
101 
102 		// prepend an invalid configuration to denote an unchosen dependency
103 		// this is used to properly support optional dependencies (when
104 		// getChildren() returns no configurations for an optional dependency,
105 		// but getAllConfigs() has already provided an existing list of configs)
106 		foreach (ref cfgs; all_configs) cfgs = CONFIG.invalid ~ cfgs;
107 
108 		logDebug("Configurations used for dependency resolution:");
109 		foreach (n, i; package_indices) logDebug("  %s (%s): %s", n, i, all_configs[i]);
110 
111 		auto config_indices = new size_t[all_configs.length];
112 		config_indices[] = 0;
113 
114 		string last_error;
115 
116 		visited = null;
117 		sizediff_t validateConfigs(TreeNode parent, ref string error)
118 		{
119 			import std.algorithm : max;
120 
121 			if (parent in visited) return -1;
122 
123 			visited[parent] = true;
124 			sizediff_t maxcpi = -1;
125 			sizediff_t parentidx = package_indices.get(rootPackage(parent.pack), -1);
126 			auto parentbase = rootPackage(parent.pack);
127 
128 			// loop over all dependencies
129 			foreach (ch; getChildren(parent)) {
130 				auto basepack = rootPackage(ch.pack);
131 				assert(basepack in package_indices, format("%s not in packages %s", basepack, package_indices));
132 
133 				// get the current config/version of the current dependency
134 				sizediff_t childidx = package_indices[basepack];
135 				if (all_configs[childidx] == [CONFIG.invalid]) {
136 					enforce(parentbase != root_base_pack, format("Root package %s contains reference to invalid package %s", parent.pack, ch.pack));
137 					// choose another parent config to avoid the invalid child
138 					if (parentidx > maxcpi) {
139 						error = format("Package %s contains invalid dependency %s", parent.pack, ch.pack);
140 						logDiagnostic("%s (ci=%s)", error, parentidx);
141 						maxcpi = parentidx;
142 					}
143 				} else {
144 					auto config = all_configs[childidx][config_indices[childidx]];
145 					auto chnode = TreeNode(ch.pack, config);
146 
147 					if (config == CONFIG.invalid || !matches(ch.configs, config)) {
148 						// if we are at the root level, we can safely skip the maxcpi computation and instead choose another childidx config
149 						if (parentbase == root_base_pack) {
150 							error = format("No match for dependency %s %s of %s", ch.pack, ch.configs, parent.pack);
151 							return childidx;
152 						}
153 
154 						if (childidx > maxcpi) {
155 							maxcpi = max(childidx, parentidx);
156 							error = format("Dependency %s -> %s %s mismatches with selected version %s", parent.pack, ch.pack, ch.configs, config);
157 							logDebug("%s (ci=%s)", error, maxcpi);
158 						}
159 
160 						// we know that either the child or the parent needs to be switched
161 						// to another configuration, no need to continue with other children
162 						if (config == CONFIG.invalid) break;
163 					}
164 
165 					maxcpi = max(maxcpi, validateConfigs(chnode, error));
166 				}
167 			}
168 			return maxcpi;
169 		}
170 
171 		string first_error;
172 
173 		while (true) {
174 			// check if the current combination of configurations works out
175 			visited = null;
176 			string error;
177 			auto conflict_index = validateConfigs(root, error);
178 			if (!first_error) first_error = error;
179 
180 			// print out current iteration state
181 			logDebug("Interation (ci=%s) %s", conflict_index, {
182 				import std.array : join;
183 				auto cs = new string[all_configs.length];
184 				foreach (p, i; package_indices) {
185 					if (all_configs[i].length)
186 						cs[i] = p~" "~all_configs[i][config_indices[i]].to!string~(i >= 0 && i >= conflict_index ? " (C)" : "");
187 					else cs[i] = p ~ " [no config]";
188 				}
189 				return cs.join(", ");
190 			}());
191 
192 			if (conflict_index < 0) {
193 				CONFIG[string] ret;
194 				foreach (p, i; package_indices)
195 					if (all_configs[i].length) {
196 						auto cfg = all_configs[i][config_indices[i]];
197 						if (cfg != CONFIG.invalid) ret[p] = cfg;
198 					}
199 				return ret;
200 			}
201 
202 			// find the next combination of configurations
203 			foreach_reverse (pi, ref i; config_indices) {
204 				if (pi > conflict_index) i = 0;
205 				else if (++i >= all_configs[pi].length) i = 0;
206 				else break;
207 			}
208 			if (config_indices.all!"a==0") {
209 				if (throw_on_failure) throw new Exception("Could not find a valid dependency tree configuration: "~first_error);
210 				else return null;
211 			}
212 		}
213 	}
214 
215 	protected abstract CONFIG[] getAllConfigs(string pack);
216 	protected abstract CONFIG[] getSpecificConfigs(TreeNodes nodes);
217 	protected abstract TreeNodes[] getChildren(TreeNode node);
218 	protected abstract bool matches(CONFIGS configs, CONFIG config);
219 }
220 
221 
222 unittest {
223 	static struct IntConfig {
224 		int value;
225 		alias value this;
226 		enum invalid = IntConfig(-1);
227 	}
228 	static IntConfig ic(int v) { return IntConfig(v); }
229 
230 	static class TestResolver : DependencyResolver!(IntConfig[], IntConfig) {
231 		private TreeNodes[][string] m_children;
232 		this(TreeNodes[][string] children) { m_children = children; }
233 		protected override IntConfig[] getAllConfigs(string pack) {
234 			auto ret = appender!(IntConfig[]);
235 			foreach (p; m_children.byKey) {
236 				if (p.length <= pack.length+1) continue;
237 				if (p[0 .. pack.length] != pack || p[pack.length] != ':') continue;
238 				auto didx = p.lastIndexOf(':');
239 				ret ~= ic(p[didx+1 .. $].to!uint);
240 			}
241 			ret.data.sort!"a>b"();
242 			return ret.data;
243 		}
244 		protected override IntConfig[] getSpecificConfigs(TreeNodes nodes) { return null; }
245 		protected override TreeNodes[] getChildren(TreeNode node) { return m_children.get(node.pack ~ ":" ~ node.config.to!string(), null); }
246 		protected override bool matches(IntConfig[] configs, IntConfig config) { return configs.canFind(config); }
247 	}
248 
249 	// properly back up if conflicts are detected along the way (d:2 vs d:1)
250 	with (TestResolver) {
251 		auto res = new TestResolver([
252 			"a:0": [TreeNodes("b", [ic(2), ic(1)]), TreeNodes("d", [ic(1)]), TreeNodes("e", [ic(2), ic(1)])],
253 			"b:1": [TreeNodes("c", [ic(2), ic(1)]), TreeNodes("d", [ic(1)])],
254 			"b:2": [TreeNodes("c", [ic(3), ic(2)]), TreeNodes("d", [ic(2), ic(1)])],
255 			"c:1": [], "c:2": [], "c:3": [],
256 			"d:1": [], "d:2": [],
257 			"e:1": [], "e:2": [],
258 		]);
259 		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)))));
260 	}
261 
262 	// handle cyclic dependencies gracefully
263 	with (TestResolver) {
264 		auto res = new TestResolver([
265 			"a:0": [TreeNodes("b", [ic(1)])],
266 			"b:1": [TreeNodes("b", [ic(1)])]
267 		]);
268 		assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)]);
269 	}
270 }