diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index 8203b7af..cb750f11 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -4,20 +4,45 @@ namespace Php\Pie\Command; +use Composer\Package\BasePackage; +use Composer\Package\CompletePackageInterface; +use Php\Pie\BinaryFile; +use Php\Pie\ComposerIntegration\PieComposerFactory; +use Php\Pie\ComposerIntegration\PieComposerRequest; +use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; +use Php\Pie\DependencyResolver\Package; +use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\TargetPlatform; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; +use function array_combine; +use function array_filter; +use function array_key_exists; +use function array_map; use function array_walk; +use function file_exists; use function sprintf; +use function substr; +use const DIRECTORY_SEPARATOR; + +/** @psalm-import-type PieMetadata from PieInstalledJsonMetadataKeys */ #[AsCommand( name: 'show', description: 'List the installed modules and their versions.', )] final class ShowCommand extends Command { + public function __construct( + private readonly ContainerInterface $container, + ) { + parent::__construct(); + } + public function configure(): void { parent::configure(); @@ -29,15 +54,109 @@ public function execute(InputInterface $input, OutputInterface $output): int { $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); - $extensions = $targetPlatform->phpBinaryPath->extensions(); + $piePackages = $this->buildListOfPieInstalledPackages($output, $targetPlatform); + $phpEnabledExtensions = $targetPlatform->phpBinaryPath->extensions(); + $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); + $extensionEnding = $targetPlatform->operatingSystem === OperatingSystem::Windows ? '.dll' : '.so'; + $output->writeln("\n" . 'Loaded extensions:'); array_walk( - $extensions, - static function (string $version, string $name) use ($output): void { - $output->writeln(sprintf('%s:%s', $name, $version)); + $phpEnabledExtensions, + static function (string $version, string $phpExtensionName) use ($output, $piePackages, $extensionPath, $extensionEnding): void { + if (! array_key_exists($phpExtensionName, $piePackages)) { + $output->writeln(sprintf(' %s:%s', $phpExtensionName, $version)); + + return; + } + + $piePackage = $piePackages[$phpExtensionName]; + + $output->writeln(sprintf( + ' %s:%s (from 🥧 %s%s)', + $phpExtensionName, + $version, + $piePackage->prettyNameAndVersion(), + self::verifyChecksumInformation( + $extensionPath, + $phpExtensionName, + $extensionEnding, + PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($piePackage->composerPackage), + ), + )); }, ); return Command::SUCCESS; } + + /** + * @param PieMetadata $installedJsonMetadata + * @psalm-param '.dll'|'.so' $extensionEnding + */ + private static function verifyChecksumInformation( + string $extensionPath, + string $phpExtensionName, + string $extensionEnding, + array $installedJsonMetadata, + ): string { + $expectedConventionalBinaryPath = $extensionPath . DIRECTORY_SEPARATOR . $phpExtensionName . $extensionEnding; + + // The extension may not be in the usual path (since you can specify a full path to an extension in the INI file) + if (! file_exists($expectedConventionalBinaryPath)) { + return ''; + } + + $pieExpectedBinaryPath = array_key_exists(PieInstalledJsonMetadataKeys::InstalledBinary->value, $installedJsonMetadata) ? $installedJsonMetadata[PieInstalledJsonMetadataKeys::InstalledBinary->value] : null; + $pieExpectedChecksum = array_key_exists(PieInstalledJsonMetadataKeys::BinaryChecksum->value, $installedJsonMetadata) ? $installedJsonMetadata[PieInstalledJsonMetadataKeys::BinaryChecksum->value] : null; + + // Some other kind of mismatch of file path, or we don't have a stored checksum available + if ($expectedConventionalBinaryPath !== $pieExpectedBinaryPath || $pieExpectedChecksum === null) { + return ''; + } + + $actualInstalledBinary = BinaryFile::fromFileWithSha256Checksum($expectedConventionalBinaryPath); + if ($actualInstalledBinary->checksum !== $pieExpectedChecksum) { + return ' ⚠️ was ' . substr($actualInstalledBinary->checksum, 0, 8) . '..., expected ' . substr($pieExpectedChecksum, 0, 8) . '...'; + } + + return ' ✅'; + } + + /** @return array */ + private function buildListOfPieInstalledPackages( + OutputInterface $output, + TargetPlatform $targetPlatform, + ): array { + $composerInstalledPackages = array_map( + static function (CompletePackageInterface $package): Package { + return Package::fromComposerCompletePackage($package); + }, + array_filter( + PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + $output, + $targetPlatform, + ), + ) + ->getRepositoryManager() + ->getLocalRepository() + ->getPackages(), + static function (BasePackage $basePackage): bool { + return $basePackage instanceof CompletePackageInterface; + }, + ), + ); + + return array_combine( + array_map( + /** @return non-empty-string */ + static function (Package $package): string { + return $package->extensionName->name(); + }, + $composerInstalledPackages, + ), + $composerInstalledPackages, + ); + } } diff --git a/src/ComposerIntegration/PieComposerRequest.php b/src/ComposerIntegration/PieComposerRequest.php index 2679bdd9..405f78b9 100644 --- a/src/ComposerIntegration/PieComposerRequest.php +++ b/src/ComposerIntegration/PieComposerRequest.php @@ -27,4 +27,23 @@ public function __construct( public readonly bool $attemptToSetupIniFile, ) { } + + /** + * Useful for when we don't want to perform any "write" style operations; + * for example just reading metadata about the installed system. + */ + public static function noOperation( + OutputInterface $pieOutput, + TargetPlatform $targetPlatform, + ): self { + return new PieComposerRequest( + $pieOutput, + $targetPlatform, + new RequestedPackageAndVersion('null', null), + PieOperation::Resolve, + [], + null, + false, + ); + } } diff --git a/src/ComposerIntegration/PieInstalledJsonMetadataKeys.php b/src/ComposerIntegration/PieInstalledJsonMetadataKeys.php index 0d7e6067..048bc948 100644 --- a/src/ComposerIntegration/PieInstalledJsonMetadataKeys.php +++ b/src/ComposerIntegration/PieInstalledJsonMetadataKeys.php @@ -4,7 +4,29 @@ namespace Php\Pie\ComposerIntegration; -/** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ +use Composer\Package\CompletePackageInterface; + +use function array_column; +use function array_key_exists; +use function is_string; + +/** + * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks + * + * @psalm-type PieMetadata = array{ + * pie-target-platform-php-path?: non-empty-string, + * pie-target-platform-php-config-path?: non-empty-string, + * pie-target-platform-php-version?: non-empty-string, + * pie-target-platform-php-thread-safety?: non-empty-string, + * pie-target-platform-php-windows-compiler?: non-empty-string, + * pie-target-platform-architecture?: non-empty-string, + * pie-configure-options?: non-empty-string, + * pie-built-binary?: non-empty-string, + * pie-installed-binary-checksum?: non-empty-string, + * pie-installed-binary?: non-empty-string, + * pie-phpize-binary?: non-empty-string, + * } + */ enum PieInstalledJsonMetadataKeys: string { case TargetPlatformPhpPath = 'pie-target-platform-php-path'; @@ -18,4 +40,26 @@ enum PieInstalledJsonMetadataKeys: string case BinaryChecksum = 'pie-installed-binary-checksum'; case InstalledBinary = 'pie-installed-binary'; case PhpizeBinary = 'pie-phpize-binary'; + + /** @return PieMetadata */ + public static function pieMetadataFromComposerPackage(CompletePackageInterface $composerPackage): array + { + $composerPackageExtras = $composerPackage->getExtra(); + + $onlyPieExtras = []; + + foreach (array_column(self::cases(), 'value') as $pieMetadataKey) { + if ( + ! array_key_exists($pieMetadataKey, $composerPackageExtras) + || ! is_string($composerPackageExtras[$pieMetadataKey]) + || $composerPackageExtras[$pieMetadataKey] === '' + ) { + continue; + } + + $onlyPieExtras[$pieMetadataKey] = $composerPackageExtras[$pieMetadataKey]; + } + + return $onlyPieExtras; + } } diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index f421b65a..f75392a6 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -225,11 +225,11 @@ public function operatingSystem(): OperatingSystem public function operatingSystemFamily(): OperatingSystemFamily { - $output = Process::run([ + $output = self::cleanWarningAndDeprecationsFromOutput(Process::run([ $this->phpBinaryPath, '-r', 'echo PHP_OS_FAMILY;', - ]); + ])); $osFamily = OperatingSystemFamily::tryFrom(strtolower(trim($output))); Assert::notNull($osFamily, 'Could not determine operating system family'); diff --git a/test/unit/ComposerIntegration/PieInstalledJsonMetadataKeysTest.php b/test/unit/ComposerIntegration/PieInstalledJsonMetadataKeysTest.php new file mode 100644 index 00000000..e689d09e --- /dev/null +++ b/test/unit/ComposerIntegration/PieInstalledJsonMetadataKeysTest.php @@ -0,0 +1,44 @@ +createMock(CompletePackageInterface::class); + $composerPackage->expects(self::once()) + ->method('getExtra') + ->willReturn([]); + + self::assertSame([], PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($composerPackage)); + } + + public function testPieMetadataFromComposerPackageWithPopulatedExtra(): void + { + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage->expects(self::once()) + ->method('getExtra') + ->willReturn([ + PieInstalledJsonMetadataKeys::InstalledBinary->value => '/path/to/some/file', + PieInstalledJsonMetadataKeys::BinaryChecksum->value => 'some-checksum-value', + 'something else' => 'hopefully this does not make it in', + ]); + + self::assertEqualsCanonicalizing( + [ + PieInstalledJsonMetadataKeys::InstalledBinary->value => '/path/to/some/file', + PieInstalledJsonMetadataKeys::BinaryChecksum->value => 'some-checksum-value', + ], + PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($composerPackage), + ); + } +}