diff --git a/features/build-extensions.feature b/features/build-extensions.feature index 3006594f..109435e2 100644 --- a/features/build-extensions.feature +++ b/features/build-extensions.feature @@ -15,3 +15,8 @@ Feature: Extensions can be built with PIE Example: An extension can be built with configure options When I run a command to build an extension with configure options Then the extension should have been built with options + + # pie build + Example: Multiple extensions can be built at once + When I run a command to build multiple extensions + Then the extensions should have been built diff --git a/features/bundled-php-extensions.feature b/features/bundled-php-extensions.feature index 2a6f0d50..f598c678 100644 --- a/features/bundled-php-extensions.feature +++ b/features/bundled-php-extensions.feature @@ -6,6 +6,7 @@ Feature: Bundled PHP extensions can be installed When I install the sodium extension with PIE Then the extension should have been installed and enabled + # pie uninstall php/sodium Example: A bundled extension installed with PIE can be uninstalled Given I have the sodium extension installed with PIE When I run a command to uninstall an extension diff --git a/features/download-extensions.feature b/features/download-extensions.feature index 7f33956e..7d7e7a0b 100644 --- a/features/download-extensions.feature +++ b/features/download-extensions.feature @@ -20,3 +20,8 @@ Feature: Extensions can be downloaded with PIE Example: An in-development version can be downloaded on non-Windows systems When I run a command to download version "dev-main" of an extension Then version "dev-main" should have been downloaded + + # pie download + Example: Multiple extensions can be downloaded at once + When I run a command to download multiple extensions + Then the extensions should have been downloaded diff --git a/features/install-extensions.feature b/features/install-extensions.feature index 63d0b239..a2b6ebe6 100644 --- a/features/install-extensions.feature +++ b/features/install-extensions.feature @@ -9,3 +9,8 @@ Feature: Extensions can be installed with PIE Example: An extension can be installed and enabled When I run a command to install an extension Then the extension should have been installed and enabled + + # pie install + Example: Multiple extensions can be installed at once + When I run a command to install multiple extensions + Then the extensions should have been installed and enabled diff --git a/features/uninstall-extensions.feature b/features/uninstall-extensions.feature index 93002d4f..11c13cad 100644 --- a/features/uninstall-extensions.feature +++ b/features/uninstall-extensions.feature @@ -5,3 +5,9 @@ Feature: Extensions can be uninstalled with PIE Given an extension was previously installed and enabled When I run a command to uninstall an extension Then the extension should not be installed anymore + + # pie uninstall + Example: Multiple extensions can be uninstalled at once + Given multiple extensions were previously installed and enabled + When I run a command to uninstall multiple extensions + Then the extensions should not be installed anymore diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 5ec0f102..fb827ed1 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -36,12 +36,6 @@ parameters: count: 1 path: src/Command/SelfVerifyCommand.php - - - message: '#^Cannot cast mixed to string\.$#' - identifier: cast.string - count: 1 - path: src/Command/UninstallCommand.php - - message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' identifier: function.alreadyNarrowedType diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 062eac76..278b6936 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -14,6 +14,7 @@ use Php\Pie\DependencyResolver\DependencyInstaller\PrescanSystemDependencies; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\DependencyResolver\InvalidPackageName; +use Php\Pie\DependencyResolver\ResolvedPackageRequest; use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; use Php\Pie\Platform\PackageManager; @@ -25,8 +26,6 @@ use Symfony\Component\Console\Output\OutputInterface; use Throwable; -use function sprintf; - #[AsCommand( name: 'build', description: 'Download and build a PIE-compatible PHP extension, without installing it.', @@ -56,7 +55,7 @@ public function execute(InputInterface $input, OutputInterface $output): int { $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); try { - $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $requestedNamesAndVersions = CommandHelper::requestedNameAndVersionPairs($input); } catch (InvalidPackageName $invalidPackageName) { return CommandHelper::handlePackageNotFound( $invalidPackageName, @@ -84,7 +83,7 @@ public function execute(InputInterface $input, OutputInterface $output): int new PieComposerRequest( $this->io, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, PieOperation::Resolve, [], // Configure options are not needed for resolve only false, // setting up INI not needed for build @@ -92,26 +91,30 @@ public function execute(InputInterface $input, OutputInterface $output): int ); if (CommandHelper::shouldCheckSystemDependencies($input)) { - try { - ($this->prescanSystemDependencies)( - $composer, - $targetPlatform, - $requestedNameAndVersion, - CommandHelper::autoInstallSystemDependencies($input), - ); - } catch (Throwable $anything) { - $this->io->writeError( - 'Skipping system dependency pre-scan due to exception: ' . $anything->getMessage(), - verbosity: IOInterface::VERBOSE, - ); + foreach ($requestedNamesAndVersions as $requestedNameAndVersion) { + try { + ($this->prescanSystemDependencies)( + $composer, + $targetPlatform, + $requestedNameAndVersion, + CommandHelper::autoInstallSystemDependencies($input), + ); + } catch (Throwable $anything) { + $this->io->writeError( + 'Skipping system dependency pre-scan due to exception: ' . $anything->getMessage(), + verbosity: IOInterface::VERBOSE, + ); + } } } try { - $package = ($this->dependencyResolver)( + $resolvedPackages = CommandHelper::resolveRequestedPackages( + $this->dependencyResolver, + $this->io, $composer, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, $forceInstallPackageVersion, ); } catch (UnableToResolveRequirement $unableToResolveRequirement) { @@ -129,19 +132,18 @@ public function execute(InputInterface $input, OutputInterface $output): int return self::INVALID; } - $this->io->write(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 + // Now we know what packages we have, we can validate the configure options for the command and re-create the // Composer instance with the populated configure options - CommandHelper::bindConfigureOptionsFromPackage($this, $package, $input); - $configureOptionsValues = CommandHelper::processConfigureOptionsFromInput($package, $input); + $resolvedPiePackages = ResolvedPackageRequest::piePackages($resolvedPackages); + CommandHelper::bindConfigureOptionsFromPackage($this, $resolvedPiePackages, $input); + $configureOptionsValues = CommandHelper::processConfigureOptionsFromInput($resolvedPiePackages, $input); $composer = PieComposerFactory::createPieComposer( $this->container, new PieComposerRequest( $this->io, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, PieOperation::Build, $configureOptionsValues, false, // setting up INI not needed for build @@ -150,10 +152,9 @@ public function execute(InputInterface $input, OutputInterface $output): int try { $this->composerIntegrationHandler->runInstall( - $package, + $resolvedPackages, $composer, $targetPlatform, - $requestedNameAndVersion, $forceInstallPackageVersion, false, ); diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 3d0f0254..c16849dc 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -16,9 +16,12 @@ use OutOfRangeException; use Php\Pie\ComposerIntegration\PieComposerFactory; use Php\Pie\ComposerIntegration\PieComposerRequest; +use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal; +use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\DependencyResolver\InvalidPackageName; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\DependencyResolver\ResolvedPackageRequest; use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\ExtensionName; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; @@ -37,6 +40,7 @@ use function array_key_exists; use function array_map; +use function array_values; use function assert; use function count; use function explode; @@ -110,8 +114,8 @@ public static function configureDownloadBuildInstallOptions(Command $command, bo if ($withRequestedPackageAndVersion) { $command->addArgument( self::ARG_REQUESTED_PACKAGE_AND_VERSION, - InputArgument::OPTIONAL, - 'The PIE package name and version constraint to use, in the format {vendor/package}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.', + InputArgument::OPTIONAL | InputArgument::IS_ARRAY, + 'The PIE package names and versions constraint to use, in the format {vendor/package}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.', ); } @@ -312,74 +316,152 @@ public static function shouldCheckSystemDependencies(InputInterface $input): boo || ! $input->getOption(self::OPTION_SUPPRESS_SYSTEM_DEPENDENCIES_CHECK); } - public static function requestedNameAndVersionPair(InputInterface $input): RequestedPackageAndVersion + /** @return non-empty-list */ + public static function requestedNameAndVersionPairs(InputInterface $input): array { - $requestedPackageString = $input->getArgument(self::ARG_REQUESTED_PACKAGE_AND_VERSION); + $requestedPackageStrings = $input->getArgument(self::ARG_REQUESTED_PACKAGE_AND_VERSION); - if (! is_string($requestedPackageString) || $requestedPackageString === '') { + if (is_string($requestedPackageStrings)) { + $requestedPackageStrings = [$requestedPackageStrings]; + } + + if (! is_array($requestedPackageStrings) || ! count($requestedPackageStrings)) { throw new InvalidArgumentException('No package was requested for installation'); } - $nameAndVersionPairs = (new VersionParser()) - ->parseNameVersionPairs([$requestedPackageString]); - $requestedNameAndVersionPair = reset($nameAndVersionPairs); + Assert::allStringNotEmpty($requestedPackageStrings); - if (! is_array($requestedNameAndVersionPair)) { - throw new InvalidArgumentException('Failed to parse the name/version pair'); - } + return array_values(array_map( + static function (string $requestedPackageString): RequestedPackageAndVersion { + $nameAndVersionPairs = (new VersionParser()) + ->parseNameVersionPairs([$requestedPackageString]); + $requestedNameAndVersionPair = reset($nameAndVersionPairs); - if (! array_key_exists('version', $requestedNameAndVersionPair)) { - $requestedNameAndVersionPair['version'] = null; - } + if (! is_array($requestedNameAndVersionPair)) { + throw new InvalidArgumentException('Failed to parse the name/version pair'); + } + + if (! array_key_exists('version', $requestedNameAndVersionPair)) { + $requestedNameAndVersionPair['version'] = null; + } + + Assert::stringNotEmpty($requestedNameAndVersionPair['name']); + Assert::nullOrStringNotEmpty($requestedNameAndVersionPair['version']); - Assert::stringNotEmpty($requestedNameAndVersionPair['name']); - Assert::nullOrStringNotEmpty($requestedNameAndVersionPair['version']); + return new RequestedPackageAndVersion( + $requestedNameAndVersionPair['name'], + $requestedNameAndVersionPair['version'], + ); + }, + $requestedPackageStrings, + )); + } - return new RequestedPackageAndVersion( - $requestedNameAndVersionPair['name'], - $requestedNameAndVersionPair['version'], + /** + * @param non-empty-list $requestedNamesAndVersions + * + * @return non-empty-list + * + * @throws UnableToResolveRequirement + * @throws BundledPhpExtensionRefusal + */ + public static function resolveRequestedPackages( + DependencyResolver $dependencyResolver, + IOInterface $io, + Composer $composer, + TargetPlatform $targetPlatform, + array $requestedNamesAndVersions, + bool $forceInstallPackageVersion, + ): array { + return array_map( + static function (RequestedPackageAndVersion $requestedNameAndVersion) use ($dependencyResolver, $io, $composer, $targetPlatform, $forceInstallPackageVersion): ResolvedPackageRequest { + $resolvedPackage = $dependencyResolver( + $composer, + $targetPlatform, + $requestedNameAndVersion, + $forceInstallPackageVersion, + ); + + $io->write(sprintf( + 'Found package: %s which provides %s', + $resolvedPackage->piePackage->prettyNameAndVersion(), + $resolvedPackage->piePackage->extensionName()->nameWithExtPrefix(), + )); + + return $resolvedPackage; + }, + $requestedNamesAndVersions, ); } - public static function bindConfigureOptionsFromPackage(Command $command, Package $package, InputInterface $input): void + /** + * @param non-empty-list $packages + * + * @throws ConfigureOptionCollision if two of the requested packages declare a configure option with the same name. + */ + public static function bindConfigureOptionsFromPackage(Command $command, array $packages, InputInterface $input): void { - foreach ($package->configureOptions() as $configureOption) { - $command->addOption( - $configureOption->name, - null, - $configureOption->needsValue ? InputOption::VALUE_REQUIRED : InputOption::VALUE_NONE, - $configureOption->description, - ); + /** @var array $optionOwners */ + $optionOwners = []; + + foreach ($packages as $package) { + foreach ($package->configureOptions() as $configureOption) { + if (array_key_exists($configureOption->name, $optionOwners)) { + throw ConfigureOptionCollision::forOptionName( + $configureOption->name, + $optionOwners[$configureOption->name], + $package, + ); + } + + $optionOwners[$configureOption->name] = $package; + + $command->addOption( + $configureOption->name, + null, + $configureOption->needsValue ? InputOption::VALUE_REQUIRED : InputOption::VALUE_NONE, + $configureOption->description, + ); + } } self::validateInput($input, $command); } - /** @return list */ - public static function processConfigureOptionsFromInput(Package $package, InputInterface $input): array + /** + * @param non-empty-list $packages + * + * @return array> Keyed by package name + */ + public static function processConfigureOptionsFromInput(array $packages, InputInterface $input): array { $configureOptionsValues = []; - foreach ($package->configureOptions() as $configureOption) { - if (! $input->hasOption($configureOption->name)) { - continue; - } + foreach ($packages as $package) { + $optionsForPackage = []; + foreach ($package->configureOptions() as $configureOption) { + if (! $input->hasOption($configureOption->name)) { + continue; + } - $value = $input->getOption($configureOption->name); + $value = $input->getOption($configureOption->name); - if ($configureOption->needsValue) { - if (is_string($value) && $value !== '') { - $configureOptionsValues[] = '--' . $configureOption->name . '=' . $value; + if ($configureOption->needsValue) { + if (is_string($value) && $value !== '') { + $optionsForPackage[] = '--' . $configureOption->name . '=' . $value; + } + + continue; } - continue; - } + Assert::boolean($value); + if ($value !== true) { + continue; + } - Assert::boolean($value); - if ($value !== true) { - continue; + $optionsForPackage[] = '--' . $configureOption->name; } - $configureOptionsValues[] = '--' . $configureOption->name; + $configureOptionsValues[$package->name()] = $optionsForPackage; } return $configureOptionsValues; diff --git a/src/Command/ConfigureOptionCollision.php b/src/Command/ConfigureOptionCollision.php new file mode 100644 index 00000000..b42839e7 --- /dev/null +++ b/src/Command/ConfigureOptionCollision.php @@ -0,0 +1,24 @@ +name(), + $secondPackage->name(), + $optionName, + )); + } +} diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 34ce6554..b8781ed8 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -21,8 +21,6 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use function sprintf; - #[AsCommand( name: 'download', description: 'Same behaviour as build, but puts the files in a local directory for manual building and installation.', @@ -52,7 +50,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); try { - $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $requestedNamesAndVersions = CommandHelper::requestedNameAndVersionPairs($input); } catch (InvalidPackageName $invalidPackageName) { return CommandHelper::handlePackageNotFound( $invalidPackageName, @@ -71,7 +69,7 @@ public function execute(InputInterface $input, OutputInterface $output): int new PieComposerRequest( $this->io, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, PieOperation::Download, [], // Configure options are not needed for download only false, // setting up INI not needed for download @@ -79,10 +77,12 @@ public function execute(InputInterface $input, OutputInterface $output): int ); try { - $package = ($this->dependencyResolver)( + $resolvedPackages = CommandHelper::resolveRequestedPackages( + $this->dependencyResolver, + $this->io, $composer, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, $forceInstallPackageVersion, ); } catch (UnableToResolveRequirement $unableToResolveRequirement) { @@ -100,14 +100,11 @@ public function execute(InputInterface $input, OutputInterface $output): int return self::INVALID; } - $this->io->write(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); - try { $this->composerIntegrationHandler->runInstall( - $package, + $resolvedPackages, $composer, $targetPlatform, - $requestedNameAndVersion, $forceInstallPackageVersion, false, ); diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php index 21112a61..fc30a98e 100644 --- a/src/Command/InfoCommand.php +++ b/src/Command/InfoCommand.php @@ -19,6 +19,7 @@ use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; @@ -46,7 +47,13 @@ public function configure(): void { parent::configure(); - CommandHelper::configureDownloadBuildInstallOptions($this); + // `info` only ever supports a single package, unlike the other download/build/install commands. + CommandHelper::configureDownloadBuildInstallOptions($this, false); + $this->addArgument( + CommandHelper::ARG_REQUESTED_PACKAGE_AND_VERSION, + InputArgument::REQUIRED, + 'The PIE package name and version constraint to use, in the format {vendor/package}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.', + ); } public function execute(InputInterface $input, OutputInterface $output): int @@ -56,7 +63,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); try { - $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $requestedNamesAndVersions = CommandHelper::requestedNameAndVersionPairs($input); } catch (InvalidPackageName $invalidPackageName) { return CommandHelper::handlePackageNotFound( $invalidPackageName, @@ -74,7 +81,7 @@ public function execute(InputInterface $input, OutputInterface $output): int new PieComposerRequest( $this->io, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, PieOperation::Resolve, [], // Configure options are not needed for resolve only false, // setting up INI not needed for info @@ -82,10 +89,12 @@ public function execute(InputInterface $input, OutputInterface $output): int ); try { - $package = ($this->dependencyResolver)( + $resolvedPackages = CommandHelper::resolveRequestedPackages( + $this->dependencyResolver, + $this->io, $composer, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, true, ); } catch (UnableToResolveRequirement $unableToResolveRequirement) { @@ -103,7 +112,7 @@ public function execute(InputInterface $input, OutputInterface $output): int return self::INVALID; } - $this->io->write(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); + $package = $resolvedPackages[0]->piePackage; $this->io->write(sprintf('Extension name: %s', $package->extensionName()->name())); $this->io->write(sprintf('Extension type: %s (%s)', $package->extensionType()->value, $package->extensionType()->name)); diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index c74c8f7d..618d0114 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -14,6 +14,7 @@ use Php\Pie\DependencyResolver\DependencyInstaller\PrescanSystemDependencies; use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\DependencyResolver\InvalidPackageName; +use Php\Pie\DependencyResolver\ResolvedPackageRequest; use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\Installing\InstallForPhpProject\FindMatchingPackages; use Php\Pie\Platform\PackageManager; @@ -26,8 +27,6 @@ use Symfony\Component\Console\Output\OutputInterface; use Throwable; -use function sprintf; - #[AsCommand( name: 'install', description: 'Download, build, and install a PIE-compatible PHP extension.', @@ -70,7 +69,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); try { - $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $requestedNamesAndVersions = CommandHelper::requestedNameAndVersionPairs($input); } catch (InvalidPackageName $invalidPackageName) { return CommandHelper::handlePackageNotFound( $invalidPackageName, @@ -98,7 +97,7 @@ public function execute(InputInterface $input, OutputInterface $output): int new PieComposerRequest( $this->io, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, PieOperation::Resolve, [], // Configure options are not needed for resolve only false, // setting up INI not needed for resolve step @@ -106,26 +105,30 @@ public function execute(InputInterface $input, OutputInterface $output): int ); if (CommandHelper::shouldCheckSystemDependencies($input)) { - try { - ($this->prescanSystemDependencies)( - $composer, - $targetPlatform, - $requestedNameAndVersion, - CommandHelper::autoInstallSystemDependencies($input), - ); - } catch (Throwable $anything) { - $this->io->writeError( - 'Skipping system dependency pre-scan due to exception: ' . $anything->getMessage(), - verbosity: IOInterface::VERBOSE, - ); + foreach ($requestedNamesAndVersions as $requestedNameAndVersion) { + try { + ($this->prescanSystemDependencies)( + $composer, + $targetPlatform, + $requestedNameAndVersion, + CommandHelper::autoInstallSystemDependencies($input), + ); + } catch (Throwable $anything) { + $this->io->writeError( + 'Skipping system dependency pre-scan due to exception: ' . $anything->getMessage(), + verbosity: IOInterface::VERBOSE, + ); + } } } try { - $package = ($this->dependencyResolver)( + $resolvedPackages = CommandHelper::resolveRequestedPackages( + $this->dependencyResolver, + $this->io, $composer, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, $forceInstallPackageVersion, ); } catch (UnableToResolveRequirement $unableToResolveRequirement) { @@ -143,19 +146,18 @@ public function execute(InputInterface $input, OutputInterface $output): int return self::INVALID; } - $this->io->write(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 + // Now we know what packages we have, we can validate the configure options for the command and re-create the // Composer instance with the populated configure options - CommandHelper::bindConfigureOptionsFromPackage($this, $package, $input); - $configureOptionsValues = CommandHelper::processConfigureOptionsFromInput($package, $input); + $resolvedPiePackages = ResolvedPackageRequest::piePackages($resolvedPackages); + CommandHelper::bindConfigureOptionsFromPackage($this, $resolvedPiePackages, $input); + $configureOptionsValues = CommandHelper::processConfigureOptionsFromInput($resolvedPiePackages, $input); $composer = PieComposerFactory::createPieComposer( $this->container, new PieComposerRequest( $this->io, $targetPlatform, - $requestedNameAndVersion, + $requestedNamesAndVersions, PieOperation::Install, $configureOptionsValues, CommandHelper::determineAttemptToSetupIniFile($input), @@ -164,10 +166,9 @@ public function execute(InputInterface $input, OutputInterface $output): int try { $this->composerIntegrationHandler->runInstall( - $package, + $resolvedPackages, $composer, $targetPlatform, - $requestedNameAndVersion, $forceInstallPackageVersion, true, ); diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php index bd33fd1d..884b161c 100644 --- a/src/Command/InstallExtensionsForProjectCommand.php +++ b/src/Command/InstallExtensionsForProjectCommand.php @@ -12,6 +12,7 @@ use Php\Pie\ComposerIntegration\PieComposerFactory; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieJsonEditor; +use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Installing\InstallForPhpProject\CheckExtensionStatus; @@ -36,9 +37,11 @@ use Throwable; use Webmozart\Assert\Assert; +use function array_filter; use function array_keys; use function array_map; -use function array_walk; +use function array_values; +use function count; use function implode; use function Safe\getcwd; use function Safe\realpath; @@ -136,26 +139,56 @@ private function handlePhpProject(InputInterface $input, RootPackageInterface $r $anyErrorsHappened = false; - array_walk( + $scheduledForInstall = array_values(array_filter(array_map( + function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePackages, &$anyErrorsHappened, $targetPlatform, $extensionToPackageSelections): RequestedPackageAndVersion|null { + $result = $this->handleSingleExtensionRequiredByPhpProject( + $pieComposer, + $link, + $installedPiePackages, + $targetPlatform, + $phpEnabledExtensions, + $extensionToPackageSelections, + ); + + if ($result instanceof RequestedPackageAndVersion) { + return $result; + } + + if ($result === false) { + $anyErrorsHappened = true; + } + + return null; + }, $extensionsRequired, - function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePackages, $input, &$anyErrorsHappened, $targetPlatform, $extensionToPackageSelections): void { - if ( - $this->handleSingleExtensionRequiredByPhpProject( + ))); + + if (count($scheduledForInstall)) { + $nicePackageList = implode(', ', array_map( + static fn (RequestedPackageAndVersion $req) => $req->prettyNameAndVersion(), + $scheduledForInstall, + )); + + try { + $this->io->write( + sprintf('Invoking pie install of %s', $nicePackageList), + verbosity: IOInterface::VERBOSE, + ); + Assert::same( + 0, + $this->installSelectedPackage->withSubCommand( + $scheduledForInstall, + $this, $input, - $pieComposer, - $link, - $installedPiePackages, - $targetPlatform, - $phpEnabledExtensions, - $extensionToPackageSelections, - ) - ) { - return; - } + ), + 'Non-zero exit code %s whilst installing ' . $nicePackageList, + ); + } catch (Throwable $t) { + $this->io->writeError('' . $t->getMessage() . ''); $anyErrorsHappened = true; - }, - ); + } + } $this->io->write(PHP_EOL . 'Finished checking extensions.'); @@ -172,14 +205,13 @@ function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePac * @param array $extensionToPackageSelections */ private function handleSingleExtensionRequiredByPhpProject( - InputInterface $input, Composer $pieComposer, Link $link, PiePackageList $installedPiePackages, TargetPlatform $targetPlatform, array $phpEnabledExtensions, array $extensionToPackageSelections, - ): bool { + ): RequestedPackageAndVersion|bool { $extension = ExtensionName::normaliseFromString($link->getTarget()); $piePackagesForExtension = $installedPiePackages ->findByPhpFormattedExtensionName($extension->phpFormattedExtensionName()) @@ -223,28 +255,7 @@ private function handleSingleExtensionRequiredByPhpProject( return false; } - try { - $this->io->write( - sprintf('Invoking pie install of %s', $requestedPackageAndVersion->prettyNameAndVersion()), - verbosity: IOInterface::VERBOSE, - ); - Assert::same( - 0, - $this->installSelectedPackage->withSubCommand( - $extension, - $requestedPackageAndVersion, - $this, - $input, - ), - 'Non-zero exit code %s whilst installing ' . $requestedPackageAndVersion->package, - ); - - return true; - } catch (Throwable $t) { - $this->io->writeError('' . $t->getMessage() . ''); - - return false; - } + return $requestedPackageAndVersion; } public function execute(InputInterface $input, OutputInterface $output): int diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index ec457f09..d43f5f41 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -151,16 +151,16 @@ function (string $version, string $phpExtensionName) use ($composer, $rootPackag } $updateNotice = ''; - if ($latestConstrainedPackage !== null && $latestConstrainedPackage->version() !== $piePackage->version()) { + if ($latestConstrainedPackage !== null && $latestConstrainedPackage->piePackage->version() !== $piePackage->version()) { $updateNotice = sprintf( ', upgradable to %s (within %s)', - $latestConstrainedPackage->version(), + $latestConstrainedPackage->piePackage->version(), $packageRequirement, ); } - if ($latestPackage !== null && $latestPackage->version() !== $latestConstrainedPackage->version()) { - $updateNotice .= sprintf(', latest version is %s', $latestPackage->version()); + if ($latestPackage !== null && $latestPackage->piePackage->version() !== $latestConstrainedPackage->piePackage->version()) { + $updateNotice .= sprintf(', latest version is %s', $latestPackage->piePackage->version()); } $this->io->write(sprintf( diff --git a/src/Command/UninstallCommand.php b/src/Command/UninstallCommand.php index 86b2bf67..cfe4604a 100644 --- a/src/Command/UninstallCommand.php +++ b/src/Command/UninstallCommand.php @@ -4,15 +4,15 @@ namespace Php\Pie\Command; -use Composer\Composer; use Composer\IO\IOInterface; use Composer\IO\NullIO; +use OutOfRangeException; use Php\Pie\ComposerIntegration\ComposerIntegrationHandler; use Php\Pie\ComposerIntegration\PieComposerFactory; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; -use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\DependencyResolver\ResolvedPackageRequest; use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\TargetPlatform; use Psr\Container\ContainerInterface; @@ -23,13 +23,15 @@ use Symfony\Component\Console\Output\OutputInterface; use Webmozart\Assert\Assert; +use function array_map; + #[AsCommand( name: 'uninstall', description: 'Disable and remove an extension that has been installed with PIE', )] final class UninstallCommand extends Command { - private const ARG_PACKAGE_NAME = 'package-name'; + private const ARG_PACKAGE_NAMES = 'package-name'; public function __construct( private readonly InstalledPiePackages $installedPiePackages, @@ -45,9 +47,9 @@ public function configure(): void parent::configure(); $this->addArgument( - self::ARG_PACKAGE_NAME, - InputArgument::REQUIRED, - 'The package name to remove, in the format {vendor/package}, for example `xdebug/xdebug`', + self::ARG_PACKAGE_NAMES, + InputArgument::REQUIRED | InputArgument::IS_ARRAY, + 'The package names to remove, in the format {vendor/package}, for example `xdebug/xdebug`', ); CommandHelper::configurePhpConfigOptions($this); @@ -59,9 +61,13 @@ public function execute(InputInterface $input, OutputInterface $output): int $this->io->write('This command may need elevated privileges, and may prompt you for your password.'); } - $packageToRemove = (string) $input->getArgument(self::ARG_PACKAGE_NAME); - Assert::stringNotEmpty($packageToRemove); - $requestedPackageAndVersionToRemove = new RequestedPackageAndVersion($packageToRemove, null); + $packagesToRemove = $input->getArgument(self::ARG_PACKAGE_NAMES); + Assert::isList($packagesToRemove); + Assert::allStringNotEmpty($packagesToRemove); + $requestedPackageAndVersionsToRemove = array_map( + static fn (string $packageName) => new RequestedPackageAndVersion($packageName, null), + $packagesToRemove, + ); $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); @@ -75,10 +81,18 @@ public function execute(InputInterface $input, OutputInterface $output): int ), ); - $piePackage = $this->findPiePackageByPackageName($packageToRemove, $composer); + $piePackages = $this->installedPiePackages->allPiePackages($composer); - if ($piePackage === null) { - $this->io->writeError('No package found: ' . $packageToRemove . ''); + try { + $resolvedPackages = array_map( + static fn (RequestedPackageAndVersion $req) => new ResolvedPackageRequest( + $piePackages->findByPackageName($req->package), + $req, + ), + $requestedPackageAndVersionsToRemove, + ); + } catch (OutOfRangeException $exception) { + $this->io->writeError('No package found: ' . $exception->getMessage() . ''); return 1; } @@ -88,7 +102,7 @@ public function execute(InputInterface $input, OutputInterface $output): int new PieComposerRequest( $this->io, $targetPlatform, - $requestedPackageAndVersionToRemove, + $requestedPackageAndVersionsToRemove, PieOperation::Uninstall, [], // Configure options are not needed for uninstall true, @@ -96,25 +110,11 @@ public function execute(InputInterface $input, OutputInterface $output): int ); $this->composerIntegrationHandler->runUninstall( - $piePackage, + $resolvedPackages, $composer, $targetPlatform, - $requestedPackageAndVersionToRemove, ); return 0; } - - private function findPiePackageByPackageName(string $packageToRemove, Composer $composer): Package|null - { - $piePackages = $this->installedPiePackages->allPiePackages($composer); - - foreach ($piePackages->packages() as $piePackage) { - if ($piePackage->name() === $packageToRemove) { - return $piePackage; - } - } - - return null; - } } diff --git a/src/ComposerIntegration/AddInstalledJsonMetadata.php b/src/ComposerIntegration/AddInstalledJsonMetadata.php index b829d2e0..d7907753 100644 --- a/src/ComposerIntegration/AddInstalledJsonMetadata.php +++ b/src/ComposerIntegration/AddInstalledJsonMetadata.php @@ -69,7 +69,7 @@ public function addBuildMetadata( $composer, $composerPackage, InstalledJsonMetadata::KEY_CONFIGURE_OPTIONS, - implode(' ', $composerRequest->configureOptions), + implode(' ', $composerRequest->configureOptionsFor($composerPackage->getName())), ); $this->addPieMetadata( diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index 6557487f..82009239 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -11,13 +11,14 @@ use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; -use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\DependencyResolver\ResolvedPackageRequest; use Php\Pie\ExtensionName; use Php\Pie\Platform; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Emoji; use Psr\Container\ContainerInterface; +use function array_map; use function assert; use function file_exists; use function sprintf; @@ -32,25 +33,23 @@ public function __construct( ) { } - public function runInstall( - Package $package, + private function addPackageIntoPieJson( + ResolvedPackageRequest $resolvedPackageRequest, Composer $composer, TargetPlatform $targetPlatform, - RequestedPackageAndVersion $requestedPackageAndVersion, - bool $forceInstallPackageVersion, - bool $runCleanup, + PieJsonEditor $pieJsonEditor, ): void { - $versionSelector = VersionSelectorFactory::make($composer, $requestedPackageAndVersion, $targetPlatform); + $versionSelector = VersionSelectorFactory::make($composer, $resolvedPackageRequest->requestedPackageAndVersion, $targetPlatform); - $recommendedRequireVersion = $requestedPackageAndVersion->version; + $recommendedRequireVersion = $resolvedPackageRequest->requestedPackageAndVersion->version; // If user did not request a specific require version, use Composer to recommend one for the pie.json if ($recommendedRequireVersion === null) { - $recommendedRequireVersion = $versionSelector->findRecommendedRequireVersion($package->composerPackage()); + $recommendedRequireVersion = $versionSelector->findRecommendedRequireVersion($resolvedPackageRequest->piePackage->composerPackage()); } - if ($package->isBundledPhpExtension()) { - $stability = $package->composerPackage()->getStability(); + if ($resolvedPackageRequest->piePackage->isBundledPhpExtension()) { + $stability = $resolvedPackageRequest->piePackage->composerPackage()->getStability(); $stabilitySuffix = ''; if ($stability !== 'stable') { $stabilitySuffix = '@' . $stability; @@ -60,11 +59,32 @@ public function runInstall( } // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file + $pieJsonEditor->addRequire( + $resolvedPackageRequest->requestedPackageAndVersion->package, + $recommendedRequireVersion !== '' ? $recommendedRequireVersion : '*', + ); + } + + /** @param list $resolvedRequestedPackages */ + public function runInstall( + array $resolvedRequestedPackages, + Composer $composer, + TargetPlatform $targetPlatform, + bool $forceInstallPackageVersion, + bool $runCleanup, + ): void { $pieComposerJson = Platform::getPieJsonFilename($targetPlatform); $pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform); - $originalPieJsonContent = $pieJsonEditor->addRequire( - $requestedPackageAndVersion->package, - $recommendedRequireVersion !== '' ? $recommendedRequireVersion : '*', + $originalPieJsonContent = $pieJsonEditor->currentContent(); + + array_map( + fn (ResolvedPackageRequest $resolvedPackageRequest) => $this->addPackageIntoPieJson( + $resolvedPackageRequest, + $composer, + $targetPlatform, + $pieJsonEditor, + ), + $resolvedRequestedPackages, ); // Refresh the Composer instance so it re-reads the updated pie.json @@ -129,7 +149,7 @@ public function runInstall( $composerInstaller = PieComposerInstaller::createWithPhpBinary( $targetPlatform->phpBinaryPath, - $package->extensionName(), + ResolvedPackageRequest::extensionNames($resolvedRequestedPackages), $this->arrayCollectionIo, $composer, ); @@ -143,7 +163,7 @@ public function runInstall( if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) { $composerInstaller->setUpdate(true); - $composerInstaller->setUpdateAllowList([$requestedPackageAndVersion->package]); + $composerInstaller->setUpdateAllowList(ResolvedPackageRequest::requestedPackageNames($resolvedRequestedPackages)); } $resultCode = $composerInstaller->run(); @@ -162,23 +182,27 @@ public function runInstall( ($this->vendorCleanup)($composer); } + /** @param list $resolvedPackagesToRemove */ public function runUninstall( - Package $packageToRemove, + array $resolvedPackagesToRemove, Composer $composer, TargetPlatform $targetPlatform, - RequestedPackageAndVersion $requestedPackageAndVersionToRemove, ): void { // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file $pieComposerJson = Platform::getPieJsonFilename($targetPlatform); $pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform); - $originalPieJsonContent = $pieJsonEditor->removeRequire($requestedPackageAndVersionToRemove->package); + $originalPieJsonContent = $pieJsonEditor->currentContent(); + + foreach ($resolvedPackagesToRemove as $resolvedPackageRequest) { + $pieJsonEditor->removeRequire($resolvedPackageRequest->requestedPackageAndVersion->package); + } // Refresh the Composer instance so it re-reads the updated pie.json $composer = PieComposerFactory::recreatePieComposer($this->container, $composer); $composerInstaller = PieComposerInstaller::createWithPhpBinary( $targetPlatform->phpBinaryPath, - $packageToRemove->extensionName(), + ResolvedPackageRequest::extensionNames($resolvedPackagesToRemove), $this->arrayCollectionIo, $composer, ); @@ -191,7 +215,7 @@ public function runUninstall( if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) { $composerInstaller->setUpdate(true); - $composerInstaller->setUpdateAllowList([$requestedPackageAndVersionToRemove->package]); + $composerInstaller->setUpdateAllowList(ResolvedPackageRequest::requestedPackageNames($resolvedPackagesToRemove)); } $resultCode = $composerInstaller->run(); diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php index 45a97ab3..619b524d 100644 --- a/src/ComposerIntegration/InstallAndBuildProcess.php +++ b/src/ComposerIntegration/InstallAndBuildProcess.php @@ -61,7 +61,7 @@ public function __invoke( $builtBinaryFile = ($this->pieBuild)( $downloadedPackage, $composerRequest->targetPlatform, - $composerRequest->configureOptions, + $composerRequest->configureOptionsFor($downloadedPackage->package->name()), $io, ); diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index e0f41347..8ad19d12 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -66,7 +66,7 @@ function (OperationInterface $operation): void { } // Install requests for other packages than the one we want should be ignored - if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + if (! $this->composerRequest->isFor($composerPackage->getName())) { return; } @@ -80,7 +80,7 @@ function (OperationInterface $operation): void { foreach ($downloadUrlMethods as $downloadUrlMethod) { $this->io->write('Trying to download using: ' . $downloadUrlMethod->value, verbosity: IOInterface::VERY_VERBOSE); - if ($downloadUrlMethod === DownloadUrlMethod::PrePackagedBinary && $this->composerRequest->configureOptions !== []) { + if ($downloadUrlMethod === DownloadUrlMethod::PrePackagedBinary && $this->composerRequest->configureOptionsFor($composerPackage->getName()) !== []) { $configureOptionsConflictMessage = 'Cannot use pre-packaged-binary download method, as configure options were passed.'; $downloadMethodFailures[$downloadUrlMethod->value] = $configureOptionsConflictMessage; diff --git a/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php index c26e7384..43850e7b 100644 --- a/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php +++ b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php @@ -58,7 +58,7 @@ function (OperationInterface $operation) use ($pieOutput): bool { return false; } - $isRequestedPiePackage = $this->composerRequest->requestedPackage->package === $operation->getPackage()->getName(); + $isRequestedPiePackage = $this->composerRequest->isFor($operation->getPackage()->getName()); if (! $isRequestedPiePackage) { $pieOutput->writeError( diff --git a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php index 1adabc02..8729a807 100644 --- a/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php +++ b/src/ComposerIntegration/PhpBinaryPathBasedPlatformRepository.php @@ -17,6 +17,7 @@ use Symfony\Component\Process\Exception\ProcessFailedException; use UnexpectedValueException; +use function array_map; use function explode; use function in_array; use function str_replace; @@ -30,7 +31,8 @@ class PhpBinaryPathBasedPlatformRepository extends PlatformRepository { private VersionParser $versionParser; - public function __construct(PhpBinaryPath $phpBinaryPath, Composer $composer, InstalledPiePackages $installedPiePackages, ExtensionName|null $extensionBeingInstalled) + /** @param list $extensionsBeingInstalled */ + public function __construct(PhpBinaryPath $phpBinaryPath, Composer $composer, InstalledPiePackages $installedPiePackages, array $extensionsBeingInstalled) { $this->versionParser = new VersionParser(); $this->packages = []; @@ -58,6 +60,8 @@ public function __construct(PhpBinaryPath $phpBinaryPath, Composer $composer, In } } + $extensionNamesBeingInstalled = array_map(static fn (ExtensionName $ext) => $ext->name(), $extensionsBeingInstalled); + foreach ($extVersions as $extension => $extensionVersion) { /** * If the extension we're trying to exclude is not excluded from this list if it is already installed @@ -65,7 +69,7 @@ public function __construct(PhpBinaryPath $phpBinaryPath, Composer $composer, In * * @link https://github.com/php/pie/issues/150 */ - if ($extensionBeingInstalled !== null && $extension === $extensionBeingInstalled->name()) { + if (in_array($extension, $extensionNamesBeingInstalled, true)) { continue; } diff --git a/src/ComposerIntegration/PieComposerInstaller.php b/src/ComposerIntegration/PieComposerInstaller.php index 423768be..24c2fe58 100644 --- a/src/ComposerIntegration/PieComposerInstaller.php +++ b/src/ComposerIntegration/PieComposerInstaller.php @@ -16,22 +16,25 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class PieComposerInstaller extends Installer { - private PhpBinaryPath|null $phpBinaryPath = null; - private ExtensionName|null $extensionBeingInstalled = null; - private Composer|null $composer = null; + private PhpBinaryPath|null $phpBinaryPath = null; + /** @var list */ + private array $extensionsBeingInstalled = []; + private Composer|null $composer = null; protected function createPlatformRepo(bool $forUpdate): PlatformRepository { Assert::notNull($this->phpBinaryPath, '$phpBinaryPath was not set, maybe createWithPhpBinary was not used?'); - Assert::notNull($this->extensionBeingInstalled, '$extensionBeingInstalled was not set, maybe createWithPhpBinary was not used?'); + Assert::isNonEmptyList($this->extensionsBeingInstalled, '$extensionsBeingInstalled was not set, maybe createWithPhpBinary was not used?'); + Assert::allIsInstanceOf($this->extensionsBeingInstalled, ExtensionName::class, '$extensionsBeingInstalled were not all ExtensionName instances'); Assert::notNull($this->composer, '$composer was not set, maybe createWithPhpBinary was not used?'); - return new PhpBinaryPathBasedPlatformRepository($this->phpBinaryPath, $this->composer, new InstalledPiePackages(), $this->extensionBeingInstalled); + return new PhpBinaryPathBasedPlatformRepository($this->phpBinaryPath, $this->composer, new InstalledPiePackages(), $this->extensionsBeingInstalled); } + /** @param list $extensionsBeingInstalled */ public static function createWithPhpBinary( PhpBinaryPath $php, - ExtensionName $extensionBeingInstalled, + array $extensionsBeingInstalled, IOInterface $io, Composer $composer, ): self { @@ -47,9 +50,9 @@ public static function createWithPhpBinary( $composer->getAutoloadGenerator(), ); - $composerInstaller->phpBinaryPath = $php; - $composerInstaller->extensionBeingInstalled = $extensionBeingInstalled; - $composerInstaller->composer = $composer; + $composerInstaller->phpBinaryPath = $php; + $composerInstaller->extensionsBeingInstalled = $extensionsBeingInstalled; + $composerInstaller->composer = $composer; return $composerInstaller; } diff --git a/src/ComposerIntegration/PieComposerRequest.php b/src/ComposerIntegration/PieComposerRequest.php index 3b96902d..5c137853 100644 --- a/src/ComposerIntegration/PieComposerRequest.php +++ b/src/ComposerIntegration/PieComposerRequest.php @@ -8,6 +8,9 @@ use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\Platform\TargetPlatform; +use function array_map; +use function in_array; + /** * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks * @@ -15,15 +18,28 @@ */ final class PieComposerRequest { - /** @param list $configureOptions */ + /** @var list */ + private readonly array $requestedPackageNames; + + /** + * @param list $requestedPackages + * @param array> $configureOptions Keyed by package name + */ public function __construct( public readonly IOInterface $pieOutput, public readonly TargetPlatform $targetPlatform, - public readonly RequestedPackageAndVersion $requestedPackage, + public readonly array $requestedPackages, public readonly PieOperation $operation, public readonly array $configureOptions, public readonly bool $attemptToSetupIniFile, ) { + $this->requestedPackageNames = array_map(static fn (RequestedPackageAndVersion $request) => $request->package, $this->requestedPackages); + } + + /** @return list */ + public function configureOptionsFor(string $packageName): array + { + return $this->configureOptions[$packageName] ?? []; } /** @@ -37,10 +53,15 @@ public static function noOperation( return new PieComposerRequest( $pieOutput, $targetPlatform, - new RequestedPackageAndVersion('null/null', null), + [], PieOperation::Resolve, [], false, ); } + + public function isFor(string $packageName): bool + { + return in_array($packageName, $this->requestedPackageNames); + } } diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php index 3a84f71c..8ba54ce7 100644 --- a/src/ComposerIntegration/PieJsonEditor.php +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -68,6 +68,11 @@ public function ensureExists(): self return $this; } + public function currentContent(): string + { + return file_get_contents($this->pieJsonFilename); + } + /** * Add a package to the `require` section of the given `pie.json`. Returns * the original `pie.json` content, in case it needs to be restored later. @@ -77,7 +82,7 @@ public function ensureExists(): self */ public function addRequire(string $package, string $version): string { - $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + $originalPieJsonContent = $this->currentContent(); (new JsonConfigSource( new JsonFile( @@ -97,7 +102,7 @@ public function addRequire(string $package, string $version): string */ public function removeRequire(string $package): string { - $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + $originalPieJsonContent = $this->currentContent(); (new JsonConfigSource( new JsonFile( @@ -115,7 +120,7 @@ public function revert(string $originalPieJsonContent): void public function excludePackagistOrg(): string { - $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + $originalPieJsonContent = $this->currentContent(); (new JsonConfigSource( new JsonFile( @@ -138,7 +143,7 @@ public function addRepository( string $type, string $url, ): string { - $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + $originalPieJsonContent = $this->currentContent(); (new JsonConfigSource( new JsonFile( @@ -162,7 +167,7 @@ public function addRepository( public function removeRepository( string $name, ): string { - $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + $originalPieJsonContent = $this->currentContent(); (new JsonConfigSource( new JsonFile( diff --git a/src/ComposerIntegration/PiePackageInstaller.php b/src/ComposerIntegration/PiePackageInstaller.php index bec8b961..8fb583e8 100644 --- a/src/ComposerIntegration/PiePackageInstaller.php +++ b/src/ComposerIntegration/PiePackageInstaller.php @@ -11,8 +11,11 @@ use Composer\PartialComposer; use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; +use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\ExtensionType; +use function array_map; +use function implode; use function sprintf; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ @@ -39,12 +42,12 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa ?->then(function () use ($composerPackage) { $io = $this->composerRequest->pieOutput; - if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + if (! $this->composerRequest->isFor($composerPackage->getName())) { $io->write( sprintf( - 'Skipping %s install request from Composer as it was not the expected PIE package %s', + 'Skipping %s install request from Composer as it was not the expected PIE package(s) %s', $composerPackage->getName(), - $this->composerRequest->requestedPackage->package, + implode(', ', array_map(static fn (RequestedPackageAndVersion $req) => $req->package, $this->composerRequest->requestedPackages)), ), verbosity: IOInterface::VERY_VERBOSE, ); @@ -81,12 +84,12 @@ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $ ?->then(function () use ($composerPackage) { $io = $this->composerRequest->pieOutput; - if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + if (! $this->composerRequest->isFor($composerPackage->getName())) { $io->write( sprintf( - 'Skipping %s uninstall request from Composer as it was not the expected PIE package %s', + 'Skipping %s uninstall request from Composer as it was not the expected PIE package(s) %s', $composerPackage->getName(), - $this->composerRequest->requestedPackage->package, + implode(', ', array_map(static fn (RequestedPackageAndVersion $req) => $req->package, $this->composerRequest->requestedPackages)), ), verbosity: IOInterface::VERY_VERBOSE, ); diff --git a/src/ComposerIntegration/VersionSelectorFactory.php b/src/ComposerIntegration/VersionSelectorFactory.php index 3baa2068..7079f59f 100644 --- a/src/ComposerIntegration/VersionSelectorFactory.php +++ b/src/ComposerIntegration/VersionSelectorFactory.php @@ -35,7 +35,7 @@ public static function make( ): VersionSelector { return new VersionSelector( self::factoryRepositorySet($composer, $requestedPackageAndVersion), - new PhpBinaryPathBasedPlatformRepository($targetPlatform->phpBinaryPath, $composer, new InstalledPiePackages(), null), + new PhpBinaryPathBasedPlatformRepository($targetPlatform->phpBinaryPath, $composer, new InstalledPiePackages(), []), ); } } diff --git a/src/Container.php b/src/Container.php index 7dfb663a..0e79508d 100644 --- a/src/Container.php +++ b/src/Container.php @@ -49,6 +49,7 @@ use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\ConsoleOutput; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\EventDispatcher\EventDispatcher; @@ -239,6 +240,21 @@ public static function testFactory(): ContainerInterface return self::$testBuffer; }); + // QuieterConsoleIO is wired separately from IOInterface in self::factory(), and writes + // directly to a real ConsoleOutput; override it here only, so tests don't leak its output + // to the terminal. + $container->singleton(QuieterConsoleIO::class, static function (ContainerInterface $container): QuieterConsoleIO { + return new QuieterConsoleIO( + $container->get(InputInterface::class), + new NullOutput(), + new MinimalHelperSet( + [ + 'question' => new QuestionHelper(), + ], + ), + ); + }); + return $container; } diff --git a/src/DependencyResolver/DependencyInstaller/PrescanSystemDependencies.php b/src/DependencyResolver/DependencyInstaller/PrescanSystemDependencies.php index 4b3a303e..d3fff14e 100644 --- a/src/DependencyResolver/DependencyInstaller/PrescanSystemDependencies.php +++ b/src/DependencyResolver/DependencyInstaller/PrescanSystemDependencies.php @@ -58,7 +58,7 @@ public function __invoke( ); $unmetDependencies = array_filter( - ($this->fetchDependencyStatuses)($targetPlatform, $composer, $package->composerPackage()), + ($this->fetchDependencyStatuses)($targetPlatform, $composer, $package->piePackage->composerPackage()), static function (DependencyStatus $dependencyStatus): bool { return ! $dependencyStatus->satisfied(); }, diff --git a/src/DependencyResolver/DependencyResolver.php b/src/DependencyResolver/DependencyResolver.php index 8179089f..5a6e06d7 100644 --- a/src/DependencyResolver/DependencyResolver.php +++ b/src/DependencyResolver/DependencyResolver.php @@ -16,5 +16,5 @@ public function __invoke( TargetPlatform $targetPlatform, RequestedPackageAndVersion $requestedPackageAndVersion, bool $forceInstallPackageVersion, - ): Package; + ): ResolvedPackageRequest; } diff --git a/src/DependencyResolver/FetchDependencyStatuses.php b/src/DependencyResolver/FetchDependencyStatuses.php index 4269173f..8ba213ab 100644 --- a/src/DependencyResolver/FetchDependencyStatuses.php +++ b/src/DependencyResolver/FetchDependencyStatuses.php @@ -28,7 +28,7 @@ public function __invoke(TargetPlatform $targetPlatform, Composer $composer, Com /** @var array $platformConstraints */ $platformConstraints = []; - $composerPlatform = new PhpBinaryPathBasedPlatformRepository($targetPlatform->phpBinaryPath, $composer, new InstalledPiePackages(), null); + $composerPlatform = new PhpBinaryPathBasedPlatformRepository($targetPlatform->phpBinaryPath, $composer, new InstalledPiePackages(), []); foreach ($composerPlatform->getPackages() as $platformPackage) { $platformConstraints[$platformPackage->getName()] = new Constraint('==', $platformPackage->getVersion()); } diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index d377fe49..059ca117 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -33,7 +33,7 @@ public function __invoke( TargetPlatform $targetPlatform, RequestedPackageAndVersion $requestedPackageAndVersion, bool $forceInstallPackageVersion, - ): Package { + ): ResolvedPackageRequest { $versionSelector = VersionSelectorFactory::make($composer, $requestedPackageAndVersion, $targetPlatform); $package = $versionSelector->findBestCandidate( @@ -73,7 +73,7 @@ public function __invoke( $this->assertCompatibleThreadSafetyMode($targetPlatform->threadSafety, $piePackage); } - return $piePackage; + return new ResolvedPackageRequest($piePackage, $requestedPackageAndVersion); } private function assertCompatibleThreadSafetyMode(ThreadSafetyMode $threadSafetyMode, Package $resolvedPackage): void diff --git a/src/DependencyResolver/ResolvedPackageRequest.php b/src/DependencyResolver/ResolvedPackageRequest.php new file mode 100644 index 00000000..af5ae722 --- /dev/null +++ b/src/DependencyResolver/ResolvedPackageRequest.php @@ -0,0 +1,58 @@ + $resolvedPackageRequests + * + * @return list + */ + public static function extensionNames(array $resolvedPackageRequests): array + { + return array_map( + static fn (self $resolvedPackageRequest): ExtensionName => $resolvedPackageRequest->piePackage->extensionName(), + $resolvedPackageRequests, + ); + } + + /** + * @param list $resolvedPackageRequests + * + * @return list + */ + public static function requestedPackageNames(array $resolvedPackageRequests): array + { + return array_map( + static fn (self $resolvedPackageRequest): string => $resolvedPackageRequest->requestedPackageAndVersion->package, + $resolvedPackageRequests, + ); + } + + /** + * @param non-empty-list $resolvedPackageRequests + * + * @return non-empty-list + */ + public static function piePackages(array $resolvedPackageRequests): array + { + return array_map( + static fn (self $resolvedPackageRequest): Package => $resolvedPackageRequest->piePackage, + $resolvedPackageRequests, + ); + } +} diff --git a/src/Installing/InstallForPhpProject/InstallPiePackageFromPath.php b/src/Installing/InstallForPhpProject/InstallPiePackageFromPath.php index 3e8771fc..4aeb5291 100644 --- a/src/Installing/InstallForPhpProject/InstallPiePackageFromPath.php +++ b/src/Installing/InstallForPhpProject/InstallPiePackageFromPath.php @@ -39,7 +39,7 @@ public function __invoke( $invokeContext, [ 'command' => 'install', - 'requested-package-and-version' => $pieRootPackage->getName() . ':*@dev', + 'requested-package-and-version' => [$pieRootPackage->getName() . ':*@dev'], ], $input, ); diff --git a/src/Installing/InstallForPhpProject/InstallSelectedPackage.php b/src/Installing/InstallForPhpProject/InstallSelectedPackage.php index 97d64207..e50f8cca 100644 --- a/src/Installing/InstallForPhpProject/InstallSelectedPackage.php +++ b/src/Installing/InstallForPhpProject/InstallSelectedPackage.php @@ -6,11 +6,11 @@ use Php\Pie\Command\InvokeSubCommand; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; -use Php\Pie\ExtensionName; -use Php\Pie\Util\OutputFormatterWithPrefix; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use function array_map; + /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class InstallSelectedPackage { @@ -19,22 +19,26 @@ public function __construct( ) { } + /** @param list $selectedPackages */ public function withSubCommand( - ExtensionName $ext, - RequestedPackageAndVersion $selectedPackage, + array $selectedPackages, Command $command, InputInterface $input, ): int { $params = [ 'command' => 'install', - 'requested-package-and-version' => $selectedPackage->prettyNameAndVersion(), + 'requested-package-and-version' => [ + ...array_map( + static fn (RequestedPackageAndVersion $package) => $package->prettyNameAndVersion(), + $selectedPackages, + ), + ], ]; return ($this->invokeSubCommand)( $command, $params, $input, - OutputFormatterWithPrefix::newWithPrefix(' ' . $ext->name() . '> '), ); } } diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index fc4d51b7..5fb1b59b 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -14,6 +14,7 @@ use Symfony\Component\Process\Process; use Webmozart\Assert\Assert; +use function array_map; use function array_merge; use function Safe\copy; use function Safe\preg_match_all; @@ -30,10 +31,9 @@ class CliContext implements Context private string|null $errorOutput = null; private int|null $exitCode = null; /** @var list */ - private array $phpArguments = []; - private string $theExtension = 'example_pie_extension'; - /** @var non-empty-string */ - private string $thePackage = 'asgrim/example-pie-extension'; + private array $phpArguments = []; + /** @var list */ + private array $interactions = []; private string|null $workingDirectory = null; /** @throws PcreException */ @@ -58,12 +58,22 @@ public function removeInstalledExtensions(): void #[Given('an extension was previously downloaded but not built')] public function iRunACommandToDownloadTheLatestVersionOfAnExtension(): void { + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; $this->runPieCommand(['download', 'asgrim/example-pie-extension']); } + #[When('I run a command to download multiple extensions')] + public function iRunACommandToDownloadMultipleExtensions(): void + { + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; + $this->interactions[] = ['extension' => 'quickhash', 'package' => 'derickr/quickhash']; + $this->runPieCommand(['download', 'asgrim/example-pie-extension', 'derickr/quickhash']); + } + #[When('I run a command to download version :version of an extension')] public function iRunACommandToDownloadSpecificVersionOfAnExtension(string $version): void { + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; $this->runPieCommand(['download', 'asgrim/example-pie-extension:' . $version]); } @@ -111,18 +121,25 @@ private function assertCommandSuccessful(): void } #[Then('the latest version should have been downloaded')] + #[Then('the extensions should have been downloaded')] public function theLatestVersionShouldHaveBeenDownloaded(): void { $this->assertCommandSuccessful(); - Assert::regex($this->output, '#Found package: asgrim/example-pie-extension:v?\d+\.\d+\.\d+ which provides ext-example_pie_extension#'); - Assert::regex($this->output, '#Extracted asgrim/example-pie-extension:v?\d+\.\d+\.\d+ source to: #'); + + foreach ($this->interactions as $downloads) { + Assert::regex($this->output, '#Found package: ' . $downloads['package'] . ':v?\d+\.\d+\.\d+ which provides ext-' . $downloads['extension'] . '#'); + Assert::regex($this->output, '#Extracted ' . $downloads['package'] . ':v?\d+\.\d+\.\d+ source to: #'); + } } #[Then('version :version should have been downloaded')] public function versionOfTheExtensionShouldHaveBeen(string $version): void { $this->assertCommandSuccessful(); - Assert::contains($this->output, 'Found package: asgrim/example-pie-extension:' . $version); + + foreach ($this->interactions as $downloads) { + Assert::contains($this->output, 'Found package: ' . $downloads['package'] . ':' . $version); + } } #[When('I run a command to build an extension')] @@ -132,7 +149,14 @@ public function iRunACommandToBuildAnExtension(): void $this->runPieCommand(['build', 'asgrim/example-pie-extension']); } + #[When('I run a command to build multiple extensions')] + public function iRunACommandToBuildMultipleExtensions(): void + { + $this->runPieCommand(['build', 'asgrim/example-pie-extension', 'derickr/quickhash']); + } + #[Then('the extension should have been built')] + #[Then('the extensions should have been built')] public function theExtensionShouldHaveBeenBuilt(): void { $this->assertCommandSuccessful(); @@ -188,53 +212,70 @@ public function theExtensionShouldHaveBeenBuiltWithOptions(): void #[Given('an extension was previously installed and enabled')] public function iRunACommandToInstallAnExtension(): void { - $this->theExtension = 'example_pie_extension'; - $this->thePackage = 'asgrim/example-pie-extension'; - $this->runPieCommand(['install', $this->thePackage]); + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; + $this->runPieCommand(['install', 'asgrim/example-pie-extension']); + } + + #[When('I run a command to install multiple extensions')] + #[Given('multiple extensions were previously installed and enabled')] + public function iRunACommandToInstallMultipleExtensions(): void + { + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; + $this->interactions[] = ['extension' => 'quickhash', 'package' => 'derickr/quickhash']; + $this->runPieCommand(['install', 'asgrim/example-pie-extension', 'derickr/quickhash']); } #[When('I run a command to forcefully install an extension')] public function iRunACommandToForcefullyInstallAnExtension(): void { - $this->theExtension = 'example_pie_extension'; - $this->thePackage = 'asgrim/example-pie-extension'; - $this->runPieCommand(['install', '--force', $this->thePackage]); + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; + $this->runPieCommand(['install', '--force', 'asgrim/example-pie-extension']); } #[When('I run a command to install an extension without enabling it')] public function iRunACommandToInstallAnExtensionWithoutEnabling(): void { - $this->theExtension = 'example_pie_extension'; - $this->thePackage = 'asgrim/example-pie-extension'; - $this->runPieCommand(['install', $this->thePackage, '--skip-enable-extension']); + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; + $this->runPieCommand(['install', 'asgrim/example-pie-extension', '--skip-enable-extension']); } #[When('I run a command to uninstall an extension')] public function iRunACommandToUninstallAnExtension(): void { - $this->runPieCommand(['uninstall', $this->thePackage]); + $this->runPieCommand(['uninstall', ...array_map(static fn (array $interaction) => $interaction['package'], $this->interactions)]); + } + + #[When('I run a command to uninstall multiple extensions')] + public function iRunACommandToUninstallMultipleExtensions(): void + { + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; + $this->interactions[] = ['extension' => 'quickhash', 'package' => 'derickr/quickhash']; + $this->runPieCommand(['uninstall', 'asgrim/example-pie-extension', 'derickr/quickhash']); } #[Then('the extension should not be installed anymore')] + #[Then('the extensions should not be installed anymore')] public function theExtensionShouldNotBeInstalled(): void { $this->assertCommandSuccessful(); - if (Platform::isWindows()) { - Assert::regex($this->output, '#👋 Removed extension: [-\\\_:.a-zA-Z0-9]+\\\php_' . $this->theExtension . '.dll#'); - } else { - Assert::regex($this->output, '#👋 Removed extension: [-_.a-zA-Z0-9/]+/' . $this->theExtension . '.so#'); - } + foreach ($this->interactions as $uninstall) { + if (Platform::isWindows()) { + Assert::regex($this->output, '#👋 Removed extension: [-\\\_:.a-zA-Z0-9]+\\\php_' . $uninstall['extension'] . '.dll#'); + } else { + Assert::regex($this->output, '#👋 Removed extension: [-_.a-zA-Z0-9/]+/' . $uninstall['extension'] . '.so#'); + } - $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $this->theExtension . '")?"yes":"no";'])) - ->mustRun() - ->getOutput(); + $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $uninstall['extension'] . '")?"yes":"no";'])) + ->mustRun() + ->getOutput(); - Assert::same( - $isExtEnabled, - 'no', - sprintf("Failed to remove extension.\n\nOutput:\n%s\n\nError output:\n%s\n", $this->output, $this->errorOutput), - ); + Assert::same( + $isExtEnabled, + 'no', + sprintf("Failed to remove extension.\n\nOutput:\n%s\n\nError output:\n%s\n", $this->output, $this->errorOutput), + ); + } } #[Then('the extension should have been installed')] @@ -244,35 +285,40 @@ public function theExtensionShouldHaveBeenInstalled(): void Assert::contains($this->output, 'Extension has NOT been automatically enabled.'); - if (Platform::isWindows()) { - Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_' . $this->theExtension . '.dll#'); + foreach ($this->interactions as $install) { + if (Platform::isWindows()) { + Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_' . $install['extension'] . '.dll#'); - return; - } + continue; + } - Assert::regex($this->output, '#Install complete: [-_.a-zA-Z0-9/]+/' . $this->theExtension . '.so#'); + Assert::regex($this->output, '#Install complete: [-_.a-zA-Z0-9/]+/' . $install['extension'] . '.so#'); + } } #[Then('the extension should have been installed and enabled')] + #[Then('the extensions should have been installed and enabled')] public function theExtensionShouldHaveBeenInstalledAndEnabled(): void { $this->assertCommandSuccessful(); Assert::contains($this->output, 'Extension is enabled and loaded'); - if (Platform::isWindows()) { - Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_' . $this->theExtension . '.dll#'); + foreach ($this->interactions as $install) { + if (Platform::isWindows()) { + Assert::regex($this->output, '#Copied DLL to: [-\\\_:.a-zA-Z0-9]+\\\php_' . $install['extension'] . '.dll#'); - return; - } + continue; + } - Assert::regex($this->output, '#Install complete: [-_.a-zA-Z0-9/]+/' . $this->theExtension . '.so#'); + Assert::regex($this->output, '#Install complete: [-_.a-zA-Z0-9/]+/' . $install['extension'] . '.so#'); - $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $this->theExtension . '")?"yes":"no";'])) - ->mustRun() - ->getOutput(); + $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $install['extension'] . '")?"yes":"no";'])) + ->mustRun() + ->getOutput(); - Assert::same($isExtEnabled, 'yes'); + Assert::same($isExtEnabled, 'yes'); + } } #[Then('the extension should not have been re-installed')] @@ -280,13 +326,15 @@ public function theExtensionShouldNotHaveBeenReinstalled(): void { $this->assertCommandSuccessful(); - Assert::contains($this->output, 'PIE package asgrim/example-pie-extension (example_pie_extension) is already installed and verified.'); + foreach ($this->interactions as $noops) { + Assert::contains($this->output, 'PIE package ' . $noops['package'] . ' (' . $noops['extension'] . ') is already installed and verified.'); - $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $this->theExtension . '")?"yes":"no";'])) - ->mustRun() - ->getOutput(); + $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $noops['extension'] . '")?"yes":"no";'])) + ->mustRun() + ->getOutput(); - Assert::same($isExtEnabled, 'yes'); + Assert::same($isExtEnabled, 'yes'); + } } #[Given('I have an invalid extension installed')] @@ -345,9 +393,8 @@ public function iHaveLibsodiumOnMySystem(): void #[Given('I have the sodium extension installed with PIE')] public function iInstallTheSodiumExtensionWithPie(): void { - $this->theExtension = 'sodium'; - $this->thePackage = 'php/sodium'; - $this->runPieCommand(['install', $this->thePackage]); + $this->interactions[] = ['extension' => 'sodium', 'package' => 'php/sodium']; + $this->runPieCommand(['install', 'php/sodium']); } #[Given('I do not have libsodium on my system')] @@ -359,9 +406,8 @@ public function iDoNotHaveLibsodiumOnMySystem(): void #[When('I display information about the sodium extension with PIE')] public function iDisplayInformationAboutTheSodiumExtensionWithPie(): void { - $this->theExtension = 'sodium'; - $this->thePackage = 'php/sodium'; - $this->runPieCommand(['info', $this->thePackage]); + $this->interactions[] = ['extension' => 'sodium', 'package' => 'php/sodium']; + $this->runPieCommand(['info', 'php/sodium']); } #[Then('the information should show that libsodium is a missing dependency')] @@ -445,8 +491,8 @@ public function iAmInAPIEProject(): void #[When('I run a command to install the extensions without package selections')] public function iRunACommandToInstallTheExtension(): void { - $this->theExtension = 'example_pie_extension'; - $this->thePackage = 'asgrim/example-pie-extension'; + // Note: implied from composer.json, we don't explicitly request the packages here + $this->interactions[] = ['extension' => 'example_pie_extension', 'package' => 'asgrim/example-pie-extension']; $this->runPieCommand(['install']); } diff --git a/test/integration/Command/BuildCommandTest.php b/test/integration/Command/BuildCommandTest.php index 6db411a1..eaf71aa1 100644 --- a/test/integration/Command/BuildCommandTest.php +++ b/test/integration/Command/BuildCommandTest.php @@ -27,7 +27,7 @@ public function setUp(): void public function testBuildCommandWillBuildTheExtension(): void { - $this->commandTester->execute(['requested-package-and-version' => self::TEST_PACKAGE]); + $this->commandTester->execute(['requested-package-and-version' => [self::TEST_PACKAGE]]); $this->commandTester->assertCommandIsSuccessful(); diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index 7ce923c6..62e29360 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -68,7 +68,7 @@ public function testDownloadCommandWillDownloadCompatibleExtension( string $requestedVersion, string $expectedVersion, ): void { - $this->commandTester->execute(['requested-package-and-version' => $requestedVersion]); + $this->commandTester->execute(['requested-package-and-version' => [$requestedVersion]]); $this->commandTester->assertCommandIsSuccessful(); @@ -83,7 +83,7 @@ public function testDownloadCommandWillDownloadSpecificCommits(): void self::markTestSkipped('This test can only run on non-Windows systems'); } - $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:dev-main#9b5e6c80a1e05556e4e6824f0c112a4992cee001']); + $this->commandTester->execute(['requested-package-and-version' => ['asgrim/example-pie-extension:dev-main#9b5e6c80a1e05556e4e6824f0c112a4992cee001']]); $this->commandTester->assertCommandIsSuccessful(); @@ -110,7 +110,7 @@ public function testDownloadingWithPhpConfig(string $requestedVersion, string $e $this->commandTester->execute([ '--with-php-config' => $phpConfigPath, - 'requested-package-and-version' => $requestedVersion, + 'requested-package-and-version' => [$requestedVersion], ]); $this->commandTester->assertCommandIsSuccessful(); @@ -132,7 +132,7 @@ public function testDownloadingWithPhpPath(string $requestedVersion, string $exp $this->commandTester->execute([ '--with-php-path' => $phpBinaryPath, - 'requested-package-and-version' => $requestedVersion, + 'requested-package-and-version' => [$requestedVersion], ]); $this->commandTester->assertCommandIsSuccessful(); @@ -146,7 +146,7 @@ public function testDownloadingWithPhpPath(string $requestedVersion, string $exp public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void { // 1.0.0 is only compatible with PHP 8.3.0 - self::assertSame(1, $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( @@ -164,7 +164,7 @@ public function testDownloadCommandPassesWhenUsingIncompatiblePhpVersionWithForc $this->commandTester->execute( [ - 'requested-package-and-version' => $incompatiblePackage, + 'requested-package-and-version' => [$incompatiblePackage], '--force' => true, ], ); diff --git a/test/integration/Command/InfoCommandTest.php b/test/integration/Command/InfoCommandTest.php index 5871f1f3..8df309ba 100644 --- a/test/integration/Command/InfoCommandTest.php +++ b/test/integration/Command/InfoCommandTest.php @@ -22,7 +22,7 @@ public function setUp(): void public function testInfoCommandDisplaysInformation(): void { - $this->commandTester->execute(['requested-package-and-version' => 'asgrim/example-pie-extension:dev-main#9b5e6c80a1e05556e4e6824f0c112a4992cee001']); + $this->commandTester->execute(['requested-package-and-version' => ['asgrim/example-pie-extension:dev-main#9b5e6c80a1e05556e4e6824f0c112a4992cee001']]); $this->commandTester->assertCommandIsSuccessful(); diff --git a/test/integration/Command/InstallCommandTest.php b/test/integration/Command/InstallCommandTest.php index 631325bc..62921439 100644 --- a/test/integration/Command/InstallCommandTest.php +++ b/test/integration/Command/InstallCommandTest.php @@ -90,7 +90,7 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin $this->commandTester->execute( [ - 'requested-package-and-version' => self::TEST_PACKAGE, + 'requested-package-and-version' => [self::TEST_PACKAGE], '--with-php-config' => $phpConfigPath, '--skip-enable-extension' => true, ], @@ -117,7 +117,7 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin public function testInstallCommandWillInstallCompatibleExtensionWindows(): void { $this->commandTester->execute([ - 'requested-package-and-version' => self::TEST_PACKAGE, + 'requested-package-and-version' => [self::TEST_PACKAGE], '--skip-enable-extension' => true, ]); diff --git a/test/integration/Command/InstallExtensionsForProjectCommandTest.php b/test/integration/Command/InstallExtensionsForProjectCommandTest.php index b13e47e1..8376dc9b 100644 --- a/test/integration/Command/InstallExtensionsForProjectCommandTest.php +++ b/test/integration/Command/InstallExtensionsForProjectCommandTest.php @@ -18,7 +18,6 @@ use Php\Pie\ComposerIntegration\QuieterConsoleIO; use Php\Pie\Container; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; -use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Installing\InstallForPhpProject\CheckExtensionStatus; use Php\Pie\Installing\InstallForPhpProject\ComposerFactoryForProject; @@ -131,11 +130,14 @@ public function testInstallingExtensionsForPhpProject(): void $this->installSelectedPackage->expects(self::once()) ->method('withSubCommand') ->with( - ExtensionName::normaliseFromString('foobar'), - new RequestedPackageAndVersion( - 'vendor1/foobar', - '^1.2', - ), + [ + new RequestedPackageAndVersion( + 'vendor1/foobar', + '^1.2', + ), + ], + self::isInstanceOf(Command::class), + self::isInstanceOf(InputInterface::class), ) ->willReturn(0); diff --git a/test/integration/Command/ShowCommandTest.php b/test/integration/Command/ShowCommandTest.php index c097b402..c1d2370e 100644 --- a/test/integration/Command/ShowCommandTest.php +++ b/test/integration/Command/ShowCommandTest.php @@ -65,7 +65,7 @@ public function testExecuteWithAvailableConstrainedUpdates(): void $installCommand = new CommandTester(Container::testFactory()->get(InstallCommand::class)); $installCommand->execute([ - 'requested-package-and-version' => self::TEST_PACKAGE . ':2.0.2', + 'requested-package-and-version' => [self::TEST_PACKAGE . ':2.0.2'], '--with-php-config' => $phpConfig, ]); $installCommand->assertCommandIsSuccessful(); @@ -113,7 +113,7 @@ public function testExecuteWithAvailableUnconstrainedUpdates(): void $installCommand = new CommandTester(Container::testFactory()->get(InstallCommand::class)); $installCommand->execute([ - 'requested-package-and-version' => self::TEST_PACKAGE . ':2.0.2', + 'requested-package-and-version' => [self::TEST_PACKAGE . ':2.0.2'], '--with-php-config' => $phpConfig, ]); $installCommand->assertCommandIsSuccessful(); @@ -161,7 +161,7 @@ public function testExecuteWithOnlyUnconstrainedUpdates(): void $installCommand = new CommandTester(Container::testFactory()->get(InstallCommand::class)); $installCommand->execute([ - 'requested-package-and-version' => self::TEST_PACKAGE . ':2.0.2', + 'requested-package-and-version' => [self::TEST_PACKAGE . ':2.0.2'], '--with-php-config' => $phpConfig, ]); $installCommand->assertCommandIsSuccessful(); diff --git a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php index 750e7109..0cd2aed7 100644 --- a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -83,7 +83,7 @@ public function testDependenciesAreResolvedToExpectedVersions( new PieComposerRequest( $this->createMock(IOInterface::class), $targetPlatform, - $requestedPackageAndVersion, + [$requestedPackageAndVersion], PieOperation::Resolve, [], false, @@ -94,8 +94,8 @@ public function testDependenciesAreResolvedToExpectedVersions( false, ); - self::assertSame($expectedVersion, $package->version()); - self::assertNotNull($package->downloadUrl()); - self::assertStringMatchesFormat($expectedDownloadUrl, $package->downloadUrl()); + self::assertSame($expectedVersion, $package->piePackage->version()); + self::assertNotNull($package->piePackage->downloadUrl()); + self::assertStringMatchesFormat($expectedDownloadUrl, $package->piePackage->downloadUrl()); } } diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index 04c1f1ee..084661d8 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -16,8 +16,14 @@ use Composer\Util\Platform; use InvalidArgumentException; use Php\Pie\Command\CommandHelper; +use Php\Pie\Command\ConfigureOptionCollision; +use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal; +use Php\Pie\DependencyResolver\DependencyResolver; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\DependencyResolver\ResolvedPackageRequest; +use Php\Pie\DependencyResolver\UnableToResolveRequirement; +use Php\Pie\Platform\TargetPlatform; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; @@ -66,11 +72,38 @@ public function testRequestedNameAndVersionPair(string $requestedPackageAndVersi $input->expects(self::once()) ->method('getArgument') ->with('requested-package-and-version') - ->willReturn($requestedPackageAndVersion); + ->willReturn([$requestedPackageAndVersion]); self::assertEquals( - new RequestedPackageAndVersion($expectedPackage, $expectedVersion), - CommandHelper::requestedNameAndVersionPair($input), + [new RequestedPackageAndVersion($expectedPackage, $expectedVersion)], + CommandHelper::requestedNameAndVersionPairs($input), + ); + } + + public function testRequestedNameAndVersionPairSupportsMultiple(): void + { + $input = $this->createMock(InputInterface::class); + + $input->expects(self::once()) + ->method('getArgument') + ->with('requested-package-and-version') + ->willReturn([ + 'a/ext', + 'b/ext:^1.2', + 'c/ext:*', + 'd/ext:@alpha', + 'e/ext:1.2.3', + ]); + + self::assertEquals( + [ + new RequestedPackageAndVersion('a/ext', null), + new RequestedPackageAndVersion('b/ext', '^1.2'), + new RequestedPackageAndVersion('c/ext', '*'), + new RequestedPackageAndVersion('d/ext', '@alpha'), + new RequestedPackageAndVersion('e/ext', '1.2.3'), + ], + CommandHelper::requestedNameAndVersionPairs($input), ); } @@ -85,7 +118,7 @@ public function testInvalidRequestedNameAndVersionPairThrowsExceptionWhenNoPacka $this->expectException(InvalidArgumentException::class); $this->expectExceptionMessage('No package was requested for installation'); - CommandHelper::requestedNameAndVersionPair($input); + CommandHelper::requestedNameAndVersionPairs($input); } public function testBindingConfigurationOptionsFromPackage(): void @@ -93,6 +126,106 @@ public function testBindingConfigurationOptionsFromPackage(): void self::markTestIncomplete(__METHOD__); } + private static function packageNamed(string $prettyName, string $prettyVersion): Package + { + $composerPackage = self::createStub(CompletePackageInterface::class); + $composerPackage->method('getPrettyName')->willReturn($prettyName); + $composerPackage->method('getPrettyVersion')->willReturn($prettyVersion); + $composerPackage->method('getType')->willReturn('php-ext'); + + return Package::fromComposerCompletePackage($composerPackage); + } + + public function testResolveRequestedPackagesResolvesEachRequestedPackageAndWritesOutput(): void + { + $requestedA = new RequestedPackageAndVersion('foo/bar', null); + $requestedB = new RequestedPackageAndVersion('baz/qux', '^1.0'); + + $resolvedA = new ResolvedPackageRequest(self::packageNamed('foo/bar', '1.0.0'), $requestedA); + $resolvedB = new ResolvedPackageRequest(self::packageNamed('baz/qux', '2.0.0'), $requestedB); + + $composer = $this->createMock(Composer::class); + $targetPlatform = $this->createMock(TargetPlatform::class); + + $dependencyResolver = $this->createMock(DependencyResolver::class); + $dependencyResolver->expects(self::exactly(2)) + ->method('__invoke') + ->willReturnCallback( + static function (Composer $givenComposer, TargetPlatform $givenTargetPlatform, RequestedPackageAndVersion $requested, bool $force) use ($composer, $targetPlatform, $requestedA, $requestedB, $resolvedA, $resolvedB): ResolvedPackageRequest { + self::assertSame($composer, $givenComposer); + self::assertSame($targetPlatform, $givenTargetPlatform); + self::assertTrue($force); + + if ($requested === $requestedA) { + return $resolvedA; + } + + self::assertSame($requestedB, $requested); + + return $resolvedB; + }, + ); + + $io = new BufferIO(); + + $resolvedPackages = CommandHelper::resolveRequestedPackages( + $dependencyResolver, + $io, + $composer, + $targetPlatform, + [$requestedA, $requestedB], + true, + ); + + self::assertSame([$resolvedA, $resolvedB], $resolvedPackages); + self::assertSame( + "Found package: foo/bar:1.0.0 which provides ext-bar\nFound package: baz/qux:2.0.0 which provides ext-qux", + str_replace("\r\n", "\n", trim($io->getOutput())), + ); + } + + public function testResolveRequestedPackagesPropagatesUnableToResolveRequirement(): void + { + $requested = new RequestedPackageAndVersion('foo/bar', null); + + $exception = new UnableToResolveRequirement('Could not resolve foo/bar', $requested); + + $dependencyResolver = $this->createMock(DependencyResolver::class); + $dependencyResolver->method('__invoke')->willThrowException($exception); + + $this->expectExceptionObject($exception); + + CommandHelper::resolveRequestedPackages( + $dependencyResolver, + new NullIO(), + $this->createMock(Composer::class), + $this->createMock(TargetPlatform::class), + [$requested], + false, + ); + } + + public function testResolveRequestedPackagesPropagatesBundledPhpExtensionRefusal(): void + { + $requested = new RequestedPackageAndVersion('foo/bar', null); + + $exception = BundledPhpExtensionRefusal::forPackage(self::packageNamed('foo/bar', '1.0.0')); + + $dependencyResolver = $this->createMock(DependencyResolver::class); + $dependencyResolver->method('__invoke')->willThrowException($exception); + + $this->expectExceptionObject($exception); + + CommandHelper::resolveRequestedPackages( + $dependencyResolver, + new NullIO(), + $this->createMock(Composer::class), + $this->createMock(TargetPlatform::class), + [$requested], + false, + ); + } + public function testProcessingConfigureOptionsFromInput(): void { $composerPackage = $this->createMock(CompletePackageInterface::class); @@ -116,17 +249,52 @@ public function testProcessingConfigureOptionsFromInput(): void $input = new ArrayInput(['--with-stuff' => 'lolz', '--enable-thing' => true], $inputDefinition); - $options = CommandHelper::processConfigureOptionsFromInput($package, $input); + $options = CommandHelper::processConfigureOptionsFromInput([$package], $input); self::assertSame( [ - '--with-stuff=lolz', - '--enable-thing', + 'foo/bar' => [ + '--with-stuff=lolz', + '--enable-thing', + ], ], $options, ); } + public function testBindConfigureOptionsFromPackageThrowsWhenTwoPackagesDeclareSameOptionName(): void + { + $composerPackageA = $this->createMock(CompletePackageInterface::class); + $composerPackageA->method('getPrettyName')->willReturn('foo/bar'); + $composerPackageA->method('getPrettyVersion')->willReturn('1.0.0'); + $composerPackageA->method('getType')->willReturn('php-ext'); + $composerPackageA->method('getPhpExt')->willReturn([ + 'configure-options' => [ + ['name' => 'with-stuff', 'needs-value' => true], + ], + ]); + $packageA = Package::fromComposerCompletePackage($composerPackageA); + + $composerPackageB = $this->createMock(CompletePackageInterface::class); + $composerPackageB->method('getPrettyName')->willReturn('baz/qux'); + $composerPackageB->method('getPrettyVersion')->willReturn('2.0.0'); + $composerPackageB->method('getType')->willReturn('php-ext'); + $composerPackageB->method('getPhpExt')->willReturn([ + 'configure-options' => [ + ['name' => 'with-stuff'], + ], + ]); + $packageB = Package::fromComposerCompletePackage($composerPackageB); + + $command = new Command(); + $input = new ArrayInput([]); + + $this->expectException(ConfigureOptionCollision::class); + $this->expectExceptionMessage('Both foo/bar and baz/qux declare a configure option named --with-stuff'); + + CommandHelper::bindConfigureOptionsFromPackage($command, [$packageA, $packageB], $input); + } + #[RequiresOperatingSystemFamily('Windows')] public function testWindowsMachinesCannotUseWithPhpConfigOption(): void { diff --git a/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php b/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php index 111f2324..26485177 100644 --- a/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php +++ b/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php @@ -62,9 +62,9 @@ public function testMetadataForDownloads(): void null, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.0'), + [new RequestedPackageAndVersion('foo/bar', '^1.0')], PieOperation::Build, - ['--foo', '--bar="yes"'], + ['foo/bar' => ['--foo', '--bar="yes"']], false, ), clone $package, @@ -101,9 +101,9 @@ public function testMetadataForBuilds(): void null, new PhpizePath('/path/to/phpize'), ), - new RequestedPackageAndVersion('foo/bar', '^1.0'), + [new RequestedPackageAndVersion('foo/bar', '^1.0')], PieOperation::Build, - ['--foo', '--bar="yes"'], + ['foo/bar' => ['--foo', '--bar="yes"']], false, ), clone $package, diff --git a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php index be713a05..fc4d8378 100644 --- a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php +++ b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php @@ -66,9 +66,9 @@ public function testDownloadWithoutBuildAndInstall(): void null, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.0'), + [new RequestedPackageAndVersion('foo/bar', '^1.0')], PieOperation::Download, - ['--foo', '--bar="yes"'], + ['foo/bar' => ['--foo', '--bar="yes"']], false, ); $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); @@ -107,9 +107,9 @@ public function testDownloadAndBuildWithoutInstall(): void null, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.0'), + [new RequestedPackageAndVersion('foo/bar', '^1.0')], PieOperation::Build, - ['--foo', '--bar="yes"'], + ['foo/bar' => ['--foo', '--bar="yes"']], false, ); $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); @@ -151,9 +151,9 @@ public function testDownloadBuildAndInstall(): void null, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.0'), + [new RequestedPackageAndVersion('foo/bar', '^1.0')], PieOperation::Install, - ['--foo', '--bar="yes"'], + ['foo/bar' => ['--foo', '--bar="yes"']], false, ); $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index f0241c27..e2e64fa8 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -80,7 +80,7 @@ public function testEventListenerRegistration(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -122,7 +122,7 @@ public function testNonInstallOperationsAreIgnored(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -164,7 +164,7 @@ public function testNonCompletePackagesAreIgnored(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -206,7 +206,7 @@ public function testInstallOperationsForDifferentPackagesAreIgnored(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -248,7 +248,7 @@ public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -303,7 +303,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -363,7 +363,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -424,7 +424,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsFound(): WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -485,7 +485,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzBinaryWhenBinaryIsNotFound( WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -539,9 +539,9 @@ public function testPrePackagedBinaryMethodIsIgnoredWhenConfigureOptionsArePasse WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, - ['--with-foo'], + ['foo/bar' => ['--with-foo']], false, ), ); @@ -597,7 +597,7 @@ public function testNoSelectedDownloadUrlMethodWillThrowException(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, diff --git a/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php index e6c1d733..b7fe028b 100644 --- a/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php +++ b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php @@ -73,7 +73,7 @@ public function testEventListenerRegistration(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('foo/bar', '^1.1'), + [new RequestedPackageAndVersion('foo/bar', '^1.1')], PieOperation::Install, [], false, @@ -109,7 +109,7 @@ public function testUnrelatedInstallOperationsAreRemoved(): void WindowsCompiler::VC15, null, ), - new RequestedPackageAndVersion('bat/baz', '^3.2'), + [new RequestedPackageAndVersion('bat/baz', '^3.2')], PieOperation::Install, [], false, diff --git a/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php b/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php index e827f201..ca1b5d4b 100644 --- a/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php +++ b/test/unit/ComposerIntegration/PhpBinaryPathBasedPlatformRepositoryTest.php @@ -51,7 +51,7 @@ public function testPlatformRepositoryContainsExpectedPacakges(): void 'another' => '1.2.3-alpha.34', ]); - $platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, null); + $platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, []); self::assertSame( [ @@ -91,7 +91,7 @@ public function testPlatformRepositoryExcludesExtensionBeingInstalled(): void 'extension_being_installed' => '1.2.3', ]); - $platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, $extensionBeingInstalled); + $platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, [$extensionBeingInstalled]); self::assertSame( [ @@ -134,7 +134,7 @@ public function testPlatformRepositoryExcludesReplacedExtensions(): void 'replaced_extension' => '3.0.0', ]); - $platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, $extensionBeingInstalled); + $platformRepository = new PhpBinaryPathBasedPlatformRepository($phpBinaryPath, $composer, $installedPiePackages, [$extensionBeingInstalled]); self::assertSame( [ @@ -228,7 +228,7 @@ public function testLibrariesAreIncluded(string $packageName): void PhpBinaryPath::fromCurrentProcess(), $this->createMock(Composer::class), $installedPiePackages, - ExtensionName::normaliseFromString('extension_being_installed'), + [ExtensionName::normaliseFromString('extension_being_installed')], ))->getPackages(), ), )); diff --git a/test/unit/DependencyResolver/DependencyInstaller/PrescanSystemDependenciesTest.php b/test/unit/DependencyResolver/DependencyInstaller/PrescanSystemDependenciesTest.php index 8f292c0e..2fb1c747 100644 --- a/test/unit/DependencyResolver/DependencyInstaller/PrescanSystemDependenciesTest.php +++ b/test/unit/DependencyResolver/DependencyInstaller/PrescanSystemDependenciesTest.php @@ -16,6 +16,7 @@ use Php\Pie\DependencyResolver\FetchDependencyStatuses; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\DependencyResolver\ResolvedPackageRequest; use Php\Pie\Platform\PackageManager; use Php\Pie\Platform\TargetPlatform; use PHPUnit\Framework\Attributes\CoversClass; @@ -77,7 +78,7 @@ public function testAllDependenciesSatisfied(): void $this->dependencyResolver->expects(self::once()) ->method('__invoke') ->with($this->composer, $this->targetPlatform, $request, true) - ->willReturn($piePackage); + ->willReturn(new ResolvedPackageRequest($piePackage, $request)); $versionParser = new VersionParser(); @@ -113,7 +114,7 @@ public function testMissingDependencyThatDoesNotHaveAnyPackageManagerDefinition( $this->dependencyResolver->expects(self::once()) ->method('__invoke') ->with($this->composer, $this->targetPlatform, $request, true) - ->willReturn($piePackage); + ->willReturn(new ResolvedPackageRequest($piePackage, $request)); $versionParser = new VersionParser(); @@ -153,7 +154,7 @@ public function testMissingDependencyThatDoesNotHaveMyPackageManagerDefinition() $this->dependencyResolver->expects(self::once()) ->method('__invoke') ->with($this->composer, $this->targetPlatform, $request, true) - ->willReturn($piePackage); + ->willReturn(new ResolvedPackageRequest($piePackage, $request)); $versionParser = new VersionParser(); @@ -193,7 +194,7 @@ public function testMissingDependenciesFailToInstall(): void $this->dependencyResolver->expects(self::once()) ->method('__invoke') ->with($this->composer, $this->targetPlatform, $request, true) - ->willReturn($piePackage); + ->willReturn(new ResolvedPackageRequest($piePackage, $request)); $versionParser = new VersionParser(); @@ -233,7 +234,7 @@ public function testMissingDependenciesAreSuccessfullyInstalled(): void $this->dependencyResolver->expects(self::once()) ->method('__invoke') ->with($this->composer, $this->targetPlatform, $request, true) - ->willReturn($piePackage); + ->willReturn(new ResolvedPackageRequest($piePackage, $request)); $versionParser = new VersionParser(); @@ -274,7 +275,7 @@ public function testMissingDependenciesAreNotInstalledWhenShouldNotAutoInstallAn $this->dependencyResolver->expects(self::once()) ->method('__invoke') ->with($this->composer, $this->targetPlatform, $request, true) - ->willReturn($piePackage); + ->willReturn(new ResolvedPackageRequest($piePackage, $request)); $versionParser = new VersionParser(); diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 114f66cc..5522eee5 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -97,8 +97,8 @@ public function testPackageThatCanBeResolved(): void $this->createMock(QuieterConsoleIO::class), ))($this->composer, $targetPlatform, new RequestedPackageAndVersion('asgrim/example-pie-extension', '^1.0'), false); - self::assertSame('asgrim/example-pie-extension', $package->name()); - self::assertStringStartsWith('1.', $package->version()); + self::assertSame('asgrim/example-pie-extension', $package->piePackage->name()); + self::assertStringStartsWith('1.', $package->piePackage->version()); } /** @return array, 1: non-empty-string, 2: non-empty-string}> */ @@ -190,8 +190,8 @@ public function testUnresolvedPackageCanBeInstalledWithForceOption(array $platfo true, ); - self::assertSame('asgrim/example-pie-extension', $package->name()); - self::assertStringStartsWith('1.', $package->version()); + self::assertSame('asgrim/example-pie-extension', $package->piePackage->name()); + self::assertStringStartsWith('1.', $package->piePackage->version()); } public function testZtsOnlyPackageCannotBeInstalledOnNtsSystem(): void @@ -423,8 +423,8 @@ public function testPackageThatCanBeResolvedWithReplaceConflict(): void $this->createMock(QuieterConsoleIO::class), ))($this->composer, $targetPlatform, new RequestedPackageAndVersion('asgrim/example-pie-extension', '^1.0'), false); - self::assertSame('asgrim/example-pie-extension', $package->name()); - self::assertStringStartsWith('1.', $package->version()); + self::assertSame('asgrim/example-pie-extension', $package->piePackage->name()); + self::assertStringStartsWith('1.', $package->piePackage->version()); } public function testBundledExtensionCannotBeInstalledOnDevPhpVersion(): void @@ -539,6 +539,6 @@ public function testBundledExtensionWillInstallOnBuildProviderWithForce(string $ $package = $resolver->__invoke($this->composer, $targetPlatform, $requestedPackage, true); - self::assertSame('php/bundled', $package->name()); + self::assertSame('php/bundled', $package->piePackage->name()); } } diff --git a/test/unit/DependencyResolver/ResolvedPackageRequestTest.php b/test/unit/DependencyResolver/ResolvedPackageRequestTest.php new file mode 100644 index 00000000..8b7494d7 --- /dev/null +++ b/test/unit/DependencyResolver/ResolvedPackageRequestTest.php @@ -0,0 +1,70 @@ +method('getPrettyName')->willReturn($prettyName); + $composerPackage->method('getPrettyVersion')->willReturn($prettyVersion); + $composerPackage->method('getType')->willReturn('php-ext'); + + return Package::fromComposerCompletePackage($composerPackage); + } + + public function testExtensionNames(): void + { + $resolvedPackageRequests = [ + new ResolvedPackageRequest(self::packageNamed('foo/bar', '1.0.0'), new RequestedPackageAndVersion('foo/bar', null)), + new ResolvedPackageRequest(self::packageNamed('baz/qux', '2.0.0'), new RequestedPackageAndVersion('baz/qux', '^2.0')), + ]; + + self::assertSame( + ['bar', 'qux'], + array_map(static fn ($extensionName) => $extensionName->name(), ResolvedPackageRequest::extensionNames($resolvedPackageRequests)), + ); + } + + public function testRequestedPackageNames(): void + { + $resolvedPackageRequests = [ + new ResolvedPackageRequest(self::packageNamed('foo/bar', '1.0.0'), new RequestedPackageAndVersion('foo/bar', null)), + new ResolvedPackageRequest(self::packageNamed('baz/qux', '2.0.0'), new RequestedPackageAndVersion('baz/qux', '^2.0')), + ]; + + self::assertSame( + ['foo/bar', 'baz/qux'], + ResolvedPackageRequest::requestedPackageNames($resolvedPackageRequests), + ); + } + + public function testPiePackages(): void + { + $packageA = self::packageNamed('foo/bar', '1.0.0'); + $packageB = self::packageNamed('baz/qux', '2.0.0'); + + $resolvedPackageRequests = [ + new ResolvedPackageRequest($packageA, new RequestedPackageAndVersion('foo/bar', null)), + new ResolvedPackageRequest($packageB, new RequestedPackageAndVersion('baz/qux', '^2.0')), + ]; + + self::assertSame( + [$packageA, $packageB], + ResolvedPackageRequest::piePackages($resolvedPackageRequests), + ); + } +} diff --git a/test/unit/Installing/InstallForPhpProject/InstallPiePackageFromPathTest.php b/test/unit/Installing/InstallForPhpProject/InstallPiePackageFromPathTest.php index 20df724f..59233566 100644 --- a/test/unit/Installing/InstallForPhpProject/InstallPiePackageFromPathTest.php +++ b/test/unit/Installing/InstallForPhpProject/InstallPiePackageFromPathTest.php @@ -59,7 +59,7 @@ public function testInvokeWithSuccessfulSubCommand(): void $this->command, [ 'command' => 'install', - 'requested-package-and-version' => 'foo/bar:*@dev', + 'requested-package-and-version' => ['foo/bar:*@dev'], ], $this->input, ) @@ -101,7 +101,7 @@ public function testInvokeWithSubCommandException(): void $this->command, [ 'command' => 'install', - 'requested-package-and-version' => 'foo/bar:*@dev', + 'requested-package-and-version' => ['foo/bar:*@dev'], ], $this->input, ) diff --git a/test/unit/Installing/InstallForPhpProject/InstallSelectedPackageTest.php b/test/unit/Installing/InstallForPhpProject/InstallSelectedPackageTest.php index 1d74a7f1..910271a7 100644 --- a/test/unit/Installing/InstallForPhpProject/InstallSelectedPackageTest.php +++ b/test/unit/Installing/InstallForPhpProject/InstallSelectedPackageTest.php @@ -6,9 +6,7 @@ use Php\Pie\Command\InvokeSubCommand; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; -use Php\Pie\ExtensionName; use Php\Pie\Installing\InstallForPhpProject\InstallSelectedPackage; -use Php\Pie\Util\OutputFormatterWithPrefix; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Command\Command; @@ -28,20 +26,20 @@ public function testSubCommandIsInvoked(): void $command, [ 'command' => 'install', - 'requested-package-and-version' => 'foo/foo:^1.0', + 'requested-package-and-version' => ['foo/foo:^1.0'], ], $input, - self::isInstanceOf(OutputFormatterWithPrefix::class), ) ->willReturn(0); $installer = new InstallSelectedPackage($invoker); $installer->withSubCommand( - ExtensionName::normaliseFromString('foo'), - new RequestedPackageAndVersion( - 'foo/foo', - '^1.0', - ), + [ + new RequestedPackageAndVersion( + 'foo/foo', + '^1.0', + ), + ], $command, $input, );