diff --git a/src/Composer/Downloader/Util/Filesystem.php b/src/Composer/Downloader/Util/Filesystem.php index 5320299b3..66e423e1d 100644 --- a/src/Composer/Downloader/Util/Filesystem.php +++ b/src/Composer/Downloader/Util/Filesystem.php @@ -41,4 +41,83 @@ class Filesystem } } } + + /** + * Returns the shortest path from $from to $to + * + * @param string $from + * @param string $to + * @return string + */ + public function findShortestPath($from, $to) + { + if (!$this->isAbsolutePath($from) || !$this->isAbsolutePath($to)) { + throw new \InvalidArgumentException('from and to must be absolute paths'); + } + + if (dirname($from) === dirname($to)) { + return './'.basename($to); + } + $from = strtr($from, '\\', '/'); + $to = strtr($to, '\\', '/'); + + $commonPath = dirname($to); + while (strpos($from, $commonPath) !== 0 && '/' !== $commonPath && !preg_match('{^[a-z]:/$}i', $commonPath)) { + $commonPath = strtr(dirname($commonPath), '\\', '/'); + } + + if (0 !== strpos($from, $commonPath) || '/' === $commonPath) { + return $to; + } + + $commonPath = rtrim($commonPath, '/') . '/'; + $sourcePathDepth = substr_count(substr($from, strlen($commonPath)), '/'); + $commonPathCode = str_repeat('../', $sourcePathDepth); + return $commonPathCode . substr($to, strlen($commonPath)); + } + + /** + * Returns PHP code that, when executed in $from, will return the path to $to + * + * @param string $from + * @param string $to + * @return string + */ + public function findShortestPathCode($from, $to) + { + if (!$this->isAbsolutePath($from) || !$this->isAbsolutePath($to)) { + throw new \InvalidArgumentException('from and to must be absolute paths'); + } + + if ($from === $to) { + return '__FILE__'; + } + $from = strtr($from, '\\', '/'); + $to = strtr($to, '\\', '/'); + + $commonPath = dirname($to); + while (strpos($from, $commonPath) !== 0 && '/' !== $commonPath && !preg_match('{^[a-z]:/$}i', $commonPath)) { + $commonPath = strtr(dirname($commonPath), '\\', '/'); + } + + if (0 !== strpos($from, $commonPath) || '/' === $commonPath) { + return var_export($to, true); + } + + $commonPath = rtrim($commonPath, '/') . '/'; + $sourcePathDepth = substr_count(substr($from, strlen($commonPath)), '/'); + $commonPathCode = str_repeat('dirname(', $sourcePathDepth).'__DIR__'.str_repeat(')', $sourcePathDepth); + return $commonPathCode . '.' . var_export('/' . substr($to, strlen($commonPath)), true); + } + + /** + * Checks if the given path is absolute + * + * @param string $path + * @return Boolean + */ + public function isAbsolutePath($path) + { + return substr($path, 0, 1) === '/' || substr($path, 1, 1) === ':'; + } } diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 12302f129..0435d8777 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -31,6 +31,7 @@ class LibraryInstaller implements InstallerInterface protected $downloadManager; protected $repository; private $type; + private $filesystem; /** * Initializes library installer. @@ -47,9 +48,9 @@ class LibraryInstaller implements InstallerInterface $this->repository = $repository; $this->type = $type; - $fs = new Filesystem(); - $fs->ensureDirectoryExists($vendorDir); - $fs->ensureDirectoryExists($binDir); + $this->filesystem = new Filesystem(); + $this->filesystem->ensureDirectoryExists($vendorDir); + $this->filesystem->ensureDirectoryExists($binDir); $this->vendorDir = realpath($vendorDir); $this->binDir = realpath($binDir); } @@ -132,35 +133,20 @@ class LibraryInstaller implements InstallerInterface if (!$package->getBinaries()) { return; } - foreach ($package->getBinaries() as $bin => $os) { + foreach ($package->getBinaries() as $bin) { $link = $this->binDir.'/'.basename($bin); if (file_exists($link)) { continue; } - // skip windows - if (defined('PHP_WINDOWS_VERSION_BUILD') && false === strpos($os, 'windows') && '*' !== $os) { - continue; - } - - // skip unix - if (!defined('PHP_WINDOWS_VERSION_BUILD') && false === strpos($os, 'unix') && '*' !== $os) { - continue; - } - - $binary = $this->getInstallPath($package).'/'.$bin; - $from = array( - '@php_bin@', - '@bin_dir@', - ); - $to = array( - 'php', - $this->binDir, - ); - file_put_contents($binary, str_replace($from, $to, file_get_contents($binary))); - if (defined('PHP_WINDOWS_VERSION_BUILD')) { - copy($binary, $link); + // add unixy support for cygwin and similar environments + if ('.bat' !== substr($bin, -4)) { + file_put_contents($link, $this->generateUnixyProxyCode($this->getInstallPath($package).'/'.$bin)); + chmod($link, 0777); + $link .= '.bat'; + } + file_put_contents($link, $this->generateWindowsProxyCode($this->getInstallPath($package).'/'.$bin)); } else { symlink($this->getInstallPath($package).'/'.$bin, $link); } @@ -181,4 +167,44 @@ class LibraryInstaller implements InstallerInterface unlink($link); } } + + private function generateWindowsProxyCode($bin) + { + $link = $this->binDir.'/'.basename($bin); + $binPath = $this->filesystem->findShortestPath($link, $bin); + if ('.bat' === 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 = $match[1]; + } else { + $caller = 'php'; + } + } + + return "@echo off\r\n". + "pushd .\r\n". + "cd %~dp0\r\n". + "cd ".escapeshellarg(dirname($binPath))."\r\n". + "set BIN_TARGET=%CD%\\".basename($binPath)."\r\n". + "popd\r\n". + $caller." %BIN_TARGET% %*\r\n"; + } + + private function generateUnixyProxyCode($bin) + { + $link = $this->binDir.'/'.basename($bin); + $binPath = $this->filesystem->findShortestPath($link, $bin); + + return "#!/usr/bin/env sh\n". + 'SRC_DIR=`pwd`'."\n". + 'cd `dirname \\`readlink -m "$0"\\``'."\n". + 'cd '.escapeshellarg(dirname($binPath))."\n". + 'BIN_TARGET=`pwd`/'.basename($binPath)."\n". + 'cd $SRC_DIR'."\n". + '$BIN_TARGET "$@"'."\n"; + } } diff --git a/src/Composer/Package/Loader/ArrayLoader.php b/src/Composer/Package/Loader/ArrayLoader.php index bf8f8ad88..2f8b4a478 100644 --- a/src/Composer/Package/Loader/ArrayLoader.php +++ b/src/Composer/Package/Loader/ArrayLoader.php @@ -79,9 +79,8 @@ class ArrayLoader } if (isset($config['bin']) && is_array($config['bin'])) { - foreach ($config['bin'] as $bin => $os) { - unset($config['bin'][$bin]); - $config['bin'][ltrim($bin, '/')] = $os; + foreach ($config['bin'] as $key => $bin) { + $config['bin'][$key]= ltrim($bin, '/'); } $package->setBinaries($config['bin']); } diff --git a/tests/Composer/Test/Downloader/Util/FilesystemTest.php b/tests/Composer/Test/Downloader/Util/FilesystemTest.php new file mode 100644 index 000000000..f4d9af04f --- /dev/null +++ b/tests/Composer/Test/Downloader/Util/FilesystemTest.php @@ -0,0 +1,67 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Repository; + +use Composer\Downloader\Util\Filesystem; +use Composer\Test\TestCase; + +class FilesystemTest extends TestCase +{ + /** + * @dataProvider providePathCouplesAsCode + */ + public function testFindShortestPathCode($a, $b, $expected) + { + $fs = new Filesystem; + $this->assertEquals($expected, $fs->findShortestPathCode($a, $b)); + } + + public function providePathCouplesAsCode() + { + return array( + array('/foo/bar', '/foo/bar', "__FILE__"), + array('/foo/bar', '/foo/baz', "__DIR__.'/baz'"), + array('/foo/bin/run', '/foo/vendor/acme/bin/run', "dirname(__DIR__).'/vendor/acme/bin/run'"), + array('/foo/bin/run', '/foo/vendor/acme/bin/run', "dirname(__DIR__).'/vendor/acme/bin/run'"), + array('/foo/bin/run', '/bar/bin/run', "'/bar/bin/run'"), + array('c:/bin/run', 'c:/vendor/acme/bin/run', "dirname(__DIR__).'/vendor/acme/bin/run'"), + array('c:\\bin\\run', 'c:/vendor/acme/bin/run', "dirname(__DIR__).'/vendor/acme/bin/run'"), + array('c:/bin/run', 'd:/vendor/acme/bin/run', "'d:/vendor/acme/bin/run'"), + array('c:\\bin\\run', 'd:/vendor/acme/bin/run', "'d:/vendor/acme/bin/run'"), + ); + } + + /** + * @dataProvider providePathCouples + */ + public function testFindShortestPath($a, $b, $expected) + { + $fs = new Filesystem; + $this->assertEquals($expected, $fs->findShortestPath($a, $b)); + } + + public function providePathCouples() + { + return array( + array('/foo/bar', '/foo/bar', "./bar"), + array('/foo/bar', '/foo/baz', "./baz"), + array('/foo/bin/run', '/foo/vendor/acme/bin/run', "../vendor/acme/bin/run"), + array('/foo/bin/run', '/foo/vendor/acme/bin/run', "../vendor/acme/bin/run"), + array('/foo/bin/run', '/bar/bin/run', "/bar/bin/run"), + array('c:/bin/run', 'c:/vendor/acme/bin/run', "../vendor/acme/bin/run"), + array('c:\\bin\\run', 'c:/vendor/acme/bin/run', "../vendor/acme/bin/run"), + array('c:/bin/run', 'd:/vendor/acme/bin/run', "d:/vendor/acme/bin/run"), + array('c:\\bin\\run', 'd:/vendor/acme/bin/run', "d:/vendor/acme/bin/run"), + ); + } +}