From e30a6b0b9b1e529499f278fa22fbbac351c150f2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Sun, 28 Nov 2021 13:29:54 +0100 Subject: [PATCH] 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",