LibraryInstaller.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316
  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\Composer;
  13. use Composer\IO\IOInterface;
  14. use Composer\Repository\InstalledRepositoryInterface;
  15. use Composer\Package\PackageInterface;
  16. use Composer\Util\Filesystem;
  17. use Composer\Util\ProcessExecutor;
  18. /**
  19. * Package installation manager.
  20. *
  21. * @author Jordi Boggiano <j.boggiano@seld.be>
  22. * @author Konstantin Kudryashov <ever.zet@gmail.com>
  23. */
  24. class LibraryInstaller implements InstallerInterface
  25. {
  26. protected $composer;
  27. protected $vendorDir;
  28. protected $binDir;
  29. protected $downloadManager;
  30. protected $io;
  31. protected $type;
  32. protected $filesystem;
  33. /**
  34. * Initializes library installer.
  35. *
  36. * @param IOInterface $io
  37. * @param Composer $composer
  38. * @param string $type
  39. * @param Filesystem $filesystem
  40. */
  41. public function __construct(IOInterface $io, Composer $composer, $type = 'library', Filesystem $filesystem = null)
  42. {
  43. $this->composer = $composer;
  44. $this->downloadManager = $composer->getDownloadManager();
  45. $this->io = $io;
  46. $this->type = $type;
  47. $this->filesystem = $filesystem ?: new Filesystem();
  48. $this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/');
  49. $this->binDir = rtrim($composer->getConfig()->get('bin-dir'), '/');
  50. }
  51. /**
  52. * {@inheritDoc}
  53. */
  54. public function supports($packageType)
  55. {
  56. return $packageType === $this->type || null === $this->type;
  57. }
  58. /**
  59. * {@inheritDoc}
  60. */
  61. public function isInstalled(InstalledRepositoryInterface $repo, PackageInterface $package)
  62. {
  63. return $repo->hasPackage($package) && is_readable($this->getInstallPath($package));
  64. }
  65. /**
  66. * {@inheritDoc}
  67. */
  68. public function install(InstalledRepositoryInterface $repo, PackageInterface $package)
  69. {
  70. $this->initializeVendorDir();
  71. $downloadPath = $this->getInstallPath($package);
  72. // remove the binaries if it appears the package files are missing
  73. if (!is_readable($downloadPath) && $repo->hasPackage($package)) {
  74. $this->removeBinaries($package);
  75. }
  76. $this->installCode($package);
  77. $this->installBinaries($package);
  78. if (!$repo->hasPackage($package)) {
  79. $repo->addPackage(clone $package);
  80. }
  81. }
  82. /**
  83. * {@inheritDoc}
  84. */
  85. public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target)
  86. {
  87. if (!$repo->hasPackage($initial)) {
  88. throw new \InvalidArgumentException('Package is not installed: '.$initial);
  89. }
  90. $this->initializeVendorDir();
  91. $this->removeBinaries($initial);
  92. $this->updateCode($initial, $target);
  93. $this->installBinaries($target);
  94. $repo->removePackage($initial);
  95. if (!$repo->hasPackage($target)) {
  96. $repo->addPackage(clone $target);
  97. }
  98. }
  99. /**
  100. * {@inheritDoc}
  101. */
  102. public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package)
  103. {
  104. if (!$repo->hasPackage($package)) {
  105. throw new \InvalidArgumentException('Package is not installed: '.$package);
  106. }
  107. $this->removeCode($package);
  108. $this->removeBinaries($package);
  109. $repo->removePackage($package);
  110. $downloadPath = $this->getPackageBasePath($package);
  111. if (strpos($package->getName(), '/')) {
  112. $packageVendorDir = dirname($downloadPath);
  113. if (is_dir($packageVendorDir) && $this->filesystem->isDirEmpty($packageVendorDir)) {
  114. @rmdir($packageVendorDir);
  115. }
  116. }
  117. }
  118. /**
  119. * {@inheritDoc}
  120. */
  121. public function getInstallPath(PackageInterface $package)
  122. {
  123. $targetDir = $package->getTargetDir();
  124. return $this->getPackageBasePath($package) . ($targetDir ? '/'.$targetDir : '');
  125. }
  126. protected function getPackageBasePath(PackageInterface $package)
  127. {
  128. $this->initializeVendorDir();
  129. return ($this->vendorDir ? $this->vendorDir.'/' : '') . $package->getPrettyName();
  130. }
  131. protected function installCode(PackageInterface $package)
  132. {
  133. $downloadPath = $this->getInstallPath($package);
  134. $this->downloadManager->download($package, $downloadPath);
  135. }
  136. protected function updateCode(PackageInterface $initial, PackageInterface $target)
  137. {
  138. $initialDownloadPath = $this->getInstallPath($initial);
  139. $targetDownloadPath = $this->getInstallPath($target);
  140. if ($targetDownloadPath !== $initialDownloadPath) {
  141. // if the target and initial dirs intersect, we force a remove + install
  142. // to avoid the rename wiping the target dir as part of the initial dir cleanup
  143. if (substr($initialDownloadPath, 0, strlen($targetDownloadPath)) === $targetDownloadPath
  144. || substr($targetDownloadPath, 0, strlen($initialDownloadPath)) === $initialDownloadPath
  145. ) {
  146. $this->removeCode($initial);
  147. $this->installCode($target);
  148. return;
  149. }
  150. $this->filesystem->rename($initialDownloadPath, $targetDownloadPath);
  151. }
  152. $this->downloadManager->update($initial, $target, $targetDownloadPath);
  153. }
  154. protected function removeCode(PackageInterface $package)
  155. {
  156. $downloadPath = $this->getPackageBasePath($package);
  157. $this->downloadManager->remove($package, $downloadPath);
  158. }
  159. protected function getBinaries(PackageInterface $package)
  160. {
  161. return $package->getBinaries();
  162. }
  163. protected function installBinaries(PackageInterface $package)
  164. {
  165. $binaries = $this->getBinaries($package);
  166. if (!$binaries) {
  167. return;
  168. }
  169. foreach ($binaries as $bin) {
  170. $binPath = $this->getInstallPath($package).'/'.$bin;
  171. if (!file_exists($binPath)) {
  172. $this->io->write(' <warning>Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package</warning>');
  173. continue;
  174. }
  175. // in case a custom installer returned a relative path for the
  176. // $package, we can now safely turn it into a absolute path (as we
  177. // already checked the binary's existence). The following helpers
  178. // will require absolute paths to work properly.
  179. $binPath = realpath($binPath);
  180. $this->initializeBinDir();
  181. $link = $this->binDir.'/'.basename($bin);
  182. if (file_exists($link)) {
  183. if (is_link($link)) {
  184. // likely leftover from a previous install, make sure
  185. // that the target is still executable in case this
  186. // is a fresh install of the vendor.
  187. @chmod($link, 0777 & ~umask());
  188. }
  189. $this->io->write(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file');
  190. continue;
  191. }
  192. if (defined('PHP_WINDOWS_VERSION_BUILD')) {
  193. // add unixy support for cygwin and similar environments
  194. if ('.bat' !== substr($binPath, -4)) {
  195. file_put_contents($link, $this->generateUnixyProxyCode($binPath, $link));
  196. @chmod($link, 0777 & ~umask());
  197. $link .= '.bat';
  198. if (file_exists($link)) {
  199. $this->io->write(' Skipped installation of bin '.$bin.'.bat proxy for package '.$package->getName().': a .bat proxy was already installed');
  200. }
  201. }
  202. if (!file_exists($link)) {
  203. file_put_contents($link, $this->generateWindowsProxyCode($binPath, $link));
  204. }
  205. } else {
  206. $cwd = getcwd();
  207. try {
  208. // under linux symlinks are not always supported for example
  209. // when using it in smbfs mounted folder
  210. $relativeBin = $this->filesystem->findShortestPath($link, $binPath);
  211. chdir(dirname($link));
  212. if (false === symlink($relativeBin, $link)) {
  213. throw new \ErrorException();
  214. }
  215. } catch (\ErrorException $e) {
  216. file_put_contents($link, $this->generateUnixyProxyCode($binPath, $link));
  217. }
  218. chdir($cwd);
  219. }
  220. @chmod($link, 0777 & ~umask());
  221. }
  222. }
  223. protected function removeBinaries(PackageInterface $package)
  224. {
  225. $binaries = $this->getBinaries($package);
  226. if (!$binaries) {
  227. return;
  228. }
  229. foreach ($binaries as $bin) {
  230. $link = $this->binDir.'/'.basename($bin);
  231. if (is_link($link) || file_exists($link)) {
  232. $this->filesystem->unlink($link);
  233. }
  234. if (file_exists($link.'.bat')) {
  235. $this->filesystem->unlink($link.'.bat');
  236. }
  237. }
  238. }
  239. protected function initializeVendorDir()
  240. {
  241. $this->filesystem->ensureDirectoryExists($this->vendorDir);
  242. $this->vendorDir = realpath($this->vendorDir);
  243. }
  244. protected function initializeBinDir()
  245. {
  246. $this->filesystem->ensureDirectoryExists($this->binDir);
  247. $this->binDir = realpath($this->binDir);
  248. }
  249. protected function generateWindowsProxyCode($bin, $link)
  250. {
  251. $binPath = $this->filesystem->findShortestPath($link, $bin);
  252. if ('.bat' === substr($bin, -4) || '.exe' === substr($bin, -4)) {
  253. $caller = 'call';
  254. } else {
  255. $handle = fopen($bin, 'r');
  256. $line = fgets($handle);
  257. fclose($handle);
  258. if (preg_match('{^#!/(?:usr/bin/env )?(?:[^/]+/)*(.+)$}m', $line, $match)) {
  259. $caller = trim($match[1]);
  260. } else {
  261. $caller = 'php';
  262. }
  263. }
  264. return "@ECHO OFF\r\n".
  265. "SET BIN_TARGET=%~dp0/".trim(ProcessExecutor::escape($binPath), '"')."\r\n".
  266. "{$caller} \"%BIN_TARGET%\" %*\r\n";
  267. }
  268. protected function generateUnixyProxyCode($bin, $link)
  269. {
  270. $binPath = $this->filesystem->findShortestPath($link, $bin);
  271. return "#!/usr/bin/env sh\n".
  272. 'SRC_DIR="`pwd`"'."\n".
  273. 'cd "`dirname "$0"`"'."\n".
  274. 'cd '.ProcessExecutor::escape(dirname($binPath))."\n".
  275. 'BIN_TARGET="`pwd`/'.basename($binPath)."\"\n".
  276. 'cd "$SRC_DIR"'."\n".
  277. '"$BIN_TARGET" "$@"'."\n";
  278. }
  279. }