From 1211ba1d5177cf17a635fb8eb0f16db13fdf31a4 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Mon, 12 Nov 2018 19:45:10 +0100 Subject: [PATCH 01/37] BC break: Remove workaround for loading lock files without dev requires --- src/Composer/Installer.php | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 19c0015d6..10f115daf 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -350,16 +350,7 @@ class Installer // and a lock file is present as we need to force install non-whitelisted lock file // packages in that case if (!$this->update || (!empty($this->updateWhitelist) && $this->locker->isLocked())) { - try { - $lockedRepository = $this->locker->getLockedRepository($this->devMode); - } catch (\RuntimeException $e) { - // if there are dev requires, then we really can not install - if ($this->package->getDevRequires()) { - throw $e; - } - // no require-dev in composer.json and the lock file was created with no dev info, so skip them - $lockedRepository = $this->locker->getLockedRepository(); - } + $lockedRepository = $this->locker->getLockedRepository($this->devMode); } $this->whitelistUpdateDependencies( From 10ada7bf82d317bb18813e62b7b103a0f5a86880 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 13 Sep 2018 15:23:05 +0200 Subject: [PATCH 02/37] Refactor Installer class into separate install and update processes - Introduce separate Lock and LocalRepo transactions, one for changes to the lock file, one for changes to locally installed packages based on lock file - Remove various hacks to keep dev dependencies updated and incorporated the functionality into the transaction classes - Remove installed repo, there are now local repo, locked repo and platform repo - Remove access to local repo from solver, only supply locked packages - Update can now be run to modify the lock file but not install packages to local repo --- .../DependencyResolver/DefaultPolicy.php | 54 +- .../LocalRepoTransaction.php | 185 ++++ .../DependencyResolver/LockTransaction.php | 167 +++ .../DependencyResolver/PolicyInterface.php | 4 +- src/Composer/DependencyResolver/Pool.php | 7 - .../DependencyResolver/PoolBuilder.php | 6 +- src/Composer/DependencyResolver/Problem.php | 2 +- src/Composer/DependencyResolver/Request.php | 80 +- src/Composer/DependencyResolver/Rule.php | 2 +- .../DependencyResolver/RuleSetGenerator.php | 37 +- src/Composer/DependencyResolver/Solver.php | 87 +- .../DependencyResolver/Transaction.php | 244 ----- .../EventDispatcher/EventDispatcher.php | 5 +- src/Composer/Installer.php | 992 ++++++------------ .../Installer/SuggestedPackagesReporter.php | 14 +- src/Composer/Package/Locker.php | 23 +- .../DependencyResolver/DefaultPolicyTest.php | 54 +- .../Test/DependencyResolver/RequestTest.php | 20 +- .../Test/DependencyResolver/SolverTest.php | 5 +- .../installer/update-changes-url.test | 49 + tests/Composer/Test/InstallerTest.php | 64 +- 21 files changed, 945 insertions(+), 1156 deletions(-) create mode 100644 src/Composer/DependencyResolver/LocalRepoTransaction.php create mode 100644 src/Composer/DependencyResolver/LockTransaction.php delete mode 100644 src/Composer/DependencyResolver/Transaction.php diff --git a/src/Composer/DependencyResolver/DefaultPolicy.php b/src/Composer/DependencyResolver/DefaultPolicy.php index 051bc7449..7d241b482 100644 --- a/src/Composer/DependencyResolver/DefaultPolicy.php +++ b/src/Composer/DependencyResolver/DefaultPolicy.php @@ -44,7 +44,7 @@ class DefaultPolicy implements PolicyInterface return $constraint->matchSpecific($version, true); } - public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package, $mustMatchName = false) + public function findUpdatePackages(Pool $pool, PackageInterface $package, $mustMatchName = false) { $packages = array(); @@ -57,36 +57,34 @@ class DefaultPolicy implements PolicyInterface return $packages; } - public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null) + public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null) { - $packages = $this->groupLiteralsByNamePreferInstalled($pool, $installedMap, $literals); + $packages = $this->groupLiteralsByName($pool, $literals); - foreach ($packages as &$literals) { + foreach ($packages as &$nameLiterals) { $policy = $this; - usort($literals, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true); + usort($nameLiterals, function ($a, $b) use ($policy, $pool, $requiredPackage) { + return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage, true); }); } - foreach ($packages as &$literals) { - $literals = $this->pruneToHighestPriorityOrInstalled($pool, $installedMap, $literals); - - $literals = $this->pruneToBestVersion($pool, $literals); - - $literals = $this->pruneRemoteAliases($pool, $literals); + foreach ($packages as &$sortedLiterals) { + $sortedLiterals = $this->pruneToHighestPriority($pool, $sortedLiterals); + $sortedLiterals = $this->pruneToBestVersion($pool, $sortedLiterals); + $sortedLiterals = $this->pruneRemoteAliases($pool, $sortedLiterals); } $selected = call_user_func_array('array_merge', $packages); // now sort the result across all packages to respect replaces across packages - usort($selected, function ($a, $b) use ($policy, $pool, $installedMap, $requiredPackage) { - return $policy->compareByPriorityPreferInstalled($pool, $installedMap, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage); + usort($selected, function ($a, $b) use ($policy, $pool, $requiredPackage) { + return $policy->compareByPriority($pool, $pool->literalToPackage($a), $pool->literalToPackage($b), $requiredPackage); }); return $selected; } - protected function groupLiteralsByNamePreferInstalled(Pool $pool, array $installedMap, $literals) + protected function groupLiteralsByName(Pool $pool, $literals) { $packages = array(); foreach ($literals as $literal) { @@ -95,12 +93,7 @@ class DefaultPolicy implements PolicyInterface if (!isset($packages[$packageName])) { $packages[$packageName] = array(); } - - if (isset($installedMap[abs($literal)])) { - array_unshift($packages[$packageName], $literal); - } else { - $packages[$packageName][] = $literal; - } + $packages[$packageName][] = $literal; } return $packages; @@ -109,7 +102,7 @@ class DefaultPolicy implements PolicyInterface /** * @protected */ - public function compareByPriorityPreferInstalled(Pool $pool, array $installedMap, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false) + public function compareByPriority(Pool $pool, PackageInterface $a, PackageInterface $b, $requiredPackage = null, $ignoreReplace = false) { if ($a->getRepository() === $b->getRepository()) { // prefer aliases to the original package @@ -155,14 +148,6 @@ class DefaultPolicy implements PolicyInterface return ($a->id < $b->id) ? -1 : 1; } - if (isset($installedMap[$a->id])) { - return -1; - } - - if (isset($installedMap[$b->id])) { - return 1; - } - return ($pool->getPriority($a->id) > $pool->getPriority($b->id)) ? -1 : 1; } @@ -214,9 +199,9 @@ class DefaultPolicy implements PolicyInterface } /** - * Assumes that installed packages come first and then all highest priority packages + * Assumes that highest priority packages come first */ - protected function pruneToHighestPriorityOrInstalled(Pool $pool, array $installedMap, array $literals) + protected function pruneToHighestPriority(Pool $pool, array $literals) { $selected = array(); @@ -225,11 +210,6 @@ class DefaultPolicy implements PolicyInterface foreach ($literals as $literal) { $package = $pool->literalToPackage($literal); - if (isset($installedMap[$package->id])) { - $selected[] = $literal; - continue; - } - if (null === $priority) { $priority = $pool->getPriority($package->id); } diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php new file mode 100644 index 000000000..98c0b05d7 --- /dev/null +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -0,0 +1,185 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\Package\AliasPackage; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryInterface; + +/** + * @author Nils Adermann + */ +class LocalRepoTransaction +{ + /** @var RepositoryInterface */ + protected $lockedRepository; + + /** @var RepositoryInterface */ + protected $localRepository; + + public function __construct($lockedRepository, $localRepository) + { + $this->lockedRepository = $lockedRepository; + $this->localRepository = $localRepository; + + $this->operations = $this->calculateOperations(); + } + + public function getOperations() + { + return $this->operations; + } + + protected function calculateOperations() + { + $operations = array(); + + $localPackageMap = array(); + $removeMap = array(); + foreach ($this->localRepository->getPackages() as $package) { + if (isset($localPackageMap[$package->getName()])) { + die("Alias?"); + } + $localPackageMap[$package->getName()] = $package; + $removeMap[$package->getName()] = $package; + } + + $lockedPackages = array(); + foreach ($this->lockedRepository->getPackages() as $package) { + if (isset($localPackageMap[$package->getName()])) { + $source = $localPackageMap[$package->getName()]; + + // do we need to update? + if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } else { + // TODO do we need to update metadata? force update based on reference? + } + } else { + $operations[] = new Operation\InstallOperation($package); + unset($removeMap[$package->getName()]); + } + + +/* + if (isset($lockedPackages[$package->getName()])) { + die("Alias?"); + } + $lockedPackages[$package->getName()] = $package;*/ + } + + foreach ($removeMap as $name => $package) { + $operations[] = new Operation\UninstallOperation($package, null); + } + + $operations = $this->movePluginsToFront($operations); + $operations = $this->moveUninstallsToFront($operations); + + + // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? + /* + if ('update' === $jobType) { + $targetPackage = $operation->getTargetPackage(); + if ($targetPackage->isDev()) { + $initialPackage = $operation->getInitialPackage(); + if ($targetPackage->getVersion() === $initialPackage->getVersion() + && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) + && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) + ) { + $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); + $this->io->writeError('', true, IOInterface::DEBUG); + + continue; + } + } + }*/ + + return $operations; + } + + /** + * Workaround: if your packages depend on plugins, we must be sure + * that those are installed / updated first; else it would lead to packages + * being installed multiple times in different folders, when running Composer + * twice. + * + * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, + * it at least fixes the symptoms and makes usage of composer possible (again) + * in such scenarios. + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function movePluginsToFront(array $operations) + { + $pluginsNoDeps = array(); + $pluginsWithDeps = array(); + $pluginRequires = array(); + + foreach (array_reverse($operations, true) as $idx => $op) { + if ($op instanceof Operation\InstallOperation) { + $package = $op->getPackage(); + } elseif ($op instanceof Operation\UpdateOperation) { + $package = $op->getTargetPackage(); + } else { + continue; + } + + // is this package a plugin? + $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; + + // is this a plugin or a dependency of a plugin? + if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { + // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' + $requires = array_filter(array_keys($package->getRequires()), function ($req) { + return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + }); + + // is this a plugin with no meaningful dependencies? + if ($isPlugin && !count($requires)) { + // plugins with no dependencies go to the very front + array_unshift($pluginsNoDeps, $op); + } else { + // capture the requirements for this package so those packages will be moved up as well + $pluginRequires = array_merge($pluginRequires, $requires); + // move the operation to the front + array_unshift($pluginsWithDeps, $op); + } + + unset($operations[$idx]); + } + } + + return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); + } + + /** + * Removals of packages should be executed before installations in + * case two packages resolve to the same path (due to custom installers) + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function moveUninstallsToFront(array $operations) + { + $uninstOps = array(); + foreach ($operations as $idx => $op) { + if ($op instanceof UninstallOperation) { + $uninstOps[] = $op; + unset($operations[$idx]); + } + } + + return array_merge($uninstOps, $operations); + } +} diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php new file mode 100644 index 000000000..35cf760b9 --- /dev/null +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -0,0 +1,167 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\Package\AliasPackage; +use Composer\Package\RootAliasPackage; +use Composer\Package\RootPackageInterface; +use Composer\Repository\RepositoryInterface; + +/** + * @author Nils Adermann + */ +class LockTransaction +{ + protected $policy; + /** @var Pool */ + protected $pool; + + /** + * packages in current lock file, platform repo or otherwise present + * @var array + */ + protected $presentMap; + + /** + * Packages which cannot be mapped, platform repo, root package, other fixed repos + * @var array + */ + protected $unlockableMap; + + protected $decisions; + protected $transaction; + + public function __construct($policy, $pool, $presentMap, $unlockableMap, $decisions) + { + $this->policy = $policy; + $this->pool = $pool; + $this->presentMap = $presentMap; + $this->unlockableMap = $unlockableMap; + $this->decisions = $decisions; + + $this->operations = $this->calculateOperations(); + } + + /** + * @return OperationInterface[] + */ + public function getOperations() + { + return $this->operations; + } + + protected function calculateOperations() + { + $operations = array(); + $lockMeansUpdateMap = $this->findPotentialUpdates(); + + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + $reason = $decision[Decisions::DECISION_REASON]; + + $package = $this->pool->literalToPackage($literal); + + // wanted & !present + if ($literal > 0 && !isset($this->presentMap[$package->id])) { + if (isset($lockMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { + $operations[] = new Operation\UpdateOperation($lockMeansUpdateMap[abs($literal)], $package, $reason); + + // avoid updates to one package from multiple origins + unset($lockMeansUpdateMap[abs($literal)]); + $ignoreRemove[$source->id] = true; + } else { + if ($package instanceof AliasPackage) { + $operations[] = new Operation\MarkAliasInstalledOperation($package, $reason); + } else { + $operations[] = new Operation\InstallOperation($package, $reason); + } + } + } + } + + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + $reason = $decision[Decisions::DECISION_REASON]; + $package = $this->pool->literalToPackage($literal); + + if ($literal <= 0 && isset($this->presentMap[$package->id]) && !isset($ignoreRemove[$package->id])) { + if ($package instanceof AliasPackage) { + $operations[] = new Operation\MarkAliasUninstalledOperation($package, $reason); + } else { + $operations[] = new Operation\UninstallOperation($package, $reason); + } + } + } + + return $operations; + } + + // TODO additionalFixedRepository needs to be looked at here as well? + public function getNewLockNonDevPackages() + { + $packages = array(); + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + + if ($literal > 0) { + $package = $this->pool->literalToPackage($literal); + if (!isset($this->unlockableMap[$package->id]) && !($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { + $packages[] = $package; + } + } + } + + return $packages; + } + + public function getNewLockDevPackages() + { + $packages = array(); + return $packages; + } + + protected function findPotentialUpdates() + { + $lockMeansUpdateMap = array(); + + foreach ($this->decisions as $i => $decision) { + $literal = $decision[Decisions::DECISION_LITERAL]; + $package = $this->pool->literalToPackage($literal); + + if ($package instanceof AliasPackage) { + continue; + } + + // !wanted & present + if ($literal <= 0 && isset($this->presentMap[$package->id])) { + // TODO can't we just look at existing rules? + $updates = $this->policy->findUpdatePackages($this->pool, $package); + + $literals = array($package->id); + + foreach ($updates as $update) { + $literals[] = $update->id; + } + + foreach ($literals as $updateLiteral) { + if ($updateLiteral !== $literal && !isset($lockMeansUpdateMap[$updateLiteral])) { + $lockMeansUpdateMap[$updateLiteral] = $package; + } + } + } + } + + return $lockMeansUpdateMap; + } +} diff --git a/src/Composer/DependencyResolver/PolicyInterface.php b/src/Composer/DependencyResolver/PolicyInterface.php index 3464bd594..d4db7f3a9 100644 --- a/src/Composer/DependencyResolver/PolicyInterface.php +++ b/src/Composer/DependencyResolver/PolicyInterface.php @@ -21,7 +21,7 @@ interface PolicyInterface { public function versionCompare(PackageInterface $a, PackageInterface $b, $operator); - public function findUpdatePackages(Pool $pool, array $installedMap, PackageInterface $package); + public function findUpdatePackages(Pool $pool, PackageInterface $package); - public function selectPreferredPackages(Pool $pool, array $installedMap, array $literals, $requiredPackage = null); + public function selectPreferredPackages(Pool $pool, array $literals, $requiredPackage = null); } diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 355ba25d4..e62d03999 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -12,18 +12,11 @@ namespace Composer\DependencyResolver; -use Composer\Package\BasePackage; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; -use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\EmptyConstraint; -use Composer\Repository\RepositoryInterface; -use Composer\Repository\CompositeRepository; -use Composer\Repository\ComposerRepository; -use Composer\Repository\InstalledRepositoryInterface; -use Composer\Repository\PlatformRepository; use Composer\Package\PackageInterface; /** diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 8ab6e5244..f1450d182 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -53,6 +53,10 @@ class PoolBuilder // TODO do we really want the request here? kind of want a root requirements thingy instead $loadNames = array(); + foreach ($request->getFixedPackages() as $package) { + // TODO can actually use very specific constraint + $loadNames[$package->getname()] = null; + } foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': @@ -99,7 +103,7 @@ class PoolBuilder } foreach ($this->packages as $i => $package) { - // we check all alias related packages at once, so no need ot check individual aliases + // we check all alias related packages at once, so no need to check individual aliases // isset also checks non-null value if (!$package instanceof AliasPackage && isset($this->nameConstraints[$package->getName()])) { $constraint = $this->nameConstraints[$package->getName()]; diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 271c7261f..fed294fc6 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -220,7 +220,7 @@ class Problem if (isset($job['constraint'])) { $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); } else { - $packages = array(); + $packages = $this->pool->whatProvides($job['packageName'], null); } return 'Job(cmd='.$job['cmd'].', target='.$job['packageName'].', packages=['.$this->getPackageList($packages).'])'; diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 85dc9c4d0..a225e33b6 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -12,6 +12,10 @@ namespace Composer\DependencyResolver; +use Composer\Package\Package; +use Composer\Package\PackageInterface; +use Composer\Package\RootAliasPackage; +use Composer\Repository\RepositoryInterface; use Composer\Semver\Constraint\ConstraintInterface; /** @@ -19,11 +23,14 @@ use Composer\Semver\Constraint\ConstraintInterface; */ class Request { - protected $jobs; + protected $lockedRepository; + protected $jobs = array(); + protected $fixedPackages = array(); + protected $unlockables = array(); - public function __construct() + public function __construct(RepositoryInterface $lockedRepository = null) { - $this->jobs = array(); + $this->lockedRepository = $lockedRepository; } public function install($packageName, ConstraintInterface $constraint = null) @@ -31,11 +38,6 @@ class Request $this->addJob($packageName, 'install', $constraint); } - public function update($packageName, ConstraintInterface $constraint = null) - { - $this->addJob($packageName, 'update', $constraint); - } - public function remove($packageName, ConstraintInterface $constraint = null) { $this->addJob($packageName, 'remove', $constraint); @@ -43,18 +45,21 @@ class Request /** * Mark an existing package as being installed and having to remain installed - * - * These jobs will not be tempered with by the solver - * - * @param string $packageName - * @param ConstraintInterface|null $constraint */ - public function fix($packageName, ConstraintInterface $constraint = null) + public function fixPackage(PackageInterface $package, $lockable = true) { - $this->addJob($packageName, 'install', $constraint, true); + if ($package instanceof RootAliasPackage) { + $package = $package->getAliasOf(); + } + + $this->fixedPackages[] = $package; + + if (!$lockable) { + $this->unlockables[] = $package; + } } - protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null, $fixed = false) + protected function addJob($packageName, $cmd, ConstraintInterface $constraint = null) { $packageName = strtolower($packageName); @@ -62,17 +67,48 @@ class Request 'cmd' => $cmd, 'packageName' => $packageName, 'constraint' => $constraint, - 'fixed' => $fixed, ); } - public function updateAll() - { - $this->jobs[] = array('cmd' => 'update-all'); - } - public function getJobs() { return $this->jobs; } + + public function getFixedPackages() + { + return $this->fixedPackages; + } + + public function getPresentMap() + { + $presentMap = array(); + + if ($this->lockedRepository) { + foreach ($this->lockedRepository as $package) { + $presentMap[$package->id] = $package; + } + } + + foreach ($this->fixedPackages as $package) { + $presentMap[$package->id] = $package; + } + + return $presentMap; + } + + public function getUnlockableMap() + { + $unlockableMap = array(); + + foreach ($this->unlockables as $package) { + $unlockableMap[$package->id] = $package; + } + + return $unlockableMap; + } + + public function getLockMap() + { + } } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index 4533a6b61..c238f59fa 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -231,7 +231,7 @@ abstract class Rule case self::RULE_INSTALLED_PACKAGE_OBSOLETES: return $ruleText; case self::RULE_PACKAGE_SAME_NAME: - return 'Can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.'; + return 'Same name, can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.'; case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 9aaf564a5..0a63eaacb 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -24,8 +24,6 @@ class RuleSetGenerator protected $policy; protected $pool; protected $rules; - protected $jobs; - protected $installedMap; protected $addedMap; protected $conflictAddedMap; protected $addedPackages; @@ -218,8 +216,6 @@ class RuleSetGenerator } // check obsoletes and implicit obsoletes of a package - $isInstalled = isset($this->installedMap[$package->id]); - foreach ($package->getReplaces() as $link) { if (!isset($this->addedPackagesByNames[$link->getTarget()])) { continue; @@ -232,7 +228,7 @@ class RuleSetGenerator } if (!$this->obsoleteImpossibleForAlias($package, $provider)) { - $reason = $isInstalled ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES; + $reason = Rule::RULE_PACKAGE_OBSOLETES; $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $link)); } } @@ -254,21 +250,31 @@ class RuleSetGenerator return $impossible; } - protected function addRulesForJobs($ignorePlatformReqs) + protected function addRulesForRequest($request, $ignorePlatformReqs) { - foreach ($this->jobs as $job) { + foreach ($request->getFixedPackages() as $package) { + $this->addRulesForPackage($package, $ignorePlatformReqs); + + $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_JOB_INSTALL, array( + 'cmd' => 'fix', + 'packageName' => $package->getName(), + 'constraint' => null, + 'fixed' => true + )); + $this->addRule(RuleSet::TYPE_JOB, $rule); + } + + foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': - if (!$job['fixed'] && $ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { + if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { break; } $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); if ($packages) { foreach ($packages as $package) { - if (!isset($this->installedMap[$package->id])) { - $this->addRulesForPackage($package, $ignorePlatformReqs); - } + $this->addRulesForPackage($package, $ignorePlatformReqs); } $rule = $this->createInstallOneOfRule($packages, Rule::RULE_JOB_INSTALL, $job); @@ -288,21 +294,16 @@ class RuleSetGenerator } } - public function getRulesFor($jobs, $installedMap, $ignorePlatformReqs = false) + public function getRulesFor(Request $request, $ignorePlatformReqs = false) { - $this->jobs = $jobs; $this->rules = new RuleSet; - $this->installedMap = $installedMap; $this->addedMap = array(); $this->conflictAddedMap = array(); $this->addedPackages = array(); $this->addedPackagesByNames = array(); - foreach ($this->installedMap as $package) { - $this->addRulesForPackage($package, $ignorePlatformReqs); - } - $this->addRulesForJobs($ignorePlatformReqs); + $this->addRulesForRequest($request, $ignorePlatformReqs); $this->addConflictRules(); diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 2188d99ce..1a5905c67 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -29,23 +29,18 @@ class Solver protected $policy; /** @var Pool */ protected $pool = null; - /** @var RepositoryInterface */ - protected $installed; + /** @var RuleSet */ protected $rules; /** @var RuleSetGenerator */ protected $ruleSetGenerator; - /** @var array */ - protected $jobs; - /** @var int[] */ - protected $updateMap = array(); /** @var RuleWatchGraph */ protected $watchGraph; /** @var Decisions */ protected $decisions; - /** @var int[] */ - protected $installedMap; + /** @var Package[] */ + protected $fixedMap; /** @var int */ protected $propagateIndex; @@ -67,15 +62,13 @@ class Solver /** * @param PolicyInterface $policy * @param Pool $pool - * @param RepositoryInterface $installed * @param IOInterface $io */ - public function __construct(PolicyInterface $policy, Pool $pool, RepositoryInterface $installed, IOInterface $io) + public function __construct(PolicyInterface $policy, Pool $pool, IOInterface $io) { $this->io = $io; $this->policy = $policy; $this->pool = $pool; - $this->installed = $installed; } /** @@ -164,36 +157,22 @@ class Solver } } - protected function setupInstalledMap() + protected function setupFixedMap(Request $request) { - $this->installedMap = array(); - foreach ($this->installed->getPackages() as $package) { - $this->installedMap[$package->id] = $package; + $this->fixedMap = array(); + foreach ($request->getFixedPackages() as $package) { + $this->fixedMap[$package->id] = $package; } } /** + * @param Request $request * @param bool $ignorePlatformReqs */ - protected function checkForRootRequireProblems($ignorePlatformReqs) + protected function checkForRootRequireProblems($request, $ignorePlatformReqs) { - foreach ($this->jobs as $job) { + foreach ($request->getJobs() as $job) { switch ($job['cmd']) { - case 'update': - $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']); - foreach ($packages as $package) { - if (isset($this->installedMap[$package->id])) { - $this->updateMap[$package->id] = true; - } - } - break; - - case 'update-all': - foreach ($this->installedMap as $package) { - $this->updateMap[$package->id] = true; - } - break; - case 'install': if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) { break; @@ -212,18 +191,16 @@ class Solver /** * @param Request $request * @param bool $ignorePlatformReqs - * @return array + * @return LockTransaction */ public function solve(Request $request, $ignorePlatformReqs = false) { - $this->jobs = $request->getJobs(); - - $this->setupInstalledMap(); + $this->setupFixedMap($request); $this->io->writeError('Generating rules', true, IOInterface::DEBUG); $this->ruleSetGenerator = new RuleSetGenerator($this->policy, $this->pool); - $this->rules = $this->ruleSetGenerator->getRulesFor($this->jobs, $this->installedMap, $ignorePlatformReqs); - $this->checkForRootRequireProblems($ignorePlatformReqs); + $this->rules = $this->ruleSetGenerator->getRulesFor($request, $ignorePlatformReqs); + $this->checkForRootRequireProblems($request, $ignorePlatformReqs); $this->decisions = new Decisions($this->pool); $this->watchGraph = new RuleWatchGraph; @@ -240,20 +217,11 @@ class Solver $this->io->writeError('', true, IOInterface::DEBUG); $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); - // decide to remove everything that's installed and undecided - foreach ($this->installedMap as $packageId => $void) { - if ($this->decisions->undecided($packageId)) { - $this->decisions->decide(-$packageId, 1, null); - } - } - if ($this->problems) { - throw new SolverProblemsException($this->problems, $this->installedMap, $this->learnedPool); + throw new SolverProblemsException($this->problems, $request->getPresentMap(), $this->learnedPool); } - $transaction = new Transaction($this->policy, $this->pool, $this->installedMap, $this->decisions); - - return $transaction->getOperations(); + return new LockTransaction($this->policy, $this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); } /** @@ -392,7 +360,7 @@ class Solver private function selectAndInstall($level, array $decisionQueue, $disableRules, Rule $rule) { // choose best package to install from decisionQueue - $literals = $this->policy->selectPreferredPackages($this->pool, $this->installedMap, $decisionQueue, $rule->getRequiredPackage()); + $literals = $this->policy->selectPreferredPackages($this->pool, $decisionQueue, $rule->getRequiredPackage()); $selectedLiteral = array_shift($literals); @@ -728,19 +696,14 @@ class Solver } if ($noneSatisfied && count($decisionQueue)) { - // prune all update packages until installed version - // except for requested updates - if (count($this->installed) != count($this->updateMap)) { - $prunedQueue = array(); - foreach ($decisionQueue as $literal) { - if (isset($this->installedMap[abs($literal)])) { - $prunedQueue[] = $literal; - if (isset($this->updateMap[abs($literal)])) { - $prunedQueue = $decisionQueue; - break; - } - } + // if any of the options in the decision queue are fixed, only use those + $prunedQueue = array(); + foreach ($decisionQueue as $literal) { + if (isset($this->fixedMap[abs($literal)])) { + $prunedQueue[] = $literal; } + } + if (!empty($prunedQueue)) { $decisionQueue = $prunedQueue; } } diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php deleted file mode 100644 index c8d3bbe53..000000000 --- a/src/Composer/DependencyResolver/Transaction.php +++ /dev/null @@ -1,244 +0,0 @@ - - * Jordi Boggiano - * - * For the full copyright and license information, please view the LICENSE - * file that was distributed with this source code. - */ - -namespace Composer\DependencyResolver; - -use Composer\Package\AliasPackage; - -/** - * @author Nils Adermann - */ -class Transaction -{ - protected $policy; - protected $pool; - protected $installedMap; - protected $decisions; - protected $transaction; - - public function __construct($policy, $pool, $installedMap, $decisions) - { - $this->policy = $policy; - $this->pool = $pool; - $this->installedMap = $installedMap; - $this->decisions = $decisions; - $this->transaction = array(); - } - - public function getOperations() - { - $installMeansUpdateMap = $this->findUpdates(); - - $updateMap = array(); - $installMap = array(); - $uninstallMap = array(); - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; - - $package = $this->pool->literalToPackage($literal); - - // wanted & installed || !wanted & !installed - if (($literal > 0) == isset($this->installedMap[$package->id])) { - continue; - } - - if ($literal > 0) { - if (isset($installMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { - $source = $installMeansUpdateMap[abs($literal)]; - - $updateMap[$package->id] = array( - 'package' => $package, - 'source' => $source, - 'reason' => $reason, - ); - - // avoid updates to one package from multiple origins - unset($installMeansUpdateMap[abs($literal)]); - $ignoreRemove[$source->id] = true; - } else { - $installMap[$package->id] = array( - 'package' => $package, - 'reason' => $reason, - ); - } - } - } - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; - $package = $this->pool->literalToPackage($literal); - - if ($literal <= 0 && - isset($this->installedMap[$package->id]) && - !isset($ignoreRemove[$package->id])) { - $uninstallMap[$package->id] = array( - 'package' => $package, - 'reason' => $reason, - ); - } - } - - $this->transactionFromMaps($installMap, $updateMap, $uninstallMap); - - return $this->transaction; - } - - protected function transactionFromMaps($installMap, $updateMap, $uninstallMap) - { - $queue = array_map( - function ($operation) { - return $operation['package']; - }, - $this->findRootPackages($installMap, $updateMap) - ); - - $visited = array(); - - while (!empty($queue)) { - $package = array_pop($queue); - $packageId = $package->id; - - if (!isset($visited[$packageId])) { - $queue[] = $package; - - if ($package instanceof AliasPackage) { - $queue[] = $package->getAliasOf(); - } else { - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); - - foreach ($possibleRequires as $require) { - $queue[] = $require; - } - } - } - - $visited[$package->id] = true; - } else { - if (isset($installMap[$packageId])) { - $this->install( - $installMap[$packageId]['package'], - $installMap[$packageId]['reason'] - ); - unset($installMap[$packageId]); - } - if (isset($updateMap[$packageId])) { - $this->update( - $updateMap[$packageId]['source'], - $updateMap[$packageId]['package'], - $updateMap[$packageId]['reason'] - ); - unset($updateMap[$packageId]); - } - } - } - - foreach ($uninstallMap as $uninstall) { - $this->uninstall($uninstall['package'], $uninstall['reason']); - } - } - - protected function findRootPackages($installMap, $updateMap) - { - $packages = $installMap + $updateMap; - $roots = $packages; - - foreach ($packages as $packageId => $operation) { - $package = $operation['package']; - - if (!isset($roots[$packageId])) { - continue; - } - - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint()); - - foreach ($possibleRequires as $require) { - if ($require !== $package) { - unset($roots[$require->id]); - } - } - } - } - - return $roots; - } - - protected function findUpdates() - { - $installMeansUpdateMap = array(); - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $package = $this->pool->literalToPackage($literal); - - if ($package instanceof AliasPackage) { - continue; - } - - // !wanted & installed - if ($literal <= 0 && isset($this->installedMap[$package->id])) { - $updates = $this->policy->findUpdatePackages($this->pool, $this->installedMap, $package); - - $literals = array($package->id); - - foreach ($updates as $update) { - $literals[] = $update->id; - } - - foreach ($literals as $updateLiteral) { - if ($updateLiteral !== $literal) { - $installMeansUpdateMap[abs($updateLiteral)] = $package; - } - } - } - } - - return $installMeansUpdateMap; - } - - protected function install($package, $reason) - { - if ($package instanceof AliasPackage) { - return $this->markAliasInstalled($package, $reason); - } - - $this->transaction[] = new Operation\InstallOperation($package, $reason); - } - - protected function update($from, $to, $reason) - { - $this->transaction[] = new Operation\UpdateOperation($from, $to, $reason); - } - - protected function uninstall($package, $reason) - { - if ($package instanceof AliasPackage) { - return $this->markAliasUninstalled($package, $reason); - } - - $this->transaction[] = new Operation\UninstallOperation($package, $reason); - } - - protected function markAliasInstalled($package, $reason) - { - $this->transaction[] = new Operation\MarkAliasInstalledOperation($package, $reason); - } - - protected function markAliasUninstalled($package, $reason) - { - $this->transaction[] = new Operation\MarkAliasUninstalledOperation($package, $reason); - } -} diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index c37d7cf45..c45374e5d 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -19,6 +19,7 @@ use Composer\IO\IOInterface; use Composer\Composer; use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Repository\CompositeRepository; +use Composer\Repository\RepositoryInterface; use Composer\Repository\RepositorySet; use Composer\Script; use Composer\Installer\PackageEvent; @@ -130,9 +131,9 @@ class EventDispatcher * @return int return code of the executed script if any, for php scripts a false return * value is changed to 1, anything else to 0 */ - public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, CompositeRepository $installedRepo, Request $request, array $operations = array()) + public function dispatchInstallerEvent($eventName, $devMode, PolicyInterface $policy, RepositorySet $repositorySet, RepositoryInterface $lockedRepo, Request $request, array $operations = array()) { - return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $installedRepo, $request, $operations)); + return $this->doDispatch(new InstallerEvent($eventName, $this->composer, $this->io, $devMode, $policy, $repositorySet, $lockedRepo, $request, $operations)); } /** diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 10f115daf..07433c911 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -14,6 +14,7 @@ namespace Composer; use Composer\Autoload\AutoloadGenerator; use Composer\DependencyResolver\DefaultPolicy; +use Composer\DependencyResolver\LocalRepoTransaction; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; @@ -74,6 +75,12 @@ class Installer */ protected $package; + // TODO can we get rid of the below and just use the package itself? + /** + * @var RootPackageInterface + */ + protected $fixedRootPackage; + /** * @var DownloadManager */ @@ -139,7 +146,7 @@ class Installer /** * @var RepositoryInterface */ - protected $additionalInstalledRepository; + protected $additionalFixedRepository; /** * Constructor @@ -184,6 +191,7 @@ class Installer // Force update if there is no lock file present if (!$this->update && !$this->locker->isLocked()) { + // TODO throw an error instead? $this->update = true; } @@ -202,6 +210,7 @@ class Installer putenv("COMPOSER_DEV_MODE=$devMode"); // dispatch pre event + // should we treat this more strictly as running an update and then running an install, triggering events multiple times? $eventName = $this->update ? ScriptEvents::PRE_UPDATE_CMD : ScriptEvents::PRE_INSTALL_CMD; $this->eventDispatcher->dispatchScript($eventName, $this->devMode); } @@ -209,15 +218,8 @@ class Installer $this->downloadManager->setPreferSource($this->preferSource); $this->downloadManager->setPreferDist($this->preferDist); - // create installed repo, this contains all local packages + platform packages (php & extensions) $localRepo = $this->repositoryManager->getLocalRepository(); - if ($this->update) { - $platformOverrides = $this->config->get('platform') ?: array(); - } else { - $platformOverrides = $this->locker->getPlatformOverrides(); - } - $platformRepo = new PlatformRepository(array(), $platformOverrides); - $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); + $platformRepo = $this->createPlatformRepo($this->update); $aliases = $this->getRootAliases(); $this->aliasPlatformPackages($platformRepo, $aliases); @@ -227,7 +229,12 @@ class Installer } try { - list($res, $devPackages) = $this->doInstall($localRepo, $installedRepo, $platformRepo, $aliases); + // TODO what are installs? does locking a package without downloading code count? + if ($this->update) { + $res = $this->doUpdate($localRepo, $platformRepo, $aliases, true); + } else { + $res = $this->doInstall($localRepo, $platformRepo, $aliases, false); + } if ($res !== 0) { return $res; } @@ -243,10 +250,11 @@ class Installer } // output suggestions if we're in dev mode - if ($this->devMode && !$this->skipSuggest) { - $this->suggestedPackagesReporter->output($installedRepo); + if ($this->update && $this->devMode && !$this->skipSuggest) { + $this->suggestedPackagesReporter->output($this->locker->getLockedRepository($this->devMode)); } + // TODO probably makes more sense to do this on the lock file only? # Find abandoned packages and warn user foreach ($localRepo->getPackages() as $package) { if (!$package instanceof CompletePackage || !$package->isAbandoned()) { @@ -266,30 +274,6 @@ class Installer ); } - // write lock - if ($this->update && $this->writeLock) { - $localRepo->reload(); - - $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); - $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); - - $updatedLock = $this->locker->setLockData( - array_diff($localRepo->getCanonicalPackages(), $devPackages), - $devPackages, - $platformReqs, - $platformDevReqs, - $aliases, - $this->package->getMinimumStability(), - $this->package->getStabilityFlags(), - $this->preferStable || $this->package->getPreferStable(), - $this->preferLowest, - $this->config->get('platform') ?: array() - ); - if ($updatedLock) { - $this->io->writeError('Writing lock file'); - } - } - if ($this->dumpAutoloader) { // write autoloader if ($this->optimizeAutoloader) { @@ -333,181 +317,284 @@ class Installer return 0; } + protected function doUpdate(RepositoryInterface $localRepo, PlatformRepository $platformRepo, $aliases, $doInstall) + { + $lockedRepository = null; + + if ($this->locker->isLocked()) { + $lockedRepository = $this->locker->getLockedRepository(true); + } + + if ($this->updateWhitelist) { + if (!$lockedRepository) { + $this->io->writeError('Cannot update only a partial set of packages without a lock file present.', true, IOInterface::QUIET); + return 1; + } + $this->whitelistUpdateDependencies( + $lockedRepository, + $this->package->getRequires(), + $this->package->getDevRequires() + ); + } + + $this->io->writeError('Loading composer repositories with package information'); + + // creating repository set + $policy = $this->createPolicy(true); + $repositorySet = $this->createRepositorySet($platformRepo, $aliases); + $repositories = $this->repositoryManager->getRepositories(); + foreach ($repositories as $repository) { + $repositorySet->addRepository($repository); + } + if ($lockedRepository) { + $repositorySet->addRepository($lockedRepository); + } + // TODO can we drop any locked packages that we have matching remote versions for? + + $request = $this->createRequest($this->fixedRootPackage, $platformRepo); + + if ($lockedRepository) { + // TODO do we really always need this? Maybe only to skip fix() in updateWhitelist case cause these packages get removed on full update automatically? + foreach ($lockedRepository->getPackages() as $lockedPackage) { + if (!$repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { + $constraint = new Constraint('=', $lockedPackage->getVersion()); + $constraint->setPrettyString('(stability not acceptable)'); + $request->remove($lockedPackage->getName(), $constraint); + } + } + } + + $this->io->writeError('Updating dependencies'); + + $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); + + foreach ($links as $link) { + $request->install($link->getTarget(), $link->getConstraint()); + } + + // if the updateWhitelist is enabled, packages not in it are also fixed + // to the version specified in the lock + if ($this->updateWhitelist) { + foreach ($lockedRepository->getPackages() as $lockedPackage) { + if (!$this->isUpdateable($lockedPackage) && $repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { + // TODO add reason for fix? + $request->fixPackage($lockedPackage); + } + } + } + + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); + + $pool = $repositorySet->createPool($request); + + // TODO ensure that the solver always picks most recent reference for dev packages, so they get updated even when just a new commit is pushed but version is unchanged + + // solve dependencies + $solver = new Solver($policy, $pool, $this->io); + try { + $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); + } catch (SolverProblemsException $e) { + $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError($e->getMessage()); + if (!$this->devMode) { + $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); + } + + return max(1, $e->getCode()); + } + + // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $lockedRepository, $request, $lockTransaction); + + $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); + $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE); + + if (!$lockTransaction->getOperations()) { + $this->io->writeError('Nothing to modify in lock file'); + } + + // write lock + $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); + $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); + + $updatedLock = $this->locker->setLockData( + $lockTransaction->getNewLockNonDevPackages(), + $lockTransaction->getNewLockDevPackages(), + $platformReqs, + $platformDevReqs, + $aliases, + $this->package->getMinimumStability(), + $this->package->getStabilityFlags(), + $this->preferStable || $this->package->getPreferStable(), + $this->preferLowest, + $this->config->get('platform') ?: array(), + $this->writeLock && $this->executeOperations + ); + if ($updatedLock && $this->writeLock && $this->executeOperations) { + $this->io->writeError('Writing lock file'); + } + + if ($lockTransaction->getOperations()) { + $installs = $updates = $uninstalls = array(); + foreach ($lockTransaction->getOperations() as $operation) { + if ($operation instanceof InstallOperation) { + $installs[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); + } elseif ($operation instanceof UpdateOperation) { + $updates[] = $operation->getTargetPackage()->getPrettyName().':'.$operation->getTargetPackage()->getFullPrettyVersion(); + } elseif ($operation instanceof UninstallOperation) { + $uninstalls[] = $operation->getPackage()->getPrettyName(); + } + } + + $this->io->writeError(sprintf( + "Lock file operations: %d install%s, %d update%s, %d removal%s", + count($installs), + 1 === count($installs) ? '' : 's', + count($updates), + 1 === count($updates) ? '' : 's', + count($uninstalls), + 1 === count($uninstalls) ? '' : 's' + )); + if ($installs) { + $this->io->writeError("Installs: ".implode(', ', $installs), true, IOInterface::VERBOSE); + } + if ($updates) { + $this->io->writeError("Updates: ".implode(', ', $updates), true, IOInterface::VERBOSE); + } + if ($uninstalls) { + $this->io->writeError("Removals: ".implode(', ', $uninstalls), true, IOInterface::VERBOSE); + } + } + + foreach ($lockTransaction->getOperations() as $operation) { + // collect suggestions + $jobType = $operation->getJobType(); + if ($operation instanceof InstallOperation) { + $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); + } + + // TODO should this really happen here or should this be written to the lock file? + // updating, force dev packages' references if they're in root package refs + $package = null; + if ('update' === $jobType) { + $package = $operation->getTargetPackage(); + } elseif ('install' === $jobType) { + $package = $operation->getPackage(); + } + + if ($package && $package->isDev()) { + $references = $this->package->getReferences(); + if (isset($references[$package->getName()])) { + $this->updateInstallReferences($package, $references[$package->getName()]); + } + } + + // output op, but alias op only in debug verbosity + if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { + $this->io->writeError(' - ' . $operation); + } + + // output reasons why the operation was run, only for install/update operations + if ($this->verbose && $this->io->isVeryVerbose() && in_array($jobType, array('install', 'update'))) { + $reason = $operation->getReason(); + if ($reason instanceof Rule) { + switch ($reason->getReason()) { + case Rule::RULE_JOB_INSTALL: + $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($pool)); + $this->io->writeError(''); + break; + case Rule::RULE_PACKAGE_REQUIRES: + $this->io->writeError(' REASON: '.$reason->getPrettyString($pool)); + $this->io->writeError(''); + break; + } + } + } + } + + if ($doInstall) { + // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false + return $this->doInstall($localRepo, $platformRepo, $aliases, true); + } + + return 0; + } + /** * @param RepositoryInterface $localRepo * @param RepositoryInterface $installedRepo * @param PlatformRepository $platformRepo * @param array $aliases - * @return array [int, PackageInterfaces[]|null] with the exit code and an array of dev packages on update, or null on install + * @return int exit code */ - protected function doInstall($localRepo, $installedRepo, $platformRepo, $aliases) + protected function doInstall(RepositoryInterface $localRepo, PlatformRepository $platformRepo, $aliases, $alreadySolved = false) { - // init vars - $lockedRepository = null; - $repositories = null; - - // initialize locked repo if we are installing from lock or in a partial update - // and a lock file is present as we need to force install non-whitelisted lock file - // packages in that case - if (!$this->update || (!empty($this->updateWhitelist) && $this->locker->isLocked())) { - $lockedRepository = $this->locker->getLockedRepository($this->devMode); - } - - $this->whitelistUpdateDependencies( - $lockedRepository ?: $localRepo, - $this->package->getRequires(), - $this->package->getDevRequires() - ); - - $this->io->writeError('Loading composer repositories with package information'); + $lockedRepository = $this->locker->getLockedRepository($this->devMode); // creating repository set - $policy = $this->createPolicy(); - $repositorySet = $this->createRepositorySet($aliases, $this->update ? null : $lockedRepository); - $repositorySet->addRepository($installedRepo); - if ($this->update) { - $repositories = $this->repositoryManager->getRepositories(); - foreach ($repositories as $repository) { - $repositorySet->addRepository($repository); - } - } - // Add the locked repository after the others in case we are doing a - // partial update so missing packages can be found there still. - // For installs from lock it's the only one added so it is first - if ($lockedRepository) { - $repositorySet->addRepository($lockedRepository); - } + $policy = $this->createPolicy(false); + $repositorySet = $this->createRepositorySet($platformRepo, $aliases, $lockedRepository); + $repositorySet->addRepository($lockedRepository); - // creating requirements request - $request = $this->createRequest($this->package, $platformRepo); + $this->io->writeError('Installing dependencies'.($this->devMode ? ' (including require-dev)' : '').' from lock file'); - if ($this->update) { - // remove unstable packages from the localRepo if they don't match the current stability settings - $removedUnstablePackages = array(); - foreach ($localRepo->getPackages() as $package) { - if ( - !$repositorySet->isPackageAcceptable($package->getNames(), $package->getStability()) - && $this->installationManager->isPackageInstalled($localRepo, $package) - ) { - $removedUnstablePackages[$package->getName()] = true; - $request->remove($package->getName(), new Constraint('=', $package->getVersion())); - } - } + // verify that the lock file works with the current platform repository + // we can skip this part if we're doing this as the second step after an update + if (!$alreadySolved) { + $this->io->writeError('Verifying lock file contents can be installed on current platform.'); - $this->io->writeError('Updating dependencies'.($this->devMode ? ' (including require-dev)' : '').''); - - $request->updateAll(); - - $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); - - foreach ($links as $link) { - $request->install($link->getTarget(), $link->getConstraint()); - } - - // if the updateWhitelist is enabled, packages not in it are also fixed - // to the version specified in the lock, or their currently installed version - if ($this->updateWhitelist) { - $currentPackages = $this->getCurrentPackages($installedRepo); - - // collect packages to fixate from root requirements as well as installed packages - $candidates = array(); - foreach ($links as $link) { - $candidates[$link->getTarget()] = true; - $rootRequires[$link->getTarget()] = $link; - } - foreach ($currentPackages as $package) { - $candidates[$package->getName()] = true; - } - - // fix them to the version in lock (or currently installed) if they are not updateable - foreach ($candidates as $candidate => $dummy) { - foreach ($currentPackages as $curPackage) { - if ($curPackage->getName() === $candidate) { - if (!$this->isUpdateable($curPackage) && !isset($removedUnstablePackages[$curPackage->getName()])) { - $constraint = new Constraint('=', $curPackage->getVersion()); - $description = $this->locker->isLocked() ? '(locked at' : '(installed at'; - $requiredAt = isset($rootRequires[$candidate]) ? ', required as ' . $rootRequires[$candidate]->getPrettyConstraint() : ''; - $constraint->setPrettyString($description . ' ' . $curPackage->getPrettyVersion() . $requiredAt . ')'); - $request->install($curPackage->getName(), $constraint); - } - break; - } - } - } - } - } else { - $this->io->writeError('Installing dependencies'.($this->devMode ? ' (including require-dev)' : '').' from lock file'); + // creating requirements request + $request = $this->createRequest($this->fixedRootPackage, $platformRepo); if (!$this->locker->isFresh()) { $this->io->writeError('Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. Run update to update them.', true, IOInterface::QUIET); } foreach ($lockedRepository->getPackages() as $package) { - $version = $package->getVersion(); - if (isset($aliases[$package->getName()][$version])) { - $version = $aliases[$package->getName()][$version]['alias_normalized']; - } - $constraint = new Constraint('=', $version); - $constraint->setPrettyString($package->getPrettyVersion()); - $request->install($package->getName(), $constraint); + $request->fixPackage($package); } foreach ($this->locker->getPlatformRequirements($this->devMode) as $link) { $request->install($link->getTarget(), $link->getConstraint()); } - } - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); - $pool = $repositorySet->createPool($request); + $pool = $repositorySet->createPool($request); - // force dev packages to have the latest links if we update or install from a (potentially new) lock - $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-links'); + // solve dependencies + $solver = new Solver($policy, $pool, $this->io); + try { + $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); - // solve dependencies - $solver = new Solver($policy, $pool, $installedRepo, $this->io); - try { - $operations = $solver->solve($request, $this->ignorePlatformReqs); - } catch (SolverProblemsException $e) { - $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); - $this->io->writeError($e->getMessage()); - if ($this->update && !$this->devMode) { - $this->io->writeError('Running update with --no-dev does not mean require-dev is ignored, it just means the packages will not be installed. If dev requirements are blocking the update you have to resolve those problems.', true, IOInterface::QUIET); + // installing the locked packages on this platfom resulted in lock modifying operations, there wasn't a conflict, but the lock file as-is seems to not work on this system + if (0 !== count($lockTransaction->getOperations())) { + $this->io->writeError('Your lock file cannot be installed on this system without changes, please run composer update.', true, IOInterface::QUIET); + // TODO actually display operations to explain what happened? + return 1; + } + } catch (SolverProblemsException $e) { + $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError($e->getMessage()); + + return max(1, $e->getCode()); } - return array(max(1, $e->getCode()), array()); + // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); } - // force dev packages to be updated if we update or install from a (potentially new) lock - $operations = $this->processDevPackages($localRepo, $pool, $policy, $repositories, $installedRepo, $lockedRepository, 'force-updates', $operations); + // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations); + $localRepoTransaction = new LocalRepoTransaction($lockedRepository, $localRepo); - $this->io->writeError("Analyzed ".count($pool)." packages to resolve dependencies", true, IOInterface::VERBOSE); - $this->io->writeError("Analyzed ".$solver->getRuleSetSize()." rules to resolve dependencies", true, IOInterface::VERBOSE); - - // execute operations - if (!$operations) { - $this->io->writeError('Nothing to install or update'); + if (!$localRepoTransaction->getOperations()) { + $this->io->writeError('Nothing to install, update or remove'); } - $operations = $this->movePluginsToFront($operations); - $operations = $this->moveUninstallsToFront($operations); - - // extract dev packages and mark them to be skipped if it's a --no-dev install or update - // we also force them to be uninstalled if they are present in the local repo - if ($this->update) { - $devPackages = $this->extractDevPackages($operations, $localRepo, $platformRepo, $aliases); - if (!$this->devMode) { - $operations = $this->filterDevPackageOperations($devPackages, $operations, $localRepo); - } - } else { - $devPackages = null; - } - - if ($operations) { + if ($localRepoTransaction->getOperations()) { $installs = $updates = $uninstalls = array(); - foreach ($operations as $operation) { + foreach ($localRepoTransaction->getOperations() as $operation) { if ($operation instanceof InstallOperation) { $installs[] = $operation->getPackage()->getPrettyName().':'.$operation->getPackage()->getFullPrettyVersion(); } elseif ($operation instanceof UpdateOperation) { @@ -537,307 +624,42 @@ class Installer } } - foreach ($operations as $operation) { - // collect suggestions + foreach ($localRepoTransaction->getOperations() as $operation) { $jobType = $operation->getJobType(); - if ('install' === $jobType) { - $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); - } - - // updating, force dev packages' references if they're in root package refs - if ($this->update) { - $package = null; - if ('update' === $jobType) { - $package = $operation->getTargetPackage(); - } elseif ('install' === $jobType) { - $package = $operation->getPackage(); - } - if ($package && $package->isDev()) { - $references = $this->package->getReferences(); - if (isset($references[$package->getName()])) { - $this->updateInstallReferences($package, $references[$package->getName()]); - } - } - if ('update' === $jobType) { - $targetPackage = $operation->getTargetPackage(); - if ($targetPackage->isDev()) { - $initialPackage = $operation->getInitialPackage(); - if ($targetPackage->getVersion() === $initialPackage->getVersion() - && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) - && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) - ) { - $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); - $this->io->writeError('', true, IOInterface::DEBUG); - - continue; - } - } - } - } - $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($jobType); if (defined($event) && $this->runScripts) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); + //$this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); } - // output non-alias ops when not executing operations (i.e. dry run), output alias ops in debug verbosity - if (!$this->executeOperations && false === strpos($operation->getJobType(), 'Alias')) { - $this->io->writeError(' - ' . $operation); - } elseif ($this->io->isDebug() && false !== strpos($operation->getJobType(), 'Alias')) { + // output op, but alias op only in debug verbosity + if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation); } $this->installationManager->execute($localRepo, $operation); - // output reasons why the operation was ran, only for install/update operations - if ($this->verbose && $this->io->isVeryVerbose() && in_array($jobType, array('install', 'update'))) { - $reason = $operation->getReason(); - if ($reason instanceof Rule) { - switch ($reason->getReason()) { - case Rule::RULE_JOB_INSTALL: - $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - case Rule::RULE_PACKAGE_REQUIRES: - $this->io->writeError(' REASON: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - } - } - } - - if ($this->executeOperations || $this->writeLock) { + if ($this->executeOperations) { $localRepo->write(); } $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($jobType); if (defined($event) && $this->runScripts) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); + //$this->eventDispatcher->dispatchPackageEvent(constant($event), $this->devMode, $policy, $repositorySet, $installedRepo, $request, $operations, $operation); } } - if ($this->executeOperations) { - // force source/dist urls to be updated for all packages - $this->processPackageUrls($pool, $policy, $localRepo, $repositories); - $localRepo->write(); - } - - return array(0, $devPackages); + return 0; } - /** - * Extracts the dev packages out of the localRepo - * - * This works by faking the operations so we can see what the dev packages - * would be at the end of the operation execution. This lets us then remove - * the dev packages from the list of operations accordingly if we are in a - * --no-dev install or update. - * - * @return array - */ - private function extractDevPackages(array $operations, RepositoryInterface $localRepo, PlatformRepository $platformRepo, array $aliases) + private function createPlatformRepo($forUpdate) { - if (!$this->package->getDevRequires()) { - return array(); + if ($forUpdate) { + $platformOverrides = $this->config->get('platform') ?: array(); + } else { + $platformOverrides = $this->locker->getPlatformOverrides(); } - // fake-apply all operations to this clone of the local repo so we see the complete set of package we would end up with - $tempLocalRepo = clone $localRepo; - foreach ($operations as $operation) { - switch ($operation->getJobType()) { - case 'install': - case 'markAliasInstalled': - if (!$tempLocalRepo->hasPackage($operation->getPackage())) { - $tempLocalRepo->addPackage(clone $operation->getPackage()); - } - break; - - case 'uninstall': - case 'markAliasUninstalled': - $tempLocalRepo->removePackage($operation->getPackage()); - break; - - case 'update': - $tempLocalRepo->removePackage($operation->getInitialPackage()); - if (!$tempLocalRepo->hasPackage($operation->getTargetPackage())) { - $tempLocalRepo->addPackage(clone $operation->getTargetPackage()); - } - break; - - default: - throw new \LogicException('Unknown type: '.$operation->getJobType()); - } - } - - // we have to reload the local repo to handle aliases properly - // but as it is not persisted on disk we use a loader/dumper - // to reload it in memory - $localRepo = new InstalledArrayRepository(array()); - $loader = new ArrayLoader(null, true); - $dumper = new ArrayDumper(); - foreach ($tempLocalRepo->getCanonicalPackages() as $pkg) { - $localRepo->addPackage($loader->load($dumper->dump($pkg))); - } - unset($tempLocalRepo, $loader, $dumper); - - $policy = $this->createPolicy(); - $repositorySet = $this->createRepositorySet($aliases); - $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); - $repositorySet->addRepository($installedRepo); - - // creating requirements request without dev requirements - $request = $this->createRequest($this->package, $platformRepo); - $request->updateAll(); - foreach ($this->package->getRequires() as $link) { - $request->install($link->getTarget(), $link->getConstraint()); - } - - // solve deps to see which get removed - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request); - $solver = new Solver($policy, $repositorySet->createPool($request), $installedRepo, $this->io); - $ops = $solver->solve($request, $this->ignorePlatformReqs); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $repositorySet, $installedRepo, $request, $ops); - - $devPackages = array(); - foreach ($ops as $op) { - if ($op->getJobType() === 'uninstall') { - $devPackages[] = $op->getPackage(); - } - } - - return $devPackages; - } - - /** - * @return OperationInterface[] filtered operations, dev packages are uninstalled and all operations on them ignored - */ - private function filterDevPackageOperations(array $devPackages, array $operations, RepositoryInterface $localRepo) - { - $finalOps = array(); - $packagesToSkip = array(); - foreach ($devPackages as $pkg) { - $packagesToSkip[$pkg->getName()] = true; - if ($installedDevPkg = $localRepo->findPackage($pkg->getName(), '*')) { - if ($installedDevPkg instanceof AliasPackage) { - $finalOps[] = new MarkAliasUninstalledOperation($installedDevPkg, 'non-dev install removing it'); - $installedDevPkg = $installedDevPkg->getAliasOf(); - } - $finalOps[] = new UninstallOperation($installedDevPkg, 'non-dev install removing it'); - } - } - - // skip operations applied on dev packages - foreach ($operations as $op) { - $package = $op->getJobType() === 'update' ? $op->getTargetPackage() : $op->getPackage(); - if (isset($packagesToSkip[$package->getName()])) { - continue; - } - - $finalOps[] = $op; - } - - return $finalOps; - } - - /** - * Workaround: if your packages depend on plugins, we must be sure - * that those are installed / updated first; else it would lead to packages - * being installed multiple times in different folders, when running Composer - * twice. - * - * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, - * it at least fixes the symptoms and makes usage of composer possible (again) - * in such scenarios. - * - * @param OperationInterface[] $operations - * @return OperationInterface[] reordered operation list - */ - private function movePluginsToFront(array $operations) - { - $pluginsNoDeps = array(); - $pluginsWithDeps = array(); - $pluginRequires = array(); - - foreach (array_reverse($operations, true) as $idx => $op) { - if ($op instanceof InstallOperation) { - $package = $op->getPackage(); - } elseif ($op instanceof UpdateOperation) { - $package = $op->getTargetPackage(); - } else { - continue; - } - - // is this package a plugin? - $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; - - // is this a plugin or a dependency of a plugin? - if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { - // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' - $requires = array_filter(array_keys($package->getRequires()), function ($req) { - return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); - }); - - // is this a plugin with no meaningful dependencies? - if ($isPlugin && !count($requires)) { - // plugins with no dependencies go to the very front - array_unshift($pluginsNoDeps, $op); - } else { - // capture the requirements for this package so those packages will be moved up as well - $pluginRequires = array_merge($pluginRequires, $requires); - // move the operation to the front - array_unshift($pluginsWithDeps, $op); - } - - unset($operations[$idx]); - } - } - - return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); - } - - /** - * Removals of packages should be executed before installations in - * case two packages resolve to the same path (due to custom installers) - * - * @param OperationInterface[] $operations - * @return OperationInterface[] reordered operation list - */ - private function moveUninstallsToFront(array $operations) - { - $uninstOps = array(); - foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation) { - $uninstOps[] = $op; - unset($operations[$idx]); - } - } - - return array_merge($uninstOps, $operations); - } - - /** - * @return RepositoryInterface - */ - private function createInstalledRepo(RepositoryInterface $localRepo, PlatformRepository $platformRepo) - { - // clone root package to have one in the installed repo that does not require anything - // we don't want it to be uninstallable, but its requirements should not conflict - // with the lock file for example - $installedRootPackage = clone $this->package; - $installedRootPackage->setRequires(array()); - $installedRootPackage->setDevRequires(array()); - - $repos = array( - $localRepo, - new InstalledArrayRepository(array($installedRootPackage)), - $platformRepo, - ); - $installedRepo = new CompositeRepository($repos); - if ($this->additionalInstalledRepository) { - $installedRepo->addRepository($this->additionalInstalledRepository); - } - - return $installedRepo; + return new PlatformRepository(array(), $platformOverrides); } /** @@ -845,8 +667,10 @@ class Installer * @param RepositoryInterface|null $lockedRepository * @return RepositorySet */ - private function createRepositorySet(array $rootAliases = array(), $lockedRepository = null) + private function createRepositorySet(PlatformRepository $platformRepo, array $rootAliases = array(), $lockedRepository = null) { + // TODO what's the point of rootConstraints at all, we generate the package pool taking them into account anyway? + // TODO maybe we can drop the lockedRepository here if ($this->update) { $minimumStability = $this->package->getMinimumStability(); $stabilityFlags = $this->package->getStabilityFlags(); @@ -877,17 +701,28 @@ class Installer } } - return new RepositorySet($rootAliases, $minimumStability, $stabilityFlags, $rootConstraints); + $this->fixedRootPackage = clone $this->package; + $this->fixedRootPackage->setRequires(array()); + $this->fixedRootPackage->setDevRequires(array()); + + $repositorySet = new RepositorySet($rootAliases, $minimumStability, $stabilityFlags, $rootConstraints); + $repositorySet->addRepository(new InstalledArrayRepository(array($this->fixedRootPackage))); + $repositorySet->addRepository($platformRepo); + if ($this->additionalFixedRepository) { + $repositorySet->addRepository($this->additionalFixedRepository); + } + + return $repositorySet; } /** * @return DefaultPolicy */ - private function createPolicy() + private function createPolicy($forUpdate) { $preferStable = null; $preferLowest = null; - if (!$this->update) { + if (!$forUpdate) { $preferStable = $this->locker->getPreferStable(); $preferLowest = $this->locker->getPreferLowest(); } @@ -912,191 +747,30 @@ class Installer { $request = new Request(); - $constraint = new Constraint('=', $rootPackage->getVersion()); - $constraint->setPrettyString($rootPackage->getPrettyVersion()); - $request->install($rootPackage->getName(), $constraint); + $request->fixPackage($rootPackage, false); $fixedPackages = $platformRepo->getPackages(); - if ($this->additionalInstalledRepository) { - $additionalFixedPackages = $this->additionalInstalledRepository->getPackages(); - $fixedPackages = array_merge($fixedPackages, $additionalFixedPackages); + if ($this->additionalFixedRepository) { + $fixedPackages = array_merge($fixedPackages, $this->additionalFixedRepository->getPackages()); } // fix the version of all platform packages + additionally installed packages // to prevent the solver trying to remove or update those + // TODO why not replaces? $provided = $rootPackage->getProvides(); foreach ($fixedPackages as $package) { - $constraint = new Constraint('=', $package->getVersion()); - $constraint->setPrettyString($package->getPrettyVersion()); - // skip platform packages that are provided by the root package if ($package->getRepository() !== $platformRepo || !isset($provided[$package->getName()]) - || !$provided[$package->getName()]->getConstraint()->matches($constraint) + || !$provided[$package->getName()]->getConstraint()->matches(new Constraint('=', $package->getVersion())) ) { - $request->fix($package->getName(), $constraint); + $request->fixPackage($package, false); } } return $request; } - /** - * @param WritableRepositoryInterface $localRepo - * @param Pool $pool - * @param PolicyInterface $policy - * @param array $repositories - * @param RepositoryInterface $installedRepo - * @param RepositoryInterface $lockedRepository - * @param string $task - * @param array|null $operations - * @return array - */ - private function processDevPackages($localRepo, Pool $pool, $policy, $repositories, $installedRepo, $lockedRepository, $task, array $operations = null) - { - if ($task === 'force-updates' && null === $operations) { - throw new \InvalidArgumentException('Missing operations argument'); - } - if ($task === 'force-links') { - $operations = array(); - } - - if ($this->update && $this->updateWhitelist) { - $currentPackages = $this->getCurrentPackages($installedRepo); - } - - foreach ($localRepo->getCanonicalPackages() as $package) { - // skip non-dev packages - if (!$package->isDev()) { - continue; - } - - // skip packages that will be updated/uninstalled - foreach ($operations as $operation) { - if (('update' === $operation->getJobType() && $operation->getInitialPackage()->equals($package)) - || ('uninstall' === $operation->getJobType() && $operation->getPackage()->equals($package)) - ) { - continue 2; - } - } - - if ($this->update) { - // skip package if the whitelist is enabled and it is not in it - if ($this->updateWhitelist && !$this->isUpdateable($package)) { - // check if non-updateable packages are out of date compared to the lock file to ensure we don't corrupt it - foreach ($currentPackages as $curPackage) { - if ($curPackage->isDev() && $curPackage->getName() === $package->getName() && $curPackage->getVersion() === $package->getVersion()) { - if ($task === 'force-links') { - $package->setRequires($curPackage->getRequires()); - $package->setConflicts($curPackage->getConflicts()); - $package->setProvides($curPackage->getProvides()); - $package->setReplaces($curPackage->getReplaces()); - } elseif ($task === 'force-updates') { - if (($curPackage->getSourceReference() && $curPackage->getSourceReference() !== $package->getSourceReference()) - || ($curPackage->getDistReference() && $curPackage->getDistReference() !== $package->getDistReference()) - ) { - $operations[] = new UpdateOperation($package, $curPackage); - } - } - - break; - } - } - - continue; - } - - // find similar packages (name/version) in all repositories - $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion()), true); - foreach ($matches as $index => $match) { - // skip local packages - if (!in_array($match->getRepository(), $repositories, true)) { - unset($matches[$index]); - continue; - } - - $matches[$index] = $match->getId(); - } - - // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { - $newPackage = $pool->literalToPackage($matches[0]); - - if ($task === 'force-links' && $newPackage) { - $package->setRequires($newPackage->getRequires()); - $package->setConflicts($newPackage->getConflicts()); - $package->setProvides($newPackage->getProvides()); - $package->setReplaces($newPackage->getReplaces()); - } - - if ( - $task === 'force-updates' - && $newPackage - && ( - ($newPackage->getSourceReference() && $newPackage->getSourceReference() !== $package->getSourceReference()) - || ($newPackage->getDistReference() && $newPackage->getDistReference() !== $package->getDistReference()) - ) - ) { - $operations[] = new UpdateOperation($package, $newPackage); - - continue; - } - } - - if ($task === 'force-updates') { - // force installed package to update to referenced version in root package if it does not match the installed version - $references = $this->package->getReferences(); - - if (isset($references[$package->getName()]) && $references[$package->getName()] !== $package->getSourceReference()) { - // changing the source ref to update to will be handled in the operations loop - $operations[] = new UpdateOperation($package, clone $package); - } - } - } else { - // force update to locked version if it does not match the installed version - foreach ($lockedRepository->findPackages($package->getName()) as $lockedPackage) { - if ($lockedPackage->isDev() && $lockedPackage->getVersion() === $package->getVersion()) { - if ($task === 'force-links') { - $package->setRequires($lockedPackage->getRequires()); - $package->setConflicts($lockedPackage->getConflicts()); - $package->setProvides($lockedPackage->getProvides()); - $package->setReplaces($lockedPackage->getReplaces()); - } elseif ($task === 'force-updates') { - if (($lockedPackage->getSourceReference() && $lockedPackage->getSourceReference() !== $package->getSourceReference()) - || ($lockedPackage->getDistReference() && $lockedPackage->getDistReference() !== $package->getDistReference()) - ) { - $operations[] = new UpdateOperation($package, $lockedPackage); - } - } - - break; - } - } - } - } - - return $operations; - } - - /** - * Loads the most "current" list of packages that are installed meaning from lock ideally or from installed repo as fallback - * @param RepositoryInterface $installedRepo - * @return array - */ - private function getCurrentPackages($installedRepo) - { - if ($this->locker->isLocked()) { - try { - return $this->locker->getLockedRepository(true)->getPackages(); - } catch (\RuntimeException $e) { - // fetch only non-dev packages from lock if doing a dev update fails due to a previously incomplete lock file - return $this->locker->getLockedRepository()->getPackages(); - } - } - - return $installedRepo->getPackages(); - } - /** * @return array */ @@ -1120,58 +794,6 @@ class Installer return $normalizedAliases; } - /** - * @param Pool $pool - * @param PolicyInterface $policy - * @param WritableRepositoryInterface $localRepo - * @param array $repositories - */ - private function processPackageUrls(Pool $pool, $policy, $localRepo, $repositories) - { - if (!$this->update) { - return; - } - - $rootRefs = $this->package->getReferences(); - - foreach ($localRepo->getCanonicalPackages() as $package) { - // find similar packages (name/version) in pool - $matches = $pool->whatProvides($package->getName(), new Constraint('=', $package->getVersion()), true); - foreach ($matches as $index => $match) { - // skip local packages - if (!in_array($match->getRepository(), $repositories, true)) { - unset($matches[$index]); - continue; - } - - $matches[$index] = $match->getId(); - } - - // select preferred package according to policy rules - if ($matches && $matches = $policy->selectPreferredPackages($pool, array(), $matches)) { - $newPackage = $pool->literalToPackage($matches[0]); - - // update the dist and source URLs - $sourceUrl = $package->getSourceUrl(); - $newSourceUrl = $newPackage->getSourceUrl(); - $newReference = $newPackage->getSourceReference(); - - if ($package->isDev() && isset($rootRefs[$package->getName()]) && $package->getSourceReference() === $rootRefs[$package->getName()]) { - $newReference = $rootRefs[$package->getName()]; - } - - $this->updatePackageUrl($package, $newSourceUrl, $newPackage->getSourceType(), $newReference, $newPackage->getDistUrl()); - - if ($package instanceof CompletePackage && $newPackage instanceof CompletePackage) { - $package->setAbandoned($newPackage->getReplacementPackage() ?: $newPackage->isAbandoned()); - } - - $package->setDistMirrors($newPackage->getDistMirrors()); - $package->setSourceMirrors($newPackage->getSourceMirrors()); - } - } - } - private function updatePackageUrl(PackageInterface $package, $sourceUrl, $sourceType, $sourceReference, $distUrl) { $oldSourceRef = $package->getSourceReference(); @@ -1271,18 +893,14 @@ class Installer * skipped including their dependencies, unless they are listed in the * update whitelist themselves or $whitelistAllDependencies is true. * - * @param RepositoryInterface $localOrLockRepo Use the locked repo if available, otherwise installed repo will do + * @param RepositoryInterface $lockRepo Use the locked repo * As we want the most accurate package list to work with, and installed * repo might be empty but locked repo will always be current. * @param array $rootRequires An array of links to packages in require of the root package * @param array $rootDevRequires An array of links to packages in require-dev of the root package */ - private function whitelistUpdateDependencies($localOrLockRepo, array $rootRequires, array $rootDevRequires) + private function whitelistUpdateDependencies($lockRepo, array $rootRequires, array $rootDevRequires) { - if (!$this->updateWhitelist) { - return; - } - $rootRequires = array_merge($rootRequires, $rootDevRequires); $skipPackages = array(); @@ -1293,7 +911,7 @@ class Installer } $repositorySet = new RepositorySet(array(), 'dev'); - $repositorySet->addRepository($localOrLockRepo); + $repositorySet->addRepository($lockRepo); $seen = array(); @@ -1310,7 +928,7 @@ class Installer if (empty($depPackages)) { // add any installed package matching the whitelisted name/pattern $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); - foreach ($localOrLockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { + foreach ($lockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, false); } @@ -1418,12 +1036,12 @@ class Installer } /** - * @param RepositoryInterface $additionalInstalledRepository + * @param RepositoryInterface $additionalFixedRepository * @return $this */ - public function setAdditionalInstalledRepository(RepositoryInterface $additionalInstalledRepository) + public function setAdditionalFixedRepository(RepositoryInterface $additionalFixedRepository) { - $this->additionalInstalledRepository = $additionalInstalledRepository; + $this->additionalFixedRepository = $additionalFixedRepository; return $this; } diff --git a/src/Composer/Installer/SuggestedPackagesReporter.php b/src/Composer/Installer/SuggestedPackagesReporter.php index 25788e547..023aeb91d 100644 --- a/src/Composer/Installer/SuggestedPackagesReporter.php +++ b/src/Composer/Installer/SuggestedPackagesReporter.php @@ -96,21 +96,21 @@ class SuggestedPackagesReporter * @param RepositoryInterface $installedRepo Installed packages * @return SuggestedPackagesReporter */ - public function output(RepositoryInterface $installedRepo = null) + public function output(RepositoryInterface $lockedRepo = null) { $suggestedPackages = $this->getPackages(); - $installedPackages = array(); - if (null !== $installedRepo && ! empty($suggestedPackages)) { - foreach ($installedRepo->getPackages() as $package) { - $installedPackages = array_merge( - $installedPackages, + $lockedPackages = array(); + if (null !== $lockedRepo && ! empty($suggestedPackages)) { + foreach ($lockedRepo->getPackages() as $package) { + $lockedPackages = array_merge( + $lockedPackages, $package->getNames() ); } } foreach ($suggestedPackages as $suggestion) { - if (in_array($suggestion['target'], $installedPackages)) { + if (in_array($suggestion['target'], $lockedPackages)) { continue; } diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 405d43261..063c893b6 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -40,6 +40,7 @@ class Locker private $dumper; private $process; private $lockDataCache; + private $virtualFileWritten; /** * Initializes packages locker. @@ -108,7 +109,7 @@ class Locker */ public function isLocked() { - if (!$this->lockFile->exists()) { + if (!$this->virtualFileWritten && !$this->lockFile->exists()) { return false; } @@ -282,10 +283,11 @@ class Locker * @param bool $preferStable * @param bool $preferLowest * @param array $platformOverrides + * @param bool $write Whether to actually write data to disk, useful in tests and for --dry-run * * @return bool */ - public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides) + public function setLockData(array $packages, $devPackages, array $platformReqs, $platformDevReqs, array $aliases, $minimumStability, array $stabilityFlags, $preferStable, $preferLowest, array $platformOverrides, $write = true) { $lock = array( '_readme' => array('This file locks the dependencies of your project to a known state', @@ -325,7 +327,11 @@ class Locker if (empty($lock['packages']) && empty($lock['packages-dev']) && empty($lock['platform']) && empty($lock['platform-dev'])) { if ($this->lockFile->exists()) { - unlink($this->lockFile->getPath()); + if ($write) { + unlink($this->lockFile->getPath()); + } else { + $this->virtualFileWritten = false; + } } return false; @@ -337,8 +343,15 @@ class Locker $isLocked = false; } if (!$isLocked || $lock !== $this->getLockData()) { - $this->lockFile->write($lock); - $this->lockDataCache = null; + if ($write) { + $this->lockFile->write($lock); +// $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock, 448 & JsonFile::JSON_PRETTY_PRINT)); + $this->lockDataCache = null; + $this->virtualFileWritten = false; + } else { + $this->virtualFileWritten = true; + $this->lockDataCache = JsonFile::parseJson(JsonFile::encode($lock, 448 & JsonFile::JSON_PRETTY_PRINT)); + } return true; } diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 7314c6540..919d098e3 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -29,7 +29,7 @@ class DefaultPolicyTest extends TestCase /** @var ArrayRepository */ protected $repo; /** @var ArrayRepository */ - protected $repoInstalled; + protected $repoLocked; /** @var DefaultPolicy */ protected $policy; @@ -37,7 +37,7 @@ class DefaultPolicyTest extends TestCase { $this->repositorySet = new RepositorySet(array(), 'dev'); $this->repo = new ArrayRepository; - $this->repoInstalled = new ArrayRepository; + $this->repoLocked = new ArrayRepository; $this->policy = new DefaultPolicy; } @@ -52,7 +52,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId()); $expected = array($packageA->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -68,7 +68,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -84,7 +84,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -101,7 +101,7 @@ class DefaultPolicyTest extends TestCase $expected = array($packageA1->getId()); $policy = new DefaultPolicy(true); - $selected = $policy->selectPreferredPackages($pool, array(), $literals); + $selected = $policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -117,24 +117,24 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA2->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } - public function testSelectNewestOverInstalled() + public function testSelectNewestOverLocked() { $this->repo->addPackage($packageA = $this->getPackage('A', '2.0')); - $this->repoInstalled->addPackage($packageAInstalled = $this->getPackage('A', '1.0')); - $this->repositorySet->addRepository($this->repoInstalled); + $this->repoLocked->addPackage($packageAInstalled = $this->getPackage('A', '1.0')); $this->repositorySet->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoLocked); $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA->getId(), $packageAInstalled->getId()); $expected = array($packageA->getId()); - $selected = $this->policy->selectPreferredPackages($pool, $this->mapFromRepo($this->repoInstalled), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -146,16 +146,16 @@ class DefaultPolicyTest extends TestCase $this->repo->addPackage($packageA = $this->getPackage('A', '1.0')); $otherRepository->addPackage($packageAImportant = $this->getPackage('A', '1.0')); - $this->repositorySet->addRepository($this->repoInstalled); $this->repositorySet->addRepository($otherRepository); $this->repositorySet->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoLocked); $pool = $this->repositorySet->createPoolForPackage('A'); $literals = array($packageA->getId(), $packageAImportant->getId()); $expected = array($packageAImportant->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -177,7 +177,7 @@ class DefaultPolicyTest extends TestCase $literals = array($package1->getId(), $package2->getId(), $package3->getId(), $package4->getId()); $expected = array($package2->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); @@ -188,7 +188,7 @@ class DefaultPolicyTest extends TestCase $pool = $this->repositorySet->createPoolForPackage('A'); $expected = array($package4->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -205,9 +205,9 @@ class DefaultPolicyTest extends TestCase $repoImportant->addPackage($packageA2AliasImportant = new AliasPackage($packageA2Important, '2.1.9999999.9999999-dev', '2.1.x-dev')); $packageAAliasImportant->setRootPackageAlias(true); - $this->repositorySet->addRepository($this->repoInstalled); $this->repositorySet->addRepository($repoImportant); $this->repositorySet->addRepository($this->repo); + $this->repositorySet->addRepository($this->repoLocked); $pool = $this->repositorySet->createPoolForPackage('A'); @@ -219,7 +219,7 @@ class DefaultPolicyTest extends TestCase $expected = array($packageAAliasImportant->getId()); - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -239,7 +239,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -258,7 +258,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals); + $selected = $this->policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } @@ -279,7 +279,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals, 'vendor-a/package'); + $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package'); $this->assertEquals($expected, $selected); // test with reversed order in repo @@ -295,20 +295,10 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA->getId(), $packageB->getId()); $expected = $literals; - $selected = $this->policy->selectPreferredPackages($pool, array(), $literals, 'vendor-a/package'); + $selected = $this->policy->selectPreferredPackages($pool, $literals, 'vendor-a/package'); $this->assertSame($expected, $selected); } - protected function mapFromRepo(RepositoryInterface $repo) - { - $map = array(); - foreach ($repo->getPackages() as $package) { - $map[$package->getId()] = true; - } - - return $map; - } - public function testSelectLowest() { $policy = new DefaultPolicy(false, true); @@ -322,7 +312,7 @@ class DefaultPolicyTest extends TestCase $literals = array($packageA1->getId(), $packageA2->getId()); $expected = array($packageA1->getId()); - $selected = $policy->selectPreferredPackages($pool, array(), $literals); + $selected = $policy->selectPreferredPackages($pool, $literals); $this->assertSame($expected, $selected); } diff --git a/tests/Composer/Test/DependencyResolver/RequestTest.php b/tests/Composer/Test/DependencyResolver/RequestTest.php index dfa411ed9..405f3b0c8 100644 --- a/tests/Composer/Test/DependencyResolver/RequestTest.php +++ b/tests/Composer/Test/DependencyResolver/RequestTest.php @@ -31,14 +31,12 @@ class RequestTest extends TestCase $request = new Request(); $request->install('foo'); - $request->fix('bar'); $request->remove('foobar'); $this->assertEquals( array( - array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null, 'fixed' => false), - array('cmd' => 'install', 'packageName' => 'bar', 'constraint' => null, 'fixed' => true), - array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null, 'fixed' => false), + array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => null), + array('cmd' => 'remove', 'packageName' => 'foobar', 'constraint' => null), ), $request->getJobs() ); @@ -60,21 +58,9 @@ class RequestTest extends TestCase $this->assertEquals( array( - array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint, 'fixed' => false), + array('cmd' => 'install', 'packageName' => 'foo', 'constraint' => $constraint), ), $request->getJobs() ); } - - public function testUpdateAll() - { - $request = new Request(); - - $request->updateAll(); - - $this->assertEquals( - array(array('cmd' => 'update-all')), - $request->getJobs() - ); - } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index b80e7c61d..54f726ef1 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -30,6 +30,7 @@ class SolverTest extends TestCase protected $repoSet; protected $repo; protected $repoInstalled; + protected $repoLocked; protected $request; protected $policy; protected $solver; @@ -39,6 +40,7 @@ class SolverTest extends TestCase $this->repoSet = new RepositorySet(array()); $this->repo = new ArrayRepository; $this->repoInstalled = new InstalledArrayRepository; + $this->repoLocked = new ArrayRepository; $this->request = new Request($this->repoSet); $this->policy = new DefaultPolicy; @@ -59,6 +61,7 @@ class SolverTest extends TestCase public function testSolverRemoveIfNotInstalled() { $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage(clone $packageA); $this->reposComplete(); $this->checkSolverResult(array( @@ -919,8 +922,8 @@ class SolverTest extends TestCase protected function reposComplete() { - $this->repoSet->addRepository($this->repoInstalled); $this->repoSet->addRepository($this->repo); + $this->repoSet->addRepository($this->repoLocked); } protected function createSolver() diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index d774ea188..ec108c7b9 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -95,6 +95,55 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" } } ] +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" }, + "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --EXPECT-LOCK-- { "packages": [ diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index acaf8f1ff..05c49f6a9 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -16,6 +16,7 @@ use Composer\Installer; use Composer\Console\Application; use Composer\IO\BufferIO; use Composer\Json\JsonFile; +use Composer\Package\Dumper\ArrayDumper; use Composer\Util\Filesystem; use Composer\Repository\ArrayRepository; use Composer\Repository\RepositoryManager; @@ -74,10 +75,30 @@ class InstallerTest extends TestCase foreach ($repositories as $repository) { $repositoryManager->addRepository($repository); } - - $locker = $this->getMockBuilder('Composer\Package\Locker')->disableOriginalConstructor()->getMock(); $installationManager = new InstallationManagerMock(); + // emulate a writable lock file + $lockData = null; + $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); + $lockJsonMock->expects($this->any()) + ->method('read') + ->will($this->returnCallback(function() use (&$lockData) { + return json_decode($lockData, true); + })); + $lockJsonMock->expects($this->any()) + ->method('exists') + ->will($this->returnCallback(function () use (&$lockData) { + return $lockData !== null; + })); + $lockJsonMock->expects($this->any()) + ->method('write') + ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { + $lockData = json_encode($value, JSON_PRETTY_PRINT); + })); + + $tempLockData = null; + $locker = new Locker($io, $lockJsonMock, $repositoryManager, $installationManager, '{}'); + $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock(); $installer = new Installer($io, $config, clone $rootPackage, $downloadManager, $repositoryManager, $locker, $installationManager, $eventDispatcher, $autoloadGenerator); @@ -91,7 +112,7 @@ class InstallerTest extends TestCase $expectedUninstalled = isset($options['uninstall']) ? $options['uninstall'] : array(); $installed = $installationManager->getInstalledPackages(); - $this->assertSame($expectedInstalled, $installed); + $this->assertEquals($this->makePackagesComparable($expectedInstalled), $this->makePackagesComparable($installed)); $updated = $installationManager->getUpdatedPackages(); $this->assertSame($expectedUpdated, $updated); @@ -100,6 +121,17 @@ class InstallerTest extends TestCase $this->assertSame($expectedUninstalled, $uninstalled); } + protected function makePackagesComparable($packages) + { + $dumper = new ArrayDumper(); + + $comparable = []; + foreach ($packages as $package) { + $comparable[] = $dumper->dump($package); + } + return $comparable; + } + public function provideInstaller() { $cases = array(); @@ -109,11 +141,11 @@ class InstallerTest extends TestCase $a = $this->getPackage('A', '1.0.0', 'Composer\Package\RootPackage'); $a->setRequires(array( - new Link('A', 'B', $this->getVersionConstraint('=', '1.0.0')), + 'b' => new Link('A', 'B', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $b = $this->getPackage('B', '1.0.0'); $b->setRequires(array( - new Link('B', 'A', $this->getVersionConstraint('=', '1.0.0')), + 'a' => new Link('B', 'A', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $cases[] = array( @@ -129,11 +161,11 @@ class InstallerTest extends TestCase $a = $this->getPackage('A', '1.0.0', 'Composer\Package\RootPackage'); $a->setRequires(array( - new Link('A', 'B', $this->getVersionConstraint('=', '1.0.0')), + 'b' => new Link('A', 'B', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $b = $this->getPackage('B', '1.0.0'); $b->setRequires(array( - new Link('B', 'A', $this->getVersionConstraint('=', '1.0.0')), + 'a' => new Link('B', 'A', $v = $this->getVersionConstraint('=', '1.0.0'), 'requires', $v->getPrettyString()), )); $cases[] = array( @@ -144,6 +176,7 @@ class InstallerTest extends TestCase ), ); + // TODO why are there not more cases with uninstall/update? return $cases; } @@ -182,13 +215,24 @@ class InstallerTest extends TestCase $repositoryManager = $composer->getRepositoryManager(); $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); + // emulate a writable lock file + $lockData = $lock ? json_encode($lock, JSON_PRETTY_PRINT): null; $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); $lockJsonMock->expects($this->any()) ->method('read') - ->will($this->returnValue($lock)); + ->will($this->returnCallback(function() use (&$lockData) { + return json_decode($lockData, true); + })); $lockJsonMock->expects($this->any()) ->method('exists') - ->will($this->returnValue(true)); + ->will($this->returnCallback(function () use (&$lockData) { + return $lockData !== null; + })); + $lockJsonMock->expects($this->any()) + ->method('write') + ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { + $lockData = json_encode($value, JSON_PRETTY_PRINT); + })); if ($expectLock) { $actualLock = array(); @@ -245,7 +289,7 @@ class InstallerTest extends TestCase $application->setAutoExit(false); $appOutput = fopen('php://memory', 'w+'); - $input = new StringInput($run); + $input = new StringInput($run.' -vvv'); $input->setInteractive(false); $result = $application->run($input, new StreamOutput($appOutput)); fseek($appOutput, 0); From c875f538ea49048a783d43082e3dfeb0a3d5c5dc Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 14 Feb 2019 17:57:29 +0100 Subject: [PATCH 03/37] Make root alias behaviour consistent, add root ref handling, lock to newest metadata root aliases during install should come from the lock file only, for better reproducibility we don't reuse the value from update for the following install --- .../DependencyResolver/LockTransaction.php | 10 +++- src/Composer/Installer.php | 59 +++++++++---------- src/Composer/Repository/RepositorySet.php | 2 + 3 files changed, 39 insertions(+), 32 deletions(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 35cf760b9..ad25f8fc1 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -108,7 +108,7 @@ class LockTransaction } // TODO additionalFixedRepository needs to be looked at here as well? - public function getNewLockNonDevPackages() + public function getNewLockNonDevPackages(array $rootForceReferences) { $packages = array(); foreach ($this->decisions as $i => $decision) { @@ -117,6 +117,14 @@ class LockTransaction if ($literal > 0) { $package = $this->pool->literalToPackage($literal); if (!isset($this->unlockableMap[$package->id]) && !($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { + + echo "rootRef? ".$package->getName()."\n"; + // TODO can we really just do this for all of them here? What if we're doing a partial update, should this change anyway? + if (isset($rootForceReferences[$package->getName()])) { + echo "rootRef! ".$package->getName()."\n"; + $package->setSourceReference($rootForceReferences[$package->getName()]); + } + $packages[] = $package; } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 07433c911..ffeb4ee98 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -219,21 +219,17 @@ class Installer $this->downloadManager->setPreferDist($this->preferDist); $localRepo = $this->repositoryManager->getLocalRepository(); - $platformRepo = $this->createPlatformRepo($this->update); - - $aliases = $this->getRootAliases(); - $this->aliasPlatformPackages($platformRepo, $aliases); if (!$this->suggestedPackagesReporter) { $this->suggestedPackagesReporter = new SuggestedPackagesReporter($this->io); } try { - // TODO what are installs? does locking a package without downloading code count? if ($this->update) { - $res = $this->doUpdate($localRepo, $platformRepo, $aliases, true); + // TODO introduce option to set doInstall to false (update lock file without vendor install) + $res = $this->doUpdate($localRepo, true); } else { - $res = $this->doInstall($localRepo, $platformRepo, $aliases, false); + $res = $this->doInstall($localRepo); } if ($res !== 0) { return $res; @@ -317,8 +313,11 @@ class Installer return 0; } - protected function doUpdate(RepositoryInterface $localRepo, PlatformRepository $platformRepo, $aliases, $doInstall) + protected function doUpdate(RepositoryInterface $localRepo, $doInstall) { + $platformRepo = $this->createPlatformRepo(true); + $aliases = $this->getRootAliases(true); + $lockedRepository = null; if ($this->locker->isLocked()) { @@ -377,6 +376,13 @@ class Installer if ($this->updateWhitelist) { foreach ($lockedRepository->getPackages() as $lockedPackage) { if (!$this->isUpdateable($lockedPackage) && $repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { + // need to actually allow for metadata updates at all times, so we want to fix the most recent prefered package in the repo set instead + $packages = $repositorySet->findPackages($lockedPackage->getName(), new Constraint('=', $lockedPackage->getVersion())); + $lockedPackage = isset($packages[0]) ? $packages[0] : $lockedPackage; + + // in how far do we need to reset requirements here, theoretically it's the same version so nothing should have changed, but for a dev version it could have? + + // TODO add reason for fix? $request->fixPackage($lockedPackage); } @@ -418,7 +424,7 @@ class Installer $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockNonDevPackages(), + $lockTransaction->getNewLockNonDevPackages($this->package->getReferences()), $lockTransaction->getNewLockDevPackages(), $platformReqs, $platformDevReqs, @@ -473,22 +479,6 @@ class Installer $this->suggestedPackagesReporter->addSuggestionsFromPackage($operation->getPackage()); } - // TODO should this really happen here or should this be written to the lock file? - // updating, force dev packages' references if they're in root package refs - $package = null; - if ('update' === $jobType) { - $package = $operation->getTargetPackage(); - } elseif ('install' === $jobType) { - $package = $operation->getPackage(); - } - - if ($package && $package->isDev()) { - $references = $this->package->getReferences(); - if (isset($references[$package->getName()])) { - $this->updateInstallReferences($package, $references[$package->getName()]); - } - } - // output op, but alias op only in debug verbosity if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation); @@ -514,7 +504,7 @@ class Installer if ($doInstall) { // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false - return $this->doInstall($localRepo, $platformRepo, $aliases, true); + return $this->doInstall($localRepo, true); } return 0; @@ -527,8 +517,11 @@ class Installer * @param array $aliases * @return int exit code */ - protected function doInstall(RepositoryInterface $localRepo, PlatformRepository $platformRepo, $aliases, $alreadySolved = false) + protected function doInstall(RepositoryInterface $localRepo, $alreadySolved = false) { + $platformRepo = $this->createPlatformRepo(false); + $aliases = $this->getRootAliases(false); + $lockedRepository = $this->locker->getLockedRepository($this->devMode); // creating repository set @@ -669,6 +662,8 @@ class Installer */ private function createRepositorySet(PlatformRepository $platformRepo, array $rootAliases = array(), $lockedRepository = null) { + $this->aliasPlatformPackages($platformRepo, $rootAliases); + // TODO what's the point of rootConstraints at all, we generate the package pool taking them into account anyway? // TODO maybe we can drop the lockedRepository here if ($this->update) { @@ -772,11 +767,12 @@ class Installer } /** + * @param bool $forUpdate * @return array */ - private function getRootAliases() + private function getRootAliases($forUpdate) { - if ($this->update) { + if ($forUpdate) { $aliases = $this->package->getAliases(); } else { $aliases = $this->locker->getAliases(); @@ -838,9 +834,10 @@ class Installer */ private function aliasPlatformPackages(PlatformRepository $platformRepo, $aliases) { - foreach ($aliases as $package => $versions) { + // TODO should the repository set do this? + foreach ($aliases as $packageName => $versions) { foreach ($versions as $version => $alias) { - $packages = $platformRepo->findPackages($package, $version); + $packages = $platformRepo->findPackages($packageName, $version); foreach ($packages as $package) { $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); $aliasPackage->setRootPackageAlias(true); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index bac63dd36..6a232630d 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -101,6 +101,8 @@ class RepositorySet /** * Find packages providing or matching a name and optionally meeting a constraint in all repositories * + * Returned in the order of repositories, matching priority + * * @param string $name * @param ConstraintInterface|null $constraint * @param bool $exactMatch From f1e4ccbe1dd89e183bd3e063f0c049baa4d9818a Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 01:58:12 +0200 Subject: [PATCH 04/37] Fix handling of reference updates and root references --- .../LocalRepoTransaction.php | 5 +- .../DependencyResolver/LockTransaction.php | 11 +- .../DependencyResolver/PoolBuilder.php | 40 +++- src/Composer/DependencyResolver/Request.php | 7 +- src/Composer/Installer.php | 42 +--- src/Composer/Repository/RepositorySet.php | 7 +- .../Test/DependencyResolver/SolverTest.php | 2 +- .../installer/update-changes-url.test | 14 +- .../installer/update-mirrors-changes-url.test | 213 ++++++++++++++++++ 9 files changed, 280 insertions(+), 61 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index 98c0b05d7..4ac747243 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -62,9 +62,10 @@ class LocalRepoTransaction // do we need to update? if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { $operations[] = new Operation\UpdateOperation($source, $package); - } else { - // TODO do we need to update metadata? force update based on reference? + } elseif ($package->isDev() && $package->getSourceReference() !== $localPackageMap[$package->getName()]->getSourceReference()) { + $operations[] = new Operation\UpdateOperation($source, $package); } + unset($removeMap[$package->getName()]); } else { $operations[] = new Operation\InstallOperation($package); unset($removeMap[$package->getName()]); diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index ad25f8fc1..e113988bd 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -108,7 +108,7 @@ class LockTransaction } // TODO additionalFixedRepository needs to be looked at here as well? - public function getNewLockNonDevPackages(array $rootForceReferences) + public function getNewLockNonDevPackages() { $packages = array(); foreach ($this->decisions as $i => $decision) { @@ -117,14 +117,6 @@ class LockTransaction if ($literal > 0) { $package = $this->pool->literalToPackage($literal); if (!isset($this->unlockableMap[$package->id]) && !($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { - - echo "rootRef? ".$package->getName()."\n"; - // TODO can we really just do this for all of them here? What if we're doing a partial update, should this change anyway? - if (isset($rootForceReferences[$package->getName()])) { - echo "rootRef! ".$package->getName()."\n"; - $package->setSourceReference($rootForceReferences[$package->getName()]); - } - $packages[] = $package; } } @@ -135,6 +127,7 @@ class LockTransaction public function getNewLockDevPackages() { + // TODO this is empty? $packages = array(); return $packages; } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 8f164df71..f9a7541c3 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -14,11 +14,11 @@ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; +use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Repository\AsyncRepositoryInterface; use Composer\Repository\ComposerRepository; use Composer\Repository\InstalledRepositoryInterface; -use Composer\Repository\LockArrayRepository; use Composer\Repository\PlatformRepository; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\MultiConstraint; @@ -31,6 +31,7 @@ class PoolBuilder private $isPackageAcceptableCallable; private $filterRequires; private $rootAliases; + private $rootReferences; private $aliasMap = array(); private $nameConstraints = array(); @@ -46,16 +47,17 @@ class PoolBuilder $this->filterRequires = $filterRequires; } - public function buildPool(array $repositories, array $rootAliases, Request $request) + public function buildPool(array $repositories, array $rootAliases, array $rootReferences, Request $request) { $pool = new Pool($this->filterRequires); $this->rootAliases = $rootAliases; + $this->rootReferences = $rootReferences; // TODO do we really want the request here? kind of want a root requirements thingy instead $loadNames = array(); foreach ($request->getFixedPackages() as $package) { // TODO can actually use very specific constraint - $loadNames[$package->getname()] = null; + $loadNames[$package->getName()] = null; } foreach ($request->getJobs() as $job) { switch ($job['cmd']) { @@ -94,7 +96,7 @@ class PoolBuilder foreach ($packages as $package) { if (call_user_func($this->isPackageAcceptableCallable, $package->getNames(), $package->getStability())) { - $newLoadNames += $this->loadPackage($package, $key); + $newLoadNames += $this->loadPackage($request, $package, $key); } } } @@ -132,7 +134,7 @@ class PoolBuilder if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface) { foreach ($repository->getPackages() as $package) { - $this->loadPackage($package, $key); + $this->loadPackage($request, $package, $key); } } } @@ -146,7 +148,7 @@ class PoolBuilder return $pool; } - private function loadPackage(PackageInterface $package, $repoIndex) + private function loadPackage(Request $request, PackageInterface $package, $repoIndex) { $index = count($this->packages); $this->packages[] = $package; @@ -156,8 +158,18 @@ class PoolBuilder $this->aliasMap[spl_object_hash($package->getAliasOf())][$index] = $package; } - // handle root package aliases $name = $package->getName(); + + // we're simply setting the root references on all versions for a name here and rely on the solver to pick the + // right version. It'd be more work to figure out which versions and which aliases of those versions this may + // apply to + if (isset($this->rootReferences[$name])) { + // do not modify the references on already locked packages + if (!$request->isFixedPackage($package)) { + $this->setReferences($package, $this->rootReferences[$name]); + } + } + if (isset($this->rootAliases[$name][$package->getVersion()])) { $alias = $this->rootAliases[$name][$package->getVersion()]; if ($package instanceof AliasPackage) { @@ -194,5 +206,19 @@ class PoolBuilder return $loadNames; } + + private function setReferences(Package $package, $reference) + { + $package->setSourceReference($reference); + + // only bitbucket, github and gitlab have auto generated dist URLs that easily allow replacing the reference in the dist URL + // TODO generalize this a bit for self-managed/on-prem versions? Some kind of replace token in dist urls which allow this? + if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { + $package->setDistReference($reference); + $package->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $package->getDistUrl())); + } elseif ($package->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it + $package->setDistReference($reference); + } + } } diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index a225e33b6..166ebbe07 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -52,7 +52,7 @@ class Request $package = $package->getAliasOf(); } - $this->fixedPackages[] = $package; + $this->fixedPackages[spl_object_hash($package)] = $package; if (!$lockable) { $this->unlockables[] = $package; @@ -80,6 +80,11 @@ class Request return $this->fixedPackages; } + public function isFixedPackage(PackageInterface $package) + { + return isset($this->fixedPackages[spl_object_hash($package)]); + } + public function getPresentMap() { $presentMap = array(); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index c07fc44c9..a59a4dc4c 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -39,6 +39,7 @@ use Composer\Package\CompletePackage; use Composer\Package\Link; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; +use Composer\Package\Package; use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\Package\Locker; @@ -375,25 +376,21 @@ class Installer // to the version specified in the lock if ($this->updateWhitelist) { foreach ($lockedRepository->getPackages() as $lockedPackage) { + // TODO should this really be checking acceptability here? if (!$this->isUpdateable($lockedPackage) && $repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { - // need to actually allow for metadata updates at all times, so we want to fix the most recent prefered package in the repo set instead - $packages = $repositorySet->findPackages($lockedPackage->getName(), new Constraint('=', $lockedPackage->getVersion())); - $lockedPackage = isset($packages[0]) ? $packages[0] : $lockedPackage; - - // in how far do we need to reset requirements here, theoretically it's the same version so nothing should have changed, but for a dev version it could have? - - // TODO add reason for fix? $request->fixPackage($lockedPackage); } } } + // TODO reenable events //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request); $pool = $repositorySet->createPool($request); // TODO ensure that the solver always picks most recent reference for dev packages, so they get updated even when just a new commit is pushed but version is unchanged + // should already be solved by using the remote package in all cases in the pool // solve dependencies $solver = new Solver($policy, $pool, $this->io); @@ -426,7 +423,7 @@ class Installer $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockNonDevPackages($this->package->getReferences()), + $lockTransaction->getNewLockNonDevPackages(), $lockTransaction->getNewLockDevPackages(), $platformReqs, $platformDevReqs, @@ -504,6 +501,8 @@ class Installer } } + $this->io->write('foo'); + if ($doInstall) { // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false return $this->doInstall($localRepo, true); @@ -703,7 +702,7 @@ class Installer $this->fixedRootPackage->setRequires(array()); $this->fixedRootPackage->setDevRequires(array()); - $repositorySet = new RepositorySet($rootAliases, $minimumStability, $stabilityFlags, $rootConstraints); + $repositorySet = new RepositorySet($rootAliases, $this->package->getReferences(), $minimumStability, $stabilityFlags, $rootConstraints); $repositorySet->addRepository(new InstalledArrayRepository(array($this->fixedRootPackage))); $repositorySet->addRepository($platformRepo); if ($this->additionalFixedRepository) { @@ -793,28 +792,7 @@ class Installer return $normalizedAliases; } - private function updatePackageUrl(PackageInterface $package, $sourceUrl, $sourceType, $sourceReference, $distUrl) - { - $oldSourceRef = $package->getSourceReference(); - - if ($package->getSourceUrl() !== $sourceUrl) { - $package->setSourceType($sourceType); - $package->setSourceUrl($sourceUrl); - $package->setSourceReference($sourceReference); - } - - // only update dist url for github/bitbucket/gitlab dists as they use a combination of dist url + dist reference to install - // but for other urls this is ambiguous and could result in bad outcomes - if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $distUrl)) { - $package->setDistUrl($distUrl); - $this->updateInstallReferences($package, $sourceReference); - } - - if ($this->updateWhitelist && !$this->isUpdateable($package)) { - $this->updateInstallReferences($package, $oldSourceRef); - } - } - + // TODO do we still need this function? private function updateInstallReferences(PackageInterface $package, $reference) { if (!$reference) { @@ -910,7 +888,7 @@ class Installer } } - $repositorySet = new RepositorySet(array(), 'dev'); + $repositorySet = new RepositorySet(array(), array(), 'dev'); $repositorySet->addRepository($lockRepo); $seen = array(); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 6a232630d..f7f8a57f8 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -29,6 +29,8 @@ class RepositorySet { /** @var array */ private $rootAliases; + /** @var array */ + private $rootReferences; /** @var RepositoryInterface[] */ private $repositories = array(); @@ -40,9 +42,10 @@ class RepositorySet /** @var Pool */ private $pool; - public function __construct(array $rootAliases = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) + public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) { $this->rootAliases = $rootAliases; + $this->rootReferences = $rootReferences; $this->acceptableStabilities = array(); foreach (BasePackage::$stabilities as $stability => $value) { @@ -151,7 +154,7 @@ class RepositorySet { $poolBuilder = new PoolBuilder(array($this, 'isPackageAcceptable'), $this->filterRequires); - return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $request); + return $this->pool = $poolBuilder->buildPool($this->repositories, $this->rootAliases, $this->rootReferences, $request); } // TODO unify this with above in some simpler version without "request"? diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 76abd958a..3491f9c05 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -930,7 +930,7 @@ class SolverTest extends TestCase protected function createSolver() { - $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), $this->repoInstalled, new NullIO()); + $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), new NullIO()); } protected function checkSolverResult(array $expected) diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index ec108c7b9..c9df288f6 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -3,10 +3,10 @@ Update updates URLs for updated packages if they have changed a/a is dev and gets everything updated as it updates to a new ref b/b is a tag and gets everything updated by updating the package URL directly -c/c is a tag and not whitelisted and gets the new URL but keeps its old ref +c/c is a tag and not whitelisted and remains unchanged d/d is dev but with a #ref so it should get URL updated but not the reference e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref -e/e is dev but not whitelisted and gets the new URL but keeps its old ref +f/f is dev but not whitelisted and remains unchanged g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref --COMPOSER-- { @@ -161,8 +161,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, { "name": "c/c", "version": "1.0.0", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/newc", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/newc/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, { @@ -179,8 +179,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, { "name": "f/f", "version": "dev-master", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/newf/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, { @@ -208,6 +208,6 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an --RUN-- update a/a b/b d/d g/g --EXPECT-- -Installing e/e (dev-master 1111111) Updating a/a (dev-master 1111111) to a/a (dev-master 2222222) +Installing e/e (dev-master 1111111) Updating g/g (dev-master 0000000) to g/g (dev-master 1111111) diff --git a/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test new file mode 100644 index 000000000..9d88870b0 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test @@ -0,0 +1,213 @@ +--TEST-- +Update mirrors updates URLs for all packages if they have changed without updating versions + +a/a is dev and gets everything updated as it updates to a new ref +b/b is a tag and gets everything updated by updating the package URL directly +c/c is a tag and not whitelisted and gets the new URL but keeps its old ref +d/d is dev but with a #ref so it should get URL updated but not the reference +e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref +e/e is dev but not whitelisted and gets the new URL but keeps its old ref +g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/c/newc", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/c/newc/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/d/newd", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/d/newd/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "e/e", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/e/newe", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/e/newe/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/f/newf", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/f/newf/zipball/2222222222222222222222222222222222222222", "type": "zip" } + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/g/newg", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/g/newg/zipball/2222222222222222222222222222222222222222", "type": "zip" } + } + ] + } + ], + "require": { + "a/a": "dev-master", + "b/b": "2.0.3", + "c/c": "1.0.0", + "d/d": "dev-master#1111111111111111111111111111111111111111", + "e/e": "dev-master#1111111111111111111111111111111111111111", + "f/f": "dev-master", + "g/g": "dev-master#1111111111111111111111111111111111111111" + } +} +--INSTALLED-- +[ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" } + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" }, + "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" } + } +] +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/a", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/a/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/b", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/b/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/c", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/c/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/d", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/d/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/f", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/f/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/g", "type": "git" }, + "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/g/zipball/0000000000000000000000000000000000000000", "type": "zip" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" }, + "type": "library" + }, + { + "name": "b/b", "version": "2.0.3", + "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" }, + "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" }, + "type": "library" + }, + { + "name": "c/c", "version": "1.0.0", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/c/newc", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/c/newc/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "d/d", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/d/newd", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/newd/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "e/e", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/e/newe", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/e/newe/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "f/f", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/f/newf/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + }, + { + "name": "g/g", "version": "dev-master", + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/g/newg", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/g/newg/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": { + "a/a": 20, + "d/d": 20, + "e/e": 20, + "f/f": 20, + "g/g": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--RUN-- +update a/a b/b d/d g/g +--EXPECT-- +Installing e/e (dev-master 1111111) +Updating a/a (dev-master 1111111) to a/a (dev-master 2222222) +Updating g/g (dev-master 0000000) to g/g (dev-master 1111111) From 06d11f2f382335c082dfa8bd19c6dc21792f1083 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 02:28:40 +0200 Subject: [PATCH 05/37] Fix calculation of lock transaction updates and start updating output in tests --- .../LocalRepoTransaction.php | 5 +- .../DependencyResolver/LockTransaction.php | 2 +- src/Composer/DependencyResolver/Request.php | 2 +- src/Composer/Installer.php | 51 +++++++++---------- .../installer/broken-deps-do-not-replace.test | 2 +- .../installer/github-issues-4319.test | 2 +- .../installer/github-issues-4795-2.test | 28 ++++++++-- .../Test/Fixtures/installer/suggest-prod.test | 5 +- .../installer/suggest-uninstalled.test | 7 ++- ...dating-dev-from-lock-removes-old-deps.test | 2 +- 10 files changed, 67 insertions(+), 39 deletions(-) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index 4ac747243..db224989d 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -12,6 +12,8 @@ namespace Composer\DependencyResolver; +use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\Package\AliasPackage; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; @@ -86,7 +88,6 @@ class LocalRepoTransaction $operations = $this->movePluginsToFront($operations); $operations = $this->moveUninstallsToFront($operations); - // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? /* if ('update' === $jobType) { @@ -175,7 +176,7 @@ class LocalRepoTransaction { $uninstOps = array(); foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation) { + if ($op instanceof UninstallOperation || $op instanceof MarkAliasUninstalledOperation) { $uninstOps[] = $op; unset($operations[$idx]); } diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index e113988bd..63214be9d 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -78,8 +78,8 @@ class LockTransaction $operations[] = new Operation\UpdateOperation($lockMeansUpdateMap[abs($literal)], $package, $reason); // avoid updates to one package from multiple origins + $ignoreRemove[$lockMeansUpdateMap[abs($literal)]->id] = true; unset($lockMeansUpdateMap[abs($literal)]); - $ignoreRemove[$source->id] = true; } else { if ($package instanceof AliasPackage) { $operations[] = new Operation\MarkAliasInstalledOperation($package, $reason); diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 166ebbe07..54adea9b5 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -90,7 +90,7 @@ class Request $presentMap = array(); if ($this->lockedRepository) { - foreach ($this->lockedRepository as $package) { + foreach ($this->lockedRepository->getPackages() as $package) { $presentMap[$package->id] = $package; } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index a59a4dc4c..791c7bcfd 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -351,7 +351,7 @@ class Installer } // TODO can we drop any locked packages that we have matching remote versions for? - $request = $this->createRequest($this->fixedRootPackage, $platformRepo); + $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); if ($lockedRepository) { // TODO do we really always need this? Maybe only to skip fix() in updateWhitelist case cause these packages get removed on full update automatically? @@ -422,23 +422,6 @@ class Installer $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); - $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockNonDevPackages(), - $lockTransaction->getNewLockDevPackages(), - $platformReqs, - $platformDevReqs, - $aliases, - $this->package->getMinimumStability(), - $this->package->getStabilityFlags(), - $this->preferStable || $this->package->getPreferStable(), - $this->preferLowest, - $this->config->get('platform') ?: array(), - $this->writeLock && $this->executeOperations - ); - if ($updatedLock && $this->writeLock && $this->executeOperations) { - $this->io->writeError('Writing lock file'); - } - if ($lockTransaction->getOperations()) { $installs = $updates = $uninstalls = array(); foreach ($lockTransaction->getOperations() as $operation) { @@ -501,7 +484,22 @@ class Installer } } - $this->io->write('foo'); + $updatedLock = $this->locker->setLockData( + $lockTransaction->getNewLockNonDevPackages(), + $lockTransaction->getNewLockDevPackages(), + $platformReqs, + $platformDevReqs, + $aliases, + $this->package->getMinimumStability(), + $this->package->getStabilityFlags(), + $this->preferStable || $this->package->getPreferStable(), + $this->preferLowest, + $this->config->get('platform') ?: array(), + $this->writeLock && $this->executeOperations + ); + if ($updatedLock && $this->writeLock && $this->executeOperations) { + $this->io->writeError('Writing lock file'); + } if ($doInstall) { // TODO ensure lock is used from locker as-is, since it may not have been written to disk in case of executeOperations == false @@ -530,7 +528,7 @@ class Installer $repositorySet = $this->createRepositorySet($platformRepo, $aliases, $lockedRepository); $repositorySet->addRepository($lockedRepository); - $this->io->writeError('Installing dependencies'.($this->devMode ? ' (including require-dev)' : '').' from lock file'); + $this->io->writeError('Installing dependencies from lock file'.($this->devMode ? ' (including require-dev)' : '').''); // verify that the lock file works with the current platform repository // we can skip this part if we're doing this as the second step after an update @@ -538,7 +536,7 @@ class Installer $this->io->writeError('Verifying lock file contents can be installed on current platform.'); // creating requirements request - $request = $this->createRequest($this->fixedRootPackage, $platformRepo); + $request = $this->createRequest($this->fixedRootPackage, $platformRepo, $lockedRepository); if (!$this->locker->isFresh()) { $this->io->writeError('Warning: The lock file is not up to date with the latest changes in composer.json. You may be getting outdated dependencies. Run update to update them.', true, IOInterface::QUIET); @@ -627,7 +625,7 @@ class Installer } // output op, but alias op only in debug verbosity - if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { + if ((!$this->executeOperations && false === strpos($operation->getJobType(), 'Alias')) || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation); } @@ -736,13 +734,14 @@ class Installer } /** - * @param RootPackageInterface $rootPackage - * @param PlatformRepository $platformRepo + * @param RootPackageInterface $rootPackage + * @param PlatformRepository $platformRepo + * @param RepositoryInterface|null $lockedRepository * @return Request */ - private function createRequest(RootPackageInterface $rootPackage, PlatformRepository $platformRepo) + private function createRequest(RootPackageInterface $rootPackage, PlatformRepository $platformRepo, $lockedRepository = null) { - $request = new Request(); + $request = new Request($lockedRepository); $request->fixPackage($rootPackage, false); diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index db4ef23c0..a4bfe6a4d 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -22,7 +22,7 @@ Broken dependencies should not lead to a replacer being installed which is not m install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test index ee221dab0..b387942fb 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test @@ -32,7 +32,7 @@ install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index 877ac3653..878e9429a 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -29,15 +29,37 @@ that are also a root package, when that root package is also explicitly whitelis { "name": "a/a", "version": "1.0.0" }, { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } ] - +--LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "1.0.0" + }, + { + "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } + } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": {}, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update a/a b/b --with-dependencies --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 0 installs, 2 updates, 0 removals +Updating dependencies +Lock file operations: 0 installs, 2 updates, 0 removals + - Updating b/b (1.0.0) to b/b (1.1.0) + - Updating a/a (1.0.0) to a/a (1.1.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index 40546f8d0..e28ef03e5 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -19,8 +19,11 @@ install --no-dev --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies -Package operations: 1 install, 0 updates, 0 removals +Lock file operations: 1 install, 0 updates, 0 removals + - Installing a/a (1.0.0) Writing lock file +Installing dependencies from lock file +Package operations: 1 install, 0 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index ae5ff36e3..ab22eeb6e 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -18,10 +18,13 @@ Suggestions are displayed install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Installing a/a (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals a/a suggests installing b/b (an obscure reason) -Writing lock file Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test index 04624561d..6575530d0 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-from-lock-removes-old-deps.test @@ -16,7 +16,7 @@ Installing locked dev packages should remove old dependencies "require": {} } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], From b700aa3d622895a79a7334b751b462c28a72b385 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 06:03:39 +0200 Subject: [PATCH 06/37] Sort local repo transaction as topological as possible --- .../LocalRepoTransaction.php | 100 +++++++++++++++++- tests/Composer/Test/InstallerTest.php | 2 +- 2 files changed, 98 insertions(+), 4 deletions(-) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index db224989d..8a38248f7 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -72,8 +72,6 @@ class LocalRepoTransaction $operations[] = new Operation\InstallOperation($package); unset($removeMap[$package->getName()]); } - - /* if (isset($lockedPackages[$package->getName()])) { die("Alias?"); @@ -85,7 +83,10 @@ class LocalRepoTransaction $operations[] = new Operation\UninstallOperation($package, null); } + $operations = $this->sortOperations($operations); $operations = $this->movePluginsToFront($operations); + // TODO fix this: + // we have to do this again here even though sortOperations did it because moving plugins moves them before uninstalls $operations = $this->moveUninstallsToFront($operations); // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? @@ -109,6 +110,99 @@ class LocalRepoTransaction return $operations; } + // TODO is there a more efficient / better way to do get a "good" install order? + public function sortOperations(array $operations) + { + $packageQueue = $this->lockedRepository->getPackages(); + + $packageQueue[] = null; // null is a cycle marker + + $weights = array(); + $foundWeighables = false; + + // This is sort of a topological sort, the weight represents the distance from a leaf (1 == is leaf) + // Since we can have cycles in the dep graph, any node which doesn't have an acyclic connection to all + // leaves it's connected to, cannot be assigned a weight and will be unsorted + while (!empty($packageQueue)) { + $package = array_shift($packageQueue); + + // one full cycle + if ($package === null) { + // if we were able to assign some weights, keep going + if ($foundWeighables) { + $foundWeighables = false; + $packageQueue[] = null; + continue; + } else { + foreach ($packageQueue as $package) { + $weights[$package->getName()] = PHP_INT_MAX; + } + // no point in continuing, we are in a cycle + break; + } + } + + $requires = array_filter(array_keys($package->getRequires()), function ($req) { + return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + }); + + $maxWeight = 0; + foreach ($requires as $require) { + if (!isset($weights[$require])) { + $maxWeight = null; + + // needs more calculation, so add to end of queue + $packageQueue[] = $package; + break; + } + + $maxWeight = max((int) $maxWeight, $weights[$require]); + } + if ($maxWeight !== null) { + $foundWeighables = true; + $weights[$package->getName()] = $maxWeight + 1; + } + } + + // TODO do we have any alias ops in the local repo transaction? + usort($operations, function ($opA, $opB) use ($weights) { + // uninstalls come first, if there are multiple, sort by name + if ($opA instanceof Operation\UninstallOperation) { + $packageA = $opA->getPackage(); + if ($opB instanceof Operation\UninstallOperation) { + return strcmp($packageA->getName(), $opB->getPackage()->getName()); + } + return -1; + } elseif ($opB instanceof Operation\UninstallOperation) { + return 1; + } + + + if ($opA instanceof Operation\InstallOperation) { + $packageA = $opA->getPackage(); + } elseif ($opA instanceof Operation\UpdateOperation) { + $packageA = $opA->getTargetPackage(); + } + + if ($opB instanceof Operation\InstallOperation) { + $packageB = $opB->getPackage(); + } elseif ($opB instanceof Operation\UpdateOperation) { + $packageB = $opB->getTargetPackage(); + } + + $weightA = $weights[$packageA->getName()]; + $weightB = $weights[$packageB->getName()]; + + if ($weightA === $weightB) { + return strcmp($packageA->getName(), $packageB->getName()); + } else { + return $weightA < $weightB ? -1 : 1; + } + }); + + return $operations; + } + /** * Workaround: if your packages depend on plugins, we must be sure * that those are installed / updated first; else it would lead to packages @@ -176,7 +270,7 @@ class LocalRepoTransaction { $uninstOps = array(); foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation || $op instanceof MarkAliasUninstalledOperation) { + if ($op instanceof UninstallOperation) { $uninstOps[] = $op; unset($operations[$idx]); } diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 89385c288..ce87d111f 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -97,7 +97,7 @@ class InstallerTest extends TestCase })); $tempLockData = null; - $locker = new Locker($io, $lockJsonMock, $repositoryManager, $installationManager, '{}'); + $locker = new Locker($io, $lockJsonMock, $installationManager, '{}'); $autoloadGenerator = $this->getMockBuilder('Composer\Autoload\AutoloadGenerator')->disableOriginalConstructor()->getMock(); From 4db325b7b463fa8053f05731878b0eab3d989ecb Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:13:34 +0200 Subject: [PATCH 07/37] Use the old root stack based approach to sorting operations in the transaction --- .../LocalRepoTransaction.php | 233 +++++++++--------- src/Composer/Installer.php | 6 + 2 files changed, 128 insertions(+), 111 deletions(-) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index 8a38248f7..c115c9e0c 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -15,28 +15,54 @@ namespace Composer\DependencyResolver; use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\Package\AliasPackage; +use Composer\Package\Link; +use Composer\Package\PackageInterface; use Composer\Repository\PlatformRepository; use Composer\Repository\RepositoryInterface; +use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann */ class LocalRepoTransaction { - /** @var RepositoryInterface */ - protected $lockedRepository; + /** @var array */ + protected $lockedPackages; + protected $lockedPackagesByName = array(); /** @var RepositoryInterface */ protected $localRepository; - public function __construct($lockedRepository, $localRepository) + public function __construct(RepositoryInterface $lockedRepository, $localRepository) { - $this->lockedRepository = $lockedRepository; $this->localRepository = $localRepository; - + $this->setLockedPackageMaps($lockedRepository); $this->operations = $this->calculateOperations(); } + private function setLockedPackageMaps($lockedRepository) + { + $packageSort = function (PackageInterface $a, PackageInterface $b) { + // sort alias packages by the same name behind their non alias version + if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) { + return $a instanceof AliasPackage ? -1 : 1; + } + return strcmp($b->getName(), $a->getName()); + }; + + foreach ($lockedRepository->getPackages() as $package) { + $this->lockedPackages[$package->id] = $package; + foreach ($package->getNames() as $name) { + $this->lockedPackagesByName[$name][] = $package; + } + } + + uasort($this->lockedPackages, $packageSort); + foreach ($this->lockedPackagesByName as $name => $packages) { + uasort($this->lockedPackagesByName[$name], $packageSort); + } + } + public function getOperations() { return $this->operations; @@ -48,45 +74,84 @@ class LocalRepoTransaction $localPackageMap = array(); $removeMap = array(); + $localAliasMap = array(); + $removeAliasMap = array(); foreach ($this->localRepository->getPackages() as $package) { - if (isset($localPackageMap[$package->getName()])) { - die("Alias?"); + if ($package instanceof AliasPackage) { + $localAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + } else { + $localPackageMap[$package->getName()] = $package; + $removeMap[$package->getName()] = $package; } - $localPackageMap[$package->getName()] = $package; - $removeMap[$package->getName()] = $package; } - $lockedPackages = array(); - foreach ($this->lockedRepository->getPackages() as $package) { - if (isset($localPackageMap[$package->getName()])) { - $source = $localPackageMap[$package->getName()]; + $stack = $this->getRootPackages(); - // do we need to update? - if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { - $operations[] = new Operation\UpdateOperation($source, $package); - } elseif ($package->isDev() && $package->getSourceReference() !== $localPackageMap[$package->getName()]->getSourceReference()) { - $operations[] = new Operation\UpdateOperation($source, $package); + $visited = array(); + $processed = array(); + + while (!empty($stack)) { + $package = array_pop($stack); + + if (isset($processed[$package->id])) { + continue; + } + + if (!isset($visited[$package->id])) { + $visited[$package->id] = true; + + $stack[] = $package; + if ($package instanceof AliasPackage) { + $stack[] = $package->getAliasOf(); + } else { + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->getLockedProviders($link); + + foreach ($possibleRequires as $require) { + $stack[] = $require; + } + } + } + } elseif (!isset($processed[$package->id])) { + $processed[$package->id] = true; + + if ($package instanceof AliasPackage) { + $aliasKey = $package->getName().'::'.$package->getVersion(); + if (isset($localAliasMap[$aliasKey])) { + unset($removeAliasMap[$aliasKey]); + } else { + $operations[] = new Operation\MarkAliasInstalledOperation($package); + } + } else { + if (isset($localPackageMap[$package->getName()])) { + $source = $localPackageMap[$package->getName()]; + + // do we need to update? + if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } elseif ($package->isDev() && $package->getSourceReference() !== $localPackageMap[$package->getName()]->getSourceReference()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } + unset($removeMap[$package->getName()]); + } else { + $operations[] = new Operation\InstallOperation($package); + unset($removeMap[$package->getName()]); + } } - unset($removeMap[$package->getName()]); - } else { - $operations[] = new Operation\InstallOperation($package); - unset($removeMap[$package->getName()]); } -/* - if (isset($lockedPackages[$package->getName()])) { - die("Alias?"); - } - $lockedPackages[$package->getName()] = $package;*/ } foreach ($removeMap as $name => $package) { - $operations[] = new Operation\UninstallOperation($package, null); + array_unshift($operations, new Operation\UninstallOperation($package, null)); + } + foreach ($removeAliasMap as $nameVersion => $package) { + $operations[] = new Operation\MarkAliasUninstalledOperation($package, null); } - $operations = $this->sortOperations($operations); $operations = $this->movePluginsToFront($operations); // TODO fix this: - // we have to do this again here even though sortOperations did it because moving plugins moves them before uninstalls + // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls $operations = $this->moveUninstallsToFront($operations); // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? @@ -110,97 +175,43 @@ class LocalRepoTransaction return $operations; } - // TODO is there a more efficient / better way to do get a "good" install order? - public function sortOperations(array $operations) + /** + * Determine which packages in the lock file are not required by any other packages in the lock file. + * + * These serve as a starting point to enumerate packages in a topological order despite potential cycles. + * If there are packages with a cycle on the top level the package with the lowest name gets picked + * + * @return array + */ + private function getRootPackages() { - $packageQueue = $this->lockedRepository->getPackages(); + $roots = $this->lockedPackages; - $packageQueue[] = null; // null is a cycle marker + foreach ($this->lockedPackages as $packageId => $package) { + if (!isset($roots[$packageId])) { + continue; + } - $weights = array(); - $foundWeighables = false; + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->getLockedProviders($link); - // This is sort of a topological sort, the weight represents the distance from a leaf (1 == is leaf) - // Since we can have cycles in the dep graph, any node which doesn't have an acyclic connection to all - // leaves it's connected to, cannot be assigned a weight and will be unsorted - while (!empty($packageQueue)) { - $package = array_shift($packageQueue); - - // one full cycle - if ($package === null) { - // if we were able to assign some weights, keep going - if ($foundWeighables) { - $foundWeighables = false; - $packageQueue[] = null; - continue; - } else { - foreach ($packageQueue as $package) { - $weights[$package->getName()] = PHP_INT_MAX; + foreach ($possibleRequires as $require) { + if ($require !== $package) { + unset($roots[$require->id]); } - // no point in continuing, we are in a cycle - break; } } - - $requires = array_filter(array_keys($package->getRequires()), function ($req) { - return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); - }); - - $maxWeight = 0; - foreach ($requires as $require) { - if (!isset($weights[$require])) { - $maxWeight = null; - - // needs more calculation, so add to end of queue - $packageQueue[] = $package; - break; - } - - $maxWeight = max((int) $maxWeight, $weights[$require]); - } - if ($maxWeight !== null) { - $foundWeighables = true; - $weights[$package->getName()] = $maxWeight + 1; - } } - // TODO do we have any alias ops in the local repo transaction? - usort($operations, function ($opA, $opB) use ($weights) { - // uninstalls come first, if there are multiple, sort by name - if ($opA instanceof Operation\UninstallOperation) { - $packageA = $opA->getPackage(); - if ($opB instanceof Operation\UninstallOperation) { - return strcmp($packageA->getName(), $opB->getPackage()->getName()); - } - return -1; - } elseif ($opB instanceof Operation\UninstallOperation) { - return 1; - } + return $roots; + } - - if ($opA instanceof Operation\InstallOperation) { - $packageA = $opA->getPackage(); - } elseif ($opA instanceof Operation\UpdateOperation) { - $packageA = $opA->getTargetPackage(); - } - - if ($opB instanceof Operation\InstallOperation) { - $packageB = $opB->getPackage(); - } elseif ($opB instanceof Operation\UpdateOperation) { - $packageB = $opB->getTargetPackage(); - } - - $weightA = $weights[$packageA->getName()]; - $weightB = $weights[$packageB->getName()]; - - if ($weightA === $weightB) { - return strcmp($packageA->getName(), $packageB->getName()); - } else { - return $weightA < $weightB ? -1 : 1; - } - }); - - return $operations; + private function getLockedProviders(Link $link) + { + if (!isset($this->lockedPackagesByName[$link->getTarget()])) { + return array(); + } + return $this->lockedPackagesByName[$link->getTarget()]; } /** diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 8c1e9a801..9f5de754b 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -575,6 +575,12 @@ class Installer // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); + } else { + // we still need to ensure all packages have an id for correct functionality + $id = 1; + foreach ($lockedRepository->getPackages() as $package) { + $package->id = $id++; + } } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? From c16ab6174e8e394e710df9e18d03f995fb7a2330 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:16:43 +0200 Subject: [PATCH 08/37] Fix tests: Alias install now always right after origin package, partial update requires lock file --- ...pdate-installs-from-lock-even-missing.test | 4 ++-- ...elist-patterns-with-root-dependencies.test | 22 +++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test index 9a6f0ab9e..ea4a7c997 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-installs-from-lock-even-missing.test @@ -98,8 +98,8 @@ update b/b } --EXPECT-- Updating a/a (dev-master oldmaster-a) to a/a (dev-master newmaster-a) -Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2) Marking a/a (2.2.x-dev newmaster-a) as installed, alias of a/a (dev-master newmaster-a) +Updating b/b (dev-master oldmaster-b) to b/b (dev-master newmaster-b2) Marking b/b (2.3.x-dev newmaster-b2) as installed, alias of b/b (dev-master newmaster-b2) -Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) Marking a/a (2.1.x-dev oldmaster-a) as uninstalled, alias of a/a (dev-master oldmaster-a) +Marking b/b (2.1.x-dev oldmaster-b) as uninstalled, alias of b/b (dev-master oldmaster-b) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test index a24bafb91..fe0cb1e31 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test @@ -47,6 +47,28 @@ Update with a package whitelist only updates those packages and their dependenci { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } }, + { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } }, + { "name": "whitelisted-component4", "version": "1.0.0" }, + { "name": "whitelisted-component5", "version": "1.0.0" }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update whitelisted-* --with-dependencies --EXPECT-- From 3e0e5dc1fab534fd0e139208faea32b4a4e4660a Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:17:41 +0200 Subject: [PATCH 09/37] Fix test expectation: Install and update operations are now alphabetical --- .../Composer/Test/Fixtures/installer/circular-dependency2.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/Fixtures/installer/circular-dependency2.test b/tests/Composer/Test/Fixtures/installer/circular-dependency2.test index c89beef6b..6024c17f2 100644 --- a/tests/Composer/Test/Fixtures/installer/circular-dependency2.test +++ b/tests/Composer/Test/Fixtures/installer/circular-dependency2.test @@ -32,5 +32,5 @@ Circular dependencies are possible between packages --RUN-- update -v --EXPECT-- -Installing require/itself (1.0.0) Installing regular/pkg (1.0.0) +Installing require/itself (1.0.0) From a114e26841b5bec32576c7e42cbaa7c1de7040a6 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:43:23 +0200 Subject: [PATCH 10/37] Correctly load aliases in lockedRepository to fix alias install output --- src/Composer/Package/Locker.php | 13 ++++++- .../aliased-priority-conflicting.test | 39 +++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 7aad4d318..d790837a5 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -171,8 +171,19 @@ class Locker } if (isset($lockedPackages[0]['name'])) { + $packageByName = array(); foreach ($lockedPackages as $info) { - $packages->addPackage($this->loader->load($info)); + $package = $this->loader->load($info); + $packages->addPackage($package); + $packageByName[$package->getName()] = $package; + } + + if (isset($lockData['aliases'])) { + foreach ($lockData['aliases'] as $alias) { + if (isset($packageByName[$alias['package']])) { + $packages->addPackage(new AliasPackage($packageByName[$alias['package']], $alias['alias_normalized'], $alias['alias'])); + } + } } return $packages; diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test index cda5d31d3..8d928c20d 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority-conflicting.test @@ -43,6 +43,45 @@ Aliases take precedence over default package even if default is selected }, "minimum-stability": "dev" } +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/a", "version": "dev-master", + "require": { "a/req": "dev-master" }, + "type": "library" + }, + { + "name": "a/b", "version": "dev-master", + "require": { "a/req": "dev-master" }, + "type": "library" + }, + { + "name": "a/req", "version": "dev-feature-foo", + "source": { "reference": "feat.f", "type": "git", "url": "" }, + "type": "library" + } + ], + "packages-dev": [], + "aliases": [ + { + "alias": "dev-master", + "alias_normalized": "9999999-dev", + "version": "dev-feature-foo", + "package": "a/req" + } + ], + "minimum-stability": "dev", + "stability-flags": { + "a/a": 20, + "a/b": 20, + "a/req": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- install --EXPECT-- From 33ff67abf3fda8d870208fff2ef4cf43698599db Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 07:53:31 +0200 Subject: [PATCH 11/37] Update tests for new output and for required lock file on partial update --- .../Fixtures/installer/aliased-priority.test | 2 +- .../Fixtures/installer/suggest-installed.test | 8 ++++++-- .../update-whitelist-locked-require.test | 17 +++++++++++++++++ ...telist-patterns-with-all-dependencies.test | 19 +++++++++++++++++++ .../update-whitelist-removes-unused.test | 16 ++++++++++++++++ .../Fixtures/installer/update-whitelist.test | 18 ++++++++++++++++++ 6 files changed, 77 insertions(+), 3 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/aliased-priority.test b/tests/Composer/Test/Fixtures/installer/aliased-priority.test index 97ffe5521..8dd0e8470 100644 --- a/tests/Composer/Test/Fixtures/installer/aliased-priority.test +++ b/tests/Composer/Test/Fixtures/installer/aliased-priority.test @@ -51,6 +51,6 @@ install Installing a/c (dev-feature-foo feat.f) Marking a/c (dev-master feat.f) as installed, alias of a/c (dev-feature-foo feat.f) Installing a/b (dev-master forked) +Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked) Installing a/a (dev-master master) Marking a/a (1.0.x-dev master) as installed, alias of a/a (dev-master master) -Marking a/b (1.0.x-dev forked) as installed, alias of a/b (dev-master forked) diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index 198203ce9..468a53612 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -20,9 +20,13 @@ Suggestions are not displayed for installed packages install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 2 installs, 0 updates, 0 removals +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Installing b/b (1.0.0) + - Installing a/a (1.0.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test index 381416af1..c3b20426e 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test @@ -29,6 +29,23 @@ Update with a package whitelist only updates those packages if they are not pres { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, { "name": "fixed-sub-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, + { "name": "fixed-sub-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update whitelisted dependency --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test index 8ea177cad..b3396b376 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test @@ -38,6 +38,25 @@ Update with a package whitelist pattern and all-dependencies flag updates packag { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted-component1", "version": "1.0.0" }, + { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update whitelisted-* --with-all-dependencies --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test index e658e8c06..3b28e4671 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test @@ -25,6 +25,22 @@ Update with a package whitelist removes unused packages { "name": "fixed-dependency", "version": "1.0.0" }, { "name": "old-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, + { "name": "fixed-dependency", "version": "1.0.0" }, + { "name": "old-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update --with-dependencies whitelisted --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test index 751d79e70..b53954dc6 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -33,6 +33,24 @@ Update with a package whitelist only updates those packages listed as command ar { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update whitelisted --EXPECT-- From 3989a1b8ee387b4bc38df74933f90c5e211d8cb7 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 08:52:10 +0200 Subject: [PATCH 12/37] Restore dev package extraction New approach is to use only the solved set of packages as input and then to resolve with only the non-dev requirements and to mark everything as dev that is not part of the result set, rather than transitioning a temporary local repo state by uninstalling dev packages. --- .../LocalRepoTransaction.php | 6 + .../DependencyResolver/LockTransaction.php | 46 ++++-- src/Composer/Installer.php | 133 ++++++++++++++++-- .../update-no-dev-still-resolves-dev.test | 2 +- 4 files changed, 168 insertions(+), 19 deletions(-) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index c115c9e0c..a41798b50 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -33,6 +33,9 @@ class LocalRepoTransaction /** @var RepositoryInterface */ protected $localRepository; + /** + * Reassigns ids for all packages in the lockedrepository + */ public function __construct(RepositoryInterface $lockedRepository, $localRepository) { $this->localRepository = $localRepository; @@ -50,7 +53,10 @@ class LocalRepoTransaction return strcmp($b->getName(), $a->getName()); }; + $id = 1; + $this->lockedPackages = array(); foreach ($lockedRepository->getPackages() as $package) { + $package->id = $id++; $this->lockedPackages[$package->id] = $package; foreach ($package->getNames() as $name) { $this->lockedPackagesByName[$name][] = $package; diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 63214be9d..3d5204067 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -16,7 +16,9 @@ use Composer\DependencyResolver\Operation\OperationInterface; use Composer\Package\AliasPackage; use Composer\Package\RootAliasPackage; use Composer\Package\RootPackageInterface; +use Composer\Repository\ArrayRepository; use Composer\Repository\RepositoryInterface; +use Composer\Test\Repository\ArrayRepositoryTest; /** * @author Nils Adermann @@ -40,7 +42,8 @@ class LockTransaction protected $unlockableMap; protected $decisions; - protected $transaction; + + protected $resultPackages; public function __construct($policy, $pool, $presentMap, $unlockableMap, $decisions) { @@ -104,31 +107,54 @@ class LockTransaction } } + $this->setResultPackages(); + return $operations; } - // TODO additionalFixedRepository needs to be looked at here as well? - public function getNewLockNonDevPackages() + // TODO make this a bit prettier instead of the two text indexes? + public function setResultPackages() { - $packages = array(); + $this->resultPackages = array('non-dev' => array(), 'dev' => array()); foreach ($this->decisions as $i => $decision) { $literal = $decision[Decisions::DECISION_LITERAL]; if ($literal > 0) { $package = $this->pool->literalToPackage($literal); - if (!isset($this->unlockableMap[$package->id]) && !($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { - $packages[] = $package; + if (!isset($this->unlockableMap[$package->id])) { + $this->resultPackages['non-dev'][] = $package; } } } - - return $packages; } - public function getNewLockDevPackages() + public function setNonDevPackages(LockTransaction $extractionResult) + { + $packages = $extractionResult->getNewLockPackages(false); + + $this->resultPackages['dev'] = $this->resultPackages['non-dev']; + $this->resultPackages['non-dev'] = array(); + + foreach ($packages as $package) { + foreach ($this->resultPackages['dev'] as $i => $resultPackage) { + if ($package->getName() == $resultPackage->getName()) { + $this->resultPackages['non-dev'][] = $resultPackage; + unset($this->resultPackages['dev'][$i]); + } + } + } + } + + // TODO additionalFixedRepository needs to be looked at here as well? + public function getNewLockPackages($devMode) { - // TODO this is empty? $packages = array(); + foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { + if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { + $packages[] = $package; + } + } + return $packages; } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 9f5de754b..6c94df10e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -15,6 +15,7 @@ namespace Composer; use Composer\Autoload\AutoloadGenerator; use Composer\DependencyResolver\DefaultPolicy; use Composer\DependencyResolver\LocalRepoTransaction; +use Composer\DependencyResolver\LockTransaction; use Composer\DependencyResolver\Operation\UpdateOperation; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; @@ -40,6 +41,7 @@ use Composer\Package\Link; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Package; +use Composer\Repository\ArrayRepository; use Composer\Repository\RepositorySet; use Composer\Semver\Constraint\Constraint; use Composer\Package\Locker; @@ -418,6 +420,8 @@ class Installer $this->io->writeError('Nothing to modify in lock file'); } + $this->extractDevPackages($lockTransaction, $platformRepo, $aliases, $policy); + // write lock $platformReqs = $this->extractPlatformRequirements($this->package->getRequires()); $platformDevReqs = $this->extractPlatformRequirements($this->package->getDevRequires()); @@ -485,8 +489,8 @@ class Installer } $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockNonDevPackages(), - $lockTransaction->getNewLockDevPackages(), + $lockTransaction->getNewLockPackages(false), + $lockTransaction->getNewLockPackages(true), $platformReqs, $platformDevReqs, $aliases, @@ -509,6 +513,124 @@ class Installer return 0; } + /** + * Run the solver a second time on top of the existing update result with only the current result set in the pool + * and see what packages would get removed if we only had the non-dev packages in the solver request + */ + protected function extractDevPackages(LockTransaction $lockTransaction, $platformRepo, $aliases, $policy) + { + if (!$this->package->getDevRequires()) { + return array(); + } + + ; + + $resultRepo = new ArrayRepository(array()); + $loader = new ArrayLoader(null, true); + $dumper = new ArrayDumper(); + foreach ($lockTransaction->getNewLockPackages(false) as $pkg) { + $resultRepo->addPackage($loader->load($dumper->dump($pkg))); + } + + $repositorySet = $this->createRepositorySet($platformRepo, $aliases, null); + $repositorySet->addRepository($resultRepo); + + $request = $this->createRequest($this->fixedRootPackage, $platformRepo, null); + + $links = $this->package->getRequires(); + foreach ($links as $link) { + $request->install($link->getTarget(), $link->getConstraint()); + } + + $pool = $repositorySet->createPool($request); + + // solve dependencies + $solver = new Solver($policy, $pool, $this->io); + try { + $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); + $solver = null; + } catch (SolverProblemsException $e) { + // TODO change info message here + $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError($e->getMessage()); + + return max(1, $e->getCode()); + } + + $lockTransaction->setNonDevPackages($nonDevLockTransaction); + } + + + // TODO add proper output and events to above function based on old version below + /** + * Extracts the dev packages out of the localRepo + * + * This works by faking the operations so we can see what the dev packages + * would be at the end of the operation execution. This lets us then remove + * the dev packages from the list of operations accordingly if we are in a + * --no-dev install or update. + * + * @return array + private function extractDevPackages(array $operations, RepositoryInterface $localRepo, PlatformRepository $platformRepo, array $aliases) + { + // fake-apply all operations to this clone of the local repo so we see the complete set of package we would end up with + $tempLocalRepo = clone $localRepo; + foreach ($operations as $operation) { + switch ($operation->getJobType()) { + case 'install': + case 'markAliasInstalled': + if (!$tempLocalRepo->hasPackage($operation->getPackage())) { + $tempLocalRepo->addPackage(clone $operation->getPackage()); + } + break; + case 'uninstall': + case 'markAliasUninstalled': + $tempLocalRepo->removePackage($operation->getPackage()); + break; + case 'update': + $tempLocalRepo->removePackage($operation->getInitialPackage()); + if (!$tempLocalRepo->hasPackage($operation->getTargetPackage())) { + $tempLocalRepo->addPackage(clone $operation->getTargetPackage()); + } + break; + default: + throw new \LogicException('Unknown type: '.$operation->getJobType()); + } + } + // we have to reload the local repo to handle aliases properly + // but as it is not persisted on disk we use a loader/dumper + // to reload it in memory + $localRepo = new InstalledArrayRepository(array()); + $loader = new ArrayLoader(null, true); + $dumper = new ArrayDumper(); + foreach ($tempLocalRepo->getCanonicalPackages() as $pkg) { + $localRepo->addPackage($loader->load($dumper->dump($pkg))); + } + unset($tempLocalRepo, $loader, $dumper); + $policy = $this->createPolicy(); + $pool = $this->createPool(); + $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); + $pool->addRepository($installedRepo, $aliases); + // creating requirements request without dev requirements + $request = $this->createRequest($this->package, $platformRepo); + $request->updateAll(); + foreach ($this->package->getRequires() as $link) { + $request->install($link->getTarget(), $link->getConstraint()); + } + // solve deps to see which get removed + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); + $solver = new Solver($policy, $pool, $installedRepo, $this->io); + $ops = $solver->solve($request, $this->ignorePlatformReqs); + $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); + $devPackages = array(); + foreach ($ops as $op) { + if ($op->getJobType() === 'uninstall') { + $devPackages[] = $op->getPackage(); + } + } + return $devPackages; + }*/ + /** * @param RepositoryInterface $localRepo * @param RepositoryInterface $installedRepo @@ -575,12 +697,6 @@ class Installer // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); - } else { - // we still need to ensure all packages have an id for correct functionality - $id = 1; - foreach ($lockedRepository->getPackages() as $package) { - $package->id = $id++; - } } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? @@ -672,6 +788,7 @@ class Installer // TODO what's the point of rootConstraints at all, we generate the package pool taking them into account anyway? // TODO maybe we can drop the lockedRepository here + // TODO if this gets called in doInstall, this->update is still true?! if ($this->update) { $minimumStability = $this->package->getMinimumStability(); $stabilityFlags = $this->package->getStabilityFlags(); diff --git a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test index 3b90755e2..fad73f20a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test +++ b/tests/Composer/Test/Fixtures/installer/update-no-dev-still-resolves-dev.test @@ -62,7 +62,7 @@ update --no-dev --EXPECT-- Uninstalling a/b (1.0.0) Updating a/a (1.0.0) to a/a (1.0.1) -Updating dev/pkg (dev-master old) to dev/pkg (dev-master new) Installing a/c (1.0.0) +Updating dev/pkg (dev-master old) to dev/pkg (dev-master new) Marking dev/pkg (1.1.x-dev new) as installed, alias of dev/pkg (dev-master new) Marking dev/pkg (1.0.x-dev old) as uninstalled, alias of dev/pkg (dev-master old) From 995b4f923e2cf6077abdef1999976e92bb30ac0b Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Sat, 7 Sep 2019 09:18:54 +0200 Subject: [PATCH 13/37] Fix more tests which were lacking lock files for partial updates, display fix jobs in problems --- .../DependencyResolver/LockTransaction.php | 1 + src/Composer/DependencyResolver/Problem.php | 9 ++++++- .../DependencyResolver/RuleSetGenerator.php | 8 ++++++- .../install-from-lock-removes-package.test | 2 +- .../partial-update-without-lock.test | 17 +++++++++++++ .../Fixtures/installer/solver-problems.test | 17 ++++++++++++- .../update-with-all-dependencies.test | 24 ++++++++++++++++--- 7 files changed, 71 insertions(+), 7 deletions(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 3d5204067..a0aa4cd7e 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -137,6 +137,7 @@ class LockTransaction foreach ($packages as $package) { foreach ($this->resultPackages['dev'] as $i => $resultPackage) { + // TODO this comparison is probably insufficient, aliases, what about modified versions? I guess they aren't possible? if ($package->getName() == $resultPackage->getName()) { $this->resultPackages['non-dev'][] = $resultPackage; unset($this->resultPackages['dev'][$i]); diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index e07d4d0c1..2c1c7236a 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -90,7 +90,7 @@ class Problem $packages = array(); } - if ($job && $job['cmd'] === 'install' && empty($packages)) { + if ($job && ($job['cmd'] === 'install' || $job['cmd'] === 'fix') && empty($packages)) { // handle php/hhvm if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { @@ -208,6 +208,13 @@ class Problem $packageName = $job['packageName']; $constraint = $job['constraint']; switch ($job['cmd']) { + case 'fix': + $package = $job['package']; + if ($job['lockable']) { + return 'Package '.$package->getPrettyName().' is locked to version '.$package->getPrettyVersion(); + } else { + return 'Package '.$package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer'; + } case 'install': $packages = $this->pool->whatProvides($packageName, $constraint); if (!$packages) { diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 64cb27b11..0c7350cdd 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -12,9 +12,11 @@ namespace Composer\DependencyResolver; +use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Repository\PlatformRepository; +use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann @@ -254,8 +256,10 @@ class RuleSetGenerator return $impossible; } - protected function addRulesForRequest($request, $ignorePlatformReqs) + protected function addRulesForRequest(Request $request, $ignorePlatformReqs) { + $unlockableMap = $request->getUnlockableMap(); + foreach ($request->getFixedPackages() as $package) { $this->addRulesForPackage($package, $ignorePlatformReqs); @@ -263,6 +267,8 @@ class RuleSetGenerator 'cmd' => 'fix', 'packageName' => $package->getName(), 'constraint' => null, + 'package' => $package, + 'lockable' => !isset($unlockableMap[$package->id]), 'fixed' => true )); $this->addRule(RuleSet::TYPE_JOB, $rule); diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test index 6063abfee..83569bfe7 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -25,7 +25,7 @@ Install from a lock file that deleted a package { "name": "whitelisted", "version": "1.1.0" }, { "name": "fixed-dependency", "version": "1.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test index e931a1f7d..845005649 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test @@ -28,6 +28,23 @@ Partial update without lock file should update everything whitelisted, remove ov { "name": "c/uptodate", "version": "1.0.0" }, { "name": "d/removed", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "a/old", "version": "1.0.0" }, + { "name": "b/unstable", "version": "1.1.0-alpha" }, + { "name": "c/uptodate", "version": "1.0.0" }, + { "name": "d/removed", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update b/unstable --EXPECT-LOCK-- diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 8b1336db2..b6d2180db 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -30,6 +30,21 @@ Test the error output of solver problems. { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} + --RUN-- update unstable/package requirer/pkg dependency/pkg @@ -38,7 +53,7 @@ update unstable/package requirer/pkg dependency/pkg --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 diff --git a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test index c0019e6ca..14d8e13d8 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -28,15 +28,33 @@ When `--with-all-dependencies` is used, Composer\Installer::whitelistUpdateDepen { "name": "a/a", "version": "1.0.0" }, { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } ] - +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update b/b --with-all-dependencies --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 0 installs, 2 updates, 0 removals +Updating dependencies +Lock file operations: 0 installs, 2 updates, 0 removals + - Updating b/b (1.0.0) to b/b (1.1.0) + - Updating a/a (1.0.0) to a/a (1.1.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 2 updates, 0 removals Generating autoload files --EXPECT-- From 4481cc4a8859504b908f2b9a8ece648e5423ffa8 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 29 Oct 2019 22:28:36 +0100 Subject: [PATCH 14/37] Allow an install request for a package name which is already fixed Ensures packages get loaded from locked repo correctly. We may not want to support this particular use-case at all, but for now it fixes the existing test, so we may want to revisit this later. --- src/Composer/DependencyResolver/PoolBuilder.php | 9 +++++++-- src/Composer/DependencyResolver/Problem.php | 6 +++--- src/Composer/DependencyResolver/RuleSetGenerator.php | 4 ++++ src/Composer/DependencyResolver/Solver.php | 1 - src/Composer/Installer.php | 2 -- .../Test/Fixtures/installer/solver-problems.test | 8 ++++---- 6 files changed, 18 insertions(+), 12 deletions(-) diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index f9a7541c3..5376367e2 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -62,8 +62,12 @@ class PoolBuilder foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': - $loadNames[$job['packageName']] = $job['constraint']; - $this->nameConstraints[$job['packageName']] = $job['constraint'] ? new MultiConstraint(array($job['constraint']), false) : null; + // TODO currently lock above is always NULL if we adjust that, this needs to merge constraints + // TODO does it really make sense that we can have install requests for the same package that is actively locked with non-matching constraints? + // also see the solver-problems.test test case + $constraint = array_key_exists($job['packageName'], $loadNames) ? null : $job['constraint']; + $loadNames[$job['packageName']] = $constraint; + $this->nameConstraints[$job['packageName']] = $constraint ? new MultiConstraint(array($job['constraint']), false) : null; break; } } @@ -199,6 +203,7 @@ class PoolBuilder // TODO addConstraint function? $this->nameConstraints[$require] = new MultiConstraint(array_merge(array($linkConstraint), $this->nameConstraints[$require]->getConstraints()), false); } + // else it is null and should stay null } else { $this->nameConstraints[$require] = null; } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 2c1c7236a..bc3f2de04 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -211,10 +211,10 @@ class Problem case 'fix': $package = $job['package']; if ($job['lockable']) { - return 'Package '.$package->getPrettyName().' is locked to version '.$package->getPrettyVersion(); - } else { - return 'Package '.$package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer'; + return $package->getPrettyName().' is locked to version '.$package->getPrettyVersion().' and an update of this package was not requested.'; } + + return $package->getPrettyName().' is present at version '.$package->getPrettyVersion() . ' and cannot be modified by Composer'; case 'install': $packages = $this->pool->whatProvides($packageName, $constraint); if (!$packages) { diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index 0c7350cdd..d42617a7a 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -261,6 +261,10 @@ class RuleSetGenerator $unlockableMap = $request->getUnlockableMap(); foreach ($request->getFixedPackages() as $package) { + if ($package->id == -1) { + throw new \RuntimeException("Fixed package ".$package->getName()." was not added to solver pool."); + } + $this->addRulesForPackage($package, $ignorePlatformReqs); $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_JOB_INSTALL, array( diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 348b17c2b..2abcf5d8a 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -147,7 +147,6 @@ class Solver if (abs($literal) !== abs($assertRuleLiteral)) { continue; } - $problem->addRule($assertRule); $this->disableProblem($assertRule); } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 6c94df10e..dea1e03e5 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -523,8 +523,6 @@ class Installer return array(); } - ; - $resultRepo = new ArrayRepository(array()); $loader = new ArrayLoader(null, true); $dumper = new ArrayDumper(); diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index b6d2180db..57d3ced92 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -61,12 +61,12 @@ Your requirements could not be resolved to an installable set of packages. Problem 2 - The requested package bogus/pkg could not be found in any version, there may be a typo in the package name. Problem 3 - - The requested package stable-requiree-excluded/pkg 1.0.1 exists as stable-requiree-excluded/pkg[1.0.0] but these are rejected by your constraint. - Problem 4 - - The requested package stable-requiree-excluded/pkg (installed at 1.0.0, required as 1.0.1) is satisfiable by stable-requiree-excluded/pkg[1.0.0] but these conflict with your requirements or minimum-stability. - Problem 5 - Installation request for requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found. + Problem 4 + - stable-requiree-excluded/pkg is locked to version 1.0.0 and an update of this package was not requested. + - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.0, 1.0.1]. + - Installation request for stable-requiree-excluded/pkg 1.0.1 -> satisfiable by stable-requiree-excluded/pkg[1.0.1]. Potential causes: - A typo in the package name From 5c129be5e730e29522bc942b83fc200600c9be90 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 29 Oct 2019 23:12:54 +0100 Subject: [PATCH 15/37] Partial updates without a lock file are no longer possible, update test --- .../partial-update-without-lock.test | 41 +++---------------- 1 file changed, 5 insertions(+), 36 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test index 845005649..74007af7b 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test @@ -1,5 +1,5 @@ --TEST-- -Partial update without lock file should update everything whitelisted, remove overly unstable packages +Partial update without lock file should error --COMPOSER-- { "repositories": [ @@ -28,41 +28,10 @@ Partial update without lock file should update everything whitelisted, remove ov { "name": "c/uptodate", "version": "1.0.0" }, { "name": "d/removed", "version": "1.0.0" } ] ---LOCK-- -{ - "packages": [ - { "name": "a/old", "version": "1.0.0" }, - { "name": "b/unstable", "version": "1.1.0-alpha" }, - { "name": "c/uptodate", "version": "1.0.0" }, - { "name": "d/removed", "version": "1.0.0" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} --RUN-- update b/unstable ---EXPECT-LOCK-- -{ - "packages": [ - { "name": "a/old", "version": "1.0.0", "type": "library" }, - { "name": "b/unstable", "version": "1.0.0", "type": "library" }, - { "name": "c/uptodate", "version": "1.0.0", "type": "library" }, - { "name": "d/removed", "version": "1.0.0", "type": "library" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "stable", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false, - "platform": [], - "platform-dev": [] -} +--EXPECT-OUTPUT-- +Cannot update only a partial set of packages without a lock file present. +--EXPECT-EXIT-CODE-- +1 --EXPECT-- -Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) From 48ae45e5fe8da00aaf4aef5ab9bf9eacccfe5561 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Tue, 29 Oct 2019 23:16:38 +0100 Subject: [PATCH 16/37] Correct github issue test to include a lock file, still fails because of real bug now --- .../Fixtures/installer/github-issues-4795.test | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index 1f4b1af27..4afac72aa 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -30,13 +30,28 @@ dependency of one the requirements that is whitelisted for update. { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } ] +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0", "require": { "a/a": "~1.0" } } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update b/b --with-dependencies --EXPECT-OUTPUT-- Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring. Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies Nothing to install or update Writing lock file Generating autoload files From 0ff07015a1c0951be489ab46a66c093c714fd14c Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 30 Oct 2019 00:24:25 +0100 Subject: [PATCH 17/37] Only load package info from lock file for fixed packages As a result some lock file packages are no longer in the pool, so the former installed map, now present map cannot use package ids anymore Need to revisit some more code later to simplify this, todo notes left --- .../DependencyResolver/LockTransaction.php | 47 +++++++++++++------ .../DependencyResolver/PoolBuilder.php | 18 ++++++- src/Composer/DependencyResolver/Problem.php | 2 +- src/Composer/DependencyResolver/Request.php | 11 +++-- src/Composer/DependencyResolver/Solver.php | 2 +- .../installer/github-issues-4795.test | 4 +- .../Fixtures/installer/solver-problems.test | 2 +- 7 files changed, 61 insertions(+), 25 deletions(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index a0aa4cd7e..75b2efb5b 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -76,9 +76,14 @@ class LockTransaction $package = $this->pool->literalToPackage($literal); // wanted & !present - if ($literal > 0 && !isset($this->presentMap[$package->id])) { + if ($literal > 0 && !isset($this->presentMap[spl_object_hash($package)])) { if (isset($lockMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { - $operations[] = new Operation\UpdateOperation($lockMeansUpdateMap[abs($literal)], $package, $reason); + // TODO we end up here sometimes because we prefer the remote package now to get up to date metadata + // TODO define some level of identity here for what constitutes an update and what can be ignored? new kind of metadata only update? + $target = $lockMeansUpdateMap[abs($literal)]; + if ($package->getName() !== $target->getName() || $package->getVersion() !== $target->getVersion()) { + $operations[] = new Operation\UpdateOperation($target, $package, $reason); + } // avoid updates to one package from multiple origins $ignoreRemove[$lockMeansUpdateMap[abs($literal)]->id] = true; @@ -98,7 +103,7 @@ class LockTransaction $reason = $decision[Decisions::DECISION_REASON]; $package = $this->pool->literalToPackage($literal); - if ($literal <= 0 && isset($this->presentMap[$package->id]) && !isset($ignoreRemove[$package->id])) { + if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)]) && !isset($ignoreRemove[$package->id])) { if ($package instanceof AliasPackage) { $operations[] = new Operation\MarkAliasUninstalledOperation($package, $reason); } else { @@ -163,29 +168,41 @@ class LockTransaction { $lockMeansUpdateMap = array(); + $packages = array(); + foreach ($this->decisions as $i => $decision) { $literal = $decision[Decisions::DECISION_LITERAL]; $package = $this->pool->literalToPackage($literal); + if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)])) { + $packages[spl_object_hash($package)] = $package; + } + } + + // some locked packages are not in the pool and thus, were not decided at all + foreach ($this->presentMap as $package) { + if ($package->id === -1) { + $packages[spl_object_hash($package)] = $package; + } + } + + foreach ($packages as $package) { if ($package instanceof AliasPackage) { continue; } - // !wanted & present - if ($literal <= 0 && isset($this->presentMap[$package->id])) { - // TODO can't we just look at existing rules? - $updates = $this->policy->findUpdatePackages($this->pool, $package); + // TODO can't we just look at existing rules? + $updates = $this->policy->findUpdatePackages($this->pool, $package); - $literals = array($package->id); + $literals = array($package->id); - foreach ($updates as $update) { - $literals[] = $update->id; - } + foreach ($updates as $update) { + $literals[] = $update->id; + } - foreach ($literals as $updateLiteral) { - if ($updateLiteral !== $literal && !isset($lockMeansUpdateMap[$updateLiteral])) { - $lockMeansUpdateMap[$updateLiteral] = $package; - } + foreach ($literals as $updateLiteral) { + if (!isset($lockMeansUpdateMap[$updateLiteral])) { + $lockMeansUpdateMap[$updateLiteral] = $package; } } } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 5376367e2..39ac5098f 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -59,6 +59,7 @@ class PoolBuilder // TODO can actually use very specific constraint $loadNames[$package->getName()] = null; } + foreach ($request->getJobs() as $job) { switch ($job['cmd']) { case 'install': @@ -67,11 +68,24 @@ class PoolBuilder // also see the solver-problems.test test case $constraint = array_key_exists($job['packageName'], $loadNames) ? null : $job['constraint']; $loadNames[$job['packageName']] = $constraint; - $this->nameConstraints[$job['packageName']] = $constraint ? new MultiConstraint(array($job['constraint']), false) : null; + $this->nameConstraints[$job['packageName']] = $constraint ? new MultiConstraint(array($constraint), false) : null; break; } } + // packages from the locked repository only get loaded if they are explicitly fixed + foreach ($repositories as $key => $repository) { + if ($repository === $request->getLockedRepository()) { + foreach ($repository->getPackages() as $lockedPackage) { + foreach ($request->getFixedPackages() as $package) { + if ($package === $lockedPackage) { + $loadNames += $this->loadPackage($request, $package, $key); + } + } + } + } + } + while (!empty($loadNames)) { $loadIds = array(); foreach ($repositories as $key => $repository) { @@ -86,7 +100,7 @@ class PoolBuilder $newLoadNames = array(); foreach ($repositories as $key => $repository) { - if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface) { + if ($repository instanceof PlatformRepository || $repository instanceof InstalledRepositoryInterface || $repository === $request->getLockedRepository()) { continue; } diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index bc3f2de04..c2c3f42c6 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -68,7 +68,7 @@ class Problem /** * A human readable textual representation of the problem's reasons * - * @param array $installedMap A map of all installed packages + * @param array $installedMap A map of all present packages * @return string */ public function getPrettyString(array $installedMap = array(), array $learnedPool = array()) diff --git a/src/Composer/DependencyResolver/Request.php b/src/Composer/DependencyResolver/Request.php index 54adea9b5..0656cbbae 100644 --- a/src/Composer/DependencyResolver/Request.php +++ b/src/Composer/DependencyResolver/Request.php @@ -85,18 +85,20 @@ class Request return isset($this->fixedPackages[spl_object_hash($package)]); } - public function getPresentMap() + // TODO look into removing the packageIds option, the only place true is used is for the installed map in the solver problems + // some locked packages may not be in the pool so they have a package->id of -1 + public function getPresentMap($packageIds = false) { $presentMap = array(); if ($this->lockedRepository) { foreach ($this->lockedRepository->getPackages() as $package) { - $presentMap[$package->id] = $package; + $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package; } } foreach ($this->fixedPackages as $package) { - $presentMap[$package->id] = $package; + $presentMap[$packageIds ? $package->id : spl_object_hash($package)] = $package; } return $presentMap; @@ -113,7 +115,8 @@ class Request return $unlockableMap; } - public function getLockMap() + public function getLockedRepository() { + return $this->lockedRepository; } } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 2abcf5d8a..66f325446 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -218,7 +218,7 @@ class Solver $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); if ($this->problems) { - throw new SolverProblemsException($this->problems, $request->getPresentMap(), $this->learnedPool); + throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool); } return new LockTransaction($this->policy, $this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index 4afac72aa..8e5d17dfe 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -52,8 +52,10 @@ update b/b --with-dependencies Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring. Loading composer repositories with package information Updating dependencies -Nothing to install or update +Nothing to modify in lock file Writing lock file +Installing dependencies from lock file (including require-dev) +Nothing to install, update or remove Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index 57d3ced92..94a0e3aa3 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -65,7 +65,7 @@ Your requirements could not be resolved to an installable set of packages. - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found. Problem 4 - stable-requiree-excluded/pkg is locked to version 1.0.0 and an update of this package was not requested. - - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.0, 1.0.1]. + - Same name, can only install one of: stable-requiree-excluded/pkg[1.0.1, 1.0.0]. - Installation request for stable-requiree-excluded/pkg 1.0.1 -> satisfiable by stable-requiree-excluded/pkg[1.0.1]. Potential causes: From 94d45a980ccd30039fe405a5c3d179d86c5c08fb Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 30 Oct 2019 00:32:23 +0100 Subject: [PATCH 18/37] Update lock syntax in tests and verify installed version does not impact lock generation Particularly the test tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test is interesting because it verifies that an older version will be installed on update if the new one is only present in the installed repo or vendor dir. This was the cause of a lot of weird edge cases and unreliable update behavior in Composer v1 --- .../install-missing-alias-from-lock.test | 2 +- ...e-downgrades-non-whitelisted-unstable.test | 5 ++-- .../installer/update-whitelist-patterns.test | 25 +++++++++++++++++-- .../update-whitelist-reads-lock.test | 4 +-- 4 files changed, 29 insertions(+), 7 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test index 5acb7a069..5f6459799 100644 --- a/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-missing-alias-from-lock.test @@ -29,7 +29,7 @@ Installing an old alias that doesn't exist anymore from a lock is possible "type": "library" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index 9b8d32f06..add46b848 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -53,7 +53,7 @@ update c/uptodate "packages": [ { "name": "a/old", "version": "1.0.0", "type": "library" }, { "name": "b/unstable", "version": "1.0.0", "type": "library" }, - { "name": "c/uptodate", "version": "2.0.0", "type": "library" }, + { "name": "c/uptodate", "version": "1.0.0", "type": "library" }, { "name": "d/removed", "version": "1.0.0", "type": "library" } ], "packages-dev": [], @@ -66,6 +66,7 @@ update c/uptodate "platform-dev": [] } --EXPECT-- -Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) Updating a/old (0.9.0) to a/old (1.0.0) +Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) +Updating c/uptodate (2.0.0) to c/uptodate (1.0.0) Installing d/removed (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test index de1fb1b73..d1b4fd66d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test @@ -39,10 +39,31 @@ Update with a package whitelist only updates those corresponding to the pattern { "name": "another/another", "version": "1.0" }, { "name": "no/regexp", "version": "1.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "vendor/Test-Package", "version": "1.0" }, + { "name": "vendor/NotMe", "version": "1.0" }, + { "name": "exact/Test-Package", "version": "1.0" }, + { "name": "notexact/TestPackage", "version": "1.0" }, + { "name": "all/Package1", "version": "1.0" }, + { "name": "all/Package2", "version": "1.0" }, + { "name": "another/another", "version": "1.0" }, + { "name": "no/regexp", "version": "1.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} --RUN-- update vendor/Test* exact/Test-Package notexact/Test all/* no/reg?xp --EXPECT-- -Updating vendor/Test-Package (1.0) to vendor/Test-Package (2.0) -Updating exact/Test-Package (1.0) to exact/Test-Package (2.0) Updating all/Package1 (1.0) to all/Package1 (2.0) Updating all/Package2 (1.0) to all/Package2 (2.0) +Updating exact/Test-Package (1.0) to exact/Test-Package (2.0) +Updating vendor/Test-Package (1.0) to vendor/Test-Package (2.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test index c84f0e65d..552051565 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-reads-lock.test @@ -28,7 +28,7 @@ Limited update takes rules from lock if available, and not from the installed re { "name": "toupdate/installed", "version": "1.0.0" }, { "name": "toupdate/notinstalled", "version": "1.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -43,6 +43,6 @@ Limited update takes rules from lock if available, and not from the installed re --RUN-- update toupdate/installed --EXPECT-- -Updating toupdate/installed (1.0.0) to toupdate/installed (1.1.0) Updating old/installed (0.9.0) to old/installed (1.0.0) +Updating toupdate/installed (1.0.0) to toupdate/installed (1.1.0) Installing toupdate/notinstalled (1.0.0) From e6e317bc27c06c5a74f48e04ffb343796f5a1a8d Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 30 Oct 2019 00:40:36 +0100 Subject: [PATCH 19/37] Fix test configurations, missing lock files, invalid ones aliased alias test is failing because double alias is improperly resolved now --- .../installer/install-aliased-alias.test | 2 +- .../installer/install-from-empty-lock.test | 4 ++-- .../installer/partial-update-from-lock.test | 2 +- ...irements-do-not-affect-locked-versions.test | 2 +- ...e-whitelist-patterns-with-dependencies.test | 18 ++++++++++++++++++ .../update-whitelist-with-dependencies.test | 16 ++++++++++++++++ ...updating-dev-updates-url-and-reference.test | 2 +- 7 files changed, 40 insertions(+), 6 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test index f535caa7e..2c047f8bb 100644 --- a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test +++ b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test @@ -31,6 +31,6 @@ Installing double aliased package install --EXPECT-- Installing b/b (dev-foo) +Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) Marking b/b (dev-master) as installed, alias of b/b (dev-foo) Installing a/a (dev-master) -Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) diff --git a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test index 7bb69f131..c3abd2377 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-empty-lock.test @@ -21,7 +21,7 @@ Requirements from the composer file are not installed if the lock file is presen "packages": [ { "name": "required", "version": "1.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], @@ -31,4 +31,4 @@ Requirements from the composer file are not installed if the lock file is presen --RUN-- install --EXPECT-- -Installing required (1.0.0) \ No newline at end of file +Installing required (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test index ed2002e4e..f49f23dfd 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-from-lock.test @@ -74,8 +74,8 @@ update b/unstable "platform-dev": [] } --EXPECT-- -Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) Updating a/old (0.9.0) to a/old (1.0.0) +Updating b/unstable (1.1.0-alpha) to b/unstable (1.0.0) Updating c/uptodate (2.0.0) to c/uptodate (1.0.0) Installing d/removed (1.0.0) Installing e/newreq (1.0.0) diff --git a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test index 15d1b4ef5..8868cbe13 100644 --- a/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test +++ b/tests/Composer/Test/Fixtures/installer/root-requirements-do-not-affect-locked-versions.test @@ -23,7 +23,7 @@ The locked version will not get overwritten by an install { "name": "foo/bar", "version": "1.0.0" }, { "name": "foo/baz", "version": "2.0.0" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "stable", "stability-flags": [], diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test index c685f14ce..aee2310ac 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test @@ -41,6 +41,24 @@ Update with a package whitelist only updates those packages and their dependenci { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted-component1", "version": "1.0.0" }, + { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "root-dependency", "version": "1.0.0" }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} --RUN-- update whitelisted-* --with-dependencies --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test index bb2e04193..73acfa516 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test @@ -33,6 +33,22 @@ Update with a package whitelist only updates those packages and their dependenci { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} --RUN-- update whitelisted --with-dependencies --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test index c5c838517..06870c4ee 100644 --- a/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test +++ b/tests/Composer/Test/Fixtures/installer/updating-dev-updates-url-and-reference.test @@ -28,7 +28,7 @@ Updating a dev package for new reference updates the url and reference "dist": { "reference": "oldref", "url": "oldurl", "type": "zip", "shasum": "" } } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": {"a/a":20}, From eaae360ce61857821cfd027c6fc3e5642a4c9c98 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Wed, 30 Oct 2019 00:55:11 +0100 Subject: [PATCH 20/37] Correcting lock files in test cases and updating output --- src/Composer/Package/Locker.php | 2 +- .../Fixtures/installer/abandoned-listed.test | 8 ++++++-- .../Fixtures/installer/suggest-replaced.test | 8 ++++++-- .../Fixtures/installer/update-alias-lock.test | 2 +- .../update-picks-up-change-of-vcs-type.test | 10 +++++++++- ...whitelist-patterns-without-dependencies.test | 17 +++++++++++++++++ ...date-whitelist-with-dependency-conflict.test | 16 ++++++++++++++++ 7 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index d790837a5..577f9caa3 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -162,7 +162,7 @@ class Locker if (isset($lockData['packages-dev'])) { $lockedPackages = array_merge($lockedPackages, $lockData['packages-dev']); } else { - throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or run update to install those packages.'); + throw new \RuntimeException('The lock file does not contain require-dev information, run install with the --no-dev option or delete it and run composer update to generate a new lock file.'); } } diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index 7eba0a6f0..d5e3c3d52 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -25,11 +25,15 @@ Abandoned packages are flagged install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Installing c/c (1.0.0) + - Installing a/a (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals Package a/a is abandoned, you should avoid using it. No replacement was suggested. Package c/c is abandoned, you should avoid using it. Use b/b instead. -Writing lock file Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index f18054d74..3ffcd20f7 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -20,9 +20,13 @@ Suggestions are not displayed for packages if they are replaced install --EXPECT-OUTPUT-- Loading composer repositories with package information -Updating dependencies (including require-dev) -Package operations: 2 installs, 0 updates, 0 removals +Updating dependencies +Lock file operations: 2 installs, 0 updates, 0 removals + - Installing c/c (1.0.0) + - Installing a/a (1.0.0) Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 2 installs, 0 updates, 0 removals Generating autoload files --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index f4f5e98eb..40a75807a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -36,7 +36,7 @@ Update aliased package does not mess up the lock file { "package": "a/a", "version": "dev-master", "source-reference": "1234" }, { "package": "a/a", "version": "dev-master", "alias-pretty-version": "1.0.x-dev", "alias-version": "1.0.9999999.9999999-dev" } ], - "packages-dev": null, + "packages-dev": [], "aliases": [], "minimum-stability": "dev", "stability-flags": [], diff --git a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test index 1e528d047..dfb3f650d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test +++ b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test @@ -31,7 +31,15 @@ Converting from one VCS type to another (including an URL change) should update "name": "a/a", "version": "1.0.0", "source": { "reference": "old-hg-ref", "type": "hg", "url": "old-hg-url" } } - ] + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] } --RUN-- update diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test index e5551b43f..ac07f23fd 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test @@ -37,6 +37,23 @@ Update with a package whitelist only updates those packages matching the pattern { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted-component1", "version": "1.0.0" }, + { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} --RUN-- update whitelisted-* --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test index f63229fbc..38a7bbf54 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test @@ -33,6 +33,22 @@ Update with a package whitelist only updates whitelisted packages if no dependen { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] +--LOCK-- +{ + "packages": [ + { "name": "fixed", "version": "1.0.0" }, + { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "dependency", "version": "1.0.0" }, + { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, + { "name": "unrelated-dependency", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "dev", + "stability-flags": {"a/a":20}, + "prefer-stable": false, + "prefer-lowest": false +} --RUN-- update whitelisted --EXPECT-- From 6925005ac9c115d8105289a3bc9526bc765d2b21 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 17:35:44 +0100 Subject: [PATCH 21/37] Implement update mirrors/nothing/lock as its own installer mode These special commands no longer (ab)use the partial update mechanism but rather create a special install request for all current lock file contents and later override any modified code references to the originals. This leads to up to date remote metadata but no other changes. --- src/Composer/Command/UpdateCommand.php | 16 +++++- .../DependencyResolver/LockTransaction.php | 10 +++- .../DependencyResolver/PoolBuilder.php | 16 +----- src/Composer/Installer.php | 53 +++++++++++-------- src/Composer/Package/AliasPackage.php | 5 ++ src/Composer/Package/Package.php | 17 ++++++ src/Composer/Package/PackageInterface.php | 9 ++++ .../installer/update-mirrors-changes-url.test | 23 +++----- .../update-picks-up-change-of-vcs-type.test | 2 +- tests/Composer/Test/InstallerTest.php | 10 +++- 10 files changed, 104 insertions(+), 57 deletions(-) diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 99bd2d74b..0c3d3e6c7 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -121,6 +121,19 @@ EOT } } + // the arguments lock/nothing/mirrors are not package names but trigger a mirror update instead + // they are further mutually exclusive with listing actual package names + $filteredPackages = array_filter($packages, function ($package) { + return !in_array($package, array('lock', 'nothing', 'mirrors'), true); + }); + $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages); + $packages = $filteredPackages; + + if ($updateMirrors && !empty($packages)) { + $io->writeError('You cannot simultaneously update only a selection of packages and regenerate the lock file metadata.'); + return -1; + } + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); @@ -146,7 +159,8 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $packages) + ->setUpdateMirrors($updateMirrors) + ->setUpdateWhitelist($packages) ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 75b2efb5b..7255de3dd 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -152,11 +152,19 @@ class LockTransaction } // TODO additionalFixedRepository needs to be looked at here as well? - public function getNewLockPackages($devMode) + public function getNewLockPackages($devMode, $updateMirrors = false) { $packages = array(); foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { + // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is + if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) { + foreach ($this->presentMap as $presentPackage) { + if ($package->getName() == $presentPackage->getName() && $package->getVersion() == $presentPackage->getVersion() && $presentPackage->getSourceReference()) { + $package->setSourceDistReferences($presentPackage->getSourceReference()); + } + } + } $packages[] = $package; } } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 39ac5098f..1c995bc4e 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -184,7 +184,7 @@ class PoolBuilder if (isset($this->rootReferences[$name])) { // do not modify the references on already locked packages if (!$request->isFixedPackage($package)) { - $this->setReferences($package, $this->rootReferences[$name]); + $package->setSourceDistReferences($this->rootReferences[$name]); } } @@ -225,19 +225,5 @@ class PoolBuilder return $loadNames; } - - private function setReferences(Package $package, $reference) - { - $package->setSourceReference($reference); - - // only bitbucket, github and gitlab have auto generated dist URLs that easily allow replacing the reference in the dist URL - // TODO generalize this a bit for self-managed/on-prem versions? Some kind of replace token in dist urls which allow this? - if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { - $package->setDistReference($reference); - $package->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $package->getDistUrl())); - } elseif ($package->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it - $package->setDistReference($reference); - } - } } diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index dea1e03e5..86d251756 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -38,6 +38,7 @@ use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Package\CompletePackage; use Composer\Package\Link; +use Composer\Package\LinkConstraint\VersionConstraint; use Composer\Package\Loader\ArrayLoader; use Composer\Package\Dumper\ArrayDumper; use Composer\Package\Package; @@ -137,6 +138,7 @@ class Installer * * @var array|null */ + protected $updateMirrors = false; protected $updateWhitelist = null; protected $whitelistTransitiveDependencies = false; protected $whitelistAllDependencies = false; @@ -192,6 +194,10 @@ class Installer gc_collect_cycles(); gc_disable(); + if ($this->updateWhitelist && $this->updateMirrors) { + throw new \RuntimeException("The installer options updateMirrors and updateWhitelist are mutually exclusive."); + } + // Force update if there is no lock file present if (!$this->update && !$this->locker->isLocked()) { // TODO throw an error instead? @@ -370,8 +376,15 @@ class Installer $links = array_merge($this->package->getRequires(), $this->package->getDevRequires()); - foreach ($links as $link) { - $request->install($link->getTarget(), $link->getConstraint()); + // if we're updating mirrors we want to keep exactly the same versions installed which are in the lock file, but we want current remote metadata + if ($this->updateMirrors) { + foreach ($lockedRepository->getPackages() as $lockedPackage) { + $request->install($lockedPackage->getName(), new Constraint('==', $lockedPackage->getVersion())); + } + } else { + foreach ($links as $link) { + $request->install($link->getTarget(), $link->getConstraint()); + } } // if the updateWhitelist is enabled, packages not in it are also fixed @@ -489,8 +502,8 @@ class Installer } $updatedLock = $this->locker->setLockData( - $lockTransaction->getNewLockPackages(false), - $lockTransaction->getNewLockPackages(true), + $lockTransaction->getNewLockPackages(false, $this->updateMirrors), + $lockTransaction->getNewLockPackages(true, $this->updateMirrors), $platformReqs, $platformDevReqs, $aliases, @@ -912,23 +925,6 @@ class Installer return $normalizedAliases; } - // TODO do we still need this function? - private function updateInstallReferences(PackageInterface $package, $reference) - { - if (!$reference) { - return; - } - - $package->setSourceReference($reference); - - if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $package->getDistUrl())) { - $package->setDistReference($reference); - $package->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $package->getDistUrl())); - } elseif ($package->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it - $package->setDistReference($reference); - } - } - /** * @param PlatformRepository $platformRepo * @param array $aliases @@ -1044,7 +1040,7 @@ class Installer $depPackages = array_merge($depPackages, call_user_func_array('array_merge', $matchesByPattern)); } - if (count($depPackages) == 0 && !$nameMatchesRequiredPackage && !in_array($packageName, array('nothing', 'lock', 'mirrors'))) { + if (count($depPackages) == 0 && !$nameMatchesRequiredPackage) { $this->io->writeError('Package "' . $packageName . '" listed for update is not installed. Ignoring.'); } @@ -1347,6 +1343,19 @@ class Installer return $this; } + /** + * Update the lock file to the exact same versions and references but use current remote metadata like URLs and mirror info + * + * @param bool $updateMirrors + * @return Installer + */ + public function setUpdateMirrors($updateMirrors) + { + $this->updateMirrors = $updateMirrors; + + return $this; + } + /** * restrict the update operation to a few packages, all other packages * that are already installed will be kept at their current version diff --git a/src/Composer/Package/AliasPackage.php b/src/Composer/Package/AliasPackage.php index 89f197856..b103139dd 100644 --- a/src/Composer/Package/AliasPackage.php +++ b/src/Composer/Package/AliasPackage.php @@ -411,4 +411,9 @@ class AliasPackage extends BasePackage implements CompletePackageInterface { return $this->aliasOf->setDistType($type); } + + public function setSourceDistReferences($reference) + { + return $this->aliasOf->setSourceDistReferences($reference); + } } diff --git a/src/Composer/Package/Package.php b/src/Composer/Package/Package.php index 6c7b426e7..c633e1856 100644 --- a/src/Composer/Package/Package.php +++ b/src/Composer/Package/Package.php @@ -569,6 +569,23 @@ class Package extends BasePackage return $this->archiveExcludes; } + /** + * {@inheritDoc} + */ + public function setSourceDistReferences($reference) + { + $this->setSourceReference($reference); + + // only bitbucket, github and gitlab have auto generated dist URLs that easily allow replacing the reference in the dist URL + // TODO generalize this a bit for self-managed/on-prem versions? Some kind of replace token in dist urls which allow this? + if (preg_match('{^https?://(?:(?:www\.)?bitbucket\.org|(api\.)?github\.com|(?:www\.)?gitlab\.com)/}i', $this->getDistUrl())) { + $this->setDistReference($reference); + $this->setDistUrl(preg_replace('{(?<=/|sha=)[a-f0-9]{40}(?=/|$)}i', $reference, $this->getDistUrl())); + } elseif ($this->getDistReference()) { // update the dist reference if there was one, but if none was provided ignore it + $this->setDistReference($reference); + } + } + /** * Replaces current version and pretty version with passed values. * It also sets stability. diff --git a/src/Composer/Package/PackageInterface.php b/src/Composer/Package/PackageInterface.php index 25a2e9bfe..7e83839ff 100644 --- a/src/Composer/Package/PackageInterface.php +++ b/src/Composer/Package/PackageInterface.php @@ -386,4 +386,13 @@ interface PackageInterface * @return void */ public function setDistReference($reference); + + /** + * Set dist and source references and update dist URL for ones that contain a reference + * + * @param string $reference + * + * @return void + */ + public function setSourceDistReferences($reference); } diff --git a/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test index 9d88870b0..9bfca4c85 100644 --- a/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-mirrors-changes-url.test @@ -149,14 +149,14 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "packages": [ { "name": "a/a", "version": "dev-master", - "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/a/newa", "type": "git" }, - "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/a/newa/zipball/2222222222222222222222222222222222222222", "type": "zip" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/a/newa", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/a/newa/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, { "name": "b/b", "version": "2.0.3", - "source": { "reference": "2222222222222222222222222222222222222222", "url": "https://github.com/b/newb", "type": "git" }, - "dist": { "reference": "2222222222222222222222222222222222222222", "url": "https://api.github.com/repos/b/newb/zipball/2222222222222222222222222222222222222222", "type": "zip" }, + "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/b/newb", "type": "git" }, + "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/b/newb/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, { @@ -171,12 +171,6 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/d/newd/zipball/1111111111111111111111111111111111111111", "type": "zip" }, "type": "library" }, - { - "name": "e/e", "version": "dev-master", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/e/newe", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/e/newe/zipball/1111111111111111111111111111111111111111", "type": "zip" }, - "type": "library" - }, { "name": "f/f", "version": "dev-master", "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/f/newf", "type": "git" }, @@ -185,8 +179,8 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an }, { "name": "g/g", "version": "dev-master", - "source": { "reference": "1111111111111111111111111111111111111111", "url": "https://github.com/g/newg", "type": "git" }, - "dist": { "reference": "1111111111111111111111111111111111111111", "url": "https://api.github.com/repos/g/newg/zipball/1111111111111111111111111111111111111111", "type": "zip" }, + "source": { "reference": "0000000000000000000000000000000000000000", "url": "https://github.com/g/newg", "type": "git" }, + "dist": { "reference": "0000000000000000000000000000000000000000", "url": "https://api.github.com/repos/g/newg/zipball/0000000000000000000000000000000000000000", "type": "zip" }, "type": "library" } ], @@ -206,8 +200,5 @@ g/g is dev and installed in a different ref than the #ref, so it gets updated an "platform-dev": [] } --RUN-- -update a/a b/b d/d g/g +update mirrors --EXPECT-- -Installing e/e (dev-master 1111111) -Updating a/a (dev-master 1111111) to a/a (dev-master 2222222) -Updating g/g (dev-master 0000000) to g/g (dev-master 1111111) diff --git a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test index dfb3f650d..a82487a31 100644 --- a/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test +++ b/tests/Composer/Test/Fixtures/installer/update-picks-up-change-of-vcs-type.test @@ -42,7 +42,7 @@ Converting from one VCS type to another (including an URL change) should update "platform-dev": [] } --RUN-- -update +update mirrors --EXPECT-LOCK-- { "packages": [ diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index ce87d111f..f1e55a794 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -269,11 +269,19 @@ class InstallerTest extends TestCase }); $application->get('update')->setCode(function ($input, $output) use ($installer) { + $packages = $input->getArgument('packages'); + $filteredPackages = array_filter($packages, function ($package) { + return !in_array($package, array('lock', 'nothing', 'mirrors'), true); + }); + $updateMirrors = $input->getOption('lock') || count($filteredPackages) != count($packages); + $packages = $filteredPackages; + $installer ->setDevMode(!$input->getOption('no-dev')) ->setUpdate(true) ->setDryRun($input->getOption('dry-run')) - ->setUpdateWhitelist($input->getArgument('packages')) + ->setUpdateMirrors($updateMirrors) + ->setUpdateWhitelist($packages) ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) ->setPreferStable($input->getOption('prefer-stable')) From 2d37bb4116f5092302266419746a003c32daa866 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 17:38:16 +0100 Subject: [PATCH 22/37] Outated lock files now trigger an error requesting removal rather than being ignored silently --- .../Test/Fixtures/installer/update-alias-lock.test | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test index 40a75807a..da4faeed8 100644 --- a/tests/Composer/Test/Fixtures/installer/update-alias-lock.test +++ b/tests/Composer/Test/Fixtures/installer/update-alias-lock.test @@ -29,20 +29,6 @@ Update aliased package does not mess up the lock file }, "minimum-stability": "dev" } ---LOCK-- -{ - "_": "outdated lock file, should not have to be loaded in an update", - "packages": [ - { "package": "a/a", "version": "dev-master", "source-reference": "1234" }, - { "package": "a/a", "version": "dev-master", "alias-pretty-version": "1.0.x-dev", "alias-version": "1.0.9999999.9999999-dev" } - ], - "packages-dev": [], - "aliases": [], - "minimum-stability": "dev", - "stability-flags": [], - "prefer-stable": false, - "prefer-lowest": false -} --INSTALLED-- [ { From cc274ebdf4417f13a7197efcfb72aa34bea32733 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 17:42:17 +0100 Subject: [PATCH 23/37] Do not reset references on update mirrors if VCS type has changed --- src/Composer/DependencyResolver/LockTransaction.php | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 7255de3dd..293f4a43b 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -158,9 +158,14 @@ class LockTransaction foreach ($this->resultPackages[$devMode ? 'dev' : 'non-dev'] as $package) { if (!($package instanceof AliasPackage) && !($package instanceof RootAliasPackage)) { // if we're just updating mirrors we need to reset references to the same as currently "present" packages' references to keep the lock file as-is + // we do not reset references if the currently present package didn't have any, or if the type of VCS has changed if ($updateMirrors && !isset($this->presentMap[spl_object_hash($package)])) { foreach ($this->presentMap as $presentPackage) { - if ($package->getName() == $presentPackage->getName() && $package->getVersion() == $presentPackage->getVersion() && $presentPackage->getSourceReference()) { + if ($package->getName() == $presentPackage->getName() && + $package->getVersion() == $presentPackage->getVersion() && + $presentPackage->getSourceReference() && + $presentPackage->getSourceType() === $package->getSourceType() + ) { $package->setSourceDistReferences($presentPackage->getSourceReference()); } } From c50d236378c2538c6d08b66ea733c99e29a52553 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 20:40:28 +0100 Subject: [PATCH 24/37] Correctly load branch aliases from lock file Root aliases are also stored in the lock file, so on install do not read them from composer.json. --- src/Composer/DependencyResolver/RuleSetGenerator.php | 2 +- src/Composer/Installer.php | 5 ++++- src/Composer/Package/Locker.php | 5 +++++ .../Test/Fixtures/installer/install-aliased-alias.test | 2 +- 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index d42617a7a..76f9270be 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -262,7 +262,7 @@ class RuleSetGenerator foreach ($request->getFixedPackages() as $package) { if ($package->id == -1) { - throw new \RuntimeException("Fixed package ".$package->getName()." was not added to solver pool."); + throw new \RuntimeException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool."); } $this->addRulesForPackage($package, $ignorePlatformReqs); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 86d251756..8cdf803c5 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -658,7 +658,8 @@ class Installer // creating repository set $policy = $this->createPolicy(false); - $repositorySet = $this->createRepositorySet($platformRepo, $aliases, $lockedRepository); + // use aliases from lock file only, so empty root aliases here + $repositorySet = $this->createRepositorySet($platformRepo, array(), $lockedRepository); $repositorySet->addRepository($lockedRepository); $this->io->writeError('Installing dependencies from lock file'.($this->devMode ? ' (including require-dev)' : '').''); @@ -708,6 +709,8 @@ class Installer // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); + } else { + // need to still create the pool to reconstruct aliases } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 577f9caa3..8069ba50c 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -176,6 +176,11 @@ class Locker $package = $this->loader->load($info); $packages->addPackage($package); $packageByName[$package->getName()] = $package; + + if ($package instanceof AliasPackage) { + $packages->addPackage($package->getAliasOf()); + $packageByName[$package->getAliasOf()->getName()] = $package->getAliasOf(); + } } if (isset($lockData['aliases'])) { diff --git a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test index 2c047f8bb..63410283d 100644 --- a/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test +++ b/tests/Composer/Test/Fixtures/installer/install-aliased-alias.test @@ -31,6 +31,6 @@ Installing double aliased package install --EXPECT-- Installing b/b (dev-foo) -Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) Marking b/b (dev-master) as installed, alias of b/b (dev-foo) +Marking b/b (1.0.x-dev) as installed, alias of b/b (dev-foo) Installing a/a (dev-master) From e308f043b983ef7893d478fd99ccdeeade302eff Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 21:18:09 +0100 Subject: [PATCH 25/37] Fully switch to spl_object_hash in lock transaction The pool builder tries to be minimal so it's fine for present/locked packages not be assigned a solver/pool id. Adding a test to verify correct creation of uninstall jobs --- .../DependencyResolver/LockTransaction.php | 34 ++++++---- .../update-removes-unused-locked-dep.test | 67 +++++++++++++++++++ 2 files changed, 88 insertions(+), 13 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 293f4a43b..9115e1533 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -67,6 +67,7 @@ class LockTransaction protected function calculateOperations() { $operations = array(); + $ignoreRemove = array(); $lockMeansUpdateMap = $this->findPotentialUpdates(); foreach ($this->decisions as $i => $decision) { @@ -77,17 +78,17 @@ class LockTransaction // wanted & !present if ($literal > 0 && !isset($this->presentMap[spl_object_hash($package)])) { - if (isset($lockMeansUpdateMap[abs($literal)]) && !$package instanceof AliasPackage) { + if (isset($lockMeansUpdateMap[spl_object_hash($package)]) && !$package instanceof AliasPackage) { // TODO we end up here sometimes because we prefer the remote package now to get up to date metadata // TODO define some level of identity here for what constitutes an update and what can be ignored? new kind of metadata only update? - $target = $lockMeansUpdateMap[abs($literal)]; + $target = $lockMeansUpdateMap[spl_object_hash($package)]; if ($package->getName() !== $target->getName() || $package->getVersion() !== $target->getVersion()) { $operations[] = new Operation\UpdateOperation($target, $package, $reason); } // avoid updates to one package from multiple origins - $ignoreRemove[$lockMeansUpdateMap[abs($literal)]->id] = true; - unset($lockMeansUpdateMap[abs($literal)]); + $ignoreRemove[spl_object_hash($lockMeansUpdateMap[spl_object_hash($package)])] = true; + unset($lockMeansUpdateMap[spl_object_hash($package)]); } else { if ($package instanceof AliasPackage) { $operations[] = new Operation\MarkAliasInstalledOperation($package, $reason); @@ -103,7 +104,7 @@ class LockTransaction $reason = $decision[Decisions::DECISION_REASON]; $package = $this->pool->literalToPackage($literal); - if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)]) && !isset($ignoreRemove[$package->id])) { + if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)]) && !isset($ignoreRemove[spl_object_hash($package)])) { if ($package instanceof AliasPackage) { $operations[] = new Operation\MarkAliasUninstalledOperation($package, $reason); } else { @@ -112,6 +113,17 @@ class LockTransaction } } + foreach ($this->presentMap as $package) { + if ($package->id === -1 && !isset($ignoreRemove[spl_object_hash($package)])) { + // TODO pass reason parameter to these two operations? + if ($package instanceof AliasPackage) { + $operations[] = new Operation\MarkAliasUninstalledOperation($package); + } else { + $operations[] = new Operation\UninstallOperation($package); + } + } + } + $this->setResultPackages(); return $operations; @@ -207,15 +219,11 @@ class LockTransaction // TODO can't we just look at existing rules? $updates = $this->policy->findUpdatePackages($this->pool, $package); - $literals = array($package->id); + $updatesAndPackage = array_merge(array($package), $updates); - foreach ($updates as $update) { - $literals[] = $update->id; - } - - foreach ($literals as $updateLiteral) { - if (!isset($lockMeansUpdateMap[$updateLiteral])) { - $lockMeansUpdateMap[$updateLiteral] = $package; + foreach ($updatesAndPackage as $update) { + if (!isset($lockMeansUpdateMap[spl_object_hash($update)])) { + $lockMeansUpdateMap[spl_object_hash($update)] = $package; } } } diff --git a/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test new file mode 100644 index 000000000..808afb02e --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/update-removes-unused-locked-dep.test @@ -0,0 +1,67 @@ +--TEST-- +A composer update should remove unused locked dependencies from the lock file and remove unused installed deps from disk +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } + ] + } + ], + "require": { + "a/a": "*" + } +} +--LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--INSTALLED-- +[ + { "name": "a/a", "version": "1.0.0" }, + { "name": "b/b", "version": "1.0.0" }, + { "name": "c/c", "version": "1.0.0" } +] +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [ + { "name": "a/a", "version": "1.0.0", "type": "library" } + ], + "packages-dev": [], + "aliases": [], + "minimum-stability": "stable", + "stability-flags": [], + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Lock file operations: 0 installs, 0 updates, 1 removal + - Uninstalling b/b (1.0.0) +Writing lock file +Installing dependencies from lock file (including require-dev) +Package operations: 0 installs, 0 updates, 2 removals +Generating autoload files + +--EXPECT-- +Uninstalling c/c (1.0.0) +Uninstalling b/b (1.0.0) From e6e07231057d910e5d3d5ed105a54ce7f9742e64 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 21:25:43 +0100 Subject: [PATCH 26/37] Remove unnecessary comments, aliases in lock file are correctly created --- src/Composer/Installer.php | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 8cdf803c5..493989bab 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -709,12 +709,9 @@ class Installer // TODO should we warn people / error if plugins in vendor folder do not match contents of lock file before update? //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, $this->devMode, $policy, $repositorySet, $installedRepo, $request, $lockTransaction); - } else { - // need to still create the pool to reconstruct aliases } // TODO in how far do we need to do anything here to ensure dev packages being updated to latest in lock without version change are treated correctly? - $localRepoTransaction = new LocalRepoTransaction($lockedRepository, $localRepo); if (!$localRepoTransaction->getOperations()) { From 26da52227ebac871a1a1eeddc0c10cbb9a35d38c Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 21:51:53 +0100 Subject: [PATCH 27/37] Clean up the Solver tests, no more installed repo input and new sorting The solver now only calculates a lock file transaction which does not need to be sorted in order of dependencies. This is only necessary for the local repo transaction generated without the solver during install --- src/Composer/Installer.php | 2 + src/Composer/Plugin/PluginManager.php | 2 +- .../DependencyResolver/DefaultPolicyTest.php | 6 +- .../Test/DependencyResolver/SolverTest.php | 92 ++++++++----------- 4 files changed, 45 insertions(+), 57 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 493989bab..982f9baae 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -367,6 +367,8 @@ class Installer if (!$repositorySet->isPackageAcceptable($lockedPackage->getNames(), $lockedPackage->getStability())) { $constraint = new Constraint('=', $lockedPackage->getVersion()); $constraint->setPrettyString('(stability not acceptable)'); + + // if we can get rid of this remove() here, we can generally get rid of remove support in the request $request->remove($lockedPackage->getName(), $constraint); } } diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 814b0218a..03b872d47 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -158,7 +158,7 @@ class PluginManager $localRepo = $this->composer->getRepositoryManager()->getLocalRepository(); $globalRepo = $this->globalComposer ? $this->globalComposer->getRepositoryManager()->getLocalRepository() : null; - $repositorySet = new RepositorySet(array(), 'dev'); + $repositorySet = new RepositorySet(array(), array(), 'dev'); $repositorySet->addRepository($localRepo); if ($globalRepo) { $repositorySet->addRepository($globalRepo); diff --git a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php index 919d098e3..b4df12310 100644 --- a/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php +++ b/tests/Composer/Test/DependencyResolver/DefaultPolicyTest.php @@ -35,7 +35,7 @@ class DefaultPolicyTest extends TestCase public function setUp() { - $this->repositorySet = new RepositorySet(array(), 'dev'); + $this->repositorySet = new RepositorySet(array(), array(), 'dev'); $this->repo = new ArrayRepository; $this->repoLocked = new ArrayRepository; @@ -181,7 +181,7 @@ class DefaultPolicyTest extends TestCase $this->assertSame($expected, $selected); - $this->repositorySet = new RepositorySet(array(), 'dev'); + $this->repositorySet = new RepositorySet(array(), array(), 'dev'); $this->repositorySet->addRepository($repo2); $this->repositorySet->addRepository($repo1); @@ -287,7 +287,7 @@ class DefaultPolicyTest extends TestCase $repo->addPackage($packageA = clone $packageA); $repo->addPackage($packageB = clone $packageB); - $repositorySet = new RepositorySet(array(), 'dev'); + $repositorySet = new RepositorySet(array(), array(), 'dev'); $repositorySet->addRepository($this->repo); $pool = $this->repositorySet->createPoolForPackages(array('vendor-a/replacer', 'vendor-b/replacer')); diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 3491f9c05..8f12f3122 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -29,7 +29,6 @@ class SolverTest extends TestCase { protected $repoSet; protected $repo; - protected $repoInstalled; protected $repoLocked; protected $request; protected $policy; @@ -39,10 +38,9 @@ class SolverTest extends TestCase { $this->repoSet = new RepositorySet(array()); $this->repo = new ArrayRepository; - $this->repoInstalled = new InstalledArrayRepository; $this->repoLocked = new ArrayRepository; - $this->request = new Request(); + $this->request = new Request($this->repoLocked); $this->policy = new DefaultPolicy; } @@ -58,10 +56,9 @@ class SolverTest extends TestCase )); } - public function testSolverRemoveIfNotInstalled() + public function testSolverRemoveIfNotRequested() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoLocked->addPackage(clone $packageA); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); $this->checkSolverResult(array( @@ -96,7 +93,6 @@ class SolverTest extends TestCase $repo1->addPackage($foo1 = $this->getPackage('foo', '1')); $repo2->addPackage($foo2 = $this->getPackage('foo', '1')); - $this->repoSet->addRepository($this->repoInstalled); $this->repoSet->addRepository($repo1); $this->repoSet->addRepository($repo2); @@ -172,36 +168,36 @@ class SolverTest extends TestCase $this->request->install('C'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageC), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageA), )); } - public function testSolverInstallInstalled() + public function testSolverFixLocked() { - $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); + $this->request->fixPackage($packageA); $this->checkSolverResult(array()); } - public function testSolverInstallInstalledWithAlternative() + public function testSolverFixLockedWithAlternative() { $this->repo->addPackage($this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); - $this->request->install('A'); + $this->request->fixPackage($packageA); $this->checkSolverResult(array()); } public function testSolverRemoveSingle() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->reposComplete(); $this->request->remove('A'); @@ -223,17 +219,15 @@ class SolverTest extends TestCase public function testSolverUpdateDoesOnlyUpdate() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); $this->reposComplete(); $packageA->setRequires(array('b' => new Link('A', 'B', $this->getVersionConstraint('>=', '1.0.0.0'), 'requires'))); - $this->request->install('A', $this->getVersionConstraint('=', '1.0.0.0')); + $this->request->fixPackage($packageA); $this->request->install('B', $this->getVersionConstraint('=', '1.1.0.0')); - $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); - $this->request->update('B', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -242,12 +236,11 @@ class SolverTest extends TestCase public function testSolverUpdateSingle() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1')); $this->reposComplete(); $this->request->install('A'); - $this->request->update('A'); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageA, 'to' => $newPackageA), @@ -256,8 +249,8 @@ class SolverTest extends TestCase public function testSolverUpdateAll() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.1')); $this->repo->addPackage($newPackageB = $this->getPackage('B', '1.1')); @@ -267,7 +260,6 @@ class SolverTest extends TestCase $this->reposComplete(); $this->request->install('A'); - $this->request->updateAll(); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageB, 'to' => $newPackageB), @@ -277,28 +269,26 @@ class SolverTest extends TestCase public function testSolverUpdateCurrent() { - $this->repoInstalled->addPackage($this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($this->getPackage('A', '1.0')); $this->repo->addPackage($this->getPackage('A', '1.0')); $this->reposComplete(); $this->request->install('A'); - $this->request->update('A'); $this->checkSolverResult(array()); } public function testSolverUpdateOnlyUpdatesSelectedPackage() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($packageAnewer = $this->getPackage('A', '1.1')); $this->repo->addPackage($packageBnewer = $this->getPackage('B', '1.1')); $this->reposComplete(); $this->request->install('A'); - $this->request->install('B'); - $this->request->update('A'); + $this->request->fixPackage($packageB); $this->checkSolverResult(array( array('job' => 'update', 'from' => $packageA, 'to' => $packageAnewer), @@ -307,13 +297,12 @@ class SolverTest extends TestCase public function testSolverUpdateConstrained() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2')); $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); - $this->request->update('A'); $this->checkSolverResult(array(array( 'job' => 'update', @@ -324,13 +313,12 @@ class SolverTest extends TestCase public function testSolverUpdateFullyConstrained() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2')); $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); - $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array(array( 'job' => 'update', @@ -341,14 +329,13 @@ class SolverTest extends TestCase public function testSolverUpdateFullyConstrainedPrunesInstalledPackages() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); - $this->repoInstalled->addPackage($packageB = $this->getPackage('B', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageB = $this->getPackage('B', '1.0')); $this->repo->addPackage($newPackageA = $this->getPackage('A', '1.2')); $this->repo->addPackage($this->getPackage('A', '2.0')); $this->reposComplete(); $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); - $this->request->update('A', $this->getVersionConstraint('=', '1.0.0.0')); $this->checkSolverResult(array( array( @@ -365,8 +352,8 @@ class SolverTest extends TestCase public function testSolverAllJobs() { - $this->repoInstalled->addPackage($packageD = $this->getPackage('D', '1.0')); - $this->repoInstalled->addPackage($oldPackageC = $this->getPackage('C', '1.0')); + $this->repoLocked->addPackage($packageD = $this->getPackage('D', '1.0')); + $this->repoLocked->addPackage($oldPackageC = $this->getPackage('C', '1.0')); $this->repo->addPackage($packageA = $this->getPackage('A', '2.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); @@ -379,12 +366,11 @@ class SolverTest extends TestCase $this->request->install('A'); $this->request->install('C'); - $this->request->update('C'); $this->request->remove('D'); $this->checkSolverResult(array( - array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), array('job' => 'install', 'package' => $packageB), + array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), array('job' => 'install', 'package' => $packageA), array('job' => 'remove', 'package' => $packageD), )); @@ -411,7 +397,7 @@ class SolverTest extends TestCase public function testSolverObsolete() { - $this->repoInstalled->addPackage($packageA = $this->getPackage('A', '1.0')); + $this->repoLocked->addPackage($packageA = $this->getPackage('A', '1.0')); $this->repo->addPackage($packageB = $this->getPackage('B', '1.0')); $packageB->setReplaces(array('a' => new Link('B', 'A', new MultiConstraint(array())))); @@ -540,8 +526,8 @@ class SolverTest extends TestCase $this->request->install('X'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $newPackageB), array('job' => 'install', 'package' => $packageA), + array('job' => 'install', 'package' => $newPackageB), array('job' => 'install', 'package' => $packageX), )); } @@ -584,9 +570,9 @@ class SolverTest extends TestCase $this->request->install('C'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA), - array('job' => 'install', 'package' => $packageC), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageC), + array('job' => 'install', 'package' => $packageA), )); } @@ -766,7 +752,7 @@ class SolverTest extends TestCase $msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n"; $msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n"; $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; - $msg .= " - Can only install one of: B[0.9, 1.0].\n"; + $msg .= " - Same name, can only install one of: B[0.9, 1.0].\n"; $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; $msg .= " - Installation request for a -> satisfiable by A[1.0].\n"; $this->assertEquals($msg, $e->getMessage()); @@ -820,8 +806,8 @@ class SolverTest extends TestCase $this->request->install('A', $this->getVersionConstraint('==', '1.1.0.0')); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA2), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageA2), array('job' => 'install', 'package' => $packageA2Alias), )); } @@ -843,9 +829,9 @@ class SolverTest extends TestCase $this->request->install('B'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageAAlias), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageA), )); } @@ -908,12 +894,12 @@ class SolverTest extends TestCase $this->assertFalse($this->solver->testFlagLearnedPositiveLiteral); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageF1), - array('job' => 'install', 'package' => $packageD), - array('job' => 'install', 'package' => $packageG2), array('job' => 'install', 'package' => $packageC2), + array('job' => 'install', 'package' => $packageG2), + array('job' => 'install', 'package' => $packageF1), array('job' => 'install', 'package' => $packageE), array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageD), array('job' => 'install', 'package' => $packageA), )); @@ -939,7 +925,7 @@ class SolverTest extends TestCase $transaction = $this->solver->solve($this->request); $result = array(); - foreach ($transaction as $operation) { + foreach ($transaction->getOperations() as $operation) { if ('update' === $operation->getJobType()) { $result[] = array( 'job' => 'update', From bf99f1a341083533065a6214114c8ead262869f4 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 21:56:17 +0100 Subject: [PATCH 28/37] Fix RepositorySet constructor calls to use new signature --- src/Composer/Command/CreateProjectCommand.php | 2 +- src/Composer/Command/InitCommand.php | 2 +- src/Composer/Command/ShowCommand.php | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index a718bfa0b..07b39fd9b 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -293,7 +293,7 @@ EOT throw new \InvalidArgumentException('Invalid stability provided ('.$stability.'), must be one of: '.implode(', ', array_keys(BasePackage::$stabilities))); } - $repositorySet = new RepositorySet(array(), $stability); + $repositorySet = new RepositorySet(array(), array(), $stability); $repositorySet->addRepository($sourceRepo); $phpVersion = null; diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index 08f891014..19ff99e9d 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -666,7 +666,7 @@ EOT $key = $minimumStability ?: 'default'; if (!isset($this->repositorySets[$key])) { - $this->repositorySets[$key] = $repositorySet = new RepositorySet(array(), $minimumStability ?: $this->getMinimumStability($input)); + $this->repositorySets[$key] = $repositorySet = new RepositorySet(array(), array(), $minimumStability ?: $this->getMinimumStability($input)); $repositorySet->addRepository($this->getRepos()); } diff --git a/src/Composer/Command/ShowCommand.php b/src/Composer/Command/ShowCommand.php index 91ee27143..dde0f0062 100644 --- a/src/Composer/Command/ShowCommand.php +++ b/src/Composer/Command/ShowCommand.php @@ -538,7 +538,7 @@ EOT $constraint = is_string($version) ? $this->versionParser->parseConstraints($version) : $version; $policy = new DefaultPolicy(); - $repositorySet = new RepositorySet(array(), 'dev'); + $repositorySet = new RepositorySet(array(), array(), 'dev'); $repositorySet->addRepository($repos); $matchedPackage = null; @@ -1002,7 +1002,7 @@ EOT private function getRepositorySet(Composer $composer) { if (!$this->repositorySet) { - $this->repositorySet = new RepositorySet(array(), $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); + $this->repositorySet = new RepositorySet(array(), array(), $composer->getPackage()->getMinimumStability(), $composer->getPackage()->getStabilityFlags()); $this->repositorySet->addRepository(new CompositeRepository($composer->getRepositoryManager()->getRepositories())); } From 737a613a50c7dd8eebcf7a1bc8ed7e0d7b46afdd Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 22:01:37 +0100 Subject: [PATCH 29/37] Use array() instead of [] for PHP 5.3 compat --- tests/Composer/Test/InstallerTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index f1e55a794..95fe7b89e 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -125,7 +125,7 @@ class InstallerTest extends TestCase { $dumper = new ArrayDumper(); - $comparable = []; + $comparable = array(); foreach ($packages as $package) { $comparable[] = $dumper->dump($package); } From 28596d9c12c43db91f61d69a708dd937531e4722 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 22:02:35 +0100 Subject: [PATCH 30/37] Define property which is later accessed in local repo transaction --- src/Composer/DependencyResolver/LocalRepoTransaction.php | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index a41798b50..50056e953 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -33,6 +33,9 @@ class LocalRepoTransaction /** @var RepositoryInterface */ protected $localRepository; + /** @var array */ + protected $operations; + /** * Reassigns ids for all packages in the lockedrepository */ From bd6b4e433c6118bf286b8ee7bc9cae9bd715b47e Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 22:09:24 +0100 Subject: [PATCH 31/37] Use JsonFile::JSON_PRETTY_PRINT instead of php const for PHP 5.3 compat --- tests/Composer/Test/InstallerTest.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 95fe7b89e..758121979 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -93,7 +93,7 @@ class InstallerTest extends TestCase $lockJsonMock->expects($this->any()) ->method('write') ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { - $lockData = json_encode($value, JSON_PRETTY_PRINT); + $lockData = json_encode($value, JsonFile::JSON_PRETTY_PRINT); })); $tempLockData = null; @@ -216,7 +216,7 @@ class InstallerTest extends TestCase $repositoryManager->setLocalRepository(new InstalledFilesystemRepositoryMock($jsonMock)); // emulate a writable lock file - $lockData = $lock ? json_encode($lock, JSON_PRETTY_PRINT): null; + $lockData = $lock ? json_encode($lock, JsonFile::JSON_PRETTY_PRINT): null; $lockJsonMock = $this->getMockBuilder('Composer\Json\JsonFile')->disableOriginalConstructor()->getMock(); $lockJsonMock->expects($this->any()) ->method('read') @@ -231,7 +231,7 @@ class InstallerTest extends TestCase $lockJsonMock->expects($this->any()) ->method('write') ->will($this->returnCallback(function ($value, $options = 0) use (&$lockData) { - $lockData = json_encode($value, JSON_PRETTY_PRINT); + $lockData = json_encode($value, JsonFile::JSON_PRETTY_PRINT); })); if ($expectLock) { From 0099f5636161308ef300d9c87dbc153bb936ea37 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Thu, 7 Nov 2019 22:11:54 +0100 Subject: [PATCH 32/37] Define property which is later accessed in lock transaction --- src/Composer/DependencyResolver/LockTransaction.php | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 9115e1533..76468ddc6 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -40,11 +40,14 @@ class LockTransaction * @var array */ protected $unlockableMap; - protected $decisions; - protected $resultPackages; + /** + * @var array + */ + protected $operations; + public function __construct($policy, $pool, $presentMap, $unlockableMap, $decisions) { $this->policy = $policy; From 3b26ef0f1bd9629fdd61eca3d2a010b74803e76d Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 12:13:23 +0100 Subject: [PATCH 33/37] clean up extract dev packages --- src/Composer/Installer.php | 77 ++------------------------------------ 1 file changed, 3 insertions(+), 74 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 982f9baae..c5e12b02e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -557,14 +557,14 @@ class Installer $pool = $repositorySet->createPool($request); - // solve dependencies + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); $solver = new Solver($policy, $pool, $this->io); try { $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); + //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); $solver = null; } catch (SolverProblemsException $e) { - // TODO change info message here - $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); $this->io->writeError($e->getMessage()); return max(1, $e->getCode()); @@ -573,77 +573,6 @@ class Installer $lockTransaction->setNonDevPackages($nonDevLockTransaction); } - - // TODO add proper output and events to above function based on old version below - /** - * Extracts the dev packages out of the localRepo - * - * This works by faking the operations so we can see what the dev packages - * would be at the end of the operation execution. This lets us then remove - * the dev packages from the list of operations accordingly if we are in a - * --no-dev install or update. - * - * @return array - private function extractDevPackages(array $operations, RepositoryInterface $localRepo, PlatformRepository $platformRepo, array $aliases) - { - // fake-apply all operations to this clone of the local repo so we see the complete set of package we would end up with - $tempLocalRepo = clone $localRepo; - foreach ($operations as $operation) { - switch ($operation->getJobType()) { - case 'install': - case 'markAliasInstalled': - if (!$tempLocalRepo->hasPackage($operation->getPackage())) { - $tempLocalRepo->addPackage(clone $operation->getPackage()); - } - break; - case 'uninstall': - case 'markAliasUninstalled': - $tempLocalRepo->removePackage($operation->getPackage()); - break; - case 'update': - $tempLocalRepo->removePackage($operation->getInitialPackage()); - if (!$tempLocalRepo->hasPackage($operation->getTargetPackage())) { - $tempLocalRepo->addPackage(clone $operation->getTargetPackage()); - } - break; - default: - throw new \LogicException('Unknown type: '.$operation->getJobType()); - } - } - // we have to reload the local repo to handle aliases properly - // but as it is not persisted on disk we use a loader/dumper - // to reload it in memory - $localRepo = new InstalledArrayRepository(array()); - $loader = new ArrayLoader(null, true); - $dumper = new ArrayDumper(); - foreach ($tempLocalRepo->getCanonicalPackages() as $pkg) { - $localRepo->addPackage($loader->load($dumper->dump($pkg))); - } - unset($tempLocalRepo, $loader, $dumper); - $policy = $this->createPolicy(); - $pool = $this->createPool(); - $installedRepo = $this->createInstalledRepo($localRepo, $platformRepo); - $pool->addRepository($installedRepo, $aliases); - // creating requirements request without dev requirements - $request = $this->createRequest($this->package, $platformRepo); - $request->updateAll(); - foreach ($this->package->getRequires() as $link) { - $request->install($link->getTarget(), $link->getConstraint()); - } - // solve deps to see which get removed - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); - $solver = new Solver($policy, $pool, $installedRepo, $this->io); - $ops = $solver->solve($request, $this->ignorePlatformReqs); - $this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); - $devPackages = array(); - foreach ($ops as $op) { - if ($op->getJobType() === 'uninstall') { - $devPackages[] = $op->getPackage(); - } - } - return $devPackages; - }*/ - /** * @param RepositoryInterface $localRepo * @param RepositoryInterface $installedRepo From e26405d8583dfc7e6083d3af2c5255cfc5cc2e81 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 12:26:46 +0100 Subject: [PATCH 34/37] Clean up comments and output --- src/Composer/Installer.php | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index c5e12b02e..9fa0b1573 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -200,7 +200,7 @@ class Installer // Force update if there is no lock file present if (!$this->update && !$this->locker->isLocked()) { - // TODO throw an error instead? + $this->io->writeError('No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file.'); $this->update = true; } @@ -574,11 +574,9 @@ class Installer } /** - * @param RepositoryInterface $localRepo - * @param RepositoryInterface $installedRepo - * @param PlatformRepository $platformRepo - * @param array $aliases - * @return int exit code + * @param RepositoryInterface $localRepo + * @param bool $alreadySolved Whether the function is called as part of an update command or independently + * @return int exit code */ protected function doInstall(RepositoryInterface $localRepo, $alreadySolved = false) { @@ -625,14 +623,14 @@ class Installer $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); $solver = null; - // installing the locked packages on this platfom resulted in lock modifying operations, there wasn't a conflict, but the lock file as-is seems to not work on this system + // installing the locked packages on this platform resulted in lock modifying operations, there wasn't a conflict, but the lock file as-is seems to not work on this system if (0 !== count($lockTransaction->getOperations())) { - $this->io->writeError('Your lock file cannot be installed on this system without changes, please run composer update.', true, IOInterface::QUIET); + $this->io->writeError('Your lock file cannot be installed on this system without changes. Please run composer update.', true, IOInterface::QUIET); // TODO actually display operations to explain what happened? return 1; } } catch (SolverProblemsException $e) { - $this->io->writeError('Your requirements could not be resolved to an installable set of packages.', true, IOInterface::QUIET); + $this->io->writeError('Your lock file does not contain a compatible set of packages. Please run composer update.', true, IOInterface::QUIET); $this->io->writeError($e->getMessage()); return max(1, $e->getCode()); From ff5ec54f0408ceb77c3d9398edd04d40dc9266f1 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 12:31:26 +0100 Subject: [PATCH 35/37] Correctly use install and update commands in our installer tests --- tests/Composer/Test/Fixtures/installer/abandoned-listed.test | 2 +- .../Test/Fixtures/installer/broken-deps-do-not-replace.test | 2 +- tests/Composer/Test/Fixtures/installer/github-issues-4319.test | 2 +- tests/Composer/Test/Fixtures/installer/suggest-installed.test | 2 +- tests/Composer/Test/Fixtures/installer/suggest-prod.test | 1 + tests/Composer/Test/Fixtures/installer/suggest-replaced.test | 2 +- tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test | 1 + 7 files changed, 7 insertions(+), 5 deletions(-) diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index d5e3c3d52..cdf648c0d 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -22,7 +22,7 @@ Abandoned packages are flagged } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index a4bfe6a4d..3d5cba664 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -19,7 +19,7 @@ Broken dependencies should not lead to a replacer being installed which is not m } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test index b387942fb..2fbd8784c 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test @@ -28,7 +28,7 @@ Present a clear error message when config.platform.php version results in a conf } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index 468a53612..6995f78e7 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -17,7 +17,7 @@ Suggestions are not displayed for installed packages } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index e28ef03e5..89d6ab8de 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -17,6 +17,7 @@ Suggestions are not displayed in non-dev mode --RUN-- install --no-dev --EXPECT-OUTPUT-- +No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index 3ffcd20f7..62c13e560 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -17,7 +17,7 @@ Suggestions are not displayed for packages if they are replaced } } --RUN-- -install +update --EXPECT-OUTPUT-- Loading composer repositories with package information Updating dependencies diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index ab22eeb6e..f45b710f0 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -17,6 +17,7 @@ Suggestions are displayed --RUN-- install --EXPECT-OUTPUT-- +No lock file found. Updating dependencies instead of installing from lock file. Use composer update over composer install if you do not have a lock file. Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals From 25de5218c39957dc644eb9f03c265a0bb7060534 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 15:56:46 +0100 Subject: [PATCH 36/37] Reunify lock and local repo transaction code and apply the same sorting --- .../LocalRepoTransaction.php | 275 +--------------- .../DependencyResolver/LockTransaction.php | 141 +------- src/Composer/DependencyResolver/Solver.php | 2 +- .../DependencyResolver/Transaction.php | 310 ++++++++++++++++++ src/Composer/Installer.php | 17 - .../Test/DependencyResolver/SolverTest.php | 31 +- .../Fixtures/installer/abandoned-listed.test | 2 +- .../installer/github-issues-4795-2.test | 2 +- .../Fixtures/installer/suggest-installed.test | 2 +- .../update-with-all-dependencies.test | 2 +- 10 files changed, 346 insertions(+), 438 deletions(-) create mode 100644 src/Composer/DependencyResolver/Transaction.php diff --git a/src/Composer/DependencyResolver/LocalRepoTransaction.php b/src/Composer/DependencyResolver/LocalRepoTransaction.php index 50056e953..b22f555e5 100644 --- a/src/Composer/DependencyResolver/LocalRepoTransaction.php +++ b/src/Composer/DependencyResolver/LocalRepoTransaction.php @@ -24,278 +24,13 @@ use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann */ -class LocalRepoTransaction +class LocalRepoTransaction extends Transaction { - /** @var array */ - protected $lockedPackages; - protected $lockedPackagesByName = array(); - - /** @var RepositoryInterface */ - protected $localRepository; - - /** @var array */ - protected $operations; - - /** - * Reassigns ids for all packages in the lockedrepository - */ public function __construct(RepositoryInterface $lockedRepository, $localRepository) { - $this->localRepository = $localRepository; - $this->setLockedPackageMaps($lockedRepository); - $this->operations = $this->calculateOperations(); - } - - private function setLockedPackageMaps($lockedRepository) - { - $packageSort = function (PackageInterface $a, PackageInterface $b) { - // sort alias packages by the same name behind their non alias version - if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) { - return $a instanceof AliasPackage ? -1 : 1; - } - return strcmp($b->getName(), $a->getName()); - }; - - $id = 1; - $this->lockedPackages = array(); - foreach ($lockedRepository->getPackages() as $package) { - $package->id = $id++; - $this->lockedPackages[$package->id] = $package; - foreach ($package->getNames() as $name) { - $this->lockedPackagesByName[$name][] = $package; - } - } - - uasort($this->lockedPackages, $packageSort); - foreach ($this->lockedPackagesByName as $name => $packages) { - uasort($this->lockedPackagesByName[$name], $packageSort); - } - } - - public function getOperations() - { - return $this->operations; - } - - protected function calculateOperations() - { - $operations = array(); - - $localPackageMap = array(); - $removeMap = array(); - $localAliasMap = array(); - $removeAliasMap = array(); - foreach ($this->localRepository->getPackages() as $package) { - if ($package instanceof AliasPackage) { - $localAliasMap[$package->getName().'::'.$package->getVersion()] = $package; - $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package; - } else { - $localPackageMap[$package->getName()] = $package; - $removeMap[$package->getName()] = $package; - } - } - - $stack = $this->getRootPackages(); - - $visited = array(); - $processed = array(); - - while (!empty($stack)) { - $package = array_pop($stack); - - if (isset($processed[$package->id])) { - continue; - } - - if (!isset($visited[$package->id])) { - $visited[$package->id] = true; - - $stack[] = $package; - if ($package instanceof AliasPackage) { - $stack[] = $package->getAliasOf(); - } else { - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->getLockedProviders($link); - - foreach ($possibleRequires as $require) { - $stack[] = $require; - } - } - } - } elseif (!isset($processed[$package->id])) { - $processed[$package->id] = true; - - if ($package instanceof AliasPackage) { - $aliasKey = $package->getName().'::'.$package->getVersion(); - if (isset($localAliasMap[$aliasKey])) { - unset($removeAliasMap[$aliasKey]); - } else { - $operations[] = new Operation\MarkAliasInstalledOperation($package); - } - } else { - if (isset($localPackageMap[$package->getName()])) { - $source = $localPackageMap[$package->getName()]; - - // do we need to update? - if ($package->getVersion() != $localPackageMap[$package->getName()]->getVersion()) { - $operations[] = new Operation\UpdateOperation($source, $package); - } elseif ($package->isDev() && $package->getSourceReference() !== $localPackageMap[$package->getName()]->getSourceReference()) { - $operations[] = new Operation\UpdateOperation($source, $package); - } - unset($removeMap[$package->getName()]); - } else { - $operations[] = new Operation\InstallOperation($package); - unset($removeMap[$package->getName()]); - } - } - } - } - - foreach ($removeMap as $name => $package) { - array_unshift($operations, new Operation\UninstallOperation($package, null)); - } - foreach ($removeAliasMap as $nameVersion => $package) { - $operations[] = new Operation\MarkAliasUninstalledOperation($package, null); - } - - $operations = $this->movePluginsToFront($operations); - // TODO fix this: - // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls - $operations = $this->moveUninstallsToFront($operations); - - // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? - /* - if ('update' === $jobType) { - $targetPackage = $operation->getTargetPackage(); - if ($targetPackage->isDev()) { - $initialPackage = $operation->getInitialPackage(); - if ($targetPackage->getVersion() === $initialPackage->getVersion() - && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) - && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) - ) { - $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); - $this->io->writeError('', true, IOInterface::DEBUG); - - continue; - } - } - }*/ - - return $operations; - } - - /** - * Determine which packages in the lock file are not required by any other packages in the lock file. - * - * These serve as a starting point to enumerate packages in a topological order despite potential cycles. - * If there are packages with a cycle on the top level the package with the lowest name gets picked - * - * @return array - */ - private function getRootPackages() - { - $roots = $this->lockedPackages; - - foreach ($this->lockedPackages as $packageId => $package) { - if (!isset($roots[$packageId])) { - continue; - } - - foreach ($package->getRequires() as $link) { - $possibleRequires = $this->getLockedProviders($link); - - foreach ($possibleRequires as $require) { - if ($require !== $package) { - unset($roots[$require->id]); - } - } - } - } - - return $roots; - } - - private function getLockedProviders(Link $link) - { - if (!isset($this->lockedPackagesByName[$link->getTarget()])) { - return array(); - } - return $this->lockedPackagesByName[$link->getTarget()]; - } - - /** - * Workaround: if your packages depend on plugins, we must be sure - * that those are installed / updated first; else it would lead to packages - * being installed multiple times in different folders, when running Composer - * twice. - * - * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, - * it at least fixes the symptoms and makes usage of composer possible (again) - * in such scenarios. - * - * @param Operation\OperationInterface[] $operations - * @return Operation\OperationInterface[] reordered operation list - */ - private function movePluginsToFront(array $operations) - { - $pluginsNoDeps = array(); - $pluginsWithDeps = array(); - $pluginRequires = array(); - - foreach (array_reverse($operations, true) as $idx => $op) { - if ($op instanceof Operation\InstallOperation) { - $package = $op->getPackage(); - } elseif ($op instanceof Operation\UpdateOperation) { - $package = $op->getTargetPackage(); - } else { - continue; - } - - // is this package a plugin? - $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; - - // is this a plugin or a dependency of a plugin? - if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { - // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' - $requires = array_filter(array_keys($package->getRequires()), function ($req) { - return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); - }); - - // is this a plugin with no meaningful dependencies? - if ($isPlugin && !count($requires)) { - // plugins with no dependencies go to the very front - array_unshift($pluginsNoDeps, $op); - } else { - // capture the requirements for this package so those packages will be moved up as well - $pluginRequires = array_merge($pluginRequires, $requires); - // move the operation to the front - array_unshift($pluginsWithDeps, $op); - } - - unset($operations[$idx]); - } - } - - return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); - } - - /** - * Removals of packages should be executed before installations in - * case two packages resolve to the same path (due to custom installers) - * - * @param Operation\OperationInterface[] $operations - * @return Operation\OperationInterface[] reordered operation list - */ - private function moveUninstallsToFront(array $operations) - { - $uninstOps = array(); - foreach ($operations as $idx => $op) { - if ($op instanceof UninstallOperation) { - $uninstOps[] = $op; - unset($operations[$idx]); - } - } - - return array_merge($uninstOps, $operations); + parent::__construct( + $localRepository->getPackages(), + $lockedRepository->getPackages() + ); } } diff --git a/src/Composer/DependencyResolver/LockTransaction.php b/src/Composer/DependencyResolver/LockTransaction.php index 76468ddc6..b74cfbc78 100644 --- a/src/Composer/DependencyResolver/LockTransaction.php +++ b/src/Composer/DependencyResolver/LockTransaction.php @@ -23,12 +23,8 @@ use Composer\Test\Repository\ArrayRepositoryTest; /** * @author Nils Adermann */ -class LockTransaction +class LockTransaction extends Transaction { - protected $policy; - /** @var Pool */ - protected $pool; - /** * packages in current lock file, platform repo or otherwise present * @var array @@ -40,107 +36,32 @@ class LockTransaction * @var array */ protected $unlockableMap; - protected $decisions; - protected $resultPackages; /** * @var array */ - protected $operations; + protected $resultPackages; - public function __construct($policy, $pool, $presentMap, $unlockableMap, $decisions) + public function __construct(Pool $pool, $presentMap, $unlockableMap, $decisions) { - $this->policy = $policy; - $this->pool = $pool; $this->presentMap = $presentMap; $this->unlockableMap = $unlockableMap; - $this->decisions = $decisions; - $this->operations = $this->calculateOperations(); - } + $this->setResultPackages($pool, $decisions); + parent::__construct($this->presentMap, $this->resultPackages['all']); - /** - * @return OperationInterface[] - */ - public function getOperations() - { - return $this->operations; - } - - protected function calculateOperations() - { - $operations = array(); - $ignoreRemove = array(); - $lockMeansUpdateMap = $this->findPotentialUpdates(); - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; - - $package = $this->pool->literalToPackage($literal); - - // wanted & !present - if ($literal > 0 && !isset($this->presentMap[spl_object_hash($package)])) { - if (isset($lockMeansUpdateMap[spl_object_hash($package)]) && !$package instanceof AliasPackage) { - // TODO we end up here sometimes because we prefer the remote package now to get up to date metadata - // TODO define some level of identity here for what constitutes an update and what can be ignored? new kind of metadata only update? - $target = $lockMeansUpdateMap[spl_object_hash($package)]; - if ($package->getName() !== $target->getName() || $package->getVersion() !== $target->getVersion()) { - $operations[] = new Operation\UpdateOperation($target, $package, $reason); - } - - // avoid updates to one package from multiple origins - $ignoreRemove[spl_object_hash($lockMeansUpdateMap[spl_object_hash($package)])] = true; - unset($lockMeansUpdateMap[spl_object_hash($package)]); - } else { - if ($package instanceof AliasPackage) { - $operations[] = new Operation\MarkAliasInstalledOperation($package, $reason); - } else { - $operations[] = new Operation\InstallOperation($package, $reason); - } - } - } - } - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $reason = $decision[Decisions::DECISION_REASON]; - $package = $this->pool->literalToPackage($literal); - - if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)]) && !isset($ignoreRemove[spl_object_hash($package)])) { - if ($package instanceof AliasPackage) { - $operations[] = new Operation\MarkAliasUninstalledOperation($package, $reason); - } else { - $operations[] = new Operation\UninstallOperation($package, $reason); - } - } - } - - foreach ($this->presentMap as $package) { - if ($package->id === -1 && !isset($ignoreRemove[spl_object_hash($package)])) { - // TODO pass reason parameter to these two operations? - if ($package instanceof AliasPackage) { - $operations[] = new Operation\MarkAliasUninstalledOperation($package); - } else { - $operations[] = new Operation\UninstallOperation($package); - } - } - } - - $this->setResultPackages(); - - return $operations; } // TODO make this a bit prettier instead of the two text indexes? - public function setResultPackages() + public function setResultPackages(Pool $pool, Decisions $decisions) { - $this->resultPackages = array('non-dev' => array(), 'dev' => array()); - foreach ($this->decisions as $i => $decision) { + $this->resultPackages = array('all' => array(), 'non-dev' => array(), 'dev' => array()); + foreach ($decisions as $i => $decision) { $literal = $decision[Decisions::DECISION_LITERAL]; if ($literal > 0) { - $package = $this->pool->literalToPackage($literal); + $package = $pool->literalToPackage($literal); + $this->resultPackages['all'][] = $package; if (!isset($this->unlockableMap[$package->id])) { $this->resultPackages['non-dev'][] = $package; } @@ -191,46 +112,4 @@ class LockTransaction return $packages; } - - protected function findPotentialUpdates() - { - $lockMeansUpdateMap = array(); - - $packages = array(); - - foreach ($this->decisions as $i => $decision) { - $literal = $decision[Decisions::DECISION_LITERAL]; - $package = $this->pool->literalToPackage($literal); - - if ($literal <= 0 && isset($this->presentMap[spl_object_hash($package)])) { - $packages[spl_object_hash($package)] = $package; - } - } - - // some locked packages are not in the pool and thus, were not decided at all - foreach ($this->presentMap as $package) { - if ($package->id === -1) { - $packages[spl_object_hash($package)] = $package; - } - } - - foreach ($packages as $package) { - if ($package instanceof AliasPackage) { - continue; - } - - // TODO can't we just look at existing rules? - $updates = $this->policy->findUpdatePackages($this->pool, $package); - - $updatesAndPackage = array_merge(array($package), $updates); - - foreach ($updatesAndPackage as $update) { - if (!isset($lockMeansUpdateMap[spl_object_hash($update)])) { - $lockMeansUpdateMap[spl_object_hash($update)] = $package; - } - } - } - - return $lockMeansUpdateMap; - } } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index 66f325446..ad635ff00 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -221,7 +221,7 @@ class Solver throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool); } - return new LockTransaction($this->policy, $this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); + return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); } /** diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php new file mode 100644 index 000000000..f12aeccc5 --- /dev/null +++ b/src/Composer/DependencyResolver/Transaction.php @@ -0,0 +1,310 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\DependencyResolver; + +use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; +use Composer\Package\AliasPackage; +use Composer\Package\Link; +use Composer\Package\PackageInterface; +use Composer\Repository\PlatformRepository; +use Composer\Repository\RepositoryInterface; +use Composer\Semver\Constraint\Constraint; + +/** + * @author Nils Adermann + */ +abstract class Transaction +{ + /** + * @var array + */ + protected $operations; + + /** + * Packages present at the beginning of the transaction + * @var array + */ + protected $presentPackages; + + /** + * Package set resulting from this transaction + * @var array + */ + protected $resultPackageMap; + + /** + * @var array + */ + protected $resultPackagesByName = array(); + + public function __construct($presentPackages, $resultPackages) + { + $this->presentPackages = $presentPackages; + $this->setResultPackageMaps($resultPackages); + $this->operations = $this->calculateOperations(); + + } + + public function getOperations() + { + return $this->operations; + } + + private function setResultPackageMaps($resultPackages) + { + $packageSort = function (PackageInterface $a, PackageInterface $b) { + // sort alias packages by the same name behind their non alias version + if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) { + return $a instanceof AliasPackage ? -1 : 1; + } + return strcmp($b->getName(), $a->getName()); + }; + + $this->resultPackageMap = array(); + foreach ($resultPackages as $package) { + $this->resultPackageMap[spl_object_hash($package)] = $package; + foreach ($package->getNames() as $name) { + $this->resultPackagesByName[$name][] = $package; + } + } + + uasort($this->resultPackageMap, $packageSort); + foreach ($this->resultPackagesByName as $name => $packages) { + uasort($this->resultPackagesByName[$name], $packageSort); + } + } + + protected function calculateOperations() + { + $operations = array(); + + $presentPackageMap = array(); + $removeMap = array(); + $presentAliasMap = array(); + $removeAliasMap = array(); + foreach ($this->presentPackages as $package) { + if ($package instanceof AliasPackage) { + $presentAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package; + } else { + $presentPackageMap[$package->getName()] = $package; + $removeMap[$package->getName()] = $package; + } + } + + $stack = $this->getRootPackages(); + + $visited = array(); + $processed = array(); + + while (!empty($stack)) { + $package = array_pop($stack); + + if (isset($processed[spl_object_hash($package)])) { + continue; + } + + if (!isset($visited[spl_object_hash($package)])) { + $visited[spl_object_hash($package)] = true; + + $stack[] = $package; + if ($package instanceof AliasPackage) { + $stack[] = $package->getAliasOf(); + } else { + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->getProvidersInResult($link); + + foreach ($possibleRequires as $require) { + $stack[] = $require; + } + } + } + } elseif (!isset($processed[spl_object_hash($package)])) { + $processed[spl_object_hash($package)] = true; + + if ($package instanceof AliasPackage) { + $aliasKey = $package->getName().'::'.$package->getVersion(); + if (isset($presentAliasMap[$aliasKey])) { + unset($removeAliasMap[$aliasKey]); + } else { + $operations[] = new Operation\MarkAliasInstalledOperation($package); + } + } else { + if (isset($presentPackageMap[$package->getName()])) { + $source = $presentPackageMap[$package->getName()]; + + // do we need to update? + // TODO different for lock? + if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } elseif ($package->isDev() && $package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference()) { + $operations[] = new Operation\UpdateOperation($source, $package); + } + unset($removeMap[$package->getName()]); + } else { + $operations[] = new Operation\InstallOperation($package); + unset($removeMap[$package->getName()]); + } + } + } + } + + foreach ($removeMap as $name => $package) { + array_unshift($operations, new Operation\UninstallOperation($package, null)); + } + foreach ($removeAliasMap as $nameVersion => $package) { + $operations[] = new Operation\MarkAliasUninstalledOperation($package, null); + } + + $operations = $this->movePluginsToFront($operations); + // TODO fix this: + // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls + $operations = $this->moveUninstallsToFront($operations); + + // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place? + /* + if ('update' === $jobType) { + $targetPackage = $operation->getTargetPackage(); + if ($targetPackage->isDev()) { + $initialPackage = $operation->getInitialPackage(); + if ($targetPackage->getVersion() === $initialPackage->getVersion() + && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference()) + && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference()) + ) { + $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG); + $this->io->writeError('', true, IOInterface::DEBUG); + + continue; + } + } + }*/ + + return $this->operations = $operations; + } + + /** + * Determine which packages in the result are not required by any other packages in it. + * + * These serve as a starting point to enumerate packages in a topological order despite potential cycles. + * If there are packages with a cycle on the top level the package with the lowest name gets picked + * + * @return array + */ + protected function getRootPackages() + { + $roots = $this->resultPackageMap; + + foreach ($this->resultPackageMap as $packageHash => $package) { + if (!isset($roots[$packageHash])) { + continue; + } + + foreach ($package->getRequires() as $link) { + $possibleRequires = $this->getProvidersInResult($link); + + foreach ($possibleRequires as $require) { + if ($require !== $package) { + unset($roots[spl_object_hash($require)]); + } + } + } + } + + return $roots; + } + + protected function getProvidersInResult(Link $link) + { + if (!isset($this->resultPackagesByName[$link->getTarget()])) { + return array(); + } + return $this->resultPackagesByName[$link->getTarget()]; + } + + /** + * Workaround: if your packages depend on plugins, we must be sure + * that those are installed / updated first; else it would lead to packages + * being installed multiple times in different folders, when running Composer + * twice. + * + * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147, + * it at least fixes the symptoms and makes usage of composer possible (again) + * in such scenarios. + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function movePluginsToFront(array $operations) + { + $pluginsNoDeps = array(); + $pluginsWithDeps = array(); + $pluginRequires = array(); + + foreach (array_reverse($operations, true) as $idx => $op) { + if ($op instanceof Operation\InstallOperation) { + $package = $op->getPackage(); + } elseif ($op instanceof Operation\UpdateOperation) { + $package = $op->getTargetPackage(); + } else { + continue; + } + + // is this package a plugin? + $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer'; + + // is this a plugin or a dependency of a plugin? + if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) { + // get the package's requires, but filter out any platform requirements or 'composer-plugin-api' + $requires = array_filter(array_keys($package->getRequires()), function ($req) { + return $req !== 'composer-plugin-api' && !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req); + }); + + // is this a plugin with no meaningful dependencies? + if ($isPlugin && !count($requires)) { + // plugins with no dependencies go to the very front + array_unshift($pluginsNoDeps, $op); + } else { + // capture the requirements for this package so those packages will be moved up as well + $pluginRequires = array_merge($pluginRequires, $requires); + // move the operation to the front + array_unshift($pluginsWithDeps, $op); + } + + unset($operations[$idx]); + } + } + + return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations); + } + + /** + * Removals of packages should be executed before installations in + * case two packages resolve to the same path (due to custom installers) + * + * @param Operation\OperationInterface[] $operations + * @return Operation\OperationInterface[] reordered operation list + */ + private function moveUninstallsToFront(array $operations) + { + $uninstOps = array(); + foreach ($operations as $idx => $op) { + if ($op instanceof UninstallOperation) { + $uninstOps[] = $op; + unset($operations[$idx]); + } + } + + return array_merge($uninstOps, $operations); + } +} diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 8b5c85057..941717912 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -486,23 +486,6 @@ class Installer if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { $this->io->writeError(' - ' . $operation); } - - // output reasons why the operation was run, only for install/update operations - if ($this->verbose && $this->io->isVeryVerbose() && in_array($jobType, array('install', 'update'))) { - $reason = $operation->getReason(); - if ($reason instanceof Rule) { - switch ($reason->getReason()) { - case Rule::RULE_JOB_INSTALL: - $this->io->writeError(' REASON: Required by the root package: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - case Rule::RULE_PACKAGE_REQUIRES: - $this->io->writeError(' REASON: '.$reason->getPrettyString($pool)); - $this->io->writeError(''); - break; - } - } - } } $updatedLock = $this->locker->setLockData( diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index 8f12f3122..4c733a4fb 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -168,9 +168,9 @@ class SolverTest extends TestCase $this->request->install('C'); $this->checkSolverResult(array( + array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageC), array('job' => 'install', 'package' => $packageB), - array('job' => 'install', 'package' => $packageA), )); } @@ -338,15 +338,15 @@ class SolverTest extends TestCase $this->request->install('A', $this->getVersionConstraint('<', '2.0.0.0')); $this->checkSolverResult(array( + array( + 'job' => 'remove', + 'package' => $packageB, + ), array( 'job' => 'update', 'from' => $packageA, 'to' => $newPackageA, ), - array( - 'job' => 'remove', - 'package' => $packageB, - ), )); } @@ -369,10 +369,10 @@ class SolverTest extends TestCase $this->request->remove('D'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageB), - array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), - array('job' => 'install', 'package' => $packageA), array('job' => 'remove', 'package' => $packageD), + array('job' => 'install', 'package' => $packageB), + array('job' => 'install', 'package' => $packageA), + array('job' => 'update', 'from' => $oldPackageC, 'to' => $packageC), )); } @@ -406,7 +406,8 @@ class SolverTest extends TestCase $this->request->install('B'); $this->checkSolverResult(array( - array('job' => 'update', 'from' => $packageA, 'to' => $packageB), + array('job' => 'remove', 'package' => $packageA), + array('job' => 'install', 'package' => $packageB), )); } @@ -526,8 +527,8 @@ class SolverTest extends TestCase $this->request->install('X'); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $newPackageB), + array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageX), )); } @@ -571,8 +572,8 @@ class SolverTest extends TestCase $this->checkSolverResult(array( array('job' => 'install', 'package' => $packageB), - array('job' => 'install', 'package' => $packageC), array('job' => 'install', 'package' => $packageA), + array('job' => 'install', 'package' => $packageC), )); } @@ -829,9 +830,9 @@ class SolverTest extends TestCase $this->request->install('B'); $this->checkSolverResult(array( + array('job' => 'install', 'package' => $packageA), array('job' => 'install', 'package' => $packageAAlias), array('job' => 'install', 'package' => $packageB), - array('job' => 'install', 'package' => $packageA), )); } @@ -894,12 +895,12 @@ class SolverTest extends TestCase $this->assertFalse($this->solver->testFlagLearnedPositiveLiteral); $this->checkSolverResult(array( - array('job' => 'install', 'package' => $packageC2), - array('job' => 'install', 'package' => $packageG2), array('job' => 'install', 'package' => $packageF1), + array('job' => 'install', 'package' => $packageD), + array('job' => 'install', 'package' => $packageG2), + array('job' => 'install', 'package' => $packageC2), array('job' => 'install', 'package' => $packageE), array('job' => 'install', 'package' => $packageB), - array('job' => 'install', 'package' => $packageD), array('job' => 'install', 'package' => $packageA), )); diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index cdf648c0d..7c671565d 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -27,8 +27,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing c/c (1.0.0) - Installing a/a (1.0.0) + - Installing c/c (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index 878e9429a..e7730cefc 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -55,8 +55,8 @@ update a/a b/b --with-dependencies Loading composer repositories with package information Updating dependencies Lock file operations: 0 installs, 2 updates, 0 removals - - Updating b/b (1.0.0) to b/b (1.1.0) - Updating a/a (1.0.0) to a/a (1.1.0) + - Updating b/b (1.0.0) to b/b (1.1.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index 6995f78e7..e53ab0065 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -22,8 +22,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing b/b (1.0.0) - Installing a/a (1.0.0) + - Installing b/b (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test index 14d8e13d8..8a72cec66 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -50,8 +50,8 @@ update b/b --with-all-dependencies Loading composer repositories with package information Updating dependencies Lock file operations: 0 installs, 2 updates, 0 removals - - Updating b/b (1.0.0) to b/b (1.1.0) - Updating a/a (1.0.0) to a/a (1.1.0) + - Updating b/b (1.0.0) to b/b (1.1.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 0 installs, 2 updates, 0 removals From 3cbe91983c63e0231d455abbf76632a80db74678 Mon Sep 17 00:00:00 2001 From: Nils Adermann Date: Fri, 8 Nov 2019 16:48:35 +0100 Subject: [PATCH 37/37] Display Locking instead of Installing for lock file install operations --- .../DependencyResolver/Operation/InstallOperation.php | 10 +++++++++- .../Operation/MarkAliasInstalledOperation.php | 10 +++++++++- .../Operation/MarkAliasUninstalledOperation.php | 10 +++++++++- .../Operation/OperationInterface.php | 8 ++++++++ .../DependencyResolver/Operation/SolverOperation.php | 6 ++++++ .../Operation/UninstallOperation.php | 10 +++++++++- .../DependencyResolver/Operation/UpdateOperation.php | 10 +++++++++- src/Composer/DependencyResolver/Transaction.php | 3 +-- src/Composer/Installer.php | 4 ++-- .../Test/Fixtures/installer/abandoned-listed.test | 4 ++-- .../Test/Fixtures/installer/suggest-installed.test | 4 ++-- .../Composer/Test/Fixtures/installer/suggest-prod.test | 2 +- .../Test/Fixtures/installer/suggest-replaced.test | 4 ++-- .../Test/Fixtures/installer/suggest-uninstalled.test | 2 +- 14 files changed, 70 insertions(+), 17 deletions(-) diff --git a/src/Composer/DependencyResolver/Operation/InstallOperation.php b/src/Composer/DependencyResolver/Operation/InstallOperation.php index 08c659c49..569ae67a4 100644 --- a/src/Composer/DependencyResolver/Operation/InstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/InstallOperation.php @@ -56,11 +56,19 @@ class InstallOperation extends SolverOperation return 'install'; } + /** + * {@inheritDoc} + */ + public function show($lock) + { + return ($lock ? 'Locking ' : 'Installing ').$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; + } + /** * {@inheritDoc} */ public function __toString() { - return 'Installing '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; + return $this->show(false); } } diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php index 920e54e67..800cf43c2 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasInstalledOperation.php @@ -60,8 +60,16 @@ class MarkAliasInstalledOperation extends SolverOperation /** * {@inheritDoc} */ - public function __toString() + public function show($lock) { return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as installed, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')'; } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->show(false); + } } diff --git a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php index 77f3aef8c..3c996f343 100644 --- a/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php +++ b/src/Composer/DependencyResolver/Operation/MarkAliasUninstalledOperation.php @@ -60,8 +60,16 @@ class MarkAliasUninstalledOperation extends SolverOperation /** * {@inheritDoc} */ - public function __toString() + public function show($lock) { return 'Marking '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).') as uninstalled, alias of '.$this->package->getAliasOf()->getPrettyName().' ('.$this->formatVersion($this->package->getAliasOf()).')'; } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->show(false); + } } diff --git a/src/Composer/DependencyResolver/Operation/OperationInterface.php b/src/Composer/DependencyResolver/Operation/OperationInterface.php index 330cbceb1..261a746af 100644 --- a/src/Composer/DependencyResolver/Operation/OperationInterface.php +++ b/src/Composer/DependencyResolver/Operation/OperationInterface.php @@ -33,6 +33,14 @@ interface OperationInterface */ public function getReason(); + /** + * Serializes the operation in a human readable format + * + * @param $lock bool Whether this is an operation on the lock file + * @return string + */ + public function show($lock); + /** * Serializes the operation in a human readable format * diff --git a/src/Composer/DependencyResolver/Operation/SolverOperation.php b/src/Composer/DependencyResolver/Operation/SolverOperation.php index e1a68585e..bb733b70f 100644 --- a/src/Composer/DependencyResolver/Operation/SolverOperation.php +++ b/src/Composer/DependencyResolver/Operation/SolverOperation.php @@ -43,6 +43,12 @@ abstract class SolverOperation implements OperationInterface return $this->reason; } + /** + * @param $lock bool Whether this is an operation on the lock file + * @return string + */ + abstract public function show($lock); + protected function formatVersion(PackageInterface $package) { return $package->getFullPrettyVersion(); diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php index b4a73811e..73321f96f 100644 --- a/src/Composer/DependencyResolver/Operation/UninstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -59,8 +59,16 @@ class UninstallOperation extends SolverOperation /** * {@inheritDoc} */ - public function __toString() + public function show($lock) { return 'Uninstalling '.$this->package->getPrettyName().' ('.$this->formatVersion($this->package).')'; } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->show(false); + } } diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php index 836725ef5..aecde33c2 100644 --- a/src/Composer/DependencyResolver/Operation/UpdateOperation.php +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -72,9 +72,17 @@ class UpdateOperation extends SolverOperation /** * {@inheritDoc} */ - public function __toString() + public function show($lock) { return 'Updating '.$this->initialPackage->getPrettyName().' ('.$this->formatVersion($this->initialPackage).') to '. $this->targetPackage->getPrettyName(). ' ('.$this->formatVersion($this->targetPackage).')'; } + + /** + * {@inheritDoc} + */ + public function __toString() + { + return $this->show(false); + } } diff --git a/src/Composer/DependencyResolver/Transaction.php b/src/Composer/DependencyResolver/Transaction.php index f12aeccc5..6496327ff 100644 --- a/src/Composer/DependencyResolver/Transaction.php +++ b/src/Composer/DependencyResolver/Transaction.php @@ -24,7 +24,7 @@ use Composer\Semver\Constraint\Constraint; /** * @author Nils Adermann */ -abstract class Transaction +class Transaction { /** * @var array @@ -53,7 +53,6 @@ abstract class Transaction $this->presentPackages = $presentPackages; $this->setResultPackageMaps($resultPackages); $this->operations = $this->calculateOperations(); - } public function getOperations() diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 941717912..9f014be1e 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -484,7 +484,7 @@ class Installer // output op, but alias op only in debug verbosity if (false === strpos($operation->getJobType(), 'Alias') || $this->io->isDebug()) { - $this->io->writeError(' - ' . $operation); + $this->io->writeError(' - ' . $operation->show(true)); } } @@ -673,7 +673,7 @@ class Installer // output op, but alias op only in debug verbosity if ((!$this->executeOperations && false === strpos($operation->getJobType(), 'Alias')) || $this->io->isDebug()) { - $this->io->writeError(' - ' . $operation); + $this->io->writeError(' - ' . $operation->show(false)); } $this->installationManager->execute($localRepo, $operation); diff --git a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test index 7c671565d..3ad5b0428 100644 --- a/tests/Composer/Test/Fixtures/installer/abandoned-listed.test +++ b/tests/Composer/Test/Fixtures/installer/abandoned-listed.test @@ -27,8 +27,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing a/a (1.0.0) - - Installing c/c (1.0.0) + - Locking a/a (1.0.0) + - Locking c/c (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-installed.test b/tests/Composer/Test/Fixtures/installer/suggest-installed.test index e53ab0065..413ac8665 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-installed.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-installed.test @@ -22,8 +22,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing a/a (1.0.0) - - Installing b/b (1.0.0) + - Locking a/a (1.0.0) + - Locking b/b (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-prod.test b/tests/Composer/Test/Fixtures/installer/suggest-prod.test index 89d6ab8de..6a00b5607 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-prod.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-prod.test @@ -21,7 +21,7 @@ install --no-dev Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals - - Installing a/a (1.0.0) + - Locking a/a (1.0.0) Writing lock file Installing dependencies from lock file Package operations: 1 install, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test index 62c13e560..e56af5093 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-replaced.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-replaced.test @@ -22,8 +22,8 @@ update Loading composer repositories with package information Updating dependencies Lock file operations: 2 installs, 0 updates, 0 removals - - Installing c/c (1.0.0) - - Installing a/a (1.0.0) + - Locking c/c (1.0.0) + - Locking a/a (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 2 installs, 0 updates, 0 removals diff --git a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test index f45b710f0..1b88b2d8b 100644 --- a/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test +++ b/tests/Composer/Test/Fixtures/installer/suggest-uninstalled.test @@ -21,7 +21,7 @@ install Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals - - Installing a/a (1.0.0) + - Locking a/a (1.0.0) Writing lock file Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals