Make the require command guess versions more accurately by delegating to the solver (except with --no-update) (#11160)
parent
723f700bea
commit
36bc30ffab
|
@ -86,7 +86,7 @@ trait PackageDiscoveryTrait
|
|||
* @return array<string>
|
||||
* @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 <info>%s</info> for <info>%s</info>',
|
||||
$requirement['version'],
|
||||
$requirement['name']
|
||||
));
|
||||
if ($useBestVersionConstraint) {
|
||||
$requirement['version'] = $version;
|
||||
$io->writeError(sprintf(
|
||||
'Using version <info>%s</info> for <info>%s</info>',
|
||||
$requirement['version'],
|
||||
$requirement['name']
|
||||
));
|
||||
} else {
|
||||
$requirement['version'] = 'guess';
|
||||
}
|
||||
}
|
||||
|
||||
$result[] = $requirement['name'] . ' ' . $requirement['version'];
|
||||
|
|
|
@ -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('<info>'.$this->file.' has been '.($this->newlyCreated ? 'created' : 'updated').'</info>');
|
||||
|
@ -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<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
|
||||
*/
|
||||
|
|
|
@ -78,7 +78,6 @@ class RequireCommandTest extends TestCase
|
|||
['packages' => ['required/pkg']],
|
||||
<<<OUTPUT
|
||||
<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
|
||||
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
|
|||
<<<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 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],
|
||||
<<<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
|
||||
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],
|
||||
<<<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
|
||||
];
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue