diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 3b0255304..7c0d19834 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -133,6 +133,10 @@ class LockTransaction extends Transaction } } + usort($usedAliases, function ($a, $b) { + return strcmp($a['package'], $b['package']); + }); + return $usedAliases; } } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 511d2b427..195864599 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -55,6 +55,11 @@ class Pool implements \Countable } } + public function getPackages() + { + return $this->packages; + } + /** * Retrieves the package object for a given package id. * diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index a7df98d71..7e1311a76 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -35,6 +35,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; // bitfield defs const BITFIELD_TYPE = 0; @@ -312,12 +313,20 @@ abstract class Rule return 'Conclusion: '.$ruleText.$learnedString; case self::RULE_PACKAGE_ALIAS: - $aliasPackage = $pool->literalToPackage($literals[0]); + 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; + } // 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[1])); + $package = $this->deduplicateDefaultBranchAlias($pool->literalToPackage($literals[$otherLiteral])); return $aliasPackage->getPrettyString() .' is an alias of '.$package->getPrettyString().' and thus requires it to be installed too.'; default: diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index ba3264857..3744bdd2a 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -28,7 +28,6 @@ class RuleSetGenerator protected $rules; protected $addedMap; protected $conflictAddedMap; - protected $addedPackages; protected $addedPackagesByNames; protected $conflictsForName; @@ -157,9 +156,8 @@ class RuleSetGenerator continue; } - $this->addedMap[$package->id] = true; + $this->addedMap[$package->id] = $package; - $this->addedPackages[] = $package; if (!$package instanceof AliasPackage) { foreach ($package->getNames(false) as $name) { $this->addedPackagesByNames[$name][] = $package; @@ -168,6 +166,11 @@ 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())); + } + // 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 if (!$package->hasSelfVersionRequires()) { @@ -194,7 +197,7 @@ class RuleSetGenerator protected function addConflictRules($ignorePlatformReqs = false) { /** @var PackageInterface $package */ - foreach ($this->addedPackages as $package) { + foreach ($this->addedMap as $package) { foreach ($package->getConflicts() as $link) { if (!isset($this->addedPackagesByNames[$link->getTarget()])) { continue; @@ -231,7 +234,7 @@ class RuleSetGenerator } // otherwise, looks like a bug - throw new \LogicException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool."); + throw new \LogicException("Fixed package ".$package->getPrettyString()." was not added to solver pool."); } $this->addRulesForPackage($package, $ignorePlatformReqs); @@ -262,6 +265,17 @@ 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()) { + $this->addRulesForPackage($package, $ignorePlatformReqs); + } + } + } + /** * @param bool|array $ignorePlatformReqs */ @@ -271,16 +285,17 @@ class RuleSetGenerator $this->addedMap = array(); $this->conflictAddedMap = array(); - $this->addedPackages = array(); $this->addedPackagesByNames = array(); $this->conflictsForName = array(); $this->addRulesForRequest($request, $ignorePlatformReqs); + $this->addRulesForRootAliases($ignorePlatformReqs); + $this->addConflictRules($ignorePlatformReqs); // Remove references to packages - $this->addedPackages = $this->addedPackagesByNames = null; + $this->addedMap = $this->addedPackagesByNames = null; return $this->rules; } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 9845c62da..303e295cb 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -789,7 +789,7 @@ class SolverTest extends TestCase $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), array('job' => 'install', 'package' => $packageA2), - array('job' => 'install', 'package' => $packageA2Alias), + array('job' => 'markAliasInstalled', 'package' => $packageA2Alias), )); } @@ -811,11 +811,40 @@ class SolverTest extends TestCase $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageA), - array('job' => 'install', 'package' => $packageAAlias), + array('job' => 'markAliasInstalled', 'package' => $packageAAlias), array('job' => 'install', 'package' => $packageB), )); } + public function testInstallRootAliasesIfAliasOfIsInstalled() + { + // root aliased, required + $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repo->addPackage($packageAAlias = $this->getAliasPackage($packageA, '1.1')); + $packageAAlias->setRootPackageAlias(true); + // root aliased, not required, should still be installed as it is root alias + $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repo->addPackage($packageBAlias = $this->getAliasPackage($packageB, '1.1')); + $packageBAlias->setRootPackageAlias(true); + // regular alias, not required, alias should not be installed + $this->repo->addPackage($packageC = $this->getPackage('C', '1.0')); + $this->repo->addPackage($packageCAlias = $this->getAliasPackage($packageC, '1.1')); + + $this->reposComplete(); + + $this->request->requireName('A', $this->getVersionConstraint('==', '1.1')); + $this->request->requireName('B', $this->getVersionConstraint('==', '1.0')); + $this->request->requireName('C', $this->getVersionConstraint('==', '1.0')); + + $this->checkSolverResult(array( + array('job' => 'install', 'package' => $packageA), + array('job' => 'markAliasInstalled', 'package' => $packageAAlias), + array('job' => 'install', 'package' => $packageB), + array('job' => 'markAliasInstalled', 'package' => $packageBAlias), + array('job' => 'install', 'package' => $packageC), + )); + } + /** * Tests for a bug introduced in commit 451bab1c2cd58e05af6e21639b829408ad023463 Solver.php line 554/523 * @@ -915,6 +944,11 @@ class SolverTest extends TestCase 'from' => $operation->getInitialPackage(), 'to' => $operation->getTargetPackage(), ); + } elseif (in_array($operation->getOperationType(), array('markAliasInstalled', 'markAliasUninstalled'))) { + $result[] = array( + 'job' => $operation->getOperationType(), + 'package' => $operation->getPackage(), + ); } else { $job = ('uninstall' === $operation->getOperationType() ? 'remove' : 'install'); $result[] = array( @@ -924,6 +958,16 @@ class SolverTest extends TestCase } } + $expectedReadable = array(); + foreach ($expected as $op) { + $expectedReadable[] = array_map('strval', $op); + } + $resultReadable = array(); + foreach ($result as $op) { + $resultReadable[] = array_map('strval', $op); + } + + $this->assertEquals($expectedReadable, $resultReadable); $this->assertEquals($expected, $result); } } diff --git a/tests/Composer/Test/Fixtures/installer/alias-in-lock.test b/tests/Composer/Test/Fixtures/installer/alias-in-lock.test index 25660566f..49cd1519f 100644 --- a/tests/Composer/Test/Fixtures/installer/alias-in-lock.test +++ b/tests/Composer/Test/Fixtures/installer/alias-in-lock.test @@ -1,5 +1,5 @@ --TEST-- -Root-defined aliases end up in lock file only if required to solve deps +Root-defined aliases end up in lock file always on full update --COMPOSER-- { "repositories": [ @@ -50,6 +50,11 @@ update "version": "3.0.2.0", "alias": "3.0.3", "alias_normalized": "3.0.3.0" + },{ + "package": "a/aliased2", + "version": "3.0.2.0", + "alias": "3.0.3", + "alias_normalized": "3.0.3.0" }], "minimum-stability": "stable", "stability-flags": [], @@ -60,6 +65,7 @@ update } --EXPECT-- Installing a/aliased2 (3.0.2) +Marking a/aliased2 (3.0.3) as installed, alias of a/aliased2 (3.0.2) Installing a/aliased (3.0.2) Marking a/aliased (3.0.3) as installed, alias of a/aliased (3.0.2) Installing b/requirer (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/alias-in-lock2.test b/tests/Composer/Test/Fixtures/installer/alias-in-lock2.test new file mode 100644 index 000000000..c781ae32f --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/alias-in-lock2.test @@ -0,0 +1,75 @@ +--TEST-- +Newly defined root aliases end up in lock file only if the package is updated +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/aliased", "version": "3.0.2" + }, + { + "name": "a/aliased2", "version": "3.0.2" + } + ] + } + ], + "require": { + "a/aliased": "3.0.2 as 3.0.3", + "a/aliased2": "3.0.2 as 3.0.3" + } +} +--LOCK-- +{ + "packages": [ + { + "name": "a/aliased", "version": "3.0.2", + "type": "library" + }, + { + "name": "a/aliased2", "version": "3.0.2", + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update a/aliased +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/aliased", "version": "3.0.2", + "type": "library" + }, + { + "name": "a/aliased2", "version": "3.0.2", + "type": "library" + } + ], + "packages-dev": [], + "aliases": [{ + "package": "a/aliased", + "version": "3.0.2.0", + "alias": "3.0.3", + "alias_normalized": "3.0.3.0" + }], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Installing a/aliased (3.0.2) +Marking a/aliased (3.0.3) as installed, alias of a/aliased (3.0.2) +Installing a/aliased2 (3.0.2) diff --git a/tests/Composer/Test/Fixtures/installer/alias-on-unloadable-package.test b/tests/Composer/Test/Fixtures/installer/alias-on-unloadable-package.test new file mode 100644 index 000000000..58cd7d818 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/alias-on-unloadable-package.test @@ -0,0 +1,30 @@ +--TEST-- +A root alias for a package which cannot be found in an acceptable version does not lead to different error. +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/aliased", "version": "1.2.3" } + ] + } + ], + "require": { + "a/aliased": "3.0.2 as 3.0.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 composer.json requires a/aliased 3.0.2 as 3.0.3, found a/aliased[1.2.3] but it does not match the constraint. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test b/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test index 28229eb87..1500d156b 100644 --- a/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test +++ b/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test @@ -61,12 +61,12 @@ update } ], "aliases": [{ - "package": "a/aliased2", + "package": "a/aliased", "version": "dev-next", "alias": "4.1.0-RC2", "alias_normalized": "4.1.0.0-RC2" }, { - "package": "a/aliased", + "package": "a/aliased2", "version": "dev-next", "alias": "4.1.0-RC2", "alias_normalized": "4.1.0.0-RC2" diff --git a/tests/Composer/Test/Fixtures/installer/root-alias-gets-loaded-for-locked-pkgs.test b/tests/Composer/Test/Fixtures/installer/root-alias-gets-loaded-for-locked-pkgs.test new file mode 100644 index 000000000..172288f75 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/root-alias-gets-loaded-for-locked-pkgs.test @@ -0,0 +1,54 @@ +--TEST-- +Newly defined root alias does not get loaded if package is loaded from lock file +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "some/dep", "version": "dev-main" }, + { "name": "foo/pkg", "version": "1.0.0", "require": {"some/dep": "^1"} } + ] + } + ], + "require": { + "some/dep": "dev-main as 1.0.0", + "foo/pkg": "^1.0" + } +} +--LOCK-- +{ + "packages": [ + { "name": "some/dep", "version": "dev-main" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--INSTALLED-- +[ + { "name": "some/dep", "version": "dev-main" } +] +--RUN-- +update foo/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 + - Root composer.json requires foo/pkg ^1.0 -> satisfiable by foo/pkg[1.0.0]. + - foo/pkg 1.0.0 requires some/dep ^1 -> found some/dep[dev-main] but it does not match the constraint. + +Use the option --with-all-dependencies (-W) to allow upgrades, downgrades and removals for packages currently locked to specific versions. + +--EXPECT--