InstallationManager.php 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506
  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\Installer;
  12. use Composer\IO\IOInterface;
  13. use Composer\Package\PackageInterface;
  14. use Composer\Package\AliasPackage;
  15. use Composer\Repository\RepositoryInterface;
  16. use Composer\Repository\InstalledRepositoryInterface;
  17. use Composer\DependencyResolver\Operation\OperationInterface;
  18. use Composer\DependencyResolver\Operation\InstallOperation;
  19. use Composer\DependencyResolver\Operation\UpdateOperation;
  20. use Composer\DependencyResolver\Operation\UninstallOperation;
  21. use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
  22. use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
  23. use Composer\EventDispatcher\EventDispatcher;
  24. use Composer\Util\StreamContextFactory;
  25. use Composer\Util\Loop;
  26. /**
  27. * Package operation manager.
  28. *
  29. * @author Konstantin Kudryashov <ever.zet@gmail.com>
  30. * @author Jordi Boggiano <j.boggiano@seld.be>
  31. * @author Nils Adermann <naderman@naderman.de>
  32. */
  33. class InstallationManager
  34. {
  35. private $installers = array();
  36. private $cache = array();
  37. private $notifiablePackages = array();
  38. private $loop;
  39. private $io;
  40. private $eventDispatcher;
  41. public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null)
  42. {
  43. $this->loop = $loop;
  44. $this->io = $io;
  45. $this->eventDispatcher = $eventDispatcher;
  46. }
  47. public function reset()
  48. {
  49. $this->notifiablePackages = array();
  50. }
  51. /**
  52. * Adds installer
  53. *
  54. * @param InstallerInterface $installer installer instance
  55. */
  56. public function addInstaller(InstallerInterface $installer)
  57. {
  58. array_unshift($this->installers, $installer);
  59. $this->cache = array();
  60. }
  61. /**
  62. * Removes installer
  63. *
  64. * @param InstallerInterface $installer installer instance
  65. */
  66. public function removeInstaller(InstallerInterface $installer)
  67. {
  68. if (false !== ($key = array_search($installer, $this->installers, true))) {
  69. array_splice($this->installers, $key, 1);
  70. $this->cache = array();
  71. }
  72. }
  73. /**
  74. * Disables plugins.
  75. *
  76. * We prevent any plugins from being instantiated by simply
  77. * deactivating the installer for them. This ensure that no third-party
  78. * code is ever executed.
  79. */
  80. public function disablePlugins()
  81. {
  82. foreach ($this->installers as $i => $installer) {
  83. if (!$installer instanceof PluginInstaller) {
  84. continue;
  85. }
  86. unset($this->installers[$i]);
  87. }
  88. }
  89. /**
  90. * Returns installer for a specific package type.
  91. *
  92. * @param string $type package type
  93. *
  94. * @throws \InvalidArgumentException if installer for provided type is not registered
  95. * @return InstallerInterface
  96. */
  97. public function getInstaller($type)
  98. {
  99. $type = strtolower($type);
  100. if (isset($this->cache[$type])) {
  101. return $this->cache[$type];
  102. }
  103. foreach ($this->installers as $installer) {
  104. if ($installer->supports($type)) {
  105. return $this->cache[$type] = $installer;
  106. }
  107. }
  108. throw new \InvalidArgumentException('Unknown installer type: '.$type);
  109. }
  110. /**
  111. * Checks whether provided package is installed in one of the registered installers.
  112. *
  113. * @param InstalledRepositoryInterface $repo repository in which to check
  114. * @param PackageInterface $package package instance
  115. *
  116. * @return bool
  117. */
  118. public function isPackageInstalled(InstalledRepositoryInterface $repo, PackageInterface $package)
  119. {
  120. if ($package instanceof AliasPackage) {
  121. return $repo->hasPackage($package) && $this->isPackageInstalled($repo, $package->getAliasOf());
  122. }
  123. return $this->getInstaller($package->getType())->isInstalled($repo, $package);
  124. }
  125. /**
  126. * Install binary for the given package.
  127. * If the installer associated to this package doesn't handle that function, it'll do nothing.
  128. *
  129. * @param PackageInterface $package Package instance
  130. */
  131. public function ensureBinariesPresence(PackageInterface $package)
  132. {
  133. try {
  134. $installer = $this->getInstaller($package->getType());
  135. } catch (\InvalidArgumentException $e) {
  136. // no installer found for the current package type (@see `getInstaller()`)
  137. return;
  138. }
  139. // if the given installer support installing binaries
  140. if ($installer instanceof BinaryPresenceInterface) {
  141. $installer->ensureBinariesPresence($package);
  142. }
  143. }
  144. /**
  145. * Executes solver operation.
  146. *
  147. * @param RepositoryInterface $repo repository in which to add/remove/update packages
  148. * @param OperationInterface[] $operations operations to execute
  149. * @param bool $devMode whether the install is being run in dev mode
  150. * @param bool $operation whether to dispatch script events
  151. */
  152. public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true)
  153. {
  154. $promises = array();
  155. $cleanupPromises = array();
  156. $loop = $this->loop;
  157. $runCleanup = function () use (&$cleanupPromises, $loop) {
  158. $promises = array();
  159. foreach ($cleanupPromises as $cleanup) {
  160. $promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) {
  161. $promise = $cleanup();
  162. if (null === $promise) {
  163. $resolve();
  164. } else {
  165. $promise->then(function () use ($resolve) {
  166. $resolve();
  167. });
  168. }
  169. });
  170. }
  171. if (!empty($promises)) {
  172. $loop->wait($promises);
  173. }
  174. };
  175. // handler Ctrl+C for unix-like systems
  176. $handleInterrupts = function_exists('pcntl_async_signals') && function_exists('pcntl_signal');
  177. $prevHandler = null;
  178. if ($handleInterrupts) {
  179. pcntl_async_signals(true);
  180. $prevHandler = pcntl_signal_get_handler(SIGINT);
  181. pcntl_signal(SIGINT, function ($sig) use ($runCleanup, $prevHandler) {
  182. $runCleanup();
  183. if (!in_array($prevHandler, array(SIG_DFL, SIG_IGN), true)) {
  184. call_user_func($prevHandler, $sig);
  185. }
  186. exit(130);
  187. });
  188. }
  189. try {
  190. foreach ($operations as $index => $operation) {
  191. $opType = $operation->getOperationType();
  192. // ignoring alias ops as they don't need to execute anything at this stage
  193. if (!in_array($opType, array('update', 'install', 'uninstall'))) {
  194. continue;
  195. }
  196. if ($opType === 'update') {
  197. $package = $operation->getTargetPackage();
  198. $initialPackage = $operation->getInitialPackage();
  199. } else {
  200. $package = $operation->getPackage();
  201. $initialPackage = null;
  202. }
  203. $installer = $this->getInstaller($package->getType());
  204. $cleanupPromises[$index] = function () use ($opType, $installer, $package, $initialPackage) {
  205. // avoid calling cleanup if the download was not even initialized for a package
  206. // as without installation source configured nothing will work
  207. if (!$package->getInstallationSource()) {
  208. return;
  209. }
  210. return $installer->cleanup($opType, $package, $initialPackage);
  211. };
  212. if ($opType !== 'uninstall') {
  213. $promise = $installer->download($package, $initialPackage);
  214. if ($promise) {
  215. $promises[] = $promise;
  216. }
  217. }
  218. }
  219. // execute all downloads first
  220. if (!empty($promises)) {
  221. $this->loop->wait($promises);
  222. }
  223. foreach ($operations as $index => $operation) {
  224. $opType = $operation->getOperationType();
  225. // ignoring alias ops as they don't need to execute anything
  226. if (!in_array($opType, array('update', 'install', 'uninstall'))) {
  227. // output alias ops in debug verbosity as they have no output otherwise
  228. if ($this->io->isDebug()) {
  229. $this->io->writeError(' - ' . $operation->show(false));
  230. }
  231. $this->$opType($repo, $operation);
  232. continue;
  233. }
  234. if ($opType === 'update') {
  235. $package = $operation->getTargetPackage();
  236. $initialPackage = $operation->getInitialPackage();
  237. } else {
  238. $package = $operation->getPackage();
  239. $initialPackage = null;
  240. }
  241. $installer = $this->getInstaller($package->getType());
  242. $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($opType);
  243. if (defined($event) && $runScripts && $this->eventDispatcher) {
  244. $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
  245. }
  246. $dispatcher = $this->eventDispatcher;
  247. $installManager = $this;
  248. $loop = $this->loop;
  249. $io = $this->io;
  250. $promise = $installer->prepare($opType, $package, $initialPackage);
  251. if (null === $promise) {
  252. $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); });
  253. }
  254. $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) {
  255. return $installManager->$opType($repo, $operation);
  256. })->then($cleanupPromises[$index])
  257. ->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) {
  258. $repo->write($devMode, $installManager);
  259. $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($opType);
  260. if (defined($event) && $runScripts && $dispatcher) {
  261. $dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation);
  262. }
  263. }, function ($e) use ($opType, $package, $io) {
  264. $io->writeError(' <error>' . ucfirst($opType) .' of '.$package->getPrettyName().' failed</error>');
  265. throw $e;
  266. });
  267. $promises[] = $promise;
  268. }
  269. // execute all prepare => installs/updates/removes => cleanup steps
  270. if (!empty($promises)) {
  271. $this->loop->wait($promises);
  272. }
  273. } catch (\Exception $e) {
  274. $runCleanup();
  275. if ($handleInterrupts) {
  276. pcntl_signal(SIGINT, $prevHandler);
  277. }
  278. throw $e;
  279. }
  280. if ($handleInterrupts) {
  281. pcntl_signal(SIGINT, $prevHandler);
  282. }
  283. // do a last write so that we write the repository even if nothing changed
  284. // as that can trigger an update of some files like InstalledVersions.php if
  285. // running a new composer version
  286. $repo->write($devMode, $this);
  287. }
  288. /**
  289. * Executes install operation.
  290. *
  291. * @param RepositoryInterface $repo repository in which to check
  292. * @param InstallOperation $operation operation instance
  293. */
  294. public function install(RepositoryInterface $repo, InstallOperation $operation)
  295. {
  296. $package = $operation->getPackage();
  297. $installer = $this->getInstaller($package->getType());
  298. $promise = $installer->install($repo, $package);
  299. $this->markForNotification($package);
  300. return $promise;
  301. }
  302. /**
  303. * Executes update operation.
  304. *
  305. * @param RepositoryInterface $repo repository in which to check
  306. * @param UpdateOperation $operation operation instance
  307. */
  308. public function update(RepositoryInterface $repo, UpdateOperation $operation)
  309. {
  310. $initial = $operation->getInitialPackage();
  311. $target = $operation->getTargetPackage();
  312. $initialType = $initial->getType();
  313. $targetType = $target->getType();
  314. if ($initialType === $targetType) {
  315. $installer = $this->getInstaller($initialType);
  316. $promise = $installer->update($repo, $initial, $target);
  317. $this->markForNotification($target);
  318. } else {
  319. $this->getInstaller($initialType)->uninstall($repo, $initial);
  320. $installer = $this->getInstaller($targetType);
  321. $promise = $installer->install($repo, $target);
  322. }
  323. return $promise;
  324. }
  325. /**
  326. * Uninstalls package.
  327. *
  328. * @param RepositoryInterface $repo repository in which to check
  329. * @param UninstallOperation $operation operation instance
  330. */
  331. public function uninstall(RepositoryInterface $repo, UninstallOperation $operation)
  332. {
  333. $package = $operation->getPackage();
  334. $installer = $this->getInstaller($package->getType());
  335. return $installer->uninstall($repo, $package);
  336. }
  337. /**
  338. * Executes markAliasInstalled operation.
  339. *
  340. * @param RepositoryInterface $repo repository in which to check
  341. * @param MarkAliasInstalledOperation $operation operation instance
  342. */
  343. public function markAliasInstalled(RepositoryInterface $repo, MarkAliasInstalledOperation $operation)
  344. {
  345. $package = $operation->getPackage();
  346. if (!$repo->hasPackage($package)) {
  347. $repo->addPackage(clone $package);
  348. }
  349. }
  350. /**
  351. * Executes markAlias operation.
  352. *
  353. * @param RepositoryInterface $repo repository in which to check
  354. * @param MarkAliasUninstalledOperation $operation operation instance
  355. */
  356. public function markAliasUninstalled(RepositoryInterface $repo, MarkAliasUninstalledOperation $operation)
  357. {
  358. $package = $operation->getPackage();
  359. $repo->removePackage($package);
  360. }
  361. /**
  362. * Returns the installation path of a package
  363. *
  364. * @param PackageInterface $package
  365. * @return string path
  366. */
  367. public function getInstallPath(PackageInterface $package)
  368. {
  369. $installer = $this->getInstaller($package->getType());
  370. return $installer->getInstallPath($package);
  371. }
  372. public function notifyInstalls(IOInterface $io)
  373. {
  374. foreach ($this->notifiablePackages as $repoUrl => $packages) {
  375. $repositoryName = parse_url($repoUrl, PHP_URL_HOST);
  376. if ($io->hasAuthentication($repositoryName)) {
  377. $auth = $io->getAuthentication($repositoryName);
  378. $authStr = base64_encode($auth['username'] . ':' . $auth['password']);
  379. $authHeader = 'Authorization: Basic '.$authStr;
  380. }
  381. // non-batch API, deprecated
  382. if (strpos($repoUrl, '%package%')) {
  383. foreach ($packages as $package) {
  384. $url = str_replace('%package%', $package->getPrettyName(), $repoUrl);
  385. $params = array(
  386. 'version' => $package->getPrettyVersion(),
  387. 'version_normalized' => $package->getVersion(),
  388. );
  389. $opts = array('http' =>
  390. array(
  391. 'method' => 'POST',
  392. 'header' => array('Content-type: application/x-www-form-urlencoded'),
  393. 'content' => http_build_query($params, '', '&'),
  394. 'timeout' => 3,
  395. ),
  396. );
  397. if (isset($authHeader)) {
  398. $opts['http']['header'][] = $authHeader;
  399. }
  400. $context = StreamContextFactory::getContext($url, $opts);
  401. @file_get_contents($url, false, $context);
  402. }
  403. continue;
  404. }
  405. $postData = array('downloads' => array());
  406. foreach ($packages as $package) {
  407. $postData['downloads'][] = array(
  408. 'name' => $package->getPrettyName(),
  409. 'version' => $package->getVersion(),
  410. );
  411. }
  412. $opts = array('http' =>
  413. array(
  414. 'method' => 'POST',
  415. 'header' => array('Content-Type: application/json'),
  416. 'content' => json_encode($postData),
  417. 'timeout' => 6,
  418. ),
  419. );
  420. if (isset($authHeader)) {
  421. $opts['http']['header'][] = $authHeader;
  422. }
  423. $context = StreamContextFactory::getContext($repoUrl, $opts);
  424. @file_get_contents($repoUrl, false, $context);
  425. }
  426. $this->reset();
  427. }
  428. private function markForNotification(PackageInterface $package)
  429. {
  430. if ($package->getNotificationUrl()) {
  431. $this->notifiablePackages[$package->getNotificationUrl()][$package->getName()] = $package;
  432. }
  433. }
  434. }