RuleSetGenerator.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335
  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\LinkConstraint\VersionConstraint;
  13. use Composer\Package\PackageInterface;
  14. use Composer\Package\AliasPackage;
  15. use Composer\Repository\PlatformRepository;
  16. use Composer\Semver\Constraint\Constraint;
  17. /**
  18. * @author Nils Adermann <naderman@naderman.de>
  19. */
  20. class RuleSetGenerator
  21. {
  22. protected $policy;
  23. protected $pool;
  24. protected $rules;
  25. protected $addedMap;
  26. protected $conflictAddedMap;
  27. protected $addedPackages;
  28. protected $addedPackagesByNames;
  29. protected $conflictsForName;
  30. public function __construct(PolicyInterface $policy, Pool $pool)
  31. {
  32. $this->policy = $policy;
  33. $this->pool = $pool;
  34. }
  35. /**
  36. * Creates a new rule for the requirements of a package
  37. *
  38. * This rule is of the form (-A|B|C), where B and C are the providers of
  39. * one requirement of the package A.
  40. *
  41. * @param PackageInterface $package The package with a requirement
  42. * @param array $providers The providers of the requirement
  43. * @param int $reason A RULE_* constant describing the
  44. * reason for generating this rule
  45. * @param mixed $reasonData Any data, e.g. the requirement name,
  46. * that goes with the reason
  47. * @return Rule|null The generated rule or null if tautological
  48. */
  49. protected function createRequireRule(PackageInterface $package, array $providers, $reason, $reasonData = null)
  50. {
  51. $literals = array(-$package->id);
  52. foreach ($providers as $provider) {
  53. // self fulfilling rule?
  54. if ($provider === $package) {
  55. return null;
  56. }
  57. $literals[] = $provider->id;
  58. }
  59. return new GenericRule($literals, $reason, $reasonData);
  60. }
  61. /**
  62. * Creates a rule to install at least one of a set of packages
  63. *
  64. * The rule is (A|B|C) with A, B and C different packages. If the given
  65. * set of packages is empty an impossible rule is generated.
  66. *
  67. * @param array $packages The set of packages to choose from
  68. * @param int $reason A RULE_* constant describing the reason for
  69. * generating this rule
  70. * @param array $reasonData Additional data like the root require or fix request info
  71. * @return Rule The generated rule
  72. */
  73. protected function createInstallOneOfRule(array $packages, $reason, $reasonData)
  74. {
  75. $literals = array();
  76. foreach ($packages as $package) {
  77. $literals[] = $package->id;
  78. }
  79. return new GenericRule($literals, $reason, $reasonData);
  80. }
  81. /**
  82. * Creates a rule for two conflicting packages
  83. *
  84. * The rule for conflicting packages A and B is (-A|-B). A is called the issuer
  85. * and B the provider.
  86. *
  87. * @param PackageInterface $issuer The package declaring the conflict
  88. * @param PackageInterface $provider The package causing the conflict
  89. * @param int $reason A RULE_* constant describing the
  90. * reason for generating this rule
  91. * @param mixed $reasonData Any data, e.g. the package name, that
  92. * goes with the reason
  93. * @return Rule|null The generated rule
  94. */
  95. protected function createRule2Literals(PackageInterface $issuer, PackageInterface $provider, $reason, $reasonData = null)
  96. {
  97. // ignore self conflict
  98. if ($issuer === $provider) {
  99. return null;
  100. }
  101. return new Rule2Literals(-$issuer->id, -$provider->id, $reason, $reasonData);
  102. }
  103. protected function createMultiConflictRule(array $packages, $reason, $reasonData = null)
  104. {
  105. $literals = array();
  106. foreach ($packages as $package) {
  107. $literals[] = -$package->id;
  108. }
  109. if (count($literals) == 2) {
  110. return new Rule2Literals($literals[0], $literals[1], $reason, $reasonData);
  111. }
  112. return new MultiConflictRule($literals, $reason, $reasonData);
  113. }
  114. /**
  115. * Adds a rule unless it duplicates an existing one of any type
  116. *
  117. * To be able to directly pass in the result of one of the rule creation
  118. * methods null is allowed which will not insert a rule.
  119. *
  120. * @param int $type A TYPE_* constant defining the rule type
  121. * @param Rule $newRule The rule about to be added
  122. */
  123. private function addRule($type, Rule $newRule = null)
  124. {
  125. if (!$newRule) {
  126. return;
  127. }
  128. $this->rules->add($newRule, $type);
  129. }
  130. protected function addRulesForPackage(PackageInterface $package, $ignorePlatformReqs)
  131. {
  132. $workQueue = new \SplQueue;
  133. $workQueue->enqueue($package);
  134. while (!$workQueue->isEmpty()) {
  135. /** @var PackageInterface $package */
  136. $package = $workQueue->dequeue();
  137. if (isset($this->addedMap[$package->id])) {
  138. continue;
  139. }
  140. $this->addedMap[$package->id] = true;
  141. $this->addedPackages[] = $package;
  142. foreach ($package->getNames() as $name) {
  143. $this->addedPackagesByNames[$name][] = $package;
  144. }
  145. foreach ($package->getRequires() as $link) {
  146. if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) {
  147. continue;
  148. }
  149. $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
  150. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, $link));
  151. foreach ($possibleRequires as $require) {
  152. $workQueue->enqueue($require);
  153. }
  154. }
  155. $packageName = $package->getName();
  156. $obsoleteProviders = $this->pool->whatProvides($packageName, null);
  157. foreach ($obsoleteProviders as $provider) {
  158. if ($provider === $package) {
  159. continue;
  160. }
  161. if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
  162. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package));
  163. } else {
  164. if (!isset($this->conflictsForName[$packageName])) {
  165. $this->conflictsForName[$packageName] = array();
  166. }
  167. if (!$package instanceof AliasPackage) {
  168. $this->conflictsForName[$packageName][$package->id] = $package;
  169. }
  170. if (!$provider instanceof AliasPackage) {
  171. $this->conflictsForName[$packageName][$provider->id] = $provider;
  172. }
  173. }
  174. }
  175. }
  176. }
  177. protected function addConflictRules($ignorePlatformReqs = false)
  178. {
  179. /** @var PackageInterface $package */
  180. foreach ($this->addedPackages as $package) {
  181. foreach ($package->getConflicts() as $link) {
  182. if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
  183. continue;
  184. }
  185. if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) {
  186. continue;
  187. }
  188. /** @var PackageInterface $possibleConflict */
  189. foreach ($this->addedPackagesByNames[$link->getTarget()] as $possibleConflict) {
  190. $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint(), true);
  191. if ($conflictMatch === Pool::MATCH || $conflictMatch === Pool::MATCH_REPLACE) {
  192. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $possibleConflict, Rule::RULE_PACKAGE_CONFLICT, $link));
  193. }
  194. }
  195. }
  196. // check obsoletes and implicit obsoletes of a package
  197. foreach ($package->getReplaces() as $link) {
  198. if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
  199. continue;
  200. }
  201. /** @var PackageInterface $possibleConflict */
  202. foreach ($this->addedPackagesByNames[$link->getTarget()] as $provider) {
  203. if ($provider === $package) {
  204. continue;
  205. }
  206. if (!$this->obsoleteImpossibleForAlias($package, $provider)) {
  207. $reason = Rule::RULE_PACKAGE_OBSOLETES;
  208. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $link));
  209. }
  210. }
  211. }
  212. }
  213. foreach ($this->conflictsForName as $name => $packages) {
  214. if (count($packages) > 1) {
  215. $reason = Rule::RULE_PACKAGE_SAME_NAME;
  216. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createMultiConflictRule($packages, $reason, null));
  217. }
  218. }
  219. }
  220. protected function obsoleteImpossibleForAlias($package, $provider)
  221. {
  222. $packageIsAlias = $package instanceof AliasPackage;
  223. $providerIsAlias = $provider instanceof AliasPackage;
  224. $impossible = (
  225. ($packageIsAlias && $package->getAliasOf() === $provider) ||
  226. ($providerIsAlias && $provider->getAliasOf() === $package) ||
  227. ($packageIsAlias && $providerIsAlias && $provider->getAliasOf() === $package->getAliasOf())
  228. );
  229. return $impossible;
  230. }
  231. protected function addRulesForRequest(Request $request, $ignorePlatformReqs)
  232. {
  233. $unlockableMap = $request->getUnlockableMap();
  234. foreach ($request->getFixedPackages() as $package) {
  235. if ($package->id == -1) {
  236. // fixed package was not added to the pool as it did not pass the stability requirements, this is fine
  237. if ($this->pool->isUnacceptableFixedPackage($package)) {
  238. continue;
  239. }
  240. // otherwise, looks like a bug
  241. throw new \LogicException("Fixed package ".$package->getName()." ".$package->getVersion().($package instanceof AliasPackage ? " (alias)" : "")." was not added to solver pool.");
  242. }
  243. $this->addRulesForPackage($package, $ignorePlatformReqs);
  244. $rule = $this->createInstallOneOfRule(array($package), Rule::RULE_FIXED, array(
  245. 'package' => $package,
  246. 'lockable' => !isset($unlockableMap[$package->id]),
  247. ));
  248. $this->addRule(RuleSet::TYPE_REQUEST, $rule);
  249. }
  250. foreach ($request->getRequires() as $packageName => $constraint) {
  251. if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $packageName)) {
  252. continue;
  253. }
  254. $packages = $this->pool->whatProvides($packageName, $constraint);
  255. if ($packages) {
  256. foreach ($packages as $package) {
  257. $this->addRulesForPackage($package, $ignorePlatformReqs);
  258. }
  259. $rule = $this->createInstallOneOfRule($packages, Rule::RULE_ROOT_REQUIRE, array(
  260. 'packageName' => $packageName,
  261. 'constraint' => $constraint,
  262. ));
  263. $this->addRule(RuleSet::TYPE_REQUEST, $rule);
  264. }
  265. }
  266. }
  267. public function getRulesFor(Request $request, $ignorePlatformReqs = false)
  268. {
  269. $this->rules = new RuleSet;
  270. $this->addedMap = array();
  271. $this->conflictAddedMap = array();
  272. $this->addedPackages = array();
  273. $this->addedPackagesByNames = array();
  274. $this->conflictsForName = array();
  275. $this->addRulesForRequest($request, $ignorePlatformReqs);
  276. $this->addConflictRules($ignorePlatformReqs);
  277. // Remove references to packages
  278. $this->addedPackages = $this->addedPackagesByNames = null;
  279. return $this->rules;
  280. }
  281. }