diff --git a/bin/pie b/bin/pie index b85fce44..3656caae 100755 --- a/bin/pie +++ b/bin/pie @@ -6,6 +6,7 @@ declare(strict_types=1); namespace Php\Pie; use Php\Pie\Command\BuildCommand; +use Php\Pie\Command\CheckBuildToolsCommand; use Php\Pie\Command\DownloadCommand; use Php\Pie\Command\InfoCommand; use Php\Pie\Command\InstallCommand; @@ -59,6 +60,7 @@ $application->setCommandLoader(new ContainerCommandLoader( 'self-update' => SelfUpdateCommand::class, 'self-verify' => SelfVerifyCommand::class, 'install-extensions-for-project' => InstallExtensionsForProjectCommand::class, + 'check-build-tools' => CheckBuildToolsCommand::class, ], )); diff --git a/src/Command/CheckBuildToolsCommand.php b/src/Command/CheckBuildToolsCommand.php new file mode 100644 index 00000000..e590569b --- /dev/null +++ b/src/Command/CheckBuildToolsCommand.php @@ -0,0 +1,109 @@ +io); + + if ($targetPlatform->operatingSystem === OperatingSystem::Windows) { + $this->io->writeError('Build tools are not required on Windows systems!'); + + return 0; + } + + $statuses = $this->checkAllBuildTools->statuses($targetPlatform, $this->packageManager); + + $this->io->write("\n" . 'Build tools typically required to build extensions:'); + + $anyMissing = false; + foreach ($statuses as $status) { + if ($status->found) { + $this->io->write(sprintf(' %s %s', Emoji::GREEN_CHECKMARK, $status->toolNames)); + continue; + } + + $anyMissing = true; + $this->io->write(sprintf( + ' %s %s%s', + Emoji::CROSS, + $status->toolNames, + $status->packageName !== null ? sprintf(' (installs with package: %s)', $status->packageName) : '', + )); + } + + if (! $anyMissing) { + $this->io->write(sprintf("\n" . '%s All build tools are installed.', Emoji::GREEN_CHECKMARK)); + + return Command::SUCCESS; + } + + $this->io->write(''); + + $this->checkAllBuildTools->check( + $this->io, + $this->packageManager, + $targetPlatform, + CommandHelper::autoInstallBuildTools($input), + ); + + // Check again things got installed as expected + if (! $this->allToolsFound($targetPlatform)) { + $this->io->writeError(sprintf("\n" . '%s Some build tools are still missing.', Emoji::CROSS)); + + return Command::FAILURE; + } + + $this->io->write(sprintf("\n" . '%s All build tools are now installed.', Emoji::GREEN_CHECKMARK)); + + return Command::SUCCESS; + } + + private function allToolsFound(TargetPlatform $targetPlatform): bool + { + foreach ($this->checkAllBuildTools->statuses($targetPlatform, $this->packageManager) as $status) { + if (! $status->found) { + return false; + } + } + + return true; + } +} diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 6bf0f0f5..4b99842f 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -109,6 +109,16 @@ public static function configurePhpConfigOptions(Command $command): void ); } + public static function configureBuildToolsCheckOptions(Command $command): void + { + $command->addOption( + self::OPTION_AUTO_INSTALL_BUILD_TOOLS, + null, + InputOption::VALUE_NONE, + 'If build tools are missing, automatically install them, instead of prompting.', + ); + } + public static function configureDownloadBuildInstallOptions(Command $command, bool $withRequestedPackageAndVersion = true): void { if ($withRequestedPackageAndVersion) { @@ -161,12 +171,7 @@ public static function configureDownloadBuildInstallOptions(Command $command, bo 'Select a PIE package for a given extension name, e.g. `--select=foo=myvendor/foo` to resolve the `ext-foo` extension to `myvendor/foo` PIE package.', ); - $command->addOption( - self::OPTION_AUTO_INSTALL_BUILD_TOOLS, - null, - InputOption::VALUE_NONE, - 'If build tools are missing, automatically install them, instead of prompting.', - ); + self::configureBuildToolsCheckOptions($command); $command->addOption( self::OPTION_SUPPRESS_BUILD_TOOLS_CHECK, null, diff --git a/src/Container.php b/src/Container.php index bf547bf3..63bd7602 100644 --- a/src/Container.php +++ b/src/Container.php @@ -15,6 +15,7 @@ use Php\Pie\Building\WindowsBuild; use Php\Pie\Command\ArgvInput; use Php\Pie\Command\BuildCommand; +use Php\Pie\Command\CheckBuildToolsCommand; use Php\Pie\Command\DownloadCommand; use Php\Pie\Command\InfoCommand; use Php\Pie\Command\InstallCommand; @@ -125,6 +126,7 @@ static function (ConsoleCommandEvent $event) use (&$displayedBanner, $io): void $container->singleton(SelfUpdateCommand::class); $container->singleton(SelfVerifyCommand::class); $container->singleton(InstallExtensionsForProjectCommand::class); + $container->singleton(CheckBuildToolsCommand::class); $container->singleton(IOInterface::class, static function (ContainerInterface $container): IOInterface { return new ConsoleIO( diff --git a/src/SelfManage/BuildTools/BuildToolStatus.php b/src/SelfManage/BuildTools/BuildToolStatus.php new file mode 100644 index 00000000..cfa13706 --- /dev/null +++ b/src/SelfManage/BuildTools/BuildToolStatus.php @@ -0,0 +1,16 @@ + */ + public function statuses(TargetPlatform $targetPlatform, PackageManager|null $packageManager): array + { + return array_map( + static function (BinaryBuildToolFinder $buildTool) use ($targetPlatform, $packageManager): BuildToolStatus { + $found = $buildTool->check($targetPlatform); + + return new BuildToolStatus( + $buildTool->toolNames(), + $found, + $found || $packageManager === null ? null : $buildTool->packageNameFor($packageManager, $targetPlatform), + ); + }, + $this->buildTools, + ); + } + public function check(IOInterface $io, PackageManager|null $packageManager, TargetPlatform $targetPlatform, bool $autoInstallIfMissing): void { $io->write('Checking if all build tools are installed.', verbosity: IOInterface::VERBOSE); /** @var list $packagesToInstall */ $packagesToInstall = []; $missingTools = []; - $allFound = true; - foreach ($this->buildTools as $buildTool) { - if ($buildTool->check($targetPlatform) !== false) { - $io->write('Build tool ' . $buildTool->toolNames() . ' is installed.', verbosity: IOInterface::VERY_VERBOSE); + foreach ($this->statuses($targetPlatform, $packageManager) as $status) { + if ($status->found) { + $io->write('Build tool ' . $status->toolNames . ' is installed.', verbosity: IOInterface::VERY_VERBOSE); continue; } - $allFound = false; - $missingTools[] = $buildTool->toolNames(); + $missingTools[] = $status->toolNames; if ($packageManager === null) { continue; } - $packageName = $buildTool->packageNameFor($packageManager, $targetPlatform); - - if ($packageName === null) { - $io->writeError('Could not find package name for build tool ' . $buildTool->toolNames() . '.', verbosity: IOInterface::VERBOSE); + if ($status->packageName === null) { + $io->writeError('Could not find package name for build tool ' . $status->toolNames . '.', verbosity: IOInterface::VERBOSE); continue; } - $packagesToInstall[] = $packageName; + $packagesToInstall[] = $status->packageName; } - if ($allFound) { + if (! count($missingTools)) { $io->write('All build tools found.', verbosity: IOInterface::VERBOSE); return; diff --git a/test/integration/Command/CheckBuildToolsCommandTest.php b/test/integration/Command/CheckBuildToolsCommandTest.php new file mode 100644 index 00000000..f44710e2 --- /dev/null +++ b/test/integration/Command/CheckBuildToolsCommandTest.php @@ -0,0 +1,53 @@ +commandTester = new CommandTester(Container::testFactory()->get(CheckBuildToolsCommand::class)); + } + + public function testCheckBuildToolsCommandListsRequiredToolsAndSucceeds(): void + { + if (Platform::isWindows()) { + self::markTestSkipped('This test can only run on non-Windows systems'); + } + + $this->commandTester->execute([]); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + self::assertStringContainsString('Build tools typically required to build extensions:', $outputString); + self::assertStringContainsString('gcc', $outputString); + self::assertStringContainsString('make', $outputString); + self::assertStringContainsString('phpize', $outputString); + self::assertStringContainsString('All build tools are installed.', $outputString); + } + + #[RequiresOperatingSystemFamily('Windows')] + public function testCheckBuildToolsCommandSkipsCheckOnWindows(): void + { + $this->commandTester->execute([]); + + $this->commandTester->assertCommandIsSuccessful(); + + $outputString = $this->commandTester->getDisplay(); + self::assertStringContainsString('Build tools are not required on Windows systems!', $outputString); + } +} diff --git a/test/unit/SelfManage/BuildTools/CheckAllBuildToolsTest.php b/test/unit/SelfManage/BuildTools/CheckAllBuildToolsTest.php index ecd756d3..ef3e2539 100644 --- a/test/unit/SelfManage/BuildTools/CheckAllBuildToolsTest.php +++ b/test/unit/SelfManage/BuildTools/CheckAllBuildToolsTest.php @@ -21,6 +21,62 @@ #[CoversClass(CheckAllBuildTools::class)] final class CheckAllBuildToolsTest extends TestCase { + private static function targetPlatform(): TargetPlatform + { + return new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + null, + ); + } + + public function testStatusesReportsFoundToolWithNoPackageName(): void + { + $checkAllBuildTools = new CheckAllBuildTools([ + new BinaryBuildToolFinder('echo', [PackageManager::Test->value => 'coreutils']), + ]); + + $statuses = $checkAllBuildTools->statuses(self::targetPlatform(), PackageManager::Test); + + self::assertCount(1, $statuses); + self::assertSame('echo', $statuses[0]->toolNames); + self::assertTrue($statuses[0]->found); + self::assertNull($statuses[0]->packageName); + } + + public function testStatusesReportsMissingToolWithPackageNameWhenPackageManagerKnown(): void + { + $checkAllBuildTools = new CheckAllBuildTools([ + new BinaryBuildToolFinder('bloop', [PackageManager::Test->value => 'coreutils']), + ]); + + $statuses = $checkAllBuildTools->statuses(self::targetPlatform(), PackageManager::Test); + + self::assertCount(1, $statuses); + self::assertSame('bloop', $statuses[0]->toolNames); + self::assertFalse($statuses[0]->found); + self::assertSame('coreutils', $statuses[0]->packageName); + } + + public function testStatusesReportsMissingToolWithoutPackageNameWhenNoPackageManagerKnown(): void + { + $checkAllBuildTools = new CheckAllBuildTools([ + new BinaryBuildToolFinder('bloop', [PackageManager::Test->value => 'coreutils']), + ]); + + $statuses = $checkAllBuildTools->statuses(self::targetPlatform(), null); + + self::assertCount(1, $statuses); + self::assertSame('bloop', $statuses[0]->toolNames); + self::assertFalse($statuses[0]->found); + self::assertNull($statuses[0]->packageName); + } + public function testCheckDoesNothingWhenAllBuildToolsAreFound(): void { $io = new BufferIO(verbosity: OutputInterface::VERBOSITY_VERY_VERBOSE); @@ -32,16 +88,7 @@ public function testCheckDoesNothingWhenAllBuildToolsAreFound(): void $checkAllBuildTools->check( $io, PackageManager::Test, - new TargetPlatform( - OperatingSystem::NonWindows, - OperatingSystemFamily::Linux, - PhpBinaryPath::fromCurrentProcess(), - Architecture::x86_64, - ThreadSafetyMode::NonThreadSafe, - 1, - null, - null, - ), + self::targetPlatform(), false, ); @@ -63,16 +110,7 @@ public function testCheckInstallsMissingToolWhenPromptedInInteractiveMode(): voi $checkAllBuildTools->check( $io, PackageManager::Test, - new TargetPlatform( - OperatingSystem::NonWindows, - OperatingSystemFamily::Linux, - PhpBinaryPath::fromCurrentProcess(), - Architecture::x86_64, - ThreadSafetyMode::NonThreadSafe, - 1, - null, - null, - ), + self::targetPlatform(), false, ); @@ -95,16 +133,7 @@ public function testCheckDoesNotInstallToolsWhenInNonInteractiveModeAndFlagNotPr $checkAllBuildTools->check( $io, PackageManager::Test, - new TargetPlatform( - OperatingSystem::NonWindows, - OperatingSystemFamily::Linux, - PhpBinaryPath::fromCurrentProcess(), - Architecture::x86_64, - ThreadSafetyMode::NonThreadSafe, - 1, - null, - null, - ), + self::targetPlatform(), false, ); @@ -126,16 +155,7 @@ public function testCheckInstallsMissingToolInNonInteractiveModeAndFlagIsProvide $checkAllBuildTools->check( $io, PackageManager::Test, - new TargetPlatform( - OperatingSystem::NonWindows, - OperatingSystemFamily::Linux, - PhpBinaryPath::fromCurrentProcess(), - Architecture::x86_64, - ThreadSafetyMode::NonThreadSafe, - 1, - null, - null, - ), + self::targetPlatform(), true, );