RuleSetGenerator.php 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372
  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\PackageInterface;
  13. use Composer\Package\AliasPackage;
  14. use Composer\Repository\PlatformRepository;
  15. /**
  16. * @author Nils Adermann <naderman@naderman.de>
  17. */
  18. class RuleSetGenerator
  19. {
  20. protected $policy;
  21. protected $pool;
  22. protected $rules;
  23. protected $jobs;
  24. protected $installedMap;
  25. protected $whitelistedMap;
  26. protected $addedMap;
  27. protected $conflictAddedMap;
  28. protected $addedPackages;
  29. protected $addedPackagesByNames;
  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 $job The job this rule was created from
  71. * @return Rule The generated rule
  72. */
  73. protected function createInstallOneOfRule(array $packages, $reason, $job)
  74. {
  75. $literals = array();
  76. foreach ($packages as $package) {
  77. $literals[] = $package->id;
  78. }
  79. return new GenericRule($literals, $reason, $job['packageName'], $job);
  80. }
  81. /**
  82. * Creates a rule to remove a package
  83. *
  84. * The rule for a package A is (-A).
  85. *
  86. * @param PackageInterface $package The package to be removed
  87. * @param int $reason A RULE_* constant describing the
  88. * reason for generating this rule
  89. * @param array $job The job this rule was created from
  90. * @return Rule The generated rule
  91. */
  92. protected function createRemoveRule(PackageInterface $package, $reason, $job)
  93. {
  94. return new GenericRule(array(-$package->id), $reason, $job['packageName'], $job);
  95. }
  96. /**
  97. * Creates a rule for two conflicting packages
  98. *
  99. * The rule for conflicting packages A and B is (-A|-B). A is called the issuer
  100. * and B the provider.
  101. *
  102. * @param PackageInterface $issuer The package declaring the conflict
  103. * @param PackageInterface $provider The package causing the conflict
  104. * @param int $reason A RULE_* constant describing the
  105. * reason for generating this rule
  106. * @param mixed $reasonData Any data, e.g. the package name, that
  107. * goes with the reason
  108. * @return Rule|null The generated rule
  109. */
  110. protected function createRule2Literals(PackageInterface $issuer, PackageInterface $provider, $reason, $reasonData = null)
  111. {
  112. // ignore self conflict
  113. if ($issuer === $provider) {
  114. return null;
  115. }
  116. return new Rule2Literals(-$issuer->id, -$provider->id, $reason, $reasonData);
  117. }
  118. /**
  119. * Adds a rule unless it duplicates an existing one of any type
  120. *
  121. * To be able to directly pass in the result of one of the rule creation
  122. * methods null is allowed which will not insert a rule.
  123. *
  124. * @param int $type A TYPE_* constant defining the rule type
  125. * @param Rule $newRule The rule about to be added
  126. */
  127. private function addRule($type, Rule $newRule = null)
  128. {
  129. if (!$newRule) {
  130. return;
  131. }
  132. $this->rules->add($newRule, $type);
  133. }
  134. protected function whitelistFromPackage(PackageInterface $package)
  135. {
  136. $workQueue = new \SplQueue;
  137. $workQueue->enqueue($package);
  138. while (!$workQueue->isEmpty()) {
  139. $package = $workQueue->dequeue();
  140. if (isset($this->whitelistedMap[$package->id])) {
  141. continue;
  142. }
  143. $this->whitelistedMap[$package->id] = true;
  144. foreach ($package->getRequires() as $link) {
  145. $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint(), true);
  146. foreach ($possibleRequires as $require) {
  147. $workQueue->enqueue($require);
  148. }
  149. }
  150. $obsoleteProviders = $this->pool->whatProvides($package->getName(), null, true);
  151. foreach ($obsoleteProviders as $provider) {
  152. if ($provider === $package) {
  153. continue;
  154. }
  155. if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
  156. $workQueue->enqueue($provider);
  157. }
  158. }
  159. }
  160. }
  161. protected function addRulesForPackage(PackageInterface $package, $ignorePlatformReqs)
  162. {
  163. $workQueue = new \SplQueue;
  164. $workQueue->enqueue($package);
  165. while (!$workQueue->isEmpty()) {
  166. /** @var PackageInterface $package */
  167. $package = $workQueue->dequeue();
  168. if (isset($this->addedMap[$package->id])) {
  169. continue;
  170. }
  171. $this->addedMap[$package->id] = true;
  172. $this->addedPackages[] = $package;
  173. foreach ($package->getNames() as $name) {
  174. $this->addedPackagesByNames[$name][] = $package;
  175. }
  176. foreach ($package->getRequires() as $link) {
  177. if ($ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $link->getTarget())) {
  178. continue;
  179. }
  180. $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint());
  181. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, $possibleRequires, Rule::RULE_PACKAGE_REQUIRES, $link));
  182. foreach ($possibleRequires as $require) {
  183. $workQueue->enqueue($require);
  184. }
  185. }
  186. $packageName = $package->getName();
  187. $obsoleteProviders = $this->pool->whatProvides($packageName, null);
  188. foreach ($obsoleteProviders as $provider) {
  189. if ($provider === $package) {
  190. continue;
  191. }
  192. if (($package instanceof AliasPackage) && $package->getAliasOf() === $provider) {
  193. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRequireRule($package, array($provider), Rule::RULE_PACKAGE_ALIAS, $package));
  194. } elseif (!$this->obsoleteImpossibleForAlias($package, $provider)) {
  195. $reason = ($packageName == $provider->getName()) ? Rule::RULE_PACKAGE_SAME_NAME : Rule::RULE_PACKAGE_IMPLICIT_OBSOLETES;
  196. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $package));
  197. }
  198. }
  199. }
  200. }
  201. protected function addConflictRules()
  202. {
  203. /** @var PackageInterface $package */
  204. foreach ($this->addedPackages as $package) {
  205. foreach ($package->getConflicts() as $link) {
  206. if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
  207. continue;
  208. }
  209. /** @var PackageInterface $possibleConflict */
  210. foreach ($this->addedPackagesByNames[$link->getTarget()] as $possibleConflict) {
  211. $conflictMatch = $this->pool->match($possibleConflict, $link->getTarget(), $link->getConstraint(), true);
  212. if ($conflictMatch === Pool::MATCH || $conflictMatch === Pool::MATCH_REPLACE) {
  213. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $possibleConflict, Rule::RULE_PACKAGE_CONFLICT, $link));
  214. }
  215. }
  216. }
  217. // check obsoletes and implicit obsoletes of a package
  218. $isInstalled = isset($this->installedMap[$package->id]);
  219. foreach ($package->getReplaces() as $link) {
  220. if (!isset($this->addedPackagesByNames[$link->getTarget()])) {
  221. continue;
  222. }
  223. /** @var PackageInterface $possibleConflict */
  224. foreach ($this->addedPackagesByNames[$link->getTarget()] as $provider) {
  225. if ($provider === $package) {
  226. continue;
  227. }
  228. if (!$this->obsoleteImpossibleForAlias($package, $provider)) {
  229. $reason = $isInstalled ? Rule::RULE_INSTALLED_PACKAGE_OBSOLETES : Rule::RULE_PACKAGE_OBSOLETES;
  230. $this->addRule(RuleSet::TYPE_PACKAGE, $this->createRule2Literals($package, $provider, $reason, $link));
  231. }
  232. }
  233. }
  234. }
  235. }
  236. protected function obsoleteImpossibleForAlias($package, $provider)
  237. {
  238. $packageIsAlias = $package instanceof AliasPackage;
  239. $providerIsAlias = $provider instanceof AliasPackage;
  240. $impossible = (
  241. ($packageIsAlias && $package->getAliasOf() === $provider) ||
  242. ($providerIsAlias && $provider->getAliasOf() === $package) ||
  243. ($packageIsAlias && $providerIsAlias && $provider->getAliasOf() === $package->getAliasOf())
  244. );
  245. return $impossible;
  246. }
  247. protected function whitelistFromJobs()
  248. {
  249. foreach ($this->jobs as $job) {
  250. switch ($job['cmd']) {
  251. case 'install':
  252. $packages = $this->pool->whatProvides($job['packageName'], $job['constraint'], true);
  253. foreach ($packages as $package) {
  254. $this->whitelistFromPackage($package);
  255. }
  256. break;
  257. }
  258. }
  259. }
  260. protected function addRulesForJobs($ignorePlatformReqs)
  261. {
  262. foreach ($this->jobs as $job) {
  263. switch ($job['cmd']) {
  264. case 'install':
  265. if (!$job['fixed'] && $ignorePlatformReqs && preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $job['packageName'])) {
  266. break;
  267. }
  268. $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
  269. if ($packages) {
  270. foreach ($packages as $package) {
  271. if (!isset($this->installedMap[$package->id])) {
  272. $this->addRulesForPackage($package, $ignorePlatformReqs);
  273. }
  274. }
  275. $rule = $this->createInstallOneOfRule($packages, Rule::RULE_JOB_INSTALL, $job);
  276. $this->addRule(RuleSet::TYPE_JOB, $rule);
  277. }
  278. break;
  279. case 'remove':
  280. // remove all packages with this name including uninstalled
  281. // ones to make sure none of them are picked as replacements
  282. $packages = $this->pool->whatProvides($job['packageName'], $job['constraint']);
  283. foreach ($packages as $package) {
  284. $rule = $this->createRemoveRule($package, Rule::RULE_JOB_REMOVE, $job);
  285. $this->addRule(RuleSet::TYPE_JOB, $rule);
  286. }
  287. break;
  288. }
  289. }
  290. }
  291. public function getRulesFor($jobs, $installedMap, $ignorePlatformReqs = false)
  292. {
  293. $this->jobs = $jobs;
  294. $this->rules = new RuleSet;
  295. $this->installedMap = $installedMap;
  296. $this->whitelistedMap = array();
  297. foreach ($this->installedMap as $package) {
  298. $this->whitelistFromPackage($package);
  299. }
  300. $this->whitelistFromJobs();
  301. $this->pool->setWhitelist($this->whitelistedMap);
  302. $this->addedMap = array();
  303. $this->conflictAddedMap = array();
  304. $this->addedPackages = array();
  305. $this->addedPackagesByNames = array();
  306. foreach ($this->installedMap as $package) {
  307. $this->addRulesForPackage($package, $ignorePlatformReqs);
  308. }
  309. $this->addRulesForJobs($ignorePlatformReqs);
  310. $this->addConflictRules();
  311. // Remove references to packages
  312. $this->addedPackages = $this->addedPackagesByNames = null;
  313. return $this->rules;
  314. }
  315. }