1
0
Fork 0

Make the require command guess versions more accurately by delegating to the solver (except with --no-update) (#11160)

pull/11169/head
Jordi Boggiano 2022-11-01 15:48:52 +01:00 committed by GitHub
parent 723f700bea
commit 36bc30ffab
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 152 additions and 22 deletions

View File

@ -86,7 +86,7 @@ trait PackageDiscoveryTrait
* @return array<string> * @return array<string>
* @throws \Exception * @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) { if (count($requires) > 0) {
$requires = $this->normalizeRequirements($requires); $requires = $this->normalizeRequirements($requires);
@ -101,16 +101,20 @@ trait PackageDiscoveryTrait
if (!isset($requirement['version'])) { if (!isset($requirement['version'])) {
// determine the best version automatically // determine the best version automatically
[$name, $version] = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, $fixed); [$name, $version] = $this->findBestVersionAndNameForPackage($input, $requirement['name'], $platformRepo, $preferredStability, $fixed);
$requirement['version'] = $version;
// replace package name from packagist.org // replace package name from packagist.org
$requirement['name'] = $name; $requirement['name'] = $name;
if ($useBestVersionConstraint) {
$requirement['version'] = $version;
$io->writeError(sprintf( $io->writeError(sprintf(
'Using version <info>%s</info> for <info>%s</info>', 'Using version <info>%s</info> for <info>%s</info>',
$requirement['version'], $requirement['version'],
$requirement['name'] $requirement['name']
)); ));
} else {
$requirement['version'] = 'guess';
}
} }
$result[] = $requirement['name'] . ' ' . $requirement['version']; $result[] = $requirement['name'] . ' ' . $requirement['version'];

View File

@ -13,8 +13,15 @@
namespace Composer\Command; namespace Composer\Command;
use Composer\DependencyResolver\Request; use Composer\DependencyResolver\Request;
use Composer\Package\AliasPackage;
use Composer\Package\CompletePackageInterface; use Composer\Package\CompletePackageInterface;
use Composer\Package\Loader\RootPackageLoader; 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\Filesystem;
use Composer\Util\PackageSorter; use Composer\Util\PackageSorter;
use Seld\Signal\SignalHandler; use Seld\Signal\SignalHandler;
@ -208,7 +215,7 @@ EOT
$input->getArgument('packages'), $input->getArgument('packages'),
$platformRepo, $platformRepo,
$preferredStability, $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') $input->getOption('fixed')
); );
} catch (\Exception $e) { } catch (\Exception $e) {
@ -259,6 +266,15 @@ EOT
$requireKey = $input->getOption('dev') ? 'require-dev' : 'require'; $requireKey = $input->getOption('dev') ? 'require-dev' : 'require';
$removeKey = $input->getOption('dev') ? 'require' : 'require-dev'; $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 // validate requirements format
$versionParser = new VersionParser(); $versionParser = new VersionParser();
foreach ($requirements as $package => $constraint) { foreach ($requirements as $package => $constraint) {
@ -307,16 +323,8 @@ EOT
} }
} }
if (!$input->getOption('dry-run') && !$this->updateFileCleanly($this->json, $requirements, $requireKey, $removeKey, $sortPackages)) { if (!$input->getOption('dry-run')) {
$composerDefinition = $this->json->read(); $this->updateFile($this->json, $requirements, $requireKey, $removeKey, $sortPackages);
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);
} }
$io->writeError('<info>'.$this->file.' has been '.($this->newlyCreated ? 'created' : 'updated').'</info>'); $io->writeError('<info>'.$this->file.' has been '.($this->newlyCreated ? 'created' : 'updated').'</info>');
@ -328,7 +336,12 @@ EOT
$composer->getPluginManager()->deactivateInstalledPlugins(); $composer->getPluginManager()->deactivateInstalledPlugins();
try { 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) { } catch (\Exception $e) {
if (!$this->dependencyResolutionCompleted) { if (!$this->dependencyResolutionCompleted) {
$this->revertComposerFile(); $this->revertComposerFile();
@ -490,6 +503,69 @@ EOT
return $status; return $status;
} }
/**
* @param list<string> $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 <info>%s</info> for <info>%s</info>',
$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<string, string> $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<string, string> $new * @param array<string, string> $new
*/ */

View File

@ -78,7 +78,6 @@ class RequireCommandTest extends TestCase
['packages' => ['required/pkg']], ['packages' => ['required/pkg']],
<<<OUTPUT <<<OUTPUT
<warning>Cannot use required/pkg's latest version 1.2.0 as it requires ext-foobar ^1 which is missing from your platform. <warning>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 ./composer.json has been updated
Running composer update required/pkg Running composer update required/pkg
Loading composer repositories with package information 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) Installing dependencies from lock file (including require-dev)
Package operations: 1 install, 0 updates, 0 removals Package operations: 1 install, 0 updates, 0 removals
- Installing required/pkg (1.0.0) - Installing required/pkg (1.0.0)
Using version ^1.0 for required/pkg
OUTPUT OUTPUT
]; ];
@ -108,7 +108,6 @@ OUTPUT
<<<OUTPUT <<<OUTPUT
<warning>Cannot use required/pkg's latest version 1.2.0 as it requires ext-foobar ^1 which is missing from your platform. <warning>Cannot use required/pkg's latest version 1.2.0 as it requires ext-foobar ^1 which is missing from your platform.
<warning>Cannot use required/pkg 1.1.0 as it requires ext-foobar ^1 which is missing from your platform. <warning>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 ./composer.json has been updated
Running composer update required/pkg Running composer update required/pkg
Loading composer repositories with package information 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 Lock file operations: 1 install, 0 updates, 0 removals
Installs: required/pkg:1.0.0 Installs: required/pkg:1.0.0
- Locking required/pkg (1.0.0) - Locking required/pkg (1.0.0)
Using version ^1.0 for required/pkg
OUTPUT OUTPUT
]; ];
@ -137,13 +137,63 @@ OUTPUT
['packages' => ['required/pkg'], '--no-install' => true], ['packages' => ['required/pkg'], '--no-install' => true],
<<<OUTPUT <<<OUTPUT
<warning>Cannot use required/pkg's latest version 1.1.0 as it requires php ^20 which is not satisfied by your platform. <warning>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 ./composer.json has been updated
Running composer update required/pkg Running composer update required/pkg
Loading composer repositories with package information Loading composer repositories with package information
Updating dependencies Updating dependencies
Lock file operations: 1 install, 0 updates, 0 removals Lock file operations: 1 install, 0 updates, 0 removals
- Locking required/pkg (1.0.0) - 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],
<<<OUTPUT
<warning>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],
<<<OUTPUT
./composer.json has been updated
Running composer update required/pkg
Loading composer repositories with package information
Updating dependencies
Lock file operations: 2 installs, 0 updates, 0 removals
- Locking existing/dep (1.1.0)
- Locking required/pkg (1.1.0)
Using version ^1.1 for required/pkg
OUTPUT OUTPUT
]; ];
} }