diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 305ad16b6..420e75786 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -268,8 +268,8 @@ class PoolBuilder } } - // if we're doing a partial update with deps and we're not loading an initial fixed package - // we also need to trigger an update for transitive deps which are being replaced + // if we're doing a partial update with deps we also need to unfix packages which are being replaced in case they + // are currently locked and thus prevent this updateable package from being installable/updateable if ($propagateUpdate && $request->getUpdateAllowTransitiveDependencies()) { foreach ($package->getReplaces() as $link) { $replace = $link->getTarget(); diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index a26cc47de..13d0ffd4f 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -92,7 +92,6 @@ class Problem } $messages = array(); - foreach ($reasons as $rule) { $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); } @@ -100,6 +99,17 @@ class Problem return "\n - ".implode("\n - ", $messages); } + public function isCausedByLock() + { + foreach ($this->reasons as $sectionRules) { + foreach ($sectionRules as $rule) { + if ($rule->isCausedByLock()) { + return true; + } + } + } + } + /** * Store a reason descriptor but ignore duplicates * diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 58f6962cd..a0824b88a 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -123,6 +123,11 @@ abstract class Rule abstract public function isAssertion(); + public function isCausedByLock() + { + return $this->getReason() === self::RULE_FIXED && $this->reasonData['lockable']; + } + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) { $literals = $this->getLiterals(); diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index a47bd1c8c..cfaa110c6 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -36,12 +36,15 @@ class SolverProblemsException extends \RuntimeException $installedMap = $request->getPresentMap(true); $text = "\n"; $hasExtensionProblems = false; + $isCausedByLock = false; foreach ($this->problems as $i => $problem) { $text .= " Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n"; if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { $hasExtensionProblems = true; } + + $isCausedByLock |= $problem->isCausedByLock(); } if (!$isDevExtraction && (strpos($text, 'could not be found') || strpos($text, 'no matching package found'))) { @@ -52,6 +55,10 @@ class SolverProblemsException extends \RuntimeException $text .= $this->createExtensionHint(); } + if ($isCausedByLock && !$isDevExtraction) { + $text .= "\nUse the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions."; + } + return $text; } diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test new file mode 100644 index 000000000..45a9adb21 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-require-new-replace.test @@ -0,0 +1,55 @@ +--TEST-- +Require a new package in the composer.json and updating with its name as an argument and with-dependencies should remove packages it replaces which are not root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" }, + { "name": "new/pkg", "version": "1.0.0", "replace": { "current/dep": "1.0.0" } } + ] + } + ], + "require": { + "current/pkg": "1.*", + "new/pkg": "1.*" + } +} +--INSTALLED-- +[ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } +] +--LOCK-- +{ + "packages": [ + { "name": "current/pkg", "version": "1.0.0", "require": { "current/dep": "*" } }, + { "name": "current/dep", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update new/pkg +--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 + - current/dep is locked to version 1.0.0 and an update of this package was not requested. + - new/pkg 1.0.0 can not be installed as that would require removing current/dep 1.0.0. new/pkg replaces current/dep and can thus not coexist with it. + - Root composer.json requires new/pkg 1.* -> satisfiable by new/pkg[1.0.0]. + +Use the option --with-all-dependencies to allow updates and removals for packages currently locked to specific versions. +--EXPECT--