Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enhance "show" command with PIE metadata #164

Merged
merged 4 commits into from
Dec 31, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
127 changes: 123 additions & 4 deletions src/Command/ShowCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand All @@ -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" . '<info>Loaded extensions:</info>');
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(' <comment>%s:%s</comment>', $phpExtensionName, $version));

return;
}

$piePackage = $piePackages[$phpExtensionName];

$output->writeln(sprintf(
' <info>%s:%s</info> (from 🥧 <info>%s</info>%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<non-empty-string, Package> */
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,
);
}
}
19 changes: 19 additions & 0 deletions src/ComposerIntegration/PieComposerRequest.php
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
}
}
46 changes: 45 additions & 1 deletion src/ComposerIntegration/PieInstalledJsonMetadataKeys.php
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -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;
}
}
4 changes: 2 additions & 2 deletions src/Platform/TargetPhp/PhpBinaryPath.php
Original file line number Diff line number Diff line change
Expand Up @@ -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');
Expand Down
44 changes: 44 additions & 0 deletions test/unit/ComposerIntegration/PieInstalledJsonMetadataKeysTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
<?php

declare(strict_types=1);

namespace Php\PieUnitTest\ComposerIntegration;

use Composer\Package\CompletePackageInterface;
use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys;
use PHPUnit\Framework\Attributes\CoversClass;
use PHPUnit\Framework\TestCase;

#[CoversClass(PieInstalledJsonMetadataKeys::class)]
final class PieInstalledJsonMetadataKeysTest extends TestCase
{
public function testPieMetadataFromComposerPackageWithEmptyExtra(): void
{
$composerPackage = $this->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),
);
}
}
Loading