PageRenderTime 46ms CodeModel.GetById 16ms RepoModel.GetById 0ms app.codeStats 0ms

/src/Composer/DependencyResolver/Solver.php

https://github.com/bantu/composer
PHP | 780 lines | 530 code | 163 blank | 87 comment | 104 complexity | 79a60f2a3dada4b1a042ca477c3cd7b3 MD5 | raw file
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\DependencyResolver;
  12. use Composer\Repository\RepositoryInterface;
  13. /**
  14. * @author Nils Adermann <naderman@naderman.de>
  15. */
  16. class Solver
  17. {
  18. const BRANCH_LITERALS = 0;
  19. const BRANCH_LEVEL = 1;
  20. protected $policy;
  21. protected $pool;
  22. protected $installed;
  23. protected $rules;
  24. protected $ruleSetGenerator;
  25. protected $updateAll;
  26. protected $addedMap = array();
  27. protected $updateMap = array();
  28. protected $watchGraph;
  29. protected $decisions;
  30. protected $installedMap;
  31. protected $propagateIndex;
  32. protected $branches = array();
  33. protected $problems = array();
  34. protected $learnedPool = array();
  35. public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed)
  36. {
  37. $this->policy = $policy;
  38. $this->pool = $pool;
  39. $this->installed = $installed;
  40. $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool);
  41. }
  42. // aka solver_makeruledecisions
  43. private function makeAssertionRuleDecisions()
  44. {
  45. $decisionStart = count($this->decisions) - 1;
  46. $rulesCount = count($this->rules);
  47. for ($ruleIndex = 0; $ruleIndex < $rulesCount; $ruleIndex++) {
  48. $rule = $this->rules->ruleById($ruleIndex);
  49. if (!$rule->isAssertion() || $rule->isDisabled()) {
  50. continue;
  51. }
  52. $literals = $rule->getLiterals();
  53. $literal = $literals[0];
  54. if (!$this->decisions->decided(abs($literal))) {
  55. $this->decisions->decide($literal, 1, $rule);
  56. continue;
  57. }
  58. if ($this->decisions->satisfy($literal)) {
  59. continue;
  60. }
  61. // found a conflict
  62. if (RuleSet::TYPE_LEARNED === $rule->getType()) {
  63. $rule->disable();
  64. continue;
  65. }
  66. $conflict = $this->decisions->decisionRule($literal);
  67. if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) {
  68. $problem = new Problem($this->pool);
  69. $problem->addRule($rule);
  70. $problem->addRule($conflict);
  71. $this->disableProblem($rule);
  72. $this->problems[] = $problem;
  73. continue;
  74. }
  75. // conflict with another job
  76. $problem = new Problem($this->pool);
  77. $problem->addRule($rule);
  78. $problem->addRule($conflict);
  79. // push all of our rules (can only be job rules)
  80. // asserting this literal on the problem stack
  81. foreach ($this->rules->getIteratorFor(RuleSet::TYPE_JOB) as $assertRule) {
  82. if ($assertRule->isDisabled() || !$assertRule->isAssertion()) {
  83. continue;
  84. }
  85. $assertRuleLiterals = $assertRule->getLiterals();
  86. $assertRuleLiteral = $assertRuleLiterals[0];
  87. if (abs($literal) !== abs($assertRuleLiteral)) {
  88. continue;
  89. }
  90. $problem->addRule($assertRule);
  91. $this->disableProblem($assertRule);
  92. }
  93. $this->problems[] = $problem;
  94. $this->decisions->resetToOffset($decisionStart);
  95. $ruleIndex = -1;
  96. }
  97. }
  98. protected function setupInstalledMap()
  99. {
  100. $this->installedMap = array();
  101. foreach ($this->installed->getPackages() as $package) {
  102. $this->installedMap[$package->getId()] = $package;
  103. }
  104. foreach ($this->jobs as $job) {
  105. switch ($job['cmd']) {
  106. case 'update':
  107. $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
  108. foreach ($packages as $package) {
  109. if (isset($this->installedMap[$package->getId()])) {
  110. $this->updateMap[$package->getId()] = true;
  111. }
  112. }
  113. break;
  114. case 'update-all':
  115. foreach ($this->installedMap as $package) {
  116. $this->updateMap[$package->getId()] = true;
  117. }
  118. break;
  119. case 'install':
  120. if (!$this->pool->whatProvides($job['packageName'], $job['constraint'])) {
  121. $problem = new Problem($this->pool);
  122. $problem->addRule(new Rule($this->pool, array(), null, null, $job));
  123. $this->problems[] = $problem;
  124. }
  125. break;
  126. }
  127. }
  128. }
  129. public function solve(Request $request)
  130. {
  131. $this->jobs = $request->getJobs();
  132. $this->setupInstalledMap();
  133. $this->decisions = new Decisions($this->pool);
  134. $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap);
  135. $this->watchGraph = new RuleWatchGraph;
  136. foreach ($this->rules as $rule) {
  137. $this->watchGraph->insert(new RuleWatchNode($rule));
  138. }
  139. /* make decisions based on job/update assertions */
  140. $this->makeAssertionRuleDecisions();
  141. $this->runSat(true);
  142. // decide to remove everything that's installed and undecided
  143. foreach ($this->installedMap as $packageId => $void) {
  144. if ($this->decisions->undecided($packageId)) {
  145. $this->decisions->decide(-$packageId, 1, null);
  146. }
  147. }
  148. if ($this->problems) {
  149. throw new SolverProblemsException($this->problems, $this->installedMap);
  150. }
  151. $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions);
  152. return $transaction->getOperations();
  153. }
  154. protected function literalFromId($id)
  155. {
  156. $package = $this->pool->packageById(abs($id));
  157. return new Literal($package, $id > 0);
  158. }
  159. /**
  160. * Makes a decision and propagates it to all rules.
  161. *
  162. * Evaluates each term affected by the decision (linked through watches)
  163. * If we find unit rules we make new decisions based on them
  164. *
  165. * @param integer $level
  166. * @return Rule|null A rule on conflict, otherwise null.
  167. */
  168. protected function propagate($level)
  169. {
  170. while ($this->decisions->validOffset($this->propagateIndex)) {
  171. $decision = $this->decisions->atOffset($this->propagateIndex);
  172. $conflict = $this->watchGraph->propagateLiteral(
  173. $decision[Decisions::DECISION_LITERAL],
  174. $level,
  175. $this->decisions
  176. );
  177. $this->propagateIndex++;
  178. if ($conflict) {
  179. return $conflict;
  180. }
  181. }
  182. return null;
  183. }
  184. /**
  185. * Reverts a decision at the given level.
  186. */
  187. private function revert($level)
  188. {
  189. while (!$this->decisions->isEmpty()) {
  190. $literal = $this->decisions->lastLiteral();
  191. if ($this->decisions->undecided($literal)) {
  192. break;
  193. }
  194. $decisionLevel = $this->decisions->decisionLevel($literal);
  195. if ($decisionLevel <= $level) {
  196. break;
  197. }
  198. $this->decisions->revertLast();
  199. $this->propagateIndex = count($this->decisions);
  200. }
  201. while (!empty($this->branches) && $this->branches[count($this->branches) - 1][self::BRANCH_LEVEL] >= $level) {
  202. array_pop($this->branches);
  203. }
  204. }
  205. /**-------------------------------------------------------------------
  206. *
  207. * setpropagatelearn
  208. *
  209. * add free decision (a positive literal) to decision queue
  210. * increase level and propagate decision
  211. * return if no conflict.
  212. *
  213. * in conflict case, analyze conflict rule, add resulting
  214. * rule to learnt rule set, make decision from learnt
  215. * rule (always unit) and re-propagate.
  216. *
  217. * returns the new solver level or 0 if unsolvable
  218. *
  219. */
  220. private function setPropagateLearn($level, $literal, $disableRules, Rule $rule)
  221. {
  222. $level++;
  223. $this->decisions->decide($literal, $level, $rule);
  224. while (true) {
  225. $rule = $this->propagate($level);
  226. if (!$rule) {
  227. break;
  228. }
  229. if ($level == 1) {
  230. return $this->analyzeUnsolvable($rule, $disableRules);
  231. }
  232. // conflict
  233. list($learnLiteral, $newLevel, $newRule, $why) = $this->analyze($level, $rule);
  234. if ($newLevel <= 0 || $newLevel >= $level) {
  235. throw new SolverBugException(
  236. "Trying to revert to invalid level ".(int) $newLevel." from level ".(int) $level."."
  237. );
  238. } elseif (!$newRule) {
  239. throw new SolverBugException(
  240. "No rule was learned from analyzing $rule at level $level."
  241. );
  242. }
  243. $level = $newLevel;
  244. $this->revert($level);
  245. $this->rules->add($newRule, RuleSet::TYPE_LEARNED);
  246. $this->learnedWhy[$newRule->getId()] = $why;
  247. $ruleNode = new RuleWatchNode($newRule);
  248. $ruleNode->watch2OnHighest($this->decisions);
  249. $this->watchGraph->insert($ruleNode);
  250. $this->decisions->decide($learnLiteral, $level, $newRule);
  251. }
  252. return $level;
  253. }
  254. private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule)
  255. {
  256. // choose best package to install from decisionQueue
  257. $literals = $this->policy->selectPreferedPackages($this->pool, $this->installedMap, $decisionQueue, $rule->getRequiredPackage());
  258. $selectedLiteral = array_shift($literals);
  259. // if there are multiple candidates, then branch
  260. if (count($literals)) {
  261. $this->branches[] = array($literals, $level);
  262. }
  263. return $this->setPropagateLearn($level, $selectedLiteral, $disableRules, $rule);
  264. }
  265. protected function analyze($level, $rule)
  266. {
  267. $analyzedRule = $rule;
  268. $ruleLevel = 1;
  269. $num = 0;
  270. $l1num = 0;
  271. $seen = array();
  272. $learnedLiterals = array(null);
  273. $decisionId = count($this->decisions);
  274. $this->learnedPool[] = array();
  275. while (true) {
  276. $this->learnedPool[count($this->learnedPool) - 1][] = $rule;
  277. foreach ($rule->getLiterals() as $literal) {
  278. // skip the one true literal
  279. if ($this->decisions->satisfy($literal)) {
  280. continue;
  281. }
  282. if (isset($seen[abs($literal)])) {
  283. continue;
  284. }
  285. $seen[abs($literal)] = true;
  286. $l = $this->decisions->decisionLevel($literal);
  287. if (1 === $l) {
  288. $l1num++;
  289. } elseif ($level === $l) {
  290. $num++;
  291. } else {
  292. // not level1 or conflict level, add to new rule
  293. $learnedLiterals[] = $literal;
  294. if ($l > $ruleLevel) {
  295. $ruleLevel = $l;
  296. }
  297. }
  298. }
  299. $l1retry = true;
  300. while ($l1retry) {
  301. $l1retry = false;
  302. if (!$num && !--$l1num) {
  303. // all level 1 literals done
  304. break 2;
  305. }
  306. while (true) {
  307. if ($decisionId <= 0) {
  308. throw new SolverBugException(
  309. "Reached invalid decision id $decisionId while looking through $rule for a literal present in the analyzed rule $analyzedRule."
  310. );
  311. }
  312. $decisionId--;
  313. $decision = $this->decisions->atOffset($decisionId);
  314. $literal = $decision[Decisions::DECISION_LITERAL];
  315. if (isset($seen[abs($literal)])) {
  316. break;
  317. }
  318. }
  319. unset($seen[abs($literal)]);
  320. if ($num && 0 === --$num) {
  321. $learnedLiterals[0] = -abs($literal);
  322. if (!$l1num) {
  323. break 2;
  324. }
  325. foreach ($learnedLiterals as $i => $learnedLiteral) {
  326. if ($i !== 0) {
  327. unset($seen[abs($learnedLiteral)]);
  328. }
  329. }
  330. // only level 1 marks left
  331. $l1num++;
  332. $l1retry = true;
  333. }
  334. }
  335. $decision = $this->decisions->atOffset($decisionId);
  336. $rule = $decision[Decisions::DECISION_REASON];
  337. }
  338. $why = count($this->learnedPool) - 1;
  339. if (!$learnedLiterals[0]) {
  340. throw new SolverBugException(
  341. "Did not find a learnable literal in analyzed rule $analyzedRule."
  342. );
  343. }
  344. $newRule = new Rule($this->pool, $learnedLiterals, Rule::RULE_LEARNED, $why);
  345. return array($learnedLiterals[0], $ruleLevel, $newRule, $why);
  346. }
  347. private function analyzeUnsolvableRule($problem, $conflictRule)
  348. {
  349. $why = $conflictRule->getId();
  350. if ($conflictRule->getType() == RuleSet::TYPE_LEARNED) {
  351. $learnedWhy = $this->learnedWhy[$why];
  352. $problemRules = $this->learnedPool[$learnedWhy];
  353. foreach ($problemRules as $problemRule) {
  354. $this->analyzeUnsolvableRule($problem, $problemRule);
  355. }
  356. return;
  357. }
  358. if ($conflictRule->getType() == RuleSet::TYPE_PACKAGE) {
  359. // package rules cannot be part of a problem
  360. return;
  361. }
  362. $problem->nextSection();
  363. $problem->addRule($conflictRule);
  364. }
  365. private function analyzeUnsolvable($conflictRule, $disableRules)
  366. {
  367. $problem = new Problem($this->pool);
  368. $problem->addRule($conflictRule);
  369. $this->analyzeUnsolvableRule($problem, $conflictRule);
  370. $this->problems[] = $problem;
  371. $seen = array();
  372. $literals = $conflictRule->getLiterals();
  373. foreach ($literals as $literal) {
  374. // skip the one true literal
  375. if ($this->decisions->satisfy($literal)) {
  376. continue;
  377. }
  378. $seen[abs($literal)] = true;
  379. }
  380. foreach ($this->decisions as $decision) {
  381. $literal = $decision[Decisions::DECISION_LITERAL];
  382. // skip literals that are not in this rule
  383. if (!isset($seen[abs($literal)])) {
  384. continue;
  385. }
  386. $why = $decision[Decisions::DECISION_REASON];
  387. $problem->addRule($why);
  388. $this->analyzeUnsolvableRule($problem, $why);
  389. $literals = $why->getLiterals();
  390. foreach ($literals as $literal) {
  391. // skip the one true literal
  392. if ($this->decisions->satisfy($literal)) {
  393. continue;
  394. }
  395. $seen[abs($literal)] = true;
  396. }
  397. }
  398. if ($disableRules) {
  399. foreach ($this->problems[count($this->problems) - 1] as $reason) {
  400. $this->disableProblem($reason['rule']);
  401. }
  402. $this->resetSolver();
  403. return 1;
  404. }
  405. return 0;
  406. }
  407. private function disableProblem($why)
  408. {
  409. $job = $why->getJob();
  410. if (!$job) {
  411. $why->disable();
  412. return;
  413. }
  414. // disable all rules of this job
  415. foreach ($this->rules as $rule) {
  416. if ($job === $rule->getJob()) {
  417. $rule->disable();
  418. }
  419. }
  420. }
  421. private function resetSolver()
  422. {
  423. $this->decisions->reset();
  424. $this->propagateIndex = 0;
  425. $this->branches = array();
  426. $this->enableDisableLearnedRules();
  427. $this->makeAssertionRuleDecisions();
  428. }
  429. /*-------------------------------------------------------------------
  430. * enable/disable learnt rules
  431. *
  432. * we have enabled or disabled some of our rules. We now re-enable all
  433. * of our learnt rules except the ones that were learnt from rules that
  434. * are now disabled.
  435. */
  436. private function enableDisableLearnedRules()
  437. {
  438. foreach ($this->rules->getIteratorFor(RuleSet::TYPE_LEARNED) as $rule) {
  439. $why = $this->learnedWhy[$rule->getId()];
  440. $problemRules = $this->learnedPool[$why];
  441. $foundDisabled = false;
  442. foreach ($problemRules as $problemRule) {
  443. if ($problemRule->isDisabled()) {
  444. $foundDisabled = true;
  445. break;
  446. }
  447. }
  448. if ($foundDisabled && $rule->isEnabled()) {
  449. $rule->disable();
  450. } elseif (!$foundDisabled && $rule->isDisabled()) {
  451. $rule->enable();
  452. }
  453. }
  454. }
  455. private function runSat($disableRules = true)
  456. {
  457. $this->propagateIndex = 0;
  458. // /*
  459. // * here's the main loop:
  460. // * 1) propagate new decisions (only needed once)
  461. // * 2) fulfill jobs
  462. // * 3) fulfill all unresolved rules
  463. // * 4) minimalize solution if we had choices
  464. // * if we encounter a problem, we rewind to a safe level and restart
  465. // * with step 1
  466. // */
  467. $decisionQueue = array();
  468. $decisionSupplementQueue = array();
  469. $disableRules = array();
  470. $level = 1;
  471. $systemLevel = $level + 1;
  472. $installedPos = 0;
  473. while (true) {
  474. if (1 === $level) {
  475. $conflictRule = $this->propagate($level);
  476. if (null !== $conflictRule) {
  477. if ($this->analyzeUnsolvable($conflictRule, $disableRules)) {
  478. continue;
  479. }
  480. return;
  481. }
  482. }
  483. // handle job rules
  484. if ($level < $systemLevel) {
  485. $iterator = $this->rules->getIteratorFor(RuleSet::TYPE_JOB);
  486. foreach ($iterator as $rule) {
  487. if ($rule->isEnabled()) {
  488. $decisionQueue = array();
  489. $noneSatisfied = true;
  490. foreach ($rule->getLiterals() as $literal) {
  491. if ($this->decisions->satisfy($literal)) {
  492. $noneSatisfied = false;
  493. break;
  494. }
  495. if ($literal > 0 && $this->decisions->undecided($literal)) {
  496. $decisionQueue[] = $literal;
  497. }
  498. }
  499. if ($noneSatisfied && count($decisionQueue)) {
  500. // prune all update packages until installed version
  501. // except for requested updates
  502. if (count($this->installed) != count($this->updateMap)) {
  503. $prunedQueue = array();
  504. foreach ($decisionQueue as $literal) {
  505. if (isset($this->installedMap[abs($literal)])) {
  506. $prunedQueue[] = $literal;
  507. if (isset($this->updateMap[abs($literal)])) {
  508. $prunedQueue = $decisionQueue;
  509. break;
  510. }
  511. }
  512. }
  513. $decisionQueue = $prunedQueue;
  514. }
  515. }
  516. if ($noneSatisfied && count($decisionQueue)) {
  517. $oLevel = $level;
  518. $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
  519. if (0 === $level) {
  520. return;
  521. }
  522. if ($level <= $oLevel) {
  523. break;
  524. }
  525. }
  526. }
  527. }
  528. $systemLevel = $level + 1;
  529. // jobs left
  530. $iterator->next();
  531. if ($iterator->valid()) {
  532. continue;
  533. }
  534. }
  535. if ($level < $systemLevel) {
  536. $systemLevel = $level;
  537. }
  538. for ($i = 0, $n = 0; $n < count($this->rules); $i++, $n++) {
  539. if ($i == count($this->rules)) {
  540. $i = 0;
  541. }
  542. $rule = $this->rules->ruleById($i);
  543. $literals = $rule->getLiterals();
  544. if ($rule->isDisabled()) {
  545. continue;
  546. }
  547. $decisionQueue = array();
  548. // make sure that
  549. // * all negative literals are installed
  550. // * no positive literal is installed
  551. // i.e. the rule is not fulfilled and we
  552. // just need to decide on the positive literals
  553. //
  554. foreach ($literals as $literal) {
  555. if ($literal <= 0) {
  556. if (!$this->decisions->decidedInstall(abs($literal))) {
  557. continue 2; // next rule
  558. }
  559. } else {
  560. if ($this->decisions->decidedInstall(abs($literal))) {
  561. continue 2; // next rule
  562. }
  563. if ($this->decisions->undecided(abs($literal))) {
  564. $decisionQueue[] = $literal;
  565. }
  566. }
  567. }
  568. // need to have at least 2 item to pick from
  569. if (count($decisionQueue) < 2) {
  570. continue;
  571. }
  572. $oLevel = $level;
  573. $level = $this->selectAndInstall($level, $decisionQueue, $disableRules, $rule);
  574. if (0 === $level) {
  575. return;
  576. }
  577. // something changed, so look at all rules again
  578. $n = -1;
  579. }
  580. if ($level < $systemLevel) {
  581. continue;
  582. }
  583. // minimization step
  584. if (count($this->branches)) {
  585. $lastLiteral = null;
  586. $lastLevel = null;
  587. $lastBranchIndex = 0;
  588. $lastBranchOffset = 0;
  589. $l = 0;
  590. for ($i = count($this->branches) - 1; $i >= 0; $i--) {
  591. list($literals, $l) = $this->branches[$i];
  592. foreach ($literals as $offset => $literal) {
  593. if ($literal && $literal > 0 && $this->decisions->decisionLevel($literal) > $l + 1) {
  594. $lastLiteral = $literal;
  595. $lastBranchIndex = $i;
  596. $lastBranchOffset = $offset;
  597. $lastLevel = $l;
  598. }
  599. }
  600. }
  601. if ($lastLiteral) {
  602. unset($this->branches[$lastBranchIndex][self::BRANCH_LITERALS][$lastBranchOffset]);
  603. $level = $lastLevel;
  604. $this->revert($level);
  605. $why = $this->decisions->lastReason();
  606. $oLevel = $level;
  607. $level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why);
  608. if ($level == 0) {
  609. return;
  610. }
  611. continue;
  612. }
  613. }
  614. break;
  615. }
  616. }
  617. }