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>
|
* @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'];
|
||||||
|
|
|
@ -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
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -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
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue