diff --git a/src/Composer/DependencyResolver/Decisions.php b/src/Composer/DependencyResolver/Decisions.php index 2fdf6da74..02997819e 100644 --- a/src/Composer/DependencyResolver/Decisions.php +++ b/src/Composer/DependencyResolver/Decisions.php @@ -197,16 +197,21 @@ class Decisions implements \Iterator, \Countable } } - public function __toString() + public function toString(Pool $pool = null) { $decisionMap = $this->decisionMap; ksort($decisionMap); $str = '['; foreach ($decisionMap as $packageId => $level) { - $str .= $packageId.':'.$level.','; + $str .= (($pool) ? $pool->literalToPackage($packageId) : $packageId).':'.$level.','; } $str .= ']'; return $str; } + + public function __toString() + { + return $this->toString(); + } } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index eb0f6e296..5571ec676 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -34,7 +34,7 @@ abstract class Rule const RULE_PACKAGE_SAME_NAME = 10; const RULE_LEARNED = 12; const RULE_PACKAGE_ALIAS = 13; - const RULE_PACKAGE_ROOT_ALIAS = 14; + const RULE_PACKAGE_INVERSE_ALIAS = 14; // bitfield defs const BITFIELD_TYPE = 0; @@ -311,22 +311,26 @@ abstract class Rule return 'Conclusion: '.$ruleText.$learnedString; case self::RULE_PACKAGE_ALIAS: - case self::RULE_PACKAGE_ROOT_ALIAS: - if ($this->getReason() === self::RULE_PACKAGE_ALIAS) { - $aliasPackage = $pool->literalToPackage($literals[0]); - $otherLiteral = 1; - } else { - // root alias rules work the other way around - $aliasPackage = $pool->literalToPackage($literals[1]); - $otherLiteral = 0; - } + $aliasPackage = $pool->literalToPackage($literals[0]); + // avoid returning content like "9999999-dev is an alias of dev-master" as it is useless if ($aliasPackage->getVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { return ''; } - $package = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[$otherLiteral])); + $package = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[1])); return $aliasPackage->getPrettyString() .' is an alias of '.$package->getPrettyString().' and thus requires it to be installed too.'; + case self::RULE_PACKAGE_INVERSE_ALIAS: + // inverse alias rules work the other way around than above + $aliasPackage = $pool->literalToPackage($literals[1]); + + // avoid returning content like "9999999-dev is an alias of dev-master" as it is useless + if ($aliasPackage->getVersion() === VersionParser::DEFAULT_BRANCH_ALIAS) { + return ''; + } + $package = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[0])); + + return $aliasPackage->getPrettyString() .' is an alias of '.$package->getPrettyString().' and must be installed with it.'; default: $ruleText = ''; foreach ($literals as $i => $literal) { diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index edd492b4e..ec8f9ed8c 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -164,10 +164,8 @@ class RuleSetGenerator $workQueue->enqueue($package->getAliasOf()); $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($package->getAliasOf()), Rule::RULE_PACKAGE_ALIAS, $package)); - // root aliases must be installed with their main package, so create a rule the other way around as well - if ($package->isRootPackageAlias()) { - $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package->getAliasOf(), array($package), Rule::RULE_PACKAGE_ROOT_ALIAS, $package->getAliasOf())); - } + // aliases must be installed with their main package, so create a rule the other way around as well + $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package->getAliasOf(), array($package), Rule::RULE_PACKAGE_INVERSE_ALIAS, $package->getAliasOf())); // if alias package has no self.version requires, its requirements do not // need to be added as the aliased package processing will take care of it @@ -197,6 +195,7 @@ class RuleSetGenerator /** @var PackageInterface $package */ foreach ($this->addedMap as $package) { foreach ($package->getConflicts() as $link) { + // even if conlict ends up being with an alias, there would be at least one actual package by this name if (!isset($this->addedPackagesByNames[$link->getTarget()])) { continue; } @@ -205,10 +204,14 @@ class RuleSetGenerator continue; } - /** @var PackageInterface $possibleConflict */ - foreach ($this->addedPackagesByNames[$link->getTarget()] as $possibleConflict) { - if ($this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint())) { - $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $possibleConflict, Rule::RULE_PACKAGE_CONFLICT, $link)); + $conflicts = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); + + foreach ($conflicts as $conflict) { + // define the conflict rule for regular packages, for alias packages it's only needed if the name + // matches the conflict exactly, otherwise the name match is by provide/replace which means the + // package which this is an alias of will conflict anyway, so no need to create additional rules + if (!$conflict instanceof AliasPackage || $conflict->getName() === $link->getTarget()) { + $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $conflict, Rule::RULE_PACKAGE_CONFLICT, $link)); } } } @@ -266,9 +269,13 @@ class RuleSetGenerator protected function addRulesForRootAliases($ignorePlatformReqs) { foreach ($this->pool->getPackages() as $package) { - // ensure that rules for root alias packages get loaded even if the root alias itself isn't required - // otherwise a package could be installed without its root alias which leads to unexpected behavior - if ($package instanceof AliasPackage && $package->isRootPackageAlias()) { + // ensure that rules for root alias packages and aliases of packages which were loaded are also loaded + // even if the alias itself isn't required, otherwise a package could be installed without its alias which + // leads to unexpected behavior + if (!isset($this->addedMap[$package->id]) && + $package instanceof AliasPackage && + ($package->isRootPackageAlias() || isset($this->addedMap[$package->getAliasOf()->id])) + ) { $this->addRulesForPackage($package, $ignorePlatformReqs); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 25992e58b..cf916fb6b 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -840,6 +840,7 @@ class SolverTest extends TestCase array('job' => 'install', 'package' => $packageB), array('job' => 'markAliasInstalled', 'package' => $packageBAlias), array('job' => 'install', 'package' => $packageC), + array('job' => 'markAliasInstalled', 'package' => $packageCAlias), )); } diff --git a/tests/Composer/Test/Fixtures/installer/conflict-on-root-with-alias-prevents-update-if-not-required.test b/tests/Composer/Test/Fixtures/installer/conflict-on-root-with-alias-prevents-update-if-not-required.test new file mode 100644 index 000000000..3a457b22c --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-on-root-with-alias-prevents-update-if-not-required.test @@ -0,0 +1,38 @@ +--TEST-- +Test that a root package conflict with a branch alias leads to an error, even if the branch alias isn't required. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "some/dep", "version": "1.0.0" }, + { "name": "some/dep", "version": "1.1.0" }, + { "name": "some/dep", "version": "1.2.0" }, + { "name": "some/dep", "version": "dev-main", "extra": {"branch-alias": {"dev-main": "1.3.x-dev"} } }, + { "name": "some/dep", "version": "1.2.x-dev" } + ] + } + ], + "require": { + "some/dep": "dev-main" + }, + "conflict": { + "some/dep": ">=1.3" + } +} +--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__ is present at version 1.0.0+no-version-set and cannot be modified by Composer + - some/dep 1.3.x-dev is an alias of some/dep dev-main and must be installed with it. + - __root__ 1.0.0+no-version-set conflicts with some/dep 1.3.x-dev. + - Root composer.json requires some/dep dev-main -> satisfiable by some/dep[dev-main]. +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/conflict-with-alias-in-lock-does-prevents-install.test b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-in-lock-does-prevents-install.test new file mode 100644 index 000000000..c19ca396c --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-in-lock-does-prevents-install.test @@ -0,0 +1,54 @@ +--TEST-- +Test that conflict with a branch alias in the lock file leads to an error on install from lock, even if the branch alias was removed on the remote end. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "some/dep", "version": "1.0.0" }, + { "name": "some/dep", "version": "1.1.0" }, + { "name": "some/dep", "version": "1.2.0" }, + { "name": "some/dep", "version": "dev-main" }, + { "name": "some/dep", "version": "1.2.x-dev" }, + { "name": "conflictor/foo", "version": "1.0.0", "conflict": { "some/dep": ">=1.3" } } + ] + } + ], + "require": { + "some/dep": "dev-main", + "conflictor/foo": "1.0.0" + } +} +--LOCK-- +{ + "packages": [ + { "name": "conflictor/foo", "version": "1.0.0", "conflict": { "some/dep": ">=1.3" }, "type": "library" }, + { "name": "some/dep", "version": "dev-main", "extra": {"branch-alias": {"dev-main": "1.3.x-dev"} }, "type": "library" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "some/dep": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +install +--EXPECT-EXIT-CODE-- +2 +--EXPECT-OUTPUT-- +Installing dependencies from lock file (including require-dev) +Verifying lock file contents can be installed on current platform. +Your lock file does not contain a compatible set of packages. Please run composer update. + + Problem 1 + - conflictor/foo is locked to version 1.0.0 and an update of this package was not requested. + - conflictor/foo 1.0.0 conflicts with some/dep 1.3.x-dev. + - some/dep is locked to version 1.3.x-dev and an update of this package was not requested. + +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update-if-not-required.test b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update-if-not-required.test new file mode 100644 index 000000000..78cfd0362 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update-if-not-required.test @@ -0,0 +1,38 @@ +--TEST-- +Test that conflict of a dependency with a branch alias of another dependency is not ignored, even if the alias is not required for installation. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "some/dep", "version": "1.0.0" }, + { "name": "some/dep", "version": "1.1.0" }, + { "name": "some/dep", "version": "1.2.0" }, + { "name": "some/dep", "version": "dev-main", "extra": {"branch-alias": {"dev-main": "1.3.x-dev"} } }, + { "name": "some/dep", "version": "1.2.x-dev" }, + { "name": "conflictor/foo", "version": "1.0.0", "conflict": { "some/dep": ">=1.3" } } + ] + } + ], + "require": { + "some/dep": "dev-main", + "conflictor/foo": "1.0.0" + } +} +--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 some/dep dev-main -> satisfiable by some/dep[dev-main]. + - conflictor/foo 1.0.0 conflicts with some/dep 1.3.x-dev. + - some/dep 1.3.x-dev is an alias of some/dep dev-main and must be installed with it. + - Root composer.json requires conflictor/foo 1.0.0 -> satisfiable by conflictor/foo[1.0.0]. + +--EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update.test b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update.test new file mode 100644 index 000000000..74b61304e --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-with-alias-prevents-update.test @@ -0,0 +1,43 @@ +--TEST-- +Test that conflict on a branch alias is respected +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "some/dep", "version": "1.0.0" }, + { "name": "some/dep", "version": "1.1.0" }, + { "name": "some/dep", "version": "1.2.0" }, + { "name": "some/dep", "version": "dev-main", "extra": {"branch-alias": {"dev-main": "1.3.x-dev"} } }, + { "name": "some/dep", "version": "1.2.x-dev" } + ] + } + ], + "require": { + "some/dep": "^1.0@dev" + }, + "conflict": { + "some/dep": ">=1.3" + } +} +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "some/dep", "version": "1.2.x-dev", "type": "library" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "some/dep": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Installing some/dep (1.2.x-dev) diff --git a/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-default-branch-with-branch-alias.test similarity index 71% rename from tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test rename to tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-default-branch-with-branch-alias.test index 900028457..ca891bbc1 100644 --- a/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-dev-master.test +++ b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-does-not-match-default-branch-with-branch-alias.test @@ -1,5 +1,5 @@ --TEST-- -Test that a conflict against >=5 does not include dev-master or other dev-x +Test that a conflict against >=5 does not include the default branch if it has a branch alias defined. --COMPOSER-- { "repositories": [ @@ -7,7 +7,7 @@ Test that a conflict against >=5 does not include dev-master or other dev-x "type": "package", "package": [ { "name": "conflicter/pkg", "version": "1.0.0", "conflict": { "victim/pkg": ">=5", "victim/pkg2": ">=5" } }, - { "name": "victim/pkg", "version": "dev-master", "default-branch": true }, + { "name": "victim/pkg", "version": "dev-master", "default-branch": true, "extra": { "branch-alias": { "dev-master": "2.x-dev" } } }, { "name": "victim/pkg2", "version": "dev-foo" } ] } @@ -27,5 +27,5 @@ update --EXPECT-- Installing conflicter/pkg (1.0.0) Installing victim/pkg (dev-master) -Marking victim/pkg (9999999-dev) as installed, alias of victim/pkg (dev-master) +Marking victim/pkg (2.x-dev) as installed, alias of victim/pkg (dev-master) Installing victim/pkg2 (dev-foo) diff --git a/tests/Composer/Test/Fixtures/installer/unbounded-conflict-matches-default-branch.test b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-matches-default-branch.test new file mode 100644 index 000000000..d4a3b4d85 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/unbounded-conflict-matches-default-branch.test @@ -0,0 +1,39 @@ +--TEST-- +Test that a conflict against >=5 includes the default branch if it has no branch alias defined (and then uses the default 9999999-dev alias). +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "conflicter/pkg", "version": "1.0.0", "conflict": { "victim/pkg": ">=5", "victim/pkg2": ">=5" } }, + { "name": "victim/pkg", "version": "dev-master", "default-branch": true }, + { "name": "victim/pkg2", "version": "dev-foo" } + ] + } + ], + "require": { + "conflicter/pkg": "1.0.0", + "victim/pkg": "*", + "victim/pkg2": "*" + }, + "minimum-stability": "dev" +} + + +--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 + - conflicter/pkg 1.0.0 conflicts with victim/pkg dev-master. + - Root composer.json requires conflicter/pkg 1.0.0 -> satisfiable by conflicter/pkg[1.0.0]. + - Root composer.json requires victim/pkg * -> satisfiable by victim/pkg[dev-master]. + +--EXPECT--