From e30a6b0b9b1e529499f278fa22fbbac351c150f2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 28 Nov 2021 13:29:54 +0100 Subject: [PATCH 1/2] Add support for ignoring the upper bound of platform requirements using "name+" notation --- doc/03-cli.md | 10 +++- .../DependencyResolver/RuleSetGenerator.php | 13 ++++- src/Composer/DependencyResolver/Solver.php | 3 ++ .../IgnoreListPlatformRequirementFilter.php | 53 +++++++++++++++++-- src/Composer/Installer.php | 12 +++-- ...package-requirement-list-upper-bounds.test | 50 +++++++++++++++++ tests/deprecations-8.1.json | 4 +- 7 files changed, 131 insertions(+), 14 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirement-list-upper-bounds.test diff --git a/doc/03-cli.md b/doc/03-cli.md index 12125919c..727e5fc5d 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -117,7 +117,10 @@ resolution. See also the [`platform`](06-config.md#platform) config option. * **--ignore-platform-req:** ignore a specific platform requirement(`php`, `hhvm`, `lib-*` and `ext-*`) and force the installation even if the local machine - does not fulfill it. Multiple requirements can be ignored via wildcard. + does not fulfill it. Multiple requirements can be ignored via wildcard. Appending + a `+` makes it only ignore the upper-bound of the requirements. For example, if a package + requires `php: ^7`, then the option `--ignore-platform-req=php+` would allow installing on PHP8, + but installation on PHP 5.6 would still fail. ## update / u @@ -202,7 +205,10 @@ php composer.phar update vendor/package:2.0.1 vendor/package2:3.0.* See also the [`platform`](06-config.md#platform) config option. * **--ignore-platform-req:** ignore a specific platform requirement(`php`, `hhvm`, `lib-*` and `ext-*`) and force the installation even if the local machine - does not fulfill it. Multiple requirements can be ignored via wildcard. + does not fulfill it. Multiple requirements can be ignored via wildcard. Appending + a `+` makes it only ignore the upper-bound of the requirements. For example, if a package + requires `php: ^7`, then the option `--ignore-platform-req=php+` would allow installing on PHP8, + but installation on PHP 5.6 would still fail. * **--prefer-stable:** Prefer stable versions of dependencies. * **--prefer-lowest:** Prefer lowest versions of dependencies. Useful for testing minimal versions of requirements, generally used with `--prefer-stable`. diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 94c834841..9164c333a 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -12,6 +12,7 @@ namespace Composer\DependencyResolver; +use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Package\BasePackage; @@ -197,11 +198,14 @@ class RuleSetGenerator } foreach ($package->getRequires() as $link) { + $constraint = $link->getConstraint(); if ($platformRequirementFilter->isIgnored($link->getTarget())) { continue; + } elseif ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { + $constraint = $platformRequirementFilter->filterConstraint($link->getTarget(), $constraint); } - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + $possibleRequires = $this->pool->whatProvides($link->getTarget(), $constraint); $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, $link)); @@ -225,11 +229,14 @@ class RuleSetGenerator continue; } + $constraint = $link->getConstraint(); if ($platformRequirementFilter->isIgnored($link->getTarget())) { continue; + } elseif ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { + $constraint = $platformRequirementFilter->filterConstraint($link->getTarget(), $constraint); } - $conflicts = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + $conflicts = $this->pool->whatProvides($link->getTarget(), $constraint); foreach ($conflicts as $conflict) { // define the conflict rule for regular packages, for alias packages it's only needed if the name @@ -277,6 +284,8 @@ class RuleSetGenerator foreach ($request->getRequires() as $packageName => $constraint) { if ($platformRequirementFilter->isIgnored($packageName)) { continue; + } elseif ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { + $constraint = $platformRequirementFilter->filterConstraint($packageName, $constraint); } $packages = $this->pool->whatProvides($packageName, $constraint); diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 9b112d87f..28b9e40da 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -12,6 +12,7 @@ namespace Composer\DependencyResolver; +use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\IO\IOInterface; @@ -174,6 +175,8 @@ class Solver foreach ($request->getRequires() as $packageName => $constraint) { if ($platformRequirementFilter->isIgnored($packageName)) { continue; + } elseif ($platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { + $constraint = $platformRequirementFilter->filterConstraint($packageName, $constraint); } if (!$this->pool->whatProvides($packageName, $constraint)) { diff --git a/src/Composer/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilter.php b/src/Composer/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilter.php index de1388edd..4e3ac7f58 100644 --- a/src/Composer/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilter.php +++ b/src/Composer/Filter/PlatformRequirementFilter/IgnoreListPlatformRequirementFilter.php @@ -5,20 +5,40 @@ namespace Composer\Filter\PlatformRequirementFilter; use Composer\Package\BasePackage; use Composer\Pcre\Preg; use Composer\Repository\PlatformRepository; +use Composer\Semver\Constraint\Constraint; +use Composer\Semver\Constraint\ConstraintInterface; +use Composer\Semver\Constraint\MatchAllConstraint; +use Composer\Semver\Constraint\MultiConstraint; +use Composer\Semver\Interval; +use Composer\Semver\Intervals; final class IgnoreListPlatformRequirementFilter implements PlatformRequirementFilterInterface { /** * @var string */ - private $regexp; + private $ignoreRegex; + + /** + * @var string + */ + private $ignoreUpperBoundRegex; /** * @param string[] $reqList */ public function __construct(array $reqList) { - $this->regexp = BasePackage::packageNamesToRegexp($reqList); + $ignoreAll = $ignoreUpperBound = array(); + foreach ($reqList as $req) { + if (substr($req, -1) === '+') { + $ignoreUpperBound[] = substr($req, 0, -1); + } else { + $ignoreAll[] = $req; + } + } + $this->ignoreRegex = BasePackage::packageNamesToRegexp($ignoreAll); + $this->ignoreUpperBoundRegex = BasePackage::packageNamesToRegexp($ignoreUpperBound); } /** @@ -31,6 +51,33 @@ final class IgnoreListPlatformRequirementFilter implements PlatformRequirementFi return false; } - return Preg::isMatch($this->regexp, $req); + return Preg::isMatch($this->ignoreRegex, $req); + } + + /** + * @param string $req + * @return ConstraintInterface + */ + public function filterConstraint($req, ConstraintInterface $constraint) + { + if (!PlatformRepository::isPlatformPackage($req)) { + return $constraint; + } + + if (!Preg::isMatch($this->ignoreUpperBoundRegex, $req)) { + return $constraint; + } + + if (Preg::isMatch($this->ignoreRegex, $req)) { + return new MatchAllConstraint; + } + + $intervals = Intervals::get($constraint); + $last = end($intervals['numeric']); + if ($last !== false && (string) $last->getEnd() !== (string) Interval::untilPositiveInfinity()) { + $constraint = new MultiConstraint(array($constraint, new Constraint('>=', $last->getEnd()->getVersion())), false); + } + + return $constraint; } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index ebdc30caa..c78dc8fd7 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -28,6 +28,7 @@ use Composer\DependencyResolver\SolverProblemsException; use Composer\DependencyResolver\PolicyInterface; use Composer\Downloader\DownloadManager; use Composer\EventDispatcher\EventDispatcher; +use Composer\Filter\PlatformRequirementFilter\IgnoreListPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; use Composer\Installer\InstallationManager; @@ -807,15 +808,16 @@ class Installer $rootRequires = array(); foreach ($requires as $req => $constraint) { + if ($constraint instanceof Link) { + $constraint = $constraint->getConstraint(); + } // skip platform requirements from the root package to avoid filtering out existing platform packages if ($this->platformRequirementFilter->isIgnored($req)) { continue; + } elseif ($this->platformRequirementFilter instanceof IgnoreListPlatformRequirementFilter) { + $constraint = $this->platformRequirementFilter->filterConstraint($req, $constraint); } - if ($constraint instanceof Link) { - $rootRequires[$req] = $constraint->getConstraint(); - } else { - $rootRequires[$req] = $constraint; - } + $rootRequires[$req] = $constraint; } $this->fixedRootPackage = clone $this->package; diff --git a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirement-list-upper-bounds.test b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirement-list-upper-bounds.test new file mode 100644 index 000000000..3d9d8540a --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirement-list-upper-bounds.test @@ -0,0 +1,50 @@ +--TEST-- +Update with ignore-platform-req list ignoring upper bound of a dependency +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.1", "require": { "ext-foo-bar": "3.*" } }, + { "name": "b/b", "version": "1.0.1", "require": { "ext-foo-bar": "10.*" } } + ] + } + ], + "require": { + "a/a": "1.0.*", + "b/b": "1.0.*", + "php": "^4.3", + "ext-foo-baz": "9.0.0" + }, + "provide": { + "ext-foo-bar": "5.0.3" + } +} +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0" } +] +--RUN-- +update --ignore-platform-req=php+ --ignore-platform-req=ext-foo-bar+ --ignore-platform-req=ext-foo-baz+ + +--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 extension ext-foo-baz [== 9.0.0.0 || >= 9.0.0.0] but it is missing from your system. Install or enable PHP's foo-baz extension. + Problem 2 + - Root composer.json requires b/b 1.0.* -> satisfiable by b/b[1.0.1]. + - b/b 1.0.1 requires ext-foo-bar 10.* -> it has the wrong version (5.0.3 provided by __root__ 1.0.0+no-version-set) installed. Install or enable PHP's foo-bar extension. + +To enable extensions, verify that they are enabled in your .ini files: +__inilist__ +You can also run `php --ini` in a terminal to see which files are used by PHP in CLI mode. +Alternatively, you can run Composer with `--ignore-platform-req=ext-foo-baz --ignore-platform-req=ext-foo-bar` to temporarily ignore these required extensions. + +--EXPECT-- diff --git a/tests/deprecations-8.1.json b/tests/deprecations-8.1.json index 0982624a2..50fe04397 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": 1800 + "count": 1820 }, { "location": "Composer\\Test\\InstallerTest::testIntegrationWithPoolOptimizer", "message": "preg_match(): Passing null to parameter #4 ($flags) of type int is deprecated", - "count": 1800 + "count": 1820 }, { "location": "Composer\\Test\\Package\\Archiver\\ArchivableFilesFinderTest::testManualExcludes", From ca5d5b40ee804e22b649836271cfc229d38eef8d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 7 Dec 2021 09:50:35 +0100 Subject: [PATCH 2/2] Fix platform package description for replacer/provider and packages which are not really loaded in general --- composer.json | 2 +- src/Composer/DependencyResolver/Problem.php | 49 +++++++++++++------ .../platform-ext-solver-problems.test | 2 +- .../Fixtures/installer/solver-problems.test | 3 +- ...package-requirement-list-upper-bounds.test | 2 +- 5 files changed, 39 insertions(+), 19 deletions(-) diff --git a/composer.json b/composer.json index ad67dc3e9..b772f9546 100644 --- a/composer.json +++ b/composer.json @@ -79,7 +79,7 @@ ], "scripts": { "compile": "@php -dphar.readonly=0 bin/compile", - "test": "simple-phpunit", + "test": "@php simple-phpunit", "phpstan-setup": [ "@composer config platform --unset", "@composer update", diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index deb39d5f6..836f308b4 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -15,6 +15,7 @@ namespace Composer\DependencyResolver; use Composer\Package\CompletePackageInterface; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; +use Composer\Package\Link; use Composer\Package\PackageInterface; use Composer\Package\RootPackageInterface; use Composer\Pcre\Preg; @@ -242,19 +243,19 @@ class Problem $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".'); + $version = self::getPlatformPackageVersion($pool, $packageName, phpversion($ext) ?: '0'); + if (null === $version) { + if (extension_loaded($ext)) { + 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, 'it is missing from your system. Install or enable PHP\'s '.$ext.' extension.'); } - return array($msg, $error.'. Install or enable PHP\'s '.$ext.' extension.'); + return array($msg, 'it has the wrong version installed ('.$version.').'); } // handle linked libs @@ -431,11 +432,31 @@ class Problem $available = $pool->whatProvides($packageName); if (count($available)) { - $firstAvailable = reset($available); - $version = $firstAvailable->getPrettyVersion(); - $extra = $firstAvailable->getExtra(); - if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { - $version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription()); + $selected = null; + foreach ($available as $pkg) { + if ($pkg->getRepository() instanceof PlatformRepository) { + $selected = $pkg; + break; + } + } + if ($selected === null) { + $selected = reset($available); + } + + // must be a package providing/replacing and not a real platform package + if ($selected->getName() !== $packageName) { + /** @var Link $link */ + foreach (array_merge(array_values($selected->getProvides()), array_values($selected->getReplaces())) as $link) { + if ($link->getTarget() === $packageName) { + return $link->getPrettyConstraint().' '.substr($link->getDescription(), 0, -1).'d by '.$selected->getPrettyString(); + } + } + } + + $version = $selected->getPrettyVersion(); + $extra = $selected->getExtra(); + if ($selected instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { + $version .= '; ' . str_replace('Package ', '', $selected->getDescription()); } } else { return null; diff --git a/tests/Composer/Test/Fixtures/installer/platform-ext-solver-problems.test b/tests/Composer/Test/Fixtures/installer/platform-ext-solver-problems.test index aea9122b8..be8313f55 100644 --- a/tests/Composer/Test/Fixtures/installer/platform-ext-solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/platform-ext-solver-problems.test @@ -47,7 +47,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires a/a * -> satisfiable by a/a[1.0.0]. - - a/a 1.0.0 requires ext-filter 2.0.0 -> it has the wrong version (7.4.0; overridden via config.platform, actual: %s) installed. Install or enable PHP's filter extension. + - a/a 1.0.0 requires ext-filter 2.0.0 -> it has the wrong version installed (7.4.0; overridden via config.platform, actual: %s). To enable extensions, verify that they are enabled in your .ini files: __inilist__ diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 66d10f1ef..6b9c03630 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -119,7 +119,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 6 - Root composer.json requires linked library lib-icu 1001.* but it has the wrong version installed, try upgrading the intl extension. Problem 7 - - Root composer.json requires PHP extension ext-xml 1002.* but it has the wrong version (%s) installed. Install or enable PHP's xml extension. + - Root composer.json requires PHP extension ext-xml 1002.* but it has the wrong version installed (%s). Problem 8 - Root composer.json requires php 1 but your php version (%s) does not satisfy that requirement. Problem 9 @@ -161,4 +161,3 @@ Alternatively, you can run Composer with `--ignore-platform-req=ext-xml` to temp Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions. --EXPECT-- - diff --git a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirement-list-upper-bounds.test b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirement-list-upper-bounds.test index 3d9d8540a..0c7e776de 100644 --- a/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirement-list-upper-bounds.test +++ b/tests/Composer/Test/Fixtures/installer/update-ignore-platform-package-requirement-list-upper-bounds.test @@ -40,7 +40,7 @@ Your requirements could not be resolved to an installable set of packages. - Root composer.json requires PHP extension ext-foo-baz [== 9.0.0.0 || >= 9.0.0.0] but it is missing from your system. Install or enable PHP's foo-baz extension. Problem 2 - Root composer.json requires b/b 1.0.* -> satisfiable by b/b[1.0.1]. - - b/b 1.0.1 requires ext-foo-bar 10.* -> it has the wrong version (5.0.3 provided by __root__ 1.0.0+no-version-set) installed. Install or enable PHP's foo-bar extension. + - b/b 1.0.1 requires ext-foo-bar 10.* -> it has the wrong version installed (5.0.3 provided by __root__ 1.0.0+no-version-set). To enable extensions, verify that they are enabled in your .ini files: __inilist__