1
0
Fork 0

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.
pull/5100/head
Helmut Hummel 2016-03-23 21:36:38 +01:00
parent a8e9df55dc
commit b1ec99faed
3 changed files with 222 additions and 177 deletions

View File

@ -0,0 +1,205 @@
<?php
/*
* This file is part of Composer.
*
* (c) Nils Adermann <naderman@naderman.de>
* Jordi Boggiano <j.boggiano@seld.be>
*
* 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 <j.boggiano@seld.be>
* @author Konstantin Kudryashov <ever.zet@gmail.com>
* @author Helmut Hummel <info@helhum.io>
*/
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(' <warning>Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package</warning>');
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 = <<<PROXY
#!/usr/bin/env sh
dir=$(d=\${0%[/\\\\]*}; cd "\$d"; cd $binDir && pwd)
# See if we are running in Cygwin by checking for cygpath program
if command -v 'cygpath' >/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;
}
}

View File

@ -17,8 +17,6 @@ use Composer\IO\IOInterface;
use Composer\Repository\InstalledRepositoryInterface; use Composer\Repository\InstalledRepositoryInterface;
use Composer\Package\PackageInterface; use Composer\Package\PackageInterface;
use Composer\Util\Filesystem; use Composer\Util\Filesystem;
use Composer\Util\Platform;
use Composer\Util\ProcessExecutor;
use Composer\Util\Silencer; use Composer\Util\Silencer;
/** /**
@ -37,16 +35,18 @@ class LibraryInstaller implements InstallerInterface
protected $type; protected $type;
protected $filesystem; protected $filesystem;
protected $binCompat; protected $binCompat;
protected $libraryBinariesHandler;
/** /**
* Initializes library installer. * Initializes library installer.
* *
* @param IOInterface $io * @param IOInterface $io
* @param Composer $composer * @param Composer $composer
* @param string $type * @param string $type
* @param Filesystem $filesystem * @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->composer = $composer;
$this->downloadManager = $composer->getDownloadManager(); $this->downloadManager = $composer->getDownloadManager();
@ -55,8 +55,7 @@ class LibraryInstaller implements InstallerInterface
$this->filesystem = $filesystem ?: new Filesystem(); $this->filesystem = $filesystem ?: new Filesystem();
$this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/'); $this->vendorDir = rtrim($composer->getConfig()->get('vendor-dir'), '/');
$this->binDir = rtrim($composer->getConfig()->get('bin-dir'), '/'); $this->libraryBinariesHandler = $libraryBinariesHandler ?: new LibraryBinariesHandler(rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $this->io, $this->filesystem);
$this->binCompat = $composer->getConfig()->get('bin-compat');
} }
/** /**
@ -85,11 +84,11 @@ class LibraryInstaller implements InstallerInterface
// remove the binaries if it appears the package files are missing // remove the binaries if it appears the package files are missing
if (!is_readable($downloadPath) && $repo->hasPackage($package)) { if (!is_readable($downloadPath) && $repo->hasPackage($package)) {
$this->removeBinaries($package); $this->libraryBinariesHandler->removeBinaries($package);
} }
$this->installCode($package); $this->installCode($package);
$this->installBinaries($package); $this->libraryBinariesHandler->installBinaries($package, $this->getInstallPath($package));
if (!$repo->hasPackage($package)) { if (!$repo->hasPackage($package)) {
$repo->addPackage(clone $package); $repo->addPackage(clone $package);
} }
@ -106,9 +105,9 @@ class LibraryInstaller implements InstallerInterface
$this->initializeVendorDir(); $this->initializeVendorDir();
$this->removeBinaries($initial); $this->libraryBinariesHandler->removeBinaries($initial);
$this->updateCode($initial, $target); $this->updateCode($initial, $target);
$this->installBinaries($target); $this->libraryBinariesHandler->installBinaries($target, $this->getInstallPath($target));
$repo->removePackage($initial); $repo->removePackage($initial);
if (!$repo->hasPackage($target)) { if (!$repo->hasPackage($target)) {
$repo->addPackage(clone $target); $repo->addPackage(clone $target);
@ -125,7 +124,7 @@ class LibraryInstaller implements InstallerInterface
} }
$this->removeCode($package); $this->removeCode($package);
$this->removeBinaries($package); $this->libraryBinariesHandler->removeBinaries($package);
$repo->removePackage($package); $repo->removePackage($package);
$downloadPath = $this->getPackageBasePath($package); $downloadPath = $this->getPackageBasePath($package);
@ -204,168 +203,9 @@ class LibraryInstaller implements InstallerInterface
$this->downloadManager->remove($package, $downloadPath); $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(' <warning>Skipped installation of bin '.$bin.' for package '.$package->getName().': file not found in package</warning>');
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() protected function initializeVendorDir()
{ {
$this->filesystem->ensureDirectoryExists($this->vendorDir); $this->filesystem->ensureDirectoryExists($this->vendorDir);
$this->vendorDir = realpath($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 = <<<PROXY
#!/usr/bin/env sh
dir=$(d=\${0%[/\\\\]*}; cd "\$d"; cd $binDir && pwd)
# See if we are running in Cygwin by checking for cygpath program
if command -v 'cygpath' >/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;
}
} }

View File

@ -143,22 +143,22 @@ class LibraryInstallerTest extends TestCase
$target = $this->createPackageMock(); $target = $this->createPackageMock();
$initial $initial
->expects($this->once()) ->expects($this->any())
->method('getPrettyName') ->method('getPrettyName')
->will($this->returnValue('package1')); ->will($this->returnValue('package1'));
$initial $initial
->expects($this->once()) ->expects($this->any())
->method('getTargetDir') ->method('getTargetDir')
->will($this->returnValue('oldtarget')); ->will($this->returnValue('oldtarget'));
$target $target
->expects($this->once()) ->expects($this->any())
->method('getPrettyName') ->method('getPrettyName')
->will($this->returnValue('package1')); ->will($this->returnValue('package1'));
$target $target
->expects($this->once()) ->expects($this->any())
->method('getTargetDir') ->method('getTargetDir')
->will($this->returnValue('newtarget')); ->will($this->returnValue('newtarget'));