Newer
Older
dub_jkp / source / dub / dependencyresolver.d
@Sönke Ludwig Sönke Ludwig on 26 Apr 2016 13 KB Fix second failing case of #803.
  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, map, 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. DependencyType depType = DependencyType.required;
  25.  
  26. hash_t toHash() const nothrow @trusted {
  27. size_t ret = typeid(string).getHash(&pack);
  28. ret ^= typeid(CONFIGS).getHash(&configs);
  29. return ret;
  30. }
  31. bool opEqual(in ref TreeNodes other) const { return pack == other.pack && configs == other.configs; }
  32. int opCmp(in ref TreeNodes other) const {
  33. if (pack != other.pack) return pack < other.pack ? -1 : 1;
  34. if (configs != other.configs) return configs < other.configs ? -1 : 1;
  35. return 0;
  36. }
  37. }
  38.  
  39. static struct TreeNode {
  40. string pack;
  41. CONFIG config;
  42.  
  43. hash_t toHash() const nothrow @trusted {
  44. size_t ret = typeid(string).getHash(&pack);
  45. ret ^= typeid(CONFIG).getHash(&config);
  46. return ret;
  47. }
  48. bool opEqual(in ref TreeNode other) const { return pack == other.pack && config == other.config; }
  49. int opCmp(in ref TreeNode other) const {
  50. if (pack != other.pack) return pack < other.pack ? -1 : 1;
  51. if (config != other.config) return config < other.config ? -1 : 1;
  52. return 0;
  53. }
  54. }
  55.  
  56. CONFIG[string] resolve(TreeNode root, bool throw_on_failure = true)
  57. {
  58. auto root_base_pack = basePackage(root.pack);
  59.  
  60. // find all possible configurations of each possible dependency
  61. size_t[string] package_indices;
  62. string[size_t] package_names;
  63. CONFIG[][] all_configs;
  64. bool[] any_config;
  65. bool[string] maybe_optional_deps;
  66. bool[TreeNode] visited;
  67.  
  68. void findConfigsRec(TreeNode parent, bool parent_unique)
  69. {
  70. if (parent in visited) return;
  71. visited[parent] = true;
  72.  
  73. foreach (ch; getChildren(parent)) {
  74. auto basepack = basePackage(ch.pack);
  75. auto pidx = all_configs.length;
  76.  
  77. if (ch.depType != DependencyType.required) maybe_optional_deps[ch.pack] = true;
  78.  
  79. CONFIG[] configs;
  80. if (auto pi = basepack in package_indices) {
  81. pidx = *pi;
  82. configs = all_configs[*pi];
  83. } else {
  84. if (basepack == root_base_pack) configs = [root.config];
  85. else configs = getAllConfigs(basepack);
  86. all_configs ~= configs;
  87. package_indices[basepack] = pidx;
  88. package_names[pidx] = basepack;
  89. }
  90.  
  91. foreach (c; getSpecificConfigs(basepack, ch))
  92. if (!configs.canFind(c))
  93. configs = c ~ configs;
  94.  
  95. if (any_config.length <= pidx) any_config.length = pidx+1;
  96. any_config[pidx] = configs.length > 0;
  97.  
  98. // eliminate configurations from which we know that they can't satisfy
  99. // the uniquely defined root dependencies (==version or ~branch style dependencies)
  100. if (parent_unique) configs = configs.filter!(c => matches(ch.configs, c)).array;
  101.  
  102. all_configs[pidx] = configs;
  103.  
  104. foreach (v; configs)
  105. findConfigsRec(TreeNode(ch.pack, v), parent_unique && configs.length == 1);
  106. }
  107. }
  108. findConfigsRec(root, true);
  109.  
  110. // append an invalid configuration to denote an unchosen dependency
  111. // this is used to properly support optional dependencies (when
  112. // getChildren() returns no configurations for an optional dependency,
  113. // but getAllConfigs() has already provided an existing list of configs)
  114. foreach (i, ref cfgs; all_configs)
  115. if (cfgs.length == 0 || package_names[i] in maybe_optional_deps)
  116. cfgs = cfgs ~ CONFIG.invalid;
  117.  
  118. logDebug("Configurations used for dependency resolution:");
  119. foreach (n, i; package_indices) logDebug(" %s (%s%s): %s", n, i, n in maybe_optional_deps ? ", maybe optional" : ", required", all_configs[i]);
  120.  
  121. auto config_indices = new size_t[all_configs.length];
  122. config_indices[] = 0;
  123.  
  124. string last_error;
  125.  
  126. visited = null;
  127. sizediff_t validateConfigs(TreeNode parent, ref string error)
  128. {
  129. import std.algorithm : max;
  130.  
  131. if (parent in visited) return -1;
  132.  
  133. visited[parent] = true;
  134. sizediff_t maxcpi = -1;
  135. sizediff_t parentidx = package_indices.get(basePackage(parent.pack), -1);
  136. auto parentbase = basePackage(parent.pack);
  137.  
  138. // loop over all dependencies
  139. foreach (ch; getChildren(parent)) {
  140. auto basepack = basePackage(ch.pack);
  141. assert(basepack in package_indices, format("%s not in packages %s", basepack, package_indices));
  142.  
  143. // get the current config/version of the current dependency
  144. sizediff_t childidx = package_indices[basepack];
  145. if (all_configs[childidx] == [CONFIG.invalid]) {
  146. // ignore invalid optional dependencies
  147. if (ch.depType != DependencyType.required)
  148. continue;
  149.  
  150. if (parentbase == root_base_pack) {
  151. import std.uni : toLower;
  152. auto lp = ch.pack.toLower();
  153. if (lp != ch.pack) {
  154. logError("Dependency \"%s\" of %s contains upper case letters, but must be lower case.", ch.pack, parent.pack);
  155. if (getAllConfigs(lp).length) logError("Did you mean \"%s\"?", lp);
  156. }
  157. bool pvalid = any_config[childidx];
  158. if (pvalid)
  159. throw new Exception(format("Root package %s reference %s %s cannot be satisfied.", parent.pack, ch.pack, ch.configs));
  160. else
  161. throw new Exception(format("Root package %s references unknown package %s", parent.pack, ch.pack));
  162. }
  163. // choose another parent config to avoid the invalid child
  164. if (parentidx > maxcpi) {
  165. error = format("Package %s contains invalid dependency %s", parent.pack, ch.pack);
  166. logDiagnostic("%s (ci=%s)", error, parentidx);
  167. maxcpi = parentidx;
  168. }
  169. } else {
  170. auto config = all_configs[childidx][config_indices[childidx]];
  171. auto chnode = TreeNode(ch.pack, config);
  172.  
  173. if (config == CONFIG.invalid || !matches(ch.configs, config)) {
  174. // ignore missing optional dependencies
  175. if (config == CONFIG.invalid && ch.depType != DependencyType.required)
  176. continue;
  177.  
  178. // if we are at the root level, we can safely skip the maxcpi computation and instead choose another childidx config
  179. if (parentbase == root_base_pack) {
  180. error = format("No match for dependency %s %s of %s", ch.pack, ch.configs, parent.pack);
  181. return childidx;
  182. }
  183.  
  184. if (childidx > maxcpi) {
  185. maxcpi = max(childidx, parentidx);
  186. error = format("Dependency %s -> %s %s mismatches with selected version %s", parent.pack, ch.pack, ch.configs, config);
  187. logDebug("%s (ci=%s)", error, maxcpi);
  188. }
  189.  
  190. // we know that either the child or the parent needs to be switched
  191. // to another configuration, no need to continue with other children
  192. if (config == CONFIG.invalid) break;
  193. }
  194.  
  195. maxcpi = max(maxcpi, validateConfigs(chnode, error));
  196. }
  197. }
  198. return maxcpi;
  199. }
  200.  
  201. string first_error;
  202.  
  203. while (true) {
  204. // check if the current combination of configurations works out
  205. visited = null;
  206. string error;
  207. auto conflict_index = validateConfigs(root, error);
  208. if (first_error !is null) first_error = error;
  209.  
  210. // print out current iteration state
  211. logDebug("Interation (ci=%s) %s", conflict_index, {
  212. import std.array : join;
  213. auto cs = new string[all_configs.length];
  214. foreach (p, i; package_indices) {
  215. if (all_configs[i].length)
  216. cs[i] = p~" "~all_configs[i][config_indices[i]].to!string~(i >= 0 && i >= conflict_index ? " (C)" : "");
  217. else cs[i] = p ~ " [no config]";
  218. }
  219. return cs.join(", ");
  220. }());
  221.  
  222. if (conflict_index < 0) {
  223. CONFIG[string] ret;
  224. foreach (p, i; package_indices)
  225. if (all_configs[i].length) {
  226. auto cfg = all_configs[i][config_indices[i]];
  227. if (cfg != CONFIG.invalid) ret[p] = cfg;
  228. }
  229. logDebug("Resolved dependencies before optional-purge: %s", ret.byKey.map!(k => k~" "~ret[k].to!string));
  230. purgeOptionalDependencies(root, ret);
  231. logDebug("Resolved dependencies after optional-purge: %s", ret.byKey.map!(k => k~" "~ret[k].to!string));
  232. return ret;
  233. }
  234.  
  235. // find the next combination of configurations
  236. foreach_reverse (pi, ref i; config_indices) {
  237. if (pi > conflict_index) i = 0;
  238. else if (++i >= all_configs[pi].length) i = 0;
  239. else break;
  240. }
  241. if (config_indices.all!"a==0") {
  242. if (throw_on_failure) throw new Exception("Could not find a valid dependency tree configuration: "~first_error);
  243. else return null;
  244. }
  245. }
  246. }
  247.  
  248. protected abstract CONFIG[] getAllConfigs(string pack);
  249. protected abstract CONFIG[] getSpecificConfigs(string pack, TreeNodes nodes);
  250. protected abstract TreeNodes[] getChildren(TreeNode node);
  251. protected abstract bool matches(CONFIGS configs, CONFIG config);
  252.  
  253. private void purgeOptionalDependencies(TreeNode root, ref CONFIG[string] configs)
  254. {
  255. bool[string] required;
  256.  
  257. void markRecursively(TreeNode node)
  258. {
  259. if (node.pack in required) return;
  260. required[basePackage(node.pack)] = true;
  261. foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional))
  262. if (auto dp = basePackage(dep.pack) in configs)
  263. markRecursively(TreeNode(dep.pack, *dp));
  264. }
  265.  
  266. // recursively mark all required dependencies of the concrete dependency tree
  267. markRecursively(root);
  268.  
  269. // remove all un-marked configurations
  270. foreach (p; configs.keys.dup)
  271. if (p !in required)
  272. configs.remove(p);
  273. }
  274. }
  275.  
  276. enum DependencyType {
  277. required,
  278. optionalDefault,
  279. optional
  280. }
  281.  
  282. private string basePackage(string p)
  283. {
  284. auto idx = indexOf(p, ":");
  285. if (idx < 0) return p;
  286. return p[0 .. idx];
  287. }
  288.  
  289.  
  290. unittest {
  291. static struct IntConfig {
  292. int value;
  293. alias value this;
  294. enum invalid = IntConfig(-1);
  295. }
  296. static IntConfig ic(int v) { return IntConfig(v); }
  297. static struct IntConfigs {
  298. IntConfig[] configs;
  299. alias configs this;
  300. }
  301. static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); }
  302.  
  303. static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) {
  304. private TreeNodes[][string] m_children;
  305. this(TreeNodes[][string] children) { m_children = children; }
  306. protected override IntConfig[] getAllConfigs(string pack) {
  307. auto ret = appender!(IntConfig[]);
  308. foreach (p; m_children.byKey) {
  309. if (p.length <= pack.length+1) continue;
  310. if (p[0 .. pack.length] != pack || p[pack.length] != ':') continue;
  311. auto didx = p.lastIndexOf(':');
  312. ret ~= ic(p[didx+1 .. $].to!uint);
  313. }
  314. ret.data.sort!"a>b"();
  315. return ret.data;
  316. }
  317. protected override IntConfig[] getSpecificConfigs(string pack, TreeNodes nodes) { return null; }
  318. protected override TreeNodes[] getChildren(TreeNode node) { return m_children.get(node.pack ~ ":" ~ node.config.to!string(), null); }
  319. protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); }
  320. }
  321.  
  322. // properly back up if conflicts are detected along the way (d:2 vs d:1)
  323. with (TestResolver) {
  324. auto res = new TestResolver([
  325. "a:0": [TreeNodes("b", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)])), TreeNodes("e", ics([ic(2), ic(1)]))],
  326. "b:1": [TreeNodes("c", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)]))],
  327. "b:2": [TreeNodes("c", ics([ic(3), ic(2)])), TreeNodes("d", ics([ic(2), ic(1)]))],
  328. "c:1": [], "c:2": [], "c:3": [],
  329. "d:1": [], "d:2": [],
  330. "e:1": [], "e:2": [],
  331. ]);
  332. 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)))));
  333. }
  334.  
  335. // handle cyclic dependencies gracefully
  336. with (TestResolver) {
  337. auto res = new TestResolver([
  338. "a:0": [TreeNodes("b", ics([ic(1)]))],
  339. "b:1": [TreeNodes("b", ics([ic(1)]))]
  340. ]);
  341. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)]);
  342. }
  343.  
  344. // don't choose optional dependencies by default
  345. with (TestResolver) {
  346. auto res = new TestResolver([
  347. "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)],
  348. "b:1": []
  349. ]);
  350. assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0)))));
  351. }
  352.  
  353. // choose default optional dependencies by default
  354. with (TestResolver) {
  355. auto res = new TestResolver([
  356. "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optionalDefault)],
  357. "b:1": []
  358. ]);
  359. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)], to!string(res.resolve(TreeNode("a", ic(0)))));
  360. }
  361.  
  362. // choose optional dependency if non-optional within the dependency tree
  363. with (TestResolver) {
  364. auto res = new TestResolver([
  365. "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional), TreeNodes("c", ics([ic(1)]))],
  366. "b:1": [],
  367. "c:1": [TreeNodes("b", ics([ic(1)]))]
  368. ]);
  369. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1), "c":ic(1)], to!string(res.resolve(TreeNode("a", ic(0)))));
  370. }
  371.  
  372. // don't choose optional dependency if non-optional outside of final dependency tree
  373. with (TestResolver) {
  374. auto res = new TestResolver([
  375. "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)],
  376. "b:1": [],
  377. "preset:0": [TreeNodes("b", ics([ic(1)]))]
  378. ]);
  379. assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0)))));
  380. }
  381.  
  382. // don't choose optional dependency if non-optional in a non-selected version
  383. with (TestResolver) {
  384. auto res = new TestResolver([
  385. "a:0": [TreeNodes("b", ics([ic(1), ic(2)]))],
  386. "b:1": [TreeNodes("c", ics([ic(1)]))],
  387. "b:2": [TreeNodes("c", ics([ic(1)]), DependencyType.optional)],
  388. "c:1": []
  389. ]);
  390. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0)))));
  391. }
  392.  
  393. // make sure non-satisfyable dependencies are not a problem, even if non-optional in some dependencies
  394. with (TestResolver) {
  395. auto res = new TestResolver([
  396. "a:0": [TreeNodes("b", ics([ic(1), ic(2)]))],
  397. "b:1": [TreeNodes("c", ics([ic(2)]))],
  398. "b:2": [TreeNodes("c", ics([ic(2)]), DependencyType.optional)],
  399. "c:1": []
  400. ]);
  401. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0)))));
  402. }
  403. }