diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 30c035bd4..f09ed0fe2 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -89,7 +89,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - extensions: "intl" + extensions: "intl, zip" ini-values: "memory_limit=-1, phar.readonly=0" php-version: "${{ matrix.php-version }}" diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 967f79dcd..d2894e09b 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -31,7 +31,7 @@ jobs: uses: "shivammathur/setup-php@v2" with: coverage: "none" - extensions: "intl" + extensions: "intl, zip" ini-values: "memory_limit=-1" php-version: "${{ matrix.php-version }}" tools: "cs2pr" @@ -52,5 +52,5 @@ jobs: - name: Run PHPStan run: | - bin/composer require --dev phpstan/phpstan:^0.12 phpunit/phpunit:^7.5 --with-all-dependencies - vendor/bin/phpstan analyse --configuration=phpstan/config.neon --error-format=checkstyle | cs2pr + bin/composer require --dev phpstan/phpstan:^0.12.26 phpunit/phpunit:^7.5 --with-all-dependencies + vendor/bin/phpstan analyse --configuration=phpstan/config.neon || vendor/bin/phpstan analyse --configuration=phpstan/config.neon --error-format=checkstyle | cs2pr diff --git a/UPGRADE-2.0.md b/UPGRADE-2.0.md index 33729371a..2b9ccf24a 100644 --- a/UPGRADE-2.0.md +++ b/UPGRADE-2.0.md @@ -17,6 +17,7 @@ - `PluginInterface` added a deactivate (so plugin can stop whatever it is doing) and an uninstall (so the plugin can remove any files it created or do general cleanup) method. - Plugins implementing `EventSubscriberInterface` will be deregistered from the EventDispatcher automatically when being deactivated, nothing to do there. - `Pool` objects are now created via the `RepositorySet` class, you should use that in case you were using the `Pool` class directly. +- Custom installers extending from LibraryInstaller should be aware that in Composer 2 it MAY return PromiseInterface instances when calling parent::install/update/uninstall/installCode/removeCode. See [composer/installers](https://github.com/composer/installers/commit/5006d0c28730ade233a8f42ec31ac68fb1c5c9bb) for an example of how to handle this best. - The `Composer\Installer` class changed quite a bit internally, but the inputs are almost the same: - `setAdditionalInstalledRepository` is now `setAdditionalFixedRepository` - `setUpdateWhitelist` is now `setUpdateAllowList` diff --git a/composer.json b/composer.json index 024dc454a..981f96afe 100644 --- a/composer.json +++ b/composer.json @@ -22,7 +22,7 @@ } ], "require": { - "php": "^5.3.2 || ^7.0", + "php": "^5.3.2 || ^7.0 || ^8.0", "composer/ca-bundle": "^1.0", "composer/semver": "^3.0", "composer/spdx-licenses": "^1.2", diff --git a/composer.lock b/composer.lock index a958c8a20..aa50c62df 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "b39e04ca4a44810c96876dae02629707", + "content-hash": "9b94ece2895724239b36aec399e5321a", "packages": [ { "name": "composer/ca-bundle", @@ -268,7 +268,7 @@ "support": { "irc": "irc://irc.freenode.org/composer", "issues": "https://github.com/composer/xdebug-handler/issues", - "source": "https://github.com/composer/xdebug-handler/tree/master" + "source": "https://github.com/composer/xdebug-handler/tree/1.4.2" }, "funding": [ { @@ -1504,7 +1504,7 @@ "prefer-stable": false, "prefer-lowest": false, "platform": { - "php": "^5.3.2 || ^7.0" + "php": "^5.3.2 || ^7.0 || ^8.0" }, "platform-dev": [], "platform-overrides": { diff --git a/doc/03-cli.md b/doc/03-cli.md index 374ef952e..edfaaa9ed 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -785,7 +785,7 @@ Lists the name, version and license of every package installed. Use ### Options -* **--format:** Format of the output: text or json (default: "text") +* **--format:** Format of the output: text, json or summary (default: "text") * **--no-dev:** Remove dev dependencies from the output ## run-script diff --git a/phpstan/config.neon b/phpstan/config.neon index 8b8bf62ce..c8c130a31 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -1,7 +1,5 @@ parameters: level: 1 - autoload_files: - - '../src/bootstrap.php' excludes_analyse: - '../tests/Composer/Test/Fixtures/*' - '../tests/Composer/Test/Autoload/Fixtures/*' @@ -28,11 +26,9 @@ parameters: # BC with older PHPUnit - '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~' - # hhvm should have support for $this in closures - - - count: 1 - message: '~^Using \$this inside anonymous function is prohibited because of PHP 5\.3 support\.$~' - path: '../tests/Composer/Test/Repository/PlatformRepositoryTest.php' + # ZipArchive::* Class constants are already checked before use. + - '~^Access to undefined constant ZipArchive::~' + paths: - ../src - ../tests diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 4dee65417..07cb9aaa8 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -257,16 +257,16 @@ EOF; EOF; } - $blacklist = null; + $excluded = null; if (!empty($autoloads['exclude-from-classmap'])) { - $blacklist = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}'; + $excluded = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}'; } $classMap = array(); $ambiguousClasses = array(); $scannedFiles = array(); foreach ($autoloads['classmap'] as $dir) { - $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, null, null, $classMap, $ambiguousClasses, $scannedFiles); + $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $excluded, null, null, $classMap, $ambiguousClasses, $scannedFiles); } if ($scanPsrPackages) { @@ -289,7 +289,7 @@ EOF; continue; } - $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, $namespace, $group['type'], $classMap, $ambiguousClasses, $scannedFiles); + $classMap = $this->addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $excluded, $namespace, $group['type'], $classMap, $ambiguousClasses, $scannedFiles); } } } @@ -368,9 +368,9 @@ EOF; return count($classMap); } - private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $blacklist, $namespaceFilter, $autoloadType, array $classMap, array &$ambiguousClasses, array &$scannedFiles) + private function addClassMapCode($filesystem, $basePath, $vendorPath, $dir, $excluded, $namespaceFilter, $autoloadType, array $classMap, array &$ambiguousClasses, array &$scannedFiles) { - foreach ($this->generateClassMap($dir, $blacklist, $namespaceFilter, $autoloadType, true, $scannedFiles) as $class => $path) { + foreach ($this->generateClassMap($dir, $excluded, $namespaceFilter, $autoloadType, true, $scannedFiles) as $class => $path) { $pathCode = $this->getPathCode($filesystem, $basePath, $vendorPath, $path).",\n"; if (!isset($classMap[$class])) { $classMap[$class] = $pathCode; @@ -382,9 +382,9 @@ EOF; return $classMap; } - private function generateClassMap($dir, $blacklist, $namespaceFilter, $autoloadType, $showAmbiguousWarning, array &$scannedFiles) + private function generateClassMap($dir, $excluded, $namespaceFilter, $autoloadType, $showAmbiguousWarning, array &$scannedFiles) { - return ClassMapGenerator::createMap($dir, $blacklist, $showAmbiguousWarning ? $this->io : null, $namespaceFilter, $autoloadType, $scannedFiles); + return ClassMapGenerator::createMap($dir, $excluded, $showAmbiguousWarning ? $this->io : null, $namespaceFilter, $autoloadType, $scannedFiles); } public function buildPackageMap(InstallationManager $installationManager, PackageInterface $mainPackage, array $packages) @@ -488,15 +488,15 @@ EOF; } if (isset($autoloads['classmap'])) { - $blacklist = null; + $excluded = null; if (!empty($autoloads['exclude-from-classmap'])) { - $blacklist = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}'; + $excluded = '{(' . implode('|', $autoloads['exclude-from-classmap']) . ')}'; } $scannedFiles = array(); foreach ($autoloads['classmap'] as $dir) { try { - $loader->addClassMap($this->generateClassMap($dir, $blacklist, null, null, false, $scannedFiles)); + $loader->addClassMap($this->generateClassMap($dir, $excluded, null, null, false, $scannedFiles)); } catch (\RuntimeException $e) { $this->io->writeError(''.$e->getMessage().''); } @@ -640,6 +640,10 @@ EOF; } } + if ($match[1] === 'zend-opcache') { + $match[1] = 'zend opcache'; + } + $extension = var_export($match[1], true); if ($match[1] === 'pcntl' || $match[1] === 'readline') { $requiredExtensions[$extension] = "PHP_SAPI !== 'cli' || extension_loaded($extension) || \$missingExtensions[] = $extension;\n"; diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index b86a4e9ce..ae6371fa4 100644 --- a/src/Composer/Autoload/ClassMapGenerator.php +++ b/src/Composer/Autoload/ClassMapGenerator.php @@ -51,7 +51,7 @@ class ClassMapGenerator * Iterate over all files in the given directory searching for classes * * @param \Iterator|string $path The path to search in or an iterator - * @param string $blacklist Regex that matches against the file path that exclude from the classmap. + * @param string $excluded Regex that matches against the file path that exclude from the classmap. * @param IOInterface $io IO object * @param string $namespace Optional namespace prefix to filter by * @param string $autoloadType psr-0|psr-4 Optional autoload standard to use mapping rules @@ -59,7 +59,7 @@ class ClassMapGenerator * @throws \RuntimeException When the path is neither an existing file nor directory * @return array A class map array */ - public static function createMap($path, $blacklist = null, IOInterface $io = null, $namespace = null, $autoloadType = null, &$scannedFiles = array()) + public static function createMap($path, $excluded = null, IOInterface $io = null, $namespace = null, $autoloadType = null, &$scannedFiles = array()) { $basePath = $path; if (is_string($path)) { @@ -102,12 +102,12 @@ class ClassMapGenerator continue; } - // check the realpath of the file against the blacklist as the path might be a symlink and the blacklist is realpath'd so symlink are resolved - if ($blacklist && preg_match($blacklist, strtr($realPath, '\\', '/'))) { + // check the realpath of the file against the excluded paths as the path might be a symlink and the excluded path is realpath'd so symlink are resolved + if ($excluded && preg_match($excluded, strtr($realPath, '\\', '/'))) { continue; } // check non-realpath of file for directories symlink in project dir - if ($blacklist && preg_match($blacklist, strtr($filePath, '\\', '/'))) { + if ($excluded && preg_match($excluded, strtr($filePath, '\\', '/'))) { continue; } diff --git a/src/Composer/Command/ArchiveCommand.php b/src/Composer/Command/ArchiveCommand.php index fd6d454f5..b521e4927 100644 --- a/src/Composer/Command/ArchiveCommand.php +++ b/src/Composer/Command/ArchiveCommand.php @@ -23,6 +23,7 @@ use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; use Composer\Util\Filesystem; use Composer\Util\Loop; +use Composer\Util\ProcessExecutor; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; @@ -112,9 +113,10 @@ EOT $archiveManager = $composer->getArchiveManager(); } else { $factory = new Factory; + $process = new ProcessExecutor(); $httpDownloader = $factory->createHttpDownloader($io, $config); - $downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader); - $archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader)); + $downloadManager = $factory->createDownloadManager($io, $config, $httpDownloader, $process); + $archiveManager = $factory->createArchiveManager($config, $downloadManager, new Loop($httpDownloader, $process)); } if ($packageName) { diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 07ff8bed8..27d9919d7 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -38,6 +38,7 @@ use Symfony\Component\Finder\Finder; use Composer\Json\JsonFile; use Composer\Config\JsonConfigSource; use Composer\Util\Filesystem; +use Composer\Util\ProcessExecutor; use Composer\Util\Loop; use Composer\Package\Version\VersionParser; @@ -186,7 +187,8 @@ EOT $composer = Factory::create($io, null, $disablePlugins); } - $fs = new Filesystem(); + $process = new ProcessExecutor($io); + $fs = new Filesystem($process); if ($noScripts === false) { // dispatch event @@ -199,6 +201,8 @@ EOT // install dependencies of the created project if ($noInstall === false) { + $composer->getInstallationManager()->setOutputProgress(!$noProgress); + $installer = Installer::create($io, $composer); $installer->setPreferSource($preferSource) ->setPreferDist($preferDist) @@ -210,6 +214,10 @@ EOT ->setClassMapAuthoritative($config->get('classmap-authoritative')) ->setApcuAutoloader($config->get('apcu-autoloader')); + if (!$composer->getLocker()->isLocked()) { + $installer->setUpdate(true); + } + if ($disablePlugins) { $installer->disablePlugins(); } @@ -307,7 +315,8 @@ EOT $directory = getcwd() . DIRECTORY_SEPARATOR . array_pop($parts); } - $fs = new Filesystem(); + $process = new ProcessExecutor($io); + $fs = new Filesystem($process); if (!$fs->isAbsolutePath($directory)) { $directory = getcwd() . DIRECTORY_SEPARATOR . $directory; } @@ -397,12 +406,13 @@ EOT $factory = new Factory(); $httpDownloader = $factory->createHttpDownloader($io, $config); - $dm = $factory->createDownloadManager($io, $config, $httpDownloader); + $dm = $factory->createDownloadManager($io, $config, $httpDownloader, $process); $dm->setPreferSource($preferSource) ->setPreferDist($preferDist); - $projectInstaller = new ProjectInstaller($directory, $dm); - $im = $factory->createInstallationManager(new Loop($httpDownloader), $io); + $projectInstaller = new ProjectInstaller($directory, $dm, $fs); + $im = $factory->createInstallationManager(new Loop($httpDownloader, $process), $io); + $im->setOutputProgress(!$noProgress); $im->addInstaller($projectInstaller); $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), array(new InstallOperation($package))); $im->notifyInstalls($io); diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 80e6f8423..c0b82ada7 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -431,7 +431,11 @@ EOT } $versionsUtil = new Versions($config, $this->httpDownloader); - $latest = $versionsUtil->getLatest(); + try { + $latest = $versionsUtil->getLatest(); + } catch (\Exception $e) { + return $e; + } if (Composer::VERSION !== $latest['version'] && Composer::VERSION !== '@package_version@') { return 'You are not running the latest '.$versionsUtil->getChannel().' version, run `composer self-update` to update ('.Composer::VERSION.' => '.$latest['version'].')'; diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 93523cba7..f8294c47b 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -106,6 +106,8 @@ EOT $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + $composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress')); + $install ->setDryRun($input->getOption('dry-run')) ->setVerbose($input->getOption('verbose')) diff --git a/src/Composer/Command/LicensesCommand.php b/src/Composer/Command/LicensesCommand.php index 85cb64a7f..c9a099f26 100644 --- a/src/Composer/Command/LicensesCommand.php +++ b/src/Composer/Command/LicensesCommand.php @@ -21,6 +21,7 @@ use Symfony\Component\Console\Helper\Table; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Style\SymfonyStyle; /** * @author BenoƮt Merlet @@ -111,6 +112,28 @@ EOT ))); break; + case 'summary': + $dependencies = array(); + foreach ($packages as $package) { + $license = $package->getLicense(); + $licenseName = $license[0]; + if (!isset($dependencies[$licenseName])) { + $dependencies[$licenseName] = 0; + } + $dependencies[$licenseName]++; + } + + $rows = array(); + foreach ($dependencies as $usedLicense => $numberOfDependencies) { + $rows[] = array($usedLicense, $numberOfDependencies); + } + + $symfonyIo = new SymfonyStyle($input, $output); + $symfonyIo->table( + array('License', 'Number of dependencies'), + $rows + ); + break; default: throw new \RuntimeException(sprintf('Unsupported format "%s". See help for supported formats.', $format)); } diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 62e23dd9f..53c040412 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -220,6 +220,8 @@ EOT $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'remove', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress')); + $install = Installer::create($io, $composer); $updateDevMode = !$input->getOption('update-no-dev'); @@ -237,7 +239,7 @@ EOT $flags .= ' --with-dependencies'; } - $io->writeError('Running composer update '.implode(' ', $packages).$flags); + $io->writeError('Running composer update '.implode(' ', $packages).$flags.''); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index b77ab5a4e..9525b9e77 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -281,11 +281,13 @@ EOT $flags .= ' --with-dependencies'; } - $io->writeError('Running composer update '.implode(' ', array_keys($requirements)).$flags); + $io->writeError('Running composer update '.implode(' ', array_keys($requirements)).$flags.''); $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'require', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress')); + $install = Installer::create($io, $composer); $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index 9576b141f..809db532c 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -180,6 +180,8 @@ EOT $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'update', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + $composer->getInstallationManager()->setOutputProgress(!$input->getOption('no-progress')); + $install = Installer::create($io, $composer); $config = $composer->getConfig(); diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index af735bcce..4160a69ac 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -156,7 +156,7 @@ class Application extends BaseApplication } // prompt user for dir change if no composer.json is present in current dir - if ($io->isInteractive() && !$newWorkDir && !in_array($commandName, array('', 'list', 'init', 'about', 'help', 'diagnose', 'self-update', 'global', 'create-project'), true) && !file_exists(Factory::getComposerFile())) { + if ($io->isInteractive() && !$newWorkDir && !in_array($commandName, array('', 'list', 'init', 'about', 'help', 'diagnose', 'self-update', 'global', 'create-project', 'outdated'), true) && !file_exists(Factory::getComposerFile())) { $dir = dirname(getcwd()); $home = realpath(getenv('HOME') ?: getenv('USERPROFILE') ?: '/'); diff --git a/src/Composer/DependencyResolver/Operation/InstallOperation.php b/src/Composer/DependencyResolver/Operation/InstallOperation.php index 9ac6beabe..822d0ab69 100644 --- a/src/Composer/DependencyResolver/Operation/InstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/InstallOperation.php @@ -61,7 +61,12 @@ class InstallOperation extends SolverOperation */ public function show($lock) { - return ($lock ? 'Locking ' : 'Installing ').''.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; + return self::format($this->package, $lock); + } + + public static function format(PackageInterface $package, $lock = false) + { + return ($lock ? 'Locking ' : 'Installing ').''.$package->getPrettyName().' ('.$package->getFullPrettyVersion().')'; } /** diff --git a/src/Composer/DependencyResolver/Operation/UninstallOperation.php b/src/Composer/DependencyResolver/Operation/UninstallOperation.php index 4618e0722..13c0d4831 100644 --- a/src/Composer/DependencyResolver/Operation/UninstallOperation.php +++ b/src/Composer/DependencyResolver/Operation/UninstallOperation.php @@ -61,7 +61,12 @@ class UninstallOperation extends SolverOperation */ public function show($lock) { - return 'Removing '.$this->package->getPrettyName().' ('.$this->package->getFullPrettyVersion().')'; + return self::format($this->package, $lock); + } + + public static function format(PackageInterface $package, $lock = false) + { + return 'Removing '.$package->getPrettyName().' ('.$package->getFullPrettyVersion().')'; } /** diff --git a/src/Composer/DependencyResolver/Operation/UpdateOperation.php b/src/Composer/DependencyResolver/Operation/UpdateOperation.php index 528efdd14..37fd78892 100644 --- a/src/Composer/DependencyResolver/Operation/UpdateOperation.php +++ b/src/Composer/DependencyResolver/Operation/UpdateOperation.php @@ -75,20 +75,25 @@ class UpdateOperation extends SolverOperation */ public function show($lock) { - $fromVersion = $this->initialPackage->getFullPrettyVersion(); - $toVersion = $this->targetPackage->getFullPrettyVersion(); + return self::format($this->initialPackage, $this->targetPackage, $lock); + } - if ($fromVersion === $toVersion && $this->initialPackage->getSourceReference() !== $this->targetPackage->getSourceReference()) { - $fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); - $toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); - } elseif ($fromVersion === $toVersion && $this->initialPackage->getDistReference() !== $this->targetPackage->getDistReference()) { - $fromVersion = $this->initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); - $toVersion = $this->targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); + public static function format(PackageInterface $initialPackage, PackageInterface $targetPackage, $lock = false) + { + $fromVersion = $initialPackage->getFullPrettyVersion(); + $toVersion = $targetPackage->getFullPrettyVersion(); + + if ($fromVersion === $toVersion && $initialPackage->getSourceReference() !== $targetPackage->getSourceReference()) { + $fromVersion = $initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); + $toVersion = $targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_SOURCE_REF); + } elseif ($fromVersion === $toVersion && $initialPackage->getDistReference() !== $targetPackage->getDistReference()) { + $fromVersion = $initialPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); + $toVersion = $targetPackage->getFullPrettyVersion(true, PackageInterface::DISPLAY_DIST_REF); } - $actionName = VersionParser::isUpgrade($this->initialPackage->getVersion(), $this->targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading'; + $actionName = VersionParser::isUpgrade($initialPackage->getVersion(), $targetPackage->getVersion()) ? 'Upgrading' : 'Downgrading'; - return $actionName.' '.$this->initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')'; + return $actionName.' '.$initialPackage->getPrettyName().' ('.$fromVersion.' => '.$toVersion.')'; } /** diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 26ca947d3..b46447f72 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -14,6 +14,7 @@ namespace Composer\DependencyResolver; use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; +use Composer\Semver\CompilingMatcher; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\Constraint; use Composer\Package\PackageInterface; @@ -146,9 +147,7 @@ class Pool implements \Countable $candidateVersion = $candidate->getVersion(); if ($candidateName === $name) { - $pkgConstraint = new Constraint('==', $candidateVersion); - - if ($constraint === null || $constraint->matches($pkgConstraint)) { + if ($constraint === null || CompilingMatcher::match($constraint, Constraint::OP_EQ, $candidateVersion)) { return true; } diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 29903f493..38a5fac3c 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -12,21 +12,21 @@ namespace Composer\DependencyResolver; +use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; use Composer\Package\AliasPackage; use Composer\Package\BasePackage; -use Composer\Package\Package; use Composer\Package\PackageInterface; use Composer\Package\Version\StabilityFilter; +use Composer\Plugin\PluginEvents; +use Composer\Plugin\PrePoolCreateEvent; use Composer\Repository\PlatformRepository; use Composer\Repository\RootPackageRepository; +use Composer\Semver\CompilingMatcher; use Composer\Semver\Constraint\Constraint; use Composer\Semver\Constraint\ConstraintInterface; use Composer\Semver\Constraint\MatchAllConstraint; use Composer\Semver\Constraint\MultiConstraint; -use Composer\EventDispatcher\EventDispatcher; -use Composer\Plugin\PrePoolCreateEvent; -use Composer\Plugin\PluginEvents; /** * @author Nils Adermann @@ -88,7 +88,7 @@ class PoolBuilder * @param int[] $stabilityFlags an array of package name => BasePackage::STABILITY_* value * @psalm-param array $stabilityFlags * @param array[] $rootAliases - * @psalm-param list $rootAliases + * @psalm-param array> $rootAliases * @param string[] $rootReferences an array of package name => source reference * @psalm-param array $rootReferences */ @@ -96,7 +96,7 @@ class PoolBuilder { $this->acceptableStabilities = $acceptableStabilities; $this->stabilityFlags = $stabilityFlags; - $this->rootAliases = $this->getRootAliasesPerPackage($rootAliases); + $this->rootAliases = $rootAliases; $this->rootReferences = $rootReferences; $this->eventDispatcher = $eventDispatcher; $this->io = $io; @@ -209,7 +209,7 @@ class PoolBuilder $found = false; foreach ($aliasedPackages as $packageOrAlias) { - if ($constraint->matches(new Constraint('==', $packageOrAlias->getVersion()))) { + if (CompilingMatcher::match($constraint, Constraint::OP_EQ, $packageOrAlias->getVersion())) { $found = true; } } @@ -425,19 +425,5 @@ class PoolBuilder unset($this->skippedLoad[$name]); unset($this->loadedNames[$name]); } - - private function getRootAliasesPerPackage(array $aliases) - { - $normalizedAliases = array(); - - foreach ($aliases as $alias) { - $normalizedAliases[$alias['package']][$alias['version']] = array( - 'alias' => $alias['alias'], - 'alias_normalized' => $alias['alias_normalized'], - ); - } - - return $normalizedAliases; - } } diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 283d0103e..7cf19deee 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -16,6 +16,8 @@ use Composer\Package\PackageInterface; use Symfony\Component\Finder\Finder; use Composer\IO\IOInterface; use Composer\Exception\IrrecoverableDownloadException; +use React\Promise\PromiseInterface; +use Composer\DependencyResolver\Operation\InstallOperation; /** * Base downloader for archives @@ -28,14 +30,7 @@ abstract class ArchiveDownloader extends FileDownloader { public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { - $res = parent::download($package, $path, $prevPackage, $output); - - // if not downgrading and the dir already exists it seems we have an inconsistent state in the vendor dir and the user should fix it - if (!$prevPackage && is_dir($path) && !$this->filesystem->isDirEmpty($path)) { - throw new IrrecoverableDownloadException('Expected empty path to extract '.$package.' into but directory exists: '.$path); - } - - return $res; + return parent::download($package, $path, $prevPackage, $output); } /** @@ -46,40 +41,75 @@ abstract class ArchiveDownloader extends FileDownloader public function install(PackageInterface $package, $path, $output = true) { if ($output) { - $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): Extracting archive"); + $this->io->writeError(" - " . InstallOperation::format($package).": Extracting archive"); } else { $this->io->writeError('Extracting archive', false); } - $this->filesystem->ensureDirectoryExists($path); - if (!$this->filesystem->isDirEmpty($path)) { - throw new \UnexpectedValueException('Expected empty path to extract '.$package.' into but directory exists: '.$path); - } + $this->filesystem->emptyDirectory($path); do { $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); } while (is_dir($temporaryDir)); + $this->addCleanupPath($package, $temporaryDir); + $this->addCleanupPath($package, $path); + + $this->filesystem->ensureDirectoryExists($temporaryDir); $fileName = $this->getFileName($package, $path); - try { - $this->filesystem->ensureDirectoryExists($temporaryDir); - try { - $this->extract($package, $fileName, $temporaryDir); - } catch (\Exception $e) { - // remove cache if the file was corrupted - parent::clearLastCacheWrite($package); - throw $e; - } + $filesystem = $this->filesystem; + $self = $this; - $this->filesystem->unlink($fileName); + $cleanup = function () use ($path, $filesystem, $temporaryDir, $package, $self) { + // remove cache if the file was corrupted + $self->clearLastCacheWrite($package); + + // clean up + $filesystem->removeDirectory($path); + $filesystem->removeDirectory($temporaryDir); + $self->removeCleanupPath($package, $temporaryDir); + $self->removeCleanupPath($package, $path); + }; + + $promise = null; + try { + $promise = $this->extract($package, $fileName, $temporaryDir); + } catch (\Exception $e) { + $cleanup(); + throw $e; + } + + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); + } + + return $promise->then(function () use ($self, $package, $filesystem, $fileName, $temporaryDir, $path) { + $filesystem->unlink($fileName); + + /** + * Returns the folder content, excluding .DS_Store + * + * @param string $dir Directory + * @return \SplFileInfo[] + */ + $getFolderContent = function ($dir) { + $finder = Finder::create() + ->ignoreVCS(false) + ->ignoreDotFiles(false) + ->notName('.DS_Store') + ->depth(0) + ->in($dir); + + return iterator_to_array($finder); + }; $renameAsOne = false; - if (!file_exists($path) || ($this->filesystem->isDirEmpty($path) && $this->filesystem->removeDirectory($path))) { + if (!file_exists($path) || ($filesystem->isDirEmpty($path) && $filesystem->removeDirectory($path))) { $renameAsOne = true; } - $contentDir = $this->getFolderContent($temporaryDir); + $contentDir = $getFolderContent($temporaryDir); $singleDirAtTopLevel = 1 === count($contentDir) && is_dir(reset($contentDir)); if ($renameAsOne) { @@ -89,28 +119,28 @@ abstract class ArchiveDownloader extends FileDownloader } else { $extractedDir = $temporaryDir; } - $this->filesystem->rename($extractedDir, $path); + $filesystem->rename($extractedDir, $path); } else { // only one dir in the archive, extract its contents out of it if ($singleDirAtTopLevel) { - $contentDir = $this->getFolderContent((string) reset($contentDir)); + $contentDir = $getFolderContent((string) reset($contentDir)); } // move files back out of the temp dir foreach ($contentDir as $file) { $file = (string) $file; - $this->filesystem->rename($file, $path . '/' . basename($file)); + $filesystem->rename($file, $path . '/' . basename($file)); } } - $this->filesystem->removeDirectory($temporaryDir); - } catch (\Exception $e) { - // clean up - $this->filesystem->removeDirectory($path); - $this->filesystem->removeDirectory($temporaryDir); + $filesystem->removeDirectory($temporaryDir); + $self->removeCleanupPath($package, $temporaryDir); + $self->removeCleanupPath($package, $path); + }, function ($e) use ($cleanup) { + $cleanup(); throw $e; - } + }); } /** @@ -119,25 +149,8 @@ abstract class ArchiveDownloader extends FileDownloader * @param string $file Extracted file * @param string $path Directory * + * @return PromiseInterface|null * @throws \UnexpectedValueException If can not extract downloaded file to path */ abstract protected function extract(PackageInterface $package, $file, $path); - - /** - * Returns the folder content, excluding dotfiles - * - * @param string $dir Directory - * @return \SplFileInfo[] - */ - private function getFolderContent($dir) - { - $finder = Finder::create() - ->ignoreVCS(false) - ->ignoreDotFiles(false) - ->notName('.DS_Store') - ->depth(0) - ->in($dir); - - return iterator_to_array($finder); - } } diff --git a/src/Composer/Downloader/DownloadManager.php b/src/Composer/Downloader/DownloadManager.php index 45467eb4c..182ce88da 100644 --- a/src/Composer/Downloader/DownloadManager.php +++ b/src/Composer/Downloader/DownloadManager.php @@ -25,11 +25,17 @@ use React\Promise\PromiseInterface; */ class DownloadManager { + /** @var IOInterface */ private $io; + /** @var bool */ private $preferDist = false; + /** @var bool */ private $preferSource = false; + /** @var array */ private $packagePreferences = array(); + /** @var Filesystem */ private $filesystem; + /** @var array */ private $downloaders = array(); /** diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index c598efd9a..8fbb48e9c 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -18,6 +18,9 @@ use Composer\Factory; use Composer\IO\IOInterface; use Composer\IO\NullIO; use Composer\Package\Comparer\Comparer; +use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\Plugin\PluginEvents; @@ -27,7 +30,9 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\Util\Filesystem; use Composer\Util\HttpDownloader; use Composer\Util\Url as UrlUtil; +use Composer\Util\ProcessExecutor; use Composer\Downloader\TransportException; +use React\Promise\PromiseInterface; /** * Base downloader for files @@ -39,16 +44,25 @@ use Composer\Downloader\TransportException; */ class FileDownloader implements DownloaderInterface, ChangeReportInterface { + /** @var IOInterface */ protected $io; + /** @var Config */ protected $config; + /** @var HttpDownloader */ protected $httpDownloader; + /** @var Filesystem */ protected $filesystem; + /** @var Cache */ protected $cache; + /** @var EventDispatcher */ + protected $eventDispatcher; + /** @var ProcessExecutor */ + protected $process; /** * @private this is only public for php 5.3 support in closures */ public $lastCacheWrites = array(); - private $eventDispatcher; + private $additionalCleanupPaths = array(); /** * Constructor. @@ -60,14 +74,15 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface * @param Cache $cache Cache instance * @param Filesystem $filesystem The filesystem */ - public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $filesystem = null, ProcessExecutor $process = null) { $this->io = $io; $this->config = $config; $this->eventDispatcher = $eventDispatcher; $this->httpDownloader = $httpDownloader; - $this->filesystem = $filesystem ?: new Filesystem(); $this->cache = $cache; + $this->process = $process ?: new ProcessExecutor($io); + $this->filesystem = $filesystem ?: new Filesystem($this->process); if ($this->cache && $this->cache->gcIsNecessary()) { $this->cache->gc($config->get('cache-files-ttl'), $config->get('cache-files-maxsize')); @@ -119,8 +134,9 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $url = reset($urls); if ($eventDispatcher) { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']); + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed'], 'package', $package); $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $url['processed'] = $preFileDownloadEvent->getProcessedUrl(); } $checksum = $package->getDistSha1Checksum(); @@ -252,6 +268,12 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $path, ); + if (isset($this->additionalCleanupPaths[$package->getName()])) { + foreach ($this->additionalCleanupPaths[$package->getName()] as $path) { + $this->filesystem->remove($path); + } + } + foreach ($dirsToCleanUp as $dir) { if (is_dir($dir) && $this->filesystem->isDirEmpty($dir)) { $this->filesystem->removeDirectory($dir); @@ -265,7 +287,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface public function install(PackageInterface $package, $path, $output = true) { if ($output) { - $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - " . InstallOperation::format($package)); } $this->filesystem->emptyDirectory($path); @@ -285,22 +307,49 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface } } + /** + * TODO mark private in v3 + * @protected This is public due to PHP 5.3 + */ + public function addCleanupPath(PackageInterface $package, $path) + { + $this->additionalCleanupPaths[$package->getName()][] = $path; + } + + /** + * TODO mark private in v3 + * @protected This is public due to PHP 5.3 + */ + public function removeCleanupPath(PackageInterface $package, $path) + { + if (isset($this->additionalCleanupPaths[$package->getName()])) { + $idx = array_search($path, $this->additionalCleanupPaths[$package->getName()]); + if (false !== $idx) { + unset($this->additionalCleanupPaths[$package->getName()][$idx]); + } + } + } + /** * {@inheritDoc} */ public function update(PackageInterface $initial, PackageInterface $target, $path) { - $name = $target->getName(); - $from = $initial->getFullPrettyVersion(); - $to = $target->getFullPrettyVersion(); + $this->io->writeError(" - " . UpdateOperation::format($initial, $target) . ": ", false); - $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; - $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); + $promise = $this->remove($initial, $path, false); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); + } + $self = $this; + $io = $this->io; - $this->remove($initial, $path, false); - $this->install($target, $path, false); + return $promise->then(function () use ($self, $target, $path, $io) { + $promise = $self->install($target, $path, false); + $io->writeError(''); - $this->io->writeError(''); + return $promise; + }); } /** @@ -309,7 +358,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface public function remove(PackageInterface $package, $path, $output = true) { if ($output) { - $this->io->writeError(" - Removing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - " . UninstallOperation::format($package)); } if (!$this->filesystem->removeDirectory($path)) { throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); @@ -374,9 +423,14 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $output = ''; try { - $res = $this->download($package, $targetDir.'_compare', null, false); + if (is_dir($targetDir.'_compare')) { + $this->filesystem->removeDirectory($targetDir.'_compare'); + } + + $this->download($package, $targetDir.'_compare', null, false); $this->httpDownloader->wait(); - $res = $this->install($package, $targetDir.'_compare', false); + $this->install($package, $targetDir.'_compare', false); + $this->process->wait(); $comparer = new Comparer(); $comparer->setSource($targetDir.'_compare'); diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index 37e7f94da..e5fe253a2 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -495,7 +495,7 @@ class GitDownloader extends VcsDownloader implements DvcsDownloaderInterface protected function discardChanges($path) { $path = $this->normalizePath($path); - if (0 !== $this->process->execute('git reset --hard', $output, $path)) { + if (0 !== $this->process->execute('git clean -df && git reset --hard', $output, $path)) { throw new \RuntimeException("Could not reset changes\n\n:".$this->process->getErrorOutput()); } diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index 91be4593d..3ebff597a 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -20,6 +20,7 @@ use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; +use Composer\Util\Filesystem; /** * GZip archive downloader. @@ -28,15 +29,6 @@ use Composer\IO\IOInterface; */ class GzipDownloader extends ArchiveDownloader { - /** @var ProcessExecutor */ - protected $process; - - public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) - { - $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); - } - protected function extract(PackageInterface $package, $file, $path) { $filename = pathinfo(parse_url($package->getDistUrl(), PHP_URL_PATH), PATHINFO_FILENAME); diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index 648c987b2..190267c5c 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -18,10 +18,18 @@ use Composer\Package\PackageInterface; use Composer\Package\Version\VersionGuesser; use Composer\Package\Version\VersionParser; use Composer\Util\Platform; +use Composer\IO\IOInterface; +use Composer\Config; +use Composer\Cache; +use Composer\Util\HttpDownloader; use Composer\Util\ProcessExecutor; -use Composer\Util\Filesystem as ComposerFilesystem; +use Composer\Util\Filesystem; +use Composer\EventDispatcher\EventDispatcher; use Symfony\Component\Filesystem\Exception\IOException; -use Symfony\Component\Filesystem\Filesystem; +use Symfony\Component\Filesystem\Filesystem as SymfonyFilesystem; +use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; /** * Download a package from a local path. @@ -77,11 +85,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter if (realpath($path) === $realUrl) { if ($output) { - $this->io->writeError(sprintf( - ' - Installing %s (%s): Source already present', - $package->getName(), - $package->getFullPrettyVersion() - )); + $this->io->writeError(" - " . InstallOperation::format($package).': Source already present'); } else { $this->io->writeError('Source already present', false); } @@ -115,15 +119,11 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $allowedStrategies = array(self::STRATEGY_MIRROR); } - $fileSystem = new Filesystem(); + $symfonyFilesystem = new SymfonyFilesystem(); $this->filesystem->removeDirectory($path); if ($output) { - $this->io->writeError(sprintf( - ' - Installing %s (%s): ', - $package->getName(), - $package->getFullPrettyVersion() - ), false); + $this->io->writeError(" - " . InstallOperation::format($package).': ', false); } $isFallback = false; @@ -142,9 +142,9 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $path = rtrim($path, "/"); $this->io->writeError(sprintf('Symlinking from %s', $url), false); if ($transportOptions['relative']) { - $fileSystem->symlink($shortestPath, $path); + $symfonyFilesystem->symlink($shortestPath, $path); } else { - $fileSystem->symlink($realUrl, $path); + $symfonyFilesystem->symlink($realUrl, $path); } } } catch (IOException $e) { @@ -161,12 +161,11 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter // Fallback if symlink failed or if symlink is not allowed for the package if (self::STRATEGY_MIRROR == $currentStrategy) { - $fs = new ComposerFilesystem(); - $realUrl = $fs->normalizePath($realUrl); + $realUrl = $this->filesystem->normalizePath($realUrl); $this->io->writeError(sprintf('%sMirroring from %s', $isFallback ? ' ' : '', $url), false); $iterator = new ArchivableFilesFinder($realUrl, array()); - $fileSystem->mirror($realUrl, $path, $iterator); + $symfonyFilesystem->mirror($realUrl, $path, $iterator); } if ($output) { @@ -183,7 +182,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter if ($path === $realUrl) { if ($output) { - $this->io->writeError(" - Removing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "), source is still present in $path"); + $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path"); } return; @@ -196,7 +195,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter */ if (Platform::isWindows() && $this->filesystem->isJunction($path)) { if ($output) { - $this->io->writeError(" - Removing junction for " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - " . UninstallOperation::format($package).", source is still present in $path"); } if (!$this->filesystem->removeJunction($path)) { $this->io->writeError(" Could not remove junction at " . $path . " - is another process locking it?"); @@ -213,7 +212,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter public function getVcsReference(PackageInterface $package, $path) { $parser = new VersionParser; - $guesser = new VersionGuesser($this->config, new ProcessExecutor($this->io), $parser); + $guesser = new VersionGuesser($this->config, $this->process, $parser); $dumper = new ArrayDumper; $packageConfig = $dumper->dump($package); diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index d0fbadcc6..b0484b661 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -19,6 +19,7 @@ use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; +use Composer\Util\Filesystem; use Composer\IO\IOInterface; use Composer\Package\PackageInterface; use RarArchive; @@ -32,15 +33,6 @@ use RarArchive; */ class RarDownloader extends ArchiveDownloader { - /** @var ProcessExecutor */ - protected $process; - - public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) - { - $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); - } - protected function extract(PackageInterface $package, $file, $path) { $processError = null; diff --git a/src/Composer/Downloader/SvnDownloader.php b/src/Composer/Downloader/SvnDownloader.php index 634c4a7d5..f73737a7c 100644 --- a/src/Composer/Downloader/SvnDownloader.php +++ b/src/Composer/Downloader/SvnDownloader.php @@ -65,7 +65,7 @@ class SvnDownloader extends VcsDownloader throw new \RuntimeException('The .svn directory is missing from '.$path.', see https://getcomposer.org/commit-deps for more information'); } - $util = new SvnUtil($url, $this->io, $this->config); + $util = new SvnUtil($url, $this->io, $this->config, $this->process); $flags = ""; if (version_compare($util->binaryVersion(), '1.7.0', '>=')) { $flags .= ' --ignore-ancestry'; @@ -103,7 +103,7 @@ class SvnDownloader extends VcsDownloader */ protected function execute(PackageInterface $package, $baseUrl, $command, $url, $cwd = null, $path = null) { - $util = new SvnUtil($baseUrl, $this->io, $this->config); + $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process); $util->setCacheCredentials($this->cacheCredentials); try { return $util->execute($command, $url, $cwd, $path, $this->io->isVerbose()); @@ -202,7 +202,7 @@ class SvnDownloader extends VcsDownloader $command = sprintf('svn log -r%s:%s --incremental', ProcessExecutor::escape($fromRevision), ProcessExecutor::escape($toRevision)); - $util = new SvnUtil($baseUrl, $this->io, $this->config); + $util = new SvnUtil($baseUrl, $this->io, $this->config, $this->process); $util->setCacheCredentials($this->cacheCredentials); try { return $util->executeLocal($command, $path, null, $this->io->isVerbose()); diff --git a/src/Composer/Downloader/VcsDownloader.php b/src/Composer/Downloader/VcsDownloader.php index da078e76f..4b5f94f0c 100644 --- a/src/Composer/Downloader/VcsDownloader.php +++ b/src/Composer/Downloader/VcsDownloader.php @@ -21,6 +21,9 @@ use Composer\Util\ProcessExecutor; use Composer\IO\IOInterface; use Composer\Util\Filesystem; use React\Promise\PromiseInterface; +use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; /** * @author Jordi Boggiano @@ -120,7 +123,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa throw new \InvalidArgumentException('Package '.$package->getPrettyName().' is missing reference information'); } - $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . "): ", false); + $this->io->writeError(" - " . InstallOperation::format($package).': ', false); $urls = $this->prepareUrls($package->getSourceUrls()); while ($url = array_shift($urls)) { @@ -153,23 +156,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa throw new \InvalidArgumentException('Package '.$target->getPrettyName().' is missing reference information'); } - $name = $target->getName(); - if ($initial->getPrettyVersion() == $target->getPrettyVersion()) { - if ($target->getSourceType() === 'svn') { - $from = $initial->getSourceReference(); - $to = $target->getSourceReference(); - } else { - $from = substr($initial->getSourceReference(), 0, 7); - $to = substr($target->getSourceReference(), 0, 7); - } - $name .= ' '.$initial->getPrettyVersion(); - } else { - $from = $initial->getFullPrettyVersion(); - $to = $target->getFullPrettyVersion(); - } - - $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; - $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); + $this->io->writeError(" - " . UpdateOperation::format($initial, $target).': ', false); $urls = $this->prepareUrls($target->getSourceUrls()); @@ -227,7 +214,7 @@ abstract class VcsDownloader implements DownloaderInterface, ChangeReportInterfa */ public function remove(PackageInterface $package, $path) { - $this->io->writeError(" - Removing " . $package->getName() . " (" . $package->getPrettyVersion() . ")"); + $this->io->writeError(" - " . UninstallOperation::format($package)); if (!$this->filesystem->removeDirectory($path)) { throw new \RuntimeException('Could not completely delete '.$path.', aborting.'); } diff --git a/src/Composer/Downloader/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index 371ceda1b..b043af333 100644 --- a/src/Composer/Downloader/XzDownloader.php +++ b/src/Composer/Downloader/XzDownloader.php @@ -19,6 +19,7 @@ use Composer\Package\PackageInterface; use Composer\Util\ProcessExecutor; use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; +use Composer\Util\Filesystem; /** * Xz archive downloader. @@ -28,16 +29,6 @@ use Composer\IO\IOInterface; */ class XzDownloader extends ArchiveDownloader { - /** @var ProcessExecutor */ - protected $process; - - public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) - { - $this->process = $process ?: new ProcessExecutor($io); - - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); - } - protected function extract(PackageInterface $package, $file, $path) { $command = 'tar -xJf ' . ProcessExecutor::escape($file) . ' -C ' . ProcessExecutor::escape($path); diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 29c7fd82a..83b904a8a 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -19,6 +19,7 @@ use Composer\Package\PackageInterface; use Composer\Util\IniHelper; use Composer\Util\Platform; use Composer\Util\ProcessExecutor; +use Composer\Util\Filesystem; use Composer\Util\HttpDownloader; use Composer\IO\IOInterface; use Symfony\Component\Process\ExecutableFinder; @@ -33,17 +34,9 @@ class ZipDownloader extends ArchiveDownloader private static $hasZipArchive; private static $isWindows; - /** @var ProcessExecutor */ - protected $process; /** @var ZipArchive|null */ private $zipArchiveObject; - public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) - { - $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); - } - /** * {@inheritDoc} */ @@ -85,9 +78,8 @@ class ZipDownloader extends ArchiveDownloader * @param string $file File to extract * @param string $path Path where to extract file * @param bool $isLastChance If true it is called as a fallback and should throw an exception - * @return bool Success status */ - protected function extractWithSystemUnzip($file, $path, $isLastChance) + private function extractWithSystemUnzip(PackageInterface $package, $file, $path, $isLastChance, $async = false) { if (!self::$hasZipArchive) { // Force Exception throwing if the Other alternative is not available @@ -97,18 +89,56 @@ class ZipDownloader extends ArchiveDownloader if (!self::$hasSystemUnzip && !$isLastChance) { // This was call as the favorite extract way, but is not available // We switch to the alternative - return $this->extractWithZipArchive($file, $path, true); + return $this->extractWithZipArchive($package, $file, $path, true); + } + + // When called after a ZipArchive failed, perhaps there is some files to overwrite + $overwrite = $isLastChance ? '-o' : ''; + $command = 'unzip -qq '.$overwrite.' '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path); + + if ($async) { + $self = $this; + $io = $this->io; + $tryFallback = function ($processError) use ($isLastChance, $io, $self, $file, $path, $package) { + if ($isLastChance) { + throw $processError; + } + + if (!is_file($file)) { + $io->writeError(' '.$processError->getMessage().''); + $io->writeError(' This most likely is due to a custom installer plugin not handling the returned Promise from the downloader'); + $io->writeError(' See https://github.com/composer/installers/commit/5006d0c28730ade233a8f42ec31ac68fb1c5c9bb for an example fix'); + } else { + $io->writeError(' '.$processError->getMessage().''); + $io->writeError(' The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems)'); + $io->writeError(' Unzip with unzip command failed, falling back to ZipArchive class'); + } + + return $self->extractWithZipArchive($package, $file, $path, true); + }; + + try { + $promise = $this->process->executeAsync($command); + + return $promise->then(function ($process) use ($tryFallback, $command, $package, $file) { + if (!$process->isSuccessful()) { + $output = $process->getErrorOutput(); + $output = str_replace(', '.$file.'.zip or '.$file.'.ZIP', '', $output); + + return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.$command."\n\n".$output)); + } + }); + } catch (\Exception $e) { + return $tryFallback($e); + } catch (\Throwable $e) { + return $tryFallback($e); + } } $processError = null; - // When called after a ZipArchive failed, perhaps there is some files to overwrite - $overwrite = $isLastChance ? '-o' : ''; - - $command = 'unzip -qq '.$overwrite.' '.ProcessExecutor::escape($file).' -d '.ProcessExecutor::escape($path); - try { if (0 === $exitCode = $this->process->execute($command, $ignoredOutput)) { - return true; + return \React\Promise\resolve(); } $processError = new \RuntimeException('Failed to execute ('.$exitCode.') '.$command."\n\n".$this->process->getErrorOutput()); @@ -120,11 +150,11 @@ class ZipDownloader extends ArchiveDownloader throw $processError; } - $this->io->writeError(' '.$processError->getMessage()); + $this->io->writeError(' '.$processError->getMessage().''); $this->io->writeError(' The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems)'); $this->io->writeError(' Unzip with unzip command failed, falling back to ZipArchive class'); - return $this->extractWithZipArchive($file, $path, true); + return $this->extractWithZipArchive($package, $file, $path, true); } /** @@ -133,9 +163,11 @@ class ZipDownloader extends ArchiveDownloader * @param string $file File to extract * @param string $path Path where to extract file * @param bool $isLastChance If true it is called as a fallback and should throw an exception - * @return bool Success status + * + * TODO v3 should make this private once we can drop PHP 5.3 support + * @protected */ - protected function extractWithZipArchive($file, $path, $isLastChance) + public function extractWithZipArchive(PackageInterface $package, $file, $path, $isLastChance) { if (!self::$hasSystemUnzip) { // Force Exception throwing if the Other alternative is not available @@ -145,7 +177,7 @@ class ZipDownloader extends ArchiveDownloader if (!self::$hasZipArchive && !$isLastChance) { // This was call as the favorite extract way, but is not available // We switch to the alternative - return $this->extractWithSystemUnzip($file, $path, true); + return $this->extractWithSystemUnzip($package, $file, $path, true); } $processError = null; @@ -158,7 +190,7 @@ class ZipDownloader extends ArchiveDownloader if (true === $extractResult) { $zipArchive->close(); - return true; + return \React\Promise\resolve(); } $processError = new \RuntimeException(rtrim("There was an error extracting the ZIP file, it is either corrupted or using an invalid format.\n")); @@ -169,16 +201,18 @@ class ZipDownloader extends ArchiveDownloader $processError = new \RuntimeException('The archive may contain identical file names with different capitalization (which fails on case insensitive filesystems): '.$e->getMessage(), 0, $e); } catch (\Exception $e) { $processError = $e; + } catch (\Throwable $e) { + $processError = $e; } if ($isLastChance) { throw $processError; } - $this->io->writeError(' '.$processError->getMessage()); + $this->io->writeError(' '.$processError->getMessage().''); $this->io->writeError(' Unzip with ZipArchive class failed, falling back to unzip command'); - return $this->extractWithSystemUnzip($file, $path, true); + return $this->extractWithSystemUnzip($package, $file, $path, true); } /** @@ -191,10 +225,10 @@ class ZipDownloader extends ArchiveDownloader { // Each extract calls its alternative if not available or fails if (self::$isWindows) { - $this->extractWithZipArchive($file, $path, false); - } else { - $this->extractWithSystemUnzip($file, $path, false); + return $this->extractWithZipArchive($package, $file, $path, false); } + + return $this->extractWithSystemUnzip($package, $file, $path, false, true); } /** diff --git a/src/Composer/EventDispatcher/EventDispatcher.php b/src/Composer/EventDispatcher/EventDispatcher.php index 2db26eb5f..3aed66f76 100644 --- a/src/Composer/EventDispatcher/EventDispatcher.php +++ b/src/Composer/EventDispatcher/EventDispatcher.php @@ -28,6 +28,7 @@ use Composer\Installer\PackageEvent; use Composer\Installer\BinaryInstaller; use Composer\Util\ProcessExecutor; use Composer\Script\Event as ScriptEvent; +use Composer\ClassLoader; use Symfony\Component\Process\PhpExecutableFinder; /** @@ -45,11 +46,17 @@ use Symfony\Component\Process\PhpExecutableFinder; */ class EventDispatcher { + /** @var Composer */ protected $composer; + /** @var IOInterface */ protected $io; + /** @var ?ClassLoader */ protected $loader; + /** @var ProcessExecutor */ protected $process; + /** @var array>> */ protected $listeners = array(); + /** @var list */ private $eventStack; /** diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 1b084461c..509fa62b0 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -335,15 +335,16 @@ class Factory } $httpDownloader = self::createHttpDownloader($io, $config); - $loop = new Loop($httpDownloader); + $process = new ProcessExecutor($io); + $loop = new Loop($httpDownloader, $process); $composer->setLoop($loop); // initialize event dispatcher - $dispatcher = new EventDispatcher($composer, $io); + $dispatcher = new EventDispatcher($composer, $io, $process); $composer->setEventDispatcher($dispatcher); // initialize repository manager - $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher); + $rm = RepositoryFactory::manager($io, $config, $httpDownloader, $dispatcher, $process); $composer->setRepositoryManager($rm); // force-set the version of the global package if not defined as @@ -354,7 +355,7 @@ class Factory // load package $parser = new VersionParser; - $guesser = new VersionGuesser($config, new ProcessExecutor($io), $parser); + $guesser = new VersionGuesser($config, $process, $parser); $loader = new Package\Loader\RootPackageLoader($rm, $config, $parser, $guesser, $io); $package = $loader->load($localConfig, 'Composer\Package\RootPackage', $cwd); $composer->setPackage($package); @@ -368,7 +369,7 @@ class Factory if ($fullLoad) { // initialize download manager - $dm = $this->createDownloadManager($io, $config, $httpDownloader, $dispatcher); + $dm = $this->createDownloadManager($io, $config, $httpDownloader, $process, $dispatcher); $composer->setDownloadManager($dm); // initialize autoload generator @@ -381,7 +382,7 @@ class Factory } // add installers to the manager (must happen after download manager is created since they read it out of $composer) - $this->createDefaultInstallers($im, $composer, $io); + $this->createDefaultInstallers($im, $composer, $io, $process); if ($fullLoad) { $globalComposer = null; @@ -399,7 +400,7 @@ class Factory if ($fullLoad && isset($composerFile)) { $lockFile = self::getLockFile($composerFile); - $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile)); + $locker = new Package\Locker($io, new JsonFile($lockFile, null, $io), $im, file_get_contents($composerFile), $process); $composer->setLocker($locker); } @@ -460,14 +461,16 @@ class Factory * @param EventDispatcher $eventDispatcher * @return Downloader\DownloadManager */ - public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) + public function createDownloadManager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, ProcessExecutor $process, EventDispatcher $eventDispatcher = null) { $cache = null; if ($config->get('cache-files-ttl') > 0) { $cache = new Cache($io, $config->get('cache-files-dir'), 'a-z0-9_./'); } - $dm = new Downloader\DownloadManager($io); + $fs = new Filesystem($process); + + $dm = new Downloader\DownloadManager($io, false, $fs); switch ($preferred = $config->get('preferred-install')) { case 'dist': $dm->setPreferDist(true); @@ -485,22 +488,19 @@ class Factory $dm->setPreferences($preferred); } - $executor = new ProcessExecutor($io); - $fs = new Filesystem($executor); - - $dm->setDownloader('git', new Downloader\GitDownloader($io, $config, $executor, $fs)); - $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config, $executor, $fs)); - $dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $executor, $fs)); - $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $executor, $fs)); - $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config)); - $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); - $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); - $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); - $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); - $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $executor)); - $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); - $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); - $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache)); + $dm->setDownloader('git', new Downloader\GitDownloader($io, $config, $process, $fs)); + $dm->setDownloader('svn', new Downloader\SvnDownloader($io, $config, $process, $fs)); + $dm->setDownloader('fossil', new Downloader\FossilDownloader($io, $config, $process, $fs)); + $dm->setDownloader('hg', new Downloader\HgDownloader($io, $config, $process, $fs)); + $dm->setDownloader('perforce', new Downloader\PerforceDownloader($io, $config, $process, $fs)); + $dm->setDownloader('zip', new Downloader\ZipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); + $dm->setDownloader('rar', new Downloader\RarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); + $dm->setDownloader('tar', new Downloader\TarDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); + $dm->setDownloader('gzip', new Downloader\GzipDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); + $dm->setDownloader('xz', new Downloader\XzDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); + $dm->setDownloader('phar', new Downloader\PharDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); + $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); + $dm->setDownloader('path', new Downloader\PathDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs, $process)); return $dm; } @@ -544,10 +544,13 @@ class Factory * @param Composer $composer * @param IO\IOInterface $io */ - protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io) + protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io, ProcessExecutor $process = null) { - $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null)); - $im->addInstaller(new Installer\PluginInstaller($io, $composer)); + $fs = new Filesystem($process); + $binaryInstaller = new Installer\BinaryInstaller($io, rtrim($composer->getConfig()->get('bin-dir'), '/'), $composer->getConfig()->get('bin-compat'), $fs); + + $im->addInstaller(new Installer\LibraryInstaller($io, $composer, null, $fs, $binaryInstaller)); + $im->addInstaller(new Installer\PluginInstaller($io, $composer, $fs, $binaryInstaller)); $im->addInstaller(new Installer\MetapackageInstaller($io)); } diff --git a/src/Composer/IO/ConsoleIO.php b/src/Composer/IO/ConsoleIO.php index 925a528be..ebe38f26a 100644 --- a/src/Composer/IO/ConsoleIO.php +++ b/src/Composer/IO/ConsoleIO.php @@ -14,6 +14,7 @@ namespace Composer\IO; use Composer\Question\StrictConfirmationQuestion; use Symfony\Component\Console\Helper\HelperSet; +use Symfony\Component\Console\Helper\ProgressBar; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -253,6 +254,15 @@ class ConsoleIO extends BaseIO } } + /** + * @param int $max + * @return ProgressBar + */ + public function getProgressBar($max = 0) + { + return new ProgressBar($this->getErrorOutput(), $max); + } + /** * {@inheritDoc} */ diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 23421f666..40a041927 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -226,7 +226,7 @@ class Installer } if ($this->runScripts) { - $_SERVER['COMPOSER_DEV_MODE'] = (int) $this->devMode; + $_SERVER['COMPOSER_DEV_MODE'] = $this->devMode ? '1' : '0'; putenv('COMPOSER_DEV_MODE='.$_SERVER['COMPOSER_DEV_MODE']); // dispatch pre event @@ -685,7 +685,7 @@ class Installer } if ($this->executeOperations) { - $this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode); + $this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode, $this->runScripts); } else { foreach ($localRepoTransaction->getOperations() as $operation) { // output op, but alias op only in debug verbosity diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index bd1bf8aee..7dfb2ff3e 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -13,6 +13,7 @@ namespace Composer\Installer; use Composer\IO\IOInterface; +use Composer\IO\ConsoleIO; use Composer\Package\PackageInterface; use Composer\Package\AliasPackage; use Composer\Repository\RepositoryInterface; @@ -26,6 +27,7 @@ use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\StreamContextFactory; use Composer\Util\Loop; +use React\Promise\PromiseInterface; /** * Package operation manager. @@ -36,12 +38,20 @@ use Composer\Util\Loop; */ class InstallationManager { + /** @var array */ private $installers = array(); + /** @var array */ private $cache = array(); + /** @var array> */ private $notifiablePackages = array(); + /** @var Loop */ private $loop; + /** @var IOInterface */ private $io; + /** @var EventDispatcher */ private $eventDispatcher; + /** @var bool */ + private $outputProgress; public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) { @@ -166,7 +176,7 @@ class InstallationManager * @param RepositoryInterface $repo repository in which to add/remove/update packages * @param OperationInterface[] $operations operations to execute * @param bool $devMode whether the install is being run in dev mode - * @param bool $operation whether to dispatch script events + * @param bool $runScripts whether to dispatch script events */ public function execute(RepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true) { @@ -177,10 +187,12 @@ class InstallationManager $runCleanup = function () use (&$cleanupPromises, $loop) { $promises = array(); + $loop->abortJobs(); + foreach ($cleanupPromises as $cleanup) { $promises[] = new \React\Promise\Promise(function ($resolve, $reject) use ($cleanup) { $promise = $cleanup(); - if (null === $promise) { + if (!$promise instanceof PromiseInterface) { $resolve(); } else { $promise->then(function () use ($resolve) { @@ -259,69 +271,44 @@ class InstallationManager // execute all downloads first if (!empty($promises)) { - $this->loop->wait($promises); + $progress = null; + if ($this->outputProgress && $this->io instanceof ConsoleIO && !$this->io->isDebug() && count($promises) > 1) { + $progress = $this->io->getProgressBar(); + } + $this->loop->wait($promises, $progress); + if ($progress) { + $progress->clear(); + } } + // execute operations in batches to make sure every plugin is installed in the + // right order and activated before the packages depending on it are installed + $batches = array(); + $batch = array(); foreach ($operations as $index => $operation) { - $opType = $operation->getOperationType(); + if (in_array($operation->getOperationType(), array('update', 'install'), true)) { + $package = $operation->getOperationType() === 'update' ? $operation->getTargetPackage() : $operation->getPackage(); + if ($package->getType() === 'composer-plugin' || $package->getType() === 'composer-installer') { + if ($batch) { + $batches[] = $batch; + } + unset($operations[$index]); + $batches[] = array($index => $operation); + $batch = array(); - // ignoring alias ops as they don't need to execute anything - if (!in_array($opType, array('update', 'install', 'uninstall'))) { - // output alias ops in debug verbosity as they have no output otherwise - if ($this->io->isDebug()) { - $this->io->writeError(' - ' . $operation->show(false)); + continue; } - $this->$opType($repo, $operation); - - continue; } - - if ($opType === 'update') { - $package = $operation->getTargetPackage(); - $initialPackage = $operation->getInitialPackage(); - } else { - $package = $operation->getPackage(); - $initialPackage = null; - } - $installer = $this->getInstaller($package->getType()); - - $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($opType); - if (defined($event) && $runScripts && $this->eventDispatcher) { - $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); - } - - $dispatcher = $this->eventDispatcher; - $installManager = $this; - $loop = $this->loop; - $io = $this->io; - - $promise = $installer->prepare($opType, $package, $initialPackage); - if (null === $promise) { - $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); - } - - $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) { - return $installManager->$opType($repo, $operation); - })->then($cleanupPromises[$index]) - ->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { - $repo->write($devMode, $installManager); - - $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($opType); - if (defined($event) && $runScripts && $dispatcher) { - $dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); - } - }, function ($e) use ($opType, $package, $io) { - $io->writeError(' ' . ucfirst($opType) .' of '.$package->getPrettyName().' failed'); - - throw $e; - }); - - $promises[] = $promise; + unset($operations[$index]); + $batch[$index] = $operation; } - // execute all prepare => installs/updates/removes => cleanup steps - if (!empty($promises)) { - $this->loop->wait($promises); + if ($batch) { + $batches[] = $batch; + } + + foreach ($batches as $batch) { + $this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts); } } catch (\Exception $e) { $runCleanup(); @@ -349,6 +336,77 @@ class InstallationManager $repo->write($devMode, $this); } + private function executeBatch(RepositoryInterface $repo, array $operations, array $cleanupPromises, $devMode, $runScripts) + { + foreach ($operations as $index => $operation) { + $opType = $operation->getOperationType(); + + // ignoring alias ops as they don't need to execute anything + if (!in_array($opType, array('update', 'install', 'uninstall'))) { + // output alias ops in debug verbosity as they have no output otherwise + if ($this->io->isDebug()) { + $this->io->writeError(' - ' . $operation->show(false)); + } + $this->$opType($repo, $operation); + + continue; + } + + if ($opType === 'update') { + $package = $operation->getTargetPackage(); + $initialPackage = $operation->getInitialPackage(); + } else { + $package = $operation->getPackage(); + $initialPackage = null; + } + $installer = $this->getInstaller($package->getType()); + + $event = 'Composer\Installer\PackageEvents::PRE_PACKAGE_'.strtoupper($opType); + if (defined($event) && $runScripts && $this->eventDispatcher) { + $this->eventDispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); + } + + $dispatcher = $this->eventDispatcher; + $installManager = $this; + $io = $this->io; + + $promise = $installer->prepare($opType, $package, $initialPackage); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); + } + + $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) { + return $installManager->$opType($repo, $operation); + })->then($cleanupPromises[$index]) + ->then(function () use ($opType, $runScripts, $dispatcher, $installManager, $devMode, $repo, $operations, $operation) { + $repo->write($devMode, $installManager); + + $event = 'Composer\Installer\PackageEvents::POST_PACKAGE_'.strtoupper($opType); + if (defined($event) && $runScripts && $dispatcher) { + $dispatcher->dispatchPackageEvent(constant($event), $devMode, $repo, $operations, $operation); + } + }, function ($e) use ($opType, $package, $io) { + $io->writeError(' ' . ucfirst($opType) .' of '.$package->getPrettyName().' failed'); + + throw $e; + }); + + $promises[] = $promise; + } + + // execute all prepare => installs/updates/removes => cleanup steps + if (!empty($promises)) { + $progress = null; + if ($this->outputProgress && $this->io instanceof ConsoleIO && !$this->io->isDebug() && count($promises) > 1) { + $progress = $this->io->getProgressBar(); + } + $this->loop->wait($promises, $progress); + if ($progress) { + $progress->clear(); + } + } + } + /** * Executes install operation. * @@ -447,6 +505,11 @@ class InstallationManager return $installer->getInstallPath($package); } + public function setOutputProgress($outputProgress) + { + $this->outputProgress = $outputProgress; + } + public function notifyInstalls(IOInterface $io) { foreach ($this->notifiablePackages as $repoUrl => $packages) { diff --git a/src/Composer/Installer/LibraryInstaller.php b/src/Composer/Installer/LibraryInstaller.php index 5e99e1f47..d70cd4f63 100644 --- a/src/Composer/Installer/LibraryInstaller.php +++ b/src/Composer/Installer/LibraryInstaller.php @@ -19,6 +19,7 @@ use Composer\Package\PackageInterface; use Composer\Util\Filesystem; use Composer\Util\Silencer; use Composer\Util\Platform; +use React\Promise\PromiseInterface; /** * Package installation manager. @@ -131,11 +132,19 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface $this->binaryInstaller->removeBinaries($package); } - $this->installCode($package); - $this->binaryInstaller->installBinaries($package, $this->getInstallPath($package)); - if (!$repo->hasPackage($package)) { - $repo->addPackage(clone $package); + $promise = $this->installCode($package); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); } + + $binaryInstaller = $this->binaryInstaller; + $installPath = $this->getInstallPath($package); + return $promise->then(function () use ($binaryInstaller, $installPath, $package, $repo) { + $binaryInstaller->installBinaries($package, $installPath); + if (!$repo->hasPackage($package)) { + $repo->addPackage(clone $package); + } + }); } /** @@ -150,12 +159,20 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface $this->initializeVendorDir(); $this->binaryInstaller->removeBinaries($initial); - $this->updateCode($initial, $target); - $this->binaryInstaller->installBinaries($target, $this->getInstallPath($target)); - $repo->removePackage($initial); - if (!$repo->hasPackage($target)) { - $repo->addPackage(clone $target); + $promise = $this->updateCode($initial, $target); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); } + + $binaryInstaller = $this->binaryInstaller; + $installPath = $this->getInstallPath($target); + return $promise->then(function () use ($binaryInstaller, $installPath, $target, $initial, $repo) { + $binaryInstaller->installBinaries($target, $installPath); + $repo->removePackage($initial); + if (!$repo->hasPackage($target)) { + $repo->addPackage(clone $target); + } + }); } /** @@ -167,17 +184,25 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface throw new \InvalidArgumentException('Package is not installed: '.$package); } - $this->removeCode($package); - $this->binaryInstaller->removeBinaries($package); - $repo->removePackage($package); - - $downloadPath = $this->getPackageBasePath($package); - if (strpos($package->getName(), '/')) { - $packageVendorDir = dirname($downloadPath); - if (is_dir($packageVendorDir) && $this->filesystem->isDirEmpty($packageVendorDir)) { - Silencer::call('rmdir', $packageVendorDir); - } + $promise = $this->removeCode($package); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); } + + $binaryInstaller = $this->binaryInstaller; + $downloadPath = $this->getPackageBasePath($package); + $filesystem = $this->filesystem; + return $promise->then(function () use ($binaryInstaller, $filesystem, $downloadPath, $package, $repo) { + $binaryInstaller->removeBinaries($package); + $repo->removePackage($package); + + if (strpos($package->getName(), '/')) { + $packageVendorDir = dirname($downloadPath); + if (is_dir($packageVendorDir) && $filesystem->isDirEmpty($packageVendorDir)) { + Silencer::call('rmdir', $packageVendorDir); + } + } + }); } /** @@ -227,7 +252,7 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface protected function installCode(PackageInterface $package) { $downloadPath = $this->getInstallPath($package); - $this->downloadManager->install($package, $downloadPath); + return $this->downloadManager->install($package, $downloadPath); } protected function updateCode(PackageInterface $initial, PackageInterface $target) @@ -240,21 +265,31 @@ class LibraryInstaller implements InstallerInterface, BinaryPresenceInterface if (substr($initialDownloadPath, 0, strlen($targetDownloadPath)) === $targetDownloadPath || substr($targetDownloadPath, 0, strlen($initialDownloadPath)) === $initialDownloadPath ) { - $this->removeCode($initial); - $this->installCode($target); + $promise = $this->removeCode($initial); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); + } - return; + $self = $this; + return $promise->then(function () use ($self, $target) { + $reflMethod = new \ReflectionMethod($self, 'installCode'); + $reflMethod->setAccessible(true); + + // equivalent of $this->installCode($target) with php 5.3 support + // TODO remove this once 5.3 support is dropped + return $reflMethod->invoke($self, $target); + }); } $this->filesystem->rename($initialDownloadPath, $targetDownloadPath); } - $this->downloadManager->update($initial, $target, $targetDownloadPath); + return $this->downloadManager->update($initial, $target, $targetDownloadPath); } protected function removeCode(PackageInterface $package) { $downloadPath = $this->getPackageBasePath($package); - $this->downloadManager->remove($package, $downloadPath); + return $this->downloadManager->remove($package, $downloadPath); } protected function initializeVendorDir() diff --git a/src/Composer/Installer/MetapackageInstaller.php b/src/Composer/Installer/MetapackageInstaller.php index fbd983fa3..fb07e9bb1 100644 --- a/src/Composer/Installer/MetapackageInstaller.php +++ b/src/Composer/Installer/MetapackageInstaller.php @@ -16,6 +16,9 @@ use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; use Composer\Package\Version\VersionParser; use Composer\IO\IOInterface; +use Composer\DependencyResolver\Operation\UpdateOperation; +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; /** * Metapackage installation manager. @@ -76,7 +79,7 @@ class MetapackageInstaller implements InstallerInterface */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { - $this->io->writeError(" - Installing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - " . InstallOperation::format($package)); $repo->addPackage(clone $package); } @@ -90,11 +93,7 @@ class MetapackageInstaller implements InstallerInterface throw new \InvalidArgumentException('Package is not installed: '.$initial); } - $name = $target->getName(); - $from = $initial->getFullPrettyVersion(); - $to = $target->getFullPrettyVersion(); - $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; - $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . ")"); + $this->io->writeError(" - " . UpdateOperation::format($initial, $target)); $repo->removePackage($initial); $repo->addPackage(clone $target); @@ -109,7 +108,7 @@ class MetapackageInstaller implements InstallerInterface throw new \InvalidArgumentException('Package is not installed: '.$package); } - $this->io->writeError(" - Removing " . $package->getName() . " (" . $package->getFullPrettyVersion() . ")"); + $this->io->writeError(" - " . UninstallOperation::format($package)); $repo->removePackage($package); } diff --git a/src/Composer/Installer/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php index 6d5a26be0..ae281ab08 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -16,6 +16,8 @@ use Composer\Composer; use Composer\IO\IOInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; +use Composer\Util\Filesystem; +use React\Promise\PromiseInterface; /** * Installer for plugin packages @@ -33,9 +35,9 @@ class PluginInstaller extends LibraryInstaller * @param IOInterface $io * @param Composer $composer */ - public function __construct(IOInterface $io, Composer $composer) + public function __construct(IOInterface $io, Composer $composer, Filesystem $fs = null, BinaryInstaller $binaryInstaller = null) { - parent::__construct($io, $composer, 'composer-plugin'); + parent::__construct($io, $composer, 'composer-plugin', $fs, $binaryInstaller); $this->installationManager = $composer->getInstallationManager(); } @@ -65,15 +67,20 @@ class PluginInstaller extends LibraryInstaller */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { - parent::install($repo, $package); - try { - $this->composer->getPluginManager()->registerPackage($package, true); - } catch (\Exception $e) { - // Rollback installation - $this->io->writeError('Plugin initialization failed ('.$e->getMessage().'), uninstalling plugin'); - parent::uninstall($repo, $package); - throw $e; + $promise = parent::install($repo, $package); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); } + + $pluginManager = $this->composer->getPluginManager(); + $self = $this; + return $promise->then(function () use ($self, $pluginManager, $package, $repo) { + try { + $pluginManager->registerPackage($package, true); + } catch (\Exception $e) { + $self->rollbackInstall($e, $repo, $package); + } + }); } /** @@ -81,22 +88,38 @@ class PluginInstaller extends LibraryInstaller */ public function update(InstalledRepositoryInterface $repo, PackageInterface $initial, PackageInterface $target) { - parent::update($repo, $initial, $target); - - try { - $this->composer->getPluginManager()->deactivatePackage($initial, true); - $this->composer->getPluginManager()->registerPackage($target, true); - } catch (\Exception $e) { - // Rollback installation - $this->io->writeError('Plugin initialization failed, uninstalling plugin'); - parent::uninstall($repo, $target); - throw $e; + $promise = parent::update($repo, $initial, $target); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); } + + $pluginManager = $this->composer->getPluginManager(); + $self = $this; + return $promise->then(function () use ($self, $pluginManager, $initial, $target, $repo) { + try { + $pluginManager->deactivatePackage($initial, true); + $pluginManager->registerPackage($target, true); + } catch (\Exception $e) { + $self->rollbackInstall($e, $repo, $target); + } + }); } public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) { $this->composer->getPluginManager()->uninstallPackage($package, true); + + return parent::uninstall($repo, $package); + } + + /** + * TODO v3 should make this private once we can drop PHP 5.3 support + * @private + */ + public function rollbackInstall(\Exception $e, InstalledRepositoryInterface $repo, PackageInterface $package) + { + $this->io->writeError('Plugin initialization failed ('.$e->getMessage().'), uninstalling plugin'); parent::uninstall($repo, $package); + throw $e; } } diff --git a/src/Composer/Installer/ProjectInstaller.php b/src/Composer/Installer/ProjectInstaller.php index 069c741ec..5bae889dc 100644 --- a/src/Composer/Installer/ProjectInstaller.php +++ b/src/Composer/Installer/ProjectInstaller.php @@ -29,11 +29,11 @@ class ProjectInstaller implements InstallerInterface private $downloadManager; private $filesystem; - public function __construct($installPath, DownloadManager $dm) + public function __construct($installPath, DownloadManager $dm, Filesystem $fs) { $this->installPath = rtrim(strtr($installPath, '\\', '/'), '/').'/'; $this->downloadManager = $dm; - $this->filesystem = new Filesystem; + $this->filesystem = $fs; } /** @@ -76,7 +76,7 @@ class ProjectInstaller implements InstallerInterface */ public function prepare($type, PackageInterface $package, PackageInterface $prevPackage = null) { - $this->downloadManager->prepare($type, $package, $this->installPath, $prevPackage); + return $this->downloadManager->prepare($type, $package, $this->installPath, $prevPackage); } /** @@ -84,7 +84,7 @@ class ProjectInstaller implements InstallerInterface */ public function cleanup($type, PackageInterface $package, PackageInterface $prevPackage = null) { - $this->downloadManager->cleanup($type, $package, $this->installPath, $prevPackage); + return $this->downloadManager->cleanup($type, $package, $this->installPath, $prevPackage); } /** @@ -92,7 +92,7 @@ class ProjectInstaller implements InstallerInterface */ public function install(InstalledRepositoryInterface $repo, PackageInterface $package) { - $this->downloadManager->install($package, $this->installPath); + return $this->downloadManager->install($package, $this->installPath); } /** diff --git a/src/Composer/Package/Archiver/ArchiveManager.php b/src/Composer/Package/Archiver/ArchiveManager.php index aed8ccf80..c0a75f696 100644 --- a/src/Composer/Package/Archiver/ArchiveManager.php +++ b/src/Composer/Package/Archiver/ArchiveManager.php @@ -25,7 +25,9 @@ use Composer\Json\JsonFile; */ class ArchiveManager { + /** @var DownloadManager */ protected $downloadManager; + /** @var Loop */ protected $loop; /** diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index baf3a2292..09190f9e7 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -233,7 +233,7 @@ abstract class BasePackage implements PackageInterface } // if source reference is a sha1 hash -- truncate - if ($truncate && \strlen($reference) === 40) { + if ($truncate && \strlen($reference) === 40 && $this->getSourceType() !== 'svn') { return $this->getPrettyVersion() . ' ' . substr($reference, 0, 7); } diff --git a/src/Composer/Package/Loader/RootPackageLoader.php b/src/Composer/Package/Loader/RootPackageLoader.php index 3dfe3b7d2..ac3d8f433 100644 --- a/src/Composer/Package/Loader/RootPackageLoader.php +++ b/src/Composer/Package/Loader/RootPackageLoader.php @@ -58,7 +58,7 @@ class RootPackageLoader extends ArrayLoader $this->manager = $manager; $this->config = $config; - $this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor(), $this->versionParser); + $this->versionGuesser = $versionGuesser ?: new VersionGuesser($config, new ProcessExecutor($io), $this->versionParser); $this->io = $io; } diff --git a/src/Composer/Package/Locker.php b/src/Composer/Package/Locker.php index 367c5e37b..8cc3d1eb8 100644 --- a/src/Composer/Package/Locker.php +++ b/src/Composer/Package/Locker.php @@ -57,7 +57,7 @@ class Locker * @param InstallationManager $installationManager installation manager instance * @param string $composerFileContents The contents of the composer file */ - public function __construct(IOInterface $io, JsonFile $lockFile, InstallationManager $installationManager, $composerFileContents) + public function __construct(IOInterface $io, JsonFile $lockFile, InstallationManager $installationManager, $composerFileContents, ProcessExecutor $process = null) { $this->lockFile = $lockFile; $this->installationManager = $installationManager; @@ -65,7 +65,7 @@ class Locker $this->contentHash = self::getContentHash($composerFileContents); $this->loader = new ArrayLoader(null, true); $this->dumper = new ArrayDumper(); - $this->process = new ProcessExecutor($io); + $this->process = $process ?: new ProcessExecutor($io); } /** diff --git a/src/Composer/Plugin/PluginManager.php b/src/Composer/Plugin/PluginManager.php index 8a7ca86c8..c910098fe 100644 --- a/src/Composer/Plugin/PluginManager.php +++ b/src/Composer/Plugin/PluginManager.php @@ -34,15 +34,23 @@ use Composer\Util\PackageSorter; */ class PluginManager { + /** @var Composer */ protected $composer; + /** @var IOInterface */ protected $io; + /** @var Composer */ protected $globalComposer; + /** @var VersionParser */ protected $versionParser; + /** @var bool */ protected $disablePlugins = false; + /** @var array */ protected $plugins = array(); + /** @var array */ protected $registeredPlugins = array(); + /** @var int */ private static $classCounter = 0; /** diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index c2751da02..f7b523b94 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -32,18 +32,32 @@ class PreFileDownloadEvent extends Event */ private $processedUrl; + /** + * @var string + */ + private $type; + + /** + * @var mixed + */ + private $context; + /** * Constructor. * - * @param string $name The event name - * @param HttpDownloader $httpDownloader - * @param string $processedUrl + * @param string $name The event name + * @param HttpDownloader $httpDownloader + * @param string $processedUrl + * @param string $type + * @param mixed $context */ - public function __construct($name, HttpDownloader $httpDownloader, $processedUrl) + public function __construct($name, HttpDownloader $httpDownloader, $processedUrl, $type, $context = null) { parent::__construct($name); $this->httpDownloader = $httpDownloader; $this->processedUrl = $processedUrl; + $this->type = $type; + $this->context = $context; } /** @@ -55,7 +69,7 @@ class PreFileDownloadEvent extends Event } /** - * Retrieves the processed URL this remote filesystem will be used for + * Retrieves the processed URL that will be downloaded. * * @return string */ @@ -63,4 +77,36 @@ class PreFileDownloadEvent extends Event { return $this->processedUrl; } + + /** + * Sets the processed URL that will be downloaded. + * + * @param string $processedUrl New processed URL + */ + public function setProcessedUrl($processedUrl) + { + $this->processedUrl = $processedUrl; + } + + /** + * Returns the type of this download (package, metadata). + * + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * Returns the context of this download, if any. + * + * If this download is of type package, the package object is returned. + * + * @return mixed + */ + public function getContext() + { + return $this->context; + } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index e960f63be..f7462c89f 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -23,6 +23,7 @@ use Composer\Config; use Composer\Composer; use Composer\Factory; use Composer\IO\IOInterface; +use Composer\Semver\CompilingMatcher; use Composer\Util\HttpDownloader; use Composer\Util\Loop; use Composer\Plugin\PluginEvents; @@ -764,7 +765,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito continue; } - if ($constraint && !$constraint->matches(new Constraint('==', $version))) { + if ($constraint && !CompilingMatcher::match($constraint, Constraint::OP_EQ, $version)) { continue; } @@ -1013,8 +1014,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito while ($retries--) { try { if ($this->eventDispatcher) { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename, 'metadata'); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $filename = $preFileDownloadEvent->getProcessedUrl(); } $response = $this->httpDownloader->get($filename, $this->options); @@ -1099,8 +1101,9 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito while ($retries--) { try { if ($this->eventDispatcher) { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename, 'metadata'); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $filename = $preFileDownloadEvent->getProcessedUrl(); } $options = $this->options; @@ -1155,18 +1158,19 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $retries = 3; if (isset($this->packagesNotFoundCache[$filename])) { - return new Promise(function ($resolve, $reject) { $resolve(array('packages' => array())); }); + return \React\Promise\resolve(array('packages' => array())); } if (isset($this->freshMetadataUrls[$filename]) && $lastModifiedTime) { // make it look like we got a 304 response - return new Promise(function ($resolve, $reject) { $resolve(true); }); + return \React\Promise\resolve(true); } $httpDownloader = $this->httpDownloader; if ($this->eventDispatcher) { - $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); + $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename, 'metadata'); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $filename = $preFileDownloadEvent->getProcessedUrl(); } $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array(); diff --git a/src/Composer/Repository/PlatformRepository.php b/src/Composer/Repository/PlatformRepository.php index e003976a1..c88604a3b 100644 --- a/src/Composer/Repository/PlatformRepository.php +++ b/src/Composer/Repository/PlatformRepository.php @@ -30,6 +30,7 @@ class PlatformRepository extends ArrayRepository { const PLATFORM_PACKAGE_REGEX = '{^(?:php(?:-64bit|-ipv6|-zts|-debug)?|hhvm|(?:ext|lib)-[a-z0-9](?:[_.-]?[a-z0-9]+)*|composer-(?:plugin|runtime)-api)$}iD'; + private static $hhvmVersion; private $versionParser; /** @@ -45,7 +46,7 @@ class PlatformRepository extends ArrayRepository public function __construct(array $packages = array(), array $overrides = array(), ProcessExecutor $process = null) { - $this->process = $process === null ? (new ProcessExecutor()) : $process; + $this->process = $process; foreach ($overrides as $name => $version) { $this->overrides[strtolower($name)] = array('name' => $name, 'version' => $version); } @@ -251,22 +252,7 @@ class PlatformRepository extends ArrayRepository $this->addPackage($lib); } - $hhvmVersion = defined('HHVM_VERSION') ? HHVM_VERSION : null; - if ($hhvmVersion === null && !Platform::isWindows()) { - $finder = new ExecutableFinder(); - $hhvm = $finder->find('hhvm'); - if ($hhvm !== null) { - $exitCode = $this->process->execute( - ProcessExecutor::escape($hhvm). - ' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null', - $hhvmVersion - ); - if ($exitCode !== 0) { - $hhvmVersion = null; - } - } - } - if ($hhvmVersion) { + if ($hhvmVersion = self::getHHVMVersion($this->process)) { try { $prettyVersion = $hhvmVersion; $version = $this->versionParser->normalize($prettyVersion); @@ -362,4 +348,29 @@ class PlatformRepository extends ArrayRepository { return 'ext-' . str_replace(' ', '-', $name); } + + private static function getHHVMVersion(ProcessExecutor $process = null) + { + if (null !== self::$hhvmVersion) { + return self::$hhvmVersion ?: null; + } + + self::$hhvmVersion = defined('HHVM_VERSION') ? HHVM_VERSION : null; + if (self::$hhvmVersion === null && !Platform::isWindows()) { + self::$hhvmVersion = false; + $finder = new ExecutableFinder(); + $hhvmPath = $finder->find('hhvm'); + if ($hhvmPath !== null) { + $process = $process ?: new ProcessExecutor(); + $exitCode = $process->execute( + ProcessExecutor::escape($hhvmPath). + ' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null', + self::$hhvmVersion + ); + if ($exitCode !== 0) { + self::$hhvmVersion = false; + } + } + } + } } diff --git a/src/Composer/Repository/RepositoryFactory.php b/src/Composer/Repository/RepositoryFactory.php index 97a8ee957..b36664187 100644 --- a/src/Composer/Repository/RepositoryFactory.php +++ b/src/Composer/Repository/RepositoryFactory.php @@ -17,6 +17,7 @@ use Composer\IO\IOInterface; use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\Util\HttpDownloader; +use Composer\Util\ProcessExecutor; use Composer\Json\JsonFile; /** @@ -114,9 +115,9 @@ class RepositoryFactory * @param HttpDownloader $httpDownloader * @return RepositoryManager */ - public static function manager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) + public static function manager(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, ProcessExecutor $process = null) { - $rm = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher); + $rm = new RepositoryManager($io, $config, $httpDownloader, $eventDispatcher, $process); $rm->setRepositoryClass('composer', 'Composer\Repository\ComposerRepository'); $rm->setRepositoryClass('vcs', 'Composer\Repository\VcsRepository'); $rm->setRepositoryClass('package', 'Composer\Repository\PackageRepository'); diff --git a/src/Composer/Repository/RepositoryManager.php b/src/Composer/Repository/RepositoryManager.php index 264115c10..717fce4ee 100644 --- a/src/Composer/Repository/RepositoryManager.php +++ b/src/Composer/Repository/RepositoryManager.php @@ -17,6 +17,7 @@ use Composer\Config; use Composer\EventDispatcher\EventDispatcher; use Composer\Package\PackageInterface; use Composer\Util\HttpDownloader; +use Composer\Util\ProcessExecutor; /** * Repositories manager. @@ -27,20 +28,30 @@ use Composer\Util\HttpDownloader; */ class RepositoryManager { + /** @var InstalledRepositoryInterface */ private $localRepository; + /** @var list */ private $repositories = array(); + /** @var array */ private $repositoryClasses = array(); + /** @var IOInterface */ private $io; + /** @var Config */ private $config; - private $eventDispatcher; + /** @var HttpDownloader */ private $httpDownloader; + /** @var ?EventDispatcher */ + private $eventDispatcher; + /** @var ProcessExecutor */ + private $process; - public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $eventDispatcher = null, ProcessExecutor $process = null) { $this->io = $io; $this->config = $config; $this->httpDownloader = $httpDownloader; $this->eventDispatcher = $eventDispatcher; + $this->process = $process ?: new ProcessExecutor($io); } /** @@ -130,7 +141,7 @@ class RepositoryManager unset($config['only'], $config['exclude'], $config['canonical']); } - $repository = new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher); + $repository = new $class($config, $this->io, $this->config, $this->httpDownloader, $this->eventDispatcher, $this->process); if (isset($filterConfig)) { $repository = new FilterRepository($repository, $filterConfig); diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 24d935f4a..9417dec97 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -19,6 +19,7 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; use Composer\IO\NullIO; use Composer\Package\BasePackage; +use Composer\Package\AliasPackage; use Composer\Package\Version\VersionParser; use Composer\Repository\CompositeRepository; use Composer\Repository\PlatformRepository; @@ -44,7 +45,7 @@ class RepositorySet /** * @var array[] - * @psalm-var list + * @psalm-var array> */ private $rootAliases; @@ -91,7 +92,7 @@ class RepositorySet */ public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array()) { - $this->rootAliases = $rootAliases; + $this->rootAliases = self::getRootAliasesPerPackage($rootAliases); $this->rootReferences = $rootReferences; $this->acceptableStabilities = array(); @@ -249,8 +250,22 @@ class RepositorySet $packages = array(); foreach ($this->repositories as $repository) { - $packages = array_merge($packages, $repository->getPackages()); + foreach ($repository->getPackages() as $package) { + $packages[] = $package; + + if (isset($this->rootAliases[$package->getName()][$package->getVersion()])) { + $alias = $this->rootAliases[$package->getName()][$package->getVersion()]; + while ($package instanceof AliasPackage) { + $package = $package->getAliasOf(); + } + $aliasPackage = new AliasPackage($package, $alias['alias_normalized'], $alias['alias']); + $aliasPackage->setRootPackageAlias(true); + $packages[] = $aliasPackage; + } + + } } + return new Pool($packages); } @@ -270,4 +285,18 @@ class RepositorySet return $this->createPool($request, new NullIO()); } + + private static function getRootAliasesPerPackage(array $aliases) + { + $normalizedAliases = array(); + + foreach ($aliases as $alias) { + $normalizedAliases[$alias['package']][$alias['version']] = array( + 'alias' => $alias['alias'], + 'alias_normalized' => $alias['alias_normalized'], + ); + } + + return $normalizedAliases; + } } diff --git a/src/Composer/Repository/Vcs/GitDriver.php b/src/Composer/Repository/Vcs/GitDriver.php index 1b96c4cde..faba0fd1e 100644 --- a/src/Composer/Repository/Vcs/GitDriver.php +++ b/src/Composer/Repository/Vcs/GitDriver.php @@ -223,7 +223,6 @@ class GitDriver extends VcsDriver } $process = new ProcessExecutor($io); - return $process->execute('git ls-remote --heads ' . ProcessExecutor::escape($url), $output) === 0; } } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 7b609a80c..137989cda 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -338,14 +338,12 @@ class GitHubDriver extends VcsDriver $this->branches = array(); $resource = $this->getApiUrl() . '/repos/'.$this->owner.'/'.$this->repository.'/git/refs/heads?per_page=100'; - $branchBlacklist = array('gh-pages'); - do { $response = $this->getContents($resource); $branchData = $response->decodeJson(); foreach ($branchData as $branch) { $name = substr($branch['ref'], 11); - if (!in_array($name, $branchBlacklist)) { + if ($name !== 'gh-pages') { $this->branches[$name] = $branch['object']['sha']; } } diff --git a/src/Composer/Repository/Vcs/HgDriver.php b/src/Composer/Repository/Vcs/HgDriver.php index 04a363442..62e3e4a05 100644 --- a/src/Composer/Repository/Vcs/HgDriver.php +++ b/src/Composer/Repository/Vcs/HgDriver.php @@ -228,8 +228,8 @@ class HgDriver extends VcsDriver return false; } - $processExecutor = new ProcessExecutor($io); - $exit = $processExecutor->execute(sprintf('hg identify %s', ProcessExecutor::escape($url)), $ignored); + $process = new ProcessExecutor($io); + $exit = $process->execute(sprintf('hg identify %s', ProcessExecutor::escape($url)), $ignored); return $exit === 0; } diff --git a/src/Composer/Repository/Vcs/SvnDriver.php b/src/Composer/Repository/Vcs/SvnDriver.php index a8f0c4ad4..097103487 100644 --- a/src/Composer/Repository/Vcs/SvnDriver.php +++ b/src/Composer/Repository/Vcs/SvnDriver.php @@ -307,9 +307,8 @@ class SvnDriver extends VcsDriver return false; } - $processExecutor = new ProcessExecutor($io); - - $exit = $processExecutor->execute( + $process = new ProcessExecutor($io); + $exit = $process->execute( "svn info --non-interactive ".ProcessExecutor::escape($url), $ignoredOutput ); @@ -320,14 +319,14 @@ class SvnDriver extends VcsDriver } // Subversion client 1.7 and older - if (false !== stripos($processExecutor->getErrorOutput(), 'authorization failed:')) { + if (false !== stripos($process->getErrorOutput(), 'authorization failed:')) { // This is likely a remote Subversion repository that requires // authentication. We will handle actual authentication later. return true; } // Subversion client 1.8 and newer - if (false !== stripos($processExecutor->getErrorOutput(), 'Authentication failed')) { + if (false !== stripos($process->getErrorOutput(), 'Authentication failed')) { // This is likely a remote Subversion or newer repository that requires // authentication. We will handle actual authentication later. return true; diff --git a/src/Composer/Repository/VcsRepository.php b/src/Composer/Repository/VcsRepository.php index 95bb422d2..1351c4694 100644 --- a/src/Composer/Repository/VcsRepository.php +++ b/src/Composer/Repository/VcsRepository.php @@ -53,7 +53,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt private $emptyReferences = array(); private $versionTransportExceptions = array(); - public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null, array $drivers = null, VersionCacheInterface $versionCache = null) + public function __construct(array $repoConfig, IOInterface $io, Config $config, HttpDownloader $httpDownloader, EventDispatcher $dispatcher = null, ProcessExecutor $process = null, array $drivers = null, VersionCacheInterface $versionCache = null) { parent::__construct(); $this->drivers = $drivers ?: array( @@ -78,7 +78,7 @@ class VcsRepository extends ArrayRepository implements ConfigurableRepositoryInt $this->repoConfig = $repoConfig; $this->versionCache = $versionCache; $this->httpDownloader = $httpDownloader; - $this->processExecutor = new ProcessExecutor($io); + $this->processExecutor = $process ?: new ProcessExecutor($io); } public function getRepoName() diff --git a/src/Composer/SelfUpdate/Versions.php b/src/Composer/SelfUpdate/Versions.php index a03a75054..6d1379d41 100644 --- a/src/Composer/SelfUpdate/Versions.php +++ b/src/Composer/SelfUpdate/Versions.php @@ -63,7 +63,11 @@ class Versions public function getLatest($channel = null) { - $protocol = extension_loaded('openssl') ? 'https' : 'http'; + if ($this->config->get('disable-tls') === true) { + $protocol = 'http'; + } else { + $protocol = 'https'; + } $versions = $this->httpDownloader->get($protocol . '://getcomposer.org/versions')->decodeJson(); foreach ($versions[$channel ?: $this->getChannel()] as $version) { diff --git a/src/Composer/Util/Filesystem.php b/src/Composer/Util/Filesystem.php index fd7ded57e..d5d818556 100644 --- a/src/Composer/Util/Filesystem.php +++ b/src/Composer/Util/Filesystem.php @@ -28,7 +28,7 @@ class Filesystem public function __construct(ProcessExecutor $executor = null) { - $this->processExecutor = $executor ?: new ProcessExecutor(); + $this->processExecutor = $executor; } public function remove($file) @@ -320,7 +320,7 @@ class Filesystem if (Platform::isWindows()) { // Try to copy & delete - this is a workaround for random "Access denied" errors. $command = sprintf('xcopy %s %s /E /I /Q /Y', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); - $result = $this->processExecutor->execute($command, $output); + $result = $this->getProcess()->execute($command, $output); // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache(); @@ -334,7 +334,7 @@ class Filesystem // We do not use PHP's "rename" function here since it does not support // the case where $source, and $target are located on different partitions. $command = sprintf('mv %s %s', ProcessExecutor::escape($source), ProcessExecutor::escape($target)); - $result = $this->processExecutor->execute($command, $output); + $result = $this->getProcess()->execute($command, $output); // clear stat cache because external processes aren't tracked by the php stat cache clearstatcache(); @@ -546,6 +546,10 @@ class Filesystem */ protected function getProcess() { + if (!$this->processExecutor) { + $this->processExecutor = new ProcessExecutor(); + } + return $this->processExecutor; } diff --git a/src/Composer/Util/Http/CurlDownloader.php b/src/Composer/Util/Http/CurlDownloader.php index 365caf899..257a29115 100644 --- a/src/Composer/Util/Http/CurlDownloader.php +++ b/src/Composer/Util/Http/CurlDownloader.php @@ -23,6 +23,7 @@ use Composer\Util\HttpDownloader; use React\Promise\Promise; /** + * @internal * @author Jordi Boggiano * @author Nicolas Grekas */ @@ -90,6 +91,9 @@ class CurlDownloader $this->authHelper = new AuthHelper($io, $config); } + /** + * @return int internal job id + */ public function download($resolve, $reject, $origin, $url, $options, $copyTo = null) { $attributes = array(); @@ -101,6 +105,9 @@ class CurlDownloader return $this->initDownload($resolve, $reject, $origin, $url, $options, $copyTo, $attributes); } + /** + * @return int internal job id + */ private function initDownload($resolve, $reject, $origin, $url, $options, $copyTo = null, array $attributes = array()) { $attributes = array_merge(array( @@ -199,8 +206,29 @@ class CurlDownloader } $this->checkCurlResult(curl_multi_add_handle($this->multiHandle, $curlHandle)); -// TODO progress + // TODO progress //$params['notification'](STREAM_NOTIFY_RESOLVE, STREAM_NOTIFY_SEVERITY_INFO, '', 0, 0, 0, false); + + return (int) $curlHandle; + } + + public function abortRequest($id) + { + if (isset($this->jobs[$id]) && isset($this->jobs[$id]['handle'])) { + $job = $this->jobs[$id]; + curl_multi_remove_handle($this->multiHandle, $job['handle']); + curl_close($job['handle']); + if (is_resource($job['headerHandle'])) { + fclose($job['headerHandle']); + } + if (is_resource($job['bodyHandle'])) { + fclose($job['bodyHandle']); + } + if ($job['filename']) { + @unlink($job['filename'].'~'); + } + unset($this->jobs[$id]); + } } public function tick() @@ -235,7 +263,7 @@ class CurlDownloader $statusCode = null; $response = null; try { -// TODO progress + // TODO progress //$this->onProgress($curlHandle, $job['callback'], $progress, $job['progress']); if (CURLE_OK !== $errno || $error) { throw new TransportException($error); @@ -285,8 +313,6 @@ class CurlDownloader // fail 4xx and 5xx responses and capture the response if ($statusCode >= 400 && $statusCode <= 599) { throw $this->failResponse($job, $response, $response->getStatusMessage()); -// TODO progress -// $this->io->overwriteError("Downloading (failed)", false); } if ($job['attributes']['storeAuth']) { diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 2fa8fa716..889fae07e 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -31,6 +31,7 @@ class HttpDownloader const STATUS_STARTED = 2; const STATUS_COMPLETED = 3; const STATUS_FAILED = 4; + const STATUS_ABORTED = 5; private $io; private $config; @@ -44,6 +45,7 @@ class HttpDownloader private $rfs; private $idGen = 0; private $disabled; + private $allowAsync = false; /** * @param IOInterface $io The IO instance @@ -139,6 +141,10 @@ class HttpDownloader 'origin' => Url::getOrigin($this->config, $request['url']), ); + if (!$sync && !$this->allowAsync) { + throw new \LogicException('You must use the HttpDownloader instance which is part of a Composer\Loop instance to be able to run async http requests'); + } + // capture username/password from URL if there is one if (preg_match('{^https?://([^:/]+):([^@/]+)@([^/]+)}i', $request['url'], $match)) { $this->io->setAuthentication($job['origin'], rawurldecode($match[1]), rawurldecode($match[2])); @@ -179,8 +185,20 @@ class HttpDownloader $downloader = $this; $io = $this->io; + $curl = $this->curl; - $canceler = function () {}; + $canceler = function () use (&$job, $curl) { + if ($job['status'] === self::STATUS_QUEUED) { + $job['status'] = self::STATUS_ABORTED; + } + if ($job['status'] !== self::STATUS_STARTED) { + return; + } + $job['status'] = self::STATUS_ABORTED; + if (isset($job['curl_id'])) { + $curl->abortRequest($job['curl_id']); + } + }; $promise = new Promise($resolver, $canceler); $promise->then(function ($response) use (&$job, $downloader) { @@ -189,7 +207,6 @@ class HttpDownloader // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped $downloader->markJobDone(); - $downloader->scheduleNextJob(); return $response; }, function ($e) use (&$job, $downloader) { @@ -197,7 +214,6 @@ class HttpDownloader $job['exception'] = $e; $downloader->markJobDone(); - $downloader->scheduleNextJob(); throw $e; }); @@ -239,9 +255,9 @@ class HttpDownloader } if ($job['request']['copyTo']) { - $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']); + $job['curl_id'] = $this->curl->download($resolve, $reject, $origin, $url, $options, $job['request']['copyTo']); } else { - $this->curl->download($resolve, $reject, $origin, $url, $options); + $job['curl_id'] = $this->curl->download($resolve, $reject, $origin, $url, $options); } } @@ -253,51 +269,60 @@ class HttpDownloader $this->runningJobs--; } - /** - * @private - */ - public function scheduleNextJob() - { - foreach ($this->jobs as $job) { - if ($job['status'] === self::STATUS_QUEUED) { - $this->startJob($job['id']); - if ($this->runningJobs >= $this->maxJobs) { - return; - } - } - } - } - - public function wait($index = null, $progress = false) + public function wait($index = null) { while (true) { - if ($this->curl) { - $this->curl->tick(); - } - - if (null !== $index) { - if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) { - return; - } - } else { - $done = true; - foreach ($this->jobs as $job) { - if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) { - $done = false; - break; - } elseif (!$job['sync']) { - unset($this->jobs[$job['id']]); - } - } - if ($done) { - return; - } + if (!$this->countActiveJobs($index)) { + return; } usleep(1000); } } + /** + * @internal + */ + public function enableAsync() + { + $this->allowAsync = true; + } + + /** + * @internal + * + * @return int number of active (queued or started) jobs + */ + public function countActiveJobs($index = null) + { + if ($this->runningJobs < $this->maxJobs) { + foreach ($this->jobs as $job) { + if ($job['status'] === self::STATUS_QUEUED && $this->runningJobs < $this->maxJobs) { + $this->startJob($job['id']); + } + } + } + + if ($this->curl) { + $this->curl->tick(); + } + + if (null !== $index) { + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0; + } + + $active = 0; + foreach ($this->jobs as $job) { + if ($job['status'] < self::STATUS_COMPLETED) { + $active++; + } elseif (!$job['sync']) { + unset($this->jobs[$job['id']]); + } + } + + return $active; + } + private function getResponse($index) { if (!isset($this->jobs[$index])) { diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index dfaa2ac53..00159d562 100644 --- a/src/Composer/Util/Loop.php +++ b/src/Composer/Util/Loop.php @@ -14,6 +14,7 @@ namespace Composer\Util; use Composer\Util\HttpDownloader; use React\Promise\Promise; +use Symfony\Component\Console\Helper\ProgressBar; /** * @author Jordi Boggiano @@ -21,13 +22,22 @@ use React\Promise\Promise; class Loop { private $httpDownloader; + private $processExecutor; + private $currentPromises; - public function __construct(HttpDownloader $httpDownloader) + public function __construct(HttpDownloader $httpDownloader = null, ProcessExecutor $processExecutor = null) { $this->httpDownloader = $httpDownloader; + if ($this->httpDownloader) { + $this->httpDownloader->enableAsync(); + } + $this->processExecutor = $processExecutor; + if ($this->processExecutor) { + $this->processExecutor->enableAsync(); + } } - public function wait(array $promises) + public function wait(array $promises, ProgressBar $progress = null) { /** @var \Exception|null */ $uncaught = null; @@ -39,10 +49,52 @@ class Loop } ); - $this->httpDownloader->wait(); + $this->currentPromises = $promises; + if ($progress) { + $totalJobs = 0; + if ($this->httpDownloader) { + $totalJobs += $this->httpDownloader->countActiveJobs(); + } + if ($this->processExecutor) { + $totalJobs += $this->processExecutor->countActiveJobs(); + } + $progress->start($totalJobs); + } + + while (true) { + $activeJobs = 0; + + if ($this->httpDownloader) { + $activeJobs += $this->httpDownloader->countActiveJobs(); + } + if ($this->processExecutor) { + $activeJobs += $this->processExecutor->countActiveJobs(); + } + + if ($progress) { + $progress->setProgress($progress->getMaxSteps() - $activeJobs); + } + + if (!$activeJobs) { + break; + } + + usleep(5000); + } + + $this->currentPromises = null; if ($uncaught) { throw $uncaught; } } + + public function abortJobs() + { + if ($this->currentPromises) { + foreach ($this->currentPromises as $promise) { + $promise->cancel(); + } + } + } } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index a30a04d15..96b9235c8 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -16,18 +16,32 @@ use Composer\IO\IOInterface; use Symfony\Component\Process\Process; use Symfony\Component\Process\ProcessUtils; use Symfony\Component\Process\Exception\RuntimeException; +use React\Promise\Promise; /** * @author Robert Schƶnthal + * @author Jordi Boggiano */ class ProcessExecutor { + const STATUS_QUEUED = 1; + const STATUS_STARTED = 2; + const STATUS_COMPLETED = 3; + const STATUS_FAILED = 4; + const STATUS_ABORTED = 5; + protected static $timeout = 300; protected $captureOutput; protected $errorOutput; protected $io; + private $jobs = array(); + private $runningJobs = 0; + private $maxJobs = 10; + private $idGen = 0; + private $allowAsync = false; + public function __construct(IOInterface $io = null) { $this->io = $io; @@ -112,6 +126,192 @@ class ProcessExecutor return $process->getExitCode(); } + /** + * starts a process on the commandline in async mode + * + * @param string $command the command to execute + * @param mixed $output the output will be written into this var if passed by ref + * if a callable is passed it will be used as output handler + * @param string $cwd the working directory + * @return int statuscode + */ + public function executeAsync($command, $cwd = null) + { + if (!$this->allowAsync) { + throw new \LogicException('You must use the ProcessExecutor instance which is part of a Composer\Loop instance to be able to run async processes'); + } + + $job = array( + 'id' => $this->idGen++, + 'status' => self::STATUS_QUEUED, + 'command' => $command, + 'cwd' => $cwd, + ); + + $resolver = function ($resolve, $reject) use (&$job) { + $job['status'] = ProcessExecutor::STATUS_QUEUED; + $job['resolve'] = $resolve; + $job['reject'] = $reject; + }; + + $self = $this; + $io = $this->io; + + $canceler = function () use (&$job) { + if ($job['status'] === self::STATUS_QUEUED) { + $job['status'] = self::STATUS_ABORTED; + } + if ($job['status'] !== self::STATUS_STARTED) { + return; + } + $job['status'] = self::STATUS_ABORTED; + try { + if (defined('SIGINT')) { + $job['process']->signal(SIGINT); + } + } catch (\Exception $e) { + // signal can throw in various conditions, but we don't care if it fails + } + $job['process']->stop(1); + }; + + $promise = new Promise($resolver, $canceler); + $promise = $promise->then(function () use (&$job, $self) { + if ($job['process']->isSuccessful()) { + $job['status'] = ProcessExecutor::STATUS_COMPLETED; + } else { + $job['status'] = ProcessExecutor::STATUS_FAILED; + } + + // TODO 3.0 this should be done directly on $this when PHP 5.3 is dropped + $self->markJobDone(); + + return $job['process']; + }, function ($e) use (&$job, $self) { + $job['status'] = ProcessExecutor::STATUS_FAILED; + + $self->markJobDone(); + + throw $e; + }); + $this->jobs[$job['id']] =& $job; + + if ($this->runningJobs < $this->maxJobs) { + $this->startJob($job['id']); + } + + return $promise; + } + + private function startJob($id) + { + $job =& $this->jobs[$id]; + if ($job['status'] !== self::STATUS_QUEUED) { + return; + } + + // start job + $job['status'] = self::STATUS_STARTED; + $this->runningJobs++; + + $command = $job['command']; + $cwd = $job['cwd']; + + if ($this->io && $this->io->isDebug()) { + $safeCommand = preg_replace_callback('{://(?P[^:/\s]+):(?P[^@\s/]+)@}i', function ($m) { + if (preg_match('{^[a-f0-9]{12,}$}', $m['user'])) { + return '://***:***@'; + } + + return '://'.$m['user'].':***@'; + }, $command); + $safeCommand = preg_replace("{--password (.*[^\\\\]\') }", '--password \'***\' ', $safeCommand); + $this->io->writeError('Executing async command ('.($cwd ?: 'CWD').'): '.$safeCommand); + } + + // make sure that null translate to the proper directory in case the dir is a symlink + // and we call a git command, because msysgit does not handle symlinks properly + if (null === $cwd && Platform::isWindows() && false !== strpos($command, 'git') && getcwd()) { + $cwd = realpath(getcwd()); + } + + // TODO in v3, commands should be passed in as arrays of cmd + args + if (method_exists('Symfony\Component\Process\Process', 'fromShellCommandline')) { + $process = Process::fromShellCommandline($command, $cwd, null, null, static::getTimeout()); + } else { + $process = new Process($command, $cwd, null, null, static::getTimeout()); + } + + $job['process'] = $process; + + $process->start(); + } + + public function wait($index = null) + { + while (true) { + if (!$this->countActiveJobs($index)) { + return; + } + + usleep(1000); + } + } + + /** + * @internal + */ + public function enableAsync() + { + $this->allowAsync = true; + } + + /** + * @internal + * + * @return int number of active (queued or started) jobs + */ + public function countActiveJobs($index = null) + { + // tick + foreach ($this->jobs as $job) { + if ($job['status'] === self::STATUS_STARTED) { + if (!$job['process']->isRunning()) { + call_user_func($job['resolve'], $job['process']); + } + } + + if ($this->runningJobs < $this->maxJobs) { + if ($job['status'] === self::STATUS_QUEUED) { + $this->startJob($job['id']); + } + } + } + + if (null !== $index) { + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0; + } + + $active = 0; + foreach ($this->jobs as $job) { + if ($job['status'] < self::STATUS_COMPLETED) { + $active++; + } else { + unset($this->jobs[$job['id']]); + } + } + + return $active; + } + + /** + * @private + */ + public function markJobDone() + { + $this->runningJobs--; + } + public function splitLines($output) { $output = trim($output); diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index c7077afbb..4bac6a88f 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -20,6 +20,7 @@ use Composer\Util\HttpDownloader; use Composer\Util\Http\Response; /** + * @internal * @author FranƧois Pluchino * @author Jordi Boggiano * @author Nils Adermann @@ -54,8 +55,9 @@ class RemoteFilesystem * @param Config $config The config * @param array $options The options * @param bool $disableTls + * @param AuthHelper $authHelper */ - public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false) + public function __construct(IOInterface $io, Config $config, array $options = array(), $disableTls = false, AuthHelper $authHelper = null) { $this->io = $io; @@ -70,7 +72,7 @@ class RemoteFilesystem // handle the other externally set options normally. $this->options = array_replace_recursive($this->options, $options); $this->config = $config; - $this->authHelper = new AuthHelper($io, $config); + $this->authHelper = isset($authHelper) ? $authHelper : new AuthHelper($io, $config); } /** diff --git a/tests/Composer/Test/Downloader/FileDownloaderTest.php b/tests/Composer/Test/Downloader/FileDownloaderTest.php index c86ffa2f7..ba8f95db9 100644 --- a/tests/Composer/Test/Downloader/FileDownloaderTest.php +++ b/tests/Composer/Test/Downloader/FileDownloaderTest.php @@ -139,8 +139,8 @@ class FileDownloaderTest extends TestCase ->will($this->returnValue($path.'/vendor')); try { - $promise = $downloader->download($packageMock, $path); $loop = new Loop($this->httpDownloader); + $promise = $downloader->download($packageMock, $path); $loop->wait(array($promise)); $this->fail('Download was expected to throw'); @@ -225,8 +225,8 @@ class FileDownloaderTest extends TestCase touch($dlFile); try { - $promise = $downloader->download($packageMock, $path); $loop = new Loop($this->httpDownloader); + $promise = $downloader->download($packageMock, $path); $loop->wait(array($promise)); $this->fail('Download was expected to throw'); @@ -296,8 +296,8 @@ class FileDownloaderTest extends TestCase mkdir(dirname($dlFile), 0777, true); touch($dlFile); - $promise = $downloader->download($newPackage, $path, $oldPackage); $loop = new Loop($this->httpDownloader); + $promise = $downloader->download($newPackage, $path, $oldPackage); $loop->wait(array($promise)); $downloader->update($oldPackage, $newPackage, $path); diff --git a/tests/Composer/Test/Downloader/XzDownloaderTest.php b/tests/Composer/Test/Downloader/XzDownloaderTest.php index f770b0d35..6996d67f6 100644 --- a/tests/Composer/Test/Downloader/XzDownloaderTest.php +++ b/tests/Composer/Test/Downloader/XzDownloaderTest.php @@ -70,8 +70,8 @@ class XzDownloaderTest extends TestCase $downloader = new XzDownloader($io, $config, $httpDownloader = new HttpDownloader($io, $this->getMockBuilder('Composer\Config')->getMock()), null, null, null); try { - $promise = $downloader->download($packageMock, $this->testDir.'/install-path'); $loop = new Loop($httpDownloader); + $promise = $downloader->download($packageMock, $this->testDir.'/install-path'); $loop->wait(array($promise)); $downloader->install($packageMock, $this->testDir.'/install-path'); diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index e3bbe45a8..dc0f55aec 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -60,9 +60,6 @@ class ZipDownloaderTest extends TestCase } } - /** - * @group only - */ public function testErrorMessages() { if (!class_exists('ZipArchive')) { @@ -92,8 +89,8 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', false); try { - $promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test'); $loop = new Loop($this->httpDownloader); + $promise = $downloader->download($this->package, $path = sys_get_temp_dir().'/composer-zip-test'); $loop->wait(array($promise)); $downloader->install($this->package, $path); @@ -125,7 +122,8 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(false)); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); } /** @@ -150,12 +148,10 @@ class ZipDownloaderTest extends TestCase ->will($this->throwException(new \ErrorException('Not a directory'))); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); } - /** - * @group only - */ public function testZipArchiveOnlyGood() { if (!class_exists('ZipArchive')) { @@ -174,45 +170,66 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(true)); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); } /** * @expectedException \Exception - * @expectedExceptionMessage Failed to execute (1) unzip + * @expectedExceptionMessage Failed to extract : (1) unzip */ public function testSystemUnzipOnlyFailed() { - if (!class_exists('ZipArchive')) { - $this->markTestSkipped('zip extension missing'); - } - + $this->setPrivateProperty('isWindows', false); $this->setPrivateProperty('hasSystemUnzip', true); $this->setPrivateProperty('hasZipArchive', false); + + $procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock(); + $procMock->expects($this->any()) + ->method('getExitCode') + ->will($this->returnValue(1)); + $procMock->expects($this->any()) + ->method('isSuccessful') + ->will($this->returnValue(false)); + $procMock->expects($this->any()) + ->method('getErrorOutput') + ->will($this->returnValue('output')); + $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor->expects($this->at(0)) - ->method('execute') - ->will($this->returnValue(1)); + ->method('executeAsync') + ->will($this->returnValue(\React\Promise\resolve($procMock))); - $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); } public function testSystemUnzipOnlyGood() { - if (!class_exists('ZipArchive')) { - $this->markTestSkipped('zip extension missing'); - } - + $this->setPrivateProperty('isWindows', false); $this->setPrivateProperty('hasSystemUnzip', true); $this->setPrivateProperty('hasZipArchive', false); + + $procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock(); + $procMock->expects($this->any()) + ->method('getExitCode') + ->will($this->returnValue(0)); + $procMock->expects($this->any()) + ->method('isSuccessful') + ->will($this->returnValue(true)); + $procMock->expects($this->any()) + ->method('getErrorOutput') + ->will($this->returnValue('output')); + $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor->expects($this->at(0)) - ->method('execute') - ->will($this->returnValue(0)); + ->method('executeAsync') + ->will($this->returnValue(\React\Promise\resolve($procMock))); - $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); } public function testNonWindowsFallbackGood() @@ -225,10 +242,21 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', true); $this->setPrivateProperty('hasZipArchive', true); + $procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock(); + $procMock->expects($this->any()) + ->method('getExitCode') + ->will($this->returnValue(1)); + $procMock->expects($this->any()) + ->method('isSuccessful') + ->will($this->returnValue(false)); + $procMock->expects($this->any()) + ->method('getErrorOutput') + ->will($this->returnValue('output')); + $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor->expects($this->at(0)) - ->method('execute') - ->will($this->returnValue(1)); + ->method('executeAsync') + ->will($this->returnValue(\React\Promise\resolve($procMock))); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive->expects($this->at(0)) @@ -238,9 +266,10 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(true)); - $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); } /** @@ -257,10 +286,21 @@ class ZipDownloaderTest extends TestCase $this->setPrivateProperty('hasSystemUnzip', true); $this->setPrivateProperty('hasZipArchive', true); + $procMock = $this->getMockBuilder('Symfony\Component\Process\Process')->disableOriginalConstructor()->getMock(); + $procMock->expects($this->any()) + ->method('getExitCode') + ->will($this->returnValue(1)); + $procMock->expects($this->any()) + ->method('isSuccessful') + ->will($this->returnValue(false)); + $procMock->expects($this->any()) + ->method('getErrorOutput') + ->will($this->returnValue('output')); + $processExecutor = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); $processExecutor->expects($this->at(0)) - ->method('execute') - ->will($this->returnValue(1)); + ->method('executeAsync') + ->will($this->returnValue(\React\Promise\resolve($procMock))); $zipArchive = $this->getMockBuilder('ZipArchive')->getMock(); $zipArchive->expects($this->at(0)) @@ -270,9 +310,10 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(false)); - $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); } public function testWindowsFallbackGood() @@ -298,9 +339,10 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(false)); - $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); } /** @@ -330,9 +372,28 @@ class ZipDownloaderTest extends TestCase ->method('extractTo') ->will($this->returnValue(false)); - $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, $processExecutor); + $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); $this->setPrivateProperty('zipArchiveObject', $zipArchive, $downloader); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); + } + + private function wait($promise) + { + if (null === $promise) { + return; + } + + $e = null; + $promise->then(function () { + // noop + }, function ($ex) use (&$e) { + $e = $ex; + }); + + if ($e) { + throw $e; + } } } @@ -350,6 +411,6 @@ class MockedZipDownloader extends ZipDownloader public function extract(PackageInterface $package, $file, $path) { - parent::extract($package, $file, $path); + return parent::extract($package, $file, $path); } } diff --git a/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test b/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test new file mode 100644 index 000000000..28229eb87 --- /dev/null +++ b/tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test @@ -0,0 +1,90 @@ +--TEST-- +Aliases are loaded when splitting require-dev from require (https://github.com/composer/composer/issues/8954) +--COMPOSER-- +{ + "repositories": [ + { + "type": "package", + "package": [ + { + "name": "a/aliased", "version": "dev-next", "replace": { "a/aliased-replaced": "self.version" } + }, + { + "name": "b/requirer", "version": "2.3.0", + "require": { "a/aliased-replaced": "^4.0" } + }, + { + "name": "a/aliased2", "version": "dev-next", "replace": { "a/aliased-replaced2": "self.version" } + }, + { + "name": "b/requirer2", "version": "2.3.0", + "require": { "a/aliased-replaced": "^4.0", "a/aliased-replaced2": "^4.0" } + } + ] + } + ], + "require": { + "a/aliased": "dev-next as 4.1.0-RC2", + "b/requirer": "2.3.0" + }, + "require-dev": { + "a/aliased2": "dev-next as 4.1.0-RC2", + "b/requirer2": "2.3.0" + } +} +--RUN-- +update +--EXPECT-LOCK-- +{ + "packages": [ + { + "name": "a/aliased", "version": "dev-next", + "type": "library", + "replace": { "a/aliased-replaced": "self.version" } + }, + { + "name": "b/requirer", "version": "2.3.0", + "require": { "a/aliased-replaced": "^4.0" }, + "type": "library" + } + ], + "packages-dev": [ + { + "name": "a/aliased2", "version": "dev-next", + "type": "library", + "replace": { "a/aliased-replaced2": "self.version" } + }, + { + "name": "b/requirer2", "version": "2.3.0", + "require": { "a/aliased-replaced": "^4.0", "a/aliased-replaced2": "^4.0" }, + "type": "library" + } + ], + "aliases": [{ + "package": "a/aliased2", + "version": "dev-next", + "alias": "4.1.0-RC2", + "alias_normalized": "4.1.0.0-RC2" + }, { + "package": "a/aliased", + "version": "dev-next", + "alias": "4.1.0-RC2", + "alias_normalized": "4.1.0.0-RC2" + }], + "minimum-stability": "stable", + "stability-flags": { + "a/aliased": 20, + "a/aliased2": 20 + }, + "prefer-stable": false, + "prefer-lowest": false, + "platform": [], + "platform-dev": [] +} +--EXPECT-- +Installing a/aliased (dev-next) +Marking a/aliased (4.1.0-RC2) as installed, alias of a/aliased (dev-next) +Installing b/requirer (2.3.0) +Installing a/aliased2 (dev-next) +Marking a/aliased2 (4.1.0-RC2) as installed, alias of a/aliased2 (dev-next) +Installing b/requirer2 (2.3.0) diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index b8968c35b..62bda3d7d 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test @@ -2,8 +2,8 @@ See Github issue #4795 ( github.com/composer/composer/issues/4795 ). -Composer\Installer::whitelistUpdateDependencies should not output a warning for dependencies that need to be updated -that are also a root package, when that root package is also explicitly whitelisted. +Composer\Installer::allowListUpdateDependencies should not output a warning for dependencies that need to be updated +that are also a root package, when that root package is also explicitly allowed. --COMPOSER-- { diff --git a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test index dc722c379..39803f725 100644 --- a/tests/Composer/Test/Fixtures/installer/github-issues-4795.test +++ b/tests/Composer/Test/Fixtures/installer/github-issues-4795.test @@ -2,8 +2,8 @@ See Github issue #4795 ( github.com/composer/composer/issues/4795 ). -Composer\Installer::whitelistUpdateDependencies intentionally ignores root requirements even if said package is also a -dependency of one the requirements that is whitelisted for update. +Composer\Installer::allowListUpdateDependencies intentionally ignores root requirements even if said package is also a +dependency of one the requirements that is allowed for update. --COMPOSER-- { diff --git a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test index b996ff65b..4160fac55 100644 --- a/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test +++ b/tests/Composer/Test/Fixtures/installer/install-from-lock-removes-package.test @@ -6,8 +6,8 @@ Install from a lock file that deleted a package { "type": "package", "package": [ - { "name": "whitelisted/pkg", "version": "1.1.0" }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.1.0" }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, { "name": "fixed/dependency", "version": "1.1.0" }, { "name": "fixed/dependency", "version": "1.0.0" }, { "name": "old/dependency", "version": "1.0.0" } @@ -15,14 +15,14 @@ Install from a lock file that deleted a package } ], "require": { - "whitelisted/pkg": "1.*", + "allowed/pkg": "1.*", "fixed/dependency": "1.*" } } --LOCK-- { "packages": [ - { "name": "whitelisted/pkg", "version": "1.1.0" }, + { "name": "allowed/pkg", "version": "1.1.0" }, { "name": "fixed/dependency", "version": "1.0.0" } ], "packages-dev": [], @@ -33,7 +33,7 @@ Install from a lock file that deleted a package } --INSTALLED-- [ - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, { "name": "fixed/dependency", "version": "1.0.0" }, { "name": "old/dependency", "version": "1.0.0" } ] @@ -41,4 +41,4 @@ Install from a lock file that deleted a package install --EXPECT-- Removing old/dependency (1.0.0) -Upgrading whitelisted/pkg (1.0.0 => 1.1.0) +Upgrading allowed/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test index f45f6d528..614c87515 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-forces-dev-reference-from-lock-for-non-updated-packages.test @@ -1,5 +1,5 @@ --TEST-- -Partial update forces updates dev reference from lock file for non whitelisted packages +Partial update forces updates dev reference from lock file for non allowed packages --COMPOSER-- { "repositories": [ diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test index 0f009ae6f..317f2396d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-locked-require.test @@ -1,13 +1,13 @@ --TEST-- -Update with a package whitelist only updates those packages if they are not present in composer.json +Update with a package allow list only updates those packages if they are not present in composer.json --COMPOSER-- { "repositories": [ { "type": "package", "package": [ - { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "fixed/dependency": "1.*" } }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, + { "name": "allowed/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "fixed/dependency": "1.*" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, { "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "fixed/dependency", "version": "1.1.0", "require": { "fixed/sub-dependency": "1.*" } }, @@ -18,13 +18,13 @@ Update with a package whitelist only updates those packages if they are not pres } ], "require": { - "whitelisted/pkg": "1.*", + "allowed/pkg": "1.*", "fixed/dependency": "1.*" } } --INSTALLED-- [ - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, { "name": "fixed/sub-dependency", "version": "1.0.0" } @@ -32,7 +32,7 @@ Update with a package whitelist only updates those packages if they are not pres --LOCK-- { "packages": [ - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0", "fixed/dependency": "1.*" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "fixed/dependency", "version": "1.0.0", "require": { "fixed/sub-dependency": "1.*" } }, { "name": "fixed/sub-dependency", "version": "1.0.0" } @@ -47,7 +47,7 @@ Update with a package whitelist only updates those packages if they are not pres "platform-dev": [] } --RUN-- -update whitelisted/pkg dependency/pkg +update allowed/pkg dependency/pkg --EXPECT-- Upgrading dependency/pkg (1.0.0 => 1.1.0) -Upgrading whitelisted/pkg (1.0.0 => 1.1.0) +Upgrading allowed/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test index 95fd639f2..5c796181a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-all-dependencies.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist pattern and all-dependencies flag updates packages and their dependencies, even if defined as root dependency, matching the pattern +Update with a package allow list pattern and all-dependencies flag updates packages and their dependencies, even if defined as root dependency, matching the pattern --COMPOSER-- { "repositories": [ @@ -8,10 +8,10 @@ Update with a package whitelist pattern and all-dependencies flag updates packag "package": [ { "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.1.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "allowed/pkg-component1", "version": "1.1.0" }, + { "name": "allowed/pkg-component1", "version": "1.0.0" }, + { "name": "allowed/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, { "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, @@ -23,8 +23,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag ], "require": { "fixed/pkg": "1.*", - "whitelisted/pkg-component1": "1.*", - "whitelisted/pkg-component2": "1.*", + "allowed/pkg-component1": "1.*", + "allowed/pkg-component2": "1.*", "dependency/pkg": "1.*", "unrelated/pkg": "1.*" } @@ -32,8 +32,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag --INSTALLED-- [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg-component1", "version": "1.0.0" }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -42,8 +42,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag { "packages": [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg-component1", "version": "1.0.0" }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -58,8 +58,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag "platform-dev": [] } --RUN-- -update whitelisted/pkg-* --with-all-dependencies +update allowed/pkg-* --with-all-dependencies --EXPECT-- -Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) +Upgrading allowed/pkg-component1 (1.0.0 => 1.1.0) Upgrading dependency/pkg (1.0.0 => 1.1.0) -Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) +Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test index d40a924ab..d82138384 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-dependencies.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those packages and their dependencies matching the pattern but no dependencies defined as roo package +Update with a package allow list only updates those packages and their dependencies matching the pattern but no dependencies defined as roo package --COMPOSER-- { "repositories": [ @@ -8,10 +8,10 @@ Update with a package whitelist only updates those packages and their dependenci "package": [ { "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.1.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, + { "name": "allowed/pkg-component1", "version": "1.1.0" }, + { "name": "allowed/pkg-component1", "version": "1.0.0" }, + { "name": "allowed/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*", "root/pkg-dependency": "1.*" } }, { "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "root/pkg-dependency", "version": "1.1.0" }, @@ -25,8 +25,8 @@ Update with a package whitelist only updates those packages and their dependenci ], "require": { "fixed/pkg": "1.*", - "whitelisted/pkg-component1": "1.*", - "whitelisted/pkg-component2": "1.*", + "allowed/pkg-component1": "1.*", + "allowed/pkg-component2": "1.*", "root/pkg-dependency": "1.*", "unrelated/pkg": "1.*" } @@ -34,8 +34,8 @@ Update with a package whitelist only updates those packages and their dependenci --INSTALLED-- [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg-component1", "version": "1.0.0" }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "root/pkg-dependency", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, @@ -45,8 +45,8 @@ Update with a package whitelist only updates those packages and their dependenci { "packages": [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg-component1", "version": "1.0.0" }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "root/pkg-dependency", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, @@ -60,8 +60,8 @@ Update with a package whitelist only updates those packages and their dependenci "prefer-lowest": false } --RUN-- -update whitelisted/pkg-* --with-dependencies +update allowed/pkg-* --with-dependencies --EXPECT-- -Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) +Upgrading allowed/pkg-component1 (1.0.0 => 1.1.0) Upgrading dependency/pkg (1.0.0 => 1.1.0) -Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) +Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test index 55a07b118..5cabcbb4a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-with-root-dependencies.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those packages and their dependencies matching the pattern +Update with a package allow list only updates those packages and their dependencies matching the pattern --COMPOSER-- { "repositories": [ @@ -8,16 +8,16 @@ Update with a package whitelist only updates those packages and their dependenci "package": [ { "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.1.0", "require": { "whitelisted/pkg-component2": "1.1.0" } }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, - { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "whitelisted/pkg-component5": "1.0.0" } }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, - { "name": "whitelisted/pkg-component3", "version": "1.1.0", "require": { "whitelisted/pkg-component4": "1.1.0" } }, - { "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, - { "name": "whitelisted/pkg-component4", "version": "1.1.0" }, - { "name": "whitelisted/pkg-component4", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component5", "version": "1.1.0" }, - { "name": "whitelisted/pkg-component5", "version": "1.0.0" }, + { "name": "allowed/pkg-component1", "version": "1.1.0", "require": { "allowed/pkg-component2": "1.1.0" } }, + { "name": "allowed/pkg-component1", "version": "1.0.0", "require": { "allowed/pkg-component2": "1.0.0" } }, + { "name": "allowed/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0", "allowed/pkg-component5": "1.0.0" } }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg-component3", "version": "1.1.0", "require": { "allowed/pkg-component4": "1.1.0" } }, + { "name": "allowed/pkg-component3", "version": "1.0.0", "require": { "allowed/pkg-component4": "1.0.0" } }, + { "name": "allowed/pkg-component4", "version": "1.1.0" }, + { "name": "allowed/pkg-component4", "version": "1.0.0" }, + { "name": "allowed/pkg-component5", "version": "1.1.0" }, + { "name": "allowed/pkg-component5", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, @@ -29,20 +29,20 @@ Update with a package whitelist only updates those packages and their dependenci ], "require": { "fixed/pkg": "1.*", - "whitelisted/pkg-component1": "1.*", - "whitelisted/pkg-component2": "1.*", - "whitelisted/pkg-component3": "1.0.0", + "allowed/pkg-component1": "1.*", + "allowed/pkg-component2": "1.*", + "allowed/pkg-component3": "1.0.0", "unrelated/pkg": "1.*" } } --INSTALLED-- [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, - { "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, - { "name": "whitelisted/pkg-component4", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component5", "version": "1.0.0" }, + { "name": "allowed/pkg-component1", "version": "1.0.0", "require": { "allowed/pkg-component2": "1.0.0" } }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg-component3", "version": "1.0.0", "require": { "allowed/pkg-component4": "1.0.0" } }, + { "name": "allowed/pkg-component4", "version": "1.0.0" }, + { "name": "allowed/pkg-component5", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -51,11 +51,11 @@ Update with a package whitelist only updates those packages and their dependenci { "packages": [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0", "require": { "whitelisted/pkg-component2": "1.0.0" } }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, - { "name": "whitelisted/pkg-component3", "version": "1.0.0", "require": { "whitelisted/pkg-component4": "1.0.0" } }, - { "name": "whitelisted/pkg-component4", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component5", "version": "1.0.0" }, + { "name": "allowed/pkg-component1", "version": "1.0.0", "require": { "allowed/pkg-component2": "1.0.0" } }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg-component3", "version": "1.0.0", "require": { "allowed/pkg-component4": "1.0.0" } }, + { "name": "allowed/pkg-component4", "version": "1.0.0" }, + { "name": "allowed/pkg-component5", "version": "1.0.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -70,8 +70,8 @@ Update with a package whitelist only updates those packages and their dependenci "platform-dev": [] } --RUN-- -update whitelisted/pkg-* foobar --with-dependencies +update allowed/pkg-* foobar --with-dependencies --EXPECT-- Upgrading dependency/pkg (1.0.0 => 1.1.0) -Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) -Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) +Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0) +Upgrading allowed/pkg-component1 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test index 6cd1d7778..6ceec16b2 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns-without-dependencies.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those packages matching the pattern +Update with a package allow list only updates those packages matching the pattern --COMPOSER-- { "repositories": [ @@ -8,10 +8,10 @@ Update with a package whitelist only updates those packages matching the pattern "package": [ { "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.1.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "allowed/pkg-component1", "version": "1.1.0" }, + { "name": "allowed/pkg-component1", "version": "1.0.0" }, + { "name": "allowed/pkg-component2", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, { "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, @@ -23,16 +23,16 @@ Update with a package whitelist only updates those packages matching the pattern ], "require": { "fixed/pkg": "1.*", - "whitelisted/pkg-component1": "1.*", - "whitelisted/pkg-component2": "1.*", + "allowed/pkg-component1": "1.*", + "allowed/pkg-component2": "1.*", "unrelated/pkg": "1.*" } } --INSTALLED-- [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg-component1", "version": "1.0.0" }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -41,8 +41,8 @@ Update with a package whitelist only updates those packages matching the pattern { "packages": [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component1", "version": "1.0.0" }, - { "name": "whitelisted/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg-component1", "version": "1.0.0" }, + { "name": "allowed/pkg-component2", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -55,7 +55,7 @@ Update with a package whitelist only updates those packages matching the pattern "prefer-lowest": false } --RUN-- -update whitelisted/pkg-* +update allowed/pkg-* --EXPECT-- -Upgrading whitelisted/pkg-component1 (1.0.0 => 1.1.0) -Upgrading whitelisted/pkg-component2 (1.0.0 => 1.1.0) +Upgrading allowed/pkg-component1 (1.0.0 => 1.1.0) +Upgrading allowed/pkg-component2 (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test index 738f0af74..fde1c9eab 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-patterns.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those corresponding to the pattern +Update with a package allow list only updates those corresponding to the pattern --COMPOSER-- { "repositories": [ diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test index 9360bc2f6..7f2299566 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-removes-unused.test @@ -1,13 +1,13 @@ --TEST-- -Update with a package whitelist removes unused packages +Update with a package allow list removes unused packages --COMPOSER-- { "repositories": [ { "type": "package", "package": [ - { "name": "whitelisted/pkg", "version": "1.1.0" }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.1.0" }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "fixed/dependency": "1.0.0", "old/dependency": "1.0.0" } }, { "name": "fixed/dependency", "version": "1.1.0" }, { "name": "fixed/dependency", "version": "1.0.0" }, { "name": "old/dependency", "version": "1.0.0" } @@ -15,20 +15,20 @@ Update with a package whitelist removes unused packages } ], "require": { - "whitelisted/pkg": "1.*", + "allowed/pkg": "1.*", "fixed/dependency": "1.*" } } --INSTALLED-- [ - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, { "name": "fixed/dependency", "version": "1.0.0" }, { "name": "old/dependency", "version": "1.0.0" } ] --LOCK-- { "packages": [ - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "old/dependency": "1.0.0", "fixed/dependency": "1.0.0" } }, { "name": "fixed/dependency", "version": "1.0.0" }, { "name": "old/dependency", "version": "1.0.0" } ], @@ -42,7 +42,7 @@ Update with a package whitelist removes unused packages "platform-dev": [] } --RUN-- -update --with-dependencies whitelisted/pkg +update --with-dependencies allowed/pkg --EXPECT-- Removing old/dependency (1.0.0) -Upgrading whitelisted/pkg (1.0.0 => 1.1.0) +Upgrading allowed/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test index 079ad9d2b..5b02b7d0d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependencies.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those packages and their dependencies listed as command arguments +Update with a package allow list only updates those packages and their dependencies listed as command arguments --COMPOSER-- { "repositories": [ @@ -8,8 +8,8 @@ Update with a package whitelist only updates those packages and their dependenci "package": [ { "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, @@ -21,14 +21,14 @@ Update with a package whitelist only updates those packages and their dependenci ], "require": { "fixed/pkg": "1.*", - "whitelisted/pkg": "1.*", + "allowed/pkg": "1.*", "unrelated/pkg": "1.*" } } --INSTALLED-- [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -37,7 +37,7 @@ Update with a package whitelist only updates those packages and their dependenci { "packages": [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -50,7 +50,7 @@ Update with a package whitelist only updates those packages and their dependenci "prefer-lowest": false } --RUN-- -update whitelisted/pkg --with-dependencies +update allowed/pkg --with-dependencies --EXPECT-- Upgrading dependency/pkg (1.0.0 => 1.1.0) -Upgrading whitelisted/pkg (1.0.0 => 1.1.0) +Upgrading allowed/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test index 299c505cb..abdc48979 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list-with-dependency-conflict.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates whitelisted packages if no dependency conflicts +Update with a package allow list only updates allowed packages if no dependency conflicts --COMPOSER-- { "repositories": [ @@ -8,8 +8,8 @@ Update with a package whitelist only updates whitelisted packages if no dependen "package": [ { "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.1.0" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, @@ -21,14 +21,14 @@ Update with a package whitelist only updates whitelisted packages if no dependen ], "require": { "fixed/pkg": "1.*", - "whitelisted/pkg": "1.*", + "allowed/pkg": "1.*", "unrelated/pkg": "1.*" } } --INSTALLED-- [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -37,7 +37,7 @@ Update with a package whitelist only updates whitelisted packages if no dependen { "packages": [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.0.0" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -50,5 +50,5 @@ Update with a package whitelist only updates whitelisted packages if no dependen "prefer-lowest": false } --RUN-- -update whitelisted/pkg +update allowed/pkg --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-allow-list.test b/tests/Composer/Test/Fixtures/installer/update-allow-list.test index a02e00c4b..593375442 100644 --- a/tests/Composer/Test/Fixtures/installer/update-allow-list.test +++ b/tests/Composer/Test/Fixtures/installer/update-allow-list.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those packages listed as command arguments +Update with a package allow list only updates those packages listed as command arguments --COMPOSER-- { "repositories": [ @@ -8,8 +8,8 @@ Update with a package whitelist only updates those packages listed as command ar "package": [ { "name": "fixed/pkg", "version": "1.1.0" }, { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "allowed/pkg", "version": "1.1.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, { "name": "dependency/pkg", "version": "1.1.0" }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.1.0", "require": { "unrelated/pkg-dependency": "1.*" } }, @@ -21,14 +21,14 @@ Update with a package whitelist only updates those packages listed as command ar ], "require": { "fixed/pkg": "1.*", - "whitelisted/pkg": "1.*", + "allowed/pkg": "1.*", "unrelated/pkg": "1.*" } } --INSTALLED-- [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -37,7 +37,7 @@ Update with a package whitelist only updates those packages listed as command ar { "packages": [ { "name": "fixed/pkg", "version": "1.0.0" }, - { "name": "whitelisted/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, + { "name": "allowed/pkg", "version": "1.0.0", "require": { "dependency/pkg": "1.*" } }, { "name": "dependency/pkg", "version": "1.0.0" }, { "name": "unrelated/pkg", "version": "1.0.0", "require": { "unrelated/pkg-dependency": "1.*" } }, { "name": "unrelated/pkg-dependency", "version": "1.0.0" } @@ -52,6 +52,6 @@ Update with a package whitelist only updates those packages listed as command ar "platform-dev": [] } --RUN-- -update whitelisted/pkg +update allowed/pkg --EXPECT-- -Upgrading whitelisted/pkg (1.0.0 => 1.1.0) +Upgrading allowed/pkg (1.0.0 => 1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index 4831c7705..521b5611a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-changes-url.test +++ b/tests/Composer/Test/Fixtures/installer/update-changes-url.test @@ -3,10 +3,10 @@ Update updates URLs for updated packages if they have changed a/a is dev and gets everything updated as it updates to a new ref b/b is a tag and gets everything updated by updating the package URL directly -c/c is a tag and not whitelisted and remains unchanged +c/c is a tag and not allowlisted and remains unchanged d/d is dev but with a #ref so it should get URL updated but not the reference e/e is dev and newly installed with a #ref so it should get the correct URL but with the #111 ref -f/f is dev but not whitelisted and remains unchanged +f/f is dev but not allowlisted and remains unchanged g/g is dev and installed in a different ref than the #ref, so it gets updated and gets the new URL but not the new ref --COMPOSER-- { diff --git a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test index a950b247a..e9fc88ef9 100644 --- a/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-with-all-dependencies.test @@ -2,7 +2,7 @@ See Github issue #6661 ( github.com/composer/composer/issues/6661 ). -When `--with-all-dependencies` is used, Composer\Installer::whitelistUpdateDependencies should update the dependencies of all whitelisted packages, even if the dependency is a root requirement. +When `--with-all-dependencies` is used, Composer should update the dependencies of all allowed packages, even if the dependency is a root requirement. --COMPOSER-- { diff --git a/tests/Composer/Test/Mock/FactoryMock.php b/tests/Composer/Test/Mock/FactoryMock.php index 608d572dd..a073f0ab7 100644 --- a/tests/Composer/Test/Mock/FactoryMock.php +++ b/tests/Composer/Test/Mock/FactoryMock.php @@ -23,6 +23,7 @@ use Composer\EventDispatcher\EventDispatcher; use Composer\IO\IOInterface; use Composer\Test\TestCase; use Composer\Util\Loop; +use Composer\Util\ProcessExecutor; class FactoryMock extends Factory { @@ -47,7 +48,7 @@ class FactoryMock extends Factory return new InstallationManagerMock(); } - protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io) + protected function createDefaultInstallers(Installer\InstallationManager $im, Composer $composer, IOInterface $io, ProcessExecutor $process = null) { } diff --git a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php index 45a635437..9204e6fa6 100644 --- a/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php +++ b/tests/Composer/Test/Package/Archiver/ArchiveManagerTest.php @@ -18,6 +18,7 @@ use Composer\Package\Archiver\ArchiveManager; use Composer\Package\PackageInterface; use Composer\Util\Loop; use Composer\Test\Mock\FactoryMock; +use Composer\Util\ProcessExecutor; class ArchiveManagerTest extends ArchiverTest { @@ -36,7 +37,8 @@ class ArchiveManagerTest extends ArchiverTest $dm = $factory->createDownloadManager( $io = new NullIO, $config = FactoryMock::createConfig(), - $httpDownloader = $factory->createHttpDownloader($io, $config) + $httpDownloader = $factory->createHttpDownloader($io, $config), + new ProcessExecutor($io) ); $loop = new Loop($httpDownloader); $this->manager = $factory->createArchiveManager($factory->createConfig(), $dm, $loop); diff --git a/tests/Composer/Test/Package/BasePackageTest.php b/tests/Composer/Test/Package/BasePackageTest.php index 33d384d69..5f5f17cd1 100644 --- a/tests/Composer/Test/Package/BasePackageTest.php +++ b/tests/Composer/Test/Package/BasePackageTest.php @@ -81,7 +81,7 @@ class BasePackageTest extends TestCase $createPackage = function ($arr) use ($self) { $package = $self->getMockForAbstractClass('\Composer\Package\BasePackage', array(), '', false); $package->expects($self->once())->method('isDev')->will($self->returnValue(true)); - $package->expects($self->once())->method('getSourceType')->will($self->returnValue('git')); + $package->expects($self->any())->method('getSourceType')->will($self->returnValue('git')); $package->expects($self->once())->method('getPrettyVersion')->will($self->returnValue('PrettyVersion')); $package->expects($self->any())->method('getSourceReference')->will($self->returnValue($arr['sourceReference'])); diff --git a/tests/Composer/Test/Repository/ComposerRepositoryTest.php b/tests/Composer/Test/Repository/ComposerRepositoryTest.php index 4fcbbb431..01e3be4ce 100644 --- a/tests/Composer/Test/Repository/ComposerRepositoryTest.php +++ b/tests/Composer/Test/Repository/ComposerRepositoryTest.php @@ -189,16 +189,19 @@ class ComposerRepositoryTest extends TestCase ->getMock(); $httpDownloader->expects($this->at(0)) + ->method('enableAsync'); + + $httpDownloader->expects($this->at(1)) ->method('get') ->with($url = 'http://example.org/packages.json') ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array('search' => '/search.json?q=%query%&type=%type%')))); - $httpDownloader->expects($this->at(1)) + $httpDownloader->expects($this->at(2)) ->method('get') ->with($url = 'http://example.org/search.json?q=foo&type=composer-plugin') ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode($result))); - $httpDownloader->expects($this->at(2)) + $httpDownloader->expects($this->at(3)) ->method('get') ->with($url = 'http://example.org/search.json?q=foo&type=library') ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array()))); @@ -291,6 +294,9 @@ class ComposerRepositoryTest extends TestCase ->getMock(); $httpDownloader->expects($this->at(0)) + ->method('enableAsync'); + + $httpDownloader->expects($this->at(1)) ->method('get') ->with($url = 'http://example.org/packages.json') ->willReturn(new \Composer\Util\Http\Response(array('url' => $url), 200, array(), json_encode(array( diff --git a/tests/Composer/Test/Repository/PlatformRepositoryTest.php b/tests/Composer/Test/Repository/PlatformRepositoryTest.php index aa51a2fc6..519aadb31 100644 --- a/tests/Composer/Test/Repository/PlatformRepositoryTest.php +++ b/tests/Composer/Test/Repository/PlatformRepositoryTest.php @@ -14,11 +14,15 @@ namespace Composer\Test\Repository; use Composer\Repository\PlatformRepository; use Composer\Test\TestCase; +use Composer\Util\ProcessExecutor; +use Composer\Package\Version\VersionParser; use Composer\Util\Platform; use Symfony\Component\Process\ExecutableFinder; -class PlatformRepositoryTest extends TestCase { - public function testHHVMVersionWhenExecutingInHHVM() { +class PlatformRepositoryTest extends TestCase +{ + public function testHHVMVersionWhenExecutingInHHVM() + { if (!defined('HHVM_VERSION_ID')) { $this->markTestSkipped('Not running with HHVM'); return; @@ -36,7 +40,8 @@ class PlatformRepositoryTest extends TestCase { ); } - public function testHHVMVersionWhenExecutingInPHP() { + public function testHHVMVersionWhenExecutingInPHP() + { if (defined('HHVM_VERSION_ID')) { $this->markTestSkipped('Running with HHVM'); return; @@ -54,17 +59,18 @@ class PlatformRepositoryTest extends TestCase { if ($hhvm === null) { $this->markTestSkipped('HHVM is not installed'); } - $process = $this->getMockBuilder('Composer\Util\ProcessExecutor')->getMock(); - $process->expects($this->once())->method('execute')->will($this->returnCallback( - function($command, &$out) { - $this->assertContains('HHVM_VERSION', $command); - $out = '4.0.1-dev'; - return 0; - } - )); - $repository = new PlatformRepository(array(), array(), $process); + $repository = new PlatformRepository(array(), array()); $package = $repository->findPackage('hhvm', '*'); $this->assertNotNull($package, 'failed to find HHVM package'); - $this->assertSame('4.0.1.0-dev', $package->getVersion()); + + $process = new ProcessExecutor(); + $exitCode = $process->execute( + ProcessExecutor::escape($hhvm). + ' --php -d hhvm.jit=0 -r "echo HHVM_VERSION;" 2>/dev/null', + $version + ); + $parser = new VersionParser; + + $this->assertSame($parser->normalize($version), $package->getVersion()); } } diff --git a/tests/Composer/Test/Util/RemoteFilesystemTest.php b/tests/Composer/Test/Util/RemoteFilesystemTest.php index fe4f213c6..361dd1669 100644 --- a/tests/Composer/Test/Util/RemoteFilesystemTest.php +++ b/tests/Composer/Test/Util/RemoteFilesystemTest.php @@ -12,32 +12,25 @@ namespace Composer\Test\Util; +use Composer\Config; +use Composer\IO\ConsoleIO; +use Composer\IO\IOInterface; +use Composer\Util\AuthHelper; use Composer\Util\RemoteFilesystem; use Composer\Test\TestCase; +use PHPUnit\Framework\MockObject\MockObject; +use ReflectionMethod; +use ReflectionProperty; class RemoteFilesystemTest extends TestCase { - private function getConfigMock() - { - $config = $this->getMockBuilder('Composer\Config')->getMock(); - $config->expects($this->any()) - ->method('get') - ->will($this->returnCallback(function ($key) { - if ($key === 'github-domains' || $key === 'gitlab-domains') { - return array(); - } - })); - - return $config; - } - public function testGetOptionsForUrl() { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io = $this->getIOInterfaceMock(); $io ->expects($this->once()) ->method('hasAuthentication') - ->will($this->returnValue(false)) + ->willReturn(false) ; $res = $this->callGetOptionsForUrl($io, array('http://example.org', array())); @@ -46,16 +39,16 @@ class RemoteFilesystemTest extends TestCase public function testGetOptionsForUrlWithAuthorization() { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io = $this->getIOInterfaceMock(); $io ->expects($this->once()) ->method('hasAuthentication') - ->will($this->returnValue(true)) + ->willReturn(true) ; $io ->expects($this->once()) ->method('getAuthentication') - ->will($this->returnValue(array('username' => 'login', 'password' => 'password'))) + ->willReturn(array('username' => 'login', 'password' => 'password')) ; $options = $this->callGetOptionsForUrl($io, array('http://example.org', array())); @@ -71,17 +64,17 @@ class RemoteFilesystemTest extends TestCase public function testGetOptionsForUrlWithStreamOptions() { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io = $this->getIOInterfaceMock(); $io ->expects($this->once()) ->method('hasAuthentication') - ->will($this->returnValue(true)) + ->willReturn(true) ; $io ->expects($this->once()) ->method('getAuthentication') - ->will($this->returnValue(array('username' => null, 'password' => null))) + ->willReturn(array('username' => null, 'password' => null)) ; $streamOptions = array('ssl' => array( @@ -94,17 +87,17 @@ class RemoteFilesystemTest extends TestCase public function testGetOptionsForUrlWithCallOptionsKeepsHeader() { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io = $this->getIOInterfaceMock(); $io ->expects($this->once()) ->method('hasAuthentication') - ->will($this->returnValue(true)) + ->willReturn(true) ; $io ->expects($this->once()) ->method('getAuthentication') - ->will($this->returnValue(array('username' => null, 'password' => null))) + ->willReturn(array('username' => null, 'password' => null)) ; $streamOptions = array('http' => array( @@ -127,14 +120,14 @@ class RemoteFilesystemTest extends TestCase public function testCallbackGetFileSize() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); + $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock()); $this->callCallbackGet($fs, STREAM_NOTIFY_FILE_SIZE_IS, 0, '', 0, 0, 20); $this->assertAttributeEquals(20, 'bytesMax', $fs); } public function testCallbackGetNotifyProgress() { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io = $this->getIOInterfaceMock(); $io ->expects($this->once()) ->method('overwriteError') @@ -150,21 +143,21 @@ class RemoteFilesystemTest extends TestCase public function testCallbackGetPassesThrough404() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); + $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock()); $this->assertNull($this->callCallbackGet($fs, STREAM_NOTIFY_FAILURE, 0, 'HTTP/1.1 404 Not Found', 404, 0, 0)); } public function testGetContents() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); + $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock()); $this->assertContains('testGetContents', $fs->getContents('http://example.org', 'file://'.__FILE__)); } public function testCopy() { - $fs = new RemoteFilesystem($this->getMockBuilder('Composer\IO\IOInterface')->getMock(), $this->getConfigMock()); + $fs = new RemoteFilesystem($this->getIOInterfaceMock(), $this->getConfigMock()); $file = tempnam(sys_get_temp_dir(), 'c'); $this->assertTrue($fs->copy('http://example.org', 'file://'.__FILE__, $file)); @@ -173,17 +166,96 @@ class RemoteFilesystemTest extends TestCase unlink($file); } + /** + * @expectedException \Composer\Downloader\TransportException + */ + public function testCopyWithNoRetryOnFailure() + { + $fs = $this->getRemoteFilesystemWithMockedMethods(array('getRemoteContents')); + + $fs->expects($this->once())->method('getRemoteContents') + ->willReturnCallback(function ($originUrl, $fileUrl, $ctx, &$http_response_header) { + + $http_response_header = array('http/1.1 401 unauthorized'); + + return ''; + + }); + + + $file = tempnam(sys_get_temp_dir(), 'z'); + unlink($file); + + $fs->copy( + 'http://example.org', + 'file://' . __FILE__, + $file, + true, + array('retry-auth-failure' => false) + ); + } + + public function testCopyWithSuccessOnRetry() + { + $authHelper = $this->getAuthHelperWithMockedMethods(array('promptAuthIfNeeded')); + $fs = $this->getRemoteFilesystemWithMockedMethods(array('getRemoteContents'), $authHelper); + + $authHelper->expects($this->once()) + ->method('promptAuthIfNeeded') + ->willReturn(array( + 'storeAuth' => true, + 'retry' => true + )); + + $fs->expects($this->at(0)) + ->method('getRemoteContents') + ->willReturnCallback(function ($originUrl, $fileUrl, $ctx, &$http_response_header) { + + $http_response_header = array('http/1.1 401 unauthorized'); + + return ''; + + }); + + $fs->expects($this->at(1)) + ->method('getRemoteContents') + ->willReturnCallback(function ($originUrl, $fileUrl, $ctx, &$http_response_header) { + + $http_response_header = array('http/1.1 200 OK'); + + return 'copy( + 'http://example.org', + 'file://' . __FILE__, + $file, + true, + array('retry-auth-failure' => true) + ); + + $this->assertTrue($copyResult); + $this->assertFileExists($file); + $this->assertContains('Copied', file_get_contents($file)); + + unlink($file); + } + /** * @group TLS */ public function testGetOptionsForUrlCreatesSecureTlsDefaults() { - $io = $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + $io = $this->getIOInterfaceMock(); $res = $this->callGetOptionsForUrl($io, array('example.org', array('ssl' => array('cafile' => '/some/path/file.crt'))), array(), 'http://www.example.org'); $this->assertTrue(isset($res['ssl']['ciphers'])); - $this->assertRegExp("|!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA|", $res['ssl']['ciphers']); + $this->assertRegExp('|!aNULL:!eNULL:!EXPORT:!DES:!RC4:!MD5:!PSK:!aECDH:!EDH-DSS-DES-CBC3-SHA:!EDH-RSA-DES-CBC3-SHA:!KRB5-DES-CBC3-SHA|', $res['ssl']['ciphers']); $this->assertTrue($res['ssl']['verify_peer']); $this->assertTrue($res['ssl']['SNI_enabled']); $this->assertEquals(7, $res['ssl']['verify_depth']); @@ -220,6 +292,7 @@ class RemoteFilesystemTest extends TestCase */ public function testBitBucketPublicDownload($url, $contents) { + /** @var ConsoleIO $io */ $io = $this ->getMockBuilder('Composer\IO\ConsoleIO') ->disableOriginalConstructor() @@ -242,6 +315,7 @@ class RemoteFilesystemTest extends TestCase */ public function testBitBucketPublicDownloadWithAuthConfigured($url, $contents) { + /** @var MockObject|ConsoleIO $io */ $io = $this ->getMockBuilder('Composer\IO\ConsoleIO') ->disableOriginalConstructor() @@ -249,13 +323,12 @@ class RemoteFilesystemTest extends TestCase $domains = array(); $io - ->expects($this->any()) ->method('hasAuthentication') - ->will($this->returnCallback(function ($arg) use (&$domains) { + ->willReturnCallback(function ($arg) use (&$domains) { $domains[] = $arg; // first time is called with bitbucket.org, then it redirects to bbuseruploads.s3.amazonaws.com so next time we have no auth configured return $arg === 'bitbucket.org'; - })); + }); $io ->expects($this->at(1)) ->method('getAuthentication') @@ -275,11 +348,11 @@ class RemoteFilesystemTest extends TestCase $this->assertEquals(array('bitbucket.org', 'bbuseruploads.s3.amazonaws.com'), $domains); } - protected function callGetOptionsForUrl($io, array $args = array(), array $options = array(), $fileUrl = '') + private function callGetOptionsForUrl($io, array $args = array(), array $options = array(), $fileUrl = '') { $fs = new RemoteFilesystem($io, $this->getConfigMock(), $options); - $ref = new \ReflectionMethod($fs, 'getOptionsForUrl'); - $prop = new \ReflectionProperty($fs, 'fileUrl'); + $ref = new ReflectionMethod($fs, 'getOptionsForUrl'); + $prop = new ReflectionProperty($fs, 'fileUrl'); $ref->setAccessible(true); $prop->setAccessible(true); @@ -288,17 +361,80 @@ class RemoteFilesystemTest extends TestCase return $ref->invokeArgs($fs, $args); } - protected function callCallbackGet(RemoteFilesystem $fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax) + /** + * @return MockObject|Config + */ + private function getConfigMock() { - $ref = new \ReflectionMethod($fs, 'callbackGet'); + $config = $this->getMockBuilder('Composer\Config')->getMock(); + $config + ->method('get') + ->willReturnCallback(function ($key) { + if ($key === 'github-domains' || $key === 'gitlab-domains') { + return array(); + } + + return null; + }); + + return $config; + } + + private function callCallbackGet(RemoteFilesystem $fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax) + { + $ref = new ReflectionMethod($fs, 'callbackGet'); $ref->setAccessible(true); $ref->invoke($fs, $notificationCode, $severity, $message, $messageCode, $bytesTransferred, $bytesMax); } - protected function setAttribute($object, $attribute, $value) + private function setAttribute($object, $attribute, $value) { - $attr = new \ReflectionProperty($object, $attribute); + $attr = new ReflectionProperty($object, $attribute); $attr->setAccessible(true); $attr->setValue($object, $value); } + + /** + * @return MockObject|IOInterface + */ + private function getIOInterfaceMock() + { + return $this->getMockBuilder('Composer\IO\IOInterface')->getMock(); + } + + /** + * @param array $mockedMethods + * @param AuthHelper $authHelper + * + * @return RemoteFilesystem|MockObject + */ + private function getRemoteFilesystemWithMockedMethods(array $mockedMethods, AuthHelper $authHelper = null) + { + return $this->getMockBuilder('Composer\Util\RemoteFilesystem') + ->setConstructorArgs(array( + $this->getIOInterfaceMock(), + $this->getConfigMock(), + array(), + false, + $authHelper + )) + ->setMethods($mockedMethods) + ->getMock(); + } + + /** + * @param array $mockedMethods + * + * @return AuthHelper|MockObject + */ + private function getAuthHelperWithMockedMethods(array $mockedMethods) + { + return $this->getMockBuilder('Composer\Util\AuthHelper') + ->setConstructorArgs(array( + $this->getIOInterfaceMock(), + $this->getConfigMock() + )) + ->setMethods($mockedMethods) + ->getMock(); + } }