Przeglądaj źródła

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

Make plugins have actual constraints instead of fixed versions
Jordi Boggiano 10 lat temu
rodzic
commit
17c2a8019e

+ 23 - 9
doc/articles/plugins.md

@@ -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`.
-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
    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
 ```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

+ 1 - 0
src/Composer/Config.php

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

+ 16 - 0
src/Composer/Package/Version/VersionParser.php

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

+ 19 - 4
src/Composer/Plugin/PluginManager.php

@@ -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)))) {
-                    $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);
                 $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;
+    }
 }
 }

+ 1 - 0
src/Composer/Repository/PlatformRepository.php

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

+ 67 - 0
tests/Composer/Test/Package/Version/VersionParserTest.php

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

+ 14 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v5/Installer/Plugin5.php

@@ -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)
+    {
+    }
+}

+ 12 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v5/composer.json

@@ -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": "*"
+    }
+}

+ 14 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v6/Installer/Plugin6.php

@@ -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)
+    {
+    }
+}

+ 12 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v6/composer.json

@@ -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"
+    }
+}

+ 14 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v7/Installer/Plugin7.php

@@ -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)
+    {
+    }
+}

+ 12 - 0
tests/Composer/Test/Plugin/Fixtures/plugin-v7/composer.json

@@ -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"
+    }
+}

+ 138 - 5
tests/Composer/Test/Plugin/PluginInstallerTest.php

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