Transaction.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306
  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\AliasPackage;
  13. use Composer\Package\Link;
  14. use Composer\Package\PackageInterface;
  15. use Composer\Repository\PlatformRepository;
  16. /**
  17. * @author Nils Adermann <naderman@naderman.de>
  18. */
  19. class Transaction
  20. {
  21. /**
  22. * @var array
  23. */
  24. protected $operations;
  25. /**
  26. * Packages present at the beginning of the transaction
  27. * @var array
  28. */
  29. protected $presentPackages;
  30. /**
  31. * Package set resulting from this transaction
  32. * @var array
  33. */
  34. protected $resultPackageMap;
  35. /**
  36. * @var array
  37. */
  38. protected $resultPackagesByName = array();
  39. public function __construct($presentPackages, $resultPackages)
  40. {
  41. $this->presentPackages = $presentPackages;
  42. $this->setResultPackageMaps($resultPackages);
  43. $this->operations = $this->calculateOperations();
  44. }
  45. public function getOperations()
  46. {
  47. return $this->operations;
  48. }
  49. private function setResultPackageMaps($resultPackages)
  50. {
  51. $packageSort = function (PackageInterface $a, PackageInterface $b) {
  52. // sort alias packages by the same name behind their non alias version
  53. if ($a->getName() == $b->getName() && $a instanceof AliasPackage != $b instanceof AliasPackage) {
  54. return $a instanceof AliasPackage ? -1 : 1;
  55. }
  56. return strcmp($b->getName(), $a->getName());
  57. };
  58. $this->resultPackageMap = array();
  59. foreach ($resultPackages as $package) {
  60. $this->resultPackageMap[spl_object_hash($package)] = $package;
  61. foreach ($package->getNames() as $name) {
  62. $this->resultPackagesByName[$name][] = $package;
  63. }
  64. }
  65. uasort($this->resultPackageMap, $packageSort);
  66. foreach ($this->resultPackagesByName as $name => $packages) {
  67. uasort($this->resultPackagesByName[$name], $packageSort);
  68. }
  69. }
  70. protected function calculateOperations()
  71. {
  72. $operations = array();
  73. $presentPackageMap = array();
  74. $removeMap = array();
  75. $presentAliasMap = array();
  76. $removeAliasMap = array();
  77. foreach ($this->presentPackages as $package) {
  78. if ($package instanceof AliasPackage) {
  79. $presentAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
  80. $removeAliasMap[$package->getName().'::'.$package->getVersion()] = $package;
  81. } else {
  82. $presentPackageMap[$package->getName()] = $package;
  83. $removeMap[$package->getName()] = $package;
  84. }
  85. }
  86. $stack = $this->getRootPackages();
  87. $visited = array();
  88. $processed = array();
  89. while (!empty($stack)) {
  90. $package = array_pop($stack);
  91. if (isset($processed[spl_object_hash($package)])) {
  92. continue;
  93. }
  94. if (!isset($visited[spl_object_hash($package)])) {
  95. $visited[spl_object_hash($package)] = true;
  96. $stack[] = $package;
  97. if ($package instanceof AliasPackage) {
  98. $stack[] = $package->getAliasOf();
  99. } else {
  100. foreach ($package->getRequires() as $link) {
  101. $possibleRequires = $this->getProvidersInResult($link);
  102. foreach ($possibleRequires as $require) {
  103. $stack[] = $require;
  104. }
  105. }
  106. }
  107. } elseif (!isset($processed[spl_object_hash($package)])) {
  108. $processed[spl_object_hash($package)] = true;
  109. if ($package instanceof AliasPackage) {
  110. $aliasKey = $package->getName().'::'.$package->getVersion();
  111. if (isset($presentAliasMap[$aliasKey])) {
  112. unset($removeAliasMap[$aliasKey]);
  113. } else {
  114. $operations[] = new Operation\MarkAliasInstalledOperation($package);
  115. }
  116. } else {
  117. if (isset($presentPackageMap[$package->getName()])) {
  118. $source = $presentPackageMap[$package->getName()];
  119. // do we need to update?
  120. // TODO different for lock?
  121. if ($package->getVersion() != $presentPackageMap[$package->getName()]->getVersion() ||
  122. $package->getDistReference() !== $presentPackageMap[$package->getName()]->getDistReference() ||
  123. $package->getSourceReference() !== $presentPackageMap[$package->getName()]->getSourceReference()
  124. ) {
  125. $operations[] = new Operation\UpdateOperation($source, $package);
  126. }
  127. unset($removeMap[$package->getName()]);
  128. } else {
  129. $operations[] = new Operation\InstallOperation($package);
  130. unset($removeMap[$package->getName()]);
  131. }
  132. }
  133. }
  134. }
  135. foreach ($removeMap as $name => $package) {
  136. array_unshift($operations, new Operation\UninstallOperation($package, null));
  137. }
  138. foreach ($removeAliasMap as $nameVersion => $package) {
  139. $operations[] = new Operation\MarkAliasUninstalledOperation($package, null);
  140. }
  141. $operations = $this->movePluginsToFront($operations);
  142. // TODO fix this:
  143. // we have to do this again here even though the above stack code did it because moving plugins moves them before uninstalls
  144. $operations = $this->moveUninstallsToFront($operations);
  145. // TODO skip updates which don't update? is this needed? we shouldn't schedule this update in the first place?
  146. /*
  147. if ('update' === $opType) {
  148. $targetPackage = $operation->getTargetPackage();
  149. if ($targetPackage->isDev()) {
  150. $initialPackage = $operation->getInitialPackage();
  151. if ($targetPackage->getVersion() === $initialPackage->getVersion()
  152. && (!$targetPackage->getSourceReference() || $targetPackage->getSourceReference() === $initialPackage->getSourceReference())
  153. && (!$targetPackage->getDistReference() || $targetPackage->getDistReference() === $initialPackage->getDistReference())
  154. ) {
  155. $this->io->writeError(' - Skipping update of ' . $targetPackage->getPrettyName() . ' to the same reference-locked version', true, IOInterface::DEBUG);
  156. $this->io->writeError('', true, IOInterface::DEBUG);
  157. continue;
  158. }
  159. }
  160. }*/
  161. return $this->operations = $operations;
  162. }
  163. /**
  164. * Determine which packages in the result are not required by any other packages in it.
  165. *
  166. * These serve as a starting point to enumerate packages in a topological order despite potential cycles.
  167. * If there are packages with a cycle on the top level the package with the lowest name gets picked
  168. *
  169. * @return array
  170. */
  171. protected function getRootPackages()
  172. {
  173. $roots = $this->resultPackageMap;
  174. foreach ($this->resultPackageMap as $packageHash => $package) {
  175. if (!isset($roots[$packageHash])) {
  176. continue;
  177. }
  178. foreach ($package->getRequires() as $link) {
  179. $possibleRequires = $this->getProvidersInResult($link);
  180. foreach ($possibleRequires as $require) {
  181. if ($require !== $package) {
  182. unset($roots[spl_object_hash($require)]);
  183. }
  184. }
  185. }
  186. }
  187. return $roots;
  188. }
  189. protected function getProvidersInResult(Link $link)
  190. {
  191. if (!isset($this->resultPackagesByName[$link->getTarget()])) {
  192. return array();
  193. }
  194. return $this->resultPackagesByName[$link->getTarget()];
  195. }
  196. /**
  197. * Workaround: if your packages depend on plugins, we must be sure
  198. * that those are installed / updated first; else it would lead to packages
  199. * being installed multiple times in different folders, when running Composer
  200. * twice.
  201. *
  202. * While this does not fix the root-causes of https://github.com/composer/composer/issues/1147,
  203. * it at least fixes the symptoms and makes usage of composer possible (again)
  204. * in such scenarios.
  205. *
  206. * @param Operation\OperationInterface[] $operations
  207. * @return Operation\OperationInterface[] reordered operation list
  208. */
  209. private function movePluginsToFront(array $operations)
  210. {
  211. $pluginsNoDeps = array();
  212. $pluginsWithDeps = array();
  213. $pluginRequires = array();
  214. foreach (array_reverse($operations, true) as $idx => $op) {
  215. if ($op instanceof Operation\InstallOperation) {
  216. $package = $op->getPackage();
  217. } elseif ($op instanceof Operation\UpdateOperation) {
  218. $package = $op->getTargetPackage();
  219. } else {
  220. continue;
  221. }
  222. // is this package a plugin?
  223. $isPlugin = $package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer';
  224. // is this a plugin or a dependency of a plugin?
  225. if ($isPlugin || count(array_intersect($package->getNames(), $pluginRequires))) {
  226. // get the package's requires, but filter out any platform requirements
  227. $requires = array_filter(array_keys($package->getRequires()), function ($req) {
  228. return !preg_match(PlatformRepository::PLATFORM_PACKAGE_REGEX, $req);
  229. });
  230. // is this a plugin with no meaningful dependencies?
  231. if ($isPlugin && !count($requires)) {
  232. // plugins with no dependencies go to the very front
  233. array_unshift($pluginsNoDeps, $op);
  234. } else {
  235. // capture the requirements for this package so those packages will be moved up as well
  236. $pluginRequires = array_merge($pluginRequires, $requires);
  237. // move the operation to the front
  238. array_unshift($pluginsWithDeps, $op);
  239. }
  240. unset($operations[$idx]);
  241. }
  242. }
  243. return array_merge($pluginsNoDeps, $pluginsWithDeps, $operations);
  244. }
  245. /**
  246. * Removals of packages should be executed before installations in
  247. * case two packages resolve to the same path (due to custom installers)
  248. *
  249. * @param Operation\OperationInterface[] $operations
  250. * @return Operation\OperationInterface[] reordered operation list
  251. */
  252. private function moveUninstallsToFront(array $operations)
  253. {
  254. $uninstOps = array();
  255. foreach ($operations as $idx => $op) {
  256. if ($op instanceof Operation\UninstallOperation || $op instanceof Operation\MarkAliasUninstalledOperation) {
  257. $uninstOps[] = $op;
  258. unset($operations[$idx]);
  259. }
  260. }
  261. return array_merge($uninstOps, $operations);
  262. }
  263. }