Problem.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321
  1. <?php
  2. /*
  3. * This file is part of Composer.
  4. *
  5. * (c) Nils Adermann <naderman@naderman.de>
  6. * Jordi Boggiano <j.boggiano@seld.be>
  7. *
  8. * For the full copyright and license information, please view the LICENSE
  9. * file that was distributed with this source code.
  10. */
  11. namespace Composer\DependencyResolver;
  12. use Composer\Package\CompletePackageInterface;
  13. use Composer\Repository\RepositorySet;
  14. use Composer\Semver\Constraint\Constraint;
  15. /**
  16. * Represents a problem detected while solving dependencies
  17. *
  18. * @author Nils Adermann <naderman@naderman.de>
  19. */
  20. class Problem
  21. {
  22. /**
  23. * A map containing the id of each rule part of this problem as a key
  24. * @var array
  25. */
  26. protected $reasonSeen;
  27. /**
  28. * A set of reasons for the problem, each is a rule or a root require and a rule
  29. * @var array
  30. */
  31. protected $reasons = array();
  32. protected $section = 0;
  33. /**
  34. * Add a rule as a reason
  35. *
  36. * @param Rule $rule A rule which is a reason for this problem
  37. */
  38. public function addRule(Rule $rule)
  39. {
  40. $this->addReason(spl_object_hash($rule), $rule);
  41. }
  42. /**
  43. * Retrieve all reasons for this problem
  44. *
  45. * @return array The problem's reasons
  46. */
  47. public function getReasons()
  48. {
  49. return $this->reasons;
  50. }
  51. /**
  52. * A human readable textual representation of the problem's reasons
  53. *
  54. * @param array $installedMap A map of all present packages
  55. * @return string
  56. */
  57. public function getPrettyString(RepositorySet $repositorySet, Request $request, Pool $pool, array $installedMap = array(), array $learnedPool = array())
  58. {
  59. // TODO doesn't this entirely defeat the purpose of the problem sections? what's the point of sections?
  60. $reasons = call_user_func_array('array_merge', array_reverse($this->reasons));
  61. if (count($reasons) === 1) {
  62. reset($reasons);
  63. $rule = current($reasons);
  64. if (!in_array($rule->getReason(), array(Rule::RULE_ROOT_REQUIRE, Rule::RULE_FIXED), true)) {
  65. throw new \LogicException("Single reason problems must contain a request rule.");
  66. }
  67. $reasonData = $rule->getReasonData();
  68. $packageName = $reasonData['packageName'];
  69. $constraint = $reasonData['constraint'];
  70. if (isset($constraint)) {
  71. $packages = $pool->whatProvides($packageName, $constraint);
  72. } else {
  73. $packages = array();
  74. }
  75. if (empty($packages)) {
  76. return "\n ".implode(self::getMissingPackageReason($repositorySet, $request, $pool, $packageName, $constraint));
  77. }
  78. }
  79. $messages = array();
  80. foreach ($reasons as $rule) {
  81. $messages[] = $rule->getPrettyString($repositorySet, $request, $pool, $installedMap, $learnedPool);
  82. }
  83. return "\n - ".implode("\n - ", $messages);
  84. }
  85. public function isCausedByLock()
  86. {
  87. foreach ($this->reasons as $sectionRules) {
  88. foreach ($sectionRules as $rule) {
  89. if ($rule->isCausedByLock()) {
  90. return true;
  91. }
  92. }
  93. }
  94. }
  95. /**
  96. * Store a reason descriptor but ignore duplicates
  97. *
  98. * @param string $id A canonical identifier for the reason
  99. * @param Rule $reason The reason descriptor
  100. */
  101. protected function addReason($id, Rule $reason)
  102. {
  103. // TODO: if a rule is part of a problem description in two sections, isn't this going to remove a message
  104. // that is important to understand the issue?
  105. if (!isset($this->reasonSeen[$id])) {
  106. $this->reasonSeen[$id] = true;
  107. $this->reasons[$this->section][] = $reason;
  108. }
  109. }
  110. public function nextSection()
  111. {
  112. $this->section++;
  113. }
  114. /**
  115. * @internal
  116. */
  117. public static function getMissingPackageReason(RepositorySet $repositorySet, Request $request, Pool $pool, $packageName, $constraint = null)
  118. {
  119. // handle php/hhvm
  120. if ($packageName === 'php' || $packageName === 'php-64bit' || $packageName === 'hhvm') {
  121. $version = phpversion();
  122. $available = $pool->whatProvides($packageName);
  123. if (count($available)) {
  124. $firstAvailable = reset($available);
  125. $version = $firstAvailable->getPrettyVersion();
  126. $extra = $firstAvailable->getExtra();
  127. if ($firstAvailable instanceof CompletePackageInterface && isset($extra['config.platform']) && $extra['config.platform'] === true) {
  128. $version .= '; ' . str_replace('Package ', '', $firstAvailable->getDescription());
  129. }
  130. }
  131. $msg = "- Root composer.json requires ".$packageName.self::constraintToText($constraint).' but ';
  132. if (defined('HHVM_VERSION') || (count($available) && $packageName === 'hhvm')) {
  133. return array($msg, 'your HHVM version does not satisfy that requirement.');
  134. }
  135. if ($packageName === 'hhvm') {
  136. return array($msg, 'you are running this with PHP and not HHVM.');
  137. }
  138. return array($msg, 'your '.$packageName.' version ('. $version .') does not satisfy that requirement.');
  139. }
  140. // handle php extensions
  141. if (0 === stripos($packageName, 'ext-')) {
  142. if (false !== strpos($packageName, ' ')) {
  143. return array('- ', "PHP extension ".$packageName.' should be required as '.str_replace(' ', '-', $packageName).'.');
  144. }
  145. $ext = substr($packageName, 4);
  146. $error = extension_loaded($ext) ? 'it has the wrong version ('.(phpversion($ext) ?: '0').') installed' : 'it is missing from your system';
  147. return array("- Root composer.json requires PHP extension ".$packageName.self::constraintToText($constraint).' but ', $error.'. Install or enable PHP\'s '.$ext.' extension.');
  148. }
  149. // handle linked libs
  150. if (0 === stripos($packageName, 'lib-')) {
  151. if (strtolower($packageName) === 'lib-icu') {
  152. $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.';
  153. return array("- Root composer.json requires linked library ".$packageName.self::constraintToText($constraint).' but ', $error);
  154. }
  155. 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.');
  156. }
  157. $fixedPackage = null;
  158. foreach ($request->getFixedPackages() as $package) {
  159. if ($package->getName() === $packageName) {
  160. $fixedPackage = $package;
  161. if ($pool->isUnacceptableFixedPackage($package)) {
  162. 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 list it as an argument for the update command.');
  163. }
  164. break;
  165. }
  166. }
  167. // first check if the actual requested package is found in normal conditions
  168. // if so it must mean it is rejected by another constraint than the one given here
  169. if ($packages = $repositorySet->findPackages($packageName, $constraint)) {
  170. $rootReqs = $repositorySet->getRootRequires();
  171. if (isset($rootReqs[$packageName])) {
  172. $filtered = array_filter($packages, function ($p) use ($rootReqs, $packageName) {
  173. return $rootReqs[$packageName]->matches(new Constraint('==', $p->getVersion()));
  174. });
  175. if (0 === count($filtered)) {
  176. 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().').');
  177. }
  178. }
  179. if ($fixedPackage) {
  180. $fixedConstraint = new Constraint('==', $fixedPackage->getVersion());
  181. $filtered = array_filter($packages, function ($p) use ($fixedConstraint) {
  182. return $fixedConstraint->matches(new Constraint('==', $p->getVersion()));
  183. });
  184. if (0 === count($filtered)) {
  185. 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 list it as an argument for the update command.');
  186. }
  187. }
  188. 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.');
  189. }
  190. // check if the package is found when bypassing stability checks
  191. if ($packages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_UNACCEPTABLE_STABILITIES)) {
  192. 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.');
  193. }
  194. // check if the package is found when bypassing the constraint check
  195. if ($packages = $repositorySet->findPackages($packageName, null)) {
  196. // we must first verify if a valid package would be found in a lower priority repository
  197. if ($allReposPackages = $repositorySet->findPackages($packageName, $constraint, RepositorySet::ALLOW_SHADOWED_REPOSITORIES)) {
  198. $higherRepoPackages = $repositorySet->findPackages($packageName, null);
  199. $nextRepoPackages = array();
  200. $nextRepo = null;
  201. foreach ($allReposPackages as $package) {
  202. if ($nextRepo === null || $nextRepo === $package->getRepository()) {
  203. $nextRepoPackages[] = $package;
  204. $nextRepo = $package->getRepository();
  205. } else {
  206. break;
  207. }
  208. }
  209. 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.');
  210. }
  211. 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.');
  212. }
  213. if (!preg_match('{^[A-Za-z0-9_./-]+$}', $packageName)) {
  214. $illegalChars = preg_replace('{[A-Za-z0-9_./-]+}', '', $packageName);
  215. 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.');
  216. }
  217. if ($providers = $repositorySet->getProviders($packageName)) {
  218. $maxProviders = 20;
  219. $providersStr = implode(array_map(function ($p) {
  220. $description = $p['description'] ? ' '.substr($p['description'], 0, 100) : '';
  221. return " - ${p['name']}".$description."\n";
  222. }, count($providers) > $maxProviders+1 ? array_slice($providers, 0, $maxProviders) : $providers));
  223. if (count($providers) > $maxProviders+1) {
  224. $providersStr .= ' ... and '.(count($providers)-$maxProviders).' more.'."\n";
  225. }
  226. 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.");
  227. }
  228. return array("- Root composer.json requires $packageName, it ", "could not be found in any version, there may be a typo in the package name.");
  229. }
  230. /**
  231. * @internal
  232. */
  233. public static function getPackageList(array $packages)
  234. {
  235. $prepared = array();
  236. foreach ($packages as $package) {
  237. $prepared[$package->getName()]['name'] = $package->getPrettyName();
  238. $prepared[$package->getName()]['versions'][$package->getVersion()] = $package->getPrettyVersion();
  239. }
  240. foreach ($prepared as $name => $package) {
  241. // remove the implicit dev-master alias to avoid cruft in the display
  242. if (isset($package['versions']['9999999-dev']) && isset($package['versions']['dev-master'])) {
  243. unset($package['versions']['9999999-dev']);
  244. }
  245. $prepared[$name] = $package['name'].'['.implode(', ', $package['versions']).']';
  246. }
  247. return implode(', ', $prepared);
  248. }
  249. private static function hasMultipleNames(array $packages)
  250. {
  251. $name = null;
  252. foreach ($packages as $package) {
  253. if ($name === null || $name === $package->getName()) {
  254. $name = $package->getName();
  255. } else {
  256. return true;
  257. }
  258. }
  259. return false;
  260. }
  261. /**
  262. * Turns a constraint into text usable in a sentence describing a request
  263. *
  264. * @param \Composer\Semver\Constraint\ConstraintInterface $constraint
  265. * @return string
  266. */
  267. protected static function constraintToText($constraint)
  268. {
  269. return $constraint ? ' '.$constraint->getPrettyString() : '';
  270. }
  271. }