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()); + } }