From eb2aa1483035a52cd518f67197fc5a92684c1ba7 Mon Sep 17 00:00:00 2001 From: nevvermind Date: Tue, 2 Jun 2015 02:21:06 +0100 Subject: [PATCH] Make plugins have actual constraints instead of fixed versions Instead of developing plugins against a single, fixed Plugin API version - `"composer-plugin-api": "1.0.0"`, this change will allow plugin developers to use versions like `"composer-plugin-api": "~1.1"` or `"composer-plugin-api": ">=2.1 <3.0"`, aka actual Composer-compatible constraints. Only the "1.0", "1.0.0" and "1.0.0" Plugin API versions will be regarded as BC versions, and internally converted to "^1.0"; every other declared version string will be kept as it is. Because of this new constraint flexibility, plugin version mismatches will be skipped, which means those plugin will NOT be registered to the system. Previously, a mismatch triggered a warning, but plugins were still registered. --- src/Composer/Config.php | 11 ++ .../Package/Version/VersionParser.php | 16 ++ src/Composer/Plugin/PluginInterface.php | 1 + src/Composer/Plugin/PluginManager.php | 12 +- .../Repository/PlatformRepository.php | 27 +++- .../Package/Version/VersionParserTest.php | 67 ++++++++ .../Fixtures/plugin-v5/Installer/Plugin.php | 14 ++ .../Plugin/Fixtures/plugin-v5/composer.json | 12 ++ .../Fixtures/plugin-v6/Installer/Plugin.php | 14 ++ .../Plugin/Fixtures/plugin-v6/composer.json | 12 ++ .../Fixtures/plugin-v7/Installer/Plugin.php | 14 ++ .../Plugin/Fixtures/plugin-v7/composer.json | 12 ++ .../Test/Plugin/PluginInstallerTest.php | 147 +++++++++++++++++- 13 files changed, 349 insertions(+), 10 deletions(-) create mode 100644 tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin.php create mode 100644 tests/Composer/Test/Plugin/Fixtures/plugin-v5/composer.json create mode 100644 tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin.php create mode 100644 tests/Composer/Test/Plugin/Fixtures/plugin-v6/composer.json create mode 100644 tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin.php create mode 100644 tests/Composer/Test/Plugin/Fixtures/plugin-v7/composer.json diff --git a/src/Composer/Config.php b/src/Composer/Config.php index 828b98e75..4d0ac5858 100644 --- a/src/Composer/Config.php +++ b/src/Composer/Config.php @@ -13,6 +13,7 @@ namespace Composer; use Composer\Config\ConfigSourceInterface; +use Composer\Plugin\PluginInterface; /** * @author Jordi Boggiano @@ -340,4 +341,14 @@ class Config return false; } + + /** + * Returns the version of the internal composer-plugin-api package. + * + * @return string + */ + public function getPluginApiVersion() + { + return PluginInterface::PLUGIN_API_VERSION; + } } diff --git a/src/Composer/Package/Version/VersionParser.php b/src/Composer/Package/Version/VersionParser.php index 3a214f150..6a3ec65bb 100644 --- a/src/Composer/Package/Version/VersionParser.php +++ b/src/Composer/Package/Version/VersionParser.php @@ -227,12 +227,28 @@ class VersionParser } else { $parsedConstraint = $this->parseConstraints($constraint); } + + // if the required Plugin API version is exactly "1.0.0", convert it to "^1.0", to keep BC + if ('composer-plugin-api' === $target && $this->isOldStylePluginApiVersion($constraint)) { + $parsedConstraint = $this->parseConstraints('^1.0'); + } + $res[strtolower($target)] = new Link($source, $target, $parsedConstraint, $description, $constraint); } return $res; } + /** + * @param string $requiredPluginApiVersion + * @return bool + */ + private function isOldStylePluginApiVersion($requiredPluginApiVersion) + { + // catch "1.0", "1.0.0", "1.0.0.0" etc. + return (bool) preg_match('#^1(\.0)+$#', trim($requiredPluginApiVersion)); + } + /** * Parses as constraint string into LinkConstraint objects * diff --git a/src/Composer/Plugin/PluginInterface.php b/src/Composer/Plugin/PluginInterface.php index dea5828c1..34fc0957a 100644 --- a/src/Composer/Plugin/PluginInterface.php +++ b/src/Composer/Plugin/PluginInterface.php @@ -26,6 +26,7 @@ interface PluginInterface * Version number of the fake composer-plugin-api package * * @var string + * @deprecated Use \Composer\Config::getPluginApiVersion() instead. */ const PLUGIN_API_VERSION = '1.0.0'; diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 14021dc45..29bdd2e52 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -122,6 +122,7 @@ class PluginManager foreach ($package->getRequires() as $link) { /** @var Link $link */ if ('composer-plugin-api' === $link->getTarget()) { $requiresComposer = $link->getConstraint(); + break; } } @@ -129,14 +130,17 @@ class PluginManager throw new \RuntimeException("Plugin ".$package->getName()." is missing a require statement for a version of the composer-plugin-api package."); } - if (!$requiresComposer->matches(new VersionConstraint('==', $this->versionParser->normalize(PluginInterface::PLUGIN_API_VERSION)))) { - $this->io->writeError("The plugin ".$package->getName()." requires a version of composer-plugin-api that does not match your composer installation. You may need to run composer update with the '--no-plugins' option."); + $currPluginApiVersion = $this->composer->getConfig()->getPluginApiVersion(); + $currPluginApiConstraint = new VersionConstraint('==', $this->versionParser->normalize($currPluginApiVersion)); + if (!$requiresComposer->matches($currPluginApiConstraint)) { + $this->io->writeError('The "' . $package->getName() . '" plugin was skipped because it requires a Plugin API version ("' . $requiresComposer->getPrettyString() . '") that does not match your Composer installation ("' . $currPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.'); + continue; } $this->registerPackage($package); - } + // Backward compatibility - if ('composer-installer' === $package->getType()) { + } elseif ('composer-installer' === $package->getType()) { $this->registerPackage($package); } } diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 4afd3d4e9..976cd5d04 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -12,6 +12,7 @@ namespace Composer\Repository; +use Composer\Config; use Composer\Package\PackageInterface; use Composer\Package\CompletePackage; use Composer\Package\Version\VersionParser; @@ -33,6 +34,11 @@ class PlatformRepository extends ArrayRepository */ private $overrides; + /** + * @var Config + */ + private $config; + public function __construct(array $packages = array(), array $overrides = array()) { parent::__construct($packages); @@ -62,7 +68,7 @@ class PlatformRepository extends ArrayRepository parent::addPackage($package); } - $prettyVersion = PluginInterface::PLUGIN_API_VERSION; + $prettyVersion = $this->getConfig()->getPluginApiVersion(); $version = $versionParser->normalize($prettyVersion); $composerPluginApi = new CompletePackage('composer-plugin-api', $version, $prettyVersion); $composerPluginApi->setDescription('The Composer Plugin API'); @@ -210,4 +216,23 @@ class PlatformRepository extends ArrayRepository { return 'ext-' . str_replace(' ', '-', $name); } + + /** + * @param Config $config + */ + public function setConfig(Config $config) + { + $this->config = $config; + } + + /** + * @return Config + */ + public function getConfig() + { + if (!$this->config) { + $this->config = new Config; + } + return $this->config; + } } diff --git a/tests/Composer/Test/Package/Version/VersionParserTest.php b/tests/Composer/Test/Package/Version/VersionParserTest.php index ceb959051..87416e7eb 100644 --- a/tests/Composer/Test/Package/Version/VersionParserTest.php +++ b/tests/Composer/Test/Package/Version/VersionParserTest.php @@ -12,6 +12,7 @@ namespace Composer\Test\Package\Version; +use Composer\Package\Link; use Composer\Package\Version\VersionParser; use Composer\Package\LinkConstraint\MultiConstraint; use Composer\Package\LinkConstraint\VersionConstraint; @@ -513,4 +514,70 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase array('RC', '2.0.0rc1') ); } + + public function oldStylePluginApiVersions() + { + return array( + array('1.0'), + array('1.0.0'), + array('1.0.0.0'), + ); + } + + public function newStylePluginApiVersions() + { + return array( + array('1'), + array('=1.0.0'), + array('==1.0'), + array('~1.0.0'), + array('*'), + array('3.0.*'), + array('@stable'), + array('1.0.0@stable'), + array('^5.1'), + array('>=1.0.0 <2.5'), + array('x'), + array('1.0.0-dev'), + ); + } + + /** + * @dataProvider oldStylePluginApiVersions + */ + public function testOldStylePluginApiVersionGetsConvertedIntoAnotherConstraintToKeepBc($apiVersion) + { + $parser = new VersionParser; + + /** @var Link[] $links */ + $links = $parser->parseLinks('Plugin', '9.9.9', '', array('composer-plugin-api' => $apiVersion)); + + $this->assertArrayHasKey('composer-plugin-api', $links); + $this->assertSame('^1.0', $links['composer-plugin-api']->getConstraint()->getPrettyString()); + } + + /** + * @dataProvider newStylePluginApiVersions + */ + public function testNewStylePluginApiVersionAreKeptAsDeclared($apiVersion) + { + $parser = new VersionParser; + + /** @var Link[] $links */ + $links = $parser->parseLinks('Plugin', '9.9.9', '', array('composer-plugin-api' => $apiVersion)); + + $this->assertArrayHasKey('composer-plugin-api', $links); + $this->assertSame($apiVersion, $links['composer-plugin-api']->getConstraint()->getPrettyString()); + } + + public function testPluginApiVersionDoesSupportSelfVersion() + { + $parser = new VersionParser; + + /** @var Link[] $links */ + $links = $parser->parseLinks('Plugin', '6.6.6', '', array('composer-plugin-api' => 'self.version')); + + $this->assertArrayHasKey('composer-plugin-api', $links); + $this->assertSame('6.6.6', $links['composer-plugin-api']->getConstraint()->getPrettyString()); + } } diff --git a/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin.php b/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin.php new file mode 100644 index 000000000..2dae1b48b --- /dev/null +++ b/tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin.php @@ -0,0 +1,14 @@ +=3.0.0 <5.5" + } +} diff --git a/tests/Composer/Test/Plugin/PluginInstallerTest.php b/tests/Composer/Test/Plugin/PluginInstallerTest.php index a2090082f..ba45bad0b 100644 --- a/tests/Composer/Test/Plugin/PluginInstallerTest.php +++ b/tests/Composer/Test/Plugin/PluginInstallerTest.php @@ -15,29 +15,62 @@ namespace Composer\Test\Installer; use Composer\Composer; use Composer\Config; use Composer\Installer\PluginInstaller; +use Composer\Package\CompletePackage; use Composer\Package\Loader\JsonLoader; use Composer\Package\Loader\ArrayLoader; use Composer\Plugin\PluginManager; use Composer\Autoload\AutoloadGenerator; +use Composer\TestCase; use Composer\Util\Filesystem; -class PluginInstallerTest extends \PHPUnit_Framework_TestCase +class PluginInstallerTest extends TestCase { + /** + * @var Composer + */ protected $composer; + + /** + * @var PluginManager + */ + protected $pm; + + /** + * @var AutoloadGenerator + */ + protected $autoloadGenerator; + + /** + * @var CompletePackage[] + */ protected $packages; + + /** + * @var string + */ + protected $directory; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ protected $im; - protected $pm; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ protected $repository; + + /** + * @var \PHPUnit_Framework_MockObject_MockObject + */ protected $io; - protected $autoloadGenerator; - protected $directory; protected function setUp() { $loader = new JsonLoader(new ArrayLoader()); $this->packages = array(); $this->directory = sys_get_temp_dir() . '/' . uniqid(); - for ($i = 1; $i <= 4; $i++) { + for ($i = 1; $i <= 7; $i++) { $filename = '/Fixtures/plugin-v'.$i.'/composer.json'; mkdir(dirname($this->directory . $filename), 0777, true); $this->packages[] = $loader->load(__DIR__ . $filename); @@ -181,4 +214,108 @@ class PluginInstallerTest extends \PHPUnit_Framework_TestCase $this->assertCount(1, $plugins); $this->assertEquals('installer-v1', $plugins[0]->version); } + + /** + * @param string $newPluginApiVersion + * @param CompletePackage[] $plugins + */ + private function setPluginApiVersionWithPlugins($newPluginApiVersion, array $plugins = array()) + { + // reset the plugin manager's installed plugins + $this->pm = new PluginManager($this->io, $this->composer); + + /** @var \PHPUnit_Framework_MockObject_MockObject $config */ + $config = $this->getMock('Composer\Config', array('getPluginApiVersion')); + + // mock Config to return whatever Plugin API version we wish + $config->expects($this->any()) + ->method('getPluginApiVersion') + ->will($this->returnValue($newPluginApiVersion)); + + // transfer the defaults in our Config mock and set it + $config->merge($this->composer->getConfig()->raw()); + $this->composer->setConfig($config); + + $plugApiInternalPackage = $this->getPackage( + 'composer-plugin-api', + $newPluginApiVersion, + 'Composer\Package\CompletePackage' + ); + + // Add the plugins to the repo along with the internal Plugin package on which they all rely. + $this->repository + ->expects($this->any()) + ->method('getPackages') + ->will($this->returnCallback(function() use($plugApiInternalPackage, $plugins) { + return array_merge(array($plugApiInternalPackage), $plugins); + })); + + $this->pm->loadInstalledPlugins(); + } + + public function testOldPluginVersionStyleWorksWithAPIUntil199() + { + $pluginsWithOldStyleAPIVersions = array( + $this->packages[0], + $this->packages[1], + $this->packages[2], + ); + + $this->setPluginApiVersionWithPlugins('1.0.0', $pluginsWithOldStyleAPIVersions); + $this->assertCount(3, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('1.9.9', $pluginsWithOldStyleAPIVersions); + $this->assertCount(3, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('2.0.0-dev', $pluginsWithOldStyleAPIVersions); + $this->assertCount(0, $this->pm->getPlugins()); + } + + public function testStarPluginVersionWorksWithAnyAPIVersion() + { + $starVersionPlugin = array($this->packages[4]); + + $this->setPluginApiVersionWithPlugins('1.0.0', $starVersionPlugin); + $this->assertCount(1, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('1.9.9', $starVersionPlugin); + $this->assertCount(1, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('2.0.0-dev', $starVersionPlugin); + $this->assertCount(1, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('100.0.0-stable', $starVersionPlugin); + $this->assertCount(1, $this->pm->getPlugins()); + } + + public function testPluginConstraintWorksOnlyWithCertainAPIVersion() + { + $pluginWithApiConstraint = array($this->packages[5]); + + $this->setPluginApiVersionWithPlugins('1.0.0', $pluginWithApiConstraint); + $this->assertCount(0, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('1.1.9', $pluginWithApiConstraint); + $this->assertCount(0, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('1.2.0', $pluginWithApiConstraint); + $this->assertCount(1, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('1.9.9', $pluginWithApiConstraint); + $this->assertCount(1, $this->pm->getPlugins()); + } + + public function testPluginRangeConstraintsWorkOnlyWithCertainAPIVersion() + { + $pluginWithApiConstraint = array($this->packages[6]); + + $this->setPluginApiVersionWithPlugins('1.0.0', $pluginWithApiConstraint); + $this->assertCount(0, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('3.0.0', $pluginWithApiConstraint); + $this->assertCount(1, $this->pm->getPlugins()); + + $this->setPluginApiVersionWithPlugins('5.5.0', $pluginWithApiConstraint); + $this->assertCount(0, $this->pm->getPlugins()); + } }