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'));