Newer
Older
dub_jkp / source / dub / dependencyresolver.d
  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.vibecompat.core.log;
  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.typecons : Nullable;
  18. import std.string : format, indexOf, lastIndexOf;
  19.  
  20.  
  21. /** Resolves dependency graph with multiple configurations per package.
  22.  
  23. The term "configuration" can mean any kind of alternative dependency
  24. configuration of a package. In particular, it can mean different
  25. versions of a package.
  26.  
  27. `CONFIG` is an abstract type denoting a single configuration of a certain
  28. package, whereas `CONFIGS` denotes a set of configurations. The
  29. representation of both can be freely chosen, so that `CONFIGS` for example
  30. can be defined in terms of a version range.
  31. */
  32. class DependencyResolver(CONFIGS, CONFIG) {
  33. /** Encapsulates a list of outgoing edges in the dependency graph.
  34.  
  35. A value of this type represents a single dependency with multiple
  36. possible configurations for the target package.
  37. */
  38. static struct TreeNodes {
  39. string pack;
  40. CONFIGS configs;
  41. DependencyType depType = DependencyType.required;
  42.  
  43. size_t toHash() const nothrow @trusted {
  44. size_t ret = typeid(string).getHash(&pack);
  45. ret ^= typeid(CONFIGS).getHash(&configs);
  46. return ret;
  47. }
  48. bool opEqual(in ref TreeNodes other) const { return pack == other.pack && configs == other.configs; }
  49. int opCmp(in ref TreeNodes other) const {
  50. if (pack != other.pack) return pack < other.pack ? -1 : 1;
  51. if (configs != other.configs) return configs < other.configs ? -1 : 1;
  52. return 0;
  53. }
  54. }
  55.  
  56. /** A single node in the dependency graph.
  57.  
  58. Nodes are a combination of a package and a single package configuration.
  59. */
  60. static struct TreeNode {
  61. string pack;
  62. CONFIG config;
  63.  
  64. size_t toHash() const nothrow @trusted {
  65. size_t ret = pack.hashOf();
  66. ret ^= typeid(CONFIG).getHash(&config);
  67. return ret;
  68. }
  69. bool opEqual(in ref TreeNode other) const { return pack == other.pack && config == other.config; }
  70. int opCmp(in ref TreeNode other) const {
  71. if (pack != other.pack) return pack < other.pack ? -1 : 1;
  72. if (config != other.config) return config < other.config ? -1 : 1;
  73. return 0;
  74. }
  75. }
  76.  
  77. CONFIG[string] resolve(TreeNode root, bool throw_on_failure = true)
  78. {
  79. // Leave the possibility to opt-out from the loop limit
  80. import std.process : environment;
  81. bool no_loop_limit = environment.get("DUB_NO_RESOLVE_LIMIT") !is null;
  82.  
  83. auto rootbase = root.pack.basePackageName;
  84.  
  85. // build up the dependency graph, eliminating as many configurations/
  86. // versions as possible
  87. ResolveContext context;
  88. context.configs[rootbase] = [ResolveConfig(root.config, true)];
  89. long loop_counter = no_loop_limit ? long.max : 1_000_000;
  90. constrain(root, context, loop_counter);
  91.  
  92. // remove any non-default optional dependencies
  93. purgeOptionalDependencies(root, context.result);
  94.  
  95. // the root package is implied by the `root` argument and will not be
  96. // returned explicitly
  97. context.result.remove(rootbase);
  98.  
  99. logDiagnostic("Dependency resolution result:");
  100. foreach (d; context.result.keys.sort())
  101. logDiagnostic(" %s: %s", d, context.result[d]);
  102.  
  103. return context.result;
  104. }
  105.  
  106. protected abstract CONFIG[] getAllConfigs(string pack);
  107. protected abstract CONFIG[] getSpecificConfigs(string pack, TreeNodes nodes);
  108. protected abstract TreeNodes[] getChildren(TreeNode node);
  109. protected abstract bool matches(CONFIGS configs, CONFIG config);
  110.  
  111. private static struct ResolveConfig {
  112. CONFIG config;
  113. bool included;
  114. }
  115.  
  116. private static struct ResolveContext {
  117. /** Contains all packages visited by the resolution process so far.
  118.  
  119. The key is the qualified name of the package (base + sub)
  120. */
  121. void[0][string] visited;
  122.  
  123. /// The finally chosen configurations for each package
  124. CONFIG[string] result;
  125.  
  126. /// The set of available configurations for each package
  127. ResolveConfig[][string] configs;
  128.  
  129. /// Determines if a certain package has already been processed
  130. bool isVisited(string package_) const { return (package_ in visited) !is null; }
  131.  
  132. /// Marks a package as processed
  133. void setVisited(string package_) { visited[package_] = (void[0]).init; }
  134.  
  135. /// Returns a deep clone
  136. ResolveContext clone()
  137. {
  138. ResolveContext ret;
  139. ret.visited = this.visited.dup;
  140. ret.result = this.result.dup;
  141. foreach (pack, cfgs; this.configs) {
  142. ret.configs[pack] = cfgs.dup;
  143. }
  144. return ret;
  145. }
  146. }
  147.  
  148.  
  149. /** Starting with a single node, fills `context` with a minimized set of
  150. configurations that form valid solutions.
  151. */
  152. private void constrain(TreeNode n, ref ResolveContext context, ref long max_iterations)
  153. {
  154. auto base = n.pack.basePackageName;
  155. assert(base in context.configs);
  156. if (context.isVisited(n.pack)) return;
  157. context.setVisited(n.pack);
  158. context.result[base] = n.config;
  159. foreach (j, ref sc; context.configs[base])
  160. sc.included = sc.config == n.config;
  161.  
  162. auto dependencies = getChildren(n);
  163.  
  164. foreach (dep; dependencies) {
  165. // lazily load all dependency configurations
  166. auto depbase = dep.pack.basePackageName;
  167. auto di = depbase in context.configs;
  168. if (!di) {
  169. context.configs[depbase] =
  170. getAllConfigs(depbase)
  171. .map!(c => ResolveConfig(c, true))
  172. .array;
  173. di = depbase in context.configs;
  174. }
  175.  
  176. // add any dependee defined dependency configurations
  177. foreach (sc; getSpecificConfigs(n.pack, dep))
  178. if (!(*di).canFind!(c => c.config == sc))
  179. *di = ResolveConfig(sc, true) ~ *di;
  180.  
  181. // restrain the configurations to the current dependency spec
  182. bool any_config = false;
  183. foreach (i, ref c; *di)
  184. if (c.included) {
  185. if (!matches(dep.configs, c.config))
  186. c.included = false;
  187. else any_config = true;
  188. }
  189.  
  190. if (!any_config && dep.depType == DependencyType.required) {
  191. if ((*di).length)
  192. throw new ResolveException(n, dep, context);
  193. else throw new DependencyLoadException(n, dep);
  194. }
  195. }
  196.  
  197. constrainDependencies(n, dependencies, 0, context, max_iterations);
  198. }
  199.  
  200. /** Recurses back into `constrain` while recursively going through `n`'s
  201. dependencies.
  202.  
  203. This attempts to constrain each dependency, while keeping each of them
  204. in a nested stack frame. This allows any errors to properly back
  205. propagate.
  206. */
  207. private void constrainDependencies(TreeNode n, TreeNodes[] dependencies, size_t depidx,
  208. ref ResolveContext context, ref long max_iterations)
  209. {
  210. if (depidx >= dependencies.length) return;
  211.  
  212. assert (--max_iterations > 0,
  213. "The dependency resolution process is taking too long. The"
  214. ~ " dependency graph is likely hitting a pathological case in"
  215. ~ " the resolution algorithm. Please file a bug report at"
  216. ~ " https://github.com/dlang/dub/issues and mention the package"
  217. ~ " recipe that reproduces this error.");
  218.  
  219. auto dep = &dependencies[depidx];
  220. auto depbase = dep.pack.basePackageName;
  221. auto depconfigs = context.configs[depbase];
  222.  
  223. Exception first_err;
  224.  
  225. // try each configuration/version of the current dependency
  226. foreach (i, c; depconfigs) {
  227. if (c.included) {
  228. try {
  229. // try the configuration on a cloned context
  230. auto subcontext = context.clone;
  231. constrain(TreeNode(dep.pack, c.config), subcontext, max_iterations);
  232. constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations);
  233. // if a branch succeeded, replace the current context
  234. // with the one from the branch and return
  235. context = subcontext;
  236. return;
  237. } catch (Exception e) {
  238. if (!first_err) first_err = e;
  239. }
  240. }
  241. }
  242.  
  243. // ignore unsatisfiable optional dependencies
  244. if (dep.depType != DependencyType.required) {
  245. auto subcontext = context.clone;
  246. constrainDependencies(n, dependencies, depidx+1, subcontext, max_iterations);
  247. context = subcontext;
  248. return;
  249. }
  250.  
  251. // report the first error encountered to the user
  252. if (first_err) throw first_err;
  253.  
  254. // should have thrown in constrainRec before reaching this
  255. assert(false, format("Got no configuration for dependency %s %s of %s %s!?",
  256. dep.pack, dep.configs, n.pack, n.config));
  257. }
  258.  
  259. private void purgeOptionalDependencies(TreeNode root, ref CONFIG[string] configs)
  260. {
  261. bool[string] required;
  262. bool[string] visited;
  263.  
  264. void markRecursively(TreeNode node)
  265. {
  266. if (node.pack in visited) return;
  267. visited[node.pack] = true;
  268. required[node.pack.basePackageName] = true;
  269. foreach (dep; getChildren(node).filter!(dep => dep.depType != DependencyType.optional))
  270. if (auto dp = dep.pack.basePackageName in configs)
  271. markRecursively(TreeNode(dep.pack, *dp));
  272. }
  273.  
  274. // recursively mark all required dependencies of the concrete dependency tree
  275. markRecursively(root);
  276.  
  277. // remove all un-marked configurations
  278. foreach (p; configs.keys.dup)
  279. if (p !in required)
  280. configs.remove(p);
  281. }
  282.  
  283. final class ResolveException : Exception {
  284. import std.range : chain, only;
  285. import std.typecons : tuple;
  286.  
  287. string failedNode;
  288.  
  289. this(TreeNode parent, TreeNodes dep, in ref ResolveContext context, string file = __FILE__, size_t line = __LINE__)
  290. {
  291. auto m = format("Unresolvable dependencies to package %s:", dep.pack.basePackageName);
  292. super(m, file, line);
  293.  
  294. this.failedNode = dep.pack;
  295.  
  296. auto failbase = failedNode.basePackageName;
  297.  
  298. // get the list of all dependencies to the failed package
  299. auto deps = context.visited.byKey
  300. .filter!(p => p.basePackageName in context.result)
  301. .map!(p => TreeNode(p, context.result[p.basePackageName]))
  302. .map!(n => getChildren(n)
  303. .filter!(d => d.pack.basePackageName == failbase)
  304. .map!(d => tuple(n, d))
  305. )
  306. .join
  307. .sort!((a, b) => a[0].pack < b[0].pack);
  308.  
  309. foreach (d; deps) {
  310. // filter out trivial self-dependencies
  311. if (d[0].pack.basePackageName == failbase
  312. && matches(d[1].configs, d[0].config))
  313. continue;
  314. msg ~= format("\n %s %s depends on %s %s", d[0].pack, d[0].config, d[1].pack, d[1].configs);
  315. }
  316. }
  317. }
  318.  
  319. final class DependencyLoadException : Exception {
  320. TreeNode parent;
  321. TreeNodes dependency;
  322.  
  323. this(TreeNode parent, TreeNodes dep)
  324. {
  325. auto m = format("Failed to find any versions for package %s, referenced by %s %s",
  326. dep.pack, parent.pack, parent.config);
  327. super(m, file, line);
  328.  
  329. this.parent = parent;
  330. this.dependency = dep;
  331. }
  332. }
  333. }
  334.  
  335. enum DependencyType {
  336. required,
  337. optionalDefault,
  338. optional
  339. }
  340.  
  341. private string basePackageName(string p)
  342. {
  343. import std.algorithm.searching : findSplit;
  344. return p.findSplit(":")[0];
  345. }
  346.  
  347. unittest {
  348. static struct IntConfig {
  349. int value;
  350. alias value this;
  351. enum invalid = IntConfig(-1);
  352. }
  353. static IntConfig ic(int v) { return IntConfig(v); }
  354. static struct IntConfigs {
  355. IntConfig[] configs;
  356. alias configs this;
  357. }
  358. static IntConfigs ics(IntConfig[] cfgs) { return IntConfigs(cfgs); }
  359.  
  360. static class TestResolver : DependencyResolver!(IntConfigs, IntConfig) {
  361. private TreeNodes[][string] m_children;
  362. this(TreeNodes[][string] children) { m_children = children; }
  363. protected override IntConfig[] getAllConfigs(string pack) {
  364. auto ret = appender!(IntConfig[]);
  365. foreach (p; m_children.byKey) {
  366. if (p.length <= pack.length+1) continue;
  367. if (p[0 .. pack.length] != pack || p[pack.length] != ':') continue;
  368. auto didx = p.lastIndexOf(':');
  369. ret ~= ic(p[didx+1 .. $].to!uint);
  370. }
  371. ret.data.sort!"a>b"();
  372. return ret.data;
  373. }
  374. protected override IntConfig[] getSpecificConfigs(string pack, TreeNodes nodes) { return null; }
  375. protected override TreeNodes[] getChildren(TreeNode node) { return m_children.get(node.pack ~ ":" ~ node.config.to!string(), null); }
  376. protected override bool matches(IntConfigs configs, IntConfig config) { return configs.canFind(config); }
  377. }
  378.  
  379. // properly back up if conflicts are detected along the way (d:2 vs d:1)
  380. with (TestResolver) {
  381. auto res = new TestResolver([
  382. "a:0": [TreeNodes("b", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)])), TreeNodes("e", ics([ic(2), ic(1)]))],
  383. "b:1": [TreeNodes("c", ics([ic(2), ic(1)])), TreeNodes("d", ics([ic(1)]))],
  384. "b:2": [TreeNodes("c", ics([ic(3), ic(2)])), TreeNodes("d", ics([ic(2), ic(1)]))],
  385. "c:1": [], "c:2": [], "c:3": [],
  386. "d:1": [], "d:2": [],
  387. "e:1": [], "e:2": [],
  388. ]);
  389. 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)))));
  390. }
  391.  
  392. // handle cyclic dependencies gracefully
  393. with (TestResolver) {
  394. auto res = new TestResolver([
  395. "a:0": [TreeNodes("b", ics([ic(1)]))],
  396. "b:1": [TreeNodes("b", ics([ic(1)]))]
  397. ]);
  398. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)]);
  399. }
  400.  
  401. // don't choose optional dependencies by default
  402. with (TestResolver) {
  403. auto res = new TestResolver([
  404. "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)],
  405. "b:1": []
  406. ]);
  407. assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0)))));
  408. }
  409.  
  410. // choose default optional dependencies by default
  411. with (TestResolver) {
  412. auto res = new TestResolver([
  413. "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optionalDefault)],
  414. "b:1": []
  415. ]);
  416. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1)], to!string(res.resolve(TreeNode("a", ic(0)))));
  417. }
  418.  
  419. // choose optional dependency if non-optional within the dependency tree
  420. with (TestResolver) {
  421. auto res = new TestResolver([
  422. "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional), TreeNodes("c", ics([ic(1)]))],
  423. "b:1": [],
  424. "c:1": [TreeNodes("b", ics([ic(1)]))]
  425. ]);
  426. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(1), "c":ic(1)], to!string(res.resolve(TreeNode("a", ic(0)))));
  427. }
  428.  
  429. // don't choose optional dependency if non-optional outside of final dependency tree
  430. with (TestResolver) {
  431. auto res = new TestResolver([
  432. "a:0": [TreeNodes("b", ics([ic(1)]), DependencyType.optional)],
  433. "b:1": [],
  434. "preset:0": [TreeNodes("b", ics([ic(1)]))]
  435. ]);
  436. assert(res.resolve(TreeNode("a", ic(0))).length == 0, to!string(res.resolve(TreeNode("a", ic(0)))));
  437. }
  438.  
  439. // don't choose optional dependency if non-optional in a non-selected version
  440. with (TestResolver) {
  441. auto res = new TestResolver([
  442. "a:0": [TreeNodes("b", ics([ic(1), ic(2)]))],
  443. "b:1": [TreeNodes("c", ics([ic(1)]))],
  444. "b:2": [TreeNodes("c", ics([ic(1)]), DependencyType.optional)],
  445. "c:1": []
  446. ]);
  447. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0)))));
  448. }
  449.  
  450. // make sure non-satisfiable dependencies are not a problem, even if non-optional in some dependencies
  451. with (TestResolver) {
  452. auto res = new TestResolver([
  453. "a:0": [TreeNodes("b", ics([ic(1), ic(2)]))],
  454. "b:1": [TreeNodes("c", ics([ic(2)]))],
  455. "b:2": [TreeNodes("c", ics([ic(2)]), DependencyType.optional)],
  456. "c:1": []
  457. ]);
  458. assert(res.resolve(TreeNode("a", ic(0))) == ["b":ic(2)], to!string(res.resolve(TreeNode("a", ic(0)))));
  459. }
  460.  
  461. // check error message for multiple conflicting dependencies
  462. with (TestResolver) {
  463. auto res = new TestResolver([
  464. "a:0": [TreeNodes("b", ics([ic(1)])), TreeNodes("c", ics([ic(1)]))],
  465. "b:1": [TreeNodes("d", ics([ic(1)]))],
  466. "c:1": [TreeNodes("d", ics([ic(2)]))],
  467. "d:1": [],
  468. "d:2": []
  469. ]);
  470. try {
  471. res.resolve(TreeNode("a", ic(0)));
  472. assert(false, "Expected resolve to throw.");
  473. } catch (ResolveException e) {
  474. assert(e.msg ==
  475. "Unresolvable dependencies to package d:"
  476. ~ "\n b 1 depends on d [1]"
  477. ~ "\n c 1 depends on d [2]");
  478. }
  479. }
  480.  
  481. // check error message for invalid dependency
  482. with (TestResolver) {
  483. auto res = new TestResolver([
  484. "a:0": [TreeNodes("b", ics([ic(1)]))]
  485. ]);
  486. try {
  487. res.resolve(TreeNode("a", ic(0)));
  488. assert(false, "Expected resolve to throw.");
  489. } catch (DependencyLoadException e) {
  490. assert(e.msg == "Failed to find any versions for package b, referenced by a 0");
  491. }
  492. }
  493.  
  494. // regression: unresolvable optional dependency skips the remaining dependencies
  495. with (TestResolver) {
  496. auto res = new TestResolver([
  497. "a:0": [
  498. TreeNodes("b", ics([ic(2)]), DependencyType.optional),
  499. TreeNodes("c", ics([ic(1)]))
  500. ],
  501. "b:1": [],
  502. "c:1": []
  503. ]);
  504. assert(res.resolve(TreeNode("a", ic(0))) == ["c":ic(1)]);
  505. }
  506. }