From 3b88de7fe2085d9ab16637cdddd7b60d0cb4108a Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 27 Dec 2024 11:10:02 +0000 Subject: [PATCH 1/4] Determine if PIE has knowledge of exts listed in pie show --- src/Command/ShowCommand.php | 79 ++++++++++++++++++- .../PieComposerRequest.php | 19 +++++ 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index 8203b7af..34d72c08 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -4,11 +4,22 @@ namespace Php\Pie\Command; +use Composer\Package\BasePackage; +use Composer\Package\CompletePackageInterface; +use Php\Pie\ComposerIntegration\PieComposerFactory; +use Php\Pie\ComposerIntegration\PieComposerRequest; +use Php\Pie\DependencyResolver\Package; +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 sprintf; @@ -18,6 +29,12 @@ )] final class ShowCommand extends Command { + public function __construct( + private readonly ContainerInterface $container, + ) { + parent::__construct(); + } + public function configure(): void { parent::configure(); @@ -29,15 +46,69 @@ 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(); + $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): void { + if (! array_key_exists($phpExtensionName, $piePackages)) { + $output->writeln(sprintf(' %s:%s', $phpExtensionName, $version)); + + return; + } + + // @todo determine if installed ext has drifted using the PIE checksum + + $piePackage = $piePackages[$phpExtensionName]; + $output->writeln(sprintf( + ' %s:%s (from %s)', + $phpExtensionName, + $version, + $piePackage->prettyNameAndVersion(), + )); }, ); return Command::SUCCESS; } + + /** @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, + ); + } } From 0f53238c4cdd95e0331e23c892c62e42f41774fd Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 30 Dec 2024 11:59:10 +0000 Subject: [PATCH 2/4] Added binary checksum integrity check to pie show --- src/Command/ShowCommand.php | 56 +++++++++++++++++-- .../PieInstalledJsonMetadataKeys.php | 46 ++++++++++++++- .../PieInstalledJsonMetadataKeysTest.php | 44 +++++++++++++++ 3 files changed, 141 insertions(+), 5 deletions(-) create mode 100644 test/unit/ComposerIntegration/PieInstalledJsonMetadataKeysTest.php diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index 34d72c08..a6235b0b 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -6,9 +6,12 @@ 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; @@ -21,8 +24,13 @@ 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.', @@ -48,25 +56,32 @@ public function execute(InputInterface $input, OutputInterface $output): int $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( $phpEnabledExtensions, - static function (string $version, string $phpExtensionName) use ($output, $piePackages): void { + 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; } - // @todo determine if installed ext has drifted using the PIE checksum - $piePackage = $piePackages[$phpExtensionName]; + $output->writeln(sprintf( - ' %s:%s (from %s)', + ' %s:%s (from 🥧 %s%s)', $phpExtensionName, $version, $piePackage->prettyNameAndVersion(), + self::verifyChecksumInformation( + $extensionPath, + $phpExtensionName, + $extensionEnding, + PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($piePackage->composerPackage), + ), )); }, ); @@ -74,6 +89,39 @@ static function (string $version, string $phpExtensionName) use ($output, $piePa 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 ' ✅ ' . substr($pieExpectedChecksum, 0, 8) . '...'; + } + /** @return array */ private function buildListOfPieInstalledPackages( OutputInterface $output, 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/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), + ); + } +} From c63bf8c3b99ba48768b57132e1c776be952c8844 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 30 Dec 2024 11:59:29 +0000 Subject: [PATCH 3/4] Clean warnings and deprecations from operatingSystemFamily() call --- src/Platform/TargetPhp/PhpBinaryPath.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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'); From dff97151202df0a4cfc382d5f4b1097ed75bc1e0 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 31 Dec 2024 08:20:53 +0000 Subject: [PATCH 4/4] If the checksum matches, don't need to write it out --- src/Command/ShowCommand.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index a6235b0b..cb750f11 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -119,7 +119,7 @@ private static function verifyChecksumInformation( return ' ⚠️ was ' . substr($actualInstalledBinary->checksum, 0, 8) . '..., expected ' . substr($pieExpectedChecksum, 0, 8) . '...'; } - return ' ✅ ' . substr($pieExpectedChecksum, 0, 8) . '...'; + return ' ✅'; } /** @return array */