LibraryInstaller.php 11 KB

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