diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php
index fd5588fa..72c3cd57 100644
--- a/src/Command/BuildCommand.php
+++ b/src/Command/BuildCommand.php
@@ -10,6 +10,9 @@
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieOperation;
use Php\Pie\DependencyResolver\DependencyResolver;
+use Php\Pie\DependencyResolver\InvalidPackageName;
+use Php\Pie\DependencyResolver\UnableToResolveRequirement;
+use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -28,6 +31,7 @@ public function __construct(
private readonly ContainerInterface $container,
private readonly DependencyResolver $dependencyResolver,
private readonly ComposerIntegrationHandler $composerIntegrationHandler,
+ private readonly FindMatchingPackages $findMatchingPackages,
) {
parent::__construct();
}
@@ -41,8 +45,19 @@ public function configure(): void
public function execute(InputInterface $input, OutputInterface $output): int
{
- $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
- $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
+ try {
+ $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ } catch (InvalidPackageName $invalidPackageName) {
+ return CommandHelper::handlePackageNotFound(
+ $invalidPackageName,
+ $this->findMatchingPackages,
+ $output,
+ $targetPlatform,
+ $this->container,
+ );
+ }
+
$forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
$composer = PieComposerFactory::createPieComposer(
@@ -58,12 +73,23 @@ public function execute(InputInterface $input, OutputInterface $output): int
),
);
- $package = ($this->dependencyResolver)(
- $composer,
- $targetPlatform,
- $requestedNameAndVersion,
- $forceInstallPackageVersion,
- );
+ try {
+ $package = ($this->dependencyResolver)(
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ $forceInstallPackageVersion,
+ );
+ } catch (UnableToResolveRequirement $unableToResolveRequirement) {
+ return CommandHelper::handlePackageNotFound(
+ $unableToResolveRequirement,
+ $this->findMatchingPackages,
+ $output,
+ $targetPlatform,
+ $this->container,
+ );
+ }
+
$output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix()));
// Now we know what package we have, we can validate the configure options for the command and re-create the
diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php
index 4f53e09f..61987fd8 100644
--- a/src/Command/CommandHelper.php
+++ b/src/Command/CommandHelper.php
@@ -5,32 +5,45 @@
namespace Php\Pie\Command;
use Composer\Composer;
+use Composer\Package\CompletePackageInterface;
use Composer\Package\Version\VersionParser;
use Composer\Repository\ComposerRepository;
use Composer\Repository\PathRepository;
use Composer\Repository\VcsRepository;
use Composer\Util\Platform;
use InvalidArgumentException;
+use OutOfRangeException;
+use Php\Pie\ComposerIntegration\PieComposerFactory;
+use Php\Pie\ComposerIntegration\PieComposerRequest;
+use Php\Pie\DependencyResolver\InvalidPackageName;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
+use Php\Pie\DependencyResolver\UnableToResolveRequirement;
+use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Platform as PiePlatform;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\TargetPhp\PhpBinaryPath;
use Php\Pie\Platform\TargetPhp\PhpizePath;
use Php\Pie\Platform\TargetPlatform;
+use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Input\InputOption;
use Symfony\Component\Console\Output\OutputInterface;
+use Throwable;
use Webmozart\Assert\Assert;
use function array_key_exists;
+use function array_map;
+use function count;
use function is_array;
use function is_string;
use function reset;
use function sprintf;
+use function str_starts_with;
use function strtolower;
+use function substr;
use function trim;
use const PHP_VERSION;
@@ -326,4 +339,92 @@ public static function listRepositories(Composer $composer, OutputInterface $out
));
}
}
+
+ public static function handlePackageNotFound(
+ InvalidPackageName|UnableToResolveRequirement $exception,
+ FindMatchingPackages $findMatchingPackages,
+ OutputInterface $output,
+ TargetPlatform $targetPlatform,
+ ContainerInterface $container,
+ ): int {
+ $pieComposer = PieComposerFactory::createPieComposer(
+ $container,
+ PieComposerRequest::noOperation(
+ $output,
+ $targetPlatform,
+ ),
+ );
+
+ $requestedPackageName = $exception->requestedPackageAndVersion->package;
+ if (str_starts_with($requestedPackageName, 'ext-')) {
+ $requestedPackageName = substr($requestedPackageName, 4);
+ }
+
+ $output->writeln('');
+ $output->writeln(sprintf('Could not install package: %s', $requestedPackageName));
+ $output->writeln($exception->getMessage());
+
+ try {
+ $matches = array_map(
+ static function (array $match) use ($output, $pieComposer): array {
+ $composerMatchingPackage = $pieComposer->getRepositoryManager()->findPackage($match['name'], '*');
+
+ // Attempts to augment the Composer packages found with the PIE extension name
+ if ($composerMatchingPackage instanceof CompletePackageInterface) {
+ try {
+ $match['extension-name'] = Package
+ ::fromComposerCompletePackage($composerMatchingPackage)
+ ->extensionName()
+ ->name();
+ } catch (Throwable $t) {
+ $output->writeln(
+ sprintf(
+ 'Tried looking up extension name for %s, but failed: %s',
+ $match['name'],
+ $t->getMessage(),
+ ),
+ OutputInterface::VERBOSITY_VERY_VERBOSE,
+ );
+ }
+ }
+
+ return $match;
+ },
+ $findMatchingPackages->for($pieComposer, $requestedPackageName),
+ );
+
+ if (count($matches)) {
+ $output->writeln('');
+ if (count($matches) === 1) {
+ $output->writeln('Did you mean this?');
+ } else {
+ $output->writeln('Did you mean one of these?');
+ }
+
+ array_map(
+ static function (array $match) use ($output): void {
+ $output->writeln(sprintf(
+ ' - %s%s: %s',
+ $match['name'],
+ array_key_exists('extension-name', $match) && is_string($match['extension-name'])
+ ? ' (provides extension: ' . $match['extension-name'] . ')'
+ : '',
+ $match['description'] ?? 'no description available',
+ ));
+ },
+ $matches,
+ );
+ }
+ } catch (OutOfRangeException) {
+ $output->writeln(
+ sprintf(
+ 'Tried searching for "%s", but nothing was found.',
+ $requestedPackageName,
+ ),
+ OutputInterface::VERBOSITY_VERBOSE,
+ );
+ }
+
+ return 1;
+ }
}
diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php
index 9c156459..5481dabc 100644
--- a/src/Command/DownloadCommand.php
+++ b/src/Command/DownloadCommand.php
@@ -10,6 +10,9 @@
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieOperation;
use Php\Pie\DependencyResolver\DependencyResolver;
+use Php\Pie\DependencyResolver\InvalidPackageName;
+use Php\Pie\DependencyResolver\UnableToResolveRequirement;
+use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -28,6 +31,7 @@ public function __construct(
private readonly ContainerInterface $container,
private readonly DependencyResolver $dependencyResolver,
private readonly ComposerIntegrationHandler $composerIntegrationHandler,
+ private readonly FindMatchingPackages $findMatchingPackages,
) {
parent::__construct();
}
@@ -43,8 +47,19 @@ public function execute(InputInterface $input, OutputInterface $output): int
{
CommandHelper::validateInput($input, $this);
- $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
- $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
+ try {
+ $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ } catch (InvalidPackageName $invalidPackageName) {
+ return CommandHelper::handlePackageNotFound(
+ $invalidPackageName,
+ $this->findMatchingPackages,
+ $output,
+ $targetPlatform,
+ $this->container,
+ );
+ }
+
$forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
$composer = PieComposerFactory::createPieComposer(
@@ -60,12 +75,23 @@ public function execute(InputInterface $input, OutputInterface $output): int
),
);
- $package = ($this->dependencyResolver)(
- $composer,
- $targetPlatform,
- $requestedNameAndVersion,
- $forceInstallPackageVersion,
- );
+ try {
+ $package = ($this->dependencyResolver)(
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ $forceInstallPackageVersion,
+ );
+ } catch (UnableToResolveRequirement $unableToResolveRequirement) {
+ return CommandHelper::handlePackageNotFound(
+ $unableToResolveRequirement,
+ $this->findMatchingPackages,
+ $output,
+ $targetPlatform,
+ $this->container,
+ );
+ }
+
$output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix()));
try {
diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php
index 4cb56949..fa3d6593 100644
--- a/src/Command/InfoCommand.php
+++ b/src/Command/InfoCommand.php
@@ -8,6 +8,9 @@
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieOperation;
use Php\Pie\DependencyResolver\DependencyResolver;
+use Php\Pie\DependencyResolver\InvalidPackageName;
+use Php\Pie\DependencyResolver\UnableToResolveRequirement;
+use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
@@ -26,6 +29,7 @@ final class InfoCommand extends Command
public function __construct(
private readonly ContainerInterface $container,
private readonly DependencyResolver $dependencyResolver,
+ private readonly FindMatchingPackages $findMatchingPackages,
) {
parent::__construct();
}
@@ -43,7 +47,17 @@ public function execute(InputInterface $input, OutputInterface $output): int
$targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
- $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ try {
+ $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ } catch (InvalidPackageName $invalidPackageName) {
+ return CommandHelper::handlePackageNotFound(
+ $invalidPackageName,
+ $this->findMatchingPackages,
+ $output,
+ $targetPlatform,
+ $this->container,
+ );
+ }
$composer = PieComposerFactory::createPieComposer(
$this->container,
@@ -58,12 +72,23 @@ public function execute(InputInterface $input, OutputInterface $output): int
),
);
- $package = ($this->dependencyResolver)(
- $composer,
- $targetPlatform,
- $requestedNameAndVersion,
- CommandHelper::determineForceInstallingPackageVersion($input),
- );
+ try {
+ $package = ($this->dependencyResolver)(
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ CommandHelper::determineForceInstallingPackageVersion($input),
+ );
+ } catch (UnableToResolveRequirement $unableToResolveRequirement) {
+ return CommandHelper::handlePackageNotFound(
+ $unableToResolveRequirement,
+ $this->findMatchingPackages,
+ $output,
+ $targetPlatform,
+ $this->container,
+ );
+ }
+
$output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix()));
$output->writeln(sprintf('Extension name: %s', $package->extensionName()->name()));
diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php
index fd2c8fde..ba9a53f8 100644
--- a/src/Command/InstallCommand.php
+++ b/src/Command/InstallCommand.php
@@ -10,6 +10,9 @@
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieOperation;
use Php\Pie\DependencyResolver\DependencyResolver;
+use Php\Pie\DependencyResolver\InvalidPackageName;
+use Php\Pie\DependencyResolver\UnableToResolveRequirement;
+use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages;
use Php\Pie\Platform\TargetPlatform;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
@@ -30,6 +33,7 @@ public function __construct(
private readonly DependencyResolver $dependencyResolver,
private readonly ComposerIntegrationHandler $composerIntegrationHandler,
private readonly InvokeSubCommand $invokeSubCommand,
+ private readonly FindMatchingPackages $findMatchingPackages,
) {
parent::__construct();
}
@@ -56,8 +60,19 @@ public function execute(InputInterface $input, OutputInterface $output): int
$output->writeln('This command may need elevated privileges, and may prompt you for your password.');
}
- $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
- $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output);
+ try {
+ $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input);
+ } catch (InvalidPackageName $invalidPackageName) {
+ return CommandHelper::handlePackageNotFound(
+ $invalidPackageName,
+ $this->findMatchingPackages,
+ $output,
+ $targetPlatform,
+ $this->container,
+ );
+ }
+
$forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input);
$composer = PieComposerFactory::createPieComposer(
@@ -73,12 +88,23 @@ public function execute(InputInterface $input, OutputInterface $output): int
),
);
- $package = ($this->dependencyResolver)(
- $composer,
- $targetPlatform,
- $requestedNameAndVersion,
- $forceInstallPackageVersion,
- );
+ try {
+ $package = ($this->dependencyResolver)(
+ $composer,
+ $targetPlatform,
+ $requestedNameAndVersion,
+ $forceInstallPackageVersion,
+ );
+ } catch (UnableToResolveRequirement $unableToResolveRequirement) {
+ return CommandHelper::handlePackageNotFound(
+ $unableToResolveRequirement,
+ $this->findMatchingPackages,
+ $output,
+ $targetPlatform,
+ $this->container,
+ );
+ }
+
$output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix()));
// Now we know what package we have, we can validate the configure options for the command and re-create the
diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php
index ebfe2b9d..73dd00da 100644
--- a/src/Command/InstallExtensionsForProjectCommand.php
+++ b/src/Command/InstallExtensionsForProjectCommand.php
@@ -201,7 +201,7 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePac
));
try {
- $matches = $this->findMatchingPackages->for($pieComposer, $extension);
+ $matches = $this->findMatchingPackages->for($pieComposer, $extension->name());
} catch (OutOfRangeException) {
$anyErrorsHappened = true;
diff --git a/src/ComposerIntegration/PieComposerRequest.php b/src/ComposerIntegration/PieComposerRequest.php
index 405f78b9..ef6697cd 100644
--- a/src/ComposerIntegration/PieComposerRequest.php
+++ b/src/ComposerIntegration/PieComposerRequest.php
@@ -39,7 +39,7 @@ public static function noOperation(
return new PieComposerRequest(
$pieOutput,
$targetPlatform,
- new RequestedPackageAndVersion('null', null),
+ new RequestedPackageAndVersion('null/null', null),
PieOperation::Resolve,
[],
null,
diff --git a/src/DependencyResolver/InvalidPackageName.php b/src/DependencyResolver/InvalidPackageName.php
new file mode 100644
index 00000000..ae54015f
--- /dev/null
+++ b/src/DependencyResolver/InvalidPackageName.php
@@ -0,0 +1,28 @@
+package,
+ ),
+ $requestedPackageAndVersion,
+ );
+ }
+}
diff --git a/src/DependencyResolver/RequestedPackageAndVersion.php b/src/DependencyResolver/RequestedPackageAndVersion.php
index b7de62cb..6b252e90 100644
--- a/src/DependencyResolver/RequestedPackageAndVersion.php
+++ b/src/DependencyResolver/RequestedPackageAndVersion.php
@@ -4,6 +4,8 @@
namespace Php\Pie\DependencyResolver;
+use function str_contains;
+
/**
* @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks
*
@@ -14,10 +16,15 @@ final class RequestedPackageAndVersion
/**
* @param non-empty-string $package
* @param non-empty-string|null $version
+ *
+ * @throws InvalidPackageName
*/
public function __construct(
public readonly string $package,
public readonly string|null $version,
) {
+ if (! str_contains($this->package, '/')) {
+ throw InvalidPackageName::fromMissingForwardSlash($this);
+ }
}
}
diff --git a/src/DependencyResolver/UnableToResolveRequirement.php b/src/DependencyResolver/UnableToResolveRequirement.php
index cdd9e41c..8d32e09f 100644
--- a/src/DependencyResolver/UnableToResolveRequirement.php
+++ b/src/DependencyResolver/UnableToResolveRequirement.php
@@ -16,28 +16,39 @@
class UnableToResolveRequirement extends RuntimeException
{
+ public function __construct(string $message, public readonly RequestedPackageAndVersion $requestedPackageAndVersion)
+ {
+ parent::__construct($message);
+ }
+
public static function fromRequirement(
RequestedPackageAndVersion $requestedPackageAndVersion,
QuieterConsoleIO $io,
): self {
$errors = $io->errors;
- return new self(sprintf(
- 'Unable to find an installable package %s for %s, with minimum stability %s.%s',
- $requestedPackageAndVersion->package,
- $requestedPackageAndVersion->version !== null ? sprintf('version %s', $requestedPackageAndVersion->version) : 'the latest compatible version',
- DetermineMinimumStability::fromRequestedVersion($requestedPackageAndVersion->version),
- count($errors) ? "\n\n" . implode("\n\n", array_map(static fn ($e) => strip_tags($e), $errors)) : '',
- ));
+ return new self(
+ sprintf(
+ 'Unable to find an installable package %s for %s, with minimum stability %s.%s',
+ $requestedPackageAndVersion->package,
+ $requestedPackageAndVersion->version !== null ? sprintf('version %s', $requestedPackageAndVersion->version) : 'the latest compatible version',
+ DetermineMinimumStability::fromRequestedVersion($requestedPackageAndVersion->version),
+ count($errors) ? "\n\n" . implode("\n\n", array_map(static fn ($e) => strip_tags($e), $errors)) : '',
+ ),
+ $requestedPackageAndVersion,
+ );
}
public static function toPhpOrZendExtension(PackageInterface $locatedComposerPackage, RequestedPackageAndVersion $requestedPackageAndVersion): self
{
- return new self(sprintf(
- 'Package %s was not of type php-ext or php-ext-zend (requested %s%s).',
- $locatedComposerPackage->getName(),
- $requestedPackageAndVersion->package,
- $requestedPackageAndVersion->version !== null ? sprintf(' for version %s', $requestedPackageAndVersion->version) : '',
- ));
+ return new self(
+ sprintf(
+ 'Package %s was not of type php-ext or php-ext-zend (requested %s%s).',
+ $locatedComposerPackage->getName(),
+ $requestedPackageAndVersion->package,
+ $requestedPackageAndVersion->version !== null ? sprintf(' for version %s', $requestedPackageAndVersion->version) : '',
+ ),
+ $requestedPackageAndVersion,
+ );
}
}
diff --git a/src/Installing/InstallForPhpProject/FindMatchingPackages.php b/src/Installing/InstallForPhpProject/FindMatchingPackages.php
index dbb6c401..8886ad4d 100644
--- a/src/Installing/InstallForPhpProject/FindMatchingPackages.php
+++ b/src/Installing/InstallForPhpProject/FindMatchingPackages.php
@@ -7,7 +7,6 @@
use Composer\Composer;
use Composer\Repository\RepositoryInterface;
use OutOfRangeException;
-use Php\Pie\ExtensionName;
use function array_merge;
use function count;
@@ -21,16 +20,16 @@
class FindMatchingPackages
{
/** @return MatchingPackages */
- public function for(Composer $pieComposer, ExtensionName $extension): array
+ public function for(Composer $pieComposer, string $searchTerm): array
{
$matches = [];
foreach ($pieComposer->getRepositoryManager()->getRepositories() as $repo) {
- $matches = array_merge($matches, $repo->search($extension->name(), RepositoryInterface::SEARCH_FULLTEXT, 'php-ext'));
- $matches = array_merge($matches, $repo->search($extension->name(), RepositoryInterface::SEARCH_FULLTEXT, 'php-ext-zend'));
+ $matches = array_merge($matches, $repo->search($searchTerm, RepositoryInterface::SEARCH_FULLTEXT, 'php-ext'));
+ $matches = array_merge($matches, $repo->search($searchTerm, RepositoryInterface::SEARCH_FULLTEXT, 'php-ext-zend'));
}
if (! count($matches)) {
- throw new OutOfRangeException('No matches found for ' . $extension->name());
+ throw new OutOfRangeException('No matches found for ' . $searchTerm);
}
usort($matches, static function (array $a, array $b): int {
diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php
index e892ce4d..4fd71ad6 100644
--- a/test/integration/Command/DownloadCommandTest.php
+++ b/test/integration/Command/DownloadCommandTest.php
@@ -7,7 +7,6 @@
use Composer\Util\Platform;
use Php\Pie\Command\DownloadCommand;
use Php\Pie\Container;
-use Php\Pie\DependencyResolver\UnableToResolveRequirement;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily;
@@ -145,9 +144,14 @@ public function testDownloadingWithPhpPath(string $requestedVersion, string $exp
#[RequiresPhp('<8.2')]
public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void
{
- $this->expectException(UnableToResolveRequirement::class);
// 1.0.0 is only compatible with PHP 8.3.0
- $this->commandTester->execute(['requested-package-and-version' => self::TEST_PACKAGE . ':1.0.0']);
+ self::assertSame(1, $this->commandTester->execute(['requested-package-and-version' => self::TEST_PACKAGE . ':1.0.0']));
+
+ $output = $this->commandTester->getDisplay();
+ self::assertStringContainsString(
+ 'Unable to find an installable package asgrim/example-pie-extension for version 1.0.0, with minimum stability stable.',
+ $output,
+ );
}
#[RequiresOperatingSystemFamily('Linux')]