diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 190829213..a58cf6184 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -42,11 +42,11 @@ class DefaultPolicy implements PolicyInterface return $constraint->matchSpecific($version, true); } - public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package) + public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package, $mustMatchName = false) { $packages = array(); - foreach ($pool->whatProvides($package->getName()) as $candidate) { + foreach ($pool->whatProvides($package->getName(), null, $mustMatchName) as $candidate) { if ($candidate !== $package) { $packages[] = $candidate; } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index a1bba4f3a..8db4e90a0 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -50,6 +50,7 @@ class Pool protected $versionParser; protected $providerCache = array(); protected $filterRequires; + protected $whitelist = null; protected $id = 1; public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) @@ -66,6 +67,11 @@ class Pool $this->filterRequires = $filterRequires; } + public function setWhitelist($whitelist) + { + $this->whitelist = $whitelist; + } + /** * Adds a repository and its packages to this package pool * @@ -223,21 +229,24 @@ class Pool * @param string $name The package name to be searched for * @param LinkConstraintInterface $constraint A constraint that all returned * packages must match or null to return all + * @param bool $mustMatchName Whether the name of returned packages + * must match the given name * @return array A set of packages */ - public function whatProvides($name, LinkConstraintInterface $constraint = null) + public function whatProvides($name, LinkConstraintInterface $constraint = null, $mustMatchName = false) { - if (isset($this->providerCache[$name][(string) $constraint])) { - return $this->providerCache[$name][(string) $constraint]; + $key = ((string) (int) $mustMatchName).((string) $constraint); + if (isset($this->providerCache[$name][$key])) { + return $this->providerCache[$name][$key]; } - return $this->providerCache[$name][(string) $constraint] = $this->computeWhatProvides($name, $constraint); + return $this->providerCache[$name][$key] = $this->computeWhatProvides($name, $constraint, $mustMatchName); } /** * @see whatProvides */ - private function computeWhatProvides($name, $constraint) + private function computeWhatProvides($name, $constraint, $mustMatchName = false) { $candidates = array(); @@ -259,6 +268,9 @@ class Pool $nameMatch = false; foreach ($candidates as $candidate) { + if ($this->whitelist !== null && !isset($this->whitelist[$candidate->getId()])) { + continue; + } switch ($this->match($candidate, $name, $constraint)) { case self::MATCH_NONE: break; @@ -289,7 +301,7 @@ class Pool } // if a package with the required name exists, we ignore providers - if ($nameMatch) { + if ($nameMatch || $mustMatchName) { return $matches; } diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 92c8aa175..85f83b4f5 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -46,7 +46,7 @@ class Request protected function addJob($packageName, $cmd, LinkConstraintInterface $constraint = null) { $packageName = strtolower($packageName); - $packages = $this->pool->whatProvides($packageName, $constraint); + $packages = $this->pool->whatProvides($packageName, $constraint, true); $this->jobs[] = array( 'packages' => $packages, diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index b40ce1a60..d203832db 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -25,6 +25,8 @@ class RuleSetGenerator protected $rules; protected $jobs; protected $installedMap; + protected $whitelistedMap; + protected $addedMap; public function __construct(PolicyInterface $policy, Pool $pool) { @@ -141,6 +143,41 @@ class RuleSetGenerator $this->rules->add($newRule, $type); } + protected function whitelistFromPackage(PackageInterface $package) + { + $workQueue = new \SplQueue; + $workQueue->enqueue($package); + + while (!$workQueue->isEmpty()) { + $package = $workQueue->dequeue(); + if (isset($this->whitelistedMap[$package->getId()])) { + continue; + } + + $this->whitelistedMap[$package->getId()] = true; + + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint(), true); + + foreach ($possibleRequires as $require) { + $workQueue->enqueue($require); + } + } + + $obsoleteProviders = $this->pool->whatProvides($package->getName(), null, true); + + foreach ($obsoleteProviders as $provider) { + if ($provider === $package) { + continue; + } + + if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) { + $workQueue->enqueue($provider); + } + } + } + } + protected function addRulesForPackage(PackageInterface $package) { $workQueue = new \SplQueue; @@ -236,6 +273,30 @@ class RuleSetGenerator } } + private function whitelistFromUpdatePackages(PackageInterface $package) + { + $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package, true); + + foreach ($updates as $update) { + $this->whitelistFromPackage($update); + } + } + + protected function whitelistFromJobs() + { + foreach ($this->jobs as $job) { + switch ($job['cmd']) { + case 'install': + if ($job['packages']) { + foreach ($job['packages'] as $package) { + $this->whitelistFromPackage($package); + } + } + break; + } + } + } + protected function addRulesForJobs() { foreach ($this->jobs as $job) { @@ -270,6 +331,16 @@ class RuleSetGenerator $this->rules = new RuleSet; $this->installedMap = $installedMap; + $this->whitelistedNames = array(); + foreach ($this->installedMap as $package) { + $this->whitelistFromPackage($package); + $this->whitelistFromUpdatePackages($package); + } + $this->whitelistFromJobs(); + + $this->pool->setWhitelist($this->whitelistedMap); + + $this->addedMap = array(); foreach ($this->installedMap as $package) { $this->addRulesForPackage($package); $this->addRulesForUpdatePackages($package); diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 349f6e3b4..14f9c0943 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -441,10 +441,9 @@ class SolverTest extends TestCase $this->request->install('A'); - $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageQ), - array('job' => 'install', 'package' => $packageA), - )); + // must explicitly pick the provider, so error in this case + $this->setExpectedException('Composer\DependencyResolver\SolverProblemsException'); + $this->solver->solve($this->request); } public function testSkipReplacerOfExistingPackage() @@ -574,11 +573,12 @@ class SolverTest extends TestCase $this->reposComplete(); $this->request->install('A'); + $this->request->install('C'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageB), array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageC), + array('job' => 'install', 'package' => $packageB), )); } diff --git a/tests/Composer/Test/Fixtures/installer/provide-priorities.test b/tests/Composer/Test/Fixtures/installer/provide-priorities.test deleted file mode 100644 index f97e16e6c..000000000 --- a/tests/Composer/Test/Fixtures/installer/provide-priorities.test +++ /dev/null @@ -1,34 +0,0 @@ ---TEST-- -Provide only applies when no existing package has the given name ---COMPOSER-- -{ - "repositories": [ - { - "type": "package", - "package": [ - { "name": "higher-prio-hijacker", "version": "1.1.0", "provide": { "package": "1.0.0" } }, - { "name": "provider2", "version": "1.1.0", "provide": { "package2": "1.0.0" } } - ] - }, - { - "type": "package", - "package": [ - { "name": "package", "version": "0.9.0" }, - { "name": "package", "version": "1.0.0" }, - { "name": "hijacker", "version": "1.1.0", "provide": { "package": "1.0.0" } }, - { "name": "provider3", "version": "1.1.0", "provide": { "package3": "1.0.0" } } - ] - } - ], - "require": { - "package": "1.*", - "package2": "1.*", - "provider3": "1.1.0" - } -} ---RUN-- -install ---EXPECT-- -Installing package (1.0.0) -Installing provider2 (1.1.0) -Installing provider3 (1.1.0)