Merge pull request #4089 from nevvermind/plugin-api-versions
Make plugins have actual constraints instead of fixed versionspull/4103/head
commit
17c2a8019e
|
@ -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
|
||||
|
|
|
@ -13,6 +13,7 @@
|
|||
namespace Composer;
|
||||
|
||||
use Composer\Config\ConfigSourceInterface;
|
||||
use Composer\Plugin\PluginInterface;
|
||||
|
||||
/**
|
||||
* @author Jordi Boggiano <j.boggiano@seld.be>
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,6 +12,7 @@
|
|||
|
||||
namespace Composer\Repository;
|
||||
|
||||
use Composer\Config;
|
||||
use Composer\Package\PackageInterface;
|
||||
use Composer\Package\CompletePackage;
|
||||
use Composer\Package\Version\VersionParser;
|
||||
|
|
|
@ -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 Plugin5 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\\Plugin5"
|
||||
},
|
||||
"require": {
|
||||
"composer-plugin-api": "*"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
{
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue