From b1ec99faed06829951e1c3274f84bb4ac74e779d Mon Sep 17 00:00:00 2001 From: Helmut Hummel Date: Wed, 23 Mar 2016 21:36:38 +0100 Subject: [PATCH] Extract binary installation and removal to own class The functionality to install binaries might be useful for other installers. Create API for that by extracting this functionality from the LibraryInstaller class. --- .../Installer/LibraryBinariesHandler.php | 205 ++++++++++++++++++ src/Composer/Installer/LibraryInstaller.php | 186 ++-------------- .../Test/Installer/LibraryInstallerTest.php | 8 +- 3 files changed, 222 insertions(+), 177 deletions(-) create mode 100644 src/Composer/Installer/LibraryBinariesHandler.php diff --git a/src/Composer/Installer/LibraryBinariesHandler.php b/src/Composer/Installer/LibraryBinariesHandler.php new file mode 100644 index 000000000..294210cbf --- /dev/null +++ b/src/Composer/Installer/LibraryBinariesHandler.php @@ -0,0 +1,205 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Installer; + +use Composer\IO\IOInterface; +use Composer\Package\PackageInterface; +use Composer\Util\Filesystem; +use Composer\Util\Platform; +use Composer\Util\ProcessExecutor; +use Composer\Util\Silencer; + +/** + * Package installation manager. + * + * @author Jordi Boggiano + * @author Konstantin Kudryashov + * @author Helmut Hummel + */ +class LibraryBinariesHandler +{ + protected $binDir; + protected $binCompat; + protected $io; + protected $filesystem; + + /** + * LibraryBinaryHandler constructor. + * + * @param string $binDir + * @param string $binCompat + * @param IOInterface $io + * @param Filesystem $filesystem + */ + public function __construct($binDir, $binCompat, IOInterface $io, Filesystem $filesystem = null) + { + $this->binDir = $binDir; + $this->binCompat = $binCompat; + $this->io = $io; + $this->filesystem = $filesystem ?: new Filesystem(); + } + + public function installBinaries(PackageInterface $package, $installPath) + { + $binaries = $package->getBinaries(); + if (!$binaries) { + return; + } + foreach ($binaries as $bin) { + $binPath = $installPath.'/'.$bin; + if (!file_exists($binPath)) { + $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package'); + continue; + } + + // in case a custom installer returned a relative path for the + // $package, we can now safely turn it into a absolute path (as we + // already checked the binary's existence). The following helpers + // will require absolute paths to work properly. + $binPath = realpath($binPath); + + $this->initializeBinDir(); + $link = $this->binDir.'/'.basename($bin); + if (file_exists($link)) { + if (is_link($link)) { + // likely leftover from a previous install, make sure + // that the target is still executable in case this + // is a fresh install of the vendor. + Silencer::call('chmod', $link, 0777 & ~umask()); + } + $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file'); + continue; + } + + if ($this->binCompat === "auto") { + if (Platform::isWindows()) { + $this->installFullBinaries($binPath, $link, $bin, $package); + } else { + $this->installSymlinkBinaries($binPath, $link); + } + } elseif ($this->binCompat === "full") { + $this->installFullBinaries($binPath, $link, $bin, $package); + } + Silencer::call('chmod', $link, 0777 & ~umask()); + } + } + + protected function installFullBinaries($binPath, $link, $bin, PackageInterface $package) + { + // add unixy support for cygwin and similar environments + if ('.bat' !== substr($binPath, -4)) { + $this->installUnixyProxyBinaries($binPath, $link); + @chmod($link, 0777 & ~umask()); + $link .= '.bat'; + if (file_exists($link)) { + $this->io->writeError(' Skipped installation of bin '.$bin.'.bat proxy for package '.$package->getName().': a .bat proxy was already installed'); + } + } + if (!file_exists($link)) { + file_put_contents($link, $this->generateWindowsProxyCode($binPath, $link)); + } + } + + protected function installSymlinkBinaries($binPath, $link) + { + if (!$this->filesystem->relativeSymlink($binPath, $link)) { + $this->installUnixyProxyBinaries($binPath, $link); + } + } + + protected function installUnixyProxyBinaries($binPath, $link) + { + file_put_contents($link, $this->generateUnixyProxyCode($binPath, $link)); + } + + public function removeBinaries(PackageInterface $package) + { + $binaries = $package->getBinaries(); + if (!$binaries) { + return; + } + foreach ($binaries as $bin) { + $link = $this->binDir.'/'.basename($bin); + if (is_link($link) || file_exists($link)) { + $this->filesystem->unlink($link); + } + if (file_exists($link.'.bat')) { + $this->filesystem->unlink($link.'.bat'); + } + } + + // attempt removing the bin dir in case it is left empty + if ((is_dir($this->binDir)) && ($this->filesystem->isDirEmpty($this->binDir))) { + Silencer::call('rmdir', $this->binDir); + } + } + + protected function initializeBinDir() + { + $this->filesystem->ensureDirectoryExists($this->binDir); + $this->binDir = realpath($this->binDir); + } + + protected function generateWindowsProxyCode($bin, $link) + { + $binPath = $this->filesystem->findShortestPath($link, $bin); + if ('.bat' === substr($bin, -4) || '.exe' === substr($bin, -4)) { + $caller = 'call'; + } else { + $handle = fopen($bin, 'r'); + $line = fgets($handle); + fclose($handle); + if (preg_match('{^#!/(?:usr/bin/env )?(?:[^/]+/)*(.+)$}m', $line, $match)) { + $caller = trim($match[1]); + } else { + $caller = 'php'; + } + } + + return "@ECHO OFF\r\n". + "setlocal DISABLEDELAYEDEXPANSION\r\n". + "SET BIN_TARGET=%~dp0/".trim(ProcessExecutor::escape($binPath), '"')."\r\n". + "{$caller} \"%BIN_TARGET%\" %*\r\n"; + } + + protected function generateUnixyProxyCode($bin, $link) + { + $binPath = $this->filesystem->findShortestPath($link, $bin); + + $binDir = ProcessExecutor::escape(dirname($binPath)); + $binFile = basename($binPath); + + $proxyCode = <</dev/null 2>&1; then + # Cygwin paths start with /cygdrive/ which will break windows PHP, + # so we need to translate the dir path to windows format. However + # we could be using cygwin PHP which does not require this, so we + # test if the path to PHP starts with /cygdrive/ rather than /usr/bin + if [[ $(which php) == /cygdrive/* ]]; then + dir=$(cygpath -m "\$dir"); + fi +fi + +dir=$(echo \$dir | sed 's/ /\ /g') +"\${dir}/$binFile" "$@" + +PROXY; + + return $proxyCode; + } +} diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index b9c76770c..a89af316c 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -17,8 +17,6 @@ use Composer\IO\IOInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\Util\Filesystem; -use Composer\Util\Platform; -use Composer\Util\ProcessExecutor; use Composer\Util\Silencer; /** @@ -37,16 +35,18 @@ class LibraryInstaller implements InstallerInterface protected $type; protected $filesystem; protected $binCompat; + protected $libraryBinariesHandler; /** * Initializes library installer. * - * @param IOInterface $io - * @param Composer $composer - * @param string $type - * @param Filesystem $filesystem + * @param IOInterface $io + * @param Composer $composer + * @param string $type + * @param Filesystem $filesystem + * @param LibraryBinariesHandler $libraryBinariesHandler */ - public function __construct(IOInterface $io, Composer $composer, $type = 'library', Filesystem $filesystem = null) + public function __construct(IOInterface $io, Composer $composer, $type = 'library', Filesystem $filesystem = null, LibraryBinariesHandler $libraryBinariesHandler = null) { $this->composer = $composer; $this->downloadManager = $composer->getDownloadManager(); @@ -55,8 +55,7 @@ class LibraryInstaller implements InstallerInterface $this->filesystem = $filesystem ?: new Filesystem(); $this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/'); - $this->binDir = rtrim($composer->getConfig()->get('bin-dir'), '/'); - $this->binCompat = $composer->getConfig()->get('bin-compat'); + $this->libraryBinariesHandler = $libraryBinariesHandler ?: new LibraryBinariesHandler(rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $this->io, $this->filesystem); } /** @@ -85,11 +84,11 @@ class LibraryInstaller implements InstallerInterface // remove the binaries if it appears the package files are missing if (!is_readable($downloadPath) && $repo->hasPackage($package)) { - $this->removeBinaries($package); + $this->libraryBinariesHandler->removeBinaries($package); } $this->installCode($package); - $this->installBinaries($package); + $this->libraryBinariesHandler->installBinaries($package, $this->getInstallPath($package)); if (!$repo->hasPackage($package)) { $repo->addPackage(clone $package); } @@ -106,9 +105,9 @@ class LibraryInstaller implements InstallerInterface $this->initializeVendorDir(); - $this->removeBinaries($initial); + $this->libraryBinariesHandler->removeBinaries($initial); $this->updateCode($initial, $target); - $this->installBinaries($target); + $this->libraryBinariesHandler->installBinaries($target, $this->getInstallPath($target)); $repo->removePackage($initial); if (!$repo->hasPackage($target)) { $repo->addPackage(clone $target); @@ -125,7 +124,7 @@ class LibraryInstaller implements InstallerInterface } $this->removeCode($package); - $this->removeBinaries($package); + $this->libraryBinariesHandler->removeBinaries($package); $repo->removePackage($package); $downloadPath = $this->getPackageBasePath($package); @@ -204,168 +203,9 @@ class LibraryInstaller implements InstallerInterface $this->downloadManager->remove($package, $downloadPath); } - protected function getBinaries(PackageInterface $package) - { - return $package->getBinaries(); - } - - protected function installBinaries(PackageInterface $package) - { - $binaries = $this->getBinaries($package); - if (!$binaries) { - return; - } - foreach ($binaries as $bin) { - $binPath = $this->getInstallPath($package).'/'.$bin; - if (!file_exists($binPath)) { - $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package'); - continue; - } - - // in case a custom installer returned a relative path for the - // $package, we can now safely turn it into a absolute path (as we - // already checked the binary's existence). The following helpers - // will require absolute paths to work properly. - $binPath = realpath($binPath); - - $this->initializeBinDir(); - $link = $this->binDir.'/'.basename($bin); - if (file_exists($link)) { - if (is_link($link)) { - // likely leftover from a previous install, make sure - // that the target is still executable in case this - // is a fresh install of the vendor. - Silencer::call('chmod', $link, 0777 & ~umask()); - } - $this->io->writeError(' Skipped installation of bin '.$bin.' for package '.$package->getName().': name conflicts with an existing file'); - continue; - } - - if ($this->binCompat === "auto") { - if (Platform::isWindows()) { - $this->installFullBinaries($binPath, $link, $bin, $package); - } else { - $this->installSymlinkBinaries($binPath, $link); - } - } elseif ($this->binCompat === "full") { - $this->installFullBinaries($binPath, $link, $bin, $package); - } - Silencer::call('chmod', $link, 0777 & ~umask()); - } - } - - protected function installFullBinaries($binPath, $link, $bin, PackageInterface $package) - { - // add unixy support for cygwin and similar environments - if ('.bat' !== substr($binPath, -4)) { - $this->installUnixyProxyBinaries($binPath, $link); - @chmod($link, 0777 & ~umask()); - $link .= '.bat'; - if (file_exists($link)) { - $this->io->writeError(' Skipped installation of bin '.$bin.'.bat proxy for package '.$package->getName().': a .bat proxy was already installed'); - } - } - if (!file_exists($link)) { - file_put_contents($link, $this->generateWindowsProxyCode($binPath, $link)); - } - } - - protected function installSymlinkBinaries($binPath, $link) - { - if (!$this->filesystem->relativeSymlink($binPath, $link)) { - $this->installUnixyProxyBinaries($binPath, $link); - } - } - - protected function installUnixyProxyBinaries($binPath, $link) - { - file_put_contents($link, $this->generateUnixyProxyCode($binPath, $link)); - } - - protected function removeBinaries(PackageInterface $package) - { - $binaries = $this->getBinaries($package); - if (!$binaries) { - return; - } - foreach ($binaries as $bin) { - $link = $this->binDir.'/'.basename($bin); - if (is_link($link) || file_exists($link)) { - $this->filesystem->unlink($link); - } - if (file_exists($link.'.bat')) { - $this->filesystem->unlink($link.'.bat'); - } - } - - // attempt removing the bin dir in case it is left empty - if ((is_dir($this->binDir)) && ($this->filesystem->isDirEmpty($this->binDir))) { - Silencer::call('rmdir', $this->binDir); - } - } - protected function initializeVendorDir() { $this->filesystem->ensureDirectoryExists($this->vendorDir); $this->vendorDir = realpath($this->vendorDir); } - - protected function initializeBinDir() - { - $this->filesystem->ensureDirectoryExists($this->binDir); - $this->binDir = realpath($this->binDir); - } - - protected function generateWindowsProxyCode($bin, $link) - { - $binPath = $this->filesystem->findShortestPath($link, $bin); - if ('.bat' === substr($bin, -4) || '.exe' === substr($bin, -4)) { - $caller = 'call'; - } else { - $handle = fopen($bin, 'r'); - $line = fgets($handle); - fclose($handle); - if (preg_match('{^#!/(?:usr/bin/env )?(?:[^/]+/)*(.+)$}m', $line, $match)) { - $caller = trim($match[1]); - } else { - $caller = 'php'; - } - } - - return "@ECHO OFF\r\n". - "setlocal DISABLEDELAYEDEXPANSION\r\n". - "SET BIN_TARGET=%~dp0/".trim(ProcessExecutor::escape($binPath), '"')."\r\n". - "{$caller} \"%BIN_TARGET%\" %*\r\n"; - } - - protected function generateUnixyProxyCode($bin, $link) - { - $binPath = $this->filesystem->findShortestPath($link, $bin); - - $binDir = ProcessExecutor::escape(dirname($binPath)); - $binFile = basename($binPath); - - $proxyCode = <</dev/null 2>&1; then - # Cygwin paths start with /cygdrive/ which will break windows PHP, - # so we need to translate the dir path to windows format. However - # we could be using cygwin PHP which does not require this, so we - # test if the path to PHP starts with /cygdrive/ rather than /usr/bin - if [[ $(which php) == /cygdrive/* ]]; then - dir=$(cygpath -m "\$dir"); - fi -fi - -dir=$(echo \$dir | sed 's/ /\ /g') -"\${dir}/$binFile" "$@" - -PROXY; - - return $proxyCode; - } } diff --git a/tests/Composer/Test/Installer/LibraryInstallerTest.php b/tests/Composer/Test/Installer/LibraryInstallerTest.php index 72eeb04b1..257232b0c 100644 --- a/tests/Composer/Test/Installer/LibraryInstallerTest.php +++ b/tests/Composer/Test/Installer/LibraryInstallerTest.php @@ -143,22 +143,22 @@ class LibraryInstallerTest extends TestCase $target = $this->createPackageMock(); $initial - ->expects($this->once()) + ->expects($this->any()) ->method('getPrettyName') ->will($this->returnValue('package1')); $initial - ->expects($this->once()) + ->expects($this->any()) ->method('getTargetDir') ->will($this->returnValue('oldtarget')); $target - ->expects($this->once()) + ->expects($this->any()) ->method('getPrettyName') ->will($this->returnValue('package1')); $target - ->expects($this->once()) + ->expects($this->any()) ->method('getTargetDir') ->will($this->returnValue('newtarget'));