From 91548d178b76682e8cc15efac3f93c6476d01194 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 25 Nov 2021 14:46:25 +0100 Subject: [PATCH] Add support for setting platform packages to false to disable them (#10308) Fixes #9664 --- doc/06-config.md | 3 + res/composer-schema.json | 2 +- src/Composer/Command/ConfigCommand.php | 2 +- src/Composer/Command/InitCommand.php | 6 +- src/Composer/Command/ShowCommand.php | 6 ++ src/Composer/DependencyResolver/Problem.php | 87 +++++++++++-------- .../Package/Loader/ValidatingArrayLoader.php | 7 ++ .../Repository/PlatformRepository.php | 63 +++++++++++++- ...olver-problems-with-disabled-platform.test | 86 ++++++++++++++++++ tests/deprecations-8.1.json | 4 +- 10 files changed, 224 insertions(+), 42 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/solver-problems-with-disabled-platform.test diff --git a/doc/06-config.md b/doc/06-config.md index 2a31bc512..276e40911 100644 --- a/doc/06-config.md +++ b/doc/06-config.md @@ -191,6 +191,9 @@ you may ignore it instead by passing `--ignore-platform-req=ext-foo` to `update` extensions as if you ignore one now and a new package you add a month later also requires it, you may introduce issues in production unknowingly. +If you have an extension installed locally but *not* on production, you may want +to artificially hide it from Composer using `{"ext-foo": false}`. + ## vendor-dir Defaults to `vendor`. You can install dependencies into a different directory if diff --git a/res/composer-schema.json b/res/composer-schema.json index 83d9b0e00..f371b48b7 100644 --- a/res/composer-schema.json +++ b/res/composer-schema.json @@ -203,7 +203,7 @@ "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": { - "type": "string" + "type": ["string", "boolean"] } }, "vendor-dir": { diff --git a/src/Composer/Command/ConfigCommand.php b/src/Composer/Command/ConfigCommand.php index b6dec7073..ecaa76324 100644 --- a/src/Composer/Command/ConfigCommand.php +++ b/src/Composer/Command/ConfigCommand.php @@ -704,7 +704,7 @@ EOT return 0; } - $this->configSource->addConfigSetting($settingKey, $values[0]); + $this->configSource->addConfigSetting($settingKey, $values[0] === 'false' ? false : $values[0]); return 0; } diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index b249ce507..939a3fe38 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -996,7 +996,11 @@ EOT } $platformPkg = $platformRepo->findPackage($link->getTarget(), '*'); if (!$platformPkg) { - $details[] = $candidate->getPrettyName().' '.$candidate->getPrettyVersion().' requires '.$link->getTarget().' '.$link->getPrettyConstraint().' but it is not present.'; + if ($platformRepo->isPlatformPackageDisabled($link->getTarget())) { + $details[] = $candidate->getPrettyName().' '.$candidate->getPrettyVersion().' requires '.$link->getTarget().' '.$link->getPrettyConstraint().' but it is disabled by your platform config. Enable it again with "composer config platform.'.$link->getTarget().' --unset".'; + } else { + $details[] = $candidate->getPrettyName().' '.$candidate->getPrettyVersion().' requires '.$link->getTarget().' '.$link->getPrettyConstraint().' but it is not present.'; + } continue; } if (!$link->getConstraint()->matches(new Constraint('==', $platformPkg->getVersion()))) { diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index dae6ac70b..321bcc41d 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -20,6 +20,7 @@ use Composer\Package\BasePackage; use Composer\Package\CompletePackageInterface; use Composer\Package\Link; use Composer\Package\AliasPackage; +use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Package\Version\VersionSelector; @@ -370,6 +371,11 @@ EOT } } } + if ($repo === $platformRepo) { + foreach ($platformRepo->getDisabledPackages() as $name => $package) { + $packages[$type][$name] = $package; + } + } } } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 72fb02a43..47c6c967c 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -22,6 +22,7 @@ use Composer\Repository\LockArrayRepository; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Package\Version\VersionParser; +use Composer\Repository\PlatformRepository; /** * Represents a problem detected while solving dependencies @@ -209,46 +210,62 @@ class Problem */ public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $isVerbose, $packageName, ConstraintInterface $constraint = null) { - // handle php/hhvm - if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { - $version = self::getPlatformPackageVersion($pool, $packageName, phpversion()); + if (PlatformRepository::isPlatformPackage($packageName)) { + // handle php/php-*/hhvm + if (0 === stripos($packageName, 'php') || $packageName === 'hhvm') { + $version = self::getPlatformPackageVersion($pool, $packageName, phpversion()); - $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but '; + $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but '; - if (defined('HHVM_VERSION') || ($packageName === 'hhvm' && count($pool->whatProvides($packageName)) > 0)) { - return array($msg, 'your HHVM version does not satisfy that requirement.'); + if (defined('HHVM_VERSION') || ($packageName === 'hhvm' && count($pool->whatProvides($packageName)) > 0)) { + return array($msg, 'your HHVM version does not satisfy that requirement.'); + } + + if ($packageName === 'hhvm') { + return array($msg, 'HHVM was not detected on this machine, make sure it is in your PATH.'); + } + + if (null === $version) { + return array($msg, 'the '.$packageName.' package is disabled by your platform config. Enable it again with "composer config platform.'.$packageName.' --unset".'); + } + + return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.'); } - if ($packageName === 'hhvm') { - return array($msg, 'HHVM was not detected on this machine, make sure it is in your PATH.'); + // handle php extensions + if (0 === stripos($packageName, 'ext-')) { + if (false !== strpos($packageName, ' ')) { + return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.'); + } + + $ext = substr($packageName, 4); + $msg = "- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but '; + + if (extension_loaded($ext)) { + $version = self::getPlatformPackageVersion($pool, $packageName, phpversion($ext) ?: '0'); + + if (null === $version) { + return array($msg, 'the '.$packageName.' package is disabled by your platform config. Enable it again with "composer config platform.'.$packageName.' --unset".'); + } + + $error = 'it has the wrong version ('.$version.') installed'; + } else { + $error = 'it is missing from your system'; + } + + return array($msg, $error.'. Install or enable PHP\'s '.$ext.' extension.'); } - return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.'); - } + // handle linked libs + if (0 === stripos($packageName, 'lib-')) { + if (strtolower($packageName) === 'lib-icu') { + $error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.'; - // handle php extensions - if (0 === stripos($packageName, 'ext-')) { - if (false !== strpos($packageName, ' ')) { - return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.'); + return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error); + } + + return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.'); } - - $ext = substr($packageName, 4); - $version = self::getPlatformPackageVersion($pool, $packageName, phpversion($ext) ?: '0'); - - $error = extension_loaded($ext) ? 'it has the wrong version ('.$version.') installed' : 'it is missing from your system'; - - return array("- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but ', $error.'. Install or enable PHP\'s '.$ext.' extension.'); - } - - // handle linked libs - if (0 === stripos($packageName, 'lib-')) { - if (strtolower($packageName) === 'lib-icu') { - $error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.'; - - return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error); - } - - return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.'); } $lockedPackage = null; @@ -404,9 +421,9 @@ class Problem } /** - * @param string $version * @param string $packageName - * @return string + * @param string $version the effective runtime version of the platform package + * @return ?string a version string or null if it appears the package was artificially disabled */ private static function getPlatformPackageVersion(Pool $pool, $packageName, $version) { @@ -419,6 +436,8 @@ class Problem if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { $version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription()); } + } else { + return null; } return $version; diff --git a/src/Composer/Package/Loader/ValidatingArrayLoader.php b/src/Composer/Package/Loader/ValidatingArrayLoader.php index 64df635b8..9e3d2bb0f 100644 --- a/src/Composer/Package/Loader/ValidatingArrayLoader.php +++ b/src/Composer/Package/Loader/ValidatingArrayLoader.php @@ -80,6 +80,13 @@ class ValidatingArrayLoader implements LoaderInterface if (!empty($this->config['config']['platform'])) { foreach ((array) $this->config['config']['platform'] as $key => $platform) { + if (false === $platform) { + continue; + } + if (!is_string($platform)) { + $this->errors[] = 'config.platform.' . $key . ' : invalid value ('.gettype($platform).' '.var_export($platform, true).'): expected string or false'; + continue; + } try { $this->versionParser->normalize($platform); } catch (\Exception $e) { diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 286e4f0c6..ee2199804 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -48,23 +48,36 @@ class PlatformRepository extends ArrayRepository * * Keyed by package name (lowercased) * - * @var array + * @var array */ private $overrides = array(); + /** + * Stores which packages have been disabled and their actual version + * + * @var array + */ + private $disabledPackages = array(); + /** @var Runtime */ private $runtime; /** @var HhvmDetector */ private $hhvmDetector; /** - * @param array $overrides + * @param array $overrides */ public function __construct(array $packages = array(), array $overrides = array(), Runtime $runtime = null, HhvmDetector $hhvmDetector = null) { $this->runtime = $runtime ?: new Runtime(); $this->hhvmDetector = $hhvmDetector ?: new HhvmDetector(); foreach ($overrides as $name => $version) { + if (!is_string($version) && false !== $version) { // @phpstan-ignore-line + throw new \UnexpectedValueException('config.platform.'.$name.' should be a string or false, but got '.gettype($version).' '.var_export($version, true)); + } + if ($name === 'php' && $version === false) { + throw new \UnexpectedValueException('config.platform.'.$name.' cannot be set to false as you cannot disable php entirely.'); + } $this->overrides[strtolower($name)] = array('name' => $name, 'version' => $version); } parent::__construct($packages); @@ -75,6 +88,23 @@ class PlatformRepository extends ArrayRepository return 'platform repo'; } + /** + * @param string $name + * @return boolean + */ + public function isPlatformPackageDisabled($name) + { + return isset($this->disabledPackages[$name]); + } + + /** + * @return array + */ + public function getDisabledPackages() + { + return $this->disabledPackages; + } + protected function initialize() { parent::initialize(); @@ -89,7 +119,9 @@ class PlatformRepository extends ArrayRepository throw new \InvalidArgumentException('Invalid platform package name in config.platform: '.$override['name']); } - $this->addOverriddenPackage($override); + if ($override['version'] !== false) { + $this->addOverriddenPackage($override); + } } $prettyVersion = PluginInterface::PLUGIN_API_VERSION; @@ -494,8 +526,17 @@ class PlatformRepository extends ArrayRepository */ public function addPackage(PackageInterface $package) { + if (!$package instanceof CompletePackage) { + throw new \UnexpectedValueException('Expected CompletePackage but got '.get_class($package)); + } + // Skip if overridden if (isset($this->overrides[$package->getName()])) { + if ($this->overrides[$package->getName()]['version'] === false) { + $this->addDisabledPackage($package); + return; + } + $overrider = $this->findPackage($package->getName(), '*'); if ($package->getVersion() === $overrider->getVersion()) { $actualText = 'same as actual'; @@ -511,6 +552,11 @@ class PlatformRepository extends ArrayRepository // Skip if PHP is overridden and we are adding a php-* package if (isset($this->overrides['php']) && 0 === strpos($package->getName(), 'php-')) { + if (isset($this->overrides[$package->getName()]) && $this->overrides[$package->getName()]['version'] === false) { + $this->addDisabledPackage($package); + return; + } + $overrider = $this->addOverriddenPackage($this->overrides['php'], $package->getPrettyName()); if ($package->getVersion() === $overrider->getVersion()) { $actualText = 'same as actual'; @@ -546,6 +592,17 @@ class PlatformRepository extends ArrayRepository return $package; } + /** + * @return void + */ + private function addDisabledPackage(CompletePackage $package) + { + $package->setDescription($package->getDescription().'. Package disabled via config.platform'); + $package->setExtra(array('config.platform' => true)); + + $this->disabledPackages[$package->getName()] = $package; + } + /** * Parses the version and adds a new package to the repository * diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems-with-disabled-platform.test b/tests/Composer/Test/Fixtures/installer/solver-problems-with-disabled-platform.test new file mode 100644 index 000000000..bc6a1535a --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/solver-problems-with-disabled-platform.test @@ -0,0 +1,86 @@ +--TEST-- +Test the error output of solver problems for disabled platform packages. ext/php are well reported if present but disabled, lib packages are currently not handled as it is too complex. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "dependency/pkg", "version": "1.0.0", "require": {"php-ipv6": "^8"} }, + { "name": "dependency/pkg2", "version": "1.0.0", "require": {"php-64bit": "^8"} }, + { "name": "dependency/pkg3", "version": "1.0.0", "require": {"lib-xml": "1002.*"} }, + { "name": "dependency/pkg4", "version": "1.0.0", "require": {"lib-icu": "1001.*"} }, + { "name": "dependency/pkg5", "version": "1.0.0", "require": {"ext-foobar": "1.0.0"} }, + { "name": "dependency/pkg6", "version": "1.0.0", "require": {"ext-pcre": "^8"} } + ] + } + ], + "require": { + "dependency/pkg": "1.*", + "dependency/pkg2": "1.*", + "dependency/pkg3": "1.*", + "dependency/pkg4": "1.*", + "dependency/pkg5": "1.*", + "dependency/pkg6": "1.*", + "php-64bit": "^8", + "php-ipv6": "^8", + "lib-xml": "1002.*", + "lib-icu": "1001.*", + "ext-foobar": "1.0.0", + "ext-pcre": "^8" + }, + "config": { + "platform": { + "php-64bit": false, + "php-ipv6": "8.0.3", + "lib-xml": false, + "lib-icu": false, + "ext-foobar": false, + "ext-pcre": false + } + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires php-64bit ^8 but the php-64bit package is disabled by your platform config. Enable it again with "composer config platform.php-64bit --unset". + Problem 2 + - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it. + Problem 3 + - Root composer.json requires linked library lib-icu 1001.* but it has the wrong version installed, try upgrading the intl extension. + Problem 4 + - Root composer.json requires PHP extension ext-foobar 1.0.0 but it is missing from your system. Install or enable PHP's foobar extension. + Problem 5 + - Root composer.json requires PHP extension ext-pcre ^8 but the ext-pcre package is disabled by your platform config. Enable it again with "composer config platform.ext-pcre --unset". + Problem 6 + - Root composer.json requires dependency/pkg2 1.* -> satisfiable by dependency/pkg2[1.0.0]. + - dependency/pkg2 1.0.0 requires php-64bit ^8 -> the php-64bit package is disabled by your platform config. Enable it again with "composer config platform.php-64bit --unset". + Problem 7 + - Root composer.json requires dependency/pkg3 1.* -> satisfiable by dependency/pkg3[1.0.0]. + - dependency/pkg3 1.0.0 requires lib-xml 1002.* -> it has the wrong version installed or is missing from your system, make sure to load the extension providing it. + Problem 8 + - Root composer.json requires dependency/pkg4 1.* -> satisfiable by dependency/pkg4[1.0.0]. + - dependency/pkg4 1.0.0 requires lib-icu 1001.* -> it has the wrong version installed, try upgrading the intl extension. + Problem 9 + - Root composer.json requires dependency/pkg5 1.* -> satisfiable by dependency/pkg5[1.0.0]. + - dependency/pkg5 1.0.0 requires ext-foobar 1.0.0 -> it is missing from your system. Install or enable PHP's foobar extension. + Problem 10 + - Root composer.json requires dependency/pkg6 1.* -> satisfiable by dependency/pkg6[1.0.0]. + - dependency/pkg6 1.0.0 requires ext-pcre ^8 -> the ext-pcre package is disabled by your platform config. Enable it again with "composer config platform.ext-pcre --unset". + +To enable extensions, verify that they are enabled in your .ini files: +__inilist__ +You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode. + +--EXPECT-- + diff --git a/tests/deprecations-8.1.json b/tests/deprecations-8.1.json index 87f06bf69..a987d98f0 100644 --- a/tests/deprecations-8.1.json +++ b/tests/deprecations-8.1.json @@ -77,12 +77,12 @@ { "location": "Composer\\Test\\InstallerTest::testIntegrationWithRawPool", "message": "preg_match(): Passing null to parameter #4 ($flags) of type int is deprecated", - "count": 1776 + "count": 1784 }, { "location": "Composer\\Test\\InstallerTest::testIntegrationWithPoolOptimizer", "message": "preg_match(): Passing null to parameter #4 ($flags) of type int is deprecated", - "count": 1776 + "count": 1784 }, { "location": "Composer\\Test\\Package\\Archiver\\ArchivableFilesFinderTest::testManualExcludes",