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