From 419567ba6db91e8cf15f78ba0690d5e4b6668c0e Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 4 May 2020 21:12:21 +0200 Subject: [PATCH] Update VersionSelector to take all platform requirements into account when selecting packages --- UPGRADE-2.0.md | 1 + .../Package/Version/VersionSelector.php | 47 ++++++++--- .../Package/Version/VersionSelectorTest.php | 80 ++++++++++++++++--- 3 files changed, 105 insertions(+), 23 deletions(-) diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index c8bcedf70..22486cae0 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -27,6 +27,7 @@ - packages now contain an `"installed-path"` key which lists where they were installed - there is a top level `"dev"` key which stores whether dev requirements were installed or not - `PreFileDownloadEvent` now receives an `HttpDownloader` instance instead of `RemoteFilesystem`, and that instance can not be overridden by listeners anymore +- `VersionSelector::findBestCandidate`'s third argument (phpVersion) was removed in favor of passing in a complete PlatformRepository instance into the constructor - `IOInterface` now extends PSR-3's `LoggerInterface`, and has new `writeRaw` + `writeErrorRaw` methods - `RepositoryInterface` changes: - A new `loadPackages(array $packageNameMap, array $acceptableStabilities, array $stabilityFlags)` function was added for use during pool building diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php index 9f331dffe..82d60dce7 100644 --- a/src/Composer/Package/Version/VersionSelector.php +++ b/src/Composer/Package/Version/VersionSelector.php @@ -20,6 +20,7 @@ use Composer\Composer; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; use Composer\Repository\RepositorySet; +use Composer\Repository\PlatformRepository; use Composer\Semver\Constraint\Constraint; /** @@ -32,11 +33,22 @@ class VersionSelector { private $repositorySet; + private $platformConstraints; + private $parser; - public function __construct(RepositorySet $repositorySet) + /** + * @param PlatformRepository $platformRepo If passed in, the versions found will be filtered against their requirements to eliminate any not matching the current platform packages + */ + public function __construct(RepositorySet $repositorySet, PlatformRepository $platformRepo = null) { $this->repositorySet = $repositorySet; + if ($platformRepo) { + $this->platformConstraints = array(); + foreach ($platformRepo->getPackages() as $package) { + $this->platformConstraints[$package->getName()][] = new Constraint('==', $package->getVersion()); + } + } } /** @@ -45,25 +57,38 @@ class VersionSelector * * @param string $packageName * @param string $targetPackageVersion - * @param string $targetPhpVersion * @param string $preferredStability + * @param bool $ignorePlatformReqs * @return PackageInterface|false */ - public function findBestCandidate($packageName, $targetPackageVersion = null, $targetPhpVersion = null, $preferredStability = 'stable') + public function findBestCandidate($packageName, $targetPackageVersion = null, $preferredStability = 'stable', $ignorePlatformReqs = false) { + if (!isset(BasePackage::$stabilities[$preferredStability])) { + // If you get this, maybe you are still relying on the Composer 1.x signature where the 3rd arg was the php version + throw new \UnexpectedValueException('Expected a valid stability name as 3rd argument, got '.$preferredStability); + } + $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; $candidates = $this->repositorySet->findPackages(strtolower($packageName), $constraint); - if ($targetPhpVersion) { - $phpConstraint = new Constraint('==', $this->getParser()->normalize($targetPhpVersion)); - $composerRuntimeConstraint = new Constraint('==', $this->getParser()->normalize(Composer::RUNTIME_API_VERSION)); - $composerPluginConstraint = new Constraint('==', $this->getParser()->normalize(PluginInterface::PLUGIN_API_VERSION)); - $candidates = array_filter($candidates, function ($pkg) use ($phpConstraint, $composerPluginConstraint, $composerRuntimeConstraint) { + if ($this->platformConstraints && !$ignorePlatformReqs) { + $platformConstraints = $this->platformConstraints; + $candidates = array_filter($candidates, function ($pkg) use ($platformConstraints) { $reqs = $pkg->getRequires(); - return (!isset($reqs['php']) || $reqs['php']->getConstraint()->matches($phpConstraint)) - && (!isset($reqs['composer-plugin-api']) || $reqs['composer-plugin-api']->getConstraint()->matches($composerPluginConstraint)) - && (!isset($reqs['composer-runtime-api']) || $reqs['composer-runtime-api']->getConstraint()->matches($composerRuntimeConstraint)); + foreach ($reqs as $name => $link) { + if (isset($platformConstraints[$name])) { + foreach ($platformConstraints[$name] as $constraint) { + if ($link->getConstraint()->matches($constraint)) { + continue 2; + } + } + + return false; + } + } + + return true; }); } diff --git a/tests/Composer/Test/Package/Version/VersionSelectorTest.php b/tests/Composer/Test/Package/Version/VersionSelectorTest.php index 66d07d267..14d00b438 100644 --- a/tests/Composer/Test/Package/Version/VersionSelectorTest.php +++ b/tests/Composer/Test/Package/Version/VersionSelectorTest.php @@ -15,6 +15,7 @@ namespace Composer\Test\Package\Version; use Composer\Package\Version\VersionSelector; use Composer\Package\Package; use Composer\Package\Link; +use Composer\Repository\PlatformRepository; use Composer\Semver\VersionParser; use Composer\Test\TestCase; @@ -46,27 +47,82 @@ class VersionSelectorTest extends TestCase $this->assertSame($package2, $best, 'Latest version should be 1.2.2'); } - public function testLatestVersionIsReturnedThatMatchesPhpRequirement() + public function testLatestVersionIsReturnedThatMatchesPhpRequirements() { $packageName = 'foobar'; + $platform = new PlatformRepository(array(), array('php' => '5.5.0')); + $repositorySet = $this->createMockRepositorySet(); + $versionSelector = new VersionSelector($repositorySet, $platform); + $parser = new VersionParser; $package1 = $this->createPackage('1.0.0'); - $package2 = $this->createPackage('2.0.0'); $package1->setRequires(array('php' => new Link($packageName, 'php', $parser->parseConstraints('>=5.4'), 'requires', '>=5.4'))); + $package2 = $this->createPackage('2.0.0'); $package2->setRequires(array('php' => new Link($packageName, 'php', $parser->parseConstraints('>=5.6'), 'requires', '>=5.6'))); $packages = array($package1, $package2); - $repositorySet = $this->createMockRepositorySet(); - $repositorySet->expects($this->once()) + $repositorySet->expects($this->any()) ->method('findPackages') ->with($packageName, null) ->will($this->returnValue($packages)); - $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, '5.5.0'); - + $best = $versionSelector->findBestCandidate($packageName); $this->assertSame($package1, $best, 'Latest version supporting php 5.5 should be returned (1.0.0)'); + $best = $versionSelector->findBestCandidate($packageName, null, 'stable', true); + $this->assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (2.0.0)'); + } + + public function testLatestVersionIsReturnedThatMatchesExtRequirements() + { + $packageName = 'foobar'; + + $platform = new PlatformRepository(array(), array('ext-zip' => '5.3.0')); + $repositorySet = $this->createMockRepositorySet(); + $versionSelector = new VersionSelector($repositorySet, $platform); + + $parser = new VersionParser; + $package1 = $this->createPackage('1.0.0'); + $package1->setRequires(array('ext-zip' => new Link($packageName, 'ext-zip', $parser->parseConstraints('^5.2'), 'requires', '^5.2'))); + $package2 = $this->createPackage('2.0.0'); + $package2->setRequires(array('ext-zip' => new Link($packageName, 'ext-zip', $parser->parseConstraints('^5.4'), 'requires', '^5.4'))); + $packages = array($package1, $package2); + + $repositorySet->expects($this->any()) + ->method('findPackages') + ->with($packageName, null) + ->will($this->returnValue($packages)); + + $best = $versionSelector->findBestCandidate($packageName); + $this->assertSame($package1, $best, 'Latest version supporting ext-zip 5.3.0 should be returned (1.0.0)'); + $best = $versionSelector->findBestCandidate($packageName, null, 'stable', true); + $this->assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (2.0.0)'); + } + + public function testLatestVersionIsReturnedThatMatchesComposerRequirements() + { + $packageName = 'foobar'; + + $platform = new PlatformRepository(array(), array('composer-runtime-api' => '1.0.0')); + $repositorySet = $this->createMockRepositorySet(); + $versionSelector = new VersionSelector($repositorySet, $platform); + + $parser = new VersionParser; + $package1 = $this->createPackage('1.0.0'); + $package1->setRequires(array('composer-runtime-api' => new Link($packageName, 'composer-runtime-api', $parser->parseConstraints('^1.0'), 'requires', '^1.0'))); + $package2 = $this->createPackage('1.1.0'); + $package2->setRequires(array('composer-runtime-api' => new Link($packageName, 'composer-runtime-api', $parser->parseConstraints('^2.0'), 'requires', '^2.0'))); + $packages = array($package1, $package2); + + $repositorySet->expects($this->any()) + ->method('findPackages') + ->with($packageName, null) + ->will($this->returnValue($packages)); + + $best = $versionSelector->findBestCandidate($packageName); + $this->assertSame($package1, $best, 'Latest version supporting composer 1 should be returned (1.0.0)'); + $best = $versionSelector->findBestCandidate($packageName, null, 'stable', true); + $this->assertSame($package2, $best, 'Latest version should be returned when ignoring platform reqs (1.1.0)'); } public function testMostStableVersionIsReturned() @@ -109,10 +165,10 @@ class VersionSelectorTest extends TestCase ->will($this->returnValue(array_reverse($packages))); $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, null); + $best = $versionSelector->findBestCandidate($packageName); $this->assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); - $best = $versionSelector->findBestCandidate($packageName, null, null); + $best = $versionSelector->findBestCandidate($packageName); $this->assertSame($package2, $best, 'Expecting 2.0.0-beta3, cause beta is more stable than dev'); } @@ -131,7 +187,7 @@ class VersionSelectorTest extends TestCase ->will($this->returnValue($packages)); $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, null, 'dev'); + $best = $versionSelector->findBestCandidate($packageName, null, 'dev'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); } @@ -152,7 +208,7 @@ class VersionSelectorTest extends TestCase ->will($this->returnValue($packages)); $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, null, 'beta'); + $best = $versionSelector->findBestCandidate($packageName, null, 'beta'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); } @@ -172,7 +228,7 @@ class VersionSelectorTest extends TestCase ->will($this->returnValue($packages)); $versionSelector = new VersionSelector($repositorySet); - $best = $versionSelector->findBestCandidate($packageName, null, null, 'stable'); + $best = $versionSelector->findBestCandidate($packageName, null, 'stable'); $this->assertSame($package2, $best, 'Latest version should be returned (1.1.0-beta)'); }