1
0
Fork 0

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 <j.boggiano@seld.be>
pull/11115/head
Jellyfrog 2022-10-12 13:56:35 +02:00 committed by GitHub
parent 7df744531b
commit 8ed7c46179
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 93 additions and 44 deletions

View File

@ -107,6 +107,7 @@ resolution.
* **--dry-run:** If you want to run through an installation without actually * **--dry-run:** If you want to run through an installation without actually
installing a package, you can use `--dry-run`. This will simulate the installing a package, you can use `--dry-run`. This will simulate the
installation and show you what would happen. 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). * **--dev:** Install packages listed in `require-dev` (this is the default behavior).
* **--no-dev:** Skip installing packages listed in `require-dev`. The autoloader * **--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). generation skips the `autoload-dev` rules. Also see [COMPOSER_NO_DEV](#composer-no-dev).

View File

@ -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-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('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('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('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-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.'), new InputOption('no-dev', null, InputOption::VALUE_NONE, 'Disables installation of require-dev packages.'),
@ -124,6 +125,7 @@ EOT
$install $install
->setDryRun($input->getOption('dry-run')) ->setDryRun($input->getOption('dry-run'))
->setDownloadOnly($input->getOption('download-only'))
->setVerbose($input->getOption('verbose')) ->setVerbose($input->getOption('verbose'))
->setPreferSource($preferSource) ->setPreferSource($preferSource)
->setPreferDist($preferDist) ->setPreferDist($preferDist)

View File

@ -307,9 +307,10 @@ class FileDownloader implements DownloaderInterface, ChangeReportInterface
} }
$dirsToCleanUp = [ $dirsToCleanUp = [
$path,
$this->config->get('vendor-dir').'/'.explode('/', $package->getPrettyName())[0],
$this->config->get('vendor-dir').'/composer/', $this->config->get('vendor-dir').'/composer/',
$this->config->get('vendor-dir'), $this->config->get('vendor-dir'),
$path,
]; ];
if (isset($this->additionalCleanupPaths[$package->getName()])) { if (isset($this->additionalCleanupPaths[$package->getName()])) {

View File

@ -148,6 +148,8 @@ class Installer
/** @var bool */ /** @var bool */
protected $dryRun = false; protected $dryRun = false;
/** @var bool */ /** @var bool */
protected $downloadOnly = false;
/** @var bool */
protected $verbose = false; protected $verbose = false;
/** @var bool */ /** @var bool */
protected $update = false; protected $update = false;
@ -258,6 +260,10 @@ class Installer
$this->mockLocalRepositories($this->repositoryManager); $this->mockLocalRepositories($this->repositoryManager);
} }
if ($this->downloadOnly) {
$this->dumpAutoloader = false;
}
if ($this->update && !$this->install) { if ($this->update && !$this->install) {
$this->dumpAutoloader = false; $this->dumpAutoloader = false;
} }
@ -783,7 +789,7 @@ class Installer
if ($this->executeOperations) { if ($this->executeOperations) {
$localRepo->setDevPackageNames($this->locker->getDevPackageNames()); $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 { } else {
foreach ($localRepoTransaction->getOperations() as $operation) { foreach ($localRepoTransaction->getOperations() as $operation) {
// output op, but alias op only in debug verbosity // output op, but alias op only in debug verbosity
@ -1088,6 +1094,18 @@ class Installer
return $this->dryRun; 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 * prefer source installation
* *

View File

@ -172,44 +172,20 @@ class InstallationManager
/** /**
* Executes solver operation. * Executes solver operation.
* *
* @param InstalledRepositoryInterface $repo repository in which to add/remove/update packages * @param InstalledRepositoryInterface $repo repository in which to add/remove/update packages
* @param OperationInterface[] $operations operations to execute * @param OperationInterface[] $operations operations to execute
* @param bool $devMode whether the install is being run in dev mode * @param bool $devMode whether the install is being run in dev mode
* @param bool $runScripts whether to dispatch script events * @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<callable(): ?PromiseInterface> */
$cleanupPromises = []; $cleanupPromises = [];
$loop = $this->loop; $signalHandler = SignalHandler::create([SignalHandler::SIGINT, SignalHandler::SIGTERM, SignalHandler::SIGHUP], function (string $signal, SignalHandler $handler) use (&$cleanupPromises) {
$io = $this->io; $this->io->writeError('Received '.$signal.', aborting', true, IOInterface::DEBUG);
$runCleanup = static function () use (&$cleanupPromises, $loop): void { $this->runCleanup($cleanupPromises);
$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();
$handler->exitWithLastSignal(); $handler->exitWithLastSignal();
}); });
@ -239,16 +215,20 @@ class InstallationManager
} }
foreach ($batches as $batch) { foreach ($batches as $batch) {
$this->downloadAndExecuteBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $operations); $this->downloadAndExecuteBatch($repo, $batch, $cleanupPromises, $devMode, $runScripts, $downloadOnly, $operations);
} }
} catch (\Exception $e) { } catch (\Exception $e) {
$runCleanup(); $this->runCleanup($cleanupPromises);
throw $e; throw $e;
} finally { } finally {
$signalHandler->unregister(); $signalHandler->unregister();
} }
if ($downloadOnly) {
return;
}
// do a last write so that we write the repository even if nothing changed // 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 // as that can trigger an update of some files like InstalledVersions.php if
// running a new composer version // running a new composer version
@ -257,10 +237,10 @@ class InstallationManager
/** /**
* @param OperationInterface[] $operations List of operations to execute in this batch * @param OperationInterface[] $operations List of operations to execute in this batch
* @param PromiseInterface[] $cleanupPromises * @param array<callable(): ?PromiseInterface> $cleanupPromises
* @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners * @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 = []; $promises = [];
@ -283,11 +263,11 @@ class InstallationManager
} }
$installer = $this->getInstaller($package->getType()); $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 // avoid calling cleanup if the download was not even initialized for a package
// as without installation source configured nothing will work // as without installation source configured nothing will work
if (!$package->getInstallationSource()) { if (!$package->getInstallationSource()) {
return; return \React\Promise\resolve();
} }
return $installer->cleanup($opType, $package, $initialPackage); return $installer->cleanup($opType, $package, $initialPackage);
@ -306,6 +286,11 @@ class InstallationManager
$this->waitOnPromises($promises); $this->waitOnPromises($promises);
} }
if ($downloadOnly) {
$this->runCleanup($cleanupPromises);
return;
}
// execute operations in batches to make sure every plugin is installed in the // 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 // right order and activated before the packages depending on it are installed
$batches = []; $batches = [];
@ -337,7 +322,7 @@ class InstallationManager
/** /**
* @param OperationInterface[] $operations List of operations to execute in this batch * @param OperationInterface[] $operations List of operations to execute in this batch
* @param PromiseInterface[] $cleanupPromises * @param array<callable(): ?PromiseInterface> $cleanupPromises
* @param OperationInterface[] $allOperations Complete list of operations to be executed in the install job, used for event listeners * @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 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(); $package = $operation->getPackage();
$initialPackage = null; $initialPackage = null;
} }
$installer = $this->getInstaller($package->getType()); $installer = $this->getInstaller($package->getType());
$eventName = [ $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. * Executes install operation.
* *
@ -637,4 +636,32 @@ class InstallationManager
$this->notifiablePackages[$package->getNotificationUrl()][$package->getName()] = $package; $this->notifiablePackages[$package->getNotificationUrl()][$package->getName()] = $package;
} }
} }
/**
* @param array<callable(): ?PromiseInterface> $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);
}
}
} }

View File

@ -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) { foreach ($operations as $operation) {
$method = $operation->getOperationType(); $method = $operation->getOperationType();