Skip to content
Open
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
11 changes: 8 additions & 3 deletions src/Command/InstallExtensionsForProjectCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -162,13 +162,18 @@ public function execute(InputInterface $input, OutputInterface $output): int

array_walk(
$extensionsRequired,
function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePackages, $input, &$anyErrorsHappened): void {
function (Link $link) use ($pieComposer, $phpEnabledExtensions, $installedPiePackages, $input, &$anyErrorsHappened, $targetPlatform): void {
$extension = ExtensionName::normaliseFromString($link->getTarget());
$linkRequiresConstraint = $link->getPrettyConstraint();

$piePackagesForExtension = $installedPiePackages
->findByPhpFormattedExtensionName($extension->phpFormattedExtensionName())
->onlyVerifiedFor($targetPlatform);

$piePackageVersion = null;
if (in_array($extension->name(), array_keys($installedPiePackages))) {
$piePackageVersion = $installedPiePackages[$extension->name()]->version();

if (count($piePackagesForExtension) === 1) {
$piePackageVersion = $piePackagesForExtension->onlyOne()->version();
}

$piePackageVersionMatchesLinkConstraint = null;
Expand Down
188 changes: 73 additions & 115 deletions src/Command/ShowCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,14 @@
use Php\Pie\ComposerIntegration\PieComposerRequest;
use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys;
use Php\Pie\DependencyResolver\BundledPhpExtensionRefusal;
use Php\Pie\DependencyResolver\Package;
use Php\Pie\DependencyResolver\RequestedPackageAndVersion;
use Php\Pie\DependencyResolver\ResolveDependencyWithComposer;
use Php\Pie\DependencyResolver\UnableToResolveRequirement;
use Php\Pie\File\BinaryFile;
use Php\Pie\File\BinaryFileFailedVerification;
use Php\Pie\Platform as PiePlatform;
use Php\Pie\Platform\InstalledPiePackages;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Util\Emoji;
use Php\Pie\Util\PackageVerificationStatus;
use Psr\Container\ContainerInterface;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
Expand All @@ -28,15 +27,11 @@
use Webmozart\Assert\Assert;

use function array_diff;
use function array_key_exists;
use function array_keys;
use function array_map;
use function array_walk;
use function count;
use function file_exists;
use function rtrim;
use function sprintf;
use function substr;

use const DIRECTORY_SEPARATOR;

/** @phpstan-import-type PieMetadata from PieInstalledJsonMetadataKeys */
#[AsCommand(
Expand Down Expand Up @@ -99,8 +94,6 @@ public function execute(InputInterface $input, OutputInterface $output): int

$piePackages = $this->installedPiePackages->allPiePackages($composer);
$phpEnabledExtensions = $targetPlatform->phpBinaryPath->extensions();
$extensionPath = $targetPlatform->phpBinaryPath->extensionPath();
$extensionEnding = $targetPlatform->operatingSystem === OperatingSystem::Windows ? '.dll' : '.so';
$piePackagesMatched = [];
$rootPackageRequires = $composer->getPackage()->getRequires();

Expand All @@ -110,141 +103,106 @@ public function execute(InputInterface $input, OutputInterface $output): int
));
array_walk(
$phpEnabledExtensions,
function (string $version, string $phpExtensionName) use ($composer, $rootPackageRequires, $targetPlatform, $showAll, $piePackages, $extensionPath, $extensionEnding, &$piePackagesMatched): void {
if (! array_key_exists($phpExtensionName, $piePackages)) {
function (string $version, string $phpExtensionName) use ($composer, $rootPackageRequires, $targetPlatform, $showAll, $piePackages, &$piePackagesMatched): void {
$pieMatchesForExtension = $piePackages->findByPhpFormattedExtensionName($phpExtensionName);

if (! count($pieMatchesForExtension)) {
if ($showAll) {
$this->io->write(sprintf(' <comment>%s:%s</comment>', $phpExtensionName, $version));
}

return;
}

$piePackage = $piePackages[$phpExtensionName];
$piePackagesMatched[] = $phpExtensionName;
$packageName = $piePackage->name();
$packageRequirement = $rootPackageRequires[$piePackage->name()]->getPrettyConstraint();
foreach ($pieMatchesForExtension->packages() as $piePackage) {
$packageName = $piePackage->name();
$verificationStatus = $piePackage->verifyPackageStatus($targetPlatform);
$packageRequirement = $rootPackageRequires[$packageName]->getPrettyConstraint();

try {
// Don't check for updates for bundled PHP extensions
if ($piePackage->isBundledPhpExtension()) {
throw new BundledPhpExtensionRefusal();
if ($verificationStatus === PackageVerificationStatus::InstalledBinaryMetadataMissing) {
continue;
}

Assert::stringNotEmpty($packageName);
Assert::stringNotEmpty($packageRequirement);

$latestConstrainedPackage = ($this->resolveDependencyWithComposer)(
$composer,
$targetPlatform,
new RequestedPackageAndVersion($packageName, $packageRequirement),
false,
);

$latestPackage = ($this->resolveDependencyWithComposer)(
$composer,
$targetPlatform,
new RequestedPackageAndVersion($packageName, '*'),
false,
);
} catch (UnableToResolveRequirement | BundledPhpExtensionRefusal) {
$latestConstrainedPackage = null;
$latestPackage = null;
}
$piePackagesMatched[] = $packageName;

try {
// Don't check for updates for bundled PHP extensions
if ($piePackage->isBundledPhpExtension()) {
throw new BundledPhpExtensionRefusal();
}

Assert::stringNotEmpty($packageName);
Assert::stringNotEmpty($packageRequirement);

$latestConstrainedPackage = ($this->resolveDependencyWithComposer)(
$composer,
$targetPlatform,
new RequestedPackageAndVersion($packageName, $packageRequirement),
false,
);

$latestPackage = ($this->resolveDependencyWithComposer)(
$composer,
$targetPlatform,
new RequestedPackageAndVersion($packageName, '*'),
false,
);
} catch (UnableToResolveRequirement | BundledPhpExtensionRefusal) {
$latestConstrainedPackage = null;
$latestPackage = null;
}

$updateNotice = '';
if ($latestConstrainedPackage !== null && $latestConstrainedPackage->version() !== $piePackage->version()) {
$updateNotice = sprintf(
', upgradable to %s (within %s)',
$latestConstrainedPackage->version(),
$packageRequirement,
);
}
$updateNotice = '';
if ($latestConstrainedPackage !== null && $latestConstrainedPackage->version() !== $piePackage->version()) {
$updateNotice = sprintf(
', upgradable to %s (within %s)',
$latestConstrainedPackage->version(),
$packageRequirement,
);
}

if ($latestPackage !== null && $latestPackage->version() !== $latestConstrainedPackage->version()) {
$updateNotice .= sprintf(', latest version is %s', $latestPackage->version());
}
if ($latestPackage !== null && $latestPackage->version() !== $latestConstrainedPackage->version()) {
$updateNotice .= sprintf(', latest version is %s', $latestPackage->version());
}

$this->io->write(sprintf(
' <info>%s:%s</info> (from 🥧 <info>%s</info>%s)%s',
$phpExtensionName,
$version,
$piePackage->prettyNameAndVersion(),
self::verifyChecksumInformation(
$extensionPath,
$this->io->write(sprintf(
' <info>%s:%s</info> (from 🥧 <info>%s</info> %s)%s',
$phpExtensionName,
$extensionEnding,
PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($piePackage->composerPackage()),
),
$updateNotice,
));
$version,
$piePackage->prettyNameAndVersion(),
$verificationStatus->description(),
$updateNotice,
));
}
},
);

if (! $showAll && ! count($piePackagesMatched)) {
$this->io->write('(none)');
}

$unmatchedPiePackages = array_diff(array_keys($piePackages), $piePackagesMatched);
$unmatchedPiePackageNames = array_diff(array_map(static fn (Package $piePackage) => $piePackage->name(), $piePackages->packages()), $piePackagesMatched);

if (count($unmatchedPiePackages)) {
if (count($unmatchedPiePackageNames)) {
$this->io->write(sprintf(
'%s %s <options=bold,underscore>PIE packages not loaded:</>',
"\n",
Emoji::WARNING,
));
$this->io->write('These extensions were installed with PIE but are not currently enabled.' . "\n");
$this->io->write('These extensions were set up with PIE but are not currently enabled.' . "\n");

foreach ($unmatchedPiePackages as $unmatchedPiePackage) {
$this->io->write(sprintf(' - %s', $piePackages[$unmatchedPiePackage]->prettyNameAndVersion()));
foreach ($unmatchedPiePackageNames as $unmatchedPiePackageName) {
$unmatchedPiePackage = $piePackages->findByPackageName($unmatchedPiePackageName);

$message = match ($unmatchedPiePackage->verifyPackageStatus($targetPlatform)) {
PackageVerificationStatus::ChecksumMetadataMissing => '- was built but not installed yet.',
PackageVerificationStatus::InstalledBinaryMetadataMissing => '- was downloaded but has not been built yet.',
default => '- installed but not enabled in INI file',
};
$this->io->write(rtrim(sprintf(' - %s %s', $unmatchedPiePackage->prettyNameAndVersion(), $message)));
}
}

return Command::SUCCESS;
}

/**
* @param PieMetadata $installedJsonMetadata
* @phpstan-param '.dll'|'.so' $extensionEnding
*/
private static function verifyChecksumInformation(
string $extensionPath,
string $phpExtensionName,
string $extensionEnding,
array $installedJsonMetadata,
): string {
$actualBinaryPathByConvention = $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($actualBinaryPathByConvention)) {
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 (
$pieExpectedBinaryPath === null
|| $pieExpectedChecksum === null
|| $pieExpectedBinaryPath !== $actualBinaryPathByConvention
) {
return '';
}

$expectedBinaryFileFromMetadata = new BinaryFile($pieExpectedBinaryPath, $pieExpectedChecksum);
$actualBinaryFile = BinaryFile::fromFileWithSha256Checksum($actualBinaryPathByConvention);

try {
$expectedBinaryFileFromMetadata->verifyAgainstOther($actualBinaryFile);
} catch (BinaryFileFailedVerification) {
return sprintf(
' %s was %s..., expected %s...',
Emoji::WARNING,
substr($actualBinaryFile->checksum, 0, 8),
substr($expectedBinaryFileFromMetadata->checksum, 0, 8),
);
}

return ' ' . Emoji::GREEN_CHECKMARK;
}
}
2 changes: 1 addition & 1 deletion src/Command/UninstallCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ private function findPiePackageByPackageName(string $packageToRemove, Composer $
{
$piePackages = $this->installedPiePackages->allPiePackages($composer);

foreach ($piePackages as $piePackage) {
foreach ($piePackages->packages() as $piePackage) {
if ($piePackage->name() === $packageToRemove) {
return $piePackage;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ public function __construct(PhpBinaryPath $phpBinaryPath, Composer $composer, In

$piePackages = $installedPiePackages->allPiePackages($composer);
$extensionsBeingReplacedByPiePackages = [];
foreach ($piePackages as $piePackage) {
foreach ($piePackages->packages() as $piePackage) {
foreach ($piePackage->composerPackage()->getReplaces() as $replaceLink) {
$target = $replaceLink->getTarget();
if (
Expand Down
50 changes: 50 additions & 0 deletions src/DependencyResolver/Package.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,25 +6,34 @@

use Composer\Package\CompletePackageInterface;
use InvalidArgumentException;
use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys;
use Php\Pie\ConfigureOption;
use Php\Pie\Downloading\DownloadUrlMethod;
use Php\Pie\ExtensionName;
use Php\Pie\ExtensionType;
use Php\Pie\File\BinaryFile;
use Php\Pie\File\BinaryFileFailedVerification;
use Php\Pie\Platform\OperatingSystem;
use Php\Pie\Platform\OperatingSystemFamily;
use Php\Pie\Platform\TargetPlatform;
use Php\Pie\Util\PackageVerificationStatus;
use Webmozart\Assert\Assert;

use function array_key_exists;
use function array_map;
use function array_slice;
use function count;
use function explode;
use function file_exists;
use function implode;
use function is_array;
use function parse_url;
use function str_contains;
use function str_starts_with;
use function strtolower;

use const DIRECTORY_SEPARATOR;

/**
* @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks
*
Expand Down Expand Up @@ -235,4 +244,45 @@ public function supportedDownloadUrlMethods(): array|null
{
return $this->supportedDownloadUrlMethods;
}

public function verifyPackageStatus(TargetPlatform $targetPlatform): PackageVerificationStatus
{
$extensionPath = $targetPlatform->phpBinaryPath->extensionPath();
$extensionEnding = $targetPlatform->operatingSystem === OperatingSystem::Windows ? '.dll' : '.so';
$phpExtensionName = $this->extensionName->name();

$actualBinaryPathByConvention = $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($actualBinaryPathByConvention)) {
return PackageVerificationStatus::ActualBinaryNotFound;
}

$installedJsonMetadata = PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($this->composerPackage());
$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;

if ($pieExpectedBinaryPath === null) {
return PackageVerificationStatus::InstalledBinaryMetadataMissing;
}

if ($pieExpectedChecksum === null) {
return PackageVerificationStatus::ChecksumMetadataMissing;
}

if ($pieExpectedBinaryPath !== $actualBinaryPathByConvention) {
return PackageVerificationStatus::InstalledBinaryPathDoesNotMatchActualBinaryPath;
}

$expectedBinaryFileFromMetadata = new BinaryFile($pieExpectedBinaryPath, $pieExpectedChecksum);
$actualBinaryFile = BinaryFile::fromFileWithSha256Checksum($actualBinaryPathByConvention);

try {
$expectedBinaryFileFromMetadata->verifyAgainstOther($actualBinaryFile);
} catch (BinaryFileFailedVerification) {
return PackageVerificationStatus::ChecksumMismatch;
}

return PackageVerificationStatus::Verified;
}
}
16 changes: 16 additions & 0 deletions src/ExtensionName.php
Original file line number Diff line number Diff line change
Expand Up @@ -94,4 +94,20 @@ public function nameWithExtPrefix(): string
{
return 'ext-' . $this->normalisedExtensionName;
}

/** @return non-empty-string */
public function phpFormattedExtensionName(): string
{
return match ($this->name()) {
'core' => 'Core',
'spl' => 'SPL',
'phar' => 'Phar',
'reflection' => 'Reflection',
'pdo' => 'PDO',
'ffi' => 'FFI',
'opcache' => 'Zend OPcache',
'simplexml' => 'SimpleXML',
default => $this->name(),
};
}
}
Loading
Loading