diff --git a/src/Composer/Command/PackageDiscoveryTrait.php b/src/Composer/Command/PackageDiscoveryTrait.php index a7cd22c42..db205a510 100644 --- a/src/Composer/Command/PackageDiscoveryTrait.php +++ b/src/Composer/Command/PackageDiscoveryTrait.php @@ -86,7 +86,7 @@ trait PackageDiscoveryTrait * @return array * @throws \Exception */ - final protected function determineRequirements(InputInterface $input, OutputInterface $output, array $requires = [], ?PlatformRepository $platformRepo = null, string $preferredStability = 'stable', bool $checkProvidedVersions = true, bool $fixed = false): array + final protected function determineRequirements(InputInterface $input, OutputInterface $output, array $requires = [], ?PlatformRepository $platformRepo = null, string $preferredStability = 'stable', bool $useBestVersionConstraint = true, bool $fixed = false): array { if (count($requires) > 0) { $requires = $this->normalizeRequirements($requires); @@ -101,16 +101,20 @@ trait PackageDiscoveryTrait if (!isset($requirement['version'])) { // determine the best version automatically [$name, $version] = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, $fixed); - $requirement['version'] = $version; // replace package name from packagist.org $requirement['name'] = $name; - $io->writeError(sprintf( - 'Using version %s for %s', - $requirement['version'], - $requirement['name'] - )); + if ($useBestVersionConstraint) { + $requirement['version'] = $version; + $io->writeError(sprintf( + 'Using version %s for %s', + $requirement['version'], + $requirement['name'] + )); + } else { + $requirement['version'] = 'guess'; + } } $result[] = $requirement['name'] . ' ' . $requirement['version']; diff --git a/src/Composer/Command/RequireCommand.php b/src/Composer/Command/RequireCommand.php index 241654f8e..08b4c5958 100644 --- a/src/Composer/Command/RequireCommand.php +++ b/src/Composer/Command/RequireCommand.php @@ -13,8 +13,15 @@ namespace Composer\Command; use Composer\DependencyResolver\Request; +use Composer\Package\AliasPackage; use Composer\Package\CompletePackageInterface; use Composer\Package\Loader\RootPackageLoader; +use Composer\Package\Locker; +use Composer\Package\PackageInterface; +use Composer\Package\Version\VersionBumper; +use Composer\Package\Version\VersionSelector; +use Composer\Pcre\Preg; +use Composer\Repository\RepositorySet; use Composer\Util\Filesystem; use Composer\Util\PackageSorter; use Seld\Signal\SignalHandler; @@ -208,7 +215,7 @@ EOT $input->getArgument('packages'), $platformRepo, $preferredStability, - !$input->getOption('no-update'), + $input->getOption('no-update'), // if there is no update, we need to use the best possible version constraint directly as we cannot rely on the solver to guess the best constraint $input->getOption('fixed') ); } catch (\Exception $e) { @@ -259,6 +266,15 @@ EOT $requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; $removeKey = $input->getOption('dev') ? 'require' : 'require-dev'; + // check which requirements need the version guessed + $requirementsToGuess = []; + foreach ($requirements as $package => $constraint) { + if ($constraint === 'guess') { + $requirements[$package] = '*'; + $requirementsToGuess[] = $package; + } + } + // validate requirements format $versionParser = new VersionParser(); foreach ($requirements as $package => $constraint) { @@ -307,16 +323,8 @@ EOT } } - if (!$input->getOption('dry-run') && !$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) { - $composerDefinition = $this->json->read(); - foreach ($requirements as $package => $version) { - $composerDefinition[$requireKey][$package] = $version; - unset($composerDefinition[$removeKey][$package]); - if (isset($composerDefinition[$removeKey]) && count($composerDefinition[$removeKey]) === 0) { - unset($composerDefinition[$removeKey]); - } - } - $this->json->write($composerDefinition); + if (!$input->getOption('dry-run')) { + $this->updateFile($this->json, $requirements, $requireKey, $removeKey, $sortPackages); } $io->writeError(''.$this->file.' has been '.($this->newlyCreated ? 'created' : 'updated').''); @@ -328,7 +336,12 @@ EOT $composer->getPluginManager()->deactivateInstalledPlugins(); try { - return $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey); + $result = $this->doUpdate($input, $output, $io, $requirements, $requireKey, $removeKey); + if ($result === 0 && count($requirementsToGuess) > 0) { + $this->updateRequirementsAfterResolution($requirementsToGuess, $requireKey, $removeKey, $sortPackages, $input->getOption('dry-run')); + } + + return $result; } catch (\Exception $e) { if (!$this->dependencyResolutionCompleted) { $this->revertComposerFile(); @@ -490,6 +503,69 @@ EOT return $status; } + /** + * @param list $requirementsToUpdate + */ + private function updateRequirementsAfterResolution(array $requirementsToUpdate, string $requireKey, string $removeKey, bool $sortPackages, bool $dryRun): void + { + $composer = $this->requireComposer(); + $locker = $composer->getLocker(); + $requirements = []; + $versionSelector = new VersionSelector(new RepositorySet()); + $repo = $locker->isLocked() ? $composer->getLocker()->getLockedRepository(true) : $composer->getRepositoryManager()->getLocalRepository(); + foreach ($requirementsToUpdate as $packageName) { + $package = $repo->findPackage($packageName, '*'); + while ($package instanceof AliasPackage) { + $package = $package->getAliasOf(); + } + + if (!$package instanceof PackageInterface) { + continue; + } + + $requirements[$packageName] = $versionSelector->findRecommendedRequireVersion($package); + $this->getIO()->writeError(sprintf( + 'Using version %s for %s', + $requirements[$packageName], + $packageName + )); + } + + if (!$dryRun) { + $this->updateFile($this->json, $requirements, $requireKey, $removeKey, $sortPackages); + if ($locker->isLocked()) { + $contents = file_get_contents($this->json->getPath()); + if (false === $contents) { + throw new \RuntimeException('Unable to read '.$this->json->getPath().' contents to update the lock file hash.'); + } + $lock = new JsonFile(Factory::getLockFile($this->json->getPath())); + $lockData = $lock->read(); + $lockData['content-hash'] = Locker::getContentHash($contents); + $lock->write($lockData); + } + } + } + + /** + * @param array $new + */ + private function updateFile(JsonFile $json, array $new, string $requireKey, string $removeKey, bool $sortPackages): void + { + if ($this->updateFileCleanly($json, $new, $requireKey, $removeKey, $sortPackages)) { + return; + } + + $composerDefinition = $this->json->read(); + foreach ($new as $package => $version) { + $composerDefinition[$requireKey][$package] = $version; + unset($composerDefinition[$removeKey][$package]); + if (isset($composerDefinition[$removeKey]) && count($composerDefinition[$removeKey]) === 0) { + unset($composerDefinition[$removeKey]); + } + } + $this->json->write($composerDefinition); + } + /** * @param array $new */ diff --git a/tests/Composer/Test/Command/RequireCommandTest.php b/tests/Composer/Test/Command/RequireCommandTest.php index ed7ec98b9..a97fc1c03 100644 --- a/tests/Composer/Test/Command/RequireCommandTest.php +++ b/tests/Composer/Test/Command/RequireCommandTest.php @@ -78,7 +78,6 @@ class RequireCommandTest extends TestCase ['packages' => ['required/pkg']], <<Cannot use required/pkg's latest version 1.2.0 as it requires ext-foobar ^1 which is missing from your platform. -Using version ^1.0 for required/pkg ./composer.json has been updated Running composer update required/pkg Loading composer repositories with package information @@ -88,6 +87,7 @@ Lock file operations: 1 install, 0 updates, 0 removals Installing dependencies from lock file (including require-dev) Package operations: 1 install, 0 updates, 0 removals - Installing required/pkg (1.0.0) +Using version ^1.0 for required/pkg OUTPUT ]; @@ -108,7 +108,6 @@ OUTPUT <<Cannot use required/pkg's latest version 1.2.0 as it requires ext-foobar ^1 which is missing from your platform. Cannot use required/pkg 1.1.0 as it requires ext-foobar ^1 which is missing from your platform. -Using version ^1.0 for required/pkg ./composer.json has been updated Running composer update required/pkg Loading composer repositories with package information @@ -119,6 +118,7 @@ Analyzed %d rules to resolve dependencies Lock file operations: 1 install, 0 updates, 0 removals Installs: required/pkg:1.0.0 - Locking required/pkg (1.0.0) +Using version ^1.0 for required/pkg OUTPUT ]; @@ -137,13 +137,63 @@ OUTPUT ['packages' => ['required/pkg'], '--no-install' => true], <<Cannot use required/pkg's latest version 1.1.0 as it requires php ^20 which is not satisfied by your platform. -Using version ^1.0 for required/pkg ./composer.json has been updated Running composer update required/pkg Loading composer repositories with package information Updating dependencies Lock file operations: 1 install, 0 updates, 0 removals - Locking required/pkg (1.0.0) +Using version ^1.0 for required/pkg +OUTPUT + ]; + + yield 'version selection happens early even if not completely accurate if no update is requested' => [ + [ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'required/pkg', 'version' => '1.1.0', 'require' => ['php' => '^20']], + ['name' => 'required/pkg', 'version' => '1.0.0', 'require' => ['php' => '>=7']], + ], + ], + ], + ], + ['packages' => ['required/pkg'], '--no-update' => true], + <<Cannot use required/pkg's latest version 1.1.0 as it requires php ^20 which is not satisfied by your platform. +Using version ^1.0 for required/pkg +./composer.json has been updated +OUTPUT + ]; + + yield 'pick best matching version when not provided' => [ + [ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'existing/dep', 'version' => '1.1.0', 'require' => ['required/pkg' => '^1']], + ['name' => 'required/pkg', 'version' => '2.0.0'], + ['name' => 'required/pkg', 'version' => '1.1.0'], + ['name' => 'required/pkg', 'version' => '1.0.0'], + ], + ], + ], + 'require' => [ + 'existing/dep' => '^1' + ], + ], + ['packages' => ['required/pkg'], '--no-install' => true], + <<