diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 0860038e..09983648 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -41,9 +41,9 @@ public function configure(): void public function execute(InputInterface $input, OutputInterface $output): int { - $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); - - $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); $composer = PieComposerFactory::createPieComposer( $this->container, @@ -58,7 +58,12 @@ public function execute(InputInterface $input, OutputInterface $output): int ), ); - $package = ($this->dependencyResolver)($composer, $targetPlatform, $requestedNameAndVersion); + $package = ($this->dependencyResolver)( + $composer, + $targetPlatform, + $requestedNameAndVersion, + $forceInstallPackageVersion, + ); $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); // Now we know what package we have, we can validate the configure options for the command and re-create the @@ -80,7 +85,13 @@ public function execute(InputInterface $input, OutputInterface $output): int ); try { - ($this->composerIntegrationHandler)($package, $composer, $targetPlatform, $requestedNameAndVersion); + ($this->composerIntegrationHandler)( + $package, + $composer, + $targetPlatform, + $requestedNameAndVersion, + $forceInstallPackageVersion, + ); } catch (ComposerRunFailed $composerRunFailed) { $output->writeln('' . $composerRunFailed->getMessage() . ''); diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 8e25165b..6fc1368f 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -39,6 +39,7 @@ final class CommandHelper private const OPTION_WITH_PHPIZE_PATH = 'with-phpize-path'; private const OPTION_MAKE_PARALLEL_JOBS = 'make-parallel-jobs'; private const OPTION_SKIP_ENABLE_EXTENSION = 'skip-enable-extension'; + private const OPTION_FORCE = 'force'; /** @psalm-suppress UnusedConstructor */ private function __construct() @@ -86,6 +87,12 @@ public static function configureDownloadBuildInstallOptions(Command $command): v InputOption::VALUE_NONE, 'Specify this to skip attempting to enable the extension in php.ini', ); + $command->addOption( + self::OPTION_FORCE, + null, + InputOption::VALUE_NONE, + 'To attempt to install a version that doesn\'t match the version constraints from the meta-data, for instance to install an older version than recommended, or when the signature is not available.', + ); self::configurePhpConfigOptions($command); @@ -166,6 +173,11 @@ public static function determineAttemptToSetupIniFile(InputInterface $input): bo return ! $input->hasOption(self::OPTION_SKIP_ENABLE_EXTENSION) || ! $input->getOption(self::OPTION_SKIP_ENABLE_EXTENSION); } + public static function determineForceInstallingPackageVersion(InputInterface $input): bool + { + return $input->hasOption(self::OPTION_FORCE) && $input->getOption(self::OPTION_FORCE); + } + public static function determinePhpizePathFromInputs(InputInterface $input): PhpizePath|null { if ($input->hasOption(self::OPTION_WITH_PHPIZE_PATH)) { diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index f6616290..4a7439b1 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -43,9 +43,9 @@ public function execute(InputInterface $input, OutputInterface $output): int { CommandHelper::validateInput($input, $this); - $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); - - $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); $composer = PieComposerFactory::createPieComposer( $this->container, @@ -60,11 +60,22 @@ public function execute(InputInterface $input, OutputInterface $output): int ), ); - $package = ($this->dependencyResolver)($composer, $targetPlatform, $requestedNameAndVersion); + $package = ($this->dependencyResolver)( + $composer, + $targetPlatform, + $requestedNameAndVersion, + $forceInstallPackageVersion, + ); $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); try { - ($this->composerIntegrationHandler)($package, $composer, $targetPlatform, $requestedNameAndVersion); + ($this->composerIntegrationHandler)( + $package, + $composer, + $targetPlatform, + $requestedNameAndVersion, + $forceInstallPackageVersion, + ); } catch (ComposerRunFailed $composerRunFailed) { $output->writeln('' . $composerRunFailed->getMessage() . ''); diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php index 9f1f806f..0d03e934 100644 --- a/src/Command/InfoCommand.php +++ b/src/Command/InfoCommand.php @@ -58,7 +58,12 @@ public function execute(InputInterface $input, OutputInterface $output): int ), ); - $package = ($this->dependencyResolver)($composer, $targetPlatform, $requestedNameAndVersion); + $package = ($this->dependencyResolver)( + $composer, + $targetPlatform, + $requestedNameAndVersion, + CommandHelper::determineForceInstallingPackageVersion($input), + ); $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); $output->writeln(sprintf('Extension name: %s', $package->extensionName->name())); diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index aca34af2..0640b5e4 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -46,9 +46,9 @@ public function execute(InputInterface $input, OutputInterface $output): int $output->writeln('This command may need elevated privileges, and may prompt you for your password.'); } - $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); - - $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + $requestedNameAndVersion = CommandHelper::requestedNameAndVersionPair($input); + $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); $composer = PieComposerFactory::createPieComposer( $this->container, @@ -63,7 +63,12 @@ public function execute(InputInterface $input, OutputInterface $output): int ), ); - $package = ($this->dependencyResolver)($composer, $targetPlatform, $requestedNameAndVersion); + $package = ($this->dependencyResolver)( + $composer, + $targetPlatform, + $requestedNameAndVersion, + $forceInstallPackageVersion, + ); $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); // Now we know what package we have, we can validate the configure options for the command and re-create the @@ -85,7 +90,13 @@ public function execute(InputInterface $input, OutputInterface $output): int ); try { - ($this->composerIntegrationHandler)($package, $composer, $targetPlatform, $requestedNameAndVersion); + ($this->composerIntegrationHandler)( + $package, + $composer, + $targetPlatform, + $requestedNameAndVersion, + $forceInstallPackageVersion, + ); } catch (ComposerRunFailed $composerRunFailed) { $output->writeln('' . $composerRunFailed->getMessage() . ''); diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index d5e33ca4..05d9c8b1 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -5,6 +5,7 @@ namespace Php\Pie\ComposerIntegration; use Composer\Composer; +use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; use Composer\Json\JsonManipulator; use Php\Pie\DependencyResolver\Package; @@ -27,8 +28,13 @@ public function __construct( ) { } - public function __invoke(Package $package, Composer $composer, TargetPlatform $targetPlatform, RequestedPackageAndVersion $requestedPackageAndVersion): void - { + public function __invoke( + Package $package, + Composer $composer, + TargetPlatform $targetPlatform, + RequestedPackageAndVersion $requestedPackageAndVersion, + bool $forceInstallPackageVersion, + ): void { $versionSelector = VersionSelectorFactory::make($composer, $requestedPackageAndVersion, $targetPlatform); $recommendedRequireVersion = $requestedPackageAndVersion->version; @@ -64,6 +70,7 @@ public function __invoke(Package $package, Composer $composer, TargetPlatform $t ->setInstall(true) ->setIgnoredTypes([]) ->setDryRun(false) + ->setPlatformRequirementFilter(PlatformRequirementFilterFactory::fromBoolOrList($forceInstallPackageVersion)) ->setDownloadOnly(false); if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) { diff --git a/src/DependencyResolver/DependencyResolver.php b/src/DependencyResolver/DependencyResolver.php index 98309fe0..8179089f 100644 --- a/src/DependencyResolver/DependencyResolver.php +++ b/src/DependencyResolver/DependencyResolver.php @@ -11,5 +11,10 @@ interface DependencyResolver { /** @throws UnableToResolveRequirement */ - public function __invoke(Composer $composer, TargetPlatform $targetPlatform, RequestedPackageAndVersion $requestedPackageAndVersion): Package; + public function __invoke( + Composer $composer, + TargetPlatform $targetPlatform, + RequestedPackageAndVersion $requestedPackageAndVersion, + bool $forceInstallPackageVersion, + ): Package; } diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index 8bbdc020..d2bdbd28 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -5,6 +5,7 @@ namespace Php\Pie\DependencyResolver; use Composer\Composer; +use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Package\CompletePackageInterface; use Php\Pie\ComposerIntegration\QuieterConsoleIO; use Php\Pie\ComposerIntegration\VersionSelectorFactory; @@ -23,13 +24,18 @@ public function __construct( ) { } - public function __invoke(Composer $composer, TargetPlatform $targetPlatform, RequestedPackageAndVersion $requestedPackageAndVersion): Package - { + public function __invoke( + Composer $composer, + TargetPlatform $targetPlatform, + RequestedPackageAndVersion $requestedPackageAndVersion, + bool $forceInstallPackageVersion, + ): Package { $versionSelector = VersionSelectorFactory::make($composer, $requestedPackageAndVersion, $targetPlatform); $package = $versionSelector->findBestCandidate( $requestedPackageAndVersion->package, $requestedPackageAndVersion->version, + platformRequirementFilter: PlatformRequirementFilterFactory::fromBoolOrList($forceInstallPackageVersion), io: $this->arrayCollectionIo, ); diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index 2baf4103..c15bc88e 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -151,4 +151,26 @@ public function testDownloadCommandFailsWhenUsingIncompatiblePhpVersion(): void // 1.0.0 is only compatible with PHP 8.3.0 $this->commandTester->execute(['requested-package-and-version' => self::TEST_PACKAGE . ':1.0.0']); } + + #[RequiresOperatingSystemFamily('Linux')] + #[RequiresPhp('<8.2')] + public function testDownloadCommandPassesWhenUsingIncompatiblePhpVersionWithForceOption(): void + { + // 1.0.1 is only compatible with PHP 8.3.0 + $incompatiblePackage = self::TEST_PACKAGE . ':1.0.1'; + + $this->commandTester->execute( + [ + 'requested-package-and-version' => $incompatiblePackage, + '--force' => true, + ], + ); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + + self::assertStringContainsString('Found package: ' . $incompatiblePackage . ' which provides', $outputString); + self::assertStringContainsString('Extracted ' . $incompatiblePackage . ' source to', $outputString); + } } diff --git a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php index 1477b39c..43d5799e 100644 --- a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -95,6 +95,7 @@ public function testDependenciesAreResolvedToExpectedVersions( ), $targetPlatform, $requestedPackageAndVersion, + false, ); self::assertSame($expectedVersion, $package->version); diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 1fff415a..6cdd9a4b 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -64,7 +64,7 @@ public function testPackageThatCanBeResolved(): void $package = (new ResolveDependencyWithComposer( $this->createMock(QuieterConsoleIO::class), - ))($this->composer, $targetPlatform, new RequestedPackageAndVersion('asgrim/example-pie-extension', '^1.0')); + ))($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); @@ -118,9 +118,51 @@ public function testPackageThatCannotBeResolvedThrowsException(array $platformOv $package, $version, ), + false, ); } + /** + * @param array $platformOverrides + * @param non-empty-string $package + * @param non-empty-string $version + */ + #[DataProvider('unresolvableDependencies')] + public function testUnresolvedPackageCanBeInstalledWithForceOption(array $platformOverrides, string $package, string $version): void + { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath->expects(self::once()) + ->method('version') + ->willReturn($platformOverrides['php']); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $this->expectException(UnableToResolveRequirement::class); + + $package = (new ResolveDependencyWithComposer( + $this->createMock(QuieterConsoleIO::class), + ))( + $this->composer, + $targetPlatform, + new RequestedPackageAndVersion( + $package, + $version, + ), + true, + ); + + self::assertSame('asgrim/example-pie-extension', $package->name); + self::assertStringStartsWith('1.', $package->version); + } + public function testZtsOnlyPackageCannotBeInstalledOnNtsSystem(): void { $pkg = new CompletePackage('test-vendor/test-package', '1.0.0.0', '1.0.0'); @@ -164,6 +206,7 @@ public function testZtsOnlyPackageCannotBeInstalledOnNtsSystem(): void 'test-vendor/test-package', '1.0.0', ), + false, ); } @@ -210,6 +253,7 @@ public function testNtsOnlyPackageCannotBeInstalledOnZtsSystem(): void 'test-vendor/test-package', '1.0.0', ), + false, ); } @@ -256,6 +300,7 @@ public function testExtensionCanOnlyBeInstalledIfOsFamilyIsCompatible(): void 'test-vendor/test-package', '1.0.0', ), + false, ); } @@ -302,6 +347,7 @@ public function testExtensionCanOnlyBeInstalledIfOsFamilyIsNotInCompatible(): vo 'test-vendor/test-package', '1.0.0', ), + false, ); } }