diff --git a/doc/03-cli.md b/doc/03-cli.md index dd1121c80..84b1f0d5c 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -82,7 +82,7 @@ resolution. have a proper setup. * **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` requirements and force the installation even if the local machine does not - fulfill these. + fulfill these. See also the `platform` [config option](04-schema.md#config). * **--dry-run:** If you want to run through an installation without actually installing a package, you can use `--dry-run`. This will simulate the installation and show you what would happen. @@ -127,7 +127,7 @@ php composer.phar update vendor/* * **--prefer-dist:** Install packages from `dist` when available. * **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` requirements and force the installation even if the local machine does not - fulfill these. + fulfill these. See also the `platform` [config option](04-schema.md#config). * **--dry-run:** Simulate the command without actually doing anything. * **--dev:** Install packages listed in `require-dev` (this is the default behavior). * **--no-dev:** Skip installing packages listed in `require-dev`. The autoloader generation skips the `autoload-dev` rules. @@ -171,7 +171,7 @@ php composer.phar require vendor/package:2.* vendor/package2:dev-master * **--prefer-dist:** Install packages from `dist` when available. * **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` requirements and force the installation even if the local machine does not - fulfill these. + fulfill these. See also the `platform` [config option](04-schema.md#config). * **--dev:** Add packages to `require-dev`. * **--no-update:** Disables the automatic update of the dependencies. * **--no-progress:** Removes the progress display that can mess with some @@ -195,7 +195,7 @@ uninstalled. ### Options * **--ignore-platform-reqs:** ignore `php`, `hhvm`, `lib-*` and `ext-*` requirements and force the installation even if the local machine does not - fulfill these. + fulfill these. See also the `platform` [config option](04-schema.md#config). * **--dev:** Remove packages from `require-dev`. * **--no-update:** Disables the automatic update of the dependencies. * **--no-progress:** Removes the progress display that can mess with some diff --git a/doc/04-schema.md b/doc/04-schema.md index cd370dff2..138fc0d5a 100644 --- a/doc/04-schema.md +++ b/doc/04-schema.md @@ -761,6 +761,9 @@ The following options are supported: against them. For example using `{"example.org": {"username": "alice", "password": "foo"}` as the value of this option will let composer authenticate against example.org. +* **platform:** Lets you fake platform packages (PHP and extensions) so that + you can emulate a production env or define your target platform in the + config. e.g. `{"php": "5.4", "ext-something": "4.0"}`. * **vendor-dir:** Defaults to `vendor`. You can install dependencies into a different directory if you want to. `$HOME` and `~` will be replaced by your home directory's path in vendor-dir and all `*-dir` options below. diff --git a/res/composer-schema.json b/res/composer-schema.json index 6c7492c8f..99941c1e2 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -145,6 +145,11 @@ "type": ["string", "boolean"], "description": "What to do after prompting for authentication, one of: true (store), false (do not store) or \"prompt\" (ask every time), defaults to prompt." }, + "platform": { + "type": "object", + "description": "This is a hash of package name (keys) and version (values) that will be used to mock the platform packages on this machine.", + "additionalProperties": true + }, "vendor-dir": { "type": "string", "description": "The location where all packages are installed, defaults to \"vendor\"." diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index f170c9327..0a39df22b 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -410,6 +410,15 @@ EOT throw new \RuntimeException('You must pass the type and a url. Example: php composer.phar config repositories.foo vcs http://bar.com'); } + // handle platform + if (preg_match('/^platform\.(.+)/', $settingKey, $matches)) { + if ($input->getOption('unset')) { + return $this->configSource->removeConfigSetting($settingKey); + } + + return $this->configSource->addConfigSetting($settingKey, $values[0]); + } + // handle github-oauth if (preg_match('/^(github-oauth|http-basic)\.(.+)/', $settingKey, $matches)) { if ($input->getOption('unset')) { diff --git a/src/Composer/Config/JsonConfigSource.php b/src/Composer/Config/JsonConfigSource.php index 4fba2943c..ed35aa0af 100644 --- a/src/Composer/Config/JsonConfigSource.php +++ b/src/Composer/Config/JsonConfigSource.php @@ -79,7 +79,7 @@ class JsonConfigSource implements ConfigSourceInterface public function addConfigSetting($name, $value) { $this->manipulateJson('addConfigSetting', $name, $value, function (&$config, $key, $val) { - if ($key === 'github-oauth' || $key === 'http-basic') { + if (preg_match('{^(github-oauth|http-basic|platform)\.}', $key)) { list($key, $host) = explode('.', $key, 2); if ($this->authConfig) { $config[$key][$host] = $val; @@ -98,7 +98,7 @@ class JsonConfigSource implements ConfigSourceInterface public function removeConfigSetting($name) { $this->manipulateJson('removeConfigSetting', $name, function (&$config, $key) { - if ($key === 'github-oauth' || $key === 'http-basic') { + if (preg_match('{^(github-oauth|http-basic|platform)\.}', $key)) { list($key, $host) = explode('.', $key, 2); if ($this->authConfig) { unset($config[$key][$host]); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 5934dd0df..e1b712954 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -206,9 +206,12 @@ class Installer // create installed repo, this contains all local packages + platform packages (php & extensions) $localRepo = $this->repositoryManager->getLocalRepository(); - $platformOverride = $this->config->get('platform'); - $platformOverride = is_array($platformOverride) ? $platformOverride : array(); - $platformRepo = new PlatformRepository($platformOverride); + if (!$this->update && $this->locker->isLocked()) { + $platformOverrides = $this->locker->getPlatformOverrides(); + } else { + $platformOverrides = $this->config->get('platform') ?: array(); + } + $platformRepo = new PlatformRepository(array(), $platformOverrides); $repos = array( $localRepo, new InstalledArrayRepository(array($installedRootPackage)), @@ -313,7 +316,8 @@ class Installer $this->package->getMinimumStability(), $this->package->getStabilityFlags(), $this->preferStable || $this->package->getPreferStable(), - $this->preferLowest + $this->preferLowest, + $this->config->get('platform') ?: array() ); if ($updatedLock) { $this->io->writeError('Writing lock file'); diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 171cf0362..2a328093f 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -191,6 +191,13 @@ class Locker return isset($lockData['prefer-lowest']) ? $lockData['prefer-lowest'] : null; } + public function getPlatformOverrides() + { + $lockData = $this->getLockData(); + + return isset($lockData['platform-overrides']) ? $lockData['platform-overrides'] : array(); + } + public function getAliases() { $lockData = $this->getLockData(); @@ -214,19 +221,20 @@ class Locker /** * Locks provided data into lockfile. * - * @param array $packages array of packages - * @param mixed $devPackages array of dev packages or null if installed without --dev - * @param array $platformReqs array of package name => constraint for required platform packages - * @param mixed $platformDevReqs array of package name => constraint for dev-required platform packages - * @param array $aliases array of aliases + * @param array $packages array of packages + * @param mixed $devPackages array of dev packages or null if installed without --dev + * @param array $platformReqs array of package name => constraint for required platform packages + * @param mixed $platformDevReqs array of package name => constraint for dev-required platform packages + * @param array $aliases array of aliases * @param string $minimumStability * @param array $stabilityFlags * @param bool $preferStable * @param bool $preferLowest + * @param array $platformOverrides * * @return bool */ - public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest) + public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides) { $lock = array( '_readme' => array('This file locks the dependencies of your project to a known state', @@ -260,6 +268,9 @@ class Locker $lock['platform'] = $platformReqs; $lock['platform-dev'] = $platformDevReqs; + if ($platformOverrides) { + $lock['platform-overrides'] = $platformOverrides; + } if (empty($lock['packages']) && empty($lock['packages-dev']) && empty($lock['platform']) && empty($lock['platform-dev'])) { if ($this->lockFile->exists()) { diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 909eaab7c..4afd3d4e9 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -24,12 +24,22 @@ class PlatformRepository extends ArrayRepository { const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit)?|hhvm|(?:ext|lib)-[^/]+)$}i'; + /** + * Defines overrides so that the platform can be mocked + * + * Should be an array of package name => version number mappings + * + * @var array + */ private $overrides; - public function __construct(array $overrides = array()) + public function __construct(array $packages = array(), array $overrides = array()) { - parent::__construct(array()); - $this->overrides = $overrides; + parent::__construct($packages); + $this->overrides = array(); + foreach ($overrides as $name => $version) { + $this->overrides[strtolower($name)] = array('name' => $name, 'version' => $version); + } } protected function initialize() @@ -40,19 +50,17 @@ class PlatformRepository extends ArrayRepository // Add each of the override versions as options. // Later we might even replace the extensions instead. - foreach( $this->overrides as $name => $prettyVersion ) { + foreach ($this->overrides as $override) { // Check that it's a platform package. - if( preg_match(self::PLATFORM_PACKAGE_REGEX, $name) ) { - $version = $versionParser->normalize($prettyVersion); - $package = new CompletePackage($name, $version, $prettyVersion); - $package->setDescription("Overridden virtual platform package $name."); - parent::addPackage($package); + if (!preg_match(self::PLATFORM_PACKAGE_REGEX, $override['name'])) { + throw new \InvalidArgumentException('Invalid platform package name in config.platform: '.$override['name']); } - else { - throw new \InvalidArgumentException('Invalid platform package '.$name); - } - } + $version = $versionParser->normalize($override['version']); + $package = new CompletePackage($override['name'], $version, $override['version']); + $package->setDescription('Overridden virtual platform package '.$override['name']); + parent::addPackage($package); + } $prettyVersion = PluginInterface::PLUGIN_API_VERSION; $version = $versionParser->normalize($prettyVersion); @@ -186,21 +194,13 @@ class PlatformRepository extends ArrayRepository } } - // TODO: Is it a good thing to redefine the public interface - // like this, or is it better to make the "only-add-if-no-in-platform" - // feature in a - // protected function addOverriddenPackage() - // instead? + /** + * {@inheritDoc} + */ public function addPackage(PackageInterface $package) { - /* - If we can find the package in this repository, - in any version, it can only mean that it has been - added by the config key 'platform' and should - the real package (i.e. this one) should not be added. - */ - if( count($this->findPackages($package->getName())) > 0 ) { - // Log a warning that we're ignoring existing package? + // Skip if overridden + if (isset($this->overrides[strtolower($package->getName())])) { return; } parent::addPackage($package); diff --git a/tests/Composer/Test/Fixtures/installer/install-overridden-platform-packages.test b/tests/Composer/Test/Fixtures/installer/install-overridden-platform-packages.test new file mode 100644 index 000000000..b6d703dd7 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/install-overridden-platform-packages.test @@ -0,0 +1,29 @@ +--TEST-- +Install overridden platform requirements works +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0", "require": { "ext-dummy2": "1.*" } } + ] + } + ], + "require": { + "a/a": "*", + "ext-dummy": "~1.0", + "php": "1.0" + }, + "config": { + "platform": { + "php": "1.0.0", + "ext-dummy": "1.2.3", + "ext-dummy2": "1.2.3" + } + } +} +--RUN-- +install +--EXPECT-- +Installing a/a (1.0.0) diff --git a/tests/Composer/Test/Package/LockerTest.php b/tests/Composer/Test/Package/LockerTest.php index 913868cd1..916f3490c 100644 --- a/tests/Composer/Test/Package/LockerTest.php +++ b/tests/Composer/Test/Package/LockerTest.php @@ -134,11 +134,12 @@ class LockerTest extends \PHPUnit_Framework_TestCase 'stability-flags' => array(), 'platform' => array(), 'platform-dev' => array(), + 'platform-overrides' => array('foo/bar' => '1.0'), 'prefer-stable' => false, 'prefer-lowest' => false, )); - $locker->setLockData(array($package1, $package2), array(), array(), array(), array(), 'dev', array(), false, false); + $locker->setLockData(array($package1, $package2), array(), array(), array(), array(), 'dev', array(), false, false, array('foo/bar' => '1.0')); } public function testLockBadPackages() @@ -157,7 +158,7 @@ class LockerTest extends \PHPUnit_Framework_TestCase $this->setExpectedException('LogicException'); - $locker->setLockData(array($package1), array(), array(), array(), array(), 'dev', array(), false, false); + $locker->setLockData(array($package1), array(), array(), array(), array(), 'dev', array(), false, false, array()); } public function testIsFresh()