diff --git a/src/Composer/DependencyResolver/Decisions.php b/src/Composer/DependencyResolver/Decisions.php new file mode 100644 index 000000000..33e0fb27f --- /dev/null +++ b/src/Composer/DependencyResolver/Decisions.php @@ -0,0 +1,215 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +/** + * Stores decisions on installing, removing or keeping packages + * + * @author Nils Adermann + */ +class Decisions implements \Iterator +{ + const DECISION_LITERAL = 0; + const DECISION_REASON = 1; + + protected $pool; + protected $decisionMap; + protected $decisionQueue = array(); + protected $decisionQueueFree = array(); + + public function __construct($pool) + { + $this->pool = $pool; + + if (version_compare(PHP_VERSION, '5.3.4', '>=')) { + $this->decisionMap = new \SplFixedArray($this->pool->getMaxId() + 1); + } else { + $this->decisionMap = array_fill(0, $this->pool->getMaxId() + 1, 0); + } + } + + protected function addDecision($literal, $level) + { + $packageId = abs($literal); + + $previousDecision = $this->decisionMap[$packageId]; + if ($previousDecision != 0) { + $literalString = $this->pool->literalToString($literal); + $package = $this->pool->literalToPackage($literal); + throw new SolverBugException( + "Trying to decide $literalString on level $level, even though $package was previously decided as ".(int) $previousDecision."." + ); + } + + if ($literal > 0) { + $this->decisionMap[$packageId] = $level; + } else { + $this->decisionMap[$packageId] = -$level; + } + } + + public function decide($literal, $level, $why, $addToFreeQueue = false) + { + $this->addDecision($literal, $level); + $this->decisionQueue[] = array( + self::DECISION_LITERAL => $literal, + self::DECISION_REASON => $why, + ); + + if ($addToFreeQueue) { + $this->decisionQueueFree[count($this->decisionQueue) - 1] = true; + } + } + + public function contain($literal) + { + $packageId = abs($literal); + + return ( + $this->decisionMap[$packageId] > 0 && $literal > 0 || + $this->decisionMap[$packageId] < 0 && $literal < 0 + ); + } + + public function satisfy($literal) + { + $packageId = abs($literal); + + return ( + $literal > 0 && $this->decisionMap[$packageId] > 0 || + $literal < 0 && $this->decisionMap[$packageId] < 0 + ); + } + + public function conflict($literal) + { + $packageId = abs($literal); + + return ( + ($this->decisionMap[$packageId] > 0 && $literal < 0) || + ($this->decisionMap[$packageId] < 0 && $literal > 0) + ); + } + + public function decided($literalOrPackageId) + { + return $this->decisionMap[abs($literalOrPackageId)] != 0; + } + + public function undecided($literalOrPackageId) + { + return $this->decisionMap[abs($literalOrPackageId)] == 0; + } + + public function decidedInstall($literalOrPackageId) + { + return $this->decisionMap[abs($literalOrPackageId)] > 0; + } + + public function decisionLevel($literalOrPackageId) + { + return abs($this->decisionMap[abs($literalOrPackageId)]); + } + + public function decisionRule($literalOrPackageId) + { + $packageId = abs($literalOrPackageId); + + foreach ($this->decisionQueue as $i => $decision) { + if ($packageId === abs($decision[self::DECISION_LITERAL])) { + return $decision[self::DECISION_REASON]; + } + } + + return null; + } + + public function atOffset($queueOffset) + { + return $this->decisionQueue[$queueOffset]; + } + + public function validOffset($queueOffset) + { + return $queueOffset >= 0 && $queueOffset < count($this->decisionQueue); + } + + public function lastReason() + { + return $this->decisionQueue[count($this->decisionQueue) - 1][self::DECISION_REASON]; + } + + public function lastLiteral() + { + return $this->decisionQueue[count($this->decisionQueue) - 1][self::DECISION_LITERAL]; + } + + public function reset() + { + while ($decision = array_pop($this->decisionQueue)) { + $this->decisionMap[abs($decision[self::DECISION_LITERAL])] = 0; + } + + $this->decisionQueueFree = array(); + } + + public function resetToOffset($offset) + { + while (count($this->decisionQueue) > $offset + 1) { + $decision = array_pop($this->decisionQueue); + unset($this->decisionQueueFree[count($this->decisionQueue)]); + $this->decisionMap[abs($decision[self::DECISION_LITERAL])] = 0; + } + } + + public function revertLast() + { + $this->decisionMap[abs($this->lastLiteral())] = 0; + array_pop($this->decisionQueue); + } + + public function getMaxOffset() + { + return count($this->decisionQueue) - 1; + } + + public function rewind() + { + end($this->decisionQueue); + } + + public function current() + { + return current($this->decisionQueue); + } + + public function key() + { + return key($this->decisionQueue); + } + + public function next() + { + return prev($this->decisionQueue); + } + + public function valid() + { + return false !== current($this->decisionQueue); + } + + public function isEmpty() + { + return count($this->decisionQueue) === 0; + } +} diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index e32294edc..260687755 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -190,23 +190,18 @@ class RuleSetGenerator } } - // check implicit obsoletes - // for installed packages we only need to check installed/installed problems, - // as the others are picked up when looking at the uninstalled package. - if (!$isInstalled) { - $obsoleteProviders = $this->pool->whatProvides($package->getName(), null); + $obsoleteProviders = $this->pool->whatProvides($package->getName(), null); - foreach ($obsoleteProviders as $provider) { - if ($provider === $package) { - continue; - } + foreach ($obsoleteProviders as $provider) { + if ($provider === $package) { + continue; + } - if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { - $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, (string) $package)); - } elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) { - $reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES; - $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, (string) $package)); - } + if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { + $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, (string) $package)); + } elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) { + $reason = ($package->getName() == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES; + $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createConflictRule($package, $provider, $reason, (string) $package)); } } } diff --git a/src/Composer/DependencyResolver/RuleWatchGraph.php b/src/Composer/DependencyResolver/RuleWatchGraph.php index 2f27ecd22..42c764fa6 100644 --- a/src/Composer/DependencyResolver/RuleWatchGraph.php +++ b/src/Composer/DependencyResolver/RuleWatchGraph.php @@ -72,16 +72,11 @@ class RuleWatchGraph * @param int $decidedLiteral The literal which was decided (A in our example) * @param int $level The level at which the decision took place and at which * all resulting decisions should be made. - * @param Callable $decisionsSatisfyCallback A callback which checks if a - * literal has already been positively decided and the rule is thus - * already true and can be skipped. - * @param Callable $conflictCallback A callback which checks if a literal - * would conflict with previously made decisions on the same package - * @param Callable $decideCallback A callback which is responsible for - * registering decided literals resulting from unit clauses + * @param Decisions $decisions Used to check previous decisions and to + * register decisions resulting from propagation * @return Rule|null If a conflict is found the conflicting rule is returned */ - public function propagateLiteral($decidedLiteral, $level, $decisionsSatisfyCallback, $conflictCallback, $decideCallback) + public function propagateLiteral($decidedLiteral, $level, $decisions) { // we invert the decided literal here, example: // A was decided => (-A|B) now requires B to be true, so we look for @@ -99,13 +94,13 @@ class RuleWatchGraph $node = $chain->current(); $otherWatch = $node->getOtherWatch($literal); - if (!$node->getRule()->isDisabled() && !call_user_func($decisionsSatisfyCallback, $otherWatch)) { + if (!$node->getRule()->isDisabled() && !$decisions->contain($otherWatch)) { $ruleLiterals = $node->getRule()->getLiterals(); - $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $conflictCallback) { + $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $decisions) { return $literal !== $ruleLiteral && $otherWatch !== $ruleLiteral && - !call_user_func($conflictCallback, $ruleLiteral); + !$decisions->conflict($ruleLiteral); }); if ($alternativeLiterals) { @@ -114,11 +109,11 @@ class RuleWatchGraph continue; } - if (call_user_func($conflictCallback, $otherWatch)) { + if ($decisions->conflict($otherWatch)) { return $node->getRule(); } - call_user_func($decideCallback, $otherWatch, $level, $node->getRule()); + $decisions->decide($otherWatch, $level, $node->getRule()); } $chain->next(); diff --git a/src/Composer/DependencyResolver/RuleWatchNode.php b/src/Composer/DependencyResolver/RuleWatchNode.php index 9547786b8..0f1e5c08b 100644 --- a/src/Composer/DependencyResolver/RuleWatchNode.php +++ b/src/Composer/DependencyResolver/RuleWatchNode.php @@ -47,9 +47,9 @@ class RuleWatchNode * Useful for learned rules where the literal for the highest rule is most * likely to quickly lead to further decisions. * - * @param SplFixedArray $decisionMap A package to decision lookup table + * @param Decisions $decisions The decisions made so far by the solver */ - public function watch2OnHighest($decisionMap) + public function watch2OnHighest(Decisions $decisions) { $literals = $this->rule->getLiterals(); @@ -61,7 +61,7 @@ class RuleWatchNode $watchLevel = 0; foreach ($literals as $literal) { - $level = abs($decisionMap[abs($literal)]); + $level = $decisions->decisionLevel($literal); if ($level > $watchLevel) { $this->rule->watch2 = $literal; diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index fcb4fd428..971f69cfb 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -29,12 +29,9 @@ class Solver protected $addedMap = array(); protected $updateMap = array(); protected $watchGraph; - protected $decisionMap; + protected $decisions; protected $installedMap; - protected $decisionQueue = array(); - protected $decisionQueueWhy = array(); - protected $decisionQueueFree = array(); protected $propagateIndex; protected $branches = array(); protected $problems = array(); @@ -48,21 +45,10 @@ class Solver $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool); } - private function findDecisionRule($packageId) - { - foreach ($this->decisionQueue as $i => $literal) { - if ($packageId === abs($literal)) { - return $this->decisionQueueWhy[$i]; - } - } - - return null; - } - // aka solver_makeruledecisions private function makeAssertionRuleDecisions() { - $decisionStart = count($this->decisionQueue); + $decisionStart = $this->decisions->getMaxOffset(); for ($ruleIndex = 0; $ruleIndex < count($this->rules); $ruleIndex++) { $rule = $this->rules->ruleById($ruleIndex); @@ -74,12 +60,12 @@ class Solver $literals = $rule->getLiterals(); $literal = $literals[0]; - if (!$this->decided(abs($literal))) { - $this->decide($literal, 1, $rule); + if (!$this->decisions->decided(abs($literal))) { + $this->decisions->decide($literal, 1, $rule); continue; } - if ($this->decisionsSatisfy($literal)) { + if ($this->decisions->satisfy($literal)) { continue; } @@ -89,7 +75,7 @@ class Solver continue; } - $conflict = $this->findDecisionRule(abs($literal)); + $conflict = $this->decisions->decisionRule($literal); if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) { @@ -126,13 +112,7 @@ class Solver } $this->problems[] = $problem; - // start over - while (count($this->decisionQueue) > $decisionStart) { - $decisionLiteral = array_pop($this->decisionQueue); - array_pop($this->decisionQueueWhy); - unset($this->decisionQueueFree[count($this->decisionQueue)]); - $this->decisionMap[abs($decisionLiteral)] = 0; - } + $this->resetToOffset($decisionStart); $ruleIndex = -1; } } @@ -177,11 +157,7 @@ class Solver $this->setupInstalledMap(); - if (version_compare(PHP_VERSION, '5.3.4', '>=')) { - $this->decisionMap = new \SplFixedArray($this->pool->getMaxId() + 1); - } else { - $this->decisionMap = array_fill(0, $this->pool->getMaxId() + 1, 0); - } + $this->decisions = new Decisions($this->pool); $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap); $this->watchGraph = new RuleWatchGraph; @@ -195,11 +171,18 @@ class Solver $this->runSat(true); + // decide to remove everything that's installed and undecided + foreach ($this->installedMap as $packageId => $void) { + if ($this->decisions->undecided($packageId)) { + $this->decisions->decide(-$packageId, 1, null); + } + } + if ($this->problems) { throw new SolverProblemsException($this->problems); } - $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisionMap, $this->decisionQueue, $this->decisionQueueWhy); + $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions); return $transaction->getOperations(); } @@ -211,77 +194,6 @@ class Solver return new Literal($package, $id > 0); } - protected function addDecision($literal, $level) - { - $packageId = abs($literal); - - $previousDecision = $this->decisionMap[$packageId]; - if ($previousDecision != 0) { - $literalString = $this->pool->literalToString($literal); - $package = $this->pool->literalToPackage($literal); - throw new SolverBugException( - "Trying to decide $literalString on level $level, even though $package was previously decided as ".(int) $previousDecision."." - ); - } - - if ($literal > 0) { - $this->decisionMap[$packageId] = $level; - } else { - $this->decisionMap[$packageId] = -$level; - } - } - - public function decide($literal, $level, $why) - { - $this->addDecision($literal, $level); - $this->decisionQueue[] = $literal; - $this->decisionQueueWhy[] = $why; - } - - public function decisionsContain($literal) - { - $packageId = abs($literal); - - return ( - $this->decisionMap[$packageId] > 0 && $literal > 0 || - $this->decisionMap[$packageId] < 0 && $literal < 0 - ); - } - - protected function decisionsSatisfy($literal) - { - $packageId = abs($literal); - - return ( - $literal > 0 && $this->decisionMap[$packageId] > 0 || - $literal < 0 && $this->decisionMap[$packageId] < 0 - ); - } - - public function decisionsConflict($literal) - { - $packageId = abs($literal); - - return ( - ($this->decisionMap[$packageId] > 0 && $literal < 0) || - ($this->decisionMap[$packageId] < 0 && $literal > 0) - ); - } - protected function decided($packageId) - { - return $this->decisionMap[$packageId] != 0; - } - - protected function undecided($packageId) - { - return $this->decisionMap[$packageId] == 0; - } - - protected function decidedInstall($packageId) - { - return $this->decisionMap[$packageId] > 0; - } - /** * Makes a decision and propagates it to all rules. * @@ -292,15 +204,17 @@ class Solver */ protected function propagate($level) { - while ($this->propagateIndex < count($this->decisionQueue)) { + while ($this->decisions->validOffset($this->propagateIndex)) { + $decision = $this->decisions->atOffset($this->propagateIndex); + $conflict = $this->watchGraph->propagateLiteral( - $this->decisionQueue[$this->propagateIndex++], + $decision[Decisions::DECISION_LITERAL], $level, - array($this, 'decisionsContain'), - array($this, 'decisionsConflict'), - array($this, 'decide') + $this->decisions ); + $this->propagateIndex++; + if ($conflict) { return $conflict; } @@ -314,30 +228,27 @@ class Solver */ private function revert($level) { - while (!empty($this->decisionQueue)) { - $literal = $this->decisionQueue[count($this->decisionQueue) - 1]; + while (!$this->decisions->isEmpty()) { + $literal = $this->decisions->lastLiteral(); - if (!$this->decisionMap[abs($literal)]) { + if ($this->decisions->undecided($literal)) { break; } - $decisionLevel = abs($this->decisionMap[abs($literal)]); + $decisionLevel = $this->decisions->decisionLevel($literal); if ($decisionLevel <= $level) { break; } - $this->decisionMap[abs($literal)] = 0; - array_pop($this->decisionQueue); - array_pop($this->decisionQueueWhy); - - $this->propagateIndex = count($this->decisionQueue); + $this->decisions->revertLast(); + $this->propagateIndex = $this->decisions->getMaxOffset() + 1; } while (!empty($this->branches)) { list($literals, $branchLevel) = $this->branches[count($this->branches) - 1]; - if ($branchLevel >= $level) { + if ($branchLevel < $level) { break; } @@ -349,7 +260,7 @@ class Solver * * setpropagatelearn * - * add free decision (solvable to install) to decisionq + * add free decision (a positive literal) to decision queue * increase level and propagate decision * return if no conflict. * @@ -364,8 +275,7 @@ class Solver { $level++; - $this->decide($literal, $level, $rule); - $this->decisionQueueFree[count($this->decisionQueueWhy) - 1] = true; + $this->decisions->decide($literal, $level, $rule, true); while (true) { $rule = $this->propagate($level); @@ -400,10 +310,10 @@ class Solver $this->learnedWhy[$newRule->getId()] = $why; $ruleNode = new RuleWatchNode($newRule); - $ruleNode->watch2OnHighest($this->decisionMap); + $ruleNode->watch2OnHighest($this->decisions); $this->watchGraph->insert($ruleNode); - $this->decide($learnLiteral, $level, $newRule); + $this->decisions->decide($learnLiteral, $level, $newRule); } return $level; @@ -433,7 +343,7 @@ class Solver $seen = array(); $learnedLiterals = array(null); - $decisionId = count($this->decisionQueue); + $decisionId = $this->decisions->getMaxOffset() + 1; $this->learnedPool[] = array(); @@ -442,7 +352,7 @@ class Solver foreach ($rule->getLiterals() as $literal) { // skip the one true literal - if ($this->decisionsSatisfy($literal)) { + if ($this->decisions->satisfy($literal)) { continue; } @@ -451,7 +361,7 @@ class Solver } $seen[abs($literal)] = true; - $l = abs($this->decisionMap[abs($literal)]); + $l = $this->decisions->decisionLevel($literal); if (1 === $l) { $l1num++; @@ -485,7 +395,8 @@ class Solver $decisionId--; - $literal = $this->decisionQueue[$decisionId]; + $decision = $this->decisions->atOffset($decisionId); + $literal = $decision[Decisions::DECISION_LITERAL]; if (isset($seen[abs($literal)])) { break; @@ -512,7 +423,8 @@ class Solver } } - $rule = $this->decisionQueueWhy[$decisionId]; + $decision = $this->decisions->atOffset($decisionId); + $rule = $decision[Decisions::DECISION_REASON]; } $why = count($this->learnedPool) - 1; @@ -565,34 +477,35 @@ class Solver foreach ($literals as $literal) { // skip the one true literal - if ($this->decisionsSatisfy($literal)) { + if ($this->decisions->satisfy($literal)) { continue; } $seen[abs($literal)] = true; } - $decisionId = count($this->decisionQueue); + $decisionId = $this->decisions->getMaxOffset() + 1; while ($decisionId > 0) { $decisionId--; - $literal = $this->decisionQueue[$decisionId]; + $decision = $this->decisions->atOffset($decisionId); + $literal = $decision[Decisions::DECISION_LITERAL]; // skip literals that are not in this rule if (!isset($seen[abs($literal)])) { continue; } - $why = $this->decisionQueueWhy[$decisionId]; - $problem->addRule($why); + $why = $decision[Decisions::DECISION_REASON]; + $problem->addRule($why); $this->analyzeUnsolvableRule($problem, $why); $literals = $why->getLiterals(); foreach ($literals as $literal) { // skip the one true literal - if ($this->decisionsSatisfy($literal)) { + if ($this->decisions->satisfy($literal)) { continue; } $seen[abs($literal)] = true; @@ -630,12 +543,8 @@ class Solver private function resetSolver() { - while ($literal = array_pop($this->decisionQueue)) { - $this->decisionMap[abs($literal)] = 0; - } + $this->decisions->reset(); - $this->decisionQueueWhy = array(); - $this->decisionQueueFree = array(); $this->propagateIndex = 0; $this->branches = array(); @@ -717,11 +626,11 @@ class Solver $noneSatisfied = true; foreach ($rule->getLiterals() as $literal) { - if ($this->decisionsSatisfy($literal)) { + if ($this->decisions->satisfy($literal)) { $noneSatisfied = false; break; } - if ($literal > 0 && $this->undecided($literal)) { + if ($literal > 0 && $this->decisions->undecided($literal)) { $decisionQueue[] = $literal; } } @@ -794,14 +703,14 @@ class Solver // foreach ($literals as $literal) { if ($literal <= 0) { - if (!$this->decidedInstall(abs($literal))) { + if (!$this->decisions->decidedInstall(abs($literal))) { continue 2; // next rule } } else { - if ($this->decidedInstall(abs($literal))) { + if ($this->decisions->decidedInstall(abs($literal))) { continue 2; // next rule } - if ($this->undecided(abs($literal))) { + if ($this->decisions->undecided(abs($literal))) { $decisionQueue[] = $literal; } } @@ -834,28 +743,30 @@ class Solver $lastLevel = null; $lastBranchIndex = 0; $lastBranchOffset = 0; + $l = 0; for ($i = count($this->branches) - 1; $i >= 0; $i--) { - list($literals, $level) = $this->branches[$i]; + list($literals, $l) = $this->branches[$i]; foreach ($literals as $offset => $literal) { - if ($literal && $literal > 0 && $this->decisionMap[abs($literal)] > $level + 1) { + if ($literal && $literal > 0 && $this->decisions->decisionLevel($literal) > $l + 1) { $lastLiteral = $literal; $lastBranchIndex = $i; $lastBranchOffset = $offset; - $lastLevel = $level; + $lastLevel = $l; } } } if ($lastLiteral) { - $this->branches[$lastBranchIndex][$lastBranchOffset] = null; + unset($this->branches[$lastBranchIndex][0][$lastBranchOffset]); + $this->branches[$lastBranchIndex][0] = array_values($this->branches[$lastBranchIndex][0]); $minimizationSteps++; $level = $lastLevel; $this->revert($level); - $why = $this->decisionQueueWhy[count($this->decisionQueueWhy) - 1]; + $why = $this->decisions->lastReason(); $oLevel = $level; $level = $this->setPropagateLearn($level, $lastLiteral, $disableRules, $why); diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index 5f64c7245..ff801acd6 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -23,26 +23,164 @@ class Transaction protected $policy; protected $pool; protected $installedMap; - protected $decisionMap; - protected $decisionQueue; - protected $decisionQueueWhy; + protected $decisions; + protected $transaction; - public function __construct($policy, $pool, $installedMap, $decisionMap, array $decisionQueue, $decisionQueueWhy) + public function __construct($policy, $pool, $installedMap, $decisions) { $this->policy = $policy; $this->pool = $pool; $this->installedMap = $installedMap; - $this->decisionMap = $decisionMap; - $this->decisionQueue = $decisionQueue; - $this->decisionQueueWhy = $decisionQueueWhy; + $this->decisions = $decisions; + $this->transaction = array(); } public function getOperations() { - $transaction = array(); + $installMeansUpdateMap = $this->findUpdates(); + + $updateMap = array(); + $installMap = array(); + $uninstallMap = array(); + + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + $reason = $decision[Decisions::DECISION_REASON]; + + $package = $this->pool->literalToPackage($literal); + + // wanted & installed || !wanted & !installed + if (($literal > 0) == (isset($this->installedMap[$package->getId()]))) { + continue; + } + + if ($literal > 0) { + if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { + + $source = $installMeansUpdateMap[abs($literal)]; + + $updateMap[$package->getId()] = array( + 'package' => $package, + 'source' => $source, + 'reason' => $reason, + ); + + // avoid updates to one package from multiple origins + unset($installMeansUpdateMap[abs($literal)]); + $ignoreRemove[$source->getId()] = true; + } else { + $installMap[$package->getId()] = array( + 'package' => $package, + 'reason' => $reason, + ); + } + } + } + + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + $package = $this->pool->literalToPackage($literal); + + if ($literal <= 0 && + isset($this->installedMap[$package->getId()]) && + !isset($ignoreRemove[$package->getId()])) { + $uninstallMap[$package->getId()] = array( + 'package' => $package, + 'reason' => $reason, + ); + + } + } + + $this->transactionFromMaps($installMap, $updateMap, $uninstallMap); + + return $this->transaction; + } + + protected function transactionFromMaps($installMap, $updateMap, $uninstallMap) + { + $queue = array_map(function ($operation) { + return $operation['package']; + }, + $this->findRootPackages($installMap, $updateMap) + ); + + $visited = array(); + + while (!empty($queue)) { + $package = array_pop($queue); + $packageId = $package->getId(); + + if (!isset($visited[$packageId])) { + array_push($queue, $package); + + if ($package instanceof AliasPackage) { + array_push($queue, $package->getAliasOf()); + } else { + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + + foreach ($possibleRequires as $require) { + array_push($queue, $require); + } + } + } + + $visited[$package->getId()] = true; + } else { + if (isset($installMap[$packageId])) { + $this->install( + $installMap[$packageId]['package'], + $installMap[$packageId]['reason'] + ); + unset($installMap[$packageId]); + } + if (isset($updateMap[$packageId])) { + $this->update( + $updateMap[$packageId]['source'], + $updateMap[$packageId]['package'], + $updateMap[$packageId]['reason'] + ); + unset($updateMap[$packageId]); + } + } + } + + foreach ($uninstallMap as $uninstall) { + $this->uninstall($uninstall['package'], $uninstall['reason']); + } + } + + protected function findRootPackages($installMap, $updateMap) + { + $packages = $installMap + $updateMap; + $roots = $packages; + + foreach ($packages as $packageId => $operation) { + $package = $operation['package']; + + if (!isset($roots[$packageId])) { + continue; + } + + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + + foreach ($possibleRequires as $require) { + unset($roots[$require->getId()]); + } + } + } + + return $roots; + } + + protected function findUpdates() + { $installMeansUpdateMap = array(); - foreach ($this->decisionQueue as $i => $literal) { + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; $package = $this->pool->literalToPackage($literal); // !wanted & installed @@ -63,105 +201,39 @@ class Transaction } } - foreach ($this->decisionQueue as $i => $literal) { - $package = $this->pool->literalToPackage($literal); + return $installMeansUpdateMap; + } - // wanted & installed || !wanted & !installed - if (($literal > 0) == (isset($this->installedMap[$package->getId()]))) { - continue; - } - - if ($literal > 0) { - if ($package instanceof AliasPackage) { - $transaction[] = new Operation\MarkAliasInstalledOperation( - $package, $this->decisionQueueWhy[$i] - ); - continue; - } - - if (isset($installMeansUpdateMap[abs($literal)])) { - - $source = $installMeansUpdateMap[abs($literal)]; - - $transaction[] = new Operation\UpdateOperation( - $source, $package, $this->decisionQueueWhy[$i] - ); - - // avoid updates to one package from multiple origins - unset($installMeansUpdateMap[abs($literal)]); - $ignoreRemove[$source->getId()] = true; - } else { - $transaction[] = new Operation\InstallOperation( - $package, $this->decisionQueueWhy[$i] - ); - } - } elseif (!isset($ignoreRemove[$package->getId()])) { - if ($package instanceof AliasPackage) { - $transaction[] = new Operation\MarkAliasInstalledOperation( - $package, $this->decisionQueueWhy[$i] - ); - } else { - $transaction[] = new Operation\UninstallOperation( - $package, $this->decisionQueueWhy[$i] - ); - } - } + protected function install($package, $reason) + { + if ($package instanceof AliasPackage) { + return $this->markAliasInstalled($package, $reason); } - $allDecidedMap = $this->decisionMap; - foreach ($this->decisionMap as $packageId => $decision) { - if ($decision != 0) { - $package = $this->pool->packageById($packageId); - if ($package instanceof AliasPackage) { - $allDecidedMap[$package->getAliasOf()->getId()] = $decision; - } - } + $this->transaction[] = new Operation\InstallOperation($package, $reason); + } + + protected function update($from, $to, $reason) + { + $this->transaction[] = new Operation\UpdateOperation($from, $to, $reason); + } + + protected function uninstall($package, $reason) + { + if ($package instanceof AliasPackage) { + return $this->markAliasUninstalled($package, $reason); } - foreach ($allDecidedMap as $packageId => $decision) { - if ($packageId === 0) { - continue; - } + $this->transaction[] = new Operation\UninstallOperation($package, $reason); + } - if (0 == $decision && isset($this->installedMap[$packageId])) { - $package = $this->pool->packageById($packageId); + protected function markAliasInstalled($package, $reason) + { + $this->transaction[] = new Operation\MarkAliasInstalledOperation($package, $reason); + } - if ($package instanceof AliasPackage) { - $transaction[] = new Operation\MarkAliasInstalledOperation( - $package, null - ); - } else { - $transaction[] = new Operation\UninstallOperation( - $package, null - ); - } - - $this->decisionMap[$packageId] = -1; - } - } - - foreach ($allDecidedMap as $packageId => $decision) { - if ($packageId === 0) { - continue; - } - - if (0 == $decision && isset($this->installedMap[$packageId])) { - $package = $this->pool->packageById($packageId); - - if ($package instanceof AliasPackage) { - $transaction[] = new Operation\MarkAliasInstalledOperation( - $package, null - ); - } else { - $transaction[] = new Operation\UninstallOperation( - $package, null - ); - } - - $this->decisionMap[$packageId] = -1; - } - } - - return array_reverse($transaction); + protected function markAliasUninstalled($package, $reason) + { + $this->transaction[] = new Operation\MarkAliasUninstalledOperation($package, $reason); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index d361b66b0..0cec2b50a 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -118,6 +118,33 @@ class SolverTest extends TestCase )); } + public function testSolverInstallWithDepsInOrder() + { + $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repo->addPackage($packageC = $this->getPackage('C', '1.0')); + + $packageB->setRequires(array( + new Link('B', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'), + new Link('B', 'C', $this->getVersionConstraint('>=', '1.0'), 'requires'), + )); + $packageC->setRequires(array( + new Link('C', 'A', $this->getVersionConstraint('>=', '1.0'), 'requires'), + )); + + $this->reposComplete(); + + $this->request->install('A'); + $this->request->install('B'); + $this->request->install('C'); + + $this->checkSolverResult(array( + array('job' => 'install', 'package' => $packageA), + array('job' => 'install', 'package' => $packageC), + array('job' => 'install', 'package' => $packageB), + )); + } + public function testSolverInstallInstalled() { $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); @@ -291,15 +318,16 @@ class SolverTest extends TestCase $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array( - array( - 'job' => 'remove', - 'package' => $packageB, - ), array( 'job' => 'update', 'from' => $packageA, 'to' => $newPackageA, - ))); + ), + array( + 'job' => 'remove', + 'package' => $packageB, + ), + )); } public function testSolverAllJobs() @@ -324,8 +352,8 @@ class SolverTest extends TestCase $this->checkSolverResult(array( array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), array('job' => 'install', 'package' => $packageB), - array('job' => 'remove', 'package' => $packageD), array('job' => 'install', 'package' => $packageA), + array('job' => 'remove', 'package' => $packageD), )); } @@ -477,8 +505,8 @@ class SolverTest extends TestCase $this->request->install('X'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $newPackageB), + array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageX), )); } @@ -520,9 +548,9 @@ class SolverTest extends TestCase $this->request->install('A'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageC), array('job' => 'install', 'package' => $packageB), array('job' => 'install', 'package' => $packageA), + array('job' => 'install', 'package' => $packageC), )); } @@ -690,8 +718,8 @@ class SolverTest extends TestCase $this->request->install('A', $this->getVersionConstraint('==', '1.1.0.0')); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageB), array('job' => 'install', 'package' => $packageA2), + array('job' => 'install', 'package' => $packageB), array('job' => 'install', 'package' => $packageA2Alias), )); } @@ -713,9 +741,9 @@ class SolverTest extends TestCase $this->request->install('B'); $this->checkSolverResult(array( + array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageAAlias), array('job' => 'install', 'package' => $packageB), - array('job' => 'install', 'package' => $packageA), )); } diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test index 1ffa936b4..481604cc3 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test @@ -45,7 +45,7 @@ Aliases take precedence over default package even if default is selected --RUN-- install --EXPECT-- -Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f) Installing a/req (dev-feature-foo feat.f) -Installing a/b (dev-master) +Marking a/req (dev-master feat.f) as installed, alias of a/req (dev-feature-foo feat.f) Installing a/a (dev-master) +Installing a/b (dev-master) diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority.test b/tests/Composer/Test/Fixtures/installer/aliased-priority.test index 55da1ee71..0cde23361 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority.test @@ -47,9 +47,9 @@ Aliases take precedence over default package --RUN-- install --EXPECT-- -Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked) -Installing a/b (dev-master forked) -Marking a/c (dev-master feat.f) as installed, alias of a/c (dev-feature-foo feat.f) -Installing a/a (dev-master master) Installing a/c (dev-feature-foo feat.f) +Marking a/c (dev-master feat.f) as installed, alias of a/c (dev-feature-foo feat.f) +Installing a/b (dev-master forked) +Installing a/a (dev-master master) Marking a/a (1.0.x-dev master) as installed, alias of a/a (dev-master master) +Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked)