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
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.
### 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
requirements:
1. the [type][1] attribute must be `composer-plugin`.
2. the [extra][2] attribute must contain an element `class` defining the
1. The [type][1] attribute must be `composer-plugin`.
2. The [extra][2] attribute must contain an element `class` defining the
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`
to define which composer API versions your plugin is compatible with. The
current composer plugin API version is 1.0.0.
The required version of the `composer-plugin-api` follows the same [rules][7]
as a normal package's, except for the `1.0`, `1.0.0` and `1.0.0.0` _exact_
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.
In other words, `"require": { "composer-plugin-api": "1.0.0" }` means
`"require": { "composer-plugin-api": "^1.0" }`.
For example
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
{
"name": "my/plugin-package",
"type": "composer-plugin",
"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
[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
[7]: ../01-basic-usage.md#package-versions

View File

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

View File

@ -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' === strtolower($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
*

View File

@ -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,18 @@ 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>");
$currentPluginApiVersion = $this->getPluginApiVersion();
$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);
}
// Backward compatibility
if ('composer-installer' === $package->getType()) {
} elseif ('composer-installer' === $package->getType()) {
$this->registerPackage($package);
}
}
@ -272,4 +277,14 @@ class PluginManager
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;
use Composer\Config;
use Composer\Package\PackageInterface;
use Composer\Package\CompletePackage;
use Composer\Package\Version\VersionParser;

View File

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

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\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;
protected $packages;
protected $im;
/**
* @var PluginManager
*/
protected $pm;
protected $repository;
protected $io;
/**
* @var AutoloadGenerator
*/
protected $autoloadGenerator;
/**
* @var CompletePackage[]
*/
protected $packages;
/**
* @var string
*/
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()
{
$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,104 @@ 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 = $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());
}
}