diff --git a/src/Composer/Command/CreateProjectCommand.php b/src/Composer/Command/CreateProjectCommand.php index 3c25b06b071c..9156f04a80a2 100644 --- a/src/Composer/Command/CreateProjectCommand.php +++ b/src/Composer/Command/CreateProjectCommand.php @@ -434,7 +434,7 @@ protected function installRootPackage(IOInterface $io, Config $config, string $p // find the latest version if there are multiple $versionSelector = new VersionSelector($repositorySet, $platformRepo); - $package = $versionSelector->findBestCandidate($name, $packageVersion, $stability, $platformRequirementFilter); + $package = $versionSelector->findBestCandidate($name, $packageVersion, $stability, $platformRequirementFilter, 0, $io); if (!$package) { $errorMessage = "Could not find package $name with " . ($packageVersion ? "version $packageVersion" : "stability $stability"); diff --git a/src/Composer/Command/PackageDiscoveryTrait.php b/src/Composer/Command/PackageDiscoveryTrait.php index 0c2830196305..1c1ba9d1bdbb 100644 --- a/src/Composer/Command/PackageDiscoveryTrait.php +++ b/src/Composer/Command/PackageDiscoveryTrait.php @@ -282,7 +282,7 @@ private function findBestVersionAndNameForPackage(InputInterface $input, string $versionSelector = new VersionSelector($repoSet, $platformRepo); $effectiveMinimumStability = $this->getMinimumStability($input); - $package = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter); + $package = $versionSelector->findBestCandidate($name, null, $preferredStability, $platformRequirementFilter, 0, $this->getIO()); if (false === $package) { // platform packages can not be found in the pool in versions other than the local platform's has diff --git a/src/Composer/Package/Version/VersionSelector.php b/src/Composer/Package/Version/VersionSelector.php index 7f082c209951..b2a2f56ac973 100644 --- a/src/Composer/Package/Version/VersionSelector.php +++ b/src/Composer/Package/Version/VersionSelector.php @@ -15,6 +15,7 @@ use Composer\Filter\PlatformRequirementFilter\IgnoreAllPlatformRequirementFilter; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterInterface; +use Composer\IO\IOInterface; use Composer\Package\BasePackage; use Composer\Package\AliasPackage; use Composer\Package\PackageInterface; @@ -65,10 +66,11 @@ public function __construct(RepositorySet $repositorySet, PlatformRepository $pl * @param string $targetPackageVersion * @param string $preferredStability * @param PlatformRequirementFilterInterface|bool|string[] $platformRequirementFilter - * @param int $repoSetFlags* + * @param int $repoSetFlags + * @param IOInterface|null $io If passed, warnings will be output there in case versions cannot be selected due to platform requirements * @return PackageInterface|false */ - public function findBestCandidate(string $packageName, string $targetPackageVersion = null, string $preferredStability = 'stable', $platformRequirementFilter = null, int $repoSetFlags = 0) + public function findBestCandidate(string $packageName, string $targetPackageVersion = null, string $preferredStability = 'stable', $platformRequirementFilter = null, int $repoSetFlags = 0, ?IOInterface $io = null) { if (!isset(BasePackage::$stabilities[$preferredStability])) { // If you get this, maybe you are still relying on the Composer 1.x signature where the 3rd arg was the php version @@ -84,10 +86,11 @@ public function findBestCandidate(string $packageName, string $targetPackageVers $constraint = $targetPackageVersion ? $this->getParser()->parseConstraints($targetPackageVersion) : null; $candidates = $this->repositorySet->findPackages(strtolower($packageName), $constraint, $repoSetFlags); + $skippedWarnings = []; if ($this->platformConstraints && !($platformRequirementFilter instanceof IgnoreAllPlatformRequirementFilter)) { $platformConstraints = $this->platformConstraints; - $candidates = array_filter($candidates, static function ($pkg) use ($platformConstraints, $platformRequirementFilter): bool { + $candidates = array_filter($candidates, static function ($pkg) use ($platformConstraints, $platformRequirementFilter, &$skippedWarnings): bool { $reqs = $pkg->getRequires(); foreach ($reqs as $name => $link) { @@ -99,10 +102,12 @@ public function findBestCandidate(string $packageName, string $targetPackageVers } } + $skippedWarnings[$pkg->getName()][] = ['version' => $pkg->getPrettyVersion(), 'link' => $link, 'reason' => 'is not satisfied by your platform']; return false; } elseif (PlatformRepository::isPlatformPackage($name)) { // Package requires a platform package that is unknown on current platform. // It means that current platform cannot validate this constraint and so package is not installable. + $skippedWarnings[$pkg->getName()][] = ['version' => $pkg->getPrettyVersion(), 'link' => $link, 'reason' => 'is missing from your platform']; return false; } } @@ -116,6 +121,20 @@ public function findBestCandidate(string $packageName, string $targetPackageVers return false; } + if (count($skippedWarnings) > 0 && $io !== null) { + foreach ($skippedWarnings as $name => $warnings) { + foreach ($warnings as $index => $warning) { + $link = $warning['link']; + $latest = $index === 0 ? "'s latest version" : ''; + $io->writeError( + 'Cannot use '.$name.$latest.' '.$warning['version'].' as it '.$link->getDescription().' '.$link->getTarget().' '.$link->getPrettyConstraint().' which '.$warning['reason'].'.', + true, + $index === 0 ? IOInterface::NORMAL : IOInterface::VERBOSE + ); + } + } + } + // select highest version if we have many $package = reset($candidates); $minPriority = BasePackage::$stabilities[$preferredStability]; diff --git a/tests/Composer/Test/Command/RequireCommandTest.php b/tests/Composer/Test/Command/RequireCommandTest.php new file mode 100644 index 000000000000..0579d06e763e --- /dev/null +++ b/tests/Composer/Test/Command/RequireCommandTest.php @@ -0,0 +1,155 @@ + + * Jordi Boggiano + * + * For the full copyright and license information, please view the LICENSE + * file that was distributed with this source code. + */ + +namespace Composer\Test\Command; + +use Composer\Test\TestCase; +use InvalidArgumentException; + +class RequireCommandTest extends TestCase +{ + public function testRequireThrowsIfNoneMatches(): void + { + $this->expectException(InvalidArgumentException::class); + $this->expectExceptionMessage( + 'Package required/pkg has requirements incompatible with your PHP version, PHP extensions and Composer version:' . PHP_EOL . + ' - required/pkg 1.0.0 requires ext-foobar ^1 but it is not present.' + ); + + $this->initTempComposer([ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'required/pkg', 'version' => '1.0.0', 'require' => ['ext-foobar' => '^1']], + ], + ], + ], + ]); + + $appTester = $this->getApplicationTester(); + $appTester->run(['command' => 'require', '--dry-run' => true, '--no-audit' => true, 'packages' => ['required/pkg']]); + } + + /** + * @dataProvider provideRequire + * @param array $composerJson + * @param array $command + */ + public function testRequire(array $composerJson, array $command, string $expected): void + { + $this->initTempComposer($composerJson); + + $appTester = $this->getApplicationTester(); + $appTester->run(array_merge(['command' => 'require', '--dry-run' => true, '--no-audit' => true], $command)); + + if (str_contains($expected, '%d')) { + $pattern = '{^'.str_replace('%d', '[0-9.]+', preg_quote(trim($expected))).'$}'; + $this->assertMatchesRegularExpression($pattern, trim($appTester->getDisplay(true))); + } else { + $this->assertSame(trim($expected), trim($appTester->getDisplay(true))); + } + + // workaround until https://github.com/symfony/symfony/pull/46747 is merged + putenv('SHELL_VERBOSITY'); + unset($_ENV['SHELL_VERBOSITY']); + unset($_SERVER['SHELL_VERBOSITY']); + } + + public function provideRequire(): \Generator + { + yield 'warn once for missing ext but a lower package matches' => [ + [ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'required/pkg', 'version' => '1.2.0', 'require' => ['ext-foobar' => '^1']], + ['name' => 'required/pkg', 'version' => '1.1.0', 'require' => ['ext-foobar' => '^1']], + ['name' => 'required/pkg', 'version' => '1.0.0'], + ], + ], + ], + ], + ['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 +Updating dependencies +Lock file operations: 1 install, 0 updates, 0 removals + - Locking required/pkg (1.0.0) +Installing dependencies from lock file (including require-dev) +Package operations: 1 install, 0 updates, 0 removals + - Installing required/pkg (1.0.0) +OUTPUT + ]; + + yield 'warn multiple times when verbose' => [ + [ + 'repositories' => [ + 'packages' => [ + 'type' => 'package', + 'package' => [ + ['name' => 'required/pkg', 'version' => '1.2.0', 'require' => ['ext-foobar' => '^1']], + ['name' => 'required/pkg', 'version' => '1.1.0', 'require' => ['ext-foobar' => '^1']], + ['name' => 'required/pkg', 'version' => '1.0.0'], + ], + ], + ], + ], + ['packages' => ['required/pkg'], '--no-install' => true, '-v' => true], + <<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 +Updating dependencies +Dependency resolution completed in %d seconds +Analyzed %d packages to resolve dependencies +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) +OUTPUT + ]; + + yield 'warn for not satisfied req which is satisfied by lower version' => [ + [ + '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-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) +OUTPUT + ]; + } +}