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.
main
nevvermind 9 years ago
parent d4dbeeacc4
commit eb2aa14830

@ -13,6 +13,7 @@
namespace Composer;
use Composer\Config\ConfigSourceInterface;
use Composer\Plugin\PluginInterface;
/**
* @author Jordi Boggiano <j.boggiano@seld.be>
@ -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;
}
}

@ -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
*

@ -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';

@ -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("<warning>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.</warning>");
$currPluginApiVersion = $this->composer->getConfig()->getPluginApiVersion();
$currPluginApiConstraint = new VersionConstraint('==', $this->versionParser->normalize($currPluginApiVersion));
if (!$requiresComposer->matches($currPluginApiConstraint)) {
$this->io->writeError('<warning>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.</warning>');
continue;
}
$this->registerPackage($package);
}
// Backward compatibility
if ('composer-installer' === $package->getType()) {
} elseif ('composer-installer' === $package->getType()) {
$this->registerPackage($package);
}
}

@ -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;
}
}

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

@ -0,0 +1,14 @@
<?php
namespace Installer;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
class Plugin implements PluginInterface
{
public function activate(Composer $composer, IOInterface $io)
{
}
}

@ -0,0 +1,12 @@
{
"name": "plugin-v5",
"version": "1.0.0",
"type": "composer-plugin",
"autoload": { "psr-0": { "Installer": "" } },
"extra": {
"class": "Installer\\Plugin"
},
"require": {
"composer-plugin-api": "*"
}
}

@ -0,0 +1,14 @@
<?php
namespace Installer;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
class Plugin implements PluginInterface
{
public function activate(Composer $composer, IOInterface $io)
{
}
}

@ -0,0 +1,12 @@
{
"name": "plugin-v5",
"version": "1.0.0",
"type": "composer-plugin",
"autoload": { "psr-0": { "Installer": "" } },
"extra": {
"class": "Installer\\Plugin"
},
"require": {
"composer-plugin-api": "~1.2"
}
}

@ -0,0 +1,14 @@
<?php
namespace Installer;
use Composer\Composer;
use Composer\IO\IOInterface;
use Composer\Plugin\PluginInterface;
class Plugin implements PluginInterface
{
public function activate(Composer $composer, IOInterface $io)
{
}
}

@ -0,0 +1,12 @@
{
"name": "plugin-v5",
"version": "1.0.0",
"type": "composer-plugin",
"autoload": { "psr-0": { "Installer": "" } },
"extra": {
"class": "Installer\\Plugin"
},
"require": {
"composer-plugin-api": ">=3.0.0 <5.5"
}
}

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

Loading…
Cancel
Save