diff --git a/src/Composer/DependencyResolver/DebugSolver.php b/src/Composer/DependencyResolver/DebugSolver.php new file mode 100644 index 000000000..d02679e8c --- /dev/null +++ b/src/Composer/DependencyResolver/DebugSolver.php @@ -0,0 +1,83 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +/** + * @author Nils Adermann + */ +class DebugSolver extends Solver +{ + protected function printDecisionMap() + { + echo "\nDecisionMap: \n"; + foreach ($this->decisionMap as $packageId => $level) { + if ($packageId === 0) { + continue; + } + if ($level > 0) { + echo ' +' . $this->pool->packageById($packageId)."\n"; + } elseif ($level < 0) { + echo ' -' . $this->pool->packageById($packageId)."\n"; + } else { + echo ' ?' . $this->pool->packageById($packageId)."\n"; + } + } + echo "\n"; + } + + protected function printDecisionQueue() + { + echo "DecisionQueue: \n"; + foreach ($this->decisionQueue as $i => $literal) { + echo ' ' . $this->pool->literalToString($literal) . ' ' . $this->decisionQueueWhy[$i]." level ".$this->decisionMap[abs($literal)]."\n"; + } + echo "\n"; + } + + protected function printWatches() + { + echo "\nWatches:\n"; + foreach ($this->watches as $literalId => $watch) { + echo ' '.$this->literalFromId($literalId)."\n"; + $queue = array(array(' ', $watch)); + + while (!empty($queue)) { + list($indent, $watch) = array_pop($queue); + + echo $indent.$watch; + + if ($watch) { + echo ' [id='.$watch->getId().',watch1='.$this->literalFromId($watch->watch1).',watch2='.$this->literalFromId($watch->watch2)."]"; + } + + echo "\n"; + + if ($watch && ($watch->next1 == $watch || $watch->next2 == $watch)) { + if ($watch->next1 == $watch) { + echo $indent." 1 *RECURSION*"; + } + if ($watch->next2 == $watch) { + echo $indent." 2 *RECURSION*"; + } + } elseif ($watch && ($watch->next1 || $watch->next2)) { + $indent = str_replace(array('1', '2'), ' ', $indent); + + array_push($queue, array($indent.' 2 ', $watch->next2)); + array_push($queue, array($indent.' 1 ', $watch->next1)); + } + } + + echo "\n"; + } + } +} diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index ac3d38995..181993e8e 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -30,7 +30,7 @@ class DefaultPolicy implements PolicyInterface return $constraint->matchSpecific($version); } - public function findUpdatePackages(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package) + public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package) { $packages = array(); @@ -43,12 +43,6 @@ class DefaultPolicy implements PolicyInterface return $packages; } - public function installable(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package) - { - // todo: package blacklist? - return true; - } - public function getPriority(Pool $pool, PackageInterface $package) { return $pool->getPriority($package->getRepository()); @@ -56,44 +50,44 @@ class DefaultPolicy implements PolicyInterface public function selectPreferedPackages(Pool $pool, array $installedMap, array $literals) { - $packages = $this->groupLiteralsByNamePreferInstalled($installedMap, $literals); + $packages = $this->groupLiteralsByNamePreferInstalled($pool,$installedMap, $literals); foreach ($packages as &$literals) { $policy = $this; usort($literals, function ($a, $b) use ($policy, $pool, $installedMap) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $a->getPackage(), $b->getPackage(), true); + return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), true); }); } foreach ($packages as &$literals) { - $literals = $this->pruneToBestVersion($literals); + $literals = $this->pruneToBestVersion($pool, $literals); $literals = $this->pruneToHighestPriorityOrInstalled($pool, $installedMap, $literals); - $literals = $this->pruneRemoteAliases($literals); + $literals = $this->pruneRemoteAliases($pool, $literals); } $selected = call_user_func_array('array_merge', $packages); // now sort the result across all packages to respect replaces across packages usort($selected, function ($a, $b) use ($policy, $pool, $installedMap) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $a->getPackage(), $b->getPackage()); + return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b)); }); return $selected; } - protected function groupLiteralsByNamePreferInstalled(array $installedMap, $literals) + protected function groupLiteralsByNamePreferInstalled(Pool $pool, array $installedMap, $literals) { $packages = array(); foreach ($literals as $literal) { - $packageName = $literal->getPackage()->getName(); + $packageName = $pool->literalToPackage($literal)->getName(); if (!isset($packages[$packageName])) { $packages[$packageName] = array(); } - if (isset($installedMap[$literal->getPackageId()])) { + if (isset($installedMap[abs($literal)])) { array_unshift($packages[$packageName], $literal); } else { $packages[$packageName][] = $literal; @@ -171,19 +165,21 @@ class DefaultPolicy implements PolicyInterface return false; } - protected function pruneToBestVersion($literals) + protected function pruneToBestVersion(Pool $pool, $literals) { $bestLiterals = array($literals[0]); - $bestPackage = $literals[0]->getPackage(); + $bestPackage = $pool->literalToPackage($literals[0]); foreach ($literals as $i => $literal) { if (0 === $i) { continue; } - if ($this->versionCompare($literal->getPackage(), $bestPackage, '>')) { - $bestPackage = $literal->getPackage(); + $package = $pool->literalToPackage($literal); + + if ($this->versionCompare($package, $bestPackage, '>')) { + $bestPackage = $package; $bestLiterals = array($literal); - } else if ($this->versionCompare($literal->getPackage(), $bestPackage, '==')) { + } else if ($this->versionCompare($package, $bestPackage, '==')) { $bestLiterals[] = $literal; } } @@ -221,7 +217,7 @@ class DefaultPolicy implements PolicyInterface $priority = null; foreach ($literals as $literal) { - $package = $literal->getPackage(); + $package = $pool->literalToPackage($literal); if (isset($installedMap[$package->getId()])) { $selected[] = $literal; @@ -247,12 +243,12 @@ class DefaultPolicy implements PolicyInterface * * If no package is a local alias, nothing happens */ - protected function pruneRemoteAliases(array $literals) + protected function pruneRemoteAliases(Pool $pool, array $literals) { $hasLocalAlias = false; foreach ($literals as $literal) { - $package = $literal->getPackage(); + $package = $pool->literalToPackage($literal); if ($package instanceof AliasPackage && $package->isRootPackageAlias()) { $hasLocalAlias = true; @@ -266,7 +262,7 @@ class DefaultPolicy implements PolicyInterface $selected = array(); foreach ($literals as $literal) { - $package = $literal->getPackage(); + $package = $pool->literalToPackage($literal); if ($package instanceof AliasPackage && $package->isRootPackageAlias()) { $selected[] = $literal; diff --git a/src/Composer/DependencyResolver/Literal.php b/src/Composer/DependencyResolver/Literal.php deleted file mode 100644 index 7234b5857..000000000 --- a/src/Composer/DependencyResolver/Literal.php +++ /dev/null @@ -1,67 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\DependencyResolver; - -use Composer\Package\PackageInterface; - -/** - * @author Nils Adermann - */ -class Literal -{ - protected $package; - protected $wanted; - protected $id; - - public function __construct(PackageInterface $package, $wanted) - { - $this->package = $package; - $this->wanted = $wanted; - $this->id = ($this->wanted ? '' : '-') . $this->package->getId(); - } - - public function isWanted() - { - return $this->wanted; - } - - public function getPackage() - { - return $this->package; - } - - public function getPackageId() - { - return $this->package->getId(); - } - - public function getId() - { - return $this->id; - } - - public function __toString() - { - return ($this->wanted ? '+' : '-') . $this->getPackage(); - } - - public function inverted() - { - return new Literal($this->getPackage(), !$this->isWanted()); - } - - public function equals(Literal $b) - { - return $this->id === $b->id; - } -} diff --git a/src/Composer/DependencyResolver/PolicyInterface.php b/src/Composer/DependencyResolver/PolicyInterface.php index 7c26ab63f..f00f857d4 100644 --- a/src/Composer/DependencyResolver/PolicyInterface.php +++ b/src/Composer/DependencyResolver/PolicyInterface.php @@ -21,7 +21,6 @@ use Composer\Package\PackageInterface; interface PolicyInterface { function versionCompare(PackageInterface $a, PackageInterface $b, $operator); - function findUpdatePackages(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package); - function installable(Solver $solver, Pool $pool, array $installedMap, PackageInterface $package); + function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package); function selectPreferedPackages(Pool $pool, array $installedMap, array $literals); } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 2b8dec0ad..e56d906ef 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -151,4 +151,15 @@ class Pool return $result; } + + public function literalToPackage($literal) + { + $packageId = abs($literal); + return $this->packageById($packageId); + } + + public function literalToString($literal) + { + return ($literal > 0 ? '+' : '-') . $this->literalToPackage($literal); + } } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 8ac2ed4bb..b9266bae2 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -25,20 +25,6 @@ class Problem */ protected $reasons; - /** - * Add a job as a reason - * - * @param array $job A job descriptor which is a reason for this problem - * @param Rule $rule An optional rule associated with the job - */ - public function addJobRule($job, Rule $rule = null) - { - $this->addReason(serialize($job), array( - 'rule' => $rule, - 'job' => $job, - )); - } - /** * Add a rule as a reason * @@ -48,7 +34,7 @@ class Problem { $this->addReason($rule->getId(), array( 'rule' => $rule, - 'job' => null, + 'job' => $rule->getJob(), )); } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 3b9ffcc28..c0ba0de76 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -20,8 +20,6 @@ class Rule const RULE_INTERNAL_ALLOW_UPDATE = 1; const RULE_JOB_INSTALL = 2; const RULE_JOB_REMOVE = 3; - const RULE_JOB_LOCK = 4; - const RULE_NOT_INSTALLABLE = 5; const RULE_PACKAGE_CONFLICT = 6; const RULE_PACKAGE_REQUIRES = 7; const RULE_PACKAGE_OBSOLETES = 8; @@ -31,40 +29,35 @@ class Rule const RULE_LEARNED = 12; const RULE_PACKAGE_ALIAS = 13; + protected $pool; + protected $disabled; protected $literals; protected $type; protected $id; - protected $weak; - - public $watch1; - public $watch2; - public $next1; - public $next2; + protected $job; - public $ruleHash; + protected $ruleHash; - public function __construct(array $literals, $reason, $reasonData) + public function __construct(Pool $pool, array $literals, $reason, $reasonData, $job = null) { + $this->pool = $pool; + // sort all packages ascending by id - usort($literals, array($this, 'compareLiteralsById')); + sort($literals); $this->literals = $literals; $this->reason = $reason; $this->reasonData = $reasonData; $this->disabled = false; - $this->weak = false; - $this->watch1 = (count($this->literals) > 0) ? $literals[0]->getId() : 0; - $this->watch2 = (count($this->literals) > 1) ? $literals[1]->getId() : 0; + $this->job = $job; $this->type = -1; - $this->ruleHash = substr(md5(implode(',', array_map(function ($l) { - return $l->getId(); - }, $this->literals))), 0, 5); + $this->ruleHash = substr(md5(implode(',', $this->literals)), 0, 5); } public function getHash() @@ -82,6 +75,11 @@ class Rule return $this->id; } + public function getJob() + { + return $this->job; + } + /** * Checks if this rule is equal to another one * @@ -101,7 +99,7 @@ class Rule } for ($i = 0, $n = count($this->literals); $i < $n; $i++) { - if ($this->literals[$i]->getId() !== $rule->literals[$i]->getId()) { + if ($this->literals[$i] !== $rule->literals[$i]) { return false; } } @@ -139,16 +137,6 @@ class Rule return !$this->disabled; } - public function isWeak() - { - return $this->weak; - } - - public function setWeak($weak) - { - $this->weak = $weak; - } - public function getLiterals() { return $this->literals; @@ -159,24 +147,6 @@ class Rule return 1 === count($this->literals); } - public function getNext(Literal $literal) - { - if ($this->watch1 == $literal->getId()) { - return $this->next1; - } else { - return $this->next2; - } - } - - public function getOtherWatch(Literal $literal) - { - if ($this->watch1 == $literal->getId()) { - return $this->watch2; - } else { - return $this->watch1; - } - } - public function toHumanReadableString() { $ruleText = ''; @@ -184,7 +154,7 @@ class Rule if ($i != 0) { $ruleText .= '|'; } - $ruleText .= $literal; + $ruleText .= $this->pool->literalToString($literal); } switch ($this->reason) { @@ -197,25 +167,19 @@ class Rule case self::RULE_JOB_REMOVE: return "Remove command rule ($ruleText)"; - case self::RULE_JOB_LOCK: - return "Lock command rule ($ruleText)"; - - case self::RULE_NOT_INSTALLABLE: - return $ruleText; - case self::RULE_PACKAGE_CONFLICT: - $package1 = $this->literals[0]->getPackage(); - $package2 = $this->literals[1]->getPackage(); + $package1 = $this->pool->literalToPackage($this->literals[0]); + $package2 = $this->pool->literalToPackage($this->literals[1]); return 'Package "'.$package1.'" conflicts with "'.$package2.'"'; case self::RULE_PACKAGE_REQUIRES: $literals = $this->literals; $sourceLiteral = array_shift($literals); - $sourcePackage = $sourceLiteral->getPackage(); + $sourcePackage = $this->pool->literalToPackage($sourceLiteral); $requires = array(); foreach ($literals as $literal) { - $requires[] = $literal->getPackage(); + $requires[] = $this->pool->literalToPackage($literal); } $text = 'Package "'.$sourcePackage.'" contains the rule '.$this->reasonData.'. '; @@ -254,26 +218,11 @@ class Rule if ($i != 0) { $result .= '|'; } - $result .= $literal; + $result .= $this->pool->literalToString($literal); } $result .= ')'; return $result; } - - /** - * Comparison function for sorting literals by their id - * - * @param Literal $a - * @param Literal $b - * @return int 0 if the literals are equal, 1 if b is larger than a, -1 else - */ - private function compareLiteralsById(Literal $a, Literal $b) - { - if ($a->getId() === $b->getId()) { - return 0; - } - return $a->getId() < $b->getId() ? -1 : 1; - } } diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php new file mode 100644 index 000000000..67bf2fb8b --- /dev/null +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -0,0 +1,289 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\Repository\RepositoryInterface; +use Composer\Package\PackageInterface; +use Composer\Package\AliasPackage; +use Composer\DependencyResolver\Operation; + +/** + * @author Nils Adermann + */ +class RuleSetGenerator +{ + protected $policy; + protected $pool; + protected $rules; + protected $jobs; + protected $installedMap; + + public function __construct(PolicyInterface $policy, Pool $pool) + { + $this->policy = $policy; + $this->pool = $pool; + } + + /** + * Creates a new rule for the requirements of a package + * + * This rule is of the form (-A|B|C), where B and C are the providers of + * one requirement of the package A. + * + * @param PackageInterface $package The package with a requirement + * @param array $providers The providers of the requirement + * @param int $reason A RULE_* constant describing the + * reason for generating this rule + * @param mixed $reasonData Any data, e.g. the requirement name, + * that goes with the reason + * @return Rule The generated rule or null if tautological + */ + protected function createRequireRule(PackageInterface $package, array $providers, $reason, $reasonData = null) + { + $literals = array(-$package->getId()); + + foreach ($providers as $provider) { + // self fulfilling rule? + if ($provider === $package) { + return null; + } + $literals[] = $provider->getId(); + } + + return new Rule($this->pool, $literals, $reason, $reasonData); + } + + /** + * Creates a rule to install at least one of a set of packages + * + * The rule is (A|B|C) with A, B and C different packages. If the given + * set of packages is empty an impossible rule is generated. + * + * @param array $packages The set of packages to choose from + * @param int $reason A RULE_* constant describing the reason for + * generating this rule + * @param array $job The job this rule was created from + * @return Rule The generated rule + */ + protected function createInstallOneOfRule(array $packages, $reason, $job) + { + $literals = array(); + foreach ($packages as $package) { + $literals[] = $package->getId(); + } + + return new Rule($this->pool, $literals, $reason, $job['packageName'], $job); + } + + /** + * Creates a rule to remove a package + * + * The rule for a package A is (-A). + * + * @param PackageInterface $package The package to be removed + * @param int $reason A RULE_* constant describing the + * reason for generating this rule + * @param array $job The job this rule was created from + * @return Rule The generated rule + */ + protected function createRemoveRule(PackageInterface $package, $reason, $job) + { + return new Rule($this->pool, array(-$package->getId()), $reason, $job['packageName'], $job); + } + + /** + * Creates a rule for two conflicting packages + * + * The rule for conflicting packages A and B is (-A|-B). A is called the issuer + * and B the provider. + * + * @param PackageInterface $issuer The package declaring the conflict + * @param Package $provider The package causing the conflict + * @param int $reason A RULE_* constant describing the + * reason for generating this rule + * @param mixed $reasonData Any data, e.g. the package name, that + * goes with the reason + * @return Rule The generated rule + */ + protected function createConflictRule(PackageInterface $issuer, PackageInterface $provider, $reason, $reasonData = null) + { + // ignore self conflict + if ($issuer === $provider) { + return null; + } + + return new Rule($this->pool, array(-$issuer->getId(), -$provider->getId()), $reason, $reasonData); + } + + /** + * Adds a rule unless it duplicates an existing one of any type + * + * To be able to directly pass in the result of one of the rule creation + * methods. + * + * @param int $type A TYPE_* constant defining the rule type + * @param Rule $newRule The rule about to be added + */ + private function addRule($type, Rule $newRule = null) { + if ($this->rules->containsEqual($newRule)) { + return; + } + + $this->rules->add($newRule, $type); + } + + protected function addRulesForPackage(PackageInterface $package) + { + $workQueue = new \SplQueue; + $workQueue->enqueue($package); + + while (!$workQueue->isEmpty()) { + $package = $workQueue->dequeue(); + if (isset($this->addedMap[$package->getId()])) { + continue; + } + + $this->addedMap[$package->getId()] = true; + + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + + $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, (string) $link)); + + foreach ($possibleRequires as $require) { + $workQueue->enqueue($require); + } + } + + foreach ($package->getConflicts() as $link) { + $possibleConflicts = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + + foreach ($possibleConflicts as $conflict) { + $this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $conflict, Rule::RULE_PACKAGE_CONFLICT, (string) $link)); + } + } + + // check obsoletes and implicit obsoletes of a package + $isInstalled = (isset($this->installedMap[$package->getId()])); + + foreach ($package->getReplaces() as $link) { + $obsoleteProviders = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + + foreach ($obsoleteProviders as $provider) { + if ($provider === $package) { + continue; + } + + if (!$this->obsoleteImpossibleForAlias($package, $provider)) { + $reason = ($isInstalled) ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES; + $this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $provider, $reason, (string) $link)); + } + } + } + + // 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); + + 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)); + } + } + } + } + } + + protected function obsoleteImpossibleForAlias($package, $provider) + { + $packageIsAlias = $package instanceof AliasPackage; + $providerIsAlias = $provider instanceof AliasPackage; + + $impossible = ( + ($packageIsAlias && $package->getAliasOf() === $provider) || + ($providerIsAlias && $provider->getAliasOf() === $package) || + ($packageIsAlias && $providerIsAlias && $provider->getAliasOf() === $package->getAliasOf()) + ); + + return $impossible; + } + + /** + * Adds all rules for all update packages of a given package + * + * @param PackageInterface $package Rules for this package's updates are to + * be added + * @param bool $allowAll Whether downgrades are allowed + */ + private function addRulesForUpdatePackages(PackageInterface $package) + { + $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package); + + foreach ($updates as $update) { + $this->addRulesForPackage($update); + } + } + + protected function addRulesForJobs() + { + foreach ($this->jobs as $job) { + switch ($job['cmd']) { + case 'install': + if ($job['packages']) { + foreach ($job['packages'] as $package) { + if (!isset($this->installedMap[$package->getId()])) { + $this->addRulesForPackage($package); + } + } + + $rule = $this->createInstallOneOfRule($job['packages'], Rule::RULE_JOB_INSTALL, $job); + $this->addRule(RuleSet::TYPE_JOB, $rule); + } + break; + case 'remove': + // remove all packages with this name including uninstalled + // ones to make sure none of them are picked as replacements + foreach ($job['packages'] as $package) { + $rule = $this->createRemoveRule($package, Rule::RULE_JOB_REMOVE, $job); + $this->addRule(RuleSet::TYPE_JOB, $rule); + } + break; + } + } + } + + public function getRulesFor($jobs, $installedMap) + { + $this->jobs = $jobs; + $this->rules = new RuleSet; + $this->installedMap = $installedMap; + + foreach ($this->installedMap as $package) { + $this->addRulesForPackage($package); + $this->addRulesForUpdatePackages($package); + } + + $this->addRulesForJobs(); + + return $this->rules; + } +} diff --git a/src/Composer/DependencyResolver/RuleWatchChain.php b/src/Composer/DependencyResolver/RuleWatchChain.php new file mode 100644 index 000000000..2fea0d6ee --- /dev/null +++ b/src/Composer/DependencyResolver/RuleWatchChain.php @@ -0,0 +1,52 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +/** + * An extension of SplDoublyLinkedList with seek and removal of current element + * + * SplDoublyLinkedList only allows deleting a particular offset and has no + * method to set the internal iterator to a particular offset. + * + * @author Nils Adermann + */ +class RuleWatchChain extends \SplDoublyLinkedList +{ + protected $offset = 0; + + /** + * Moves the internal iterator to the specified offset + * + * @param int $offset The offset to seek to. + */ + public function seek($offset) + { + $this->rewind(); + for ($i = 0; $i < $offset; $i++, $this->next()); + } + + /** + * Removes the current element from the list + * + * As SplDoublyLinkedList only allows deleting a particular offset and + * incorrectly sets the internal iterator if you delete the current value + * this method sets the internal iterator back to the following element + * using the seek method. + */ + public function remove() + { + $offset = $this->key(); + $this->offsetUnset($offset); + $this->seek($offset); + } +} diff --git a/src/Composer/DependencyResolver/RuleWatchGraph.php b/src/Composer/DependencyResolver/RuleWatchGraph.php new file mode 100644 index 000000000..93b80f1ad --- /dev/null +++ b/src/Composer/DependencyResolver/RuleWatchGraph.php @@ -0,0 +1,149 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +/** + * The RuleWatchGraph efficiently propagates decisions to other rules + * + * All rules generated for solving a SAT problem should be inserted into the + * graph. When a decision on a literal is made, the graph can be used to + * propagate the decision to all other rules involving the rule, leading to + * other trivial decisions resulting from unit clauses. + * + * @author Nils Adermann + */ +class RuleWatchGraph +{ + protected $watchChains = array(); + + /** + * Inserts a rule node into the appropriate chains within the graph + * + * The node is prepended to the watch chains for each of the two literals it + * watches. + * + * Assertions are skipped because they only depend on a single package and + * have no alternative literal that could be true, so there is no need to + * watch chnages in any literals. + * + * @param RuleWatchNode $node The rule node to be inserted into the graph + */ + public function insert(RuleWatchNode $node) + { + if ($node->getRule()->isAssertion()) { + return; + } + + foreach (array($node->watch1, $node->watch2) as $literal) { + if (!isset($this->watchChains[$literal])) { + $this->watchChains[$literal] = new RuleWatchChain; + } + + $this->watchChains[$literal]->unshift($node); + } + } + + /** + * Propagates a decision on a literal to all rules watching the literal + * + * If a decision, e.g. +A has been made, then all rules containing -A, e.g. + * (-A|+B|+C) now need to satisfy at least one of the other literals, so + * that the rule as a whole becomes true, since with +A applied the rule + * is now (false|+B|+C) so essentialy (+B|+C). + * + * This means that all rules watching the literal -A need to be updated to + * watch 2 other literals which can still be satisfied instead. So literals + * that conflict with previously made decisions are not an option. + * + * Alternatively it can occur that a unit clause results: e.g. if in the + * above example the rule was (-A|+B), then A turning true means that + * B must now be decided true as well. + * + * @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 + * @return Rule|null If a conflict is found the conflicting rule is returned + */ + public function propagateLiteral($decidedLiteral, $level, $decisionsSatisfyCallback, $conflictCallback, $decideCallback) + { + // we invert the decided literal here, example: + // A was decided => (-A|B) now requires B to be true, so we look for + // rules which are fulfilled by -A, rather than A. + $literal = -$decidedLiteral; + + if (!isset($this->watchChains[$literal])) { + return null; + } + + $chain = $this->watchChains[$literal]; + + $chain->rewind(); + while ($chain->valid()) { + $node = $chain->current(); + $otherWatch = $node->getOtherWatch($literal); + + if (!$node->getRule()->isDisabled() && !call_user_func($decisionsSatisfyCallback, $otherWatch)) { + $ruleLiterals = $node->getRule()->getLiterals(); + + $alternativeLiterals = array_filter($ruleLiterals, function ($ruleLiteral) use ($literal, $otherWatch, $conflictCallback) { + return $literal !== $ruleLiteral && + $otherWatch !== $ruleLiteral && + !call_user_func($conflictCallback, $ruleLiteral); + }); + + if ($alternativeLiterals) { + reset($alternativeLiterals); + $this->moveWatch($literal, current($alternativeLiterals), $node); + continue; + } + + if (call_user_func($conflictCallback, $otherWatch)) { + return $node->getRule(); + } + + call_user_func($decideCallback, $otherWatch, $level, $node->getRule()); + } + + $chain->next(); + } + + return null; + } + + /** + * Moves a rule node from one watch chain to another + * + * The rule node's watched literals are updated accordingly. + * + * @param $fromLiteral A literal the node used to watch + * @param $toLiteral A literal the node should watch now + * @param $node The rule node to be moved + */ + protected function moveWatch($fromLiteral, $toLiteral, $node) + { + if (!isset($this->watchChains[$toLiteral])) { + $this->watchChains[$toLiteral] = new RuleWatchChain; + } + + $node->moveWatch($fromLiteral, $toLiteral); + $this->watchChains[$fromLiteral]->remove(); + $this->watchChains[$toLiteral]->unshift($node); + } +} diff --git a/src/Composer/DependencyResolver/RuleWatchNode.php b/src/Composer/DependencyResolver/RuleWatchNode.php new file mode 100644 index 000000000..88856974f --- /dev/null +++ b/src/Composer/DependencyResolver/RuleWatchNode.php @@ -0,0 +1,112 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +/** + * Wrapper around a Rule which keeps track of the two literals it watches + * + * Used by RuleWatchGraph to store rules in two RuleWatchChains. + * + * @author Nils Adermann + */ +class RuleWatchNode +{ + public $watch1; + public $watch2; + + protected $rule; + + /** + * Creates a new node watching the first and second literals of the rule. + * + * @param Rule $rule The rule to wrap + */ + public function __construct($rule) + { + $this->rule = $rule; + + $literals = $rule->getLiterals(); + + $this->watch1 = count($literals) > 0 ? $literals[0] : 0; + $this->watch2 = count($literals) > 1 ? $literals[1] : 0; + } + + /** + * Places the second watch on the rule's literal, decided at the highest level + * + * 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 + */ + public function watch2OnHighest($decisionMap) + { + $literals = $this->rule->getLiterals(); + + // if there are only 2 elements, both are being watched anyway + if ($literals < 3) { + return; + } + + $watchLevel = 0; + + foreach ($literals as $literal) { + $level = abs($decisionMap[abs($literal)]); + + if ($level > $watchLevel) { + $this->rule->watch2 = $literal; + $watchLevel = $level; + } + } + } + + /** + * Returns the rule this node wraps + * + * @return Rule + */ + public function getRule() + { + return $this->rule; + } + + /** + * Given one watched literal, this method returns the other watched literal + * + * @param int The watched literal that should not be returned + * @return int A literal + */ + public function getOtherWatch($literal) + { + if ($this->watch1 == $literal) { + return $this->watch2; + } else { + return $this->watch1; + } + } + + /** + * Moves a watch from one literal to another + * + * @param int $from The previously watched literal + * @param int $to The literal to be watched now + */ + public function moveWatch($from, $to) + { + if ($this->watch1 == $from) { + $this->watch1 = $to; + } else { + $this->watch2 = $to; + } + } +} diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index b49d430cc..5ec682d57 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -13,7 +13,6 @@ namespace Composer\DependencyResolver; use Composer\Repository\RepositoryInterface; -use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\DependencyResolver\Operation; @@ -26,18 +25,15 @@ class Solver protected $pool; protected $installed; protected $rules; + protected $ruleSetGenerator; protected $updateAll; - protected $ruleToJob = array(); protected $addedMap = array(); protected $updateMap = array(); - protected $watches = array(); - protected $removeWatches = array(); + protected $watchGraph; protected $decisionMap; protected $installedMap; - protected $packageToUpdateRule = array(); - protected $decisionQueue = array(); protected $decisionQueueWhy = array(); protected $decisionQueueFree = array(); @@ -45,335 +41,19 @@ class Solver protected $branches = array(); protected $problems = array(); protected $learnedPool = array(); - protected $recommendsIndex; public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed) { $this->policy = $policy; $this->pool = $pool; $this->installed = $installed; - $this->rules = new RuleSet; - } - - /** - * Creates a new rule for the requirements of a package - * - * This rule is of the form (-A|B|C), where B and C are the providers of - * one requirement of the package A. - * - * @param PackageInterface $package The package with a requirement - * @param array $providers The providers of the requirement - * @param int $reason A RULE_* constant describing the - * reason for generating this rule - * @param mixed $reasonData Any data, e.g. the requirement name, - * that goes with the reason - * @return Rule The generated rule or null if tautological - */ - protected function createRequireRule(PackageInterface $package, array $providers, $reason, $reasonData = null) - { - $literals = array(new Literal($package, false)); - - foreach ($providers as $provider) { - // self fulfilling rule? - if ($provider === $package) { - return null; - } - $literals[] = new Literal($provider, true); - } - - return new Rule($literals, $reason, $reasonData); - } - - /** - * Create a new rule for updating a package - * - * If package A1 can be updated to A2 or A3 the rule is (A1|A2|A3). - * - * @param PackageInterface $package The package to be updated - * @param array $updates An array of update candidate packages - * @param int $reason A RULE_* constant describing the - * reason for generating this rule - * @param mixed $reasonData Any data, e.g. the package name, that - * goes with the reason - * @return Rule The generated rule or null if tautology - */ - protected function createUpdateRule(PackageInterface $package, array $updates, $reason, $reasonData = null) - { - $literals = array(new Literal($package, true)); - - foreach ($updates as $update) { - $literals[] = new Literal($update, true); - } - - return new Rule($literals, $reason, $reasonData); - } - - /** - * Creates a new rule for installing a package - * - * The rule is simply (A) for a package A to be installed. - * - * @param PackageInterface $package The package to be installed - * @param int $reason A RULE_* constant describing the - * reason for generating this rule - * @param mixed $reasonData Any data, e.g. the package name, that - * goes with the reason - * @return Rule The generated rule - */ - protected function createInstallRule(PackageInterface $package, $reason, $reasonData = null) - { - return new Rule(new Literal($package, true)); - } - - /** - * Creates a rule to install at least one of a set of packages - * - * The rule is (A|B|C) with A, B and C different packages. If the given - * set of packages is empty an impossible rule is generated. - * - * @param array $packages The set of packages to choose from - * @param int $reason A RULE_* constant describing the reason for - * generating this rule - * @param mixed $reasonData Any data, e.g. the package name, that goes with - * the reason - * @return Rule The generated rule - */ - protected function createInstallOneOfRule(array $packages, $reason, $reasonData = null) - { - $literals = array(); - foreach ($packages as $package) { - $literals[] = new Literal($package, true); - } - - return new Rule($literals, $reason, $reasonData); - } - - /** - * Creates a rule to remove a package - * - * The rule for a package A is (-A). - * - * @param PackageInterface $package The package to be removed - * @param int $reason A RULE_* constant describing the - * reason for generating this rule - * @param mixed $reasonData Any data, e.g. the package name, that - * goes with the reason - * @return Rule The generated rule - */ - protected function createRemoveRule(PackageInterface $package, $reason, $reasonData = null) - { - return new Rule(array(new Literal($package, false)), $reason, $reasonData); - } - - /** - * Creates a rule for two conflicting packages - * - * The rule for conflicting packages A and B is (-A|-B). A is called the issuer - * and B the provider. - * - * @param PackageInterface $issuer The package declaring the conflict - * @param Package $provider The package causing the conflict - * @param int $reason A RULE_* constant describing the - * reason for generating this rule - * @param mixed $reasonData Any data, e.g. the package name, that - * goes with the reason - * @return Rule The generated rule - */ - protected function createConflictRule(PackageInterface $issuer, PackageInterface $provider, $reason, $reasonData = null) - { - // ignore self conflict - if ($issuer === $provider) { - return null; - } - - return new Rule(array(new Literal($issuer, false), new Literal($provider, false)), $reason, $reasonData); - } - - /** - * Adds a rule unless it duplicates an existing one of any type - * - * To be able to directly pass in the result of one of the rule creation - * methods the rule may also be null to indicate that no rule should be - * added. - * - * @param int $type A TYPE_* constant defining the rule type - * @param Rule $newRule The rule about to be added - */ - private function addRule($type, Rule $newRule = null) { - if ($newRule) { - if ($this->rules->containsEqual($newRule)) { - return; - } - - $this->rules->add($newRule, $type); - } + $this->ruleSetGenerator = new RuleSetGenerator($policy, $pool); } - protected function addRulesForPackage(PackageInterface $package) - { - $workQueue = new \SplQueue; - $workQueue->enqueue($package); - - while (!$workQueue->isEmpty()) { - $package = $workQueue->dequeue(); - if (isset($this->addedMap[$package->getId()])) { - continue; - } - - $this->addedMap[$package->getId()] = true; - - if (!$this->policy->installable($this, $this->pool, $this->installedMap, $package)) { - $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRemoveRule($package, Rule::RULE_NOT_INSTALLABLE, (string) $package)); - continue; - } - - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); - - $this->addRule(RuleSet::TYPE_PACKAGE, $rule = $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, (string) $link)); - - foreach ($possibleRequires as $require) { - $workQueue->enqueue($require); - } - } - - foreach ($package->getConflicts() as $link) { - $possibleConflicts = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); - - foreach ($possibleConflicts as $conflict) { - $this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $conflict, Rule::RULE_PACKAGE_CONFLICT, (string) $link)); - } - } - - // check obsoletes and implicit obsoletes of a package - $isInstalled = (isset($this->installedMap[$package->getId()])); - - foreach ($package->getReplaces() as $link) { - $obsoleteProviders = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); - - foreach ($obsoleteProviders as $provider) { - if ($provider === $package) { - continue; - } - - if (!$this->obsoleteImpossibleForAlias($package, $provider)) { - $reason = ($isInstalled) ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES; - $this->addRule(RuleSet::TYPE_PACKAGE, $this->createConflictRule($package, $provider, $reason, (string) $link)); - } - } - } - - // 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); - - 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)); - } else if (!$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)); - } - } - } - } - } - - protected function obsoleteImpossibleForAlias($package, $provider) - { - $packageIsAlias = $package instanceof AliasPackage; - $providerIsAlias = $provider instanceof AliasPackage; - - $impossible = ( - ($packageIsAlias && $package->getAliasOf() === $provider) || - ($providerIsAlias && $provider->getAliasOf() === $package) || - ($packageIsAlias && $providerIsAlias && $provider->getAliasOf() === $package->getAliasOf()) - ); - - return $impossible; - } - - /** - * Adds all rules for all update packages of a given package - * - * @param PackageInterface $package Rules for this package's updates are to - * be added - * @param bool $allowAll Whether downgrades are allowed - */ - private function addRulesForUpdatePackages(PackageInterface $package) - { - $updates = $this->policy->findUpdatePackages($this, $this->pool, $this->installedMap, $package); - - $this->addRulesForPackage($package); - - foreach ($updates as $update) { - $this->addRulesForPackage($update); - } - } - - /** - * Alters watch chains for a rule. - * - * Next1/2 always points to the next rule that is watching the same package. - * The watches array contains rules to start from for each package - * - */ - private function addWatchesToRule(Rule $rule) - { - // skip simple assertions of the form (A) or (-A) - if ($rule->isAssertion()) { - return; - } - - if (!isset($this->watches[$rule->watch1])) { - $this->watches[$rule->watch1] = null; - } - - $rule->next1 = $this->watches[$rule->watch1]; - $this->watches[$rule->watch1] = $rule; - - if (!isset($this->watches[$rule->watch2])) { - $this->watches[$rule->watch2] = null; - } - - $rule->next2 = $this->watches[$rule->watch2]; - $this->watches[$rule->watch2] = $rule; - } - - /** - * Put watch2 on rule's literal with highest level - */ - private function watch2OnHighest(Rule $rule) - { - $literals = $rule->getLiterals(); - - // if there are only 2 elements, both are being watched anyway - if ($literals < 3) { - return; - } - - $watchLevel = 0; - - foreach ($literals as $literal) { - $level = abs($this->decisionMap[$literal->getPackageId()]); - - if ($level > $watchLevel) { - $rule->watch2 = $literal->getId(); - $watchLevel = $level; - } - } - } - - private function findDecisionRule(PackageInterface $package) + private function findDecisionRule($packageId) { foreach ($this->decisionQueue as $i => $literal) { - if ($package === $literal->getPackage()) { + if ($packageId === abs($literal)) { return $this->decisionQueueWhy[$i]; } } @@ -384,24 +64,20 @@ class Solver // aka solver_makeruledecisions private function makeAssertionRuleDecisions() { - // do we need to decide a SYSTEMSOLVABLE at level 1? - $decisionStart = count($this->decisionQueue); for ($ruleIndex = 0; $ruleIndex < count($this->rules); $ruleIndex++) { $rule = $this->rules->ruleById($ruleIndex); - if ($rule->isWeak() || !$rule->isAssertion() || $rule->isDisabled()) { + if (!$rule->isAssertion() || $rule->isDisabled()) { continue; } $literals = $rule->getLiterals(); $literal = $literals[0]; - if (!$this->decided($literal->getPackage())) { - $this->decisionQueue[] = $literal; - $this->decisionQueueWhy[] = $rule; - $this->addDecision($literal, 1); + if (!$this->decided(abs($literal))) { + $this->decide($literal, 1, $rule); continue; } @@ -415,56 +91,40 @@ class Solver continue; } - $conflict = $this->findDecisionRule($literal->getPackage()); - /** TODO: handle conflict with systemsolvable? */ + $conflict = $this->findDecisionRule(abs($literal)); if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) { $problem = new Problem; - if ($rule->getType() == RuleSet::TYPE_JOB) { - $job = $this->ruleToJob[$rule->getId()]; - - $problem->addJobRule($job, $rule); - $problem->addRule($conflict); - $this->disableProblem($job); - } else { - $problem->addRule($rule); - $problem->addRule($conflict); - $this->disableProblem($rule); - } + $problem->addRule($rule); + $problem->addRule($conflict); + $this->disableProblem($rule); $this->problems[] = $problem; continue; } - // conflict with another job or update/feature rule + // conflict with another job $problem = new Problem; $problem->addRule($rule); $problem->addRule($conflict); - // push all of our rules (can only be feature or job rules) + // push all of our rules (can only be job rules) // asserting this literal on the problem stack foreach ($this->rules->getIteratorFor(RuleSet::TYPE_JOB) as $assertRule) { - if ($assertRule->isDisabled() || !$assertRule->isAssertion() || $assertRule->isWeak()) { + if ($assertRule->isDisabled() || !$assertRule->isAssertion()) { continue; } $assertRuleLiterals = $assertRule->getLiterals(); $assertRuleLiteral = $assertRuleLiterals[0]; - if ($literal->getPackageId() !== $assertRuleLiteral->getPackageId()) { + if (abs($literal) !== abs($assertRuleLiteral)) { continue; } - if ($assertRule->getType() === RuleSet::TYPE_JOB) { - $job = $this->ruleToJob[$assertRule->getId()]; - - $problem->addJobRule($job, $assertRule); - $this->disableProblem($job); - } else { - $problem->addRule($assertRule); - $this->disableProblem($assertRule); - } + $problem->addRule($assertRule); + $this->disableProblem($assertRule); } $this->problems[] = $problem; @@ -473,39 +133,10 @@ class Solver $decisionLiteral = array_pop($this->decisionQueue); array_pop($this->decisionQueueWhy); unset($this->decisionQueueFree[count($this->decisionQueue)]); - $this->decisionMap[$decisionLiteral->getPackageId()] = 0; + $this->decisionMap[abs($decisionLiteral)] = 0; } $ruleIndex = -1; } - - foreach ($this->rules as $rule) { - if (!$rule->isWeak() || !$rule->isAssertion() || $rule->isDisabled()) { - continue; - } - - $literals = $rule->getLiterals(); - $literal = $literals[0]; - - if ($this->decisionMap[$literal->getPackageId()] == 0) { - $this->decisionQueue[] = $literal; - $this->decisionQueueWhy[] = $rule; - $this->addDecision($literal, 1); - continue; - } - - if ($this->decisionsSatisfy($literals[0])) { - continue; - } - - // conflict, but this is a weak rule => disable - if ($rule->getType() == RuleSet::TYPE_JOB) { - $why = $this->ruleToJob[$rule->getId()]; - } else { - $why = $rule; - } - - $this->disableProblem($why); - } } protected function setupInstalledMap() @@ -514,246 +145,64 @@ class Solver foreach ($this->installed->getPackages() as $package) { $this->installedMap[$package->getId()] = $package; } - } - - public function solve(Request $request) - { - $this->jobs = $request->getJobs(); - - $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); - } foreach ($this->jobs as $job) { - foreach ($job['packages'] as $package) { - switch ($job['cmd']) { - case 'update': + switch ($job['cmd']) { + case 'update': + foreach ($job['packages'] as $package) { if (isset($this->installedMap[$package->getId()])) { $this->updateMap[$package->getId()] = true; } - break; - } - } + } + break; - switch ($job['cmd']) { case 'update-all': foreach ($this->installedMap as $package) { $this->updateMap[$package->getId()] = true; } - break; - } - } - - foreach ($this->installedMap as $package) { - $this->addRulesForPackage($package); - } - - foreach ($this->installedMap as $package) { - $this->addRulesForUpdatePackages($package); - } - - - foreach ($this->jobs as $job) { - foreach ($job['packages'] as $package) { - switch ($job['cmd']) { - case 'install': - $this->installCandidateMap[$package->getId()] = true; - $this->addRulesForPackage($package); break; - } - } - } - - foreach ($this->installedMap as $package) { - $updates = $this->policy->findUpdatePackages($this, $this->pool, $this->installedMap, $package); - $rule = $this->createUpdateRule($package, $updates, Rule::RULE_INTERNAL_ALLOW_UPDATE, (string) $package); - $this->packageToUpdateRule[$package->getId()] = $rule; - } - - foreach ($this->jobs as $job) { - switch ($job['cmd']) { case 'install': - if (empty($job['packages'])) { + if (!$job['packages']) { $problem = new Problem(); - $problem->addJobRule($job); + $problem->addRule(new Rule($this->pool, array(), null, null, $job)); $this->problems[] = $problem; - } else { - $rule = $this->createInstallOneOfRule($job['packages'], Rule::RULE_JOB_INSTALL, $job['packageName']); - $this->addRule(RuleSet::TYPE_JOB, $rule); - $this->ruleToJob[$rule->getId()] = $job; - } - break; - case 'remove': - // remove all packages with this name including uninstalled - // ones to make sure none of them are picked as replacements - - // todo: cleandeps - foreach ($job['packages'] as $package) { - $rule = $this->createRemoveRule($package, Rule::RULE_JOB_REMOVE); - $this->addRule(RuleSet::TYPE_JOB, $rule); - $this->ruleToJob[$rule->getId()] = $job; } break; - case 'lock': - foreach ($job['packages'] as $package) { - if (isset($this->installedMap[$package->getId()])) { - $rule = $this->createInstallRule($package, Rule::RULE_JOB_LOCK); - } else { - $rule = $this->createRemoveRule($package, Rule::RULE_JOB_LOCK); - } - $this->addRule(RuleSet::TYPE_JOB, $rule); - $this->ruleToJob[$rule->getId()] = $job; - } - break; } } - - foreach ($this->rules as $rule) { - $this->addWatchesToRule($rule); - } - - /* make decisions based on job/update assertions */ - $this->makeAssertionRuleDecisions(); - - $installRecommended = 0; - $this->runSat(true, $installRecommended); - - if ($this->problems) { - throw new SolverProblemsException($this->problems); - } - - return $this->createTransaction(); } - protected function createTransaction() + public function solve(Request $request) { - $transaction = array(); - $installMeansUpdateMap = array(); - - foreach ($this->decisionQueue as $i => $literal) { - $package = $literal->getPackage(); - - // !wanted & installed - if (!$literal->isWanted() && isset($this->installedMap[$package->getId()])) { - $literals = array(); - - if (isset($this->packageToUpdateRule[$package->getId()])) { - $literals = array_merge($literals, $this->packageToUpdateRule[$package->getId()]->getLiterals()); - } - - foreach ($literals as $updateLiteral) { - if (!$updateLiteral->equals($literal)) { - $installMeansUpdateMap[$updateLiteral->getPackageId()] = $package; - } - } - } - } - - foreach ($this->decisionQueue as $i => $literal) { - $package = $literal->getPackage(); - - // wanted & installed || !wanted & !installed - if ($literal->isWanted() == (isset($this->installedMap[$package->getId()]))) { - continue; - } - - if ($literal->isWanted()) { - if ($package instanceof AliasPackage) { - $transaction[] = new Operation\MarkAliasInstalledOperation( - $package, $this->decisionQueueWhy[$i] - ); - continue; - } - - if (isset($installMeansUpdateMap[$literal->getPackageId()])) { - - $source = $installMeansUpdateMap[$literal->getPackageId()]; - - $transaction[] = new Operation\UpdateOperation( - $source, $package, $this->decisionQueueWhy[$i] - ); + $this->jobs = $request->getJobs(); - // avoid updates to one package from multiple origins - unset($installMeansUpdateMap[$literal->getPackageId()]); - $ignoreRemove[$source->getId()] = true; - } else { - $transaction[] = new Operation\InstallOperation( - $package, $this->decisionQueueWhy[$i] - ); - } - } else if (!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] - ); - } - } - } + $this->setupInstalledMap(); - $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; - } - } + 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); } - 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->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap); + $this->watchGraph = new RuleWatchGraph; - $this->decisionMap[$packageId] = -1; - } + foreach ($this->rules as $rule) { + $this->watchGraph->insert(new RuleWatchNode($rule)); } - foreach ($allDecidedMap as $packageId => $decision) { - if ($packageId === 0) { - continue; - } - - if (0 == $decision && isset($this->installedMap[$packageId])) { - $package = $this->pool->packageById($packageId); + /* make decisions based on job/update assertions */ + $this->makeAssertionRuleDecisions(); - if ($package instanceof AliasPackage) { - $transaction[] = new Operation\MarkAliasInstalledOperation( - $package, null - ); - } else { - $transaction[] = new Operation\UninstallOperation( - $package, null - ); - } + $this->runSat(true); - $this->decisionMap[$packageId] = -1; - } + if ($this->problems) { + throw new SolverProblemsException($this->problems); } - return array_reverse($transaction); + $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisionMap, $this->decisionQueue, $this->decisionQueueWhy); + return $transaction->getOperations(); } protected function literalFromId($id) @@ -762,86 +211,71 @@ class Solver return new Literal($package, $id > 0); } - protected function addDecision(Literal $l, $level) - { - $this->addDecisionId($l->getId(), $level); - } - - protected function addDecisionId($literalId, $level) + protected function addDecision($literal, $level) { - $packageId = abs($literalId); + $packageId = abs($literal); $previousDecision = $this->decisionMap[$packageId]; if ($previousDecision != 0) { - $literal = $this->literalFromId($literalId); + $literalString = $this->pool->literalToString($literal); + $package = $this->pool->literalToPackage($literal); throw new SolverBugException( - "Trying to decide $literal on level $level, even though ".$literal->getPackage()." was previously decided as ".(int) $previousDecision."." + "Trying to decide $literalString on level $level, even though $package was previously decided as ".(int) $previousDecision."." ); } - if ($literalId > 0) { + if ($literal > 0) { $this->decisionMap[$packageId] = $level; } else { $this->decisionMap[$packageId] = -$level; } } - protected function decisionsContain(Literal $l) + public function decide($literal, $level, $why) { - return ( - $this->decisionMap[$l->getPackageId()] > 0 && $l->isWanted() || - $this->decisionMap[$l->getPackageId()] < 0 && !$l->isWanted() - ); + $this->addDecision($literal, $level); + $this->decisionQueue[] = $literal; + $this->decisionQueueWhy[] = $why; } - protected function decisionsContainId($literalId) + public function decisionsContain($literal) { - $packageId = abs($literalId); + $packageId = abs($literal); return ( - $this->decisionMap[$packageId] > 0 && $literalId > 0 || - $this->decisionMap[$packageId] < 0 && $literalId < 0 + $this->decisionMap[$packageId] > 0 && $literal > 0 || + $this->decisionMap[$packageId] < 0 && $literal < 0 ); } - protected function decisionsSatisfy(Literal $l) - { - return ($l->isWanted() && $this->decisionMap[$l->getPackageId()] > 0) || - (!$l->isWanted() && $this->decisionMap[$l->getPackageId()] < 0); - } - - protected function decisionsConflict(Literal $l) + protected function decisionsSatisfy($literal) { + $packageId = abs($literal); return ( - $this->decisionMap[$l->getPackageId()] > 0 && !$l->isWanted() || - $this->decisionMap[$l->getPackageId()] < 0 && $l->isWanted() + $literal > 0 && $this->decisionMap[$packageId] > 0 || + $literal < 0 && $this->decisionMap[$packageId] < 0 ); } - protected function decisionsConflictId($literalId) + public function decisionsConflict($literal) { - $packageId = abs($literalId); + $packageId = abs($literal); return ( - ($this->decisionMap[$packageId] > 0 && $literalId < 0) || - ($this->decisionMap[$packageId] < 0 && $literalId > 0) + ($this->decisionMap[$packageId] > 0 && $literal < 0) || + ($this->decisionMap[$packageId] < 0 && $literal > 0) ); } - - protected function decided(PackageInterface $p) + protected function decided($packageId) { - return $this->decisionMap[$p->getId()] != 0; + return $this->decisionMap[$packageId] != 0; } - protected function undecided(PackageInterface $p) + protected function undecided($packageId) { - return $this->decisionMap[$p->getId()] == 0; - } - - protected function decidedInstall(PackageInterface $p) { - return $this->decisionMap[$p->getId()] > 0; + return $this->decisionMap[$packageId] == 0; } - protected function decidedRemove(PackageInterface $p) { - return $this->decisionMap[$p->getId()] < 0; + protected function decidedInstall($packageId) { + return $this->decisionMap[$packageId] > 0; } /** @@ -855,76 +289,16 @@ class Solver protected function propagate($level) { while ($this->propagateIndex < count($this->decisionQueue)) { - // we invert the decided literal here, example: - // A was decided => (-A|B) now requires B to be true, so we look for - // rules which are fulfilled by -A, rather than A. - - $literal = $this->decisionQueue[$this->propagateIndex]->inverted(); - - $this->propagateIndex++; - - // /* foreach rule where 'pkg' is now FALSE */ - //for (rp = watches + pkg; *rp; rp = next_rp) - if (!isset($this->watches[$literal->getId()])) { - continue; - } - - $prevRule = null; - for ($rule = $this->watches[$literal->getId()]; $rule !== null; $prevRule = $rule, $rule = $nextRule) { - $nextRule = $rule->getNext($literal); - - if ($rule->isDisabled()) { - continue; - } - - $otherWatch = $rule->getOtherWatch($literal); - - if ($this->decisionsContainId($otherWatch)) { - continue; - } - - $ruleLiterals = $rule->getLiterals(); - - if (sizeof($ruleLiterals) > 2) { - foreach ($ruleLiterals as $ruleLiteral) { - if ($otherWatch !== $ruleLiteral->getId() && - !$this->decisionsConflict($ruleLiteral)) { - - if ($literal->getId() === $rule->watch1) { - $rule->watch1 = $ruleLiteral->getId(); - $rule->next1 = (isset($this->watches[$ruleLiteral->getId()])) ? $this->watches[$ruleLiteral->getId()] : null; - } else { - $rule->watch2 = $ruleLiteral->getId(); - $rule->next2 = (isset($this->watches[$ruleLiteral->getId()])) ? $this->watches[$ruleLiteral->getId()] : null; - } - - if ($prevRule) { - if ($prevRule->next1 == $rule) { - $prevRule->next1 = $nextRule; - } else { - $prevRule->next2 = $nextRule; - } - } else { - $this->watches[$literal->getId()] = $nextRule; - } - - $this->watches[$ruleLiteral->getId()] = $rule; - - $rule = $prevRule; - continue 2; - } - } - } - - // yay, we found a unit clause! try setting it to true - if ($this->decisionsConflictId($otherWatch)) { - return $rule; - } - - $this->addDecisionId($otherWatch, $level); + $conflict = $this->watchGraph->propagateLiteral( + $this->decisionQueue[$this->propagateIndex++], + $level, + array($this, 'decisionsContain'), + array($this, 'decisionsConflict'), + array($this, 'decide') + ); - $this->decisionQueue[] = $this->literalFromId($otherWatch); - $this->decisionQueueWhy[] = $rule; + if ($conflict) { + return $conflict; } } @@ -939,17 +313,17 @@ class Solver while (!empty($this->decisionQueue)) { $literal = $this->decisionQueue[count($this->decisionQueue) - 1]; - if (!$this->decisionMap[$literal->getPackageId()]) { + if (!$this->decisionMap[abs($literal)]) { break; } - $decisionLevel = abs($this->decisionMap[$literal->getPackageId()]); + $decisionLevel = abs($this->decisionMap[abs($literal)]); if ($decisionLevel <= $level) { break; } - $this->decisionMap[$literal->getPackageId()] = 0; + $this->decisionMap[abs($literal)] = 0; array_pop($this->decisionQueue); array_pop($this->decisionQueueWhy); @@ -965,8 +339,6 @@ class Solver array_pop($this->branches); } - - $this->recommendsIndex = -1; } /**------------------------------------------------------------------- @@ -984,13 +356,11 @@ class Solver * returns the new solver level or 0 if unsolvable * */ - private function setPropagateLearn($level, Literal $literal, $disableRules, Rule $rule) + private function setPropagateLearn($level, $literal, $disableRules, Rule $rule) { $level++; - $this->addDecision($literal, $level); - $this->decisionQueue[] = $literal; - $this->decisionQueueWhy[] = $rule; + $this->decide($literal, $level, $rule); $this->decisionQueueFree[count($this->decisionQueueWhy) - 1] = true; while (true) { @@ -1011,7 +381,7 @@ class Solver throw new SolverBugException( "Trying to revert to invalid level ".(int) $newLevel." from level ".(int) $level."." ); - } else if (!$newRule) { + } elseif (!$newRule) { throw new SolverBugException( "No rule was learned from analyzing $rule at level $level." ); @@ -1021,16 +391,15 @@ class Solver $this->revert($level); - $this->addRule(RuleSet::TYPE_LEARNED, $newRule); + $this->rules->add($newRule, RuleSet::TYPE_LEARNED); $this->learnedWhy[$newRule->getId()] = $why; - $this->watch2OnHighest($newRule); - $this->addWatchesToRule($newRule); + $ruleNode = new RuleWatchNode($newRule); + $ruleNode->watch2OnHighest($this->decisionMap); + $this->watchGraph->insert($ruleNode); - $this->addDecision($learnLiteral, $level); - $this->decisionQueue[] = $learnLiteral; - $this->decisionQueueWhy[] = $newRule; + $this->decide($learnLiteral, $level, $newRule); } return $level; @@ -1073,16 +442,16 @@ class Solver continue; } - if (isset($seen[$literal->getPackageId()])) { + if (isset($seen[abs($literal)])) { continue; } - $seen[$literal->getPackageId()] = true; + $seen[abs($literal)] = true; - $l = abs($this->decisionMap[$literal->getPackageId()]); + $l = abs($this->decisionMap[abs($literal)]); if (1 === $l) { $l1num++; - } else if ($level === $l) { + } elseif ($level === $l) { $num++; } else { // not level1 or conflict level, add to new rule @@ -1114,15 +483,15 @@ class Solver $literal = $this->decisionQueue[$decisionId]; - if (isset($seen[$literal->getPackageId()])) { + if (isset($seen[abs($literal)])) { break; } } - unset($seen[$literal->getPackageId()]); + unset($seen[abs($literal)]); if ($num && 0 === --$num) { - $learnedLiterals[0] = $this->literalFromId(-$literal->getPackageId()); + $learnedLiterals[0] = -abs($literal); if (!$l1num) { break 2; @@ -1130,7 +499,7 @@ class Solver foreach ($learnedLiterals as $i => $learnedLiteral) { if ($i !== 0) { - unset($seen[$literal->getPackageId()]); + unset($seen[abs($learnedLiteral)]); } } // only level 1 marks left @@ -1150,12 +519,12 @@ class Solver ); } - $newRule = new Rule($learnedLiterals, Rule::RULE_LEARNED, $why); + $newRule = new Rule($this->pool, $learnedLiterals, Rule::RULE_LEARNED, $why); return array($learnedLiterals[0], $ruleLevel, $newRule, $why); } - private function analyzeUnsolvableRule($problem, $conflictRule, &$lastWeakWhy) + private function analyzeUnsolvableRule($problem, $conflictRule) { $why = $conflictRule->getId(); @@ -1164,7 +533,7 @@ class Solver $problemRules = $this->learnedPool[$learnedWhy]; foreach ($problemRules as $problemRule) { - $this->analyzeUnsolvableRule($problem, $problemRule, $lastWeakWhy); + $this->analyzeUnsolvableRule($problem, $problemRule); } return; } @@ -1174,46 +543,27 @@ class Solver return; } - if ($conflictRule->isWeak()) { - /** TODO why > or < lastWeakWhy? */ - if (!$lastWeakWhy || $why > $lastWeakWhy->getId()) { - $lastWeakWhy = $conflictRule; - } - } - - if ($conflictRule->getType() == RuleSet::TYPE_JOB) { - $job = $this->ruleToJob[$conflictRule->getId()]; - $problem->addJobRule($job, $conflictRule); - } else { - $problem->addRule($conflictRule); - } + $problem->addRule($conflictRule); } private function analyzeUnsolvable($conflictRule, $disableRules) { - $lastWeakWhy = null; $problem = new Problem; $problem->addRule($conflictRule); - $this->analyzeUnsolvableRule($problem, $conflictRule, $lastWeakWhy); + $this->analyzeUnsolvableRule($problem, $conflictRule); $this->problems[] = $problem; $seen = array(); $literals = $conflictRule->getLiterals(); -/* unnecessary because unlike rule.d, watch2 == 2nd literal, unless watch2 changed - if (sizeof($literals) == 2) { - $literals[1] = $this->literalFromId($conflictRule->watch2); - } -*/ - foreach ($literals as $literal) { // skip the one true literal if ($this->decisionsSatisfy($literal)) { continue; } - $seen[$literal->getPackageId()] = true; + $seen[abs($literal)] = true; } $decisionId = count($this->decisionQueue); @@ -1224,53 +574,29 @@ class Solver $literal = $this->decisionQueue[$decisionId]; // skip literals that are not in this rule - if (!isset($seen[$literal->getPackageId()])) { + if (!isset($seen[abs($literal)])) { continue; } $why = $this->decisionQueueWhy[$decisionId]; $problem->addRule($why); - $this->analyzeUnsolvableRule($problem, $why, $lastWeakWhy); + $this->analyzeUnsolvableRule($problem, $why); $literals = $why->getLiterals(); -/* unnecessary because unlike rule.d, watch2 == 2nd literal, unless watch2 changed - if (sizeof($literals) == 2) { - $literals[1] = $this->literalFromId($why->watch2); - } -*/ foreach ($literals as $literal) { // skip the one true literal if ($this->decisionsSatisfy($literal)) { continue; } - $seen[$literal->getPackageId()] = true; - } - } - - if ($lastWeakWhy) { - array_pop($this->problems); - - if ($lastWeakWhy->getType() === RuleSet::TYPE_JOB) { - $why = $this->ruleToJob[$lastWeakWhy]; - } else { - $why = $lastWeakWhy; + $seen[abs($literal)] = true; } - - $this->disableProblem($why); - $this->resetSolver(); - - return 1; } if ($disableRules) { foreach ($this->problems[count($this->problems) - 1] as $reason) { - if ($reason['job']) { - $this->disableProblem($reason['job']); - } else { - $this->disableProblem($reason['rule']); - } + $this->disableProblem($reason['rule']); } $this->resetSolver(); @@ -1282,14 +608,15 @@ class Solver private function disableProblem($why) { - if ($why instanceof Rule) { - $why->disable(); - } else if (is_array($why)) { + $job = $why->getJob(); + if (!$job) { + $why->disable(); + } else { // disable all rules of this job - foreach ($this->ruleToJob as $ruleId => $job) { - if ($why === $job) { - $this->rules->ruleById($ruleId)->disable(); + foreach ($this->rules as $rule) { + if ($job === $rule->getJob()) { + $rule->disable(); } } } @@ -1298,14 +625,12 @@ class Solver private function resetSolver() { while ($literal = array_pop($this->decisionQueue)) { - $this->decisionMap[$literal->getPackageId()] = 0; + $this->decisionMap[abs($literal)] = 0; } $this->decisionQueueWhy = array(); $this->decisionQueueFree = array(); - $this->recommendsIndex = -1; $this->propagateIndex = 0; - $this->recommendations = array(); $this->branches = array(); $this->enableDisableLearnedRules(); @@ -1335,13 +660,13 @@ class Solver if ($foundDisabled && $rule->isEnabled()) { $rule->disable(); - } else if (!$foundDisabled && $rule->isDisabled()) { + } elseif (!$foundDisabled && $rule->isDisabled()) { $rule->enable(); } } } - private function runSat($disableRules = true, $installRecommended = false) + private function runSat($disableRules = true) { $this->propagateIndex = 0; @@ -1390,7 +715,9 @@ class Solver $noneSatisfied = false; break; } - $decisionQueue[] = $literal; + if ($literal > 0 && $this->undecided($literal)) { + $decisionQueue[] = $literal; + } } if ($noneSatisfied && count($decisionQueue)) { @@ -1399,9 +726,9 @@ class Solver if (count($this->installed) != count($this->updateMap)) { $prunedQueue = array(); foreach ($decisionQueue as $literal) { - if (isset($this->installedMap[$literal->getPackageId()])) { + if (isset($this->installedMap[abs($literal)])) { $prunedQueue[] = $literal; - if (isset($this->updateMap[$literal->getPackageId()])) { + if (isset($this->updateMap[abs($literal)])) { $prunedQueue = $decisionQueue; break; } @@ -1460,15 +787,15 @@ class Solver // just need to decide on the positive literals // foreach ($literals as $literal) { - if (!$literal->isWanted()) { - if (!$this->decidedInstall($literal->getPackage())) { + if ($literal <= 0) { + if (!$this->decidedInstall(abs($literal))) { continue 2; // next rule } } else { - if ($this->decidedInstall($literal->getPackage())) { + if ($this->decidedInstall(abs($literal))) { continue 2; // next rule } - if ($this->undecided($literal->getPackage())) { + if ($this->undecided(abs($literal))) { $decisionQueue[] = $literal; } } @@ -1506,7 +833,7 @@ class Solver list($literals, $level) = $this->branches[$i]; foreach ($literals as $offset => $literal) { - if ($literal && $literal->isWanted() && $this->decisionMap[$literal->getPackageId()] > $level + 1) { + if ($literal && $literal > 0 && $this->decisionMap[abs($literal)] > $level + 1) { $lastLiteral = $literal; $lastBranchIndex = $i; $lastBranchOffset = $offset; @@ -1538,68 +865,4 @@ class Solver break; } } - - private function printDecisionMap() - { - echo "\nDecisionMap: \n"; - foreach ($this->decisionMap as $packageId => $level) { - if ($packageId === 0) { - continue; - } - if ($level > 0) { - echo ' +' . $this->pool->packageById($packageId)."\n"; - } elseif ($level < 0) { - echo ' -' . $this->pool->packageById($packageId)."\n"; - } else { - echo ' ?' . $this->pool->packageById($packageId)."\n"; - } - } - echo "\n"; - } - - private function printDecisionQueue() - { - echo "DecisionQueue: \n"; - foreach ($this->decisionQueue as $i => $literal) { - echo ' ' . $literal . ' ' . $this->decisionQueueWhy[$i]." level ".$this->decisionMap[$literal->getPackageId()]."\n"; - } - echo "\n"; - } - - private function printWatches() - { - echo "\nWatches:\n"; - foreach ($this->watches as $literalId => $watch) { - echo ' '.$this->literalFromId($literalId)."\n"; - $queue = array(array(' ', $watch)); - - while (!empty($queue)) { - list($indent, $watch) = array_pop($queue); - - echo $indent.$watch; - - if ($watch) { - echo ' [id='.$watch->getId().',watch1='.$this->literalFromId($watch->watch1).',watch2='.$this->literalFromId($watch->watch2)."]"; - } - - echo "\n"; - - if ($watch && ($watch->next1 == $watch || $watch->next2 == $watch)) { - if ($watch->next1 == $watch) { - echo $indent." 1 *RECURSION*"; - } - if ($watch->next2 == $watch) { - echo $indent." 2 *RECURSION*"; - } - } elseif ($watch && ($watch->next1 || $watch->next2)) { - $indent = str_replace(array('1', '2'), ' ', $indent); - - array_push($queue, array($indent.' 2 ', $watch->next2)); - array_push($queue, array($indent.' 1 ', $watch->next1)); - } - } - - echo "\n"; - } - } } diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php new file mode 100644 index 000000000..2cc6d9861 --- /dev/null +++ b/src/Composer/DependencyResolver/Transaction.php @@ -0,0 +1,167 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\Package\AliasPackage; +use Composer\DependencyResolver\Operation; + +/** + * @author Nils Adermann + */ +class Transaction +{ + protected $policy; + protected $pool; + protected $installedMap; + protected $decisionMap; + protected $decisionQueue; + protected $decisionQueueWhy; + + public function __construct($policy, $pool, $installedMap, $decisionMap, array $decisionQueue, $decisionQueueWhy) + { + $this->policy = $policy; + $this->pool = $pool; + $this->installedMap = $installedMap; + $this->decisionMap = $decisionMap; + $this->decisionQueue = $decisionQueue; + $this->decisionQueueWhy = $decisionQueueWhy; + } + + public function getOperations() + { + $transaction = array(); + $installMeansUpdateMap = array(); + + foreach ($this->decisionQueue as $i => $literal) { + $package = $this->pool->literalToPackage($literal); + + // !wanted & installed + if ($literal <= 0 && isset($this->installedMap[$package->getId()])) { + $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package); + + $literals = array($package->getId()); + + foreach ($updates as $update) { + $literals[] = $update->getId(); + } + + foreach ($literals as $updateLiteral) { + if ($updateLiteral !== $literal) { + $installMeansUpdateMap[abs($updateLiteral)] = $package; + } + } + } + } + + foreach ($this->decisionQueue as $i => $literal) { + $package = $this->pool->literalToPackage($literal); + + // 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] + ); + } + } else if (!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] + ); + } + } + } + + $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; + } + } + } + + 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; + } + } + + 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); + } +} diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 8a6c64922..81530fe06 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -16,7 +16,6 @@ use Composer\Repository\ArrayRepository; use Composer\Repository\RepositoryInterface; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\Pool; -use Composer\DependencyResolver\Literal; use Composer\Package\Link; use Composer\Package\AliasPackage; use Composer\Package\LinkConstraint\VersionConstraint; @@ -44,8 +43,8 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $this->pool->addRepository($this->repo); - $literals = array(new Literal($packageA, true)); - $expected = array(new Literal($packageA, true)); + $literals = array($packageA->getId()); + $expected = array($packageA->getId()); $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals); @@ -58,8 +57,8 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA2 = $this->getPackage('A', '2.0')); $this->pool->addRepository($this->repo); - $literals = array(new Literal($packageA1, true), new Literal($packageA2, true)); - $expected = array(new Literal($packageA2, true)); + $literals = array($packageA1->getId(), $packageA2->getId()); + $expected = array($packageA2->getId()); $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals); @@ -73,8 +72,8 @@ class DefaultPolicyTest extends TestCase $this->pool->addRepository($this->repoInstalled); $this->pool->addRepository($this->repo); - $literals = array(new Literal($packageA, true), new Literal($packageAInstalled, true)); - $expected = array(new Literal($packageA, true)); + $literals = array($packageA->getId(), $packageAInstalled->getId()); + $expected = array($packageA->getId()); $selected = $this->policy->selectPreferedPackages($this->pool, $this->mapFromRepo($this->repoInstalled), $literals); @@ -92,8 +91,8 @@ class DefaultPolicyTest extends TestCase $this->pool->addRepository($this->repoImportant); $this->pool->addRepository($this->repo); - $literals = array(new Literal($packageA, true), new Literal($packageAImportant, true)); - $expected = array(new Literal($packageAImportant, true)); + $literals = array($packageA->getId(), $packageAImportant->getId()); + $expected = array($packageAImportant->getId()); $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals); @@ -119,10 +118,10 @@ class DefaultPolicyTest extends TestCase $packages = $this->pool->whatProvides('a', new VersionConstraint('=', '2.1.9999999.9999999-dev')); $literals = array(); foreach ($packages as $package) { - $literals[] = new Literal($package, true); + $literals[] = $package->getId(); } - $expected = array(new Literal($packageAAliasImportant, true)); + $expected = array($packageAAliasImportant->getId()); $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals); @@ -139,7 +138,7 @@ class DefaultPolicyTest extends TestCase $this->pool->addRepository($this->repo); - $literals = array(new Literal($packageA, true), new Literal($packageB, true)); + $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals); @@ -157,8 +156,8 @@ class DefaultPolicyTest extends TestCase $this->pool->addRepository($this->repo); - $literals = array(new Literal($packageA, true), new Literal($packageB, true)); - $expected = array(new Literal($packageA, true), new Literal($packageB, true)); + $literals = array($packageA->getId(), $packageB->getId()); + $expected = $literals; $selected = $this->policy->selectPreferedPackages($this->pool, array(), $literals); diff --git a/tests/Composer/Test/DependencyResolver/LiteralTest.php b/tests/Composer/Test/DependencyResolver/LiteralTest.php deleted file mode 100644 index ef3638601..000000000 --- a/tests/Composer/Test/DependencyResolver/LiteralTest.php +++ /dev/null @@ -1,63 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\Test\DependencyResolver; - -use Composer\DependencyResolver\Literal; -use Composer\Test\TestCase; - -class LiteralTest extends TestCase -{ - protected $package; - - public function setUp() - { - $this->package = $this->getPackage('foo', '1'); - $this->package->setId(12); - } - - public function testLiteralWanted() - { - $literal = new Literal($this->package, true); - - $this->assertEquals(12, $literal->getId()); - $this->assertEquals('+'.(string) $this->package, (string) $literal); - } - - public function testLiteralUnwanted() - { - $literal = new Literal($this->package, false); - - $this->assertEquals(-12, $literal->getId()); - $this->assertEquals('-'.(string) $this->package, (string) $literal); - } - - public function testLiteralInverted() - { - $literal = new Literal($this->package, false); - - $inverted = $literal->inverted(); - - $this->assertInstanceOf('\Composer\DependencyResolver\Literal', $inverted); - $this->assertTrue($inverted->isWanted()); - $this->assertSame($this->package, $inverted->getPackage()); - $this->assertFalse($literal->equals($inverted)); - - $doubleInverted = $inverted->inverted(); - - $this->assertInstanceOf('\Composer\DependencyResolver\Literal', $doubleInverted); - $this->assertFalse($doubleInverted->isWanted()); - $this->assertSame($this->package, $doubleInverted->getPackage()); - - $this->assertTrue($literal->equals($doubleInverted)); - } -} diff --git a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php index 56084f32a..10ec17501 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetIteratorTest.php @@ -15,6 +15,7 @@ namespace Composer\Test\DependencyResolver; use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; use Composer\DependencyResolver\RuleSetIterator; +use Composer\DependencyResolver\Pool; class ResultSetIteratorTest extends \PHPUnit_Framework_TestCase { @@ -22,13 +23,15 @@ class ResultSetIteratorTest extends \PHPUnit_Framework_TestCase protected function setUp() { + $this->pool = new Pool; + $this->rules = array( RuleSet::TYPE_JOB => array( - new Rule(array(), 'job1', null), - new Rule(array(), 'job2', null), + new Rule($this->pool, array(), 'job1', null), + new Rule($this->pool, array(), 'job2', null), ), RuleSet::TYPE_LEARNED => array( - new Rule(array(), 'update1', null), + new Rule($this->pool, array(), 'update1', null), ), RuleSet::TYPE_PACKAGE => array(), ); diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index f319651ae..a6b108500 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -14,21 +14,29 @@ namespace Composer\Test\DependencyResolver; use Composer\DependencyResolver\Rule; use Composer\DependencyResolver\RuleSet; -use Composer\DependencyResolver\Literal; +use Composer\DependencyResolver\Pool; +use Composer\Repository\ArrayRepository; use Composer\Test\TestCase; class RuleSetTest extends TestCase { + protected $pool; + + public function setUp() + { + $this->pool = new Pool; + } + public function testAdd() { $rules = array( RuleSet::TYPE_PACKAGE => array(), RuleSet::TYPE_JOB => array( - new Rule(array(), 'job1', null), - new Rule(array(), 'job2', null), + new Rule($this->pool, array(), 'job1', null), + new Rule($this->pool, array(), 'job2', null), ), RuleSet::TYPE_LEARNED => array( - new Rule(array(), 'update1', null), + new Rule($this->pool, array(), 'update1', null), ), ); @@ -48,15 +56,15 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $ruleSet->add(new Rule(array(), 'job1', null), 7); + $ruleSet->add(new Rule($this->pool, array(), 'job1', null), 7); } public function testCount() { $ruleSet = new RuleSet; - $ruleSet->add(new Rule(array(), 'job1', null), RuleSet::TYPE_JOB); - $ruleSet->add(new Rule(array(), 'job2', null), RuleSet::TYPE_JOB); + $ruleSet->add(new Rule($this->pool, array(), 'job1', null), RuleSet::TYPE_JOB); + $ruleSet->add(new Rule($this->pool, array(), 'job2', null), RuleSet::TYPE_JOB); $this->assertEquals(2, $ruleSet->count()); } @@ -65,7 +73,7 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $rule = new Rule(array(), 'job1', null); + $rule = new Rule($this->pool, array(), 'job1', null); $ruleSet->add($rule, RuleSet::TYPE_JOB); $this->assertSame($rule, $ruleSet->ruleById(0)); @@ -75,8 +83,8 @@ class RuleSetTest extends TestCase { $ruleSet = new RuleSet; - $rule1 = new Rule(array(), 'job1', null); - $rule2 = new Rule(array(), 'job1', null); + $rule1 = new Rule($this->pool, array(), 'job1', null); + $rule2 = new Rule($this->pool, array(), 'job1', null); $ruleSet->add($rule1, RuleSet::TYPE_JOB); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); @@ -90,8 +98,8 @@ class RuleSetTest extends TestCase public function testGetIteratorFor() { $ruleSet = new RuleSet; - $rule1 = new Rule(array(), 'job1', null); - $rule2 = new Rule(array(), 'job1', null); + $rule1 = new Rule($this->pool, array(), 'job1', null); + $rule2 = new Rule($this->pool, array(), 'job1', null); $ruleSet->add($rule1, RuleSet::TYPE_JOB); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); @@ -104,8 +112,8 @@ class RuleSetTest extends TestCase public function testGetIteratorWithout() { $ruleSet = new RuleSet; - $rule1 = new Rule(array(), 'job1', null); - $rule2 = new Rule(array(), 'job1', null); + $rule1 = new Rule($this->pool, array(), 'job1', null); + $rule2 = new Rule($this->pool, array(), 'job1', null); $ruleSet->add($rule1, RuleSet::TYPE_JOB); $ruleSet->add($rule2, RuleSet::TYPE_LEARNED); @@ -149,9 +157,13 @@ class RuleSetTest extends TestCase public function testToString() { + $repo = new ArrayRepository; + $repo->addPackage($p = $this->getPackage('foo', '2.1')); + $this->pool->addRepository($repo); + $ruleSet = new RuleSet; - $literal = new Literal($this->getPackage('foo', '2.1'), true); - $rule = new Rule(array($literal), 'job1', null); + $literal = $p->getId(); + $rule = new Rule($this->pool, array($literal), 'job1', null); $ruleSet->add($rule, RuleSet::TYPE_JOB); diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index f76f46eb1..8d4c732a2 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -13,22 +13,29 @@ namespace Composer\Test\DependencyResolver; use Composer\DependencyResolver\Rule; -use Composer\DependencyResolver\Literal; +use Composer\DependencyResolver\Pool; +use Composer\Repository\ArrayRepository; use Composer\Test\TestCase; class RuleTest extends TestCase { + protected $pool; + + public function setUp() + { + $this->pool = new Pool; + } + public function testGetHash() { - $rule = new Rule(array(), 'job1', null); - $rule->ruleHash = '123'; + $rule = new Rule($this->pool, array(123), 'job1', null); - $this->assertEquals('123', $rule->getHash()); + $this->assertEquals(substr(md5('123'), 0, 5), $rule->getHash()); } public function testSetAndGetId() { - $rule = new Rule(array(), 'job1', null); + $rule = new Rule($this->pool, array(), 'job1', null); $rule->setId(666); $this->assertEquals(666, $rule->getId()); @@ -36,73 +43,31 @@ class RuleTest extends TestCase public function testEqualsForRulesWithDifferentHashes() { - $rule = new Rule(array(), 'job1', null); - $rule->ruleHash = '123'; - - $rule2 = new Rule(array(), 'job1', null); - $rule2->ruleHash = '321'; - - $this->assertFalse($rule->equals($rule2)); - } - - public function testEqualsForRulesWithDifferentLiterals() - { - $literal = $this->getLiteralMock(); - $literal->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $rule = new Rule(array($literal), 'job1', null); - $rule->ruleHash = '123'; - - $literal = $this->getLiteralMock(); - $literal->expects($this->any()) - ->method('getId') - ->will($this->returnValue(12)); - $rule2 = new Rule(array($literal), 'job1', null); - $rule2->ruleHash = '123'; + $rule = new Rule($this->pool, array(1, 2), 'job1', null); + $rule2 = new Rule($this->pool, array(1, 3), 'job1', null); $this->assertFalse($rule->equals($rule2)); } public function testEqualsForRulesWithDifferLiteralsQuantity() { - $literal = $this->getLiteralMock(); - $literal->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $literal2 = $this->getLiteralMock(); - $literal2->expects($this->any()) - ->method('getId') - ->will($this->returnValue(12)); - - $rule = new Rule(array($literal, $literal2), 'job1', null); - $rule->ruleHash = '123'; - $rule2 = new Rule(array($literal), 'job1', null); - $rule2->ruleHash = '123'; + $rule = new Rule($this->pool, array(1, 12), 'job1', null); + $rule2 = new Rule($this->pool, array(1), 'job1', null); $this->assertFalse($rule->equals($rule2)); } - public function testEqualsForRulesWithThisSameLiterals() + public function testEqualsForRulesWithSameLiterals() { - $literal = $this->getLiteralMock(); - $literal->expects($this->any()) - ->method('getId') - ->will($this->returnValue(1)); - $literal2 = $this->getLiteralMock(); - $literal2->expects($this->any()) - ->method('getId') - ->will($this->returnValue(12)); - - $rule = new Rule(array($literal, $literal2), 'job1', null); - $rule2 = new Rule(array($literal, $literal2), 'job1', null); + $rule = new Rule($this->pool, array(1, 12), 'job1', null); + $rule2 = new Rule($this->pool, array(1, 12), 'job1', null); $this->assertTrue($rule->equals($rule2)); } public function testSetAndGetType() { - $rule = new Rule(array(), 'job1', null); + $rule = new Rule($this->pool, array(), 'job1', null); $rule->setType('someType'); $this->assertEquals('someType', $rule->getType()); @@ -110,7 +75,7 @@ class RuleTest extends TestCase public function testEnable() { - $rule = new Rule(array(), 'job1', null); + $rule = new Rule($this->pool, array(), 'job1', null); $rule->disable(); $rule->enable(); @@ -120,7 +85,7 @@ class RuleTest extends TestCase public function testDisable() { - $rule = new Rule(array(), 'job1', null); + $rule = new Rule($this->pool, array(), 'job1', null); $rule->enable(); $rule->disable(); @@ -128,24 +93,10 @@ class RuleTest extends TestCase $this->assertFalse($rule->isEnabled()); } - public function testSetWeak() - { - $rule = new Rule(array(), 'job1', null); - $rule->setWeak(true); - - $rule2 = new Rule(array(), 'job1', null); - $rule2->setWeak(false); - - $this->assertTrue($rule->isWeak()); - $this->assertFalse($rule2->isWeak()); - } - public function testIsAssertions() { - $literal = $this->getLiteralMock(); - $literal2 = $this->getLiteralMock(); - $rule = new Rule(array($literal, $literal2), 'job1', null); - $rule2 = new Rule(array($literal), 'job1', null); + $rule = new Rule($this->pool, array(1, 12), 'job1', null); + $rule2 = new Rule($this->pool, array(1), 'job1', null); $this->assertFalse($rule->isAssertion()); $this->assertTrue($rule2->isAssertion()); @@ -153,18 +104,13 @@ class RuleTest extends TestCase public function testToString() { - $literal = new Literal($this->getPackage('foo', '2.1'), true); - $literal2 = new Literal($this->getPackage('baz', '1.1'), false); + $repo = new ArrayRepository; + $repo->addPackage($p1 = $this->getPackage('foo', '2.1')); + $repo->addPackage($p2 = $this->getPackage('baz', '1.1')); + $this->pool->addRepository($repo); - $rule = new Rule(array($literal, $literal2), 'job1', null); + $rule = new Rule($this->pool, array($p1->getId(), -$p2->getId()), 'job1', null); $this->assertEquals('(-baz-1.1.0.0|+foo-2.1.0.0)', $rule->__toString()); } - - private function getLiteralMock() - { - return $this->getMockBuilder('Composer\DependencyResolver\Literal') - ->disableOriginalConstructor() - ->getMock(); - } }