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);