From 8ed7c4617907e9ddd2a56631800c82a382e8d7dd Mon Sep 17 00:00:00 2001 From: Jellyfrog Date: Wed, 12 Oct 2022 13:56:35 +0200 Subject: [PATCH] Add download-only mode (#11041) composer install --download-only to prime the cache/download archives but not do any actual of the actual installing Fixes #11035 Co-authored-by: Jordi Boggiano --- doc/03-cli.md | 1 + src/Composer/Command/InstallCommand.php | 2 + src/Composer/Downloader/FileDownloader.php | 3 +- src/Composer/Installer.php | 20 +++- .../Installer/InstallationManager.php | 109 +++++++++++------- .../Test/Mock/InstallationManagerMock.php | 2 +- 6 files changed, 93 insertions(+), 44 deletions(-) diff --git a/doc/03-cli.md b/doc/03-cli.md index cb2e6d0d4..7b5718ebc 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -107,6 +107,7 @@ resolution. * **--dry-run:** If you want to run through an installation without actually installing a package, you can use `--dry-run`. This will simulate the installation and show you what would happen. +* **--download-only:** Download only, do not install packages. * **--dev:** Install packages listed in `require-dev` (this is the default behavior). * **--no-dev:** Skip installing packages listed in `require-dev`. The autoloader generation skips the `autoload-dev` rules. Also see [COMPOSER_NO_DEV](#composer-no-dev). diff --git a/src/Composer/Command/InstallCommand.php b/src/Composer/Command/InstallCommand.php index 87d7986da..a469837bb 100644 --- a/src/Composer/Command/InstallCommand.php +++ b/src/Composer/Command/InstallCommand.php @@ -46,6 +46,7 @@ class InstallCommand extends BaseCommand new InputOption('prefer-dist', null, InputOption::VALUE_NONE, 'Forces installation from package dist (default behavior).'), new InputOption('prefer-install', null, InputOption::VALUE_REQUIRED, 'Forces installation from package dist|source|auto (auto chooses source for dev versions, dist for the rest).', null, $this->suggestPreferInstall()), new InputOption('dry-run', null, InputOption::VALUE_NONE, 'Outputs the operations but will not execute anything (implicitly enables --verbose).'), + new InputOption('download-only', null, InputOption::VALUE_NONE, 'Download only, do not install packages.'), new InputOption('dev', null, InputOption::VALUE_NONE, 'DEPRECATED: Enables installation of require-dev packages (enabled by default, only present for BC).'), new InputOption('no-suggest', null, InputOption::VALUE_NONE, 'DEPRECATED: This flag does not exist anymore.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'), @@ -124,6 +125,7 @@ EOT $install ->setDryRun($input->getOption('dry-run')) + ->setDownloadOnly($input->getOption('download-only')) ->setVerbose($input->getOption('verbose')) ->setPreferSource($preferSource) ->setPreferDist($preferDist) diff --git a/src/Composer/Downloader/FileDownloader.php b/src/Composer/Downloader/FileDownloader.php index b8907553a..87760644f 100644 --- a/src/Composer/Downloader/FileDownloader.php +++ b/src/Composer/Downloader/FileDownloader.php @@ -307,9 +307,10 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface } $dirsToCleanUp = [ + $path, + $this->config->get('vendor-dir').'/'.explode('/', $package->getPrettyName())[0], $this->config->get('vendor-dir').'/composer/', $this->config->get('vendor-dir'), - $path, ]; if (isset($this->additionalCleanupPaths[$package->getName()])) { diff --git a/src/Composer/Installer.php b/src/Composer/Installer.php index 38708c496..4c3062e9f 100644 --- a/src/Composer/Installer.php +++ b/src/Composer/Installer.php @@ -148,6 +148,8 @@ class Installer /** @var bool */ protected $dryRun = false; /** @var bool */ + protected $downloadOnly = false; + /** @var bool */ protected $verbose = false; /** @var bool */ protected $update = false; @@ -258,6 +260,10 @@ class Installer $this->mockLocalRepositories($this->repositoryManager); } + if ($this->downloadOnly) { + $this->dumpAutoloader = false; + } + if ($this->update && !$this->install) { $this->dumpAutoloader = false; } @@ -783,7 +789,7 @@ class Installer if ($this->executeOperations) { $localRepo->setDevPackageNames($this->locker->getDevPackageNames()); - $this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode, $this->runScripts); + $this->installationManager->execute($localRepo, $localRepoTransaction->getOperations(), $this->devMode, $this->runScripts, $this->downloadOnly); } else { foreach ($localRepoTransaction->getOperations() as $operation) { // output op, but alias op only in debug verbosity @@ -1088,6 +1094,18 @@ class Installer return $this->dryRun; } + /** + * Whether to download only or not. + * + * @return Installer + */ + public function setDownloadOnly(bool $downloadOnly = true): self + { + $this->downloadOnly = $downloadOnly; + + return $this; + } + /** * prefer source installation * diff --git a/src/Composer/Installer/InstallationManager.php b/src/Composer/Installer/InstallationManager.php index ecec136a4..f7e1b9f9d 100644 --- a/src/Composer/Installer/InstallationManager.php +++ b/src/Composer/Installer/InstallationManager.php @@ -172,44 +172,20 @@ class InstallationManager /** * Executes solver operation. * - * @param InstalledRepositoryInterface $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 $runScripts whether to dispatch script events + * @param InstalledRepositoryInterface $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 $runScripts whether to dispatch script events + * @param bool $downloadOnly whether to only download packages */ - public function execute(InstalledRepositoryInterface $repo, array $operations, bool $devMode = true, bool $runScripts = true): void + public function execute(InstalledRepositoryInterface $repo, array $operations, bool $devMode = true, bool $runScripts = true, bool $downloadOnly = false): void { - /** @var PromiseInterface[] */ + /** @var array */ $cleanupPromises = []; - $loop = $this->loop; - $io = $this->io; - $runCleanup = static function () use (&$cleanupPromises, $loop): void { - $promises = []; - - $loop->abortJobs(); - - foreach ($cleanupPromises as $cleanup) { - $promises[] = new \React\Promise\Promise(static function ($resolve, $reject) use ($cleanup): void { - $promise = $cleanup(); - if (!$promise instanceof PromiseInterface) { - $resolve(); - } else { - $promise->then(static function () use ($resolve): void { - $resolve(); - }); - } - }); - } - - if (!empty($promises)) { - $loop->wait($promises); - } - }; - - $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], static function (string $signal, SignalHandler $handler) use ($io, $runCleanup) { - $io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG); - $runCleanup(); + $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) use (&$cleanupPromises) { + $this->io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG); + $this->runCleanup($cleanupPromises); $handler->exitWithLastSignal(); }); @@ -239,16 +215,20 @@ class InstallationManager } foreach ($batches as $batch) { - $this->downloadAndExecuteBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $operations); + $this->downloadAndExecuteBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $downloadOnly, $operations); } } catch (\Exception $e) { - $runCleanup(); + $this->runCleanup($cleanupPromises); throw $e; } finally { $signalHandler->unregister(); } + if ($downloadOnly) { + return; + } + // do a last write so that we write the repository even if nothing changed // as that can trigger an update of some files like InstalledVersions.php if // running a new composer version @@ -257,10 +237,10 @@ class InstallationManager /** * @param OperationInterface[] $operations List of operations to execute in this batch - * @param PromiseInterface[] $cleanupPromises + * @param array $cleanupPromises * @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners */ - private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, array $operations, array &$cleanupPromises, bool $devMode, bool $runScripts, array $allOperations): void + private function downloadAndExecuteBatch(InstalledRepositoryInterface $repo, array $operations, array &$cleanupPromises, bool $devMode, bool $runScripts, bool $downloadOnly, array $allOperations): void { $promises = []; @@ -283,11 +263,11 @@ class InstallationManager } $installer = $this->getInstaller($package->getType()); - $cleanupPromises[$index] = static function () use ($opType, $installer, $package, $initialPackage) { + $cleanupPromises[$index] = static function () use ($opType, $installer, $package, $initialPackage): ?PromiseInterface { // avoid calling cleanup if the download was not even initialized for a package // as without installation source configured nothing will work if (!$package->getInstallationSource()) { - return; + return \React\Promise\resolve(); } return $installer->cleanup($opType, $package, $initialPackage); @@ -306,6 +286,11 @@ class InstallationManager $this->waitOnPromises($promises); } + if ($downloadOnly) { + $this->runCleanup($cleanupPromises); + return; + } + // execute operations in batches to make sure every plugin is installed in the // right order and activated before the packages depending on it are installed $batches = []; @@ -337,7 +322,7 @@ class InstallationManager /** * @param OperationInterface[] $operations List of operations to execute in this batch - * @param PromiseInterface[] $cleanupPromises + * @param array $cleanupPromises * @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners */ private function executeBatch(InstalledRepositoryInterface $repo, array $operations, array $cleanupPromises, bool $devMode, bool $runScripts, array $allOperations): void @@ -368,6 +353,7 @@ class InstallationManager $package = $operation->getPackage(); $initialPackage = null; } + $installer = $this->getInstaller($package->getType()); $eventName = [ @@ -451,6 +437,19 @@ class InstallationManager } } + /** + * Executes download operation. + * + * $param PackageInterface $package + */ + public function download(PackageInterface $package): ?PromiseInterface + { + $installer = $this->getInstaller($package->getType()); + $promise = $installer->cleanup("install", $package); + + return $promise; + } + /** * Executes install operation. * @@ -637,4 +636,32 @@ class InstallationManager $this->notifiablePackages[$package->getNotificationUrl()][$package->getName()] = $package; } } + + /** + * @param array $cleanupPromises + * @return void + */ + private function runCleanup(array $cleanupPromises): void + { + $promises = []; + + $this->loop->abortJobs(); + + foreach ($cleanupPromises as $cleanup) { + $promises[] = new \React\Promise\Promise(static function ($resolve, $reject) use ($cleanup): void { + $promise = $cleanup(); + if (!$promise instanceof PromiseInterface) { + $resolve(); + } else { + $promise->then(static function () use ($resolve): void { + $resolve(); + }); + } + }); + } + + if (!empty($promises)) { + $this->loop->wait($promises); + } + } } diff --git a/tests/Composer/Test/Mock/InstallationManagerMock.php b/tests/Composer/Test/Mock/InstallationManagerMock.php index bfdc580e4..9aaeadd60 100644 --- a/tests/Composer/Test/Mock/InstallationManagerMock.php +++ b/tests/Composer/Test/Mock/InstallationManagerMock.php @@ -46,7 +46,7 @@ class InstallationManagerMock extends InstallationManager { } - public function execute(InstalledRepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true): void + public function execute(InstalledRepositoryInterface $repo, array $operations, $devMode = true, $runScripts = true, $downloadOnly = false): void { foreach ($operations as $operation) { $method = $operation->getOperationType();