diff --git a/src/Composer/Command/BaseDependencyCommand.php b/src/Composer/Command/BaseDependencyCommand.php index 00fed9240..be7703f02 100644 --- a/src/Composer/Command/BaseDependencyCommand.php +++ b/src/Composer/Command/BaseDependencyCommand.php @@ -89,7 +89,7 @@ class BaseDependencyCommand extends BaseCommand ); // Find packages that are or provide the requested package first - $packages = $repositorySet->findPackages(strtolower($needle), null, false); + $packages = $repositorySet->findPackages(strtolower($needle), null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); if (empty($packages)) { throw new \InvalidArgumentException(sprintf('Could not find package "%s" in your project', $needle)); } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index bcfc67fea..360924143 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -31,7 +31,6 @@ class PoolBuilder private $stabilityFlags; private $rootAliases; private $rootReferences; - private $rootRequires; private $aliasMap = array(); private $nameConstraints = array(); @@ -39,13 +38,12 @@ class PoolBuilder private $packages = array(); private $unacceptableFixedPackages = array(); - public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences, array $rootRequires = array()) + public function __construct(array $acceptableStabilities, array $stabilityFlags, array $rootAliases, array $rootReferences) { $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; - $this->rootRequires = $rootRequires; } public function buildPool(array $repositories, Request $request) diff --git a/src/Composer/DependencyResolver/Problem.php b/src/Composer/DependencyResolver/Problem.php index 5291bdfaa..82e649e3f 100644 --- a/src/Composer/DependencyResolver/Problem.php +++ b/src/Composer/DependencyResolver/Problem.php @@ -13,6 +13,8 @@ namespace Composer\DependencyResolver; use Composer\Package\CompletePackageInterface; +use Composer\Repository\RepositorySet; +use Composer\Semver\Constraint\Constraint; /** * Represents a problem detected while solving dependencies @@ -35,13 +37,6 @@ class Problem protected $section = 0; - protected $pool; - - public function __construct(Pool $pool) - { - $this->pool = $pool; - } - /** * Add a rule as a reason * @@ -68,7 +63,7 @@ class Problem * @param array $installedMap A map of all present packages * @return string */ - public function getPrettyString(array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) { // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections? $reasons = call_user_func_array('array_merge', array_reverse($this->reasons)); @@ -81,91 +76,25 @@ class Problem throw new \LogicException("Single reason problems must contain a request rule."); } - $request = $rule->getReasonData(); - $packageName = $request['packageName']; - $constraint = $request['constraint']; + $reasonData = $rule->getReasonData(); + $packageName = $reasonData['packageName']; + $constraint = $reasonData['constraint']; if (isset($constraint)) { - $packages = $this->pool->whatProvides($packageName, $constraint); + $packages = $pool->whatProvides($packageName, $constraint); } else { $packages = array(); } if (empty($packages)) { - // handle php/hhvm - if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { - $version = phpversion(); - $available = $this->pool->whatProvides($packageName); - - if (count($available)) { - $firstAvailable = reset($available); - $version = $firstAvailable->getPrettyVersion(); - $extra = $firstAvailable->getExtra(); - if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { - $version .= '; ' . $firstAvailable->getDescription(); - } - } - - $msg = "\n - This package requires ".$packageName.$this->constraintToText($constraint).' but '; - - if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) { - return $msg . 'your HHVM version does not satisfy that requirement.'; - } - - if ($packageName === 'hhvm') { - return $msg . 'you are running this with PHP and not HHVM.'; - } - - return $msg . 'your PHP version ('. $version .') does not satisfy that requirement.'; - } - - // handle php extensions - if (0 === stripos($packageName, 'ext-')) { - if (false !== strpos($packageName, ' ')) { - return "\n - The requested PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.'; - } - - $ext = substr($packageName, 4); - $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system'; - - return "\n - The requested PHP extension ".$packageName.$this->constraintToText($constraint).' '.$error.'. Install or enable PHP\'s '.$ext.' extension.'; - } - - // handle linked libs - if (0 === stripos($packageName, 'lib-')) { - if (strtolower($packageName) === 'lib-icu') { - $error = extension_loaded('intl') ? 'has the wrong version installed, try upgrading the intl extension.' : 'is missing from your system, make sure the intl extension is loaded.'; - - return "\n - The requested linked library ".$packageName.$this->constraintToText($constraint).' '.$error; - } - - return "\n - The requested linked library ".$packageName.$this->constraintToText($constraint).' has the wrong version installed or is missing from your system, make sure to load the extension providing it.'; - } - - if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) { - $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName); - - return "\n - The requested package ".$packageName.' could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'; - } - - // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet - /*if ($providers = $this->pool->whatProvides($packageName, $constraint, true, true)) { - return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' is satisfiable by '.$this->getPackageList($providers).' but these conflict with your requirements or minimum-stability.'; - }*/ - - // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet - /*if ($providers = $this->pool->whatProvides($packageName, null, true, true)) { - return "\n - The requested package ".$packageName.$this->constraintToText($constraint).' exists as '.$this->getPackageList($providers).' but these are rejected by your constraint.'; - }*/ - - return "\n - The requested package ".$packageName.' could not be found in any version, there may be a typo in the package name.'; + return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint)); } } $messages = array(); foreach ($reasons as $rule) { - $messages[] = $rule->getPrettyString($this->pool, $installedMap, $learnedPool); + $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); } return "\n - ".implode("\n - ", $messages); @@ -193,7 +122,150 @@ class Problem $this->section++; } - protected function getPackageList($packages) + /** + * @internal + */ + public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null) + { + // handle php/hhvm + if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') { + $version = phpversion(); + $available = $pool->whatProvides($packageName); + + if (count($available)) { + $firstAvailable = reset($available); + $version = $firstAvailable->getPrettyVersion(); + $extra = $firstAvailable->getExtra(); + if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) { + $version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription()); + } + } + + $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but '; + + if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) { + return array($msg, 'your HHVM version does not satisfy that requirement.'); + } + + if ($packageName === 'hhvm') { + return array($msg, 'you are running this with PHP and not HHVM.'); + } + + return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.'); + } + + // handle php extensions + if (0 === stripos($packageName, 'ext-')) { + if (false !== strpos($packageName, ' ')) { + return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.'); + } + + $ext = substr($packageName, 4); + $error = extension_loaded($ext) ? 'it has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'it is missing from your system'; + + return array("- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but ', $error.'. Install or enable PHP\'s '.$ext.' extension.'); + } + + // handle linked libs + if (0 === stripos($packageName, 'lib-')) { + if (strtolower($packageName) === 'lib-icu') { + $error = extension_loaded('intl') ? 'it has the wrong version installed, try upgrading the intl extension.' : 'it is missing from your system, make sure the intl extension is loaded.'; + + return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error); + } + + return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', 'it has the wrong version installed or is missing from your system, make sure to load the extension providing it.'); + } + + $fixedPackage = null; + foreach ($request->getFixedPackages() as $package) { + if ($package->getName() === $packageName) { + $fixedPackage = $package; + if ($pool->isUnacceptableFixedPackage($package)) { + return array("- ", $package->getPrettyName().' is fixed to '.$package->getPrettyVersion().' (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you whitelist it for update.'); + } + break; + } + } + + // first check if the actual requested package is found in normal conditions + // if so it must mean it is rejected by another constraint than the one given here + if ($packages = $repositorySet->findPackages($packageName, $constraint)) { + $rootReqs = $repositorySet->getRootRequires(); + if (isset($rootReqs[$packageName])) { + $filtered = array_filter($packages, function ($p) use ($rootReqs, $packageName) { + return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion())); + }); + if (0 === count($filtered)) { + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with your root composer.json require ('.$rootReqs[$packageName]->getPrettyString().').'); + } + } + + if ($fixedPackage) { + $fixedConstraint = new Constraint('==', $fixedPackage->getVersion()); + $filtered = array_filter($packages, function ($p) use ($fixedConstraint) { + return $fixedConstraint->matches(new Constraint('==', $p->getVersion())); + }); + if (0 === count($filtered)) { + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but the package is fixed to '.$fixedPackage->getPrettyVersion().' (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update.'); + } + } + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these conflict' : 'it conflicts').' with another require.'); + } + + // check if the package is found when bypassing stability checks + if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) { + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your minimum-stability.'); + } + + // check if the package is found when bypassing the constraint check + if ($packages = $repositorySet->findPackages($packageName, null)) { + // we must first verify if a valid package would be found in a lower priority repository + if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) { + $higherRepoPackages = $repositorySet->findPackages($packageName, null); + $nextRepoPackages = array(); + $nextRepo = null; + + foreach ($allReposPackages as $package) { + if ($nextRepo === null || $nextRepo === $package->getRepository()) { + $nextRepoPackages[] = $package; + $nextRepo = $package->getRepository(); + } else { + break; + } + } + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', it is ', 'satisfiable by '.self::getPackageList($nextRepoPackages).' from '.$nextRepo->getRepoName().' but '.self::getPackageList($higherRepoPackages).' from '.reset($higherRepoPackages)->getRepository()->getRepoName().' has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable.'); + } + + return array("- Root composer.json requires $packageName".self::constraintToText($constraint) . ', ', 'found '.self::getPackageList($packages).' but '.(self::hasMultipleNames($packages) ? 'these do' : 'it does').' not match your constraint.'); + } + + if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) { + $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName); + + return array("- Root composer.json requires $packageName, it ", 'could not be found, it looks like its name is invalid, "'.$illegalChars.'" is not allowed in package names.'); + } + + if ($providers = $repositorySet->getProviders($packageName)) { + $maxProviders = 20; + $providersStr = implode(array_map(function ($p) { + return " - ${p['name']} ".substr($p['description'], 0, 100)."\n"; + }, count($providers) > $maxProviders+1 ? array_slice($providers, 0, $maxProviders) : $providers)); + if (count($providers) > $maxProviders+1) { + $providersStr .= ' ... and '.(count($providers)-$maxProviders).' more.'."\n"; + } + return array("- Root composer.json requires $packageName".self::constraintToText($constraint).", it ", "could not be found in any version, but the following packages provide it: \n".$providersStr." Consider requiring one of these to satisfy the $packageName requirement."); + } + + return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name."); + } + + /** + * @internal + */ + public static function getPackageList(array $packages) { $prepared = array(); foreach ($packages as $package) { @@ -207,13 +279,27 @@ class Problem return implode(', ', $prepared); } + private static function hasMultipleNames(array $packages) + { + $name = null; + foreach ($packages as $package) { + if ($name === null || $name === $package->getName()) { + $name = $package->getName(); + } else { + return true; + } + } + + return false; + } + /** * Turns a constraint into text usable in a sentence describing a request * * @param \Composer\Semver\Constraint\ConstraintInterface $constraint * @return string */ - protected function constraintToText($constraint) + protected static function constraintToText($constraint) { return $constraint ? ' '.$constraint->getPrettyString() : ''; } diff --git a/src/Composer/DependencyResolver/Rule.php b/src/Composer/DependencyResolver/Rule.php index fa4a0f574..58f6962cd 100644 --- a/src/Composer/DependencyResolver/Rule.php +++ b/src/Composer/DependencyResolver/Rule.php @@ -15,6 +15,7 @@ namespace Composer\DependencyResolver; use Composer\Package\CompletePackage; use Composer\Package\Link; use Composer\Package\PackageInterface; +use Composer\Repository\RepositorySet; /** * @author Nils Adermann @@ -122,7 +123,7 @@ abstract class Rule abstract public function isAssertion(); - public function getPrettyString(Pool $pool, array $installedMap = array(), array $learnedPool = array()) + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array()) { $literals = $this->getLiterals(); @@ -161,7 +162,7 @@ abstract class Rule $package1 = $pool->literalToPackage($literals[0]); $package2 = $pool->literalToPackage($literals[1]); - return $package1->getPrettyString().' conflicts with '.$this->formatPackagesUnique($pool, array($package2)).'.'; + return $package2->getPrettyString().' conflicts with '.$package1->getPrettyString().'.'; case self::RULE_PACKAGE_REQUIRES: $sourceLiteral = array_shift($literals); @@ -178,85 +179,103 @@ abstract class Rule } else { $targetName = $this->reasonData->getTarget(); - if ($targetName === 'php' || $targetName === 'php-64bit' || $targetName === 'hhvm') { - // handle php/hhvm - if (defined('HHVM_VERSION')) { - return $text . ' -> your HHVM version does not satisfy that requirement.'; - } + $reason = Problem::getMissingPackageReason($repositorySet, $request, $pool, $targetName, $this->reasonData->getConstraint()); - $packages = $pool->whatProvides($targetName); - $package = count($packages) ? current($packages) : phpversion(); - - if ($targetName === 'hhvm') { - if ($package instanceof CompletePackage) { - return $text . ' -> your HHVM version ('.$package->getPrettyVersion().') does not satisfy that requirement.'; - } else { - return $text . ' -> you are running this with PHP and not HHVM.'; - } - } - - - if (!($package instanceof CompletePackage)) { - return $text . ' -> your PHP version ('.phpversion().') does not satisfy that requirement.'; - } - - $extra = $package->getExtra(); - - if (!empty($extra['config.platform'])) { - $text .= ' -> your PHP version ('.phpversion().') overridden by "config.platform.php" version ('.$package->getPrettyVersion().') does not satisfy that requirement.'; - } else { - $text .= ' -> your PHP version ('.$package->getPrettyVersion().') does not satisfy that requirement.'; - } - - return $text; - } - - if (0 === strpos($targetName, 'ext-')) { - // handle php extensions - $ext = substr($targetName, 4); - $error = extension_loaded($ext) ? 'has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'is missing from your system'; - - return $text . ' -> the requested PHP extension '.$ext.' '.$error.'.'; - } - - if (0 === strpos($targetName, 'lib-')) { - // handle linked libs - $lib = substr($targetName, 4); - - return $text . ' -> the requested linked library '.$lib.' has the wrong version installed or is missing from your system, make sure to have the extension providing it.'; - } - - // TODO: The pool doesn't know about these anymore, it has to ask the RepositorySet - /*if ($providers = $pool->whatProvides($targetName, $this->reasonData->getConstraint(), true, true)) { - return $text . ' -> satisfiable by ' . $this->formatPackagesUnique($pool, $providers) .' but these conflict with your requirements or minimum-stability.'; - }*/ - - return $text . ' -> no matching package found.'; + return $text . ' -> ' . $reason[1]; } return $text; case self::RULE_PACKAGE_OBSOLETES: + if (count($literals) === 2 && $literals[0] < 0 && $literals[1] < 0) { + $package1 = $pool->literalToPackage($literals[0]); + $package2 = $pool->literalToPackage($literals[1]); + + $replaces1 = $this->getReplacedNames($package1); + $replaces2 = $this->getReplacedNames($package2); + + $reason = null; + if ($conflictingNames = array_values(array_intersect($replaces1, $replaces2))) { + $reason = 'They both replace '.(count($conflictingNames) > 1 ? '['.implode(', ', $conflictingNames).']' : $conflictingNames[0]).' and can thus not coexist.'; + } elseif (in_array($package1->getName(), $replaces2, true)) { + $reason = $package2->getName().' replaces '.$package1->getName().' and can thus not coexist with it.'; + } elseif (in_array($package2->getName(), $replaces1, true)) { + $reason = $package1->getName().' replaces '.$package2->getName().' and can thus not coexist with it.'; + } + + if ($reason) { + if (isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { + // swap vars so the if below passes + $tmp = $package2; + $package2 = $package1; + $package1 = $tmp; + } + if (!isset($installedMap[$package1->id]) && isset($installedMap[$package2->id])) { + return $package1->getPrettyString().' can not be installed as that would require removing '.$package2->getPrettyString().'. '.$reason; + } + + if (!isset($installedMap[$package1->id]) && !isset($installedMap[$package2->id])) { + return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'. '.$reason; + } + } + + return 'Only one of these can be installed: '.$package1->getPrettyString().', '.$package2->getPrettyString().'.'; + } + return $ruleText; case self::RULE_INSTALLED_PACKAGE_OBSOLETES: return $ruleText; case self::RULE_PACKAGE_SAME_NAME: - return 'Same name, can only install one of: ' . $this->formatPackagesUnique($pool, $literals) . '.'; + $replacedNames = null; + $packageNames = array(); + foreach ($literals as $literal) { + $package = $pool->literalToPackage($literal); + $pkgReplaces = $this->getReplacedNames($package); + if ($pkgReplaces) { + if ($replacedNames === null) { + $replacedNames = $this->getReplacedNames($package); + } else { + $replacedNames = array_intersect($replacedNames, $this->getReplacedNames($package)); + } + } + $packageNames[$package->getName()] = true; + } + + if ($replacedNames) { + $replacedNames = array_values(array_intersect(array_keys($packageNames), $replacedNames)); + } + if ($replacedNames && count($packageNames) > 1) { + $replacer = null; + foreach ($literals as $literal) { + $package = $pool->literalToPackage($literal); + if (array_intersect($replacedNames, $this->getReplacedNames($package))) { + $replacer = $package; + break; + } + } + $replacedNames = count($replacedNames) > 1 ? '['.implode(', ', $replacedNames).']' : $replacedNames[0]; + + if ($replacer) { + return 'Only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '. '.$replacer->getName().' replaces '.$replacedNames.' and can thus not coexist with it.'; + } + } + + return 'You can only install one version of a package, so only one of these can be installed: ' . $this->formatPackagesUnique($pool, $literals) . '.'; case self::RULE_PACKAGE_IMPLICIT_OBSOLETES: return $ruleText; case self::RULE_LEARNED: - // TODO not sure this is a good idea, most of these rules should be listed in the problem anyway - $learnedString = '(learned rule, '; if (isset($learnedPool[$this->reasonData])) { + $learnedString = ', learned rules:'."\n - "; + $reasons = array(); foreach ($learnedPool[$this->reasonData] as $learnedRule) { - $learnedString .= $learnedRule->getPrettyString($pool, $installedMap, $learnedPool); + $reasons[] = $learnedRule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool); } + $learnedString .= implode("\n - ", array_unique($reasons)); } else { - $learnedString .= 'reasoning unavailable'; + $learnedString = ' (reasoning unavailable)'; } - $learnedString .= ')'; - return 'Conclusion: '.$ruleText.' '.$learnedString; + return 'Conclusion: '.$ruleText.$learnedString; case self::RULE_PACKAGE_ALIAS: return $ruleText; default: @@ -272,20 +291,23 @@ abstract class Rule */ protected function formatPackagesUnique($pool, array $packages) { - // TODO this is essentially a duplicate of Problem: getPackageList, maintain in one place only? - $prepared = array(); - foreach ($packages as $package) { + foreach ($packages as $index => $package) { if (!is_object($package)) { - $package = $pool->literalToPackage($package); + $packages[$index] = $pool->literalToPackage($package); } - $prepared[$package->getName()]['name'] = $package->getPrettyName(); - $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion(); - } - foreach ($prepared as $name => $package) { - $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']'; } - return implode(', ', $prepared); + return Problem::getPackageList($packages); + } + + private function getReplacedNames(PackageInterface $package) + { + $names = array(); + foreach ($package->getReplaces() as $link) { + $names[] = $link->getTarget(); + } + + return $names; } } diff --git a/src/Composer/DependencyResolver/RuleSet.php b/src/Composer/DependencyResolver/RuleSet.php index 3db54d9df..d37ca1e9f 100644 --- a/src/Composer/DependencyResolver/RuleSet.php +++ b/src/Composer/DependencyResolver/RuleSet.php @@ -12,6 +12,8 @@ namespace Composer\DependencyResolver; +use Composer\Repository\RepositorySet; + /** * @author Nils Adermann */ @@ -155,13 +157,13 @@ class RuleSet implements \IteratorAggregate, \Countable return array_keys($types); } - public function getPrettyString(Pool $pool = null) + public function getPrettyString(RepositorySet $repositorySet = null, Request $request = null, Pool $pool = null) { $string = "\n"; foreach ($this->rules as $type => $rules) { $string .= str_pad(self::$types[$type], 8, ' ') . ": "; foreach ($rules as $rule) { - $string .= ($pool ? $rule->getPrettyString($pool) : $rule)."\n"; + $string .= ($repositorySet && $request && $pool ? $rule->getPrettyString($repositorySet, $request, $pool) : $rule)."\n"; } $string .= "\n\n"; } @@ -171,6 +173,6 @@ class RuleSet implements \IteratorAggregate, \Countable public function __toString() { - return $this->getPrettyString(null); + return $this->getPrettyString(null, null, null); } } diff --git a/src/Composer/DependencyResolver/Solver.php b/src/Composer/DependencyResolver/Solver.php index be9494d91..e32fe2478 100644 --- a/src/Composer/DependencyResolver/Solver.php +++ b/src/Composer/DependencyResolver/Solver.php @@ -14,9 +14,7 @@ namespace Composer\DependencyResolver; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; -use Composer\Repository\RepositoryInterface; use Composer\Repository\PlatformRepository; -use Composer\Repository\RepositorySet; /** * @author Nils Adermann @@ -29,7 +27,7 @@ class Solver /** @var PolicyInterface */ protected $policy; /** @var Pool */ - protected $pool = null; + protected $pool; /** @var RuleSet */ protected $rules; @@ -120,7 +118,7 @@ class Solver $conflict = $this->decisions->decisionRule($literal); if ($conflict && RuleSet::TYPE_PACKAGE === $conflict->getType()) { - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule($rule); $problem->addRule($conflict); @@ -130,7 +128,7 @@ class Solver } // conflict with another root require/fixed package - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule($rule); $problem->addRule($conflict); @@ -177,7 +175,7 @@ class Solver } if (!$this->pool->whatProvides($packageName, $constraint)) { - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule(new GenericRule(array(), Rule::RULE_ROOT_REQUIRE, array('packageName' => $packageName, 'constraint' => $constraint))); $this->problems[] = $problem; } @@ -214,7 +212,7 @@ class Solver $this->io->writeError(sprintf('Dependency resolution completed in %.3f seconds', microtime(true) - $before), true, IOInterface::VERBOSE); if ($this->problems) { - throw new SolverProblemsException($this->problems, $request->getPresentMap(true), $this->learnedPool); + throw new SolverProblemsException($this->problems, $this->learnedPool); } return new LockTransaction($this->pool, $request->getPresentMap(), $request->getUnlockableMap(), $this->decisions); @@ -513,7 +511,7 @@ class Solver */ private function analyzeUnsolvable(Rule $conflictRule) { - $problem = new Problem($this->pool); + $problem = new Problem(); $problem->addRule($conflictRule); $this->analyzeUnsolvableRule($problem, $conflictRule); diff --git a/src/Composer/DependencyResolver/SolverProblemsException.php b/src/Composer/DependencyResolver/SolverProblemsException.php index f720aba4e..542fe0464 100644 --- a/src/Composer/DependencyResolver/SolverProblemsException.php +++ b/src/Composer/DependencyResolver/SolverProblemsException.php @@ -13,6 +13,7 @@ namespace Composer\DependencyResolver; use Composer\Util\IniHelper; +use Composer\Repository\RepositorySet; /** * @author Nils Adermann @@ -20,24 +21,23 @@ use Composer\Util\IniHelper; class SolverProblemsException extends \RuntimeException { protected $problems; - protected $installedMap; protected $learnedPool; - public function __construct(array $problems, array $installedMap, array $learnedPool) + public function __construct(array $problems, array $learnedPool) { $this->problems = $problems; - $this->installedMap = $installedMap; $this->learnedPool = $learnedPool; - parent::__construct($this->createMessage(), 2); + parent::__construct('Failed resolving dependencies with '.count($problems).' problems, call getPrettyString to get formatted details', 2); } - protected function createMessage() + public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool) { + $installedMap = $request->getPresentMap(true); $text = "\n"; $hasExtensionProblems = false; foreach ($this->problems as $i => $problem) { - $text .= " Problem ".($i + 1).$problem->getPrettyString($this->installedMap, $this->learnedPool)."\n"; + $text .= " Problem ".($i + 1).$problem->getPrettyString($repositorySet, $request, $pool, $installedMap, $this->learnedPool)."\n"; if (!$hasExtensionProblems && $this->hasExtensionProblems($problem->getReasons())) { $hasExtensionProblems = true; diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index ba026d2e2..1d4ebe4b6 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -17,6 +17,7 @@ use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; use Composer\Util\Git as GitUtil; +use Composer\Util\Url; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Cache; @@ -434,7 +435,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface $this->io->writeError(' '.$reference.' is gone (history was rewritten?)'); } - throw new \RuntimeException(GitUtil::sanitizeUrl('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize('Failed to execute ' . $command . "\n\n" . $this->process->getErrorOutput())); } protected function updateOriginUrl($path, $url) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index bbd81d997..b1ed2db2c 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -389,14 +389,14 @@ class Installer $pool = $repositorySet->createPool($request); // solve dependencies - $solver = new Solver($policy, $pool, $this->io); + $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); $ruleSetSize = $solver->getRuleSetSize(); $solver = null; } 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()); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); 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); } @@ -529,14 +529,14 @@ class Installer $pool = $repositorySet->createPool($request); //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::PRE_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request); - $solver = new Solver($policy, $pool, $this->io); + $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { $nonDevLockTransaction = $solver->solve($request, $this->ignorePlatformReqs); //$this->eventDispatcher->dispatchInstallerEvent(InstallerEvents::POST_DEPENDENCIES_SOLVING, false, $policy, $pool, $installedRepo, $request, $ops); $solver = null; } catch (SolverProblemsException $e) { $this->io->writeError('Unable to find a compatible set of packages based on your non-dev requirements alone.', true, IOInterface::QUIET); - $this->io->writeError($e->getMessage()); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); return max(1, $e->getCode()); } @@ -589,7 +589,7 @@ class Installer $pool = $repositorySet->createPool($request); // solve dependencies - $solver = new Solver($policy, $pool, $this->io); + $solver = new Solver($policy, $pool, $this->io, $repositorySet); try { $lockTransaction = $solver->solve($request, $this->ignorePlatformReqs); $solver = null; @@ -602,7 +602,7 @@ class Installer } } catch (SolverProblemsException $e) { $this->io->writeError('Your lock file does not contain a compatible set of packages. Please run composer update.', true, IOInterface::QUIET); - $this->io->writeError($e->getMessage()); + $this->io->writeError($e->getPrettyString($repositorySet, $request, $pool)); return max(1, $e->getCode()); } @@ -884,7 +884,7 @@ class Installer $packageQueue = new \SplQueue; $nameMatchesRequiredPackage = false; - $depPackages = $repositorySet->findPackages($packageName, null, false); + $depPackages = $repositorySet->findPackages($packageName, null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); $matchesByPattern = array(); // check if the name is a glob pattern that did not match directly @@ -892,7 +892,7 @@ class Installer // add any installed package matching the whitelisted name/pattern $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); foreach ($lockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { - $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, false); + $matchesByPattern[] = $repositorySet->findPackages($installedPackage['name'], null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); } // add root requirements which match the whitelisted name/pattern @@ -933,7 +933,7 @@ class Installer $requires = $package->getRequires(); foreach ($requires as $require) { - $requirePackages = $repositorySet->findPackages($require->getTarget(), null, false); + $requirePackages = $repositorySet->findPackages($require->getTarget(), null, RepositorySet::ALLOW_PROVIDERS_REPLACERS); foreach ($requirePackages as $requirePackage) { if (isset($this->updateWhitelist[$requirePackage->getName()])) { diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 03b872d47..517b27d7b 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -412,7 +412,7 @@ class PluginManager */ private function lookupInstalledPackage(RepositorySet $repositorySet, Link $link) { - $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint(), false); + $packages = $repositorySet->findPackages($link->getTarget(), $link->getConstraint(), RepositorySet::ALLOW_PROVIDERS_REPLACERS | RepositorySet::ALLOW_SHADOWED_REPOSITORIES); return !empty($packages) ? $packages[0] : null; } diff --git a/src/Composer/Repository/ArrayRepository.php b/src/Composer/Repository/ArrayRepository.php index 6c7d0e65e..ab67d42d2 100644 --- a/src/Composer/Repository/ArrayRepository.php +++ b/src/Composer/Repository/ArrayRepository.php @@ -42,6 +42,11 @@ class ArrayRepository extends BaseRepository } } + public function getRepoName() + { + return 'array repo (defining '.count($this->packages).' package'.(count($this->packages) > 1 ? 's' : '').')'; + } + /** * {@inheritDoc} */ @@ -57,7 +62,9 @@ class ArrayRepository extends BaseRepository (!$packageMap[$package->getName()] || $packageMap[$package->getName()]->matches(new Constraint('==', $package->getVersion()))) && StabilityFilter::isPackageAcceptable($acceptableStabilities, $stabilityFlags, $package->getNames(), $package->getStability()) ) { + // add selected packages which match stability requirements $result[spl_object_hash($package)] = $package; + // add the aliased package for packages where the alias matches if ($package instanceof AliasPackage && !isset($result[spl_object_hash($package->getAliasOf())])) { $result[spl_object_hash($package->getAliasOf())] = $package->getAliasOf(); } @@ -67,6 +74,7 @@ class ArrayRepository extends BaseRepository } } + // add aliases of packages that were selected, even if the aliases did not match foreach ($packages as $package) { if ($package instanceof AliasPackage) { if (isset($result[spl_object_hash($package->getAliasOf())])) { diff --git a/src/Composer/Repository/ArtifactRepository.php b/src/Composer/Repository/ArtifactRepository.php index aff80e4cd..a0acb7a61 100644 --- a/src/Composer/Repository/ArtifactRepository.php +++ b/src/Composer/Repository/ArtifactRepository.php @@ -43,6 +43,11 @@ class ArtifactRepository extends ArrayRepository implements ConfigurableReposito $this->repoConfig = $repoConfig; } + public function getRepoName() + { + return 'artifact repo ('.$this->lookup.')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 3919d2a6b..0d5b67fa6 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -34,6 +34,7 @@ use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\EmptyConstraint; use Composer\Util\Http\Response; use Composer\Util\MetadataMinifier; +use Composer\Util\Url; use React\Promise\Util as PromiseUtil; /** @@ -52,6 +53,8 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito protected $cache; protected $notifyUrl; protected $searchUrl; + /** @var string|null a URL containing %package% which can be queried to get providers of a given name */ + protected $providersApiUrl; protected $hasProviders = false; protected $providersUrl; protected $availablePackages; @@ -125,6 +128,11 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->loop = new Loop($this->httpDownloader); } + public function getRepoName() + { + return 'composer repo ('.Url::sanitize($this->url).')'; + } + public function getRepoConfig() { return $this->repoConfig; @@ -411,6 +419,17 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito return parent::search($query, $mode); } + public function getProviders($packageName) + { + if (!$this->providersApiUrl) { + return array(); + } + + $result = $this->httpDownloader->get(str_replace('%package%', $packageName, $this->providersApiUrl), $this->options)->decodeJson(); + + return $result['providers']; + } + private function getProviderNames() { $this->loadRootServerFile(); @@ -805,6 +824,10 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $this->hasProviders = true; } + if (!empty($data['providers-api'])) { + $this->providersApiUrl = $data['providers-api']; + } + return $this->rootData = $data; } diff --git a/src/Composer/Repository/CompositeRepository.php b/src/Composer/Repository/CompositeRepository.php index 8ead6693a..806934b7d 100644 --- a/src/Composer/Repository/CompositeRepository.php +++ b/src/Composer/Repository/CompositeRepository.php @@ -39,6 +39,11 @@ class CompositeRepository extends BaseRepository } } + public function getRepoName() + { + return 'composite repo ('.implode(', ', array_map(function ($repo) { return $repo->getRepoName(); }, $this->repositories)).')'; + } + /** * Returns all the wrapped repositories * diff --git a/src/Composer/Repository/InstalledArrayRepository.php b/src/Composer/Repository/InstalledArrayRepository.php index 7ad05d0fa..de1dd67d8 100644 --- a/src/Composer/Repository/InstalledArrayRepository.php +++ b/src/Composer/Repository/InstalledArrayRepository.php @@ -21,4 +21,8 @@ namespace Composer\Repository; */ class InstalledArrayRepository extends WritableArrayRepository implements InstalledRepositoryInterface { + public function getRepoName() + { + return 'installed '.parent::getRepoName(); + } } diff --git a/src/Composer/Repository/InstalledFilesystemRepository.php b/src/Composer/Repository/InstalledFilesystemRepository.php index 1ff8a0a06..bf81734d4 100644 --- a/src/Composer/Repository/InstalledFilesystemRepository.php +++ b/src/Composer/Repository/InstalledFilesystemRepository.php @@ -19,4 +19,8 @@ namespace Composer\Repository; */ class InstalledFilesystemRepository extends FilesystemRepository implements InstalledRepositoryInterface { + public function getRepoName() + { + return 'installed '.parent::getRepoName(); + } } diff --git a/src/Composer/Repository/LockArrayRepository.php b/src/Composer/Repository/LockArrayRepository.php index 0ccc998d3..8da3d5915 100644 --- a/src/Composer/Repository/LockArrayRepository.php +++ b/src/Composer/Repository/LockArrayRepository.php @@ -21,5 +21,9 @@ namespace Composer\Repository; */ class LockArrayRepository extends ArrayRepository implements RepositoryInterface { + public function getRepoName() + { + return 'lock '.parent::getRepoName(); + } } diff --git a/src/Composer/Repository/PackageRepository.php b/src/Composer/Repository/PackageRepository.php index 52b9a0f6b..de6d31d4d 100644 --- a/src/Composer/Repository/PackageRepository.php +++ b/src/Composer/Repository/PackageRepository.php @@ -58,4 +58,9 @@ class PackageRepository extends ArrayRepository $this->addPackage($package); } } + + public function getRepoName() + { + return preg_replace('{^array }', 'package ', parent::getRepoName()); + } } diff --git a/src/Composer/Repository/PathRepository.php b/src/Composer/Repository/PathRepository.php index 4db774579..699270777 100644 --- a/src/Composer/Repository/PathRepository.php +++ b/src/Composer/Repository/PathRepository.php @@ -20,6 +20,7 @@ use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionParser; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; +use Composer\Util\Url; /** * This repository allows installing local packages that are not necessarily under their own VCS. @@ -111,6 +112,11 @@ class PathRepository extends ArrayRepository implements ConfigurableRepositoryIn parent::__construct(); } + public function getRepoName() + { + return 'path repo ('.Url::sanitize($this->repoConfig['url']).')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index 5cffb6233..97e131afb 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -67,6 +67,11 @@ class PearRepository extends ArrayRepository implements ConfigurableRepositoryIn $this->repoConfig = $repoConfig; } + public function getRepoName() + { + return 'pear repo ('.$this->url.')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index 897700707..360671c7e 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -51,6 +51,11 @@ class PlatformRepository extends ArrayRepository parent::__construct($packages); } + public function getRepoName() + { + return 'platform repo'; + } + protected function initialize() { parent::initialize(); @@ -275,7 +280,7 @@ class PlatformRepository extends ArrayRepository } else { $actualText = 'actual: '.$package->getPrettyVersion(); } - $overrider->setDescription($overrider->getDescription().' ('.$actualText.')'); + $overrider->setDescription($overrider->getDescription().', '.$actualText); return; } @@ -288,7 +293,7 @@ class PlatformRepository extends ArrayRepository } else { $actualText = 'actual: '.$package->getPrettyVersion(); } - $overrider->setDescription($overrider->getDescription().' ('.$actualText.')'); + $overrider->setDescription($overrider->getDescription().', '.$actualText); return; } diff --git a/src/Composer/Repository/RepositoryInterface.php b/src/Composer/Repository/RepositoryInterface.php index 8b1bb3cff..9992778fb 100644 --- a/src/Composer/Repository/RepositoryInterface.php +++ b/src/Composer/Repository/RepositoryInterface.php @@ -83,4 +83,13 @@ interface RepositoryInterface extends \Countable * @return array[] an array of array('name' => '...', 'description' => '...') */ public function search($query, $mode = 0, $type = null); + + /** + * Returns a name representing this repository to the user + * + * This is best effort and definitely can not always be very precise + * + * @return string + */ + public function getRepoName(); } diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 10f3b7dd6..f931a17a2 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -29,6 +29,19 @@ use Composer\Package\Version\StabilityFilter; */ class RepositorySet { + /** + * Packages which replace/provide the given name might be returned as well even if they do not match the name exactly + */ + const ALLOW_PROVIDERS_REPLACERS = 1; + /** + * Packages are returned even though their stability does not match the required stability + */ + const ALLOW_UNACCEPTABLE_STABILITIES = 2; + /** + * Packages will be looked up in all repositories, even after they have been found in a higher prio one + */ + const ALLOW_SHADOWED_REPOSITORIES = 4; + /** @var array */ private $rootAliases; /** @var array */ @@ -39,10 +52,10 @@ class RepositorySet private $acceptableStabilities; private $stabilityFlags; - protected $rootRequires; + private $rootRequires; - /** @var Pool */ - private $pool; + /** @var bool */ + private $locked = false; public function __construct(array $rootAliases = array(), array $rootReferences = array(), $minimumStability = 'stable', array $stabilityFlags = array(), array $rootRequires = array()) { @@ -64,6 +77,11 @@ class RepositorySet } } + public function getRootRequires() + { + return $this->rootRequires; + } + /** * Adds a repository to this repository set * @@ -74,7 +92,7 @@ class RepositorySet */ public function addRepository(RepositoryInterface $repo) { - if ($this->pool) { + if ($this->locked) { throw new \RuntimeException("Pool has already been created from this repository set, it cannot be modified anymore."); } @@ -96,15 +114,32 @@ class RepositorySet * * @param string $name * @param ConstraintInterface|null $constraint - * @param bool $exactMatch if set to false, packages which replace/provide the given name might be returned as well even if they do not match the name exactly - * @param bool $ignoreStability if set to true, packages are returned even though their stability does not match the required stability + * @param int $flags any of the ALLOW_* constants from this class to tweak what is returned * @return array */ - public function findPackages($name, ConstraintInterface $constraint = null, $exactMatch = true, $ignoreStability = false) + public function findPackages($name, ConstraintInterface $constraint = null, $flags = 0) { + $exactMatch = ($flags & self::ALLOW_PROVIDERS_REPLACERS) === 0; + $ignoreStability = ($flags & self::ALLOW_UNACCEPTABLE_STABILITIES) !== 0; + $loadFromAllRepos = ($flags & self::ALLOW_SHADOWED_REPOSITORIES) !== 0; + $packages = array(); - foreach ($this->repositories as $repository) { - $packages[] = $repository->findPackages($name, $constraint) ?: array(); + if ($loadFromAllRepos) { + foreach ($this->repositories as $repository) { + $packages[] = $repository->findPackages($name, $constraint) ?: array(); + } + } else { + foreach ($this->repositories as $repository) { + $result = $repository->loadPackages(array($name => $constraint), $ignoreStability ? BasePackage::$stabilities : $this->acceptableStabilities, $ignoreStability ? array() : $this->stabilityFlags); + + $packages[] = $result['packages']; + foreach ($result['namesFound'] as $nameFound) { + // avoid loading the same package again from other repositories once it has been found + if ($name === $nameFound) { + break 2; + } + } + } } $candidates = $packages ? call_user_func_array('array_merge', $packages) : array(); @@ -123,6 +158,19 @@ class RepositorySet return $candidates; } + public function getProviders($packageName) + { + foreach ($this->repositories as $repository) { + if ($repository instanceof ComposerRepository) { + if ($providers = $repository->getProviders($packageName)) { + return $providers; + } + } + } + + return array(); + } + public function isPackageAcceptable($names, $stability) { return StabilityFilter::isPackageAcceptable($this->acceptableStabilities, $this->stabilityFlags, $names, $stability); @@ -135,7 +183,7 @@ class RepositorySet */ public function createPool(Request $request) { - $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences, $this->rootRequires); + $poolBuilder = new PoolBuilder($this->acceptableStabilities, $this->stabilityFlags, $this->rootAliases, $this->rootReferences); foreach ($this->repositories as $repo) { if ($repo instanceof InstalledRepositoryInterface) { @@ -143,7 +191,9 @@ class RepositorySet } } - return $this->pool = $poolBuilder->buildPool($this->repositories, $request); + $this->locked = true; + + return $poolBuilder->buildPool($this->repositories, $request); } // TODO unify this with above in some simpler version without "request"? @@ -162,13 +212,4 @@ class RepositorySet return $this->createPool($request); } - - /** - * Access the pool object after it has been created, relevant for plugins which need to read info from the pool - * @return Pool - */ - public function getPool() - { - return $this->pool; - } } diff --git a/src/Composer/Repository/RootPackageRepository.php b/src/Composer/Repository/RootPackageRepository.php index 8b5892717..721737fdc 100644 --- a/src/Composer/Repository/RootPackageRepository.php +++ b/src/Composer/Repository/RootPackageRepository.php @@ -21,4 +21,8 @@ namespace Composer\Repository; */ class RootPackageRepository extends ArrayRepository { + public function getRepoName() + { + return 'root package repo'; + } } diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 25b1b21b0..5f4578d1e 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -22,6 +22,7 @@ use Composer\Package\Loader\LoaderInterface; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; +use Composer\Util\Url; use Composer\IO\IOInterface; use Composer\Config; @@ -79,6 +80,17 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $this->processExecutor = new ProcessExecutor($io); } + public function getRepoName() + { + $driverClass = get_class($this->getDriver()); + $driverType = array_search($driverClass, $this->drivers); + if (!$driverType) { + $driverType = $driverClass; + } + + return 'vcs repo ('.$driverType.' '.Url::sanitize($this->url).')'; + } + public function getRepoConfig() { return $this->repoConfig; diff --git a/src/Composer/Util/AuthHelper.php b/src/Composer/Util/AuthHelper.php index 7b8d33d68..e4e829ea7 100644 --- a/src/Composer/Util/AuthHelper.php +++ b/src/Composer/Util/AuthHelper.php @@ -255,15 +255,4 @@ class AuthHelper return count($pathParts) >= 4 && $pathParts[3] == 'downloads'; } - - /** - * @param string $url - * @return string - */ - public function stripCredentialsFromUrl($url) - { - // GitHub repository rename result in redirect locations containing the access_token as GET parameter - // e.g. https://api.github.com/repositories/9999999999?access_token=github_token - return preg_replace('{([&?]access_token=)[^&]+}', '$1***', $url); - } } diff --git a/src/Composer/Util/Git.php b/src/Composer/Util/Git.php index 798dd4a20..2239d33ad 100644 --- a/src/Composer/Util/Git.php +++ b/src/Composer/Util/Git.php @@ -362,27 +362,16 @@ class Git return '(' . implode('|', array_map('preg_quote', $config->get('gitlab-domains'))) . ')'; } - public static function sanitizeUrl($message) - { - return preg_replace_callback('{://(?P[^@]+?):(?P.+?)@}', function ($m) { - if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) { - return '://***:***@'; - } - - return '://' . $m[1] . ':***@'; - }, $message); - } - private function throwException($message, $url) { // git might delete a directory when it fails and php will not know clearstatcache(); if (0 !== $this->process->execute('git --version', $ignoredOutput)) { - throw new \RuntimeException(self::sanitizeUrl('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', git was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); } - throw new \RuntimeException(self::sanitizeUrl($message)); + throw new \RuntimeException(Url::sanitize($message)); } /** diff --git a/src/Composer/Util/Hg.php b/src/Composer/Util/Hg.php index 3681ad5c7..d0b7fe79f 100644 --- a/src/Composer/Util/Hg.php +++ b/src/Composer/Util/Hg.php @@ -72,23 +72,12 @@ class Hg $this->throwException('Failed to clone ' . $url . ', ' . "\n\n" . $error, $url); } - public static function sanitizeUrl($message) - { - return preg_replace_callback('{://(?P[^@]+?):(?P.+?)@}', function ($m) { - if (preg_match('{^[a-f0-9]{12,}$}', $m[1])) { - return '://***:***@'; - } - - return '://' . $m[1] . ':***@'; - }, $message); - } - private function throwException($message, $url) { if (0 !== $this->process->execute('hg --version', $ignoredOutput)) { - throw new \RuntimeException(self::sanitizeUrl('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); + throw new \RuntimeException(Url::sanitize('Failed to clone ' . $url . ', hg was not found, check that it is installed and in your PATH env.' . "\n\n" . $this->process->getErrorOutput())); } - throw new \RuntimeException(self::sanitizeUrl($message)); + throw new \RuntimeException(Url::sanitize($message)); } } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index ee93ca364..017b2d1a2 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -195,7 +195,7 @@ class CurlDownloader $usingProxy = !empty($options['http']['proxy']) ? ' using proxy ' . $options['http']['proxy'] : ''; $ifModified = false !== strpos(strtolower(implode(',', $options['http']['header'])), 'if-modified-since:') ? ' if modified' : ''; if ($attributes['redirects'] === 0) { - $this->io->writeError('Downloading ' . $this->authHelper->stripCredentialsFromUrl($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); + $this->io->writeError('Downloading ' . Url::sanitize($url) . $usingProxy . $ifModified, true, IOInterface::DEBUG); } $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); @@ -254,12 +254,12 @@ class CurlDownloader $contents = stream_get_contents($job['bodyHandle']); } $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); - $this->io->writeError('['.$statusCode.'] '.$this->authHelper->stripCredentialsFromUrl($progress['url']), true, IOInterface::DEBUG); + $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG); } else { rewind($job['bodyHandle']); $contents = stream_get_contents($job['bodyHandle']); $response = new Response(array('url' => $progress['url']), $statusCode, $headers, $contents); - $this->io->writeError('['.$statusCode.'] '.$this->authHelper->stripCredentialsFromUrl($progress['url']), true, IOInterface::DEBUG); + $this->io->writeError('['.$statusCode.'] '.Url::sanitize($progress['url']), true, IOInterface::DEBUG); } fclose($job['bodyHandle']); @@ -362,7 +362,7 @@ class CurlDownloader } if (!empty($targetUrl)) { - $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, $this->authHelper->stripCredentialsFromUrl($targetUrl)), true, IOInterface::DEBUG); + $this->io->writeError(sprintf('Following redirect (%u) %s', $job['attributes']['redirects'] + 1, Url::sanitize($targetUrl)), true, IOInterface::DEBUG); return $targetUrl; } diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index cf39aaf9d..6312deabc 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -246,7 +246,7 @@ class RemoteFilesystem $actualContextOptions = stream_context_get_options($ctx); $usingProxy = !empty($actualContextOptions['http']['proxy']) ? ' using proxy ' . $actualContextOptions['http']['proxy'] : ''; - $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . $this->authHelper->stripCredentialsFromUrl($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); + $this->io->writeError((substr($origFileUrl, 0, 4) === 'http' ? 'Downloading ' : 'Reading ') . Url::sanitize($origFileUrl) . $usingProxy, true, IOInterface::DEBUG); unset($origFileUrl, $actualContextOptions); // Check for secure HTTP, but allow insecure Packagist calls to $hashed providers as file integrity is verified with sha256 @@ -704,7 +704,7 @@ class RemoteFilesystem $this->redirects++; $this->io->writeError('', true, IOInterface::DEBUG); - $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, $this->authHelper->stripCredentialsFromUrl($targetUrl)), true, IOInterface::DEBUG); + $this->io->writeError(sprintf('Following redirect (%u) %s', $this->redirects, Url::sanitize($targetUrl)), true, IOInterface::DEBUG); $additionalOptions['redirects'] = $this->redirects; diff --git a/src/Composer/Util/Url.php b/src/Composer/Util/Url.php index b12a2d54d..2da171556 100644 --- a/src/Composer/Util/Url.php +++ b/src/Composer/Util/Url.php @@ -102,4 +102,21 @@ class Url return $origin; } + + public static function sanitize($url) + { + // GitHub repository rename result in redirect locations containing the access_token as GET parameter + // e.g. https://api.github.com/repositories/9999999999?access_token=github_token + $url = preg_replace('{([&?]access_token=)[^&]+}', '$1***', $url); + + $url = preg_replace_callback('{://(?P[^:/\s@]+):(?P[^@\s/]+)@}i', function ($m) { + if (preg_match('{^[a-f0-9]{12,}$}', $m['user'])) { + return '://***:***@'; + } + + return '://'.$m['user'].':***@'; + }, $url); + + return $url; + } } diff --git a/tests/Composer/Test/DependencyResolver/RuleSetTest.php b/tests/Composer/Test/DependencyResolver/RuleSetTest.php index 2215c019b..5a89ddf79 100644 --- a/tests/Composer/Test/DependencyResolver/RuleSetTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleSetTest.php @@ -143,12 +143,15 @@ class RuleSetTest extends TestCase $p = $this->getPackage('foo', '2.1'), )); + $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); + $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); + $ruleSet = new RuleSet; $literal = $p->getId(); $rule = new GenericRule(array($literal), Rule::RULE_ROOT_REQUIRE, array('packageName' => 'foo/bar', 'constraint' => null)); $ruleSet->add($rule, RuleSet::TYPE_REQUEST); - $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($pool)); + $this->assertContains('REQUEST : No package found to satisfy root composer.json require foo/bar', $ruleSet->getPrettyString($repositorySetMock, $requestMock, $pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/RuleTest.php b/tests/Composer/Test/DependencyResolver/RuleTest.php index 6a01ff2f2..f819397fb 100644 --- a/tests/Composer/Test/DependencyResolver/RuleTest.php +++ b/tests/Composer/Test/DependencyResolver/RuleTest.php @@ -99,8 +99,11 @@ class RuleTest extends TestCase $p2 = $this->getPackage('baz', '1.1'), )); + $repositorySetMock = $this->getMockBuilder('Composer\Repository\RepositorySet')->disableOriginalConstructor()->getMock(); + $requestMock = $this->getMockBuilder('Composer\DependencyResolver\Request')->disableOriginalConstructor()->getMock(); + $rule = new GenericRule(array($p1->getId(), -$p2->getId()), Rule::RULE_PACKAGE_REQUIRES, new Link('baz', 'foo')); - $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($pool)); + $this->assertEquals('baz 1.1 relates to foo -> satisfiable by foo[2.1].', $rule->getPrettyString($repositorySetMock, $requestMock, $pool)); } } diff --git a/tests/Composer/Test/DependencyResolver/SolverTest.php b/tests/Composer/Test/DependencyResolver/SolverTest.php index bc9b39204..38e16c6a0 100644 --- a/tests/Composer/Test/DependencyResolver/SolverTest.php +++ b/tests/Composer/Test/DependencyResolver/SolverTest.php @@ -34,6 +34,7 @@ class SolverTest extends TestCase protected $request; protected $policy; protected $solver; + protected $pool; public function setUp() { @@ -82,7 +83,7 @@ class SolverTest extends TestCase $problems = $e->getProblems(); $this->assertCount(1, $problems); $this->assertEquals(2, $e->getCode()); - $this->assertEquals("\n - The requested package b could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString()); + $this->assertEquals("\n - Root composer.json requires b, it could not be found in any version, there may be a typo in the package name.", $problems[0]->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -651,9 +652,9 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; - $msg .= " - B 1.0 conflicts with A[1.0].\n"; + $msg .= " - A 1.0 conflicts with B 1.0.\n"; $msg .= " - Root composer.json requires b -> satisfiable by B[1.0].\n"; - $this->assertEquals($msg, $e->getMessage()); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -682,14 +683,8 @@ class SolverTest extends TestCase $msg = "\n"; $msg .= " Problem 1\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; - $msg .= " - A 1.0 requires b >= 2.0 -> no matching package found.\n\n"; - $msg .= "Potential causes:\n"; - $msg .= " - A typo in the package name\n"; - $msg .= " - The package is not available in a stable-enough version according to your minimum-stability setting\n"; - $msg .= " see for more details.\n"; - $msg .= " - It's a private package and you forgot to add a custom repository to find it\n\n"; - $msg .= "Read for further common problems."; - $this->assertEquals($msg, $e->getMessage()); + $msg .= " - A 1.0 requires b >= 2.0 -> found B[1.0] but it does not match your constraint.\n"; + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -731,10 +726,10 @@ class SolverTest extends TestCase $msg .= " - C 1.0 requires d >= 1.0 -> satisfiable by D[1.0].\n"; $msg .= " - D 1.0 requires b < 1.0 -> satisfiable by B[0.9].\n"; $msg .= " - B 1.0 requires c >= 1.0 -> satisfiable by C[1.0].\n"; - $msg .= " - Same name, can only install one of: B[0.9, 1.0].\n"; + $msg .= " - You can only install one version of a package, so only one of these can be installed: B[0.9, 1.0].\n"; $msg .= " - A 1.0 requires b >= 1.0 -> satisfiable by B[1.0].\n"; $msg .= " - Root composer.json requires a -> satisfiable by A[1.0].\n"; - $this->assertEquals($msg, $e->getMessage()); + $this->assertEquals($msg, $e->getPrettyString($this->repoSet, $this->request, $this->pool)); } } @@ -895,7 +890,8 @@ class SolverTest extends TestCase protected function createSolver() { - $this->solver = new Solver($this->policy, $this->repoSet->createPool($this->request), new NullIO()); + $this->pool = $this->repoSet->createPool($this->request); + $this->solver = new Solver($this->policy, $this->pool, new NullIO()); } protected function checkSolverResult(array $expected) diff --git a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test index d970d5c4c..c9a9dba6e 100644 --- a/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test +++ b/tests/Composer/Test/Fixtures/installer/broken-deps-do-not-replace.test @@ -26,7 +26,7 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - c/c 1.0.0 requires x/x 1.0 -> no matching package found. + - c/c 1.0.0 requires x/x 1.0 -> could not be found in any version, there may be a typo in the package name. - b/b 1.0.0 requires c/c 1.* -> satisfiable by c/c[1.0.0]. - Root composer.json requires b/b 1.* -> satisfiable by b/b[1.0.0]. diff --git a/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test new file mode 100644 index 000000000..a59b2ef98 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-between-dependents.test @@ -0,0 +1,38 @@ +--TEST-- +Test the error output of solver problems for conflicts between two dependents +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "conflicter/pkg", "version": "1.0.0", "conflict": { "victim/pkg": "1.0.0"} }, + { "name": "victim/pkg", "version": "1.0.0" } + ] + } + ], + "require": { + "conflicter/pkg": "1.0.0", + "victim/pkg": "1.0.0" + } +} + + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires conflicter/pkg 1.0.0 -> satisfiable by conflicter/pkg[1.0.0]. + - conflicter/pkg 1.0.0 conflicts with victim/pkg 1.0.0. + - Root composer.json requires victim/pkg 1.0.0 -> satisfiable by victim/pkg[1.0.0]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test b/tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test new file mode 100644 index 000000000..8df58ef44 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/conflict-between-root-and-dependent.test @@ -0,0 +1,40 @@ +--TEST-- +Test conflicts between a dependency's requirements and the root requirements +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { "name": "requirer/pkg", "version": "1.0.0", "require": { + "dependency/pkg": "1.0.0", + "dependency/unstable-pkg": "1.0.0-dev" + } }, + { "name": "dependency/pkg", "version": "2.0.0" }, + { "name": "dependency/pkg", "version": "1.0.0" } + ] + } + ], + "require": { + "requirer/pkg": "1.*", + "dependency/pkg": "2.*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test index 7d012cf7c..dcc94dff2 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4319.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4319.test @@ -37,7 +37,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires a/a ~1.0 -> satisfiable by a/a[1.0.0]. - - a/a 1.0.0 requires php 5.5 -> your PHP version (%s) overridden by "config.platform.php" version (5.3) does not satisfy that requirement. + - a/a 1.0.0 requires php 5.5 -> your php version (5.3; overridden via config.platform, actual: %s) does not satisfy that requirement. --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index 9735d0a2c..8e0b18e05 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -1,5 +1,5 @@ --TEST-- -Partial update from lock file should apply lock file and downgrade unstable packages even if not whitelisted +Partial update from lock file should apply lock file and if an unstable package is not allowed anymore by latest composer.json it should fail --COMPOSER-- { "repositories": [ @@ -59,12 +59,4 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - The requested package b/unstable could not be found in any version, there may be a typo in the package name. - -Potential causes: - - A typo in the package name - - The package is not available in a stable-enough version according to your minimum-stability setting - see for more details. - - It's a private package and you forgot to add a custom repository to find it - -Read for further common problems. + - b/unstable is fixed to 1.1.0-alpha (lock file version) by a partial update but that version is rejected by your minimum-stability. Make sure you whitelist it for update. diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test new file mode 100644 index 000000000..1c8bd5608 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts.test @@ -0,0 +1,49 @@ +--TEST-- +Test that names provided by a dependent and root package cause a conflict only for replace +--COMPOSER-- +{ + "version": "1.2.3", + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "provider/pkg", + "version": "1.0.0", + "provide": { "root-provided/transitive-provided": "2.*", "root-replaced/transitive-provided": "2.*" }, + "replace": { "root-provided/transitive-replaced": "2.*", "root-replaced/transitive-replaced": "2.*" } + } + ] + } + ], + "require": { + "provider/pkg": "*" + }, + "provide": { + "root-provided/transitive-replaced": "2.*", + "root-provided/transitive-provided": "2.*" + }, + "replace": { + "root-replaced/transitive-replaced": "2.*", + "root-replaced/transitive-provided": "2.*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - __root__ is present at version 1.2.3 and cannot be modified by Composer + - provider/pkg 1.0.0 can not be installed as that would require removing __root__ 1.2.3. They both replace root-replaced/transitive-replaced and can thus not coexist. + - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test new file mode 100644 index 000000000..343dab537 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts2.test @@ -0,0 +1,45 @@ +--TEST-- +Test that names provided by two dependents cause a conflict +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "provider/pkg", + "version": "1.0.0", + "provide": { "third/pkg": "2.*" } + }, + { + "name": "replacer/pkg", + "version": "1.0.0", + "replace": { "third/pkg": "2.*" } + } + ] + } + ], + "require": { + "provider/pkg": "*", + "replacer/pkg": "*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Root composer.json requires provider/pkg * -> satisfiable by provider/pkg[1.0.0]. + - Only one of these can be installed: replacer/pkg 1.0.0, provider/pkg 1.0.0. + - Root composer.json requires replacer/pkg * -> satisfiable by replacer/pkg[1.0.0]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test new file mode 100644 index 000000000..3a865f3bf --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/provider-conflicts3.test @@ -0,0 +1,54 @@ +--TEST-- +Test that a replacer can not be installed together with another version of the package it replaces +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + {"name": "replacer/pkg", "version": "2.0.0", "replace": { "regular/pkg": "self.version" }}, + {"name": "replacer/pkg", "version": "2.0.1", "replace": { "regular/pkg": "self.version" }}, + {"name": "replacer/pkg", "version": "2.0.2", "replace": { "regular/pkg": "self.version" }}, + {"name": "replacer/pkg", "version": "2.0.3", "replace": { "regular/pkg": "self.version" }}, + {"name": "regular/pkg", "version": "1.0.0"}, + {"name": "regular/pkg", "version": "1.0.1"}, + {"name": "regular/pkg", "version": "1.0.2"}, + {"name": "regular/pkg", "version": "1.0.3"}, + {"name": "regular/pkg", "version": "2.0.0"}, + {"name": "regular/pkg", "version": "2.0.1"} + ] + } + ], + "require": { + "regular/pkg": "1.*", + "replacer/pkg": "2.*" + } +} + +--RUN-- +update + +--EXPECT-EXIT-CODE-- +2 + +--EXPECT-OUTPUT-- +Loading composer repositories with package information +Updating dependencies +Your requirements could not be resolved to an installable set of packages. + + Problem 1 + - Conclusion: don't install regular/pkg 1.0.3, learned rules: + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. + - Conclusion: don't install regular/pkg 1.0.2, learned rules: + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. + - Conclusion: don't install regular/pkg 1.0.1, learned rules: + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. + - Only one of these can be installed: regular/pkg[1.0.3, 1.0.2, 1.0.1, 1.0.0], replacer/pkg[2.0.3, 2.0.2, 2.0.1, 2.0.0]. replacer/pkg replaces regular/pkg and can thus not coexist with it. + - Root composer.json requires regular/pkg 1.* -> satisfiable by regular/pkg[1.0.0, 1.0.1, 1.0.2, 1.0.3]. + - Root composer.json requires replacer/pkg 2.* -> satisfiable by replacer/pkg[2.0.0, 2.0.1, 2.0.2, 2.0.3]. + +--EXPECT-- + diff --git a/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test b/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test index 85347b4e6..816b8efe9 100644 --- a/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test +++ b/tests/Composer/Test/Fixtures/installer/provider-packages-can-not-be-installed-unless-selected.test @@ -41,7 +41,7 @@ Your requirements could not be resolved to an installable set of packages. Problem 1 - Root composer.json requires foo/standard 1.0.0 -> satisfiable by foo/standard[1.0.0]. - - foo/standard 1.0.0 requires foo/does-not-exist 1.0.0 -> no matching package found. + - foo/standard 1.0.0 requires foo/does-not-exist 1.0.0 -> could not be found in any version, there may be a typo in the package name. Potential causes: - A typo in the package name diff --git a/tests/Composer/Test/Fixtures/installer/repositories-priorities.test b/tests/Composer/Test/Fixtures/installer/repositories-priorities.test index 42f7a1a6e..bc06179e0 100644 --- a/tests/Composer/Test/Fixtures/installer/repositories-priorities.test +++ b/tests/Composer/Test/Fixtures/installer/repositories-priorities.test @@ -28,15 +28,8 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - The requested package foo/a could not be found in any version, there may be a typo in the package name. + - Root composer.json requires foo/a 2.*, it is satisfiable by foo/a[2.0.0] from package repo (defining 1 package) but foo/a[1.0.0] from package repo (defining 1 package) has higher repository priority. The packages with higher priority do not match your constraint and are therefore not installable. -Potential causes: - - A typo in the package name - - The package is not available in a stable-enough version according to your minimum-stability setting - see for more details. - - It's a private package and you forgot to add a custom repository to find it - -Read for further common problems. --EXPECT-- --EXPECT-EXIT-CODE-- 2 diff --git a/tests/Composer/Test/Fixtures/installer/solver-problems.test b/tests/Composer/Test/Fixtures/installer/solver-problems.test index a0264257f..f3905b6fa 100644 --- a/tests/Composer/Test/Fixtures/installer/solver-problems.test +++ b/tests/Composer/Test/Fixtures/installer/solver-problems.test @@ -6,34 +6,84 @@ Test the error output of solver problems. { "type": "package", "package": [ + { "name": "package/found", "version": "2.0.0", "require": { + "unstable/package2": "2.*" + } }, + { "name": "package/found2", "version": "2.0.0", "require": { + "invalid/💩package": "*" + } }, + { "name": "package/found3", "version": "2.0.0", "require": { + "unstable/package2": "2.*" + } }, + { "name": "package/found4", "version": "2.0.0", "require": { + "non-existent/pkg2": "1.*" + } }, + { "name": "package/found5", "version": "2.0.0", "require": { + "requirer/pkg": "1.*" + } }, + { "name": "package/found6", "version": "2.0.0", "require": { + "stable-requiree-excluded/pkg2": "1.0.1" + } }, + { "name": "package/found7", "version": "2.0.0", "require": { + "php-64bit": "1.0.1" + } }, + { "name": "conflict/requirer", "version": "2.0.0", "require": { + "conflict/dep": "1.0.0" + } }, + { "name": "conflict/requirer2", "version": "2.0.0", "require": { + "conflict/dep": "2.0.0" + } }, + { "name": "conflict/dep", "version": "1.0.0" }, + { "name": "conflict/dep", "version": "2.0.0" }, { "name": "unstable/package", "version": "2.0.0-alpha" }, { "name": "unstable/package", "version": "1.0.0" }, - { "name": "requirer/pkg", "version": "1.0.0", "require": {"dependency/pkg": "1.0.0" } }, + { "name": "unstable/package2", "version": "2.0.0-alpha" }, + { "name": "unstable/package2", "version": "1.0.0" }, + { "name": "requirer/pkg", "version": "1.0.0", "require": { + "dependency/pkg": "1.0.0", + "dependency/unstable-pkg": "1.0.0-dev" + } }, { "name": "dependency/pkg", "version": "2.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, + { "name": "dependency/unstable-pkg", "version": "1.0.0-dev" }, { "name": "stable-requiree-excluded/pkg", "version": "1.0.1" }, { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } ] } ], "require": { + "package/found": "2.*", + "package/found2": "2.*", + "package/found3": "2.*", + "package/found4": "2.*", + "package/found5": "2.*", + "package/found6": "2.*", + "package/found7": "2.*", + "conflict/requirer": "2.*", + "conflict/requirer2": "2.*", "unstable/package": "2.*", - "bogus/pkg": "1.*", + "non-existent/pkg": "1.*", "requirer/pkg": "1.*", "dependency/pkg": "2.*", - "stable-requiree-excluded/pkg": "1.0.1" + "stable-requiree-excluded/pkg": "1.0.1", + "lib-xml": "1002.*", + "lib-icu": "1001.*", + "ext-xml": "1002.*", + "php": "1" } } --INSTALLED-- [ - { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }, + { "name": "stable-requiree-excluded/pkg2", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" } + { "name": "stable-requiree-excluded/pkg", "version": "1.0.0" }, + { "name": "stable-requiree-excluded/pkg2", "version": "1.0.0" } ], "packages-dev": [], "aliases": [], @@ -46,7 +96,7 @@ Test the error output of solver problems. } --RUN-- -update unstable/package requirer/pkg dependency/pkg +update unstable/package requirer/pkg dependency/pkg conflict/requirer --EXPECT-EXIT-CODE-- 2 @@ -57,14 +107,44 @@ Updating dependencies Your requirements could not be resolved to an installable set of packages. Problem 1 - - The requested package unstable/package could not be found in any version, there may be a typo in the package name. + - Root composer.json requires unstable/package 2.*, found unstable/package[2.0.0-alpha] but it does not match your minimum-stability. Problem 2 - - The requested package bogus/pkg could not be found in any version, there may be a typo in the package name. + - Root composer.json requires non-existent/pkg, it could not be found in any version, there may be a typo in the package name. Problem 3 - - The requested package stable-requiree-excluded/pkg could not be found in any version, there may be a typo in the package name. + - Root composer.json requires stable-requiree-excluded/pkg 1.0.1, found stable-requiree-excluded/pkg[1.0.1] but the package is fixed to 1.0.0 (lock file version) by a partial update and that version does not match. Make sure you whitelist it for update. Problem 4 + - Root composer.json requires linked library lib-xml 1002.* but it has the wrong version installed or is missing from your system, make sure to load the extension providing it. + Problem 5 + - Root composer.json requires linked library lib-icu 1001.* but it has the wrong version installed, try upgrading the intl extension. + Problem 6 + - Root composer.json requires PHP extension ext-xml 1002.* but it has the wrong version (%s) installed. Install or enable PHP's xml extension. + Problem 7 + - Root composer.json requires php 1 but your php version (%s) does not satisfy that requirement. + Problem 8 + - Root composer.json requires package/found 2.* -> satisfiable by package/found[2.0.0]. + - package/found 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability. + Problem 9 + - Root composer.json requires package/found2 2.* -> satisfiable by package/found2[2.0.0]. + - package/found2 2.0.0 requires invalid/💩package * -> could not be found, it looks like its name is invalid, "💩" is not allowed in package names. + Problem 10 + - Root composer.json requires package/found3 2.* -> satisfiable by package/found3[2.0.0]. + - package/found3 2.0.0 requires unstable/package2 2.* -> found unstable/package2[2.0.0-alpha] but it does not match your minimum-stability. + Problem 11 + - Root composer.json requires package/found4 2.* -> satisfiable by package/found4[2.0.0]. + - package/found4 2.0.0 requires non-existent/pkg2 1.* -> could not be found in any version, there may be a typo in the package name. + Problem 12 + - Root composer.json requires package/found6 2.* -> satisfiable by package/found6[2.0.0]. + - package/found6 2.0.0 requires stable-requiree-excluded/pkg2 1.0.1 -> found stable-requiree-excluded/pkg2[1.0.0] but it does not match your constraint. + Problem 13 + - Root composer.json requires package/found7 2.* -> satisfiable by package/found7[2.0.0]. + - package/found7 2.0.0 requires php-64bit 1.0.1 -> your php-64bit version (%s) does not satisfy that requirement. + Problem 14 - Root composer.json requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. - - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> no matching package found. + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). + Problem 15 + - requirer/pkg 1.0.0 requires dependency/pkg 1.0.0 -> found dependency/pkg[1.0.0] but it conflicts with your root composer.json require (2.*). + - package/found5 2.0.0 requires requirer/pkg 1.* -> satisfiable by requirer/pkg[1.0.0]. + - Root composer.json requires package/found5 2.* -> satisfiable by package/found5[2.0.0]. Potential causes: - A typo in the package name @@ -73,6 +153,9 @@ Potential causes: - It's a private package and you forgot to add a custom repository to find it Read for further common problems. + To enable extensions, verify that they are enabled in your .ini files: +__inilist__ + You can also run `php --ini` inside terminal to see which files are used by PHP in CLI mode. --EXPECT-- diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index bb0643cf1..bb1473e71 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -323,6 +323,9 @@ class InstallerTest extends TestCase $this->assertSame(rtrim($expect), implode("\n", $installationManager->getTrace())); if ($expectOutput) { + $output = preg_replace('{^ - .*?\.ini$}m', '__inilist__', $output); + $output = preg_replace('{(__inilist__\r?\n)+}', "__inilist__\n", $output); + $this->assertStringMatchesFormat(rtrim($expectOutput), rtrim($output)); } } diff --git a/tests/Composer/Test/Util/UrlTest.php b/tests/Composer/Test/Util/UrlTest.php index 7772582a5..322bc0241 100644 --- a/tests/Composer/Test/Util/UrlTest.php +++ b/tests/Composer/Test/Util/UrlTest.php @@ -58,4 +58,25 @@ class UrlTest extends TestCase array('https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=abcd', 'https://mygitlab.com/api/v3/projects/foo%2Fbar/repository/archive.tar.bz2?sha=65', array('gitlab-domains' => array('mygitlab.com')), '65'), ); } + + /** + * @dataProvider sanitizeProvider + */ + public function testSanitize($expected, $url) + { + $this->assertSame($expected, Url::sanitize($url)); + } + + public static function sanitizeProvider() + { + return array( + array('https://foo:***@example.org/', 'https://foo:bar@example.org/'), + array('https://foo@example.org/', 'https://foo@example.org/'), + array('https://example.org/', 'https://example.org/'), + array('http://***:***@example.org', 'http://10a8f08e8d7b7b9:foo@example.org'), + array('https://foo:***@example.org:123/', 'https://foo:bar@example.org:123/'), + array('https://example.org/foo/bar?access_token=***', 'https://example.org/foo/bar?access_token=abcdef'), + array('https://example.org/foo/bar?foo=bar&access_token=***', 'https://example.org/foo/bar?foo=bar&access_token=abcdef'), + ); + } }