Browse Source

Merge remote-tracking branch 'slbmeh/feature/gh-2787'

Jordi Boggiano 9 years ago
parent
commit
06be9b88c2

+ 20 - 1
doc/06-config.md

@@ -17,7 +17,26 @@ in the PHP include path.
 ## preferred-install
 
 Defaults to `auto` and can be any of `source`, `dist` or `auto`. This option
-allows you to set the install method Composer will prefer to use.
+allows you to set the install method Composer will prefer to use. Can
+optionally be a hash of patterns for more granular install preferences.
+
+```json
+{
+    "config": {
+        "preferred-install": {
+            "my-organization/stable-package": "dist",
+            "my-organization/*": "source",
+            "partner-organization/*": "auto",
+            "*": "dist"
+        }
+    }
+}
+```
+
+> **Note:** Order matters. More specific patterns should be earlier than
+> more relaxed patterns. When mixing the string notation with the hash
+> configuration in global and package configurations the string notation
+> is translated to a `*` package pattern.
 
 ## store-auths
 

+ 2 - 2
res/composer-schema.json

@@ -117,8 +117,8 @@
                     "description": "If true, the Composer autoloader will also look for classes in the PHP include path."
                 },
                 "preferred-install": {
-                    "type": "string",
-                    "description": "The install method Composer will prefer to use, defaults to auto and can be any of source, dist or auto."
+                    "type": ["string", "object"],
+                    "description": "The install method Composer will prefer to use, defaults to auto and can be any of source, dist, auto, or a hash of {\"pattern\": \"preference\"}."
                 },
                 "notify-on-install": {
                     "type": "boolean",

+ 22 - 0
src/Composer/Config.php

@@ -21,6 +21,10 @@ class Config
 {
     const RELATIVE_PATHS = 1;
 
+    const INSTALL_PREFERENCE_AUTO = 'auto';
+    const INSTALL_PREFERENCE_DIST = 'dist';
+    const INSTALL_PREFERENCE_SOURCE = 'source';
+    
     public static $defaultConfig = array(
         'process-timeout' => 300,
         'use-include-path' => false,
@@ -120,6 +124,24 @@ class Config
             foreach ($config['config'] as $key => $val) {
                 if (in_array($key, array('github-oauth', 'gitlab-oauth', 'http-basic')) && isset($this->config[$key])) {
                     $this->config[$key] = array_merge($this->config[$key], $val);
+                } elseif ('preferred-install' === $key && isset($this->config[$key])) {
+                    if (is_array($val) || is_array($this->config[$key])) {
+                        if (is_string($val)) {
+                            $val = array('*' => $val);
+                        }
+                        if (is_string($this->config[$key])) {
+                            $this->config[$key] = array('*' => $this->config[$key]);
+                        }
+                        $this->config[$key] = array_merge($this->config[$key], $val);
+                        // the full match pattern needs to be last
+                        if (isset($this->config[$key]['*'])) {
+                            $wildcard = $this->config[$key]['*'];
+                            unset($this->config[$key]['*']);
+                            $this->config[$key]['*'] = $wildcard;
+                        }
+                    } else {
+                        $this->config[$key] = $val;
+                    }
                 } else {
                     $this->config[$key] = $val;
                 }

+ 38 - 1
src/Composer/Downloader/DownloadManager.php

@@ -12,6 +12,7 @@
 
 namespace Composer\Downloader;
 
+use Composer\Config;
 use Composer\Package\PackageInterface;
 use Composer\IO\IOInterface;
 use Composer\Util\Filesystem;
@@ -26,6 +27,7 @@ class DownloadManager
     private $io;
     private $preferDist = false;
     private $preferSource = false;
+    private $packagePreferences = array();
     private $filesystem;
     private $downloaders  = array();
 
@@ -69,6 +71,19 @@ class DownloadManager
         return $this;
     }
 
+    /**
+     * Sets fine tuned preference settings for package level source/dist selection.
+     *
+     * @param  array           $preferences array of preferences by package patterns
+     * @return DownloadManager
+     */
+    public function setPreferences(array $preferences)
+    {
+        $this->packagePreferences = $preferences;
+
+        return $this;
+    }
+
     /**
      * Sets whether to output download progress information for all registered
      * downloaders
@@ -182,7 +197,7 @@ class DownloadManager
             throw new \InvalidArgumentException('Package '.$package.' must have a source or dist specified');
         }
 
-        if ((!$package->isDev() || $this->preferDist) && !$preferSource) {
+        if (!$preferSource && ($this->preferDist || Config::INSTALL_PREFERENCE_DIST === $this->resolvePackageInstallPreference($package))) {
             $sources = array_reverse($sources);
         }
 
@@ -282,4 +297,26 @@ class DownloadManager
             $downloader->remove($package, $targetDir);
         }
     }
+
+    /**
+     * Determines the install preference of a package
+     *
+     * @param PackageInterface $package package instance
+     *
+     * @return string
+     */
+    protected function resolvePackageInstallPreference(PackageInterface $package)
+    {
+        foreach ($this->packagePreferences as $pattern => $preference) {
+            $pattern = '{^'.str_replace('*', '.*', $pattern).'$}i';
+            if (preg_match($pattern, $package->getName())) {
+                if (Config::INSTALL_PREFERENCE_DIST === $preference || (!$package->isDev() && Config::INSTALL_PREFERENCE_AUTO === $preference)) {
+                    return Config::INSTALL_PREFERENCE_DIST;
+                }
+                return Config::INSTALL_PREFERENCE_SOURCE;
+            }
+        }
+
+        return $package->isDev() ? Config::INSTALL_PREFERENCE_SOURCE : Config::INSTALL_PREFERENCE_DIST;
+    }
 }

+ 5 - 1
src/Composer/Factory.php

@@ -466,7 +466,7 @@ class Factory
         }
 
         $dm = new Downloader\DownloadManager($io);
-        switch ($config->get('preferred-install')) {
+        switch ($preferred = $config->get('preferred-install')) {
             case 'dist':
                 $dm->setPreferDist(true);
                 break;
@@ -479,6 +479,10 @@ class Factory
                 break;
         }
 
+        if (is_array($preferred)) {
+            $dm->setPreferences($preferred);
+        }
+
         $executor = new ProcessExecutor($io);
         $fs = new Filesystem($executor);
 

+ 21 - 0
tests/Composer/Test/ConfigTest.php

@@ -112,6 +112,27 @@ class ConfigTest extends \PHPUnit_Framework_TestCase
         return $data;
     }
 
+    public function testPreferredInstallAsString()
+    {
+        $config = new Config(false);
+        $config->merge(array('config' => array('preferred-install' => 'source')));
+        $config->merge(array('config' => array('preferred-install' => 'dist')));
+
+        $this->assertEquals('dist', $config->get('preferred-install'));
+    }
+
+    public function testMergePreferredInstall()
+    {
+        $config = new Config(false);
+        $config->merge(array('config' => array('preferred-install' => 'dist')));
+        $config->merge(array('config' => array('preferred-install' => array('foo/*' => 'source'))));
+
+        // This assertion needs to make sure full wildcard preferences are placed last
+        // Handled by composer because we convert string preferences for BC, all other
+        // care for ordering and collision prevention is up to the user
+        $this->assertEquals(array('foo/*' => 'source', '*' => 'dist'), $config->get('preferred-install'));
+    }
+
     public function testMergeGithubOauth()
     {
         $config = new Config(false);

+ 360 - 0
tests/Composer/Test/Downloader/DownloadManagerTest.php

@@ -757,6 +757,366 @@ class DownloadManagerTest extends \PHPUnit_Framework_TestCase
         $manager->remove($package, 'vendor/bundles/FOS/UserBundle');
     }
 
+    /**
+     * @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
+     */
+    public function testInstallPreferenceWithoutPreferenceDev()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+        $package
+            ->expects($this->once())
+            ->method('isDev')
+            ->will($this->returnValue(true));
+
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('source');
+
+        $downloader = $this->createDownloaderMock();
+        $downloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir');
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setConstructorArgs(array($this->io, false, $this->filesystem))
+            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('getDownloaderForInstalledPackage')
+            ->with($package)
+            ->will($this->returnValue($downloader));
+
+        $manager->download($package, 'target_dir');
+    }
+
+    /**
+     * @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
+     */
+    public function testInstallPreferenceWithoutPreferenceNoDev()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+        $package
+            ->expects($this->once())
+            ->method('isDev')
+            ->will($this->returnValue(false));
+
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('dist');
+
+        $downloader = $this->createDownloaderMock();
+        $downloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir');
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setConstructorArgs(array($this->io, false, $this->filesystem))
+            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('getDownloaderForInstalledPackage')
+            ->with($package)
+            ->will($this->returnValue($downloader));
+
+        $manager->download($package, 'target_dir');
+    }
+    
+    /**
+     * @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
+     */
+    public function testInstallPreferenceWithoutMatchDev()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+        $package
+            ->expects($this->once())
+            ->method('isDev')
+            ->will($this->returnValue(true));
+        $package
+            ->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('bar/package'));
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('source');
+
+        $downloader = $this->createDownloaderMock();
+        $downloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir');
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setConstructorArgs(array($this->io, false, $this->filesystem))
+            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('getDownloaderForInstalledPackage')
+            ->with($package)
+            ->will($this->returnValue($downloader));
+        $manager->setPreferences(array('foo/*' => 'source'));
+
+        $manager->download($package, 'target_dir');
+    }
+
+    /**
+     * @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
+     */
+    public function testInstallPreferenceWithoutMatchNoDev()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+        $package
+            ->expects($this->once())
+            ->method('isDev')
+            ->will($this->returnValue(false));
+        $package
+            ->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('bar/package'));
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('dist');
+
+        $downloader = $this->createDownloaderMock();
+        $downloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir');
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setConstructorArgs(array($this->io, false, $this->filesystem))
+            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('getDownloaderForInstalledPackage')
+            ->with($package)
+            ->will($this->returnValue($downloader));
+        $manager->setPreferences(array('foo/*' => 'source'));
+
+        $manager->download($package, 'target_dir');
+    }
+
+    /**
+     * @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
+     */
+    public function testInstallPreferenceWithMatchAutoDev()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+        $package
+            ->expects($this->once())
+            ->method('isDev')
+            ->will($this->returnValue(true));
+        $package
+            ->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('foo/package'));
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('source');
+
+        $downloader = $this->createDownloaderMock();
+        $downloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir');
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setConstructorArgs(array($this->io, false, $this->filesystem))
+            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('getDownloaderForInstalledPackage')
+            ->with($package)
+            ->will($this->returnValue($downloader));
+        $manager->setPreferences(array('foo/*' => 'auto'));
+
+        $manager->download($package, 'target_dir');
+    }
+
+    /**
+     * @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
+     */
+    public function testInstallPreferenceWithMatchAutoNoDev()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+        $package
+            ->expects($this->once())
+            ->method('isDev')
+            ->will($this->returnValue(false));
+        $package
+            ->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('foo/package'));
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('dist');
+
+        $downloader = $this->createDownloaderMock();
+        $downloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir');
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setConstructorArgs(array($this->io, false, $this->filesystem))
+            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('getDownloaderForInstalledPackage')
+            ->with($package)
+            ->will($this->returnValue($downloader));
+        $manager->setPreferences(array('foo/*' => 'auto'));
+
+        $manager->download($package, 'target_dir');
+    }
+
+    /**
+     * @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
+     */
+    public function testInstallPreferenceWithMatchSource()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+        $package
+            ->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('foo/package'));
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('source');
+
+        $downloader = $this->createDownloaderMock();
+        $downloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir');
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setConstructorArgs(array($this->io, false, $this->filesystem))
+            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('getDownloaderForInstalledPackage')
+            ->with($package)
+            ->will($this->returnValue($downloader));
+        $manager->setPreferences(array('foo/*' => 'source'));
+
+        $manager->download($package, 'target_dir');
+    }
+
+    /**
+     * @covers Composer\Downloader\DownloadManager::resolvePackageInstallPreference
+     */
+    public function testInstallPreferenceWithMatchDist()
+    {
+        $package = $this->createPackageMock();
+        $package
+            ->expects($this->once())
+            ->method('getSourceType')
+            ->will($this->returnValue('git'));
+        $package
+            ->expects($this->once())
+            ->method('getDistType')
+            ->will($this->returnValue('pear'));
+        $package
+            ->expects($this->once())
+            ->method('getName')
+            ->will($this->returnValue('foo/package'));
+        $package
+            ->expects($this->once())
+            ->method('setInstallationSource')
+            ->with('dist');
+
+        $downloader = $this->createDownloaderMock();
+        $downloader
+            ->expects($this->once())
+            ->method('download')
+            ->with($package, 'target_dir');
+
+        $manager = $this->getMockBuilder('Composer\Downloader\DownloadManager')
+            ->setConstructorArgs(array($this->io, false, $this->filesystem))
+            ->setMethods(array('getDownloaderForInstalledPackage'))
+            ->getMock();
+        $manager
+            ->expects($this->once())
+            ->method('getDownloaderForInstalledPackage')
+            ->with($package)
+            ->will($this->returnValue($downloader));
+        $manager->setPreferences(array('foo/*' => 'dist'));
+
+        $manager->download($package, 'target_dir');
+    }
+
     private function createDownloaderMock()
     {
         return $this->getMockBuilder('Composer\Downloader\DownloaderInterface')