From 500efbe233d82526f5a52d1ee9d51081f97ad7f1 Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Tue, 25 May 2021 22:30:15 +0200 Subject: [PATCH 1/3] Add a reinstall command, fixes #3112 --- doc/03-cli.md | 47 ++++++++ src/Composer/Command/ReinstallCommand.php | 139 ++++++++++++++++++++++ src/Composer/Console/Application.php | 1 + 3 files changed, 187 insertions(+) create mode 100644 src/Composer/Command/ReinstallCommand.php diff --git a/doc/03-cli.md b/doc/03-cli.md index 6e7de1333..3b3b44676 100644 --- a/doc/03-cli.md +++ b/doc/03-cli.md @@ -313,6 +313,53 @@ uninstalled. * **--apcu-autoloader-prefix:** Use a custom prefix for the APCu autoloader cache. Implicitly enables `--apcu-autoloader`. +## reinstall + +The `reinstall` command looks up installed packages by name, +uninstalls them and reinstalls them. This lets you do a clean install +of a package if you messed with its files, or if you wish to change +the installation type using --prefer-install. + +```sh +php composer.phar reinstall acme/foo acme/bar +``` + +You can specify more than one package name to reinstall, or use a +wildcard to select several packages at once: + +```sh +php composer.phar reinstall "acme/*" +``` + +### Options + +* **--prefer-install:** There are two ways of downloading a package: `source` + and `dist`. Composer uses `dist` by default. If you pass + `--prefer-install=source` (or `--prefer-source`) Composer will install from + `source` if there is one. This is useful if you want to make a bugfix to a + project and get a local git clone of the dependency directly. + To get the legacy behavior where Composer use `source` automatically for dev + versions of packages, use `--prefer-install=auto`. See also [config.preferred-install](06-config.md#preferred-install). + Passing this flag will override the config value. +* **--no-autoloader:** Skips autoloader generation. +* **--no-scripts:** Skips execution of scripts defined in `composer.json`. +* **--no-progress:** Removes the progress display that can mess with some + terminals or scripts which don't handle backspace characters. +* **--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. +* **--classmap-authoritative (-a):** Autoload classes from the classmap only. + Implicitly enables `--optimize-autoloader`. +* **--apcu-autoloader:** Use APCu to cache found/not-found classes. +* **--apcu-autoloader-prefix:** Use a custom prefix for the APCu autoloader cache. + Implicitly enables `--apcu-autoloader`. +* **--ignore-platform-reqs:** ignore all platform requirements. This only + has an effect in the context of the autoloader generation for the + reinstall command. +* **--ignore-platform-req:** ignore a specific platform requirement. This only + has an effect in the context of the autoloader generation for the + reinstall command. + ## check-platform-reqs The check-platform-reqs command checks that your PHP and extensions versions diff --git a/src/Composer/Command/ReinstallCommand.php b/src/Composer/Command/ReinstallCommand.php new file mode 100644 index 000000000..38b14189c --- /dev/null +++ b/src/Composer/Command/ReinstallCommand.php @@ -0,0 +1,139 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Command; + +use Composer\DependencyResolver\Operation\InstallOperation; +use Composer\DependencyResolver\Operation\UninstallOperation; +use Composer\Package\BasePackage; +use Composer\Plugin\CommandEvent; +use Composer\Plugin\PluginEvents; +use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Input\InputOption; +use Symfony\Component\Console\Input\InputArgument; +use Symfony\Component\Console\Output\OutputInterface; + +/** + * @author Jordi Boggiano + */ +class ReinstallCommand extends BaseCommand +{ + protected function configure() + { + $this + ->setName('reinstall') + ->setDescription('Uninstalls and reinstalls the given package names') + ->setDefinition(array( + new InputOption('prefer-source', null, InputOption::VALUE_NONE, 'Forces installation from package sources when possible, including VCS information.'), + 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).'), + new InputOption('no-autoloader', null, InputOption::VALUE_NONE, 'Skips autoloader generation'), + 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('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`.'), + new InputOption('apcu-autoloader', null, InputOption::VALUE_NONE, 'Use APCu to cache found/not-found classes.'), + new InputOption('apcu-autoloader-prefix', null, InputOption::VALUE_REQUIRED, 'Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader'), + new InputOption('ignore-platform-req', null, InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY, 'Ignore a specific platform requirement (php & ext- packages).'), + new InputOption('ignore-platform-reqs', null, InputOption::VALUE_NONE, 'Ignore all platform requirements (php & ext- packages).'), + new InputArgument('packages', InputArgument::IS_ARRAY | InputArgument::REQUIRED, 'List of package names to reinstall, can include a wildcard (*) to match any substring.'), + )) + ->setHelp( + <<reinstall command looks up installed packages by name, +uninstalls them and reinstalls them. This lets you do a clean install +of a package if you messed with its files, or if you wish to change +the installation type using --prefer-install. + +php composer.phar reinstall acme/foo "acme/bar-*" + +Read more at https://getcomposer.org/doc/03-cli.md#reinstall +EOT + ) + ; + } + + protected function execute(InputInterface $input, OutputInterface $output) + { + $io = $this->getIO(); + + $composer = $this->getComposer(true, $input->getOption('no-plugins')); + + $localRepo = $composer->getRepositoryManager()->getLocalRepository(); + $packages = array(); + foreach ($input->getArgument('packages') as $pattern) { + $patternRegexp = BasePackage::packageNameToRegexp($pattern); + $matched = false; + foreach ($localRepo->getCanonicalPackages() as $package) { + if (preg_match($patternRegexp, $package->getName())) { + $matched = true; + $packages[] = $package; + } + } + + if (!$matched) { + $io->writeError('Pattern "' . $pattern . '" does not match any currently installed packages.'); + } + } + + if (!$packages) { + $io->writeError('Found no packages to reinstall, aborting.'); + return 1; + } + + $installOperations = array(); + $uninstallOperations = array(); + foreach ($packages as $package) { + $uninstallOperations[] = new UninstallOperation($package); + $installOperations[] = new InstallOperation($package); + } + + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'reinstall', $input, $output); + $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); + + $config = $composer->getConfig(); + list($preferSource, $preferDist) = $this->getPreferredInstallOptions($config, $input); + + $installationManager = $composer->getInstallationManager(); + $downloadManager = $composer->getDownloadManager(); + $package = $composer->getPackage(); + + $ignorePlatformReqs = $input->getOption('ignore-platform-reqs') ?: ($input->getOption('ignore-platform-req') ?: false); + + $installationManager->setOutputProgress(!$input->getOption('no-progress')); + if ($input->getOption('no-plugins')) { + $installationManager->disablePlugins(); + } + + $downloadManager->setPreferSource($preferSource); + $downloadManager->setPreferDist($preferDist); + + $installationManager->execute($localRepo, $uninstallOperations, true, !$input->getOption('no-scripts')); + $installationManager->execute($localRepo, $installOperations, true, !$input->getOption('no-scripts')); + + if (!$input->getOption('no-autoloader')) { + $optimize = $input->getOption('optimize-autoloader') || $config->get('optimize-autoloader'); + $authoritative = $input->getOption('classmap-authoritative') || $config->get('classmap-authoritative'); + $apcuPrefix = $input->getOption('apcu-autoloader-prefix'); + $apcu = $apcuPrefix !== null || $input->getOption('apcu-autoloader') || $config->get('apcu-autoloader'); + + $generator = $composer->getAutoloadGenerator(); + $generator->setClassMapAuthoritative($authoritative); + $generator->setApcu($apcu, $apcuPrefix); + $generator->setRunScripts(!$input->getOption('no-scripts')); + $generator->setIgnorePlatformRequirements($ignorePlatformReqs); + $generator->dump($config, $localRepo, $package, $installationManager, 'composer', $optimize); + } + + return 0; + } +} diff --git a/src/Composer/Console/Application.php b/src/Composer/Console/Application.php index 8b72eb31a..bdb8700aa 100644 --- a/src/Composer/Console/Application.php +++ b/src/Composer/Console/Application.php @@ -487,6 +487,7 @@ class Application extends BaseApplication new Command\OutdatedCommand(), new Command\CheckPlatformReqsCommand(), new Command\FundCommand(), + new Command\ReinstallCommand(), )); if (strpos(__FILE__, 'phar:') === 0) { From 5737a34e53b62f910d3166b910eab7dc358ba4bb Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 31 May 2021 17:09:10 +0200 Subject: [PATCH 2/3] Sort package installs using Transaction --- src/Composer/Command/ReinstallCommand.php | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/Composer/Command/ReinstallCommand.php b/src/Composer/Command/ReinstallCommand.php index 38b14189c..b1f9d6c5a 100644 --- a/src/Composer/Command/ReinstallCommand.php +++ b/src/Composer/Command/ReinstallCommand.php @@ -14,6 +14,7 @@ namespace Composer\Command; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; +use Composer\DependencyResolver\Transaction; use Composer\Package\BasePackage; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; @@ -69,14 +70,16 @@ EOT $composer = $this->getComposer(true, $input->getOption('no-plugins')); $localRepo = $composer->getRepositoryManager()->getLocalRepository(); - $packages = array(); + $packagesToReinstall = array(); + $packageNamesToReinstall = array(); foreach ($input->getArgument('packages') as $pattern) { $patternRegexp = BasePackage::packageNameToRegexp($pattern); $matched = false; foreach ($localRepo->getCanonicalPackages() as $package) { if (preg_match($patternRegexp, $package->getName())) { $matched = true; - $packages[] = $package; + $packagesToReinstall[] = $package; + $packageNamesToReinstall[] = $package->getName(); } } @@ -85,18 +88,26 @@ EOT } } - if (!$packages) { + if (!$packagesToReinstall) { $io->writeError('Found no packages to reinstall, aborting.'); return 1; } - $installOperations = array(); $uninstallOperations = array(); - foreach ($packages as $package) { + foreach ($packagesToReinstall as $package) { $uninstallOperations[] = new UninstallOperation($package); - $installOperations[] = new InstallOperation($package); } + $presentPackages = $localRepo->getPackages(); + $resultPackages = $presentPackages; + foreach ($presentPackages as $index => $package) { + if (in_array($package->getName(), $packageNamesToReinstall, true)) { + unset($presentPackages[$index]); + } + } + $transaction = new Transaction($presentPackages, $resultPackages); + $installOperations = $transaction->getOperations(); + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'reinstall', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent); From 4dbdae3ada097967eab51f3f1f88dbc25c98597d Mon Sep 17 00:00:00 2001 From: Jordi Boggiano Date: Mon, 31 May 2021 17:18:47 +0200 Subject: [PATCH 3/3] Sort uninstalls in reverse order from installs --- src/Composer/Command/ReinstallCommand.php | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/Composer/Command/ReinstallCommand.php b/src/Composer/Command/ReinstallCommand.php index b1f9d6c5a..c96965019 100644 --- a/src/Composer/Command/ReinstallCommand.php +++ b/src/Composer/Command/ReinstallCommand.php @@ -15,6 +15,7 @@ namespace Composer\Command; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Transaction; +use Composer\Package\AliasPackage; use Composer\Package\BasePackage; use Composer\Plugin\CommandEvent; use Composer\Plugin\PluginEvents; @@ -98,6 +99,7 @@ EOT $uninstallOperations[] = new UninstallOperation($package); } + // make sure we have a list of install operations ordered by dependency/plugins $presentPackages = $localRepo->getPackages(); $resultPackages = $presentPackages; foreach ($presentPackages as $index => $package) { @@ -108,6 +110,17 @@ EOT $transaction = new Transaction($presentPackages, $resultPackages); $installOperations = $transaction->getOperations(); + // reverse-sort the uninstalls based on the install order + $installOrder = array(); + foreach ($installOperations as $index => $op) { + if ($op instanceof InstallOperation && !$op->getPackage() instanceof AliasPackage) { + $installOrder[$op->getPackage()->getName()] = $index; + } + } + usort($uninstallOperations, function ($a, $b) use ($installOrder) { + return $installOrder[$b->getPackage()->getName()] - $installOrder[$a->getPackage()->getName()]; + }); + $commandEvent = new CommandEvent(PluginEvents::COMMAND, 'reinstall', $input, $output); $composer->getEventDispatcher()->dispatch($commandEvent->getName(), $commandEvent);