1
0
Fork 0

Merge pull request #4089 from nevvermind/plugin-api-versions

Make plugins have actual constraints instead of fixed versions
pull/4103/head
Jordi Boggiano 2015-06-03 11:37:52 +01:00
commit 17c2a8019e
13 changed files with 344 additions and 19 deletions

View File

@ -16,7 +16,7 @@ specific logic.
## Creating a Plugin ## Creating a Plugin
A plugin is a regular composer package which ships its code as part of the A plugin is a regular Composer package which ships its code as part of the
package and may also depend on further packages. package and may also depend on further packages.
### Plugin Package ### Plugin Package
@ -24,23 +24,36 @@ package and may also depend on further packages.
The package file is the same as any other package file but with the following The package file is the same as any other package file but with the following
requirements: requirements:
1. the [type][1] attribute must be `composer-plugin`. 1. The [type][1] attribute must be `composer-plugin`.
2. the [extra][2] attribute must contain an element `class` defining the 2. The [extra][2] attribute must contain an element `class` defining the
class name of the plugin (including namespace). If a package contains class name of the plugin (including namespace). If a package contains
multiple plugins this can be array of class names. multiple plugins, this can be array of class names.
3. You must require the special package called `composer-plugin-api`
to define which Plugin API versions your plugin is compatible with.
Additionally you must require the special package called `composer-plugin-api` The required version of the `composer-plugin-api` follows the same [rules][7]
to define which composer API versions your plugin is compatible with. The as a normal package's, except for the `1.0`, `1.0.0` and `1.0.0.0` _exact_
current composer plugin API version is 1.0.0. values. In only these three cases, Composer will assume your plugin
actually meant `^1.0` instead. This was introduced to maintain BC with
the old style of declaring the Plugin API version.
For example In other words, `"require": { "composer-plugin-api": "1.0.0" }` means
`"require": { "composer-plugin-api": "^1.0" }`.
The current composer plugin API version is 1.0.0.
An example of a valid plugin `composer.json` file (with the autoloading
part omitted):
```json ```json
{ {
"name": "my/plugin-package", "name": "my/plugin-package",
"type": "composer-plugin", "type": "composer-plugin",
"require": { "require": {
"composer-plugin-api": "1.0.0" "composer-plugin-api": "~1.0"
},
"extra": {
"class": "My\\Plugin"
} }
} }
``` ```
@ -149,3 +162,4 @@ local project plugins are loaded.
[4]: https://github.com/composer/composer/blob/master/src/Composer/Composer.php [4]: https://github.com/composer/composer/blob/master/src/Composer/Composer.php
[5]: https://github.com/composer/composer/blob/master/src/Composer/IO/IOInterface.php [5]: https://github.com/composer/composer/blob/master/src/Composer/IO/IOInterface.php
[6]: https://github.com/composer/composer/blob/master/src/Composer/EventDispatcher/EventSubscriberInterface.php [6]: https://github.com/composer/composer/blob/master/src/Composer/EventDispatcher/EventSubscriberInterface.php
[7]: ../01-basic-usage.md#package-versions

View File

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

View File

@ -227,12 +227,28 @@ class VersionParser
} else { } else {
$parsedConstraint = $this->parseConstraints($constraint); $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' === strtolower($target) && $this->isOldStylePluginApiVersion($constraint)) {
$parsedConstraint = $this->parseConstraints('^1.0');
}
$res[strtolower($target)] = new Link($source, $target, $parsedConstraint, $description, $constraint); $res[strtolower($target)] = new Link($source, $target, $parsedConstraint, $description, $constraint);
} }
return $res; 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 * Parses as constraint string into LinkConstraint objects
* *

View File

@ -122,6 +122,7 @@ class PluginManager
foreach ($package->getRequires() as $link) { /** @var Link $link */ foreach ($package->getRequires() as $link) { /** @var Link $link */
if ('composer-plugin-api' === $link->getTarget()) { if ('composer-plugin-api' === $link->getTarget()) {
$requiresComposer = $link->getConstraint(); $requiresComposer = $link->getConstraint();
break;
} }
} }
@ -129,14 +130,18 @@ class PluginManager
throw new \RuntimeException("Plugin ".$package->getName()." is missing a require statement for a version of the composer-plugin-api package."); 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)))) { $currentPluginApiVersion = $this->getPluginApiVersion();
$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>"); $currentPluginApiConstraint = new VersionConstraint('==', $this->versionParser->normalize($currentPluginApiVersion));
if (!$requiresComposer->matches($currentPluginApiConstraint)) {
$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 ("' . $currentPluginApiVersion . '"). You may need to run composer update with the "--no-plugins" option.</warning>');
continue;
} }
$this->registerPackage($package); $this->registerPackage($package);
}
// Backward compatibility // Backward compatibility
if ('composer-installer' === $package->getType()) { } elseif ('composer-installer' === $package->getType()) {
$this->registerPackage($package); $this->registerPackage($package);
} }
} }
@ -272,4 +277,14 @@ class PluginManager
return $this->globalComposer->getInstallationManager()->getInstallPath($package); return $this->globalComposer->getInstallationManager()->getInstallPath($package);
} }
/**
* Returns the version of the internal composer-plugin-api package.
*
* @return string
*/
protected function getPluginApiVersion()
{
return PluginInterface::PLUGIN_API_VERSION;
}
} }

View File

@ -12,6 +12,7 @@
namespace Composer\Repository; namespace Composer\Repository;
use Composer\Config;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Package\CompletePackage; use Composer\Package\CompletePackage;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;

View File

@ -12,6 +12,7 @@
namespace Composer\Test\Package\Version; namespace Composer\Test\Package\Version;
use Composer\Package\Link;
use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionParser;
use Composer\Package\LinkConstraint\MultiConstraint; use Composer\Package\LinkConstraint\MultiConstraint;
use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\LinkConstraint\VersionConstraint;
@ -513,4 +514,70 @@ class VersionParserTest extends \PHPUnit_Framework_TestCase
array('RC', '2.0.0rc1') 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());
}
} }

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -15,29 +15,62 @@ namespace Composer\Test\Installer;
use Composer\Composer; use Composer\Composer;
use Composer\Config; use Composer\Config;
use Composer\Installer\PluginInstaller; use Composer\Installer\PluginInstaller;
use Composer\Package\CompletePackage;
use Composer\Package\Loader\JsonLoader; use Composer\Package\Loader\JsonLoader;
use Composer\Package\Loader\ArrayLoader; use Composer\Package\Loader\ArrayLoader;
use Composer\Plugin\PluginManager; use Composer\Plugin\PluginManager;
use Composer\Autoload\AutoloadGenerator; use Composer\Autoload\AutoloadGenerator;
use Composer\TestCase;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
class PluginInstallerTest extends \PHPUnit_Framework_TestCase class PluginInstallerTest extends TestCase
{ {
/**
* @var Composer
*/
protected $composer; protected $composer;
protected $packages;
protected $im; /**
* @var PluginManager
*/
protected $pm; protected $pm;
protected $repository;
protected $io; /**
* @var AutoloadGenerator
*/
protected $autoloadGenerator; protected $autoloadGenerator;
/**
* @var CompletePackage[]
*/
protected $packages;
/**
* @var string
*/
protected $directory; protected $directory;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $im;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $repository;
/**
* @var \PHPUnit_Framework_MockObject_MockObject
*/
protected $io;
protected function setUp() protected function setUp()
{ {
$loader = new JsonLoader(new ArrayLoader()); $loader = new JsonLoader(new ArrayLoader());
$this->packages = array(); $this->packages = array();
$this->directory = sys_get_temp_dir() . '/' . uniqid(); $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'; $filename = '/Fixtures/plugin-v'.$i.'/composer.json';
mkdir(dirname($this->directory . $filename), 0777, true); mkdir(dirname($this->directory . $filename), 0777, true);
$this->packages[] = $loader->load(__DIR__ . $filename); $this->packages[] = $loader->load(__DIR__ . $filename);
@ -181,4 +214,104 @@ class PluginInstallerTest extends \PHPUnit_Framework_TestCase
$this->assertCount(1, $plugins); $this->assertCount(1, $plugins);
$this->assertEquals('installer-v1', $plugins[0]->version); $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 = $this->getMockBuilder('Composer\Plugin\PluginManager')
->setMethods(array('getPluginApiVersion'))
->setConstructorArgs(array($this->io, $this->composer))
->getMock();
// mock the Plugin API version
$this->pm->expects($this->any())
->method('getPluginApiVersion')
->will($this->returnValue($newPluginApiVersion));
$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());
}
} }