From 95e6e16b78b62e104247d4850f60c92bc9b2076f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?J=C3=A9r=C3=A9my=20Deruss=C3=A9?= Date: Wed, 29 Apr 2020 01:47:29 +0200 Subject: [PATCH 01/41] Use Semver compiled constraints --- src/Composer/DependencyResolver/Pool.php | 5 ++--- src/Composer/DependencyResolver/PoolBuilder.php | 10 +++++----- src/Composer/Repository/ComposerRepository.php | 3 ++- 3 files changed, 9 insertions(+), 9 deletions(-) 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 9e1f9f433..1e08e0ad7 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 @@ -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; } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index e960f63be..bb845aeca 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; } From 5761228068baa934f6312b471ce309591611cdf2 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 4 Jun 2020 10:20:03 +0200 Subject: [PATCH 02/41] Make installer classes forward promises from downloaders to InstallationManager --- .../Installer/InstallationManager.php | 5 +- src/Composer/Installer/LibraryInstaller.php | 85 +++++++++++++------ src/Composer/Installer/PluginInstaller.php | 58 +++++++++---- src/Composer/Installer/ProjectInstaller.php | 6 +- 4 files changed, 106 insertions(+), 48 deletions(-) diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index bd1bf8aee..059ea7169 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -26,6 +26,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. @@ -296,8 +297,8 @@ class InstallationManager $io = $this->io; $promise = $installer->prepare($opType, $package, $initialPackage); - if (null === $promise) { - $promise = new \React\Promise\Promise(function ($resolve, $reject) { $resolve(); }); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); } $promise = $promise->then(function () use ($opType, $installManager, $repo, $operation) { 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/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php index 6d5a26be0..324ef5a99 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -16,6 +16,7 @@ use Composer\Composer; use Composer\IO\IOInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; +use React\Promise\PromiseInterface; /** * Installer for plugin packages @@ -65,15 +66,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 +87,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..2875e0a65 100644 --- a/src/Composer/Installer/ProjectInstaller.php +++ b/src/Composer/Installer/ProjectInstaller.php @@ -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); } /** From a4a617abb46cd1bb55591f9f5b31a4ff3e643ba0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 4 Jun 2020 15:30:20 +0200 Subject: [PATCH 03/41] Reduce amount of Filesystem/ProcessExecutor instantiations, add lots of docblocks --- src/Composer/Command/CreateProjectCommand.php | 11 ++-- src/Composer/Downloader/DownloadManager.php | 6 ++ src/Composer/Downloader/FileDownloader.php | 8 ++- src/Composer/Downloader/GzipDownloader.php | 5 +- src/Composer/Downloader/PathDownloader.php | 31 +++++++--- src/Composer/Downloader/RarDownloader.php | 5 +- src/Composer/Downloader/SvnDownloader.php | 6 +- src/Composer/Downloader/XzDownloader.php | 5 +- src/Composer/Downloader/ZipDownloader.php | 5 +- .../EventDispatcher/EventDispatcher.php | 7 +++ src/Composer/Factory.php | 57 ++++++++++--------- .../Installer/InstallationManager.php | 8 ++- src/Composer/Installer/PluginInstaller.php | 5 +- src/Composer/Installer/ProjectInstaller.php | 4 +- .../Package/Archiver/ArchiveManager.php | 2 + .../Package/Loader/RootPackageLoader.php | 2 +- src/Composer/Package/Locker.php | 4 +- src/Composer/Plugin/PluginManager.php | 8 +++ .../Repository/ComposerRepository.php | 4 +- .../Repository/PlatformRepository.php | 45 +++++++++------ src/Composer/Repository/RepositoryFactory.php | 5 +- src/Composer/Repository/RepositoryManager.php | 17 +++++- src/Composer/Repository/Vcs/GitDriver.php | 1 - src/Composer/Repository/Vcs/HgDriver.php | 4 +- src/Composer/Repository/Vcs/SvnDriver.php | 9 ++- src/Composer/Repository/VcsRepository.php | 4 +- src/Composer/Util/Filesystem.php | 10 +++- .../Test/Downloader/ZipDownloaderTest.php | 12 ++-- tests/Composer/Test/Mock/FactoryMock.php | 3 +- .../Package/Archiver/ArchiveManagerTest.php | 4 +- .../Repository/PlatformRepositoryTest.php | 32 ++++++----- 31 files changed, 211 insertions(+), 118 deletions(-) diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 07ff8bed8..b872c557a 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 @@ -307,7 +309,8 @@ EOT $directory = getcwd() . DIRECTORY_SEPARATOR . array_pop($parts); } - $fs = new Filesystem(); + $process = new ProcessExecutor($io); + $fs = new Filesystem($fs); if (!$fs->isAbsolutePath($directory)) { $directory = getcwd() . DIRECTORY_SEPARATOR . $directory; } @@ -397,11 +400,11 @@ 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); + $projectInstaller = new ProjectInstaller($directory, $dm, $fs); $im = $factory->createInstallationManager(new Loop($httpDownloader), $io); $im->addInstaller($projectInstaller); $im->execute(new InstalledFilesystemRepository(new JsonFile('php://memory')), array(new InstallOperation($package))); 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..d7c4bcabe 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -39,16 +39,22 @@ 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; /** * @private this is only public for php 5.3 support in closures */ public $lastCacheWrites = array(); - private $eventDispatcher; /** * Constructor. diff --git a/src/Composer/Downloader/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index 91be4593d..0b12b4380 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. @@ -31,10 +32,10 @@ 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) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $fs = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); } protected function extract(PackageInterface $package, $file, $path) diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index 648c987b2..51e9f7709 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -18,10 +18,15 @@ 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; /** * Download a package from a local path. @@ -34,6 +39,15 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter const STRATEGY_SYMLINK = 10; const STRATEGY_MIRROR = 20; + /** @var ProcessExecutor */ + private $process; + + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $fs = null, ProcessExecutor $process = null) + { + $this->process = $process ?: new ProcessExecutor($io); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); + } + /** * {@inheritdoc} */ @@ -115,7 +129,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $allowedStrategies = array(self::STRATEGY_MIRROR); } - $fileSystem = new Filesystem(); + $symfonyFilesystem = new SymfonyFilesystem(); $this->filesystem->removeDirectory($path); if ($output) { @@ -142,9 +156,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 +175,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) { @@ -213,7 +226,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..0ca15de1f 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; @@ -35,10 +36,10 @@ 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) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $fs = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); } protected function extract(PackageInterface $package, $file, $path) 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/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index 371ceda1b..34956d997 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. @@ -31,11 +32,11 @@ 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) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $fs = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); } protected function extract(PackageInterface $package, $file, $path) diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index 29c7fd82a..d0b4e8255 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; @@ -38,10 +39,10 @@ class ZipDownloader extends ArchiveDownloader /** @var ZipArchive|null */ private $zipArchiveObject; - public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, ProcessExecutor $process = null) + public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $fs = null, ProcessExecutor $process = null) { $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache); + parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); } /** 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..04da08e31 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -335,15 +335,16 @@ class Factory } $httpDownloader = self::createHttpDownloader($io, $config); + $process = new ProcessExecutor($io); $loop = new Loop($httpDownloader); $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)); + $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)); + $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs)); + $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/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 059ea7169..06e8d65b9 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -37,11 +37,17 @@ use React\Promise\PromiseInterface; */ 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; public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) @@ -181,7 +187,7 @@ class InstallationManager 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) { diff --git a/src/Composer/Installer/PluginInstaller.php b/src/Composer/Installer/PluginInstaller.php index 324ef5a99..ae281ab08 100644 --- a/src/Composer/Installer/PluginInstaller.php +++ b/src/Composer/Installer/PluginInstaller.php @@ -16,6 +16,7 @@ use Composer\Composer; use Composer\IO\IOInterface; use Composer\Repository\InstalledRepositoryInterface; use Composer\Package\PackageInterface; +use Composer\Util\Filesystem; use React\Promise\PromiseInterface; /** @@ -34,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(); } diff --git a/src/Composer/Installer/ProjectInstaller.php b/src/Composer/Installer/ProjectInstaller.php index 2875e0a65..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; } /** 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/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/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index e960f63be..8848452f2 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -1155,12 +1155,12 @@ 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; 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/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/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/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/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index e3bbe45a8..4436c6ad7 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -194,7 +194,7 @@ class ZipDownloaderTest extends TestCase ->method('execute') ->will($this->returnValue(1)); - $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); $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } @@ -211,7 +211,7 @@ class ZipDownloaderTest extends TestCase ->method('execute') ->will($this->returnValue(0)); - $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); $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); } @@ -238,7 +238,7 @@ 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'); } @@ -270,7 +270,7 @@ 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'); } @@ -298,7 +298,7 @@ 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'); } @@ -330,7 +330,7 @@ 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'); } 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/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()); } } From c9571f90b4852879b6edf75704ef65cad1125428 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Thu, 4 Jun 2020 16:03:58 +0200 Subject: [PATCH 04/41] Run phpstan with regular output and then run again to cs2pr if there was an error, to keep usable output in CI logs --- .github/workflows/phpstan.yml | 2 +- phpstan/config.neon | 6 ------ src/Composer/Command/CreateProjectCommand.php | 2 +- 3 files changed, 2 insertions(+), 8 deletions(-) diff --git a/.github/workflows/phpstan.yml b/.github/workflows/phpstan.yml index 967f79dcd..87cd149eb 100644 --- a/.github/workflows/phpstan.yml +++ b/.github/workflows/phpstan.yml @@ -53,4 +53,4 @@ 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 + vendor/bin/phpstan analyse --configuration=phpstan/config.neon || vendor/bin/phpstan analyse --configuration=phpstan/config.neon --error-format=checkstyle | cs2pr diff --git a/phpstan/config.neon b/phpstan/config.neon index 8b8bf62ce..d9bf7f8c6 100644 --- a/phpstan/config.neon +++ b/phpstan/config.neon @@ -27,12 +27,6 @@ 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' paths: - ../src - ../tests diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index b872c557a..de6701ce4 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -310,7 +310,7 @@ EOT } $process = new ProcessExecutor($io); - $fs = new Filesystem($fs); + $fs = new Filesystem($process); if (!$fs->isAbsolutePath($directory)) { $directory = getcwd() . DIRECTORY_SEPARATOR . $directory; } From 71ddc487fec43696d5905c1d6928a8b262bac2c4 Mon Sep 17 00:00:00 2001 From: Ayesh Karunaratne Date: Thu, 4 Jun 2020 23:05:22 +0700 Subject: [PATCH 05/41] Platform Check: Add a special case for `zend-opcache`. Ref #8946 The platform-check feature maps `ext-X` to `extension_loaded('X')` calls. While most of the extensions can be tested this way, the `zend-opcache` extension requires `zend opcache` to be probed instead of the `zend-opcache` name. This commit adds a special case for `zend-opcache` to use the correct name in `extension_loaded()` calls in generated `platform_check.php` file. --- src/Composer/Autoload/AutoloadGenerator.php | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 4dee65417..b74e4bd9b 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -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"; From 90425a6a50558283a216ba956392426f7e66ced3 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 10:36:40 +0200 Subject: [PATCH 06/41] Add upgrade note for custom installers --- UPGRADE-2.0.md | 1 + 1 file changed, 1 insertion(+) 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` From f7df96f96832f749731355d0029ed24f2131f4cd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 10:39:56 +0200 Subject: [PATCH 07/41] Allow php8 for Composer 2 --- composer.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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", From 29ec10d95c402537a56b64dedc3a96b8e93b1831 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 13:43:42 +0200 Subject: [PATCH 08/41] Fix output formatting --- src/Composer/Command/RemoveCommand.php | 2 +- src/Composer/Command/RequireCommand.php | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index 62e23dd9f..e540056f8 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -237,7 +237,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..d8ba23458 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -281,7 +281,7 @@ 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); From 662d6d8351840ac25dc9d9b10deddc7df242b6cb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 14:38:31 +0200 Subject: [PATCH 09/41] Update lock file --- composer.lock | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) 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": { From 6630519882a2ca86e402baa62506a70fc4482235 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 16:01:39 +0200 Subject: [PATCH 10/41] Fix #8298 for COMPOSER_DEV_MODE --- src/Composer/Installer.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 5fcc6efa2..c5c0f7a21 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -200,7 +200,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 From fa799970ada7057b1b41c67dba182ee01d7b09ec Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 7 Jun 2020 22:15:09 +0100 Subject: [PATCH 11/41] Replace whitelist with allow list --- doc/01-basic-usage.md | 2 +- doc/03-cli.md | 4 +- src/Composer/Autoload/AutoloadGenerator.php | 22 ++--- src/Composer/Autoload/ClassMapGenerator.php | 10 +- src/Composer/Cache.php | 22 ++--- src/Composer/Command/InitCommand.php | 4 +- src/Composer/Command/RemoveCommand.php | 4 +- src/Composer/Command/RequireCommand.php | 6 +- src/Composer/Command/UpdateCommand.php | 10 +- src/Composer/DependencyResolver/Pool.php | 11 ++- .../DependencyResolver/RuleSetGenerator.php | 34 +++++-- src/Composer/Installer.php | 97 ++++++++++++++----- src/Composer/Package/BasePackage.php | 8 +- src/Composer/Repository/Vcs/GitHubDriver.php | 4 +- .../installer/github-issues-4795-2.test | 4 +- .../installer/github-issues-4795.test | 6 +- .../install-from-lock-removes-package.test | 12 +-- ...e-downgrades-non-whitelisted-unstable.test | 2 +- ...ce-from-lock-for-non-updated-packages.test | 2 +- .../partial-update-without-lock.test | 2 +- .../installer/update-changes-url.test | 4 +- .../update-whitelist-locked-require.test | 14 +-- ...telist-patterns-with-all-dependencies.test | 24 ++--- ...-whitelist-patterns-with-dependencies.test | 24 ++--- ...elist-patterns-with-root-dependencies.test | 44 ++++----- ...itelist-patterns-without-dependencies.test | 24 ++--- .../installer/update-whitelist-patterns.test | 2 +- .../update-whitelist-removes-unused.test | 14 +-- .../update-whitelist-with-dependencies.test | 14 +-- ...te-whitelist-with-dependency-conflict.test | 12 +-- .../Fixtures/installer/update-whitelist.test | 14 +-- .../update-with-all-dependencies.test | 2 +- tests/Composer/Test/InstallerTest.php | 6 +- 33 files changed, 269 insertions(+), 195 deletions(-) diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index 8c634bcfd..ac8086491 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -159,7 +159,7 @@ php composer.phar update > if the `composer.lock` has not been updated since changes were made to the > `composer.json` that might affect dependency resolution. -If you only want to install or update one dependency, you can whitelist them: +If you only want to install or update one dependency, you can allow list them: ```sh php composer.phar update monolog/monolog [...] diff --git a/doc/03-cli.md b/doc/03-cli.md index 0c41e9ef7..833cf2c5c 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -157,8 +157,8 @@ php composer.phar update "vendor/*" * **--no-progress:** Removes the progress display that can mess with some terminals or scripts which don't handle backspace characters. * **--no-suggest:** Skips suggested packages in the output. -* **--with-dependencies:** Add also dependencies of whitelisted packages to the whitelist, except those that are root requirements. -* **--with-all-dependencies:** Add also all dependencies of whitelisted packages to the whitelist, including those that are root requirements. +* **--with-dependencies:** Add also dependencies of allowed packages to the allow list, except those that are root requirements. +* **--with-all-dependencies:** Add also all dependencies of allowed packages to the allow list, including those that are root requirements. * **--optimize-autoloader (-o):** Convert PSR-0/4 autoloading to classmap to get a faster autoloader. This is recommended especially for production, but can take a bit of time to run so it is currently not done by default. diff --git a/src/Composer/Autoload/AutoloadGenerator.php b/src/Composer/Autoload/AutoloadGenerator.php index 371f3ed76..863ceac43 100644 --- a/src/Composer/Autoload/AutoloadGenerator.php +++ b/src/Composer/Autoload/AutoloadGenerator.php @@ -229,16 +229,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) { @@ -261,7 +261,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); } } } @@ -336,9 +336,9 @@ EOF; return 0; } - 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; @@ -350,9 +350,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) @@ -456,15 +456,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().''); } diff --git a/src/Composer/Autoload/ClassMapGenerator.php b/src/Composer/Autoload/ClassMapGenerator.php index c0b011f3f..4adbcc8be 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()) { if (is_string($path)) { $basePath = $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/Cache.php b/src/Composer/Cache.php index 06c6a0996..069f59d5d 100644 --- a/src/Composer/Cache.php +++ b/src/Composer/Cache.php @@ -28,20 +28,20 @@ class Cache private $io; private $root; private $enabled = true; - private $whitelist; + private $allowList; private $filesystem; /** * @param IOInterface $io * @param string $cacheDir location of the cache - * @param string $whitelist List of characters that are allowed in path names (used in a regex character class) + * @param string $allowList List of characters that are allowed in path names (used in a regex character class) * @param Filesystem $filesystem optional filesystem instance */ - public function __construct(IOInterface $io, $cacheDir, $whitelist = 'a-z0-9.', Filesystem $filesystem = null) + public function __construct(IOInterface $io, $cacheDir, $allowList = 'a-z0-9.', Filesystem $filesystem = null) { $this->io = $io; $this->root = rtrim($cacheDir, '/\\') . '/'; - $this->whitelist = $whitelist; + $this->allowList = $allowList; $this->filesystem = $filesystem ?: new Filesystem(); if (!self::isUsable($cacheDir)) { @@ -77,7 +77,7 @@ class Cache public function read($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { $this->io->writeError('Reading '.$this->root . $file.' from cache', true, IOInterface::DEBUG); @@ -91,7 +91,7 @@ class Cache public function write($file, $contents) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); $this->io->writeError('Writing '.$this->root . $file.' into cache', true, IOInterface::DEBUG); @@ -129,7 +129,7 @@ class Cache public function copyFrom($file, $source) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); $this->filesystem->ensureDirectoryExists(dirname($this->root . $file)); if (!file_exists($source)) { @@ -150,7 +150,7 @@ class Cache public function copyTo($file, $target) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { try { touch($this->root . $file, filemtime($this->root . $file), time()); @@ -177,7 +177,7 @@ class Cache public function remove($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { return $this->filesystem->unlink($this->root . $file); } @@ -229,7 +229,7 @@ class Cache public function sha1($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { return sha1_file($this->root . $file); } @@ -241,7 +241,7 @@ class Cache public function sha256($file) { if ($this->enabled) { - $file = preg_replace('{[^'.$this->whitelist.']}i', '-', $file); + $file = preg_replace('{[^'.$this->allowList.']}i', '-', $file); if (file_exists($this->root . $file)) { return hash_file('sha256', $this->root . $file); } diff --git a/src/Composer/Command/InitCommand.php b/src/Composer/Command/InitCommand.php index d234a8cba..59f0488d1 100644 --- a/src/Composer/Command/InitCommand.php +++ b/src/Composer/Command/InitCommand.php @@ -86,8 +86,8 @@ EOT { $io = $this->getIO(); - $whitelist = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); - $options = array_filter(array_intersect_key($input->getOptions(), array_flip($whitelist))); + $allowList = array('name', 'description', 'author', 'type', 'homepage', 'require', 'require-dev', 'stability', 'license'); + $options = array_filter(array_intersect_key($input->getOptions(), array_flip($allowList))); if (isset($options['author'])) { $options['authors'] = $this->formatAuthors($options['author']); diff --git a/src/Composer/Command/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index e4407d4cb..e6a2fbfb3 100644 --- a/src/Composer/Command/RemoveCommand.php +++ b/src/Composer/Command/RemoveCommand.php @@ -146,8 +146,8 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($packages) - ->setWhitelistTransitiveDependencies(!$input->getOption('no-update-with-dependencies')) + ->setUpdateAllowList($packages) + ->setAllowListTransitiveDependencies(!$input->getOption('no-update-with-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setRunScripts(!$input->getOption('no-scripts')) ; diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 9b59e7feb..45bd315fe 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -237,9 +237,9 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist(array_keys($requirements)) - ->setWhitelistTransitiveDependencies($input->getOption('update-with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('update-with-all-dependencies')) + ->setUpdatAllowList(array_keys($requirements)) + ->setAllowListTransitiveDependencies($input->getOption('update-with-dependencies')) + ->setAllowListAllDependencies($input->getOption('update-with-all-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) diff --git a/src/Composer/Command/UpdateCommand.php b/src/Composer/Command/UpdateCommand.php index e68c265c0..44f1e7dea 100644 --- a/src/Composer/Command/UpdateCommand.php +++ b/src/Composer/Command/UpdateCommand.php @@ -49,8 +49,8 @@ class UpdateCommand extends BaseCommand new InputOption('no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'), new InputOption('no-progress', null, InputOption::VALUE_NONE, 'Do not output download progress.'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'Do not show package suggestions.'), - new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of whitelisted packages to the whitelist, except those defined in root package.'), - new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of whitelisted packages to the whitelist, including those defined in root package.'), + new InputOption('with-dependencies', null, InputOption::VALUE_NONE, 'Add also dependencies of allowed packages to the allow list, except those defined in root package.'), + new InputOption('with-all-dependencies', null, InputOption::VALUE_NONE, 'Add also all dependencies of allowed packages to the allow list, including those defined in root package.'), new InputOption('verbose', 'v|vv|vvv', InputOption::VALUE_NONE, 'Shows more details including new commits pulled in when updating packages.'), new InputOption('optimize-autoloader', 'o', InputOption::VALUE_NONE, 'Optimize autoloader during autoloader dump.'), new InputOption('classmap-authoritative', 'a', InputOption::VALUE_NONE, 'Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.'), @@ -148,9 +148,9 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdateWhitelist($input->getOption('lock') ? array('lock') : $packages) - ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) + ->setUpdateAllowList($input->getOption('lock') ? array('lock') : $packages) + ->setAllowListTransitiveDependencies($input->getOption('with-dependencies')) + ->setAllowListAllDependencies($input->getOption('with-all-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) diff --git a/src/Composer/DependencyResolver/Pool.php b/src/Composer/DependencyResolver/Pool.php index 085aaa7bf..8021275b6 100644 --- a/src/Composer/DependencyResolver/Pool.php +++ b/src/Composer/DependencyResolver/Pool.php @@ -50,7 +50,7 @@ class Pool implements \Countable protected $versionParser; protected $providerCache = array(); protected $filterRequires; - protected $whitelist = null; + protected $whitelist = null; // TODO 2.0 rename to allowList protected $id = 1; public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $filterRequires = array()) @@ -71,6 +71,15 @@ class Pool implements \Countable } } + public function setAllowList($allowList) + { + // call original method for BC + $this->setWhitelist($allowList); + } + + /** + * @deprecated use setAllowList instead + */ public function setWhitelist($whitelist) { $this->whitelist = $whitelist; diff --git a/src/Composer/DependencyResolver/RuleSetGenerator.php b/src/Composer/DependencyResolver/RuleSetGenerator.php index e8714a405..8638440bd 100644 --- a/src/Composer/DependencyResolver/RuleSetGenerator.php +++ b/src/Composer/DependencyResolver/RuleSetGenerator.php @@ -26,7 +26,7 @@ class RuleSetGenerator protected $rules; protected $jobs; protected $installedMap; - protected $whitelistedMap; + protected $allowListedMap; protected $addedMap; protected $conflictAddedMap; protected $addedPackages; @@ -147,6 +147,15 @@ class RuleSetGenerator $this->rules->add($newRule, $type); } + protected function allowListFromPackage(PackageInterface $package) + { + // call original method for BC + $this->whitelistFromPackage($package); + } + + /** + * @deprecated use whitelistFromPackage instead + */ protected function whitelistFromPackage(PackageInterface $package) { $workQueue = new \SplQueue; @@ -154,11 +163,11 @@ class RuleSetGenerator while (!$workQueue->isEmpty()) { $package = $workQueue->dequeue(); - if (isset($this->whitelistedMap[$package->id])) { + if (isset($this->allowListedMap[$package->id])) { continue; } - $this->whitelistedMap[$package->id] = true; + $this->allowListedMap[$package->id] = true; foreach ($package->getRequires() as $link) { $possibleRequires = $this->pool->whatProvides($link->getTarget(), $link->getConstraint(), true); @@ -294,6 +303,15 @@ class RuleSetGenerator return $impossible; } + protected function allowListFromJobs() + { + // call original method for BC + $this->whitelistFromJobs(); + } + + /** + * @deprecated use allowListFromJobs instead + */ protected function whitelistFromJobs() { foreach ($this->jobs as $job) { @@ -301,7 +319,7 @@ class RuleSetGenerator case 'install': $packages = $this->pool->whatProvides($job['packageName'], $job['constraint'], true); foreach ($packages as $package) { - $this->whitelistFromPackage($package); + $this->allowListFromPackage($package); } break; } @@ -348,13 +366,13 @@ class RuleSetGenerator $this->rules = new RuleSet; $this->installedMap = $installedMap; - $this->whitelistedMap = array(); + $this->allowListedMap = array(); foreach ($this->installedMap as $package) { - $this->whitelistFromPackage($package); + $this->allowListFromPackage($package); } - $this->whitelistFromJobs(); + $this->allowListFromJobs(); - $this->pool->setWhitelist($this->whitelistedMap); + $this->pool->setAllowList($this->allowListedMap); $this->addedMap = array(); $this->conflictAddedMap = array(); diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index c5c0f7a21..c7af69427 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -127,9 +127,9 @@ class Installer * * @var array|null */ - protected $updateWhitelist = null; - protected $whitelistDependencies = false; // TODO 2.0 rename to whitelistTransitiveDependencies - protected $whitelistAllDependencies = false; + protected $updateWhitelist = null; // TODO 2.0 rename to updateAllowList + protected $whitelistDependencies = false; // TODO 2.0 rename to allowListTransitiveDependencies + protected $whitelistAllDependencies = false; // TODO 2.0 rename to allowListAllDependencies /** * @var SuggestedPackagesReporter @@ -360,7 +360,7 @@ class Installer $repositories = null; // initialize locked repo if we are installing from lock or in a partial update - // and a lock file is present as we need to force install non-whitelisted lock file + // and a lock file is present as we need to force install non-allowed lock file // packages in that case if (!$this->update || (!empty($this->updateWhitelist) && $this->locker->isLocked())) { try { @@ -375,7 +375,7 @@ class Installer } } - $this->whitelistUpdateDependencies( + $this->allowListUpdateDependencies( $lockedRepository ?: $localRepo, $this->package->getRequires(), $this->package->getDevRequires() @@ -1011,7 +1011,7 @@ class Installer } if ($this->update) { - // skip package if the whitelist is enabled and it is not in it + // skip package if the allow list is enabled and it is not in it if ($this->updateWhitelist && !$this->isUpdateable($package)) { // check if non-updateable packages are out of date compared to the lock file to ensure we don't corrupt it foreach ($currentPackages as $curPackage) { @@ -1280,11 +1280,11 @@ class Installer private function isUpdateable(PackageInterface $package) { if (!$this->updateWhitelist) { - throw new \LogicException('isUpdateable should only be called when a whitelist is present'); + throw new \LogicException('isUpdateable should only be called when an allow list is present'); } - foreach ($this->updateWhitelist as $whiteListedPattern => $void) { - $patternRegexp = BasePackage::packageNameToRegexp($whiteListedPattern); + foreach ($this->updateWhitelist as $pattern => $void) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); if (preg_match($patternRegexp, $package->getName())) { return true; } @@ -1310,11 +1310,11 @@ class Installer } /** - * Adds all dependencies of the update whitelist to the whitelist, too. + * Adds all dependencies of the update allow list to the allow list, too. * * Packages which are listed as requirements in the root package will be * skipped including their dependencies, unless they are listed in the - * update whitelist themselves or $whitelistAllDependencies is true. + * update allow list themselves or $whitelistAllDependencies is true. * * @param RepositoryInterface $localOrLockRepo Use the locked repo if available, otherwise installed repo will do * As we want the most accurate package list to work with, and installed @@ -1322,7 +1322,7 @@ class Installer * @param array $rootRequires An array of links to packages in require of the root package * @param array $rootDevRequires An array of links to packages in require-dev of the root package */ - private function whitelistUpdateDependencies($localOrLockRepo, array $rootRequires, array $rootDevRequires) + private function allowListUpdateDependencies($localOrLockRepo, array $rootRequires, array $rootDevRequires) { if (!$this->updateWhitelist) { return; @@ -1352,16 +1352,16 @@ class Installer $matchesByPattern = array(); // check if the name is a glob pattern that did not match directly if (empty($depPackages)) { - // add any installed package matching the whitelisted name/pattern - $whitelistPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); - foreach ($localOrLockRepo->search($whitelistPatternSearchRegexp) as $installedPackage) { + // add any installed package matching the allow listed name/pattern + $allowListPatternSearchRegexp = BasePackage::packageNameToRegexp($packageName, '^%s$'); + foreach ($localOrLockRepo->search($allowListPatternSearchRegexp) as $installedPackage) { $matchesByPattern[] = $pool->whatProvides($installedPackage['name']); } - // add root requirements which match the whitelisted name/pattern - $whitelistPatternRegexp = BasePackage::packageNameToRegexp($packageName); + // add root requirements which match the allow listed name/pattern + $allowListPatternRegexp = BasePackage::packageNameToRegexp($packageName); foreach ($rootRequiredPackageNames as $rootRequiredPackageName) { - if (preg_match($whitelistPatternRegexp, $rootRequiredPackageName)) { + if (preg_match($allowListPatternRegexp, $rootRequiredPackageName)) { $nameMatchesRequiredPackage = true; break; } @@ -1404,7 +1404,7 @@ class Installer } if (isset($skipPackages[$requirePackage->getName()]) && !preg_match(BasePackage::packageNameToRegexp($packageName), $requirePackage->getName())) { - $this->io->writeError('Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly whitelisted. Ignoring.'); + $this->io->writeError('Dependency "' . $requirePackage->getName() . '" is also a root requirement, but is not explicitly allowed. Ignoring.'); continue; } @@ -1679,6 +1679,8 @@ class Installer * restrict the update operation to a few packages, all other packages * that are already installed will be kept at their current version * + * @deprecated use setAllowList instead + * * @param array $packages * @return Installer */ @@ -1690,7 +1692,20 @@ class Installer } /** - * @deprecated use setWhitelistTransitiveDependencies instead + * restrict the update operation to a few packages, all other packages + * that are already installed will be kept at their current version + * + * @param array $packages + * @return Installer + */ + public function setUpdateAllowList(array $packages) + { + // call original method for BC + return $this->setUpdateWhitelist($packages); + } + + /** + * @deprecated use setAllowListTransitiveDependencies instead */ public function setWhitelistDependencies($updateDependencies = true) { @@ -1698,11 +1713,13 @@ class Installer } /** - * Should dependencies of whitelisted packages (but not direct dependencies) be updated? + * Should dependencies of allowed packages (but not direct dependencies) be updated? * - * This will NOT whitelist any dependencies that are also directly defined + * This will NOT allow list any dependencies that are also directly defined * in the root package. * + * @deprecated use setAllowListTransitiveDependencies instead + * * @param bool $updateTransitiveDependencies * @return Installer */ @@ -1714,11 +1731,28 @@ class Installer } /** - * Should all dependencies of whitelisted packages be updated recursively? + * Should dependencies of allowed packages (but not direct dependencies) be updated? * - * This will whitelist any dependencies of the whitelisted packages, including + * This will NOT allow list any dependencies that are also directly defined + * in the root package. + * + * @param bool $updateTransitiveDependencies + * @return Installer + */ + public function setAllowListTransitiveDependencies($updateTransitiveDependencies = true) + { + // call original method for BC + return $this->setWhitelistTransitiveDependencies($updateTransitiveDependencies); + } + + /** + * Should all dependencies of allowed packages be updated recursively? + * + * This will allow list any dependencies of the allow listed packages, including * those defined in the root package. * + * @deprecated use setAllowListAllDependencies instead + * * @param bool $updateAllDependencies * @return Installer */ @@ -1729,6 +1763,21 @@ class Installer return $this; } + /** + * Should all dependencies of allowed packages be updated recursively? + * + * This will allow list any dependencies of the allow listed packages, including + * those defined in the root package. + * + * @param bool $updateAllDependencies + * @return Installer + */ + public function setAllowListAllDependencies($updateAllDependencies = true) + { + // call original method for BC + return $this->setWhitelistAllDependencies($updateAllDependencies); + } + /** * Should packages be preferred in a stable version when updating? * diff --git a/src/Composer/Package/BasePackage.php b/src/Composer/Package/BasePackage.php index 9630e7ef0..3987e7e87 100644 --- a/src/Composer/Package/BasePackage.php +++ b/src/Composer/Package/BasePackage.php @@ -238,14 +238,14 @@ abstract class BasePackage implements PackageInterface /** * Build a regexp from a package name, expanding * globs as required * - * @param string $whiteListedPattern + * @param string $allowListPattern * @param string $wrap Wrap the cleaned string by the given string * @return string */ - public static function packageNameToRegexp($whiteListedPattern, $wrap = '{^%s$}i') + public static function packageNameToRegexp($allowListPattern, $wrap = '{^%s$}i') { - $cleanedWhiteListedPattern = str_replace('\\*', '.*', preg_quote($whiteListedPattern)); + $cleanedAllowListPattern = str_replace('\\*', '.*', preg_quote($allowListPattern)); - return sprintf($wrap, $cleanedWhiteListedPattern); + return sprintf($wrap, $cleanedAllowListPattern); } } diff --git a/src/Composer/Repository/Vcs/GitHubDriver.php b/src/Composer/Repository/Vcs/GitHubDriver.php index 2fe7e872e..549625fa9 100644 --- a/src/Composer/Repository/Vcs/GitHubDriver.php +++ b/src/Composer/Repository/Vcs/GitHubDriver.php @@ -337,13 +337,11 @@ 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 { $branchData = JsonFile::parseJson($this->getContents($resource), $resource); 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/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test b/tests/Composer/Test/Fixtures/installer/github-issues-4795-2.test index 877ac3653..c8d10d4cd 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 1f4b1af27..64d8e0b39 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-- { @@ -34,7 +34,7 @@ dependency of one the requirements that is whitelisted for update. update b/b --with-dependencies --EXPECT-OUTPUT-- -Dependency "a/a" is also a root requirement, but is not explicitly whitelisted. Ignoring. +Dependency "a/a" is also a root requirement, but is not explicitly allowed. Ignoring. Loading composer repositories with package information Updating dependencies (including require-dev) Nothing to install or update 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 6063abfee..8a2bf39a1 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", "version": "1.1.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "fixed-dependency": "1.0.0", "old-dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.1.0" }, + { "name": "allowed", "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": "1.*", + "allowed": "1.*", "fixed-dependency": "1.*" } } --LOCK-- { "packages": [ - { "name": "whitelisted", "version": "1.1.0" }, + { "name": "allowed", "version": "1.1.0" }, { "name": "fixed-dependency", "version": "1.0.0" } ], "packages-dev": null, @@ -33,7 +33,7 @@ Install from a lock file that deleted a package } --INSTALLED-- [ - { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, + { "name": "allowed", "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-- Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test index 3a428c97c..99c46a918 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-downgrades-non-whitelisted-unstable.test @@ -1,5 +1,5 @@ --TEST-- -Partial update from lock file should apply lock file and downgrade unstable packages even if not whitelisted +Partial update from lock file should apply lock file and downgrade unstable packages even if not allowed --COMPOSER-- { "repositories": [ 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 4533d5a94..756c52d42 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/partial-update-without-lock.test b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test index 94be9176c..97fc4bb49 100644 --- a/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test +++ b/tests/Composer/Test/Fixtures/installer/partial-update-without-lock.test @@ -1,5 +1,5 @@ --TEST-- -Partial update without lock file should update everything whitelisted, remove overly unstable packages +Partial update without lock file should update everything allowed, remove overly unstable packages --COMPOSER-- { "repositories": [ diff --git a/tests/Composer/Test/Fixtures/installer/update-changes-url.test b/tests/Composer/Test/Fixtures/installer/update-changes-url.test index 0a0d47507..5ca2df792 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 gets the new URL but keeps its old ref +c/c is a tag and not allowed and gets the new URL but keeps its old ref 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 -e/e is dev but not whitelisted and gets the new URL but keeps its old ref +e/e is dev but not allowed and gets the new URL but keeps its old ref 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-whitelist-locked-require.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test index 381416af1..cad697e0b 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-locked-require.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed list only updates those packages if they are not present in composer.json --COMPOSER-- { "repositories": [ { "type": "package", "package": [ - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0", "fixed-dependency": "1.*" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, + { "name": "allowed", "version": "1.1.0", "require": { "dependency": "1.1.0", "fixed-dependency": "1.*" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "fixed-dependency", "version": "1.1.0", "require": { "fixed-sub-dependency": "1.*" } }, @@ -18,19 +18,19 @@ Update with a package whitelist only updates those packages if they are not pres } ], "require": { - "whitelisted": "1.*", + "allowed": "1.*", "fixed-dependency": "1.*" } } --INSTALLED-- [ - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0", "fixed-dependency": "1.*" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "fixed-dependency", "version": "1.0.0", "require": { "fixed-sub-dependency": "1.*" } }, { "name": "fixed-sub-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted dependency +update allowed dependency --EXPECT-- Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test index 8ea177cad..ec507859c 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-all-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "allowed-component1", "version": "1.1.0" }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -23,8 +23,8 @@ Update with a package whitelist pattern and all-dependencies flag updates packag ], "require": { "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", + "allowed-component1": "1.*", + "allowed-component2": "1.*", "dependency": "1.*", "unrelated": "1.*" } @@ -32,15 +32,15 @@ Update with a package whitelist pattern and all-dependencies flag updates packag --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted-* --with-all-dependencies +update allowed-* --with-all-dependencies --EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) +Updating allowed-component1 (1.0.0) to allowed-component1 (1.1.0) Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) +Updating allowed-component2 (1.0.0) to allowed-component2 (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test index c685f14ce..e9e21916d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, + { "name": "allowed-component1", "version": "1.1.0" }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.1.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.*", "root-dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "root-dependency", "version": "1.1.0" }, @@ -25,8 +25,8 @@ Update with a package whitelist only updates those packages and their dependenci ], "require": { "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", + "allowed-component1": "1.*", + "allowed-component2": "1.*", "root-dependency": "1.*", "unrelated": "1.*" } @@ -34,16 +34,16 @@ Update with a package whitelist only updates those packages and their dependenci --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "root-dependency", "version": "1.0.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted-* --with-dependencies +update allowed-* --with-dependencies --EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) +Updating allowed-component1 (1.0.0) to allowed-component1 (1.1.0) Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) +Updating allowed-component2 (1.0.0) to allowed-component2 (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test index a24bafb91..8724b4a82 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-with-root-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0", "require": { "whitelisted-component2": "1.1.0" } }, - { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.1.0", "whitelisted-component5": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "whitelisted-component3", "version": "1.1.0", "require": { "whitelisted-component4": "1.1.0" } }, - { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } }, - { "name": "whitelisted-component4", "version": "1.1.0" }, - { "name": "whitelisted-component4", "version": "1.0.0" }, - { "name": "whitelisted-component5", "version": "1.1.0" }, - { "name": "whitelisted-component5", "version": "1.0.0" }, + { "name": "allowed-component1", "version": "1.1.0", "require": { "allowed-component2": "1.1.0" } }, + { "name": "allowed-component1", "version": "1.0.0", "require": { "allowed-component2": "1.0.0" } }, + { "name": "allowed-component2", "version": "1.1.0", "require": { "dependency": "1.1.0", "allowed-component5": "1.0.0" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component3", "version": "1.1.0", "require": { "allowed-component4": "1.1.0" } }, + { "name": "allowed-component3", "version": "1.0.0", "require": { "allowed-component4": "1.0.0" } }, + { "name": "allowed-component4", "version": "1.1.0" }, + { "name": "allowed-component4", "version": "1.0.0" }, + { "name": "allowed-component5", "version": "1.1.0" }, + { "name": "allowed-component5", "version": "1.0.0" }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -29,27 +29,27 @@ Update with a package whitelist only updates those packages and their dependenci ], "require": { "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", - "whitelisted-component3": "1.0.0", + "allowed-component1": "1.*", + "allowed-component2": "1.*", + "allowed-component3": "1.0.0", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0", "require": { "whitelisted-component2": "1.0.0" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, - { "name": "whitelisted-component3", "version": "1.0.0", "require": { "whitelisted-component4": "1.0.0" } }, - { "name": "whitelisted-component4", "version": "1.0.0" }, - { "name": "whitelisted-component5", "version": "1.0.0" }, + { "name": "allowed-component1", "version": "1.0.0", "require": { "allowed-component2": "1.0.0" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component3", "version": "1.0.0", "require": { "allowed-component4": "1.0.0" } }, + { "name": "allowed-component4", "version": "1.0.0" }, + { "name": "allowed-component5", "version": "1.0.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted-* --with-dependencies +update allowed-* --with-dependencies --EXPECT-- Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) +Updating allowed-component2 (1.0.0) to allowed-component2 (1.1.0) +Updating allowed-component1 (1.0.0) to allowed-component1 (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test index e5551b43f..0dff8264a 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns-without-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.1.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "allowed-component1", "version": "1.1.0" }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.1.0", "require": { "dependency": "1.*" } }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -23,22 +23,22 @@ Update with a package whitelist only updates those packages matching the pattern ], "require": { "fixed": "1.*", - "whitelisted-component1": "1.*", - "whitelisted-component2": "1.*", + "allowed-component1": "1.*", + "allowed-component2": "1.*", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted-component1", "version": "1.0.0" }, - { "name": "whitelisted-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed-component1", "version": "1.0.0" }, + { "name": "allowed-component2", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted-* +update allowed-* --EXPECT-- -Updating whitelisted-component1 (1.0.0) to whitelisted-component1 (1.1.0) -Updating whitelisted-component2 (1.0.0) to whitelisted-component2 (1.1.0) +Updating allowed-component1 (1.0.0) to allowed-component1 (1.1.0) +Updating allowed-component2 (1.0.0) to allowed-component2 (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test index de1fb1b73..e4344cc7d 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-patterns.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those corresponding to the pattern +Update with a package allowed list only updates those corresponding to the pattern --COMPOSER-- { "repositories": [ diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test index e658e8c06..87fc11b05 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-removes-unused.test @@ -1,13 +1,13 @@ --TEST-- -Update with a package whitelist removes unused packages +Update with a package allowed list removes unused packages --COMPOSER-- { "repositories": [ { "type": "package", "package": [ - { "name": "whitelisted", "version": "1.1.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "fixed-dependency": "1.0.0", "old-dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.1.0" }, + { "name": "allowed", "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,18 +15,18 @@ Update with a package whitelist removes unused packages } ], "require": { - "whitelisted": "1.*", + "allowed": "1.*", "fixed-dependency": "1.*" } } --INSTALLED-- [ - { "name": "whitelisted", "version": "1.0.0", "require": { "old-dependency": "1.0.0", "fixed-dependency": "1.0.0" } }, + { "name": "allowed", "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" } ] --RUN-- -update --with-dependencies whitelisted +update --with-dependencies allowed --EXPECT-- Uninstalling old-dependency (1.0.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test index bb2e04193..2c0e7b9bf 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependencies.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -21,20 +21,20 @@ Update with a package whitelist only updates those packages and their dependenci ], "require": { "fixed": "1.*", - "whitelisted": "1.*", + "allowed": "1.*", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted --with-dependencies +update allowed --with-dependencies --EXPECT-- Updating dependency (1.0.0) to dependency (1.1.0) -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test b/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test index f63229fbc..81c667463 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist-with-dependency-conflict.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist-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 allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.1.0", "require": { "dependency": "1.1.0" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -21,18 +21,18 @@ Update with a package whitelist only updates whitelisted packages if no dependen ], "require": { "fixed": "1.*", - "whitelisted": "1.*", + "allowed": "1.*", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.0.0" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted +update allowed --EXPECT-- diff --git a/tests/Composer/Test/Fixtures/installer/update-whitelist.test b/tests/Composer/Test/Fixtures/installer/update-whitelist.test index 751d79e70..9cc43dba3 100644 --- a/tests/Composer/Test/Fixtures/installer/update-whitelist.test +++ b/tests/Composer/Test/Fixtures/installer/update-whitelist.test @@ -1,5 +1,5 @@ --TEST-- -Update with a package whitelist only updates those packages listed as command arguments +Update with a package allowed 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", "version": "1.1.0" }, { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.1.0", "require": { "dependency": "1.*" } }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "allowed", "version": "1.1.0", "require": { "dependency": "1.*" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.1.0" }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.1.0", "require": { "unrelated-dependency": "1.*" } }, @@ -21,19 +21,19 @@ Update with a package whitelist only updates those packages listed as command ar ], "require": { "fixed": "1.*", - "whitelisted": "1.*", + "allowed": "1.*", "unrelated": "1.*" } } --INSTALLED-- [ { "name": "fixed", "version": "1.0.0" }, - { "name": "whitelisted", "version": "1.0.0", "require": { "dependency": "1.*" } }, + { "name": "allowed", "version": "1.0.0", "require": { "dependency": "1.*" } }, { "name": "dependency", "version": "1.0.0" }, { "name": "unrelated", "version": "1.0.0", "require": { "unrelated-dependency": "1.*" } }, { "name": "unrelated-dependency", "version": "1.0.0" } ] --RUN-- -update whitelisted +update allowed --EXPECT-- -Updating whitelisted (1.0.0) to whitelisted (1.1.0) +Updating allowed (1.0.0) to allowed (1.1.0) 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 c0019e6ca..12d507a7a 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\Installer::allowListUpdateDependencies should update the dependencies of all allowed packages, even if the dependency is a root requirement. --COMPOSER-- { diff --git a/tests/Composer/Test/InstallerTest.php b/tests/Composer/Test/InstallerTest.php index 067baf17a..90f295d1d 100644 --- a/tests/Composer/Test/InstallerTest.php +++ b/tests/Composer/Test/InstallerTest.php @@ -230,9 +230,9 @@ class InstallerTest extends TestCase ->setDevMode(!$input->getOption('no-dev')) ->setUpdate(true) ->setDryRun($input->getOption('dry-run')) - ->setUpdateWhitelist($input->getArgument('packages')) - ->setWhitelistTransitiveDependencies($input->getOption('with-dependencies')) - ->setWhitelistAllDependencies($input->getOption('with-all-dependencies')) + ->setUpdateAllowList($input->getArgument('packages')) + ->setAllowListTransitiveDependencies($input->getOption('with-dependencies')) + ->setAllowListAllDependencies($input->getOption('with-all-dependencies')) ->setPreferStable($input->getOption('prefer-stable')) ->setPreferLowest($input->getOption('prefer-lowest')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')); From 491067f253f60b5f9c137236b3fbe36476c2a9e5 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Sun, 7 Jun 2020 22:31:24 +0100 Subject: [PATCH 12/41] Fixed wording --- doc/01-basic-usage.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/01-basic-usage.md b/doc/01-basic-usage.md index ac8086491..e6b1e6772 100644 --- a/doc/01-basic-usage.md +++ b/doc/01-basic-usage.md @@ -159,7 +159,7 @@ php composer.phar update > if the `composer.lock` has not been updated since changes were made to the > `composer.json` that might affect dependency resolution. -If you only want to install or update one dependency, you can allow list them: +If you only want to install or update one dependency, you can allow them: ```sh php composer.phar update monolog/monolog [...] From a97d13fc6db7a4eecb69aa41231d3eafc799ea20 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Mon, 8 Jun 2020 09:33:40 +0100 Subject: [PATCH 13/41] Fixed typo Co-authored-by: ZhangWei --- src/Composer/Command/RequireCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 45bd315fe..039250766 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -237,7 +237,7 @@ EOT ->setClassMapAuthoritative($authoritative) ->setApcuAutoloader($apcu) ->setUpdate(true) - ->setUpdatAllowList(array_keys($requirements)) + ->setUpdateAllowList(array_keys($requirements)) ->setAllowListTransitiveDependencies($input->getOption('update-with-dependencies')) ->setAllowListAllDependencies($input->getOption('update-with-all-dependencies')) ->setIgnorePlatformRequirements($input->getOption('ignore-platform-reqs')) From 13bdf8553a8a4a8ca369d95d75ac1ccdc207c30e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A4nz=20Friederes?= Date: Thu, 11 Jun 2020 21:53:31 +0200 Subject: [PATCH 14/41] Add setProcessedUrl method to PreFileDownloadEvent --- src/Composer/Downloader/FileDownloader.php | 1 + src/Composer/Plugin/PreFileDownloadEvent.php | 12 +++++++++++- src/Composer/Repository/ComposerRepository.php | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index d7c4bcabe..14ea40220 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -127,6 +127,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface if ($eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $httpDownloader, $url['processed']); $eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $url['processed'] = $preFileDownloadEvent->getProcessedUrl(); } $checksum = $package->getDistSha1Checksum(); diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index c2751da02..878ab851f 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -55,7 +55,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 +63,14 @@ class PreFileDownloadEvent extends Event { return $this->processedUrl; } + + /** + * Sets the processed URL that will be downloaded + * + * @return string + */ + public function setProcessedUrl($processedUrl) + { + $this->processedUrl = $processedUrl; + } } diff --git a/src/Composer/Repository/ComposerRepository.php b/src/Composer/Repository/ComposerRepository.php index 8848452f2..bc2844df9 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -1015,6 +1015,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $filename = $preFileDownloadEvent->getProcessedUrl(); } $response = $this->httpDownloader->get($filename, $this->options); @@ -1101,6 +1102,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $filename = $preFileDownloadEvent->getProcessedUrl(); } $options = $this->options; @@ -1167,6 +1169,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito if ($this->eventDispatcher) { $preFileDownloadEvent = new PreFileDownloadEvent(PluginEvents::PRE_FILE_DOWNLOAD, $this->httpDownloader, $filename); $this->eventDispatcher->dispatch($preFileDownloadEvent->getName(), $preFileDownloadEvent); + $filename = $preFileDownloadEvent->getProcessedUrl(); } $options = $lastModifiedTime ? array('http' => array('header' => array('If-Modified-Since: '.$lastModifiedTime))) : array(); From a17bbec842f515842b2c01c2b2b02b12ec276058 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 15 Jun 2020 13:04:02 +0200 Subject: [PATCH 15/41] Avoid double warnings about composer.json when running outdated, fixes #8958 --- src/Composer/Console/Application.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index cc39c587b..bc1cf0993 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') ?: '/'); From 54debe82102f72a80988c81b56e81d8b4f13cce5 Mon Sep 17 00:00:00 2001 From: johnstevenson Date: Fri, 28 Feb 2020 16:15:34 +0000 Subject: [PATCH 16/41] Respect disable-tls in Versions::getLatest Use http to get the latest version when disable-tls is true and error- trap DiagnoseCommand::checkVersion so that all checks can complete. Fixes #8657. --- src/Composer/Command/DiagnoseCommand.php | 6 +++++- src/Composer/SelfUpdate/Versions.php | 7 ++++++- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/src/Composer/Command/DiagnoseCommand.php b/src/Composer/Command/DiagnoseCommand.php index 6c9158630..b7739ff18 100644 --- a/src/Composer/Command/DiagnoseCommand.php +++ b/src/Composer/Command/DiagnoseCommand.php @@ -432,7 +432,11 @@ EOT } $versionsUtil = new Versions($config, $this->rfs); - $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/SelfUpdate/Versions.php b/src/Composer/SelfUpdate/Versions.php index 01d01e7e3..6a0a1bdbf 100644 --- a/src/Composer/SelfUpdate/Versions.php +++ b/src/Composer/SelfUpdate/Versions.php @@ -63,7 +63,12 @@ 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 = JsonFile::parseJson($this->rfs->getContents('getcomposer.org', $protocol . '://getcomposer.org/versions', false)); foreach ($versions[$channel ?: $this->getChannel()] as $version) { From 907367ff438481356ce86a1a27e297b02fd6c7f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A4nz=20Friederes?= Date: Mon, 15 Jun 2020 21:28:27 +0200 Subject: [PATCH 17/41] Fix PHPDoc issue --- src/Composer/Plugin/PreFileDownloadEvent.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index 878ab851f..2ae1e5380 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -67,7 +67,7 @@ class PreFileDownloadEvent extends Event /** * Sets the processed URL that will be downloaded * - * @return string + * @param string $processedUrl New processed URL */ public function setProcessedUrl($processedUrl) { From ae5904716610434a3fa62f123ebda7e2d5d79e84 Mon Sep 17 00:00:00 2001 From: Michael Stucki Date: Mon, 15 Jun 2020 21:42:41 +0200 Subject: [PATCH 18/41] Clean Git repos during discard --- src/Composer/Downloader/GitDownloader.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Composer/Downloader/GitDownloader.php b/src/Composer/Downloader/GitDownloader.php index edeaa7686..ede6dca10 100644 --- a/src/Composer/Downloader/GitDownloader.php +++ b/src/Composer/Downloader/GitDownloader.php @@ -451,7 +451,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()); } From 5c13c974286969e4c5298e74c1a3ee66ef6e2bef Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A4nz=20Friederes?= Date: Mon, 15 Jun 2020 21:43:41 +0200 Subject: [PATCH 19/41] Implement type and context properties in PreFileDownloadEvent --- src/Composer/Downloader/FileDownloader.php | 2 +- src/Composer/Plugin/PreFileDownloadEvent.php | 43 +++++++++++++++++-- .../Repository/ComposerRepository.php | 6 +-- 3 files changed, 43 insertions(+), 8 deletions(-) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 14ea40220..5ea4bdfe8 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -125,7 +125,7 @@ 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(); } diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index 2ae1e5380..03efe3ab2 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; } /** @@ -73,4 +87,25 @@ class PreFileDownloadEvent extends Event { $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 bc2844df9..6a2aba6ea 100644 --- a/src/Composer/Repository/ComposerRepository.php +++ b/src/Composer/Repository/ComposerRepository.php @@ -1013,7 +1013,7 @@ 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(); } @@ -1100,7 +1100,7 @@ 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(); } @@ -1167,7 +1167,7 @@ class ComposerRepository extends ArrayRepository implements ConfigurableReposito $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(); } From 6d9bf426553ea944b2d0931114b992d965ecf741 Mon Sep 17 00:00:00 2001 From: Michael Chekin Date: Tue, 16 Jun 2020 09:35:33 +0200 Subject: [PATCH 20/41] Additional Util\RemoteFileSystem tests (#8960) * RemoteFilesystemTest: simplifying some mock expectations calls - will($this->returnValue()) to willReturn() - will($this->returnCallBack()) to willReturnCallback() * RemoteFilesystemTest: extracting identical mocks for IOInterface into a separate getIOInterfaceMock() method * RemoteFilesystemTest: converting protected helper methods to private. * RemoteFilesystemTest: moving getConfigMock() private method after the public methods (with other private methods) * adding RemoteFileSystemTest::testCopyWithRetryAuthFailureFalse() unit test. * Allow optional injecting of AuthHelper into RemoteFilesystem constructor. * adding RemoteFileSystemTest::testCopyWithSuccessOnRetry() unit test. * using backward compatible @expectedException in RemoteFilesystemTest.php * RemoteFilesystemTest: extracting RemoteFilesystem with mocked method creation into a separate method. * RemoteFilesystemTest: extracting AuthHelper with mocked method creation into a separate method. --- src/Composer/Util/RemoteFilesystem.php | 5 +- .../Test/Util/RemoteFilesystemTest.php | 220 ++++++++++++++---- 2 files changed, 181 insertions(+), 44 deletions(-) diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index c7077afbb..b0dfbea94 100644 --- a/src/Composer/Util/RemoteFilesystem.php +++ b/src/Composer/Util/RemoteFilesystem.php @@ -54,8 +54,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 +71,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/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(); + } } From d906ff12c9257f50ade53d96f0e513f610b5dd87 Mon Sep 17 00:00:00 2001 From: Ayesh Karunaratne Date: Tue, 16 Jun 2020 15:05:44 +0700 Subject: [PATCH 21/41] PHPStan fixes: `autoload_files`, and `ignoreErrors` (#8974) * PHPStan: Remove autoload_files directive as it is not necessary anymore * PHPStan: Add error exclusions for sapi_windows_set_ctrl_handler function * PHPStan: Add error exclusions for ZipArchive::LIBZIP_VERSION * PHPStan: Require phpstan ^0.12.26 * Ensure zip ext is available on gh actions Co-authored-by: Jordi Boggiano --- .github/workflows/continuous-integration.yml | 2 +- .github/workflows/phpstan.yml | 4 ++-- phpstan/config.neon | 6 ++++-- 3 files changed, 7 insertions(+), 5 deletions(-) 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 87cd149eb..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 + 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/phpstan/config.neon b/phpstan/config.neon index d9bf7f8c6..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/*' @@ -27,6 +25,10 @@ parameters: # BC with older PHPUnit - '~^Call to an undefined static method PHPUnit\\Framework\\TestCase::setExpectedException\(\)\.$~' + + # ZipArchive::* Class constants are already checked before use. + - '~^Access to undefined constant ZipArchive::~' + paths: - ../src - ../tests From 8da2811dc3392c88d99c4b3011ce613cda5e7f8f Mon Sep 17 00:00:00 2001 From: Jonas Drieghe Date: Tue, 16 Jun 2020 10:07:53 +0200 Subject: [PATCH 22/41] Add new summary format for licenses (#8973) * Add new summary format to render the number of dependencies for each used license * Array dereferencing wasn't available on php 5.3 * Add summary format to documentation --- doc/03-cli.md | 2 +- src/Composer/Command/LicensesCommand.php | 23 +++++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) 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/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)); } From a797ee1322088415ce57639686562cdc74e8250d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 16 Jun 2020 11:56:08 +0200 Subject: [PATCH 23/41] Fix inline aliases not being loaded when extracting dev requirements, fixes #8954 --- .../DependencyResolver/PoolBuilder.php | 18 +--- src/Composer/Repository/RepositorySet.php | 35 +++++++- .../installer/aliases-with-require-dev.test | 90 +++++++++++++++++++ 3 files changed, 124 insertions(+), 19 deletions(-) create mode 100644 tests/Composer/Test/Fixtures/installer/aliases-with-require-dev.test diff --git a/src/Composer/DependencyResolver/PoolBuilder.php b/src/Composer/DependencyResolver/PoolBuilder.php index 29903f493..0da0eb87c 100644 --- a/src/Composer/DependencyResolver/PoolBuilder.php +++ b/src/Composer/DependencyResolver/PoolBuilder.php @@ -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; @@ -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/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index 24d935f4a..da352a7f3 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 = $this->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 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/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) From d5286d0cb8216d2a76f5e63c03e4bcb6f0fa0c0a Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 08:30:59 +0200 Subject: [PATCH 24/41] Add a way for FileDownloader subclasses to add paths to the cleanup stage --- src/Composer/Downloader/FileDownloader.php | 30 ++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index d7c4bcabe..f5674cf90 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -55,6 +55,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface * @private this is only public for php 5.3 support in closures */ public $lastCacheWrites = array(); + private $additionalCleanupPaths = array(); /** * Constructor. @@ -258,6 +259,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); @@ -291,6 +298,29 @@ 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} */ From 0dad963cd89be409c729830dea4128a021129090 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 08:33:44 +0200 Subject: [PATCH 25/41] Add executeAsync to ProcessExecutor and allow Loop class to wait on it in addition to HttpDownloader --- src/Composer/Factory.php | 2 +- src/Composer/Util/HttpDownloader.php | 73 ++++--- src/Composer/Util/Loop.php | 35 ++- src/Composer/Util/ProcessExecutor.php | 204 ++++++++++++++++++ .../Test/Downloader/FileDownloaderTest.php | 6 +- .../Test/Downloader/XzDownloaderTest.php | 2 +- .../Test/Downloader/ZipDownloaderTest.php | 2 +- .../Repository/ComposerRepositoryTest.php | 10 +- 8 files changed, 294 insertions(+), 40 deletions(-) diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 04da08e31..5bc41ee6b 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -336,7 +336,7 @@ class Factory $httpDownloader = self::createHttpDownloader($io, $config); $process = new ProcessExecutor($io); - $loop = new Loop($httpDownloader); + $loop = new Loop($httpDownloader, $process); $composer->setLoop($loop); // initialize event dispatcher diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 2fa8fa716..41ced41e3 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -44,6 +44,7 @@ class HttpDownloader private $rfs; private $idGen = 0; private $disabled; + private $allowAsync = false; /** * @param IOInterface $io The IO instance @@ -139,6 +140,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])); @@ -189,7 +194,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 +201,6 @@ class HttpDownloader $job['exception'] = $e; $downloader->markJobDone(); - $downloader->scheduleNextJob(); throw $e; }); @@ -251,13 +254,7 @@ class HttpDownloader public function markJobDone() { $this->runningJobs--; - } - /** - * @private - */ - public function scheduleNextJob() - { foreach ($this->jobs as $job) { if ($job['status'] === self::STATUS_QUEUED) { $this->startJob($job['id']); @@ -268,36 +265,52 @@ class HttpDownloader } } - 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->hasActiveJob($index)) { + return; } usleep(1000); } } + /** + * @internal + */ + public function enableAsync() + { + $this->allowAsync = true; + } + + /** + * @internal + */ + public function hasActiveJob($index = null) + { + 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 false; + } + return true; + } + + foreach ($this->jobs as $job) { + if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) { + return true; + } elseif (!$job['sync']) { + unset($this->jobs[$job['id']]); + } + } + + return false; + } + 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..b0061ba2d 100644 --- a/src/Composer/Util/Loop.php +++ b/src/Composer/Util/Loop.php @@ -21,10 +21,19 @@ 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) @@ -39,8 +48,30 @@ class Loop } ); - $this->httpDownloader->wait(); + $this->currentPromises = $promises; + while (true) { + $hasActiveJob = false; + + if ($this->httpDownloader) { + if ($this->httpDownloader->hasActiveJob()) { + $hasActiveJob = true; + } + } + if ($this->processExecutor) { + if ($this->processExecutor->hasActiveJob()) { + $hasActiveJob = true; + } + } + + if (!$hasActiveJob) { + break; + } + + usleep(5000); + } + + $this->currentPromises = null; if ($uncaught) { throw $uncaught; } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index a30a04d15..b443e541d 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,196 @@ 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 () use (&$job, $self) { + $job['status'] = ProcessExecutor::STATUS_FAILED; + + $self->markJobDone(); + + return \React\Promise\reject($job['process']); + }); + $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->hasActiveJob($index)) { + return; + } + + usleep(1000); + } + } + + /** + * @internal + */ + public function enableAsync() + { + $this->allowAsync = true; + } + + /** + * @internal + */ + public function hasActiveJob($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 (null !== $index) { + if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED || $this->jobs[$index]['status'] === self::STATUS_ABORTED) { + return false; + } + + return true; + } + + foreach ($this->jobs as $job) { + if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED, self::STATUS_ABORTED), true)) { + return true; + } else { + unset($this->jobs[$job['id']]); + } + } + + return false; + } + + /** + * @private + */ + public function markJobDone() + { + $this->runningJobs--; + + foreach ($this->jobs as $job) { + if ($job['status'] === self::STATUS_QUEUED) { + $this->startJob($job['id']); + if ($this->runningJobs >= $this->maxJobs) { + return; + } + } + } + } + public function splitLines($output) { $output = trim($output); 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 4436c6ad7..764af8feb 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -92,8 +92,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); 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( From 8f6e82f562ca2ba06c16d6325f079c386cdfdde0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 09:06:49 +0200 Subject: [PATCH 26/41] Add support for aborting running promises cleanly --- .../Installer/InstallationManager.php | 2 ++ src/Composer/Util/Http/CurlDownloader.php | 34 ++++++++++++++++--- src/Composer/Util/HttpDownloader.php | 26 +++++++++----- src/Composer/Util/Loop.php | 9 +++++ src/Composer/Util/ProcessExecutor.php | 8 ++--- src/Composer/Util/RemoteFilesystem.php | 1 + 6 files changed, 62 insertions(+), 18 deletions(-) diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 06e8d65b9..9e55c4ac7 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -184,6 +184,8 @@ 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(); 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 41ced41e3..6fe53390e 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; @@ -184,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) { @@ -242,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); } } @@ -294,14 +307,11 @@ class HttpDownloader } if (null !== $index) { - if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED) { - return false; - } - return true; + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED; } foreach ($this->jobs as $job) { - if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED), true)) { + if ($job['status'] < self::STATUS_COMPLETED) { return true; } elseif (!$job['sync']) { unset($this->jobs[$job['id']]); diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index b0061ba2d..b7382f1ed 100644 --- a/src/Composer/Util/Loop.php +++ b/src/Composer/Util/Loop.php @@ -76,4 +76,13 @@ class Loop 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 b443e541d..59db0c0c0 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -281,15 +281,11 @@ class ProcessExecutor } if (null !== $index) { - if ($this->jobs[$index]['status'] === self::STATUS_COMPLETED || $this->jobs[$index]['status'] === self::STATUS_FAILED || $this->jobs[$index]['status'] === self::STATUS_ABORTED) { - return false; - } - - return true; + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED; } foreach ($this->jobs as $job) { - if (!in_array($job['status'], array(self::STATUS_COMPLETED, self::STATUS_FAILED, self::STATUS_ABORTED), true)) { + if ($job['status'] < self::STATUS_COMPLETED) { return true; } else { unset($this->jobs[$job['id']]); diff --git a/src/Composer/Util/RemoteFilesystem.php b/src/Composer/Util/RemoteFilesystem.php index b0dfbea94..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 From 3af617efe892647ca1d2cfa59fa5c23215b82ba3 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 09:07:40 +0200 Subject: [PATCH 27/41] Parallelize zip extraction using async unzip processes --- src/Composer/Downloader/ArchiveDownloader.php | 97 +++++++++++-------- src/Composer/Downloader/ZipDownloader.php | 72 ++++++++++---- .../Test/Downloader/ZipDownloaderTest.php | 73 ++++++++++---- 3 files changed, 166 insertions(+), 76 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 283d0103e..a9091dbe6 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -16,6 +16,7 @@ use Composer\Package\PackageInterface; use Symfony\Component\Finder\Finder; use Composer\IO\IOInterface; use Composer\Exception\IrrecoverableDownloadException; +use React\Promise\PromiseInterface; /** * Base downloader for archives @@ -60,26 +61,62 @@ abstract class ArchiveDownloader extends FileDownloader $temporaryDir = $this->config->get('vendor-dir').'/composer/'.substr(md5(uniqid('', true)), 0, 8); } while (is_dir($temporaryDir)); + $this->addCleanupPath($package, $temporaryDir); + + $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); + }; + + $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 dotfiles + * + * @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 +126,27 @@ 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); + }, function ($e) use ($cleanup) { + $cleanup(); throw $e; - } + }); } /** @@ -119,25 +155,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/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index d0b4e8255..c5fd2b11b 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -86,9 +86,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 @@ -98,18 +97,47 @@ 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; + } + + $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) { + if (!$process->isSuccessful()) { + return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.$command."\n\n".$process->getErrorOutput())); + } + }); + } 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()); @@ -121,11 +149,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); } /** @@ -134,9 +162,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 @@ -146,7 +176,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; @@ -159,7 +189,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")); @@ -170,16 +200,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); } /** @@ -192,10 +224,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/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index 764af8feb..d86d2cdf1 100644 --- a/tests/Composer/Test/Downloader/ZipDownloaderTest.php +++ b/tests/Composer/Test/Downloader/ZipDownloaderTest.php @@ -179,37 +179,65 @@ class ZipDownloaderTest extends TestCase /** * @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, null, $processExecutor); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $e = null; + $promise->then(function () { + // noop + }, function ($ex) use (&$e) { + $e = $ex; + }); + + if ($e) { + throw $e; + } } 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, null, $processExecutor); $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); @@ -225,10 +253,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)) @@ -350,6 +389,6 @@ class MockedZipDownloader extends ZipDownloader public function extract(PackageInterface $package, $file, $path) { - parent::extract($package, $file, $path); + return parent::extract($package, $file, $path); } } From b1e15c77256dd5e05ceaef207dd5c6225061c4fa Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 09:39:11 +0200 Subject: [PATCH 28/41] Fix a couple async bugs --- src/Composer/Util/ProcessExecutor.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index 59db0c0c0..b59bced70 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -187,12 +187,12 @@ class ProcessExecutor $self->markJobDone(); return $job['process']; - }, function () use (&$job, $self) { + }, function ($e) use (&$job, $self) { $job['status'] = ProcessExecutor::STATUS_FAILED; $self->markJobDone(); - return \React\Promise\reject($job['process']); + throw $e; }); $this->jobs[$job['id']] =& $job; @@ -272,7 +272,7 @@ class ProcessExecutor public function hasActiveJob($index = null) { // tick - foreach ($this->jobs as &$job) { + foreach ($this->jobs as $job) { if ($job['status'] === self::STATUS_STARTED) { if (!$job['process']->isRunning()) { call_user_func($job['resolve'], $job['process']); From 9f380d606caa6901d10e5f3e1b917d605379cebd Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 10:01:48 +0200 Subject: [PATCH 29/41] Add basic progress bar capability while waiting for jobs to complete --- src/Composer/IO/ConsoleIO.php | 10 ++++++ .../Installer/InstallationManager.php | 10 +++++- src/Composer/Util/HttpDownloader.php | 30 ++++++++-------- src/Composer/Util/Loop.php | 34 +++++++++++++------ src/Composer/Util/ProcessExecutor.php | 28 +++++++-------- 5 files changed, 72 insertions(+), 40 deletions(-) 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/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 9e55c4ac7..47b3e2914 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; @@ -330,7 +331,14 @@ class InstallationManager // execute all prepare => installs/updates/removes => cleanup steps if (!empty($promises)) { - $this->loop->wait($promises); + $progress = null; + if ($io instanceof ConsoleIO && !$io->isDebug()) { + $progress = $io->getProgressBar(); + } + $this->loop->wait($promises, $progress); + if ($progress) { + $progress->clear(); + } } } catch (\Exception $e) { $runCleanup(); diff --git a/src/Composer/Util/HttpDownloader.php b/src/Composer/Util/HttpDownloader.php index 6fe53390e..889fae07e 100644 --- a/src/Composer/Util/HttpDownloader.php +++ b/src/Composer/Util/HttpDownloader.php @@ -267,21 +267,12 @@ class HttpDownloader public function markJobDone() { $this->runningJobs--; - - 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) { while (true) { - if (!$this->hasActiveJob($index)) { + if (!$this->countActiveJobs($index)) { return; } @@ -299,26 +290,37 @@ class HttpDownloader /** * @internal + * + * @return int number of active (queued or started) jobs */ - public function hasActiveJob($index = null) + 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; + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0; } + $active = 0; foreach ($this->jobs as $job) { if ($job['status'] < self::STATUS_COMPLETED) { - return true; + $active++; } elseif (!$job['sync']) { unset($this->jobs[$job['id']]); } } - return false; + return $active; } private function getResponse($index) diff --git a/src/Composer/Util/Loop.php b/src/Composer/Util/Loop.php index b7382f1ed..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 @@ -36,7 +37,7 @@ class Loop } } - public function wait(array $promises) + public function wait(array $promises, ProgressBar $progress = null) { /** @var \Exception|null */ $uncaught = null; @@ -50,21 +51,32 @@ class Loop $this->currentPromises = $promises; - while (true) { - $hasActiveJob = false; - + if ($progress) { + $totalJobs = 0; if ($this->httpDownloader) { - if ($this->httpDownloader->hasActiveJob()) { - $hasActiveJob = true; - } + $totalJobs += $this->httpDownloader->countActiveJobs(); } if ($this->processExecutor) { - if ($this->processExecutor->hasActiveJob()) { - $hasActiveJob = true; - } + $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 (!$hasActiveJob) { + if ($progress) { + $progress->setProgress($progress->getMaxSteps() - $activeJobs); + } + + if (!$activeJobs) { break; } diff --git a/src/Composer/Util/ProcessExecutor.php b/src/Composer/Util/ProcessExecutor.php index b59bced70..96b9235c8 100644 --- a/src/Composer/Util/ProcessExecutor.php +++ b/src/Composer/Util/ProcessExecutor.php @@ -250,7 +250,7 @@ class ProcessExecutor public function wait($index = null) { while (true) { - if (!$this->hasActiveJob($index)) { + if (!$this->countActiveJobs($index)) { return; } @@ -268,8 +268,10 @@ class ProcessExecutor /** * @internal + * + * @return int number of active (queued or started) jobs */ - public function hasActiveJob($index = null) + public function countActiveJobs($index = null) { // tick foreach ($this->jobs as $job) { @@ -278,21 +280,28 @@ class ProcessExecutor 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; + return $this->jobs[$index]['status'] < self::STATUS_COMPLETED ? 1 : 0; } + $active = 0; foreach ($this->jobs as $job) { if ($job['status'] < self::STATUS_COMPLETED) { - return true; + $active++; } else { unset($this->jobs[$job['id']]); } } - return false; + return $active; } /** @@ -301,15 +310,6 @@ class ProcessExecutor public function markJobDone() { $this->runningJobs--; - - foreach ($this->jobs as $job) { - if ($job['status'] === self::STATUS_QUEUED) { - $this->startJob($job['id']); - if ($this->runningJobs >= $this->maxJobs) { - return; - } - } - } } public function splitLines($output) From 87a0fc5506687186b426f8e4386fd491d52ea5cb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 11:11:31 +0200 Subject: [PATCH 30/41] Execute operations in batches to make sure plugins install in the expected order --- src/Composer/Downloader/ArchiveDownloader.php | 3 + .../Installer/InstallationManager.php | 147 ++++++++++-------- 2 files changed, 87 insertions(+), 63 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index a9091dbe6..bcee49f9a 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -62,6 +62,7 @@ abstract class ArchiveDownloader extends FileDownloader } while (is_dir($temporaryDir)); $this->addCleanupPath($package, $temporaryDir); + $this->addCleanupPath($package, $path); $this->filesystem->ensureDirectoryExists($temporaryDir); $fileName = $this->getFileName($package, $path); @@ -77,6 +78,7 @@ abstract class ArchiveDownloader extends FileDownloader $filesystem->removeDirectory($path); $filesystem->removeDirectory($temporaryDir); $self->removeCleanupPath($package, $temporaryDir); + $self->removeCleanupPath($package, $path); }; $promise = null; @@ -142,6 +144,7 @@ abstract class ArchiveDownloader extends FileDownloader $filesystem->removeDirectory($temporaryDir); $self->removeCleanupPath($package, $temporaryDir); + $self->removeCleanupPath($package, $path); }, function ($e) use ($cleanup) { $cleanup(); diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index 47b3e2914..54f359eaa 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -272,73 +272,23 @@ class InstallationManager $this->loop->wait($promises); } - foreach ($operations as $index => $operation) { - $opType = $operation->getOperationType(); + // 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 + while ($operations) { + $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)); + foreach ($operations as $index => $operation) { + unset($operations[$index]); + $batch[$index] = $operation; + 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') { + break; + } } - $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 (!$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 ($io instanceof ConsoleIO && !$io->isDebug()) { - $progress = $io->getProgressBar(); - } - $this->loop->wait($promises, $progress); - if ($progress) { - $progress->clear(); - } + $this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts); } } catch (\Exception $e) { $runCleanup(); @@ -366,6 +316,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 ($io instanceof ConsoleIO && !$io->isDebug() && count($promises) > 1) { + $progress = $io->getProgressBar(); + } + $this->loop->wait($promises, $progress); + if ($progress) { + $progress->clear(); + } + } + } + /** * Executes install operation. * From 9c78eda7db409125145aa2b94944c7fca55aeae1 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 13:15:50 +0200 Subject: [PATCH 31/41] Fix FileDownloader::update impl to handle promises --- src/Composer/Downloader/ArchiveDownloader.php | 4 +-- src/Composer/Downloader/FileDownloader.php | 29 ++++++++++++++----- src/Composer/Downloader/GzipDownloader.php | 9 ------ src/Composer/Downloader/PathDownloader.php | 9 ------ src/Composer/Downloader/RarDownloader.php | 9 ------ src/Composer/Downloader/XzDownloader.php | 10 ------- src/Composer/Downloader/ZipDownloader.php | 8 ----- src/Composer/Factory.php | 6 ++-- 8 files changed, 26 insertions(+), 58 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index bcee49f9a..d77d55738 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -29,14 +29,12 @@ 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); } /** diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index f5674cf90..5f88cf140 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -27,7 +27,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 @@ -51,6 +53,8 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface protected $cache; /** @var EventDispatcher */ protected $eventDispatcher; + /** @var ProcessExecutor */ + protected $process; /** * @private this is only public for php 5.3 support in closures */ @@ -67,14 +71,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')); @@ -333,10 +338,19 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $actionName = VersionParser::isUpgrade($initial->getVersion(), $target->getVersion()) ? 'Upgrading' : 'Downgrading'; $this->io->writeError(" - " . $actionName . " " . $name . " (" . $from . " => " . $to . "): ", false); - $this->remove($initial, $path, false); - $this->install($target, $path, false); + $promise = $this->remove($initial, $path, false); + if (!$promise instanceof PromiseInterface) { + $promise = \React\Promise\resolve(); + } + $self = $this; + $io = $this->io; - $this->io->writeError(''); + return $promise->then(function () use ($self, $target, $path, $io) { + $promise = $self->install($target, $path, false); + $io->writeError(''); + + return $promise; + }); } /** @@ -410,9 +424,10 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $output = ''; try { - $res = $this->download($package, $targetDir.'_compare', null, false); + $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/GzipDownloader.php b/src/Composer/Downloader/GzipDownloader.php index 0b12b4380..3ebff597a 100644 --- a/src/Composer/Downloader/GzipDownloader.php +++ b/src/Composer/Downloader/GzipDownloader.php @@ -29,15 +29,6 @@ use Composer\Util\Filesystem; */ class GzipDownloader extends ArchiveDownloader { - /** @var ProcessExecutor */ - protected $process; - - public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $fs = null, ProcessExecutor $process = null) - { - $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); - } - 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 51e9f7709..2cb74d641 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -39,15 +39,6 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter const STRATEGY_SYMLINK = 10; const STRATEGY_MIRROR = 20; - /** @var ProcessExecutor */ - private $process; - - public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $fs = null, ProcessExecutor $process = null) - { - $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); - } - /** * {@inheritdoc} */ diff --git a/src/Composer/Downloader/RarDownloader.php b/src/Composer/Downloader/RarDownloader.php index 0ca15de1f..b0484b661 100644 --- a/src/Composer/Downloader/RarDownloader.php +++ b/src/Composer/Downloader/RarDownloader.php @@ -33,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, Filesystem $fs = null, ProcessExecutor $process = null) - { - $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); - } - protected function extract(PackageInterface $package, $file, $path) { $processError = null; diff --git a/src/Composer/Downloader/XzDownloader.php b/src/Composer/Downloader/XzDownloader.php index 34956d997..b043af333 100644 --- a/src/Composer/Downloader/XzDownloader.php +++ b/src/Composer/Downloader/XzDownloader.php @@ -29,16 +29,6 @@ use Composer\Util\Filesystem; */ class XzDownloader extends ArchiveDownloader { - /** @var ProcessExecutor */ - protected $process; - - public function __construct(IOInterface $io, Config $config, HttpDownloader $downloader, EventDispatcher $eventDispatcher = null, Cache $cache = null, Filesystem $fs = null, ProcessExecutor $process = null) - { - $this->process = $process ?: new ProcessExecutor($io); - - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); - } - 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 c5fd2b11b..d891fb33b 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -34,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, Filesystem $fs = null, ProcessExecutor $process = null) - { - $this->process = $process ?: new ProcessExecutor($io); - parent::__construct($io, $config, $downloader, $eventDispatcher, $cache, $fs); - } - /** * {@inheritDoc} */ diff --git a/src/Composer/Factory.php b/src/Composer/Factory.php index 5bc41ee6b..509fa62b0 100644 --- a/src/Composer/Factory.php +++ b/src/Composer/Factory.php @@ -495,11 +495,11 @@ class Factory $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)); + $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)); - $dm->setDownloader('file', new Downloader\FileDownloader($io, $config, $httpDownloader, $eventDispatcher, $cache, $fs)); + $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; From 085fe4e7e5c9b1c0122322d62e94bba2c7f0fe4f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 13:43:21 +0200 Subject: [PATCH 32/41] Add --no-progress support and a few more fixes --- src/Composer/Command/ArchiveCommand.php | 6 +++-- src/Composer/Command/CreateProjectCommand.php | 9 +++++++- src/Composer/Command/InstallCommand.php | 2 ++ src/Composer/Command/RemoveCommand.php | 2 ++ src/Composer/Command/RequireCommand.php | 2 ++ src/Composer/Command/UpdateCommand.php | 2 ++ src/Composer/Installer.php | 2 +- .../Installer/InstallationManager.php | 22 +++++++++++++++---- 8 files changed, 39 insertions(+), 8 deletions(-) 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 de6701ce4..27d9919d7 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -201,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) @@ -212,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(); } @@ -405,7 +411,8 @@ EOT ->setPreferDist($preferDist); $projectInstaller = new ProjectInstaller($directory, $dm, $fs); - $im = $factory->createInstallationManager(new Loop($httpDownloader), $io); + $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/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/RemoveCommand.php b/src/Composer/Command/RemoveCommand.php index e540056f8..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'); diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index d8ba23458..9525b9e77 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -286,6 +286,8 @@ EOT $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/Installer.php b/src/Composer/Installer.php index 8c508eaa0..40a041927 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -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 54f359eaa..a9bbe7c3a 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -50,6 +50,8 @@ class InstallationManager private $io; /** @var EventDispatcher */ private $eventDispatcher; + /** @var bool */ + private $outputProgress; public function __construct(Loop $loop, IOInterface $io, EventDispatcher $eventDispatcher = null) { @@ -174,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) { @@ -269,7 +271,14 @@ 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 @@ -377,8 +386,8 @@ class InstallationManager // execute all prepare => installs/updates/removes => cleanup steps if (!empty($promises)) { $progress = null; - if ($io instanceof ConsoleIO && !$io->isDebug() && count($promises) > 1) { - $progress = $io->getProgressBar(); + if ($this->outputProgress && $this->io instanceof ConsoleIO && !$this->io->isDebug() && count($promises) > 1) { + $progress = $this->io->getProgressBar(); } $this->loop->wait($promises, $progress); if ($progress) { @@ -485,6 +494,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) { From ee58f25c001332bb8aaeaa442d6ca232093d911d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Fri, 5 Jun 2020 14:05:19 +0200 Subject: [PATCH 33/41] Fix ZipDownloaderTest --- .../Test/Downloader/ZipDownloaderTest.php | 74 ++++++++++++------- 1 file changed, 48 insertions(+), 26 deletions(-) diff --git a/tests/Composer/Test/Downloader/ZipDownloaderTest.php b/tests/Composer/Test/Downloader/ZipDownloaderTest.php index d86d2cdf1..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')) { @@ -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,7 +170,8 @@ 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); } /** @@ -205,16 +202,7 @@ class ZipDownloaderTest extends TestCase $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); - $e = null; - $promise->then(function () { - // noop - }, function ($ex) use (&$e) { - $e = $ex; - }); - - if ($e) { - throw $e; - } + $this->wait($promise); } public function testSystemUnzipOnlyGood() @@ -240,7 +228,8 @@ class ZipDownloaderTest extends TestCase ->will($this->returnValue(\React\Promise\resolve($procMock))); $downloader = new MockedZipDownloader($this->io, $this->config, $this->httpDownloader, null, null, null, $processExecutor); - $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $promise = $downloader->extract($this->package, 'testfile.zip', 'vendor/dir'); + $this->wait($promise); } public function testNonWindowsFallbackGood() @@ -279,7 +268,8 @@ class ZipDownloaderTest extends TestCase $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); } /** @@ -296,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)) @@ -311,7 +312,8 @@ class ZipDownloaderTest extends TestCase $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() @@ -339,7 +341,8 @@ class ZipDownloaderTest extends TestCase $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); } /** @@ -371,7 +374,26 @@ class ZipDownloaderTest extends TestCase $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; + } } } From aea074308c642e86377a2fa0fef7d0f97e5ebb26 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 16 Jun 2020 14:07:30 +0200 Subject: [PATCH 34/41] Update batching to install plugin deps before the plugin (alone an own batch) --- src/Composer/Downloader/ArchiveDownloader.php | 2 +- .../Installer/InstallationManager.php | 31 +++++++++++++------ 2 files changed, 22 insertions(+), 11 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index d77d55738..8f7f5ef83 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -95,7 +95,7 @@ abstract class ArchiveDownloader extends FileDownloader $filesystem->unlink($fileName); /** - * Returns the folder content, excluding dotfiles + * Returns the folder content, excluding .DS_Store * * @param string $dir Directory * @return \SplFileInfo[] diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index a9bbe7c3a..7dfb2ff3e 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -283,20 +283,31 @@ class InstallationManager // 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 - while ($operations) { - $batch = array(); - - foreach ($operations as $index => $operation) { - unset($operations[$index]); - $batch[$index] = $operation; - 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') { - break; + $batches = array(); + $batch = array(); + foreach ($operations as $index => $operation) { + 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(); + + continue; } } + unset($operations[$index]); + $batch[$index] = $operation; + } + if ($batch) { + $batches[] = $batch; + } + + foreach ($batches as $batch) { $this->executeBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts); } } catch (\Exception $e) { From a4f41013463412ec39139f3f745d522b663208f4 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 16 Jun 2020 13:46:20 +0100 Subject: [PATCH 35/41] Phpdoc tweaks --- src/Composer/Plugin/PreFileDownloadEvent.php | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/Composer/Plugin/PreFileDownloadEvent.php b/src/Composer/Plugin/PreFileDownloadEvent.php index 03efe3ab2..f7b523b94 100644 --- a/src/Composer/Plugin/PreFileDownloadEvent.php +++ b/src/Composer/Plugin/PreFileDownloadEvent.php @@ -69,7 +69,7 @@ class PreFileDownloadEvent extends Event } /** - * Retrieves the processed URL that will be downloaded + * Retrieves the processed URL that will be downloaded. * * @return string */ @@ -79,7 +79,7 @@ class PreFileDownloadEvent extends Event } /** - * Sets the processed URL that will be downloaded + * Sets the processed URL that will be downloaded. * * @param string $processedUrl New processed URL */ @@ -89,7 +89,7 @@ class PreFileDownloadEvent extends Event } /** - * Returns the type of this download (package, metadata) + * Returns the type of this download (package, metadata). * * @return string */ @@ -100,6 +100,7 @@ class PreFileDownloadEvent extends Event /** * Returns the context of this download, if any. + * * If this download is of type package, the package object is returned. * * @return mixed From 643852a2b070e7f29fd4e32df7e8dd8efb6806d3 Mon Sep 17 00:00:00 2001 From: Graham Campbell Date: Tue, 16 Jun 2020 13:48:59 +0100 Subject: [PATCH 36/41] Marked getRootAliasesPerPackage as static --- src/Composer/Repository/RepositorySet.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Composer/Repository/RepositorySet.php b/src/Composer/Repository/RepositorySet.php index da352a7f3..9417dec97 100644 --- a/src/Composer/Repository/RepositorySet.php +++ b/src/Composer/Repository/RepositorySet.php @@ -92,7 +92,7 @@ class RepositorySet */ public function __construct($minimumStability = 'stable', array $stabilityFlags = array(), array $rootAliases = array(), array $rootReferences = array(), array $rootRequires = array()) { - $this->rootAliases = $this->getRootAliasesPerPackage($rootAliases); + $this->rootAliases = self::getRootAliasesPerPackage($rootAliases); $this->rootReferences = $rootReferences; $this->acceptableStabilities = array(); @@ -286,7 +286,7 @@ class RepositorySet return $this->createPool($request, new NullIO()); } - private function getRootAliasesPerPackage(array $aliases) + private static function getRootAliasesPerPackage(array $aliases) { $normalizedAliases = array(); From 09fc263d373a4c44c62c71ef56f209d1daf22d92 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 16 Jun 2020 16:27:36 +0200 Subject: [PATCH 37/41] Fix status command bug --- src/Composer/Downloader/ArchiveDownloader.php | 2 +- src/Composer/Downloader/FileDownloader.php | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 8f7f5ef83..14b434b74 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -29,7 +29,7 @@ abstract class ArchiveDownloader extends FileDownloader { public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { - // 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 not upgrading/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); } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index b965e62d4..3da51d39b 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -425,6 +425,10 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface $output = ''; try { + if (is_dir($targetDir.'_compare')) { + $this->filesystem->removeDirectory($targetDir.'_compare'); + } + $this->download($package, $targetDir.'_compare', null, false); $this->httpDownloader->wait(); $this->install($package, $targetDir.'_compare', false); From e5fe35d5541d2081c447d2ea16b246186d5bfb5f Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 09:24:25 +0200 Subject: [PATCH 38/41] Update test description --- .../Test/Fixtures/installer/update-with-all-dependencies.test | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 e0c120714..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::allowListUpdateDependencies should update the dependencies of all allowed 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-- { From d1fedc3bd62cea10bc847a980fdb874af19b2b8d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 10:42:05 +0200 Subject: [PATCH 39/41] Restore old behavior of wiping dir contents before installing, fixes #8988 --- src/Composer/Downloader/ArchiveDownloader.php | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/Composer/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index 14b434b74..a78aa95bc 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -29,11 +29,6 @@ abstract class ArchiveDownloader extends FileDownloader { public function download(PackageInterface $package, $path, PackageInterface $prevPackage = null, $output = true) { - // if not upgrading/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 parent::download($package, $path, $prevPackage, $output); } @@ -50,10 +45,7 @@ abstract class ArchiveDownloader extends FileDownloader $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); From aaef3ff5ff6827532f8ad9950c3971cc9236da93 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 10:42:37 +0200 Subject: [PATCH 40/41] Improve error reporting when unzip fails due to race condition in unhandled Promise, refs #8988 --- src/Composer/Downloader/ZipDownloader.php | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/src/Composer/Downloader/ZipDownloader.php b/src/Composer/Downloader/ZipDownloader.php index d891fb33b..83b904a8a 100644 --- a/src/Composer/Downloader/ZipDownloader.php +++ b/src/Composer/Downloader/ZipDownloader.php @@ -104,9 +104,15 @@ class ZipDownloader extends ArchiveDownloader throw $processError; } - $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'); + 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); }; @@ -114,9 +120,12 @@ class ZipDownloader extends ArchiveDownloader try { $promise = $this->process->executeAsync($command); - return $promise->then(function ($process) use ($tryFallback, $command, $package) { + return $promise->then(function ($process) use ($tryFallback, $command, $package, $file) { if (!$process->isSuccessful()) { - return $tryFallback(new \RuntimeException('Failed to extract '.$package->getName().': ('.$process->getExitCode().') '.$command."\n\n".$process->getErrorOutput())); + $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) { From 83c64a9d191b64689b408906d40943958a6a8eb0 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Wed, 17 Jun 2020 12:26:32 +0200 Subject: [PATCH 41/41] Reuse operation formatting logic in downloaders --- .../Operation/InstallOperation.php | 7 +++++- .../Operation/UninstallOperation.php | 7 +++++- .../Operation/UpdateOperation.php | 25 +++++++++++-------- src/Composer/Downloader/ArchiveDownloader.php | 3 ++- src/Composer/Downloader/FileDownloader.php | 14 +++++------ src/Composer/Downloader/PathDownloader.php | 19 ++++++-------- src/Composer/Downloader/VcsDownloader.php | 25 +++++-------------- .../Installer/MetapackageInstaller.php | 13 +++++----- src/Composer/Package/BasePackage.php | 2 +- .../Composer/Test/Package/BasePackageTest.php | 2 +- 10 files changed, 56 insertions(+), 61 deletions(-) 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/Downloader/ArchiveDownloader.php b/src/Composer/Downloader/ArchiveDownloader.php index a78aa95bc..7cf19deee 100644 --- a/src/Composer/Downloader/ArchiveDownloader.php +++ b/src/Composer/Downloader/ArchiveDownloader.php @@ -17,6 +17,7 @@ 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 @@ -40,7 +41,7 @@ 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); } diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index 3da51d39b..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; @@ -284,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); @@ -332,12 +335,7 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface */ public function update(PackageInterface $initial, PackageInterface $target, $path) { - $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 . "): ", false); + $this->io->writeError(" - " . UpdateOperation::format($initial, $target) . ": ", false); $promise = $this->remove($initial, $path, false); if (!$promise instanceof PromiseInterface) { @@ -360,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.'); diff --git a/src/Composer/Downloader/PathDownloader.php b/src/Composer/Downloader/PathDownloader.php index 2cb74d641..190267c5c 100644 --- a/src/Composer/Downloader/PathDownloader.php +++ b/src/Composer/Downloader/PathDownloader.php @@ -27,6 +27,9 @@ use Composer\Util\Filesystem; use Composer\EventDispatcher\EventDispatcher; use Symfony\Component\Filesystem\Exception\IOException; 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. @@ -82,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); } @@ -124,11 +123,7 @@ class PathDownloader extends FileDownloader implements VcsCapableDownloaderInter $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; @@ -187,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; @@ -200,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?"); 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/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/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/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']));