diff --git a/src/Composer/Downloader/PearDownloader.php b/src/Composer/Downloader/PearDownloader.php index a622b4754..6f250abd3 100644 --- a/src/Composer/Downloader/PearDownloader.php +++ b/src/Composer/Downloader/PearDownloader.php @@ -12,25 +12,41 @@ namespace Composer\Downloader; +use Composer\Package\PackageInterface; + /** * Downloader for pear packages * * @author Jordi Boggiano * @author Kirill chEbba Chebunin */ -class PearDownloader extends TarDownloader +class PearDownloader extends FileDownloader { /** * {@inheritDoc} */ - protected function extract($file, $path) + public function download(PackageInterface $package, $path) { - parent::extract($file, $path); - if (file_exists($path . '/package.sig')) { - unlink($path . '/package.sig'); + parent::download($package, $path); + + $fileName = $this->getFileName($package, $path); + if ($this->io->isVerbose()) { + $this->io->write(' Installing PEAR package'); } - if (file_exists($path . '/package.xml')) { - unlink($path . '/package.xml'); + try { + $pearExtractor = new PearPackageExtractor($fileName); + $pearExtractor->extractTo($path); + + if ($this->io->isVerbose()) { + $this->io->write(' Cleaning up'); + } + unlink($fileName); + } catch (\Exception $e) { + // clean up + $this->filesystem->removeDirectory($path); + throw $e; } + + $this->io->write(''); } } diff --git a/src/Composer/Downloader/PearPackageExtractor.php b/src/Composer/Downloader/PearPackageExtractor.php new file mode 100644 index 000000000..3a83a91dd --- /dev/null +++ b/src/Composer/Downloader/PearPackageExtractor.php @@ -0,0 +1,192 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Downloader; + +use Composer\Util\Filesystem; + +/** + * Extractor for pear packages. + * + * Composer cannot rely on tar files structure when place it inside package target dir. Correct source files + * disposition must be read from package.xml + * This extract pear package source files to target dir. + * + * @author Alexey Prilipko + */ +class PearPackageExtractor +{ + /** @var Filesystem */ + private $filesystem; + private $file; + + public function __construct($file) + { + if (!is_file($file)) { + throw new \UnexpectedValueException('PEAR package file is not found at '.$file); + } + + $this->file = $file; + } + + /** + * Installs PEAR source files according to package.xml definitions and removes extracted files + * + * @param $file string path to downloaded PEAR archive file + * @param $target string target install location. all source installation would be performed relative to target path. + * @param $role string type of files to install. default role for PEAR source files are 'php'. + * + * @throws \RuntimeException + */ + public function extractTo($target, $role = 'php') + { + $this->filesystem = new Filesystem(); + + $extractionPath = $target.'/tarball'; + + try { + $archive = new \PharData($this->file); + $archive->extractTo($extractionPath, null, true); + + if (!is_file($this->combine($extractionPath, '/package.xml'))) { + throw new \RuntimeException('Invalid PEAR package. It must contain package.xml file.'); + } + + $fileCopyActions = $this->buildCopyActions($extractionPath, $role); + $this->copyFiles($fileCopyActions, $extractionPath, $target); + $this->filesystem->removeDirectory($extractionPath); + } catch (\Exception $exception) { + throw new \UnexpectedValueException(sprintf('Failed to extract PEAR package %s to %s. Reason: %s', $this->file, $target, $exception->getMessage()), 0, $exception); + } + } + + /** + * Perform copy actions on files + * + * @param $files array array('from', 'to') with relative paths + * @param $source string path to source dir. + * @param $target string path to destination dir + */ + private function copyFiles($files, $source, $target) + { + foreach ($files as $file) { + $from = $this->combine($source, $file['from']); + $to = $this->combine($target, $file['to']); + $this->copyFile($from, $to); + } + } + + private function copyFile($from, $to) + { + if (!is_file($from)) { + throw new \RuntimeException('Invalid PEAR package. package.xml defines file that is not located inside tarball.'); + } + + $this->filesystem->ensureDirectoryExists(dirname($to)); + + if (!copy($from, $to)) { + throw new \RuntimeException(sprintf('Failed to copy %s to %s', $from, $to)); + } + } + + /** + * Builds list of copy and list of remove actions that would transform extracted PEAR tarball into installed package. + * + * @param $source string path to extracted files. + * @param $role string package file types to extract. + * @return array array of 'source' => 'target', where source is location of file in the tarball (relative to source + * path, and target is destination of file (also relative to $source path) + * @throws \RuntimeException + */ + private function buildCopyActions($source, $role) + { + /** @var $package \SimpleXmlElement */ + $package = simplexml_load_file($this->combine($source, 'package.xml')); + if(false === $package) + throw new \RuntimeException('Package definition file is not valid.'); + + $packageSchemaVersion = $package['version']; + if ('1.0' == $packageSchemaVersion) { + $children = $package->release->filelist->children(); + $packageName = (string) $package->name; + $packageVersion = (string) $package->release->version; + $sourceDir = $packageName . '-' . $packageVersion; + $result = $this->buildSourceList10($children, $role, $sourceDir); + } elseif ('2.0' == $packageSchemaVersion || '2.1' == $packageSchemaVersion) { + $children = $package->contents->children(); + $packageName = (string) $package->name; + $packageVersion = (string) $package->version->release; + $sourceDir = $packageName . '-' . $packageVersion; + $result = $this->buildSourceList20($children, $role, $sourceDir); + } else { + throw new \RuntimeException('Unsupported schema version of package definition file.'); + } + + return $result; + } + + private function buildSourceList10($children, $targetRole, $source = '', $target = '', $role = null) + { + $result = array(); + + // enumerating files + foreach ($children as $child) { + /** @var $child \SimpleXMLElement */ + if ($child->getName() == 'dir') { + $dirSource = $this->combine($source, (string) $child['name']); + $dirTarget = $child['baseinstalldir'] ? : $target; + $dirRole = $child['role'] ? : $role; + $dirFiles = $this->buildSourceList10($child->children(), $targetRole, $dirSource, $dirTarget, $dirRole); + $result = array_merge($result, $dirFiles); + } elseif ($child->getName() == 'file') { + if (($child['role'] ? : $role) == $targetRole) { + $fileName = (string) ($child['name'] ? : $child[0]); // $child[0] means text content + $fileSource = $this->combine($source, $fileName); + $fileTarget = $this->combine((string) $child['baseinstalldir'] ? : $target, $fileName); + $result[] = array('from' => $fileSource, 'to' => $fileTarget); + } + } + } + + return $result; + } + + private function buildSourceList20($children, $targetRole, $source = '', $target = '', $role = null) + { + $result = array(); + + // enumerating files + foreach ($children as $child) { + /** @var $child \SimpleXMLElement */ + if ($child->getName() == 'dir') { + $dirSource = $this->combine($source, $child['name']); + $dirTarget = $child['baseinstalldir'] ? : $target; + $dirRole = $child['role'] ? : $role; + $dirFiles = $this->buildSourceList20($child->children(), $targetRole, $dirSource, $dirTarget, $dirRole); + $result = array_merge($result, $dirFiles); + } elseif ($child->getName() == 'file') { + if (($child['role'] ? : $role) == $targetRole) { + $fileSource = $this->combine($source, (string) $child['name']); + $fileTarget = $this->combine((string) ($child['baseinstalldir'] ? : $target), (string) $child['name']); + $result[] = array('from' => $fileSource, 'to' => $fileTarget); + } + } + } + + return $result; + } + + private function combine($left, $right) + { + return rtrim($left, '/') . '/' . ltrim($right, '/'); + } +} diff --git a/src/Composer/Repository/PearRepository.php b/src/Composer/Repository/PearRepository.php index b964bf08e..fb4b64736 100644 --- a/src/Composer/Repository/PearRepository.php +++ b/src/Composer/Repository/PearRepository.php @@ -163,6 +163,7 @@ class PearRepository extends ArrayRepository 'autoload' => array( 'classmap' => array(''), ), + 'include-path' => array('/'), ); try { @@ -305,6 +306,7 @@ class PearRepository extends ArrayRepository 'autoload' => array( 'classmap' => array(''), ), + 'include-path' => array('/'), ); $packageKeys = array('l' => 'license', 'd' => 'description'); foreach ($packageKeys as $pear => $composer) { diff --git a/tests/Composer/Test/Downloader/Fixtures/Package_v1.0/package.xml b/tests/Composer/Test/Downloader/Fixtures/Package_v1.0/package.xml new file mode 100644 index 000000000..907863f96 --- /dev/null +++ b/tests/Composer/Test/Downloader/Fixtures/Package_v1.0/package.xml @@ -0,0 +1,21 @@ + + + + + PEAR_Frontend_Gtk + Gtk (Desktop) PEAR Package Manager + + 0.4.0 + 2005-03-14 + PHP License + beta + Implement channels, support PEAR 1.4.0 (Greg Beaver) + Tidy up logging a little. + + + + + + + + diff --git a/tests/Composer/Test/Downloader/Fixtures/Package_v2.0/package.xml b/tests/Composer/Test/Downloader/Fixtures/Package_v2.0/package.xml new file mode 100644 index 000000000..b736f343a --- /dev/null +++ b/tests/Composer/Test/Downloader/Fixtures/Package_v2.0/package.xml @@ -0,0 +1,19 @@ + + + + Net_URL + pear.php.net + Easy parsing of Urls + Provides easy parsing of URLs and their constituent parts. + + 1.0.15 + 1.0.15 + + + + + + + + + diff --git a/tests/Composer/Test/Downloader/Fixtures/Package_v2.1/package.xml b/tests/Composer/Test/Downloader/Fixtures/Package_v2.1/package.xml new file mode 100644 index 000000000..6a982ef60 --- /dev/null +++ b/tests/Composer/Test/Downloader/Fixtures/Package_v2.1/package.xml @@ -0,0 +1,25 @@ + + + + Zend_Authentication + packages.zendframework.com + + Package Zend_Authentication summary.\n\n" . "Package detailed description here (found in README) + + + + 2.0.0beta4 + 2.0.0beta4 + + + beta + beta + + + + + + + + + \ No newline at end of file diff --git a/tests/Composer/Test/Downloader/PearDownloaderTest.php b/tests/Composer/Test/Downloader/PearDownloaderTest.php new file mode 100644 index 000000000..d3cc8d0fd --- /dev/null +++ b/tests/Composer/Test/Downloader/PearDownloaderTest.php @@ -0,0 +1,38 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Downloader; + +use Composer\Downloader\PearDownloader; + +class PearDownloaderTest extends \PHPUnit_Framework_TestCase +{ + public function testErrorMessages() + { + $packageMock = $this->getMock('Composer\Package\PackageInterface'); + $packageMock->expects($this->any()) + ->method('getDistUrl') + ->will($this->returnValue('file://'.__FILE__)) + ; + + $io = $this->getMock('Composer\IO\IOInterface'); + $downloader = new PearDownloader($io); + + try { + $downloader->download($packageMock, sys_get_temp_dir().'/composer-pear-test'); + $this->fail('Download of invalid pear packages should throw an exception'); + } catch (\UnexpectedValueException $e) { + $this->assertContains('Failed to extract PEAR package', $e->getMessage()); + } + } + +} diff --git a/tests/Composer/Test/Downloader/PearPackageExtractorTest.php b/tests/Composer/Test/Downloader/PearPackageExtractorTest.php new file mode 100644 index 000000000..42010a39c --- /dev/null +++ b/tests/Composer/Test/Downloader/PearPackageExtractorTest.php @@ -0,0 +1,81 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Downloader; + +use Composer\Downloader\PearPackageExtractor; + +class PearPackageExtractorTest extends \PHPUnit_Framework_TestCase +{ + public function testShouldExtractPackage_1_0() + { + $extractor = $this->getMockForAbstractClass('Composer\Downloader\PearPackageExtractor', array(), '', false); + $method = new \ReflectionMethod($extractor, 'buildCopyActions'); + $method->setAccessible(true); + + $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v1.0', 'php'); + + $expectedFileActions = array( + 0 => Array( + 'from' => 'PEAR_Frontend_Gtk-0.4.0/Gtk.php', + 'to' => 'PEAR/Frontend/Gtk.php', + ), + 1 => Array( + 'from' => 'PEAR_Frontend_Gtk-0.4.0/Gtk/Config.php', + 'to' => 'PEAR/Frontend/Gtk/Config.php', + ), + 2 => Array( + 'from' => 'PEAR_Frontend_Gtk-0.4.0/Gtk/xpm/black_close_icon.xpm', + 'to' => 'PEAR/Frontend/Gtk/xpm/black_close_icon.xpm', + ) + ); + $this->assertSame($expectedFileActions, $fileActions); + } + + public function testShouldExtractPackage_2_0() + { + $extractor = $this->getMockForAbstractClass('Composer\Downloader\PearPackageExtractor', array(), '', false); + $method = new \ReflectionMethod($extractor, 'buildCopyActions'); + $method->setAccessible(true); + + $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v2.0', 'php'); + + $expectedFileActions = array( + 0 => Array( + 'from' => 'Net_URL-1.0.15/URL.php', + 'to' => 'Net/URL.php', + ) + ); + $this->assertSame($expectedFileActions, $fileActions); + } + + public function testShouldExtractPackage_2_1() + { + $extractor = $this->getMockForAbstractClass('Composer\Downloader\PearPackageExtractor', array(), '', false); + $method = new \ReflectionMethod($extractor, 'buildCopyActions'); + $method->setAccessible(true); + + $fileActions = $method->invoke($extractor, __DIR__ . '/Fixtures/Package_v2.1', 'php'); + + $expectedFileActions = array( + 0 => Array( + 'from' => 'Zend_Authentication-2.0.0beta4/php/Zend/Authentication/Storage/StorageInterface.php', + 'to' => '/php/Zend/Authentication/Storage/StorageInterface.php', + ), + 1 => Array( + 'from' => 'Zend_Authentication-2.0.0beta4/php/Zend/Authentication/Result.php', + 'to' => '/php/Zend/Authentication/Result.php', + ) + ); + $this->assertSame($expectedFileActions, $fileActions); + } +} diff --git a/tests/Composer/Test/Repository/PearRepositoryTest.php b/tests/Composer/Test/Repository/PearRepositoryTest.php index 3172c06f0..7021c8615 100644 --- a/tests/Composer/Test/Repository/PearRepositoryTest.php +++ b/tests/Composer/Test/Repository/PearRepositoryTest.php @@ -29,6 +29,30 @@ class PearRepositoryTest extends TestCase */ private $remoteFilesystem; + public function testComposerNonCompatibleRepositoryShouldSetIncludePath() + { + $url = 'pear.phpmd.org'; + $expectedPackages = array( + array('name' => 'pear-phpmd/PHP_PMD', 'version' => '1.3.3'), + ); + + $repoConfig = array( + 'url' => $url + ); + + $this->createRepository($repoConfig); + + foreach ($expectedPackages as $expectedPackage) { + $package = $this->repository->findPackage($expectedPackage['name'], $expectedPackage['version']); + $this->assertInstanceOf('Composer\Package\PackageInterface', + $package, + 'Expected package ' . $expectedPackage['name'] . ', version ' . $expectedPackage['version'] . + ' not found in pear channel ' . $url + ); + $this->assertSame(array('/'), $package->getIncludePaths()); + } + } + /** * @dataProvider repositoryDataProvider * @param string $url