Newer
Older
dub_jkp / source / dub / dependencyresolver.d
  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 %s", parent.pack, ch.pack, ch.configs));
  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 !is null) 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. }