From bfceab35272420451bc5190f9374110e08300b23 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 12 May 2026 09:31:12 +0100 Subject: [PATCH 01/15] 586: resolve aliased packages in InstalledJsonMetdata --- src/ComposerIntegration/InstalledJsonMetadata.php | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/ComposerIntegration/InstalledJsonMetadata.php b/src/ComposerIntegration/InstalledJsonMetadata.php index 77a35fb5..0245e40b 100644 --- a/src/ComposerIntegration/InstalledJsonMetadata.php +++ b/src/ComposerIntegration/InstalledJsonMetadata.php @@ -4,6 +4,7 @@ namespace Php\Pie\ComposerIntegration; +use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackageInterface; use Composer\PartialComposer; use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys as MetadataKey; @@ -117,6 +118,11 @@ private function addPieMetadata( ->getRepositoryManager() ->getLocalRepository() ->findPackages($composerPackage->getName())[0]; + + if ($localRepositoryPackage instanceof CompleteAliasPackage) { + $localRepositoryPackage = $localRepositoryPackage->getAliasOf(); + } + Assert::methodExists($localRepositoryPackage, 'setExtra'); $localRepositoryPackage->setExtra(array_merge($localRepositoryPackage->getExtra(), [$key->value => $value])); From 0b8236d7d2c980ad317b33cb6e46ebf126ec8607 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 12 May 2026 09:31:47 +0100 Subject: [PATCH 02/15] 586: comment out checks for single packages --- .../OverrideDownloadUrlInstallListener.php | 6 +-- .../PieComposerFactory.php | 2 +- .../PiePackageInstaller.php | 48 +++++++++---------- 3 files changed, 28 insertions(+), 28 deletions(-) diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index e0f41347..2bd20aa5 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -66,9 +66,9 @@ function (OperationInterface $operation): void { } // Install requests for other packages than the one we want should be ignored - if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { - return; - } +// if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { +// return; +// } $piePackage = Package::fromComposerCompletePackage($composerPackage); $targetPlatform = $this->composerRequest->targetPlatform; diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index b7c2bb05..6688628f 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -70,7 +70,7 @@ public static function createPieComposer( )); OverrideDownloadUrlInstallListener::selfRegister($composer, $io, $container, $composerRequest); - RemoveUnrelatedInstallOperations::selfRegister($composer, $composerRequest); +// RemoveUnrelatedInstallOperations::selfRegister($composer, $composerRequest); $composer->getConfig()->merge(['config' => ['__PIE_REQUEST__' => $composerRequest]]); $io->loadConfiguration($composer->getConfig()); diff --git a/src/ComposerIntegration/PiePackageInstaller.php b/src/ComposerIntegration/PiePackageInstaller.php index bec8b961..4ee777e1 100644 --- a/src/ComposerIntegration/PiePackageInstaller.php +++ b/src/ComposerIntegration/PiePackageInstaller.php @@ -39,18 +39,18 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa ?->then(function () use ($composerPackage) { $io = $this->composerRequest->pieOutput; - if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { - $io->write( - sprintf( - 'Skipping %s install request from Composer as it was not the expected PIE package %s', - $composerPackage->getName(), - $this->composerRequest->requestedPackage->package, - ), - verbosity: IOInterface::VERY_VERBOSE, - ); - - return null; - } +// if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { +// $io->write( +// sprintf( +// 'Skipping %s install request from Composer as it was not the expected PIE package %s', +// $composerPackage->getName(), +// $this->composerRequest->requestedPackage->package, +// ), +// verbosity: IOInterface::VERY_VERBOSE, +// ); +// +// return null; +// } if (! $composerPackage instanceof CompletePackageInterface) { $io->writeError(sprintf( @@ -81,18 +81,18 @@ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $ ?->then(function () use ($composerPackage) { $io = $this->composerRequest->pieOutput; - if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { - $io->write( - sprintf( - 'Skipping %s uninstall request from Composer as it was not the expected PIE package %s', - $composerPackage->getName(), - $this->composerRequest->requestedPackage->package, - ), - verbosity: IOInterface::VERY_VERBOSE, - ); - - return null; - } +// if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { +// $io->write( +// sprintf( +// 'Skipping %s uninstall request from Composer as it was not the expected PIE package %s', +// $composerPackage->getName(), +// $this->composerRequest->requestedPackage->package, +// ), +// verbosity: IOInterface::VERY_VERBOSE, +// ); +// +// return null; +// } if (! $composerPackage instanceof CompletePackageInterface) { $io->writeError(sprintf( From e415db2cf528f0469408095a39cb4314e9b4e196 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 12 May 2026 09:33:06 +0100 Subject: [PATCH 03/15] 586: only force re-install PIE packages that were not already activated in Target PHP --- .../ComposerIntegrationHandler.php | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index 6a13b6b3..df173daf 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -9,10 +9,12 @@ use Composer\Installer; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\ExtensionName; use Php\Pie\Platform; use Php\Pie\Platform\TargetPlatform; use Psr\Container\ContainerInterface; +use function array_key_exists; use function file_exists; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ @@ -63,9 +65,16 @@ public function runInstall( // Refresh the Composer instance so it re-reads the updated pie.json $composer = PieComposerFactory::recreatePieComposer($this->container, $composer); - // Removing the package from the local repository will trick Composer into "re-installing" it :) - foreach ($composer->getRepositoryManager()->getLocalRepository()->findPackages($requestedPackageAndVersion->package) as $pkg) { - $composer->getRepositoryManager()->getLocalRepository()->removePackage($pkg); + $phpEnabledExtensions = $targetPlatform->phpBinaryPath->extensions(); + foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $localRepoPackage) { + $extName = ExtensionName::determineFromComposerPackage($localRepoPackage); + + // Extension is already enabled in PHP + if (array_key_exists($extName->name(), $phpEnabledExtensions)) { + continue; + } + + $composer->getRepositoryManager()->getLocalRepository()->removePackage($localRepoPackage); } $composerInstaller = PieComposerInstaller::createWithPhpBinary( From 1307172f5ac5c8b20c5b1a788a0ecd8b37deea40 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 12 May 2026 09:33:38 +0100 Subject: [PATCH 04/15] 586: forcefully remove packages between scenarios in Behat test suite --- test/behaviour/CliContext.php | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 2620c476..9bf7991e 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -5,16 +5,18 @@ namespace Php\PieBehaviourTest; use Behat\Behat\Context\Context; +use Behat\Hook\AfterScenario; use Behat\Step\Given; use Behat\Step\Then; use Behat\Step\When; use Composer\Util\Platform; +use Safe\Exceptions\PcreException; use Symfony\Component\Process\Process; use Webmozart\Assert\Assert; use function array_merge; -use function assert; use function Safe\copy; +use function Safe\preg_match_all; use function Safe\realpath; use function sprintf; use function str_contains; @@ -33,6 +35,24 @@ class CliContext implements Context private string $thePackage = 'asgrim/example-pie-extension'; private string|null $workingDirectory = null; + /** @throws PcreException */ + #[AfterScenario] + public function removeInstalledExtensions(): void + { + $this->runPieCommand(['show']); + if (! preg_match_all('#from đŸĨ§\s*([^/]+\/[^:]+)#', (string) $this->output, $installedExtensionPackageNames)) { + return; + } + + foreach ($installedExtensionPackageNames[1] as $extensionPackageName) { + if ($extensionPackageName === 'xdebug/xdebug') { + continue; + } + + $this->runPieCommand(['uninstall', $extensionPackageName]); + } + } + #[When('I run a command to download the latest version of an extension')] public function iRunACommandToDownloadTheLatestVersionOfAnExtension(): void { @@ -181,7 +201,6 @@ public function iRunACommandToInstallAnExtensionWithoutEnabling(): void #[When('I run a command to uninstall an extension')] public function iRunACommandToUninstallAnExtension(): void { - assert($this->thePackage !== ''); $this->runPieCommand(['uninstall', $this->thePackage]); } From a693f9a453a5f2db071ee19cc53f8e5a8f8905d7 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 12 May 2026 12:25:57 +0100 Subject: [PATCH 05/15] 586: use verifyPackageStatus to determine install status --- .../ComposerIntegrationHandler.php | 26 ++++++++++++++++--- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index df173daf..212c6ecd 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -7,15 +7,20 @@ use Composer\Composer; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; +use Composer\Package\CompleteAliasPackage; +use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\ExtensionName; use Php\Pie\Platform; +use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Util\PackageVerificationStatus; use Psr\Container\ContainerInterface; -use function array_key_exists; +use function assert; use function file_exists; +use function sprintf; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class ComposerIntegrationHandler @@ -65,12 +70,25 @@ public function runInstall( // Refresh the Composer instance so it re-reads the updated pie.json $composer = PieComposerFactory::recreatePieComposer($this->container, $composer); - $phpEnabledExtensions = $targetPlatform->phpBinaryPath->extensions(); foreach ($composer->getRepositoryManager()->getLocalRepository()->getPackages() as $localRepoPackage) { $extName = ExtensionName::determineFromComposerPackage($localRepoPackage); - // Extension is already enabled in PHP - if (array_key_exists($extName->name(), $phpEnabledExtensions)) { + if ($localRepoPackage instanceof CompleteAliasPackage) { + $localRepoPackage = $localRepoPackage->getAliasOf(); + } + + assert($localRepoPackage instanceof CompletePackageInterface); + $piePackage = Package::fromComposerCompletePackage($localRepoPackage); + $status = $piePackage->verifyPackageStatus($targetPlatform); + + $this->arrayCollectionIo->notice(sprintf( + 'Install status %s (%s) status=%s', + $localRepoPackage->getName(), + $extName->name(), + $status->description(), + )); + + if ($status === PackageVerificationStatus::Verified) { continue; } From fbcf1ec0c08922c86d662fb8ea10cc65fab6557e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 13 May 2026 08:22:25 +0100 Subject: [PATCH 06/15] 586: remove .so only if verified --- .../InstallAndBuildProcess.php | 1 + src/ComposerIntegration/UninstallProcess.php | 12 +++++++++--- src/Util/PackageVerificationStatus.php | 15 +++++++++++++++ 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php index 69985a78..06930289 100644 --- a/src/ComposerIntegration/InstallAndBuildProcess.php +++ b/src/ComposerIntegration/InstallAndBuildProcess.php @@ -31,6 +31,7 @@ public function __invoke( CompletePackageInterface $composerPackage, string $installPath, ): void { + // @todo determine if we should build, determine if we should install etc $io = $composerRequest->pieOutput; $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( diff --git a/src/ComposerIntegration/UninstallProcess.php b/src/ComposerIntegration/UninstallProcess.php index cce50f73..8b7b009d 100644 --- a/src/ComposerIntegration/UninstallProcess.php +++ b/src/ComposerIntegration/UninstallProcess.php @@ -33,7 +33,15 @@ public function __invoke( $piePackage = Package::fromComposerCompletePackage($composerPackage); - $affectedIniFiles = ($this->removeIniEntry)($piePackage, $targetPlatform, $io); + $status = $piePackage->verifyPackageStatus($composerRequest->targetPlatform); + + if ($status->isInstalled()) { + $io->write(sprintf('👋 Removed extension: %s', ($this->uninstall)($targetPlatform, $piePackage)->filePath)); + } else { + $io->writeError(sprintf('Did not remove extension file: %s', $status->description())); + } + + $affectedIniFiles = ($this->removeIniEntry)($piePackage, $composerRequest->targetPlatform, $io); if (count($affectedIniFiles) === 1) { $io->write( @@ -52,7 +60,5 @@ public function __invoke( ); array_walk($affectedIniFiles, static fn (string $ini) => $io->write(' - ' . $ini)); } - - $io->write(sprintf('👋 Removed extension: %s', ($this->uninstall)($targetPlatform, $piePackage)->filePath)); } } diff --git a/src/Util/PackageVerificationStatus.php b/src/Util/PackageVerificationStatus.php index d0048194..301a61cb 100644 --- a/src/Util/PackageVerificationStatus.php +++ b/src/Util/PackageVerificationStatus.php @@ -24,4 +24,19 @@ public function description(): string self::InstalledBinaryPathDoesNotMatchActualBinaryPath => Emoji::WARNING . ' - binary path mismatch', }; } + + public function isInstalled(): bool + { + return $this === self::Verified; + } + + public function isBuilt(): bool + { + return $this->isInstalled() + || $this === self::ChecksumMismatch + || $this === self::ActualBinaryNotFound + || $this === self::InstalledBinaryMetadataMissing + || $this === self::ChecksumMetadataMissing + || $this === self::InstalledBinaryPathDoesNotMatchActualBinaryPath; + } } From f17c7a1b3a2e129ab57354856b19b3015efde068 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 13 May 2026 08:31:26 +0100 Subject: [PATCH 07/15] 586: rename InstalledJsonMetadata to AddInstalledJsonMetadata --- ...adata.php => AddInstalledJsonMetadata.php} | 2 +- .../ComposerIntegrationHandler.php | 1 - .../InstallAndBuildProcess.php | 8 ++--- .../OverrideDownloadUrlInstallListener.php | 6 ++-- .../PieComposerFactory.php | 1 - ...t.php => AddInstalledJsonMetadataTest.php} | 12 ++++---- .../InstallAndBuildProcessTest.php | 30 +++++++++---------- 7 files changed, 29 insertions(+), 31 deletions(-) rename src/ComposerIntegration/{InstalledJsonMetadata.php => AddInstalledJsonMetadata.php} (99%) rename test/unit/ComposerIntegration/{InstalledJsonMetadataTest.php => AddInstalledJsonMetadataTest.php} (93%) diff --git a/src/ComposerIntegration/InstalledJsonMetadata.php b/src/ComposerIntegration/AddInstalledJsonMetadata.php similarity index 99% rename from src/ComposerIntegration/InstalledJsonMetadata.php rename to src/ComposerIntegration/AddInstalledJsonMetadata.php index 0245e40b..205f9cd3 100644 --- a/src/ComposerIntegration/InstalledJsonMetadata.php +++ b/src/ComposerIntegration/AddInstalledJsonMetadata.php @@ -15,7 +15,7 @@ use function implode; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ -class InstalledJsonMetadata +class AddInstalledJsonMetadata { public function addDownloadMetadata( PartialComposer $composer, diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index 212c6ecd..e4121037 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -13,7 +13,6 @@ use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\ExtensionName; use Php\Pie\Platform; -use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\PackageVerificationStatus; use Psr\Container\ContainerInterface; diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php index 06930289..f0840344 100644 --- a/src/ComposerIntegration/InstallAndBuildProcess.php +++ b/src/ComposerIntegration/InstallAndBuildProcess.php @@ -20,7 +20,7 @@ class InstallAndBuildProcess public function __construct( private readonly Build $pieBuild, private readonly Install $pieInstall, - private readonly InstalledJsonMetadata $installedJsonMetadata, + private readonly AddInstalledJsonMetadata $addInstalledJsonMetadata, private readonly PlaceholderReplacer $placeholderReplacer, ) { } @@ -51,7 +51,7 @@ public function __invoke( $downloadedPackage, ); - $this->installedJsonMetadata->addDownloadMetadata( + $this->addInstalledJsonMetadata->addDownloadMetadata( $composer, $composerRequest, $composerPackage, @@ -66,7 +66,7 @@ public function __invoke( $io, ); - $this->installedJsonMetadata->addBuildMetadata( + $this->addInstalledJsonMetadata->addBuildMetadata( $composer, $composerRequest, $composerPackage, @@ -78,7 +78,7 @@ public function __invoke( return; } - $this->installedJsonMetadata->addInstallMetadata( + $this->addInstalledJsonMetadata->addInstallMetadata( $composer, $composerPackage, ($this->pieInstall)( diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index 2bd20aa5..3dc57cd1 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -66,9 +66,9 @@ function (OperationInterface $operation): void { } // Install requests for other packages than the one we want should be ignored -// if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { -// return; -// } + // if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + // return; + // } $piePackage = Package::fromComposerCompletePackage($composerPackage); $targetPlatform = $this->composerRequest->targetPlatform; diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index 6688628f..ac45c495 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -13,7 +13,6 @@ use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; -use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\ExtensionType; use Php\Pie\Platform; use Psr\Container\ContainerInterface; diff --git a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php b/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php similarity index 93% rename from test/unit/ComposerIntegration/InstalledJsonMetadataTest.php rename to test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php index 20234dff..111f2324 100644 --- a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php +++ b/test/unit/ComposerIntegration/AddInstalledJsonMetadataTest.php @@ -10,7 +10,7 @@ use Composer\Package\CompletePackageInterface; use Composer\Repository\InstalledArrayRepository; use Composer\Repository\RepositoryManager; -use Php\Pie\ComposerIntegration\InstalledJsonMetadata; +use Php\Pie\ComposerIntegration\AddInstalledJsonMetadata; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; @@ -26,8 +26,8 @@ use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase; -#[CoversClass(InstalledJsonMetadata::class)] -final class InstalledJsonMetadataTest extends TestCase +#[CoversClass(AddInstalledJsonMetadata::class)] +final class AddInstalledJsonMetadataTest extends TestCase { private function mockComposerInstalledRepositoryWith(CompletePackageInterface $package): Composer&MockObject { @@ -48,7 +48,7 @@ public function testMetadataForDownloads(): void $phpBinary = PhpBinaryPath::fromCurrentProcess(); - (new InstalledJsonMetadata())->addDownloadMetadata( + (new AddInstalledJsonMetadata())->addDownloadMetadata( $this->mockComposerInstalledRepositoryWith($package), new PieComposerRequest( $this->createMock(IOInterface::class), @@ -87,7 +87,7 @@ public function testMetadataForBuilds(): void { $package = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); - (new InstalledJsonMetadata())->addBuildMetadata( + (new AddInstalledJsonMetadata())->addBuildMetadata( $this->mockComposerInstalledRepositoryWith($package), new PieComposerRequest( $this->createMock(IOInterface::class), @@ -125,7 +125,7 @@ public function testMetadataForInstalls(): void { $package = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); - (new InstalledJsonMetadata())->addInstallMetadata( + (new AddInstalledJsonMetadata())->addInstallMetadata( $this->mockComposerInstalledRepositoryWith($package), clone $package, new BinaryFile('/path/to/installed', 'ignore'), diff --git a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php index 6318ba6e..be713a05 100644 --- a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php +++ b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php @@ -9,8 +9,8 @@ use Composer\PartialComposer; use Php\Pie\Building\Build; use Php\Pie\Building\PlaceholderReplacer; +use Php\Pie\ComposerIntegration\AddInstalledJsonMetadata; use Php\Pie\ComposerIntegration\InstallAndBuildProcess; -use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; @@ -31,7 +31,7 @@ final class InstallAndBuildProcessTest extends TestCase { private Build&MockObject $pieBuild; private Install&MockObject $pieInstall; - private InstalledJsonMetadata&MockObject $installedJsonMetadata; + private AddInstalledJsonMetadata&MockObject $addInstalledJsonMetadata; private InstallAndBuildProcess $installAndBuildProcess; @@ -39,14 +39,14 @@ public function setUp(): void { parent::setUp(); - $this->pieBuild = $this->createMock(Build::class); - $this->pieInstall = $this->createMock(Install::class); - $this->installedJsonMetadata = $this->createMock(InstalledJsonMetadata::class); + $this->pieBuild = $this->createMock(Build::class); + $this->pieInstall = $this->createMock(Install::class); + $this->addInstalledJsonMetadata = $this->createMock(AddInstalledJsonMetadata::class); $this->installAndBuildProcess = new InstallAndBuildProcess( $this->pieBuild, $this->pieInstall, - $this->installedJsonMetadata, + $this->addInstalledJsonMetadata, $this->createMock(PlaceholderReplacer::class), ); } @@ -74,11 +74,11 @@ public function testDownloadWithoutBuildAndInstall(): void $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); $installPath = '/path/to/install'; - $this->installedJsonMetadata->expects(self::once())->method('addDownloadMetadata'); + $this->addInstalledJsonMetadata->expects(self::once())->method('addDownloadMetadata'); - $this->installedJsonMetadata->expects(self::never())->method('addBuildMetadata'); + $this->addInstalledJsonMetadata->expects(self::never())->method('addBuildMetadata'); - $this->installedJsonMetadata->expects(self::never())->method('addInstallMetadata'); + $this->addInstalledJsonMetadata->expects(self::never())->method('addInstallMetadata'); $this->pieBuild->expects(self::never())->method('__invoke'); @@ -115,11 +115,11 @@ public function testDownloadAndBuildWithoutInstall(): void $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); $installPath = '/path/to/install'; - $this->installedJsonMetadata->expects(self::once())->method('addDownloadMetadata'); + $this->addInstalledJsonMetadata->expects(self::once())->method('addDownloadMetadata'); - $this->installedJsonMetadata->expects(self::once())->method('addBuildMetadata'); + $this->addInstalledJsonMetadata->expects(self::once())->method('addBuildMetadata'); - $this->installedJsonMetadata->expects(self::never())->method('addInstallMetadata'); + $this->addInstalledJsonMetadata->expects(self::never())->method('addInstallMetadata'); $this->pieBuild ->expects(self::once()) @@ -159,11 +159,11 @@ public function testDownloadBuildAndInstall(): void $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); $installPath = '/path/to/install'; - $this->installedJsonMetadata->expects(self::once())->method('addDownloadMetadata'); + $this->addInstalledJsonMetadata->expects(self::once())->method('addDownloadMetadata'); - $this->installedJsonMetadata->expects(self::once())->method('addBuildMetadata'); + $this->addInstalledJsonMetadata->expects(self::once())->method('addBuildMetadata'); - $this->installedJsonMetadata->expects(self::once())->method('addInstallMetadata'); + $this->addInstalledJsonMetadata->expects(self::once())->method('addInstallMetadata'); $this->pieBuild ->expects(self::once()) From 0c7489ce6ddb0b9afb014fc494bd79896dbd7b0b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 13 May 2026 10:27:14 +0100 Subject: [PATCH 08/15] 586: make new value object for a Package InstalledJsonMetadata --- src/Command/ShowCommand.php | 2 - .../AddInstalledJsonMetadata.php | 28 +-- .../InstalledJsonMetadata.php | 168 ++++++++++++++++++ .../PieInstalledJsonMetadataKeys.php | 65 ------- src/DependencyResolver/Package.php | 14 +- src/Installing/PackageMetadataMissing.php | 10 +- src/Installing/UninstallUsingUnlink.php | 26 ++- test/behaviour/CliContext.php | 5 +- .../PieInstalledJsonMetadataKeysTest.php | 44 ----- .../Installing/PackageMetadataMissingTest.php | 5 +- .../Installing/UninstallUsingUnlinkTest.php | 18 +- 11 files changed, 221 insertions(+), 164 deletions(-) create mode 100644 src/ComposerIntegration/InstalledJsonMetadata.php delete mode 100644 src/ComposerIntegration/PieInstalledJsonMetadataKeys.php delete mode 100644 test/unit/ComposerIntegration/PieInstalledJsonMetadataKeysTest.php diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index c291dc6a..ec457f09 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -8,7 +8,6 @@ use Composer\IO\NullIO; use Php\Pie\ComposerIntegration\PieComposerFactory; 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; @@ -33,7 +32,6 @@ use function rtrim; use function sprintf; -/** @phpstan-import-type PieMetadata from PieInstalledJsonMetadataKeys */ #[AsCommand( name: 'show', description: 'List the installed modules and their versions.', diff --git a/src/ComposerIntegration/AddInstalledJsonMetadata.php b/src/ComposerIntegration/AddInstalledJsonMetadata.php index 205f9cd3..b829d2e0 100644 --- a/src/ComposerIntegration/AddInstalledJsonMetadata.php +++ b/src/ComposerIntegration/AddInstalledJsonMetadata.php @@ -7,7 +7,6 @@ use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackageInterface; use Composer\PartialComposer; -use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys as MetadataKey; use Php\Pie\File\BinaryFile; use Webmozart\Assert\Assert; @@ -25,37 +24,37 @@ public function addDownloadMetadata( $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::TargetPlatformPhpPath, + InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_PATH, $composerRequest->targetPlatform->phpBinaryPath->phpBinaryPath, ); $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::TargetPlatformPhpConfigPath, + InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_CONFIG_PATH, $composerRequest->targetPlatform->phpBinaryPath->phpConfigPath(), ); $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::TargetPlatformPhpVersion, + InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_VERSION, $composerRequest->targetPlatform->phpBinaryPath->version(), ); $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::TargetPlatformPhpThreadSafety, + InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_THREAD_SAFETY, $composerRequest->targetPlatform->threadSafety->name, ); $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::TargetPlatformPhpWindowsCompiler, + InstalledJsonMetadata::KEY_TARGET_PLATFORM_PHP_WINDOWS_COMPILER, $composerRequest->targetPlatform->windowsCompiler?->name, ); $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::TargetPlatformArchitecture, + InstalledJsonMetadata::KEY_TARGET_PLATFORM_ARCHITECTURE, $composerRequest->targetPlatform->architecture->name, ); } @@ -69,28 +68,28 @@ public function addBuildMetadata( $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::ConfigureOptions, + InstalledJsonMetadata::KEY_CONFIGURE_OPTIONS, implode(' ', $composerRequest->configureOptions), ); $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::PhpizeBinary, + InstalledJsonMetadata::KEY_PHPIZE_BINARY, $composerRequest->targetPlatform->phpizePath->phpizeBinaryPath ?? null, ); $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::BuiltBinary, + InstalledJsonMetadata::KEY_BUILT_BINARY, $builtBinary->filePath, ); $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::BinaryChecksum, + InstalledJsonMetadata::KEY_BINARY_CHECKSUM, $builtBinary->checksum, ); } @@ -103,15 +102,16 @@ public function addInstallMetadata( $this->addPieMetadata( $composer, $composerPackage, - MetadataKey::InstalledBinary, + InstalledJsonMetadata::KEY_INSTALLED_BINARY, $installedBinary->filePath, ); } + /** @param InstalledJsonMetadata::KEY_* $key */ private function addPieMetadata( PartialComposer $composer, CompletePackageInterface $composerPackage, - MetadataKey $key, + string $key, string|null $value, ): void { $localRepositoryPackage = $composer @@ -125,6 +125,6 @@ private function addPieMetadata( Assert::methodExists($localRepositoryPackage, 'setExtra'); - $localRepositoryPackage->setExtra(array_merge($localRepositoryPackage->getExtra(), [$key->value => $value])); + $localRepositoryPackage->setExtra(array_merge($localRepositoryPackage->getExtra(), [$key => $value])); } } diff --git a/src/ComposerIntegration/InstalledJsonMetadata.php b/src/ComposerIntegration/InstalledJsonMetadata.php new file mode 100644 index 00000000..72cfdf3b --- /dev/null +++ b/src/ComposerIntegration/InstalledJsonMetadata.php @@ -0,0 +1,168 @@ +getExtra(); + + $onlyPieExtras = []; + + foreach (self::ALL_KEYS as $key) { + if ( + ! array_key_exists($key, $composerPackageExtras) + || ! is_string($composerPackageExtras[$key]) + || $composerPackageExtras[$key] === '' + ) { + continue; + } + + $onlyPieExtras[$key] = $composerPackageExtras[$key]; + } + + return new self($onlyPieExtras); + } + + /** @return PieMetadata */ + public function all(): array + { + return $this->values; + } + + /** @return non-empty-string|null */ + private function nonEmptyStringOrNull(string $key): string|null + { + return array_key_exists($key, $this->values) + ? $this->values[$key] + : null; + } + + /** @return non-empty-string|null */ + public function targetPlatformPhpPath(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_TARGET_PLATFORM_PHP_PATH); + } + + /** @return non-empty-string|null */ + public function targetPlatformPhpConfigPath(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_TARGET_PLATFORM_PHP_CONFIG_PATH); + } + + /** @return non-empty-string|null */ + public function targetPlatformPhpVersion(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_TARGET_PLATFORM_PHP_VERSION); + } + + /** @return non-empty-string|null */ + public function targetPlatformPhpThreadSafety(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_TARGET_PLATFORM_PHP_THREAD_SAFETY); + } + + /** @return non-empty-string|null */ + public function targetPlatformPhpWindowsCompiler(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_TARGET_PLATFORM_PHP_WINDOWS_COMPILER); + } + + /** @return non-empty-string|null */ + public function targetPlatformArchitecture(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_TARGET_PLATFORM_ARCHITECTURE); + } + + /** @return non-empty-string|null */ + public function configureOptions(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_CONFIGURE_OPTIONS); + } + + /** @return non-empty-string|null */ + public function builtBinary(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_BUILT_BINARY); + } + + /** @return non-empty-string|null */ + public function binaryChecksum(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_BINARY_CHECKSUM); + } + + /** @return non-empty-string|null */ + public function installedBinary(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_INSTALLED_BINARY); + } + + /** @return non-empty-string|null */ + public function phpizeBinary(): string|null + { + return $this->nonEmptyStringOrNull(self::KEY_PHPIZE_BINARY); + } +} diff --git a/src/ComposerIntegration/PieInstalledJsonMetadataKeys.php b/src/ComposerIntegration/PieInstalledJsonMetadataKeys.php deleted file mode 100644 index e93210f8..00000000 --- a/src/ComposerIntegration/PieInstalledJsonMetadataKeys.php +++ /dev/null @@ -1,65 +0,0 @@ -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/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index 513b3ffb..6a33ca24 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -6,7 +6,7 @@ use Composer\Package\CompletePackageInterface; use InvalidArgumentException; -use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; +use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\ConfigureOption; use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; @@ -56,6 +56,7 @@ final class Package private bool $supportNts = true; /** @var non-empty-list|null */ private array|null $supportedDownloadUrlMethods = null; + private readonly InstalledJsonMetadata $installedJsonMetadata; public function __construct( private readonly CompletePackageInterface $composerPackage, @@ -65,6 +66,7 @@ public function __construct( private readonly string $version, private readonly string|null $downloadUrl, ) { + $this->installedJsonMetadata = InstalledJsonMetadata::fromComposerPackage($this->composerPackage); } public static function fromComposerCompletePackage(CompletePackageInterface $completePackage): self @@ -269,6 +271,11 @@ public function supportedDownloadUrlMethods(): array|null return $this->supportedDownloadUrlMethods; } + public function installedJsonMetadata(): InstalledJsonMetadata + { + return $this->installedJsonMetadata; + } + public function verifyPackageStatus(TargetPlatform $targetPlatform): PackageVerificationStatus { $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); @@ -282,9 +289,8 @@ public function verifyPackageStatus(TargetPlatform $targetPlatform): PackageVeri 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; + $pieExpectedBinaryPath = $this->installedJsonMetadata()->installedBinary(); + $pieExpectedChecksum = $this->installedJsonMetadata()->binaryChecksum(); if ($pieExpectedBinaryPath === null) { return PackageVerificationStatus::InstalledBinaryMetadataMissing; diff --git a/src/Installing/PackageMetadataMissing.php b/src/Installing/PackageMetadataMissing.php index 50c5ff7a..1509e3d1 100644 --- a/src/Installing/PackageMetadataMissing.php +++ b/src/Installing/PackageMetadataMissing.php @@ -4,6 +4,7 @@ namespace Php\Pie\Installing; +use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\DependencyResolver\Package; use RuntimeException; @@ -15,13 +16,10 @@ class PackageMetadataMissing extends RuntimeException { - /** - * @param array $actualMetadata - * @param list $wantedKeys - */ - public static function duringUninstall(Package $package, array $actualMetadata, array $wantedKeys): self + /** @param list $wantedKeys */ + public static function duringUninstall(Package $package, InstalledJsonMetadata $actualMetadata, array $wantedKeys): self { - $missingKeys = array_diff($wantedKeys, array_keys($actualMetadata)); + $missingKeys = array_diff($wantedKeys, array_keys($actualMetadata->all())); return new self(sprintf( 'PIE metadata was missing for package %s. Missing metadata key%s: %s', diff --git a/src/Installing/UninstallUsingUnlink.php b/src/Installing/UninstallUsingUnlink.php index 23b8abfd..80369d0e 100644 --- a/src/Installing/UninstallUsingUnlink.php +++ b/src/Installing/UninstallUsingUnlink.php @@ -5,7 +5,7 @@ namespace Php\Pie\Installing; use Composer\Util\Platform as ComposerPlatform; -use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; +use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\DependencyResolver\Package; use Php\Pie\File\BinaryFile; use Php\Pie\File\FailedToUnlinkFile; @@ -16,7 +16,6 @@ use Php\Pie\Util\Process; use RuntimeException; -use function array_key_exists; use function file_exists; use function is_writable; use function sprintf; @@ -28,18 +27,17 @@ class UninstallUsingUnlink implements Uninstall { public function __invoke(TargetPlatform $targetPlatform, Package $package): BinaryFile { - $pieMetadata = PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($package->composerPackage()); + $pieMetadata = $package->installedJsonMetadata(); + $pieExpectedBinaryPath = $pieMetadata->installedBinary(); + $pieExpectedChecksum = $pieMetadata->binaryChecksum(); - if ( - ! array_key_exists(PieInstalledJsonMetadataKeys::InstalledBinary->value, $pieMetadata) - || ! array_key_exists(PieInstalledJsonMetadataKeys::BinaryChecksum->value, $pieMetadata) - ) { + if ($pieExpectedBinaryPath === null || $pieExpectedChecksum === null) { throw PackageMetadataMissing::duringUninstall( $package, $pieMetadata, [ - PieInstalledJsonMetadataKeys::InstalledBinary->value, - PieInstalledJsonMetadataKeys::BinaryChecksum->value, + InstalledJsonMetadata::KEY_INSTALLED_BINARY, + InstalledJsonMetadata::KEY_BINARY_CHECKSUM, ], ); } @@ -52,19 +50,15 @@ public function __invoke(TargetPlatform $targetPlatform, Package $package): Bina . ($targetPlatform->operatingSystem === OperatingSystem::Windows ? 'php_' : '') . $package->extensionName()->name() . ($targetPlatform->operatingSystem === OperatingSystem::Windows ? '.dll' : '.so'); - if ($extensionPathByConvention !== $pieMetadata[PieInstalledJsonMetadataKeys::InstalledBinary->value]) { + if ($extensionPathByConvention !== $pieExpectedBinaryPath) { throw new RuntimeException(sprintf( 'Stored metadata path "%s" did not match expected path "%s"', - $pieMetadata[PieInstalledJsonMetadataKeys::InstalledBinary->value], + $pieExpectedBinaryPath, $extensionPathByConvention, )); } - $expectedBinaryFile = new BinaryFile( - $pieMetadata[PieInstalledJsonMetadataKeys::InstalledBinary->value], - $pieMetadata[PieInstalledJsonMetadataKeys::BinaryChecksum->value], - ); - + $expectedBinaryFile = new BinaryFile($pieExpectedBinaryPath, $pieExpectedChecksum); $expectedBinaryFile->verify(); // If the target directory isn't writable, or a .so file already exists and isn't writable, try to use sudo diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 9bf7991e..c466c0c0 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -30,8 +30,9 @@ class CliContext implements Context private string|null $errorOutput = null; private int|null $exitCode = null; /** @var list */ - private array $phpArguments = []; - private string $theExtension = 'example_pie_extension'; + private array $phpArguments = []; + private string $theExtension = 'example_pie_extension'; + /** @var non-empty-string */ private string $thePackage = 'asgrim/example-pie-extension'; private string|null $workingDirectory = null; diff --git a/test/unit/ComposerIntegration/PieInstalledJsonMetadataKeysTest.php b/test/unit/ComposerIntegration/PieInstalledJsonMetadataKeysTest.php deleted file mode 100644 index e689d09e..00000000 --- a/test/unit/ComposerIntegration/PieInstalledJsonMetadataKeysTest.php +++ /dev/null @@ -1,44 +0,0 @@ -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), - ); - } -} diff --git a/test/unit/Installing/PackageMetadataMissingTest.php b/test/unit/Installing/PackageMetadataMissingTest.php index 5fdc5a78..bb282980 100644 --- a/test/unit/Installing/PackageMetadataMissingTest.php +++ b/test/unit/Installing/PackageMetadataMissingTest.php @@ -5,6 +5,7 @@ namespace Php\PieUnitTest\Installing; use Composer\Package\CompletePackageInterface; +use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\DependencyResolver\Package; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; @@ -28,10 +29,10 @@ public function testDuringUninstall(): void $exception = PackageMetadataMissing::duringUninstall( $package, - [ + InstalledJsonMetadata::fromArray([ 'a' => 'something', 'b' => 'something else', - ], + ]), ['b', 'c', 'd'], ); diff --git a/test/unit/Installing/UninstallUsingUnlinkTest.php b/test/unit/Installing/UninstallUsingUnlinkTest.php index 66d4c8cb..f81ebd3e 100644 --- a/test/unit/Installing/UninstallUsingUnlinkTest.php +++ b/test/unit/Installing/UninstallUsingUnlinkTest.php @@ -6,7 +6,7 @@ use Composer\Package\CompletePackageInterface; use Composer\Util\Filesystem; -use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; +use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\DependencyResolver\Package; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; @@ -100,8 +100,8 @@ public function testBinaryFileIsRemoved(): void $composerPackage ->method('getExtra') ->willReturn([ - PieInstalledJsonMetadataKeys::InstalledBinary->value => $extensionFile, - PieInstalledJsonMetadataKeys::BinaryChecksum->value => $testHash, + InstalledJsonMetadata::KEY_INSTALLED_BINARY => $extensionFile, + InstalledJsonMetadata::KEY_BINARY_CHECKSUM => $testHash, ]); $package = new Package( @@ -148,8 +148,8 @@ public function testBinaryFileIsRemovedOnWindows(): void $composerPackage ->method('getExtra') ->willReturn([ - PieInstalledJsonMetadataKeys::InstalledBinary->value => $extensionFile, - PieInstalledJsonMetadataKeys::BinaryChecksum->value => $testHash, + InstalledJsonMetadata::KEY_INSTALLED_BINARY => $extensionFile, + InstalledJsonMetadata::KEY_BINARY_CHECKSUM => $testHash, ]); $package = new Package( @@ -200,8 +200,8 @@ public function testBinaryFileIsRemovedWhenInstallRootUsed(): void $composerPackage ->method('getExtra') ->willReturn([ - PieInstalledJsonMetadataKeys::InstalledBinary->value => $extensionFile, - PieInstalledJsonMetadataKeys::BinaryChecksum->value => $testHash, + InstalledJsonMetadata::KEY_INSTALLED_BINARY => $extensionFile, + InstalledJsonMetadata::KEY_BINARY_CHECKSUM => $testHash, ]); $package = new Package( @@ -251,8 +251,8 @@ public function testExtensionPathInMetadataNotMatchingConventionWillThrowExcepti $composerPackage ->method('getExtra') ->willReturn([ - PieInstalledJsonMetadataKeys::InstalledBinary->value => $extensionFile, - PieInstalledJsonMetadataKeys::BinaryChecksum->value => $testHash, + InstalledJsonMetadata::KEY_INSTALLED_BINARY => $extensionFile, + InstalledJsonMetadata::KEY_BINARY_CHECKSUM => $testHash, ]); $package = new Package( From 5d939a2e1f444a4daf55733a42fa6317301e7f13 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 14 May 2026 13:10:50 +0100 Subject: [PATCH 09/15] 586: isolate working directory for tests --- test/integration/Command/BuildCommandTest.php | 5 +- .../Command/DownloadCommandTest.php | 5 +- test/integration/Command/InfoCommandTest.php | 5 +- .../Command/InstallCommandTest.php | 61 +++++++++---------- .../IsolatedWorkingDirectoryTestCase.php | 33 ++++++++++ .../RepositoryManagementCommandsTest.php | 5 +- 6 files changed, 74 insertions(+), 40 deletions(-) create mode 100644 test/integration/Command/IsolatedWorkingDirectoryTestCase.php diff --git a/test/integration/Command/BuildCommandTest.php b/test/integration/Command/BuildCommandTest.php index 342e81ee..6db411a1 100644 --- a/test/integration/Command/BuildCommandTest.php +++ b/test/integration/Command/BuildCommandTest.php @@ -8,12 +8,11 @@ use Php\Pie\Command\BuildCommand; use Php\Pie\Container; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\TestCase; use function str_contains; #[CoversClass(BuildCommand::class)] -class BuildCommandTest extends TestCase +class BuildCommandTest extends IsolatedWorkingDirectoryTestCase { private const TEST_PACKAGE = 'asgrim/example-pie-extension'; @@ -21,6 +20,8 @@ class BuildCommandTest extends TestCase public function setUp(): void { + parent::setUp(); + $this->commandTester = new CommandTester(Container::testFactory()->get(BuildCommand::class)); } diff --git a/test/integration/Command/DownloadCommandTest.php b/test/integration/Command/DownloadCommandTest.php index fb580fe8..7ce923c6 100644 --- a/test/integration/Command/DownloadCommandTest.php +++ b/test/integration/Command/DownloadCommandTest.php @@ -11,7 +11,6 @@ use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; use PHPUnit\Framework\Attributes\RequiresPhp; -use PHPUnit\Framework\TestCase; use function array_combine; use function array_map; @@ -21,7 +20,7 @@ use const PHP_VERSION_ID; #[CoversClass(DownloadCommand::class)] -class DownloadCommandTest extends TestCase +class DownloadCommandTest extends IsolatedWorkingDirectoryTestCase { private const TEST_PACKAGE_LATEST = '2.0.9'; private const TEST_PACKAGE = 'asgrim/example-pie-extension'; @@ -30,6 +29,8 @@ class DownloadCommandTest extends TestCase public function setUp(): void { + parent::setUp(); + $this->commandTester = new CommandTester(Container::testFactory()->get(DownloadCommand::class)); } diff --git a/test/integration/Command/InfoCommandTest.php b/test/integration/Command/InfoCommandTest.php index a0f520d8..5871f1f3 100644 --- a/test/integration/Command/InfoCommandTest.php +++ b/test/integration/Command/InfoCommandTest.php @@ -7,15 +7,16 @@ use Php\Pie\Command\InfoCommand; use Php\Pie\Container; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\TestCase; #[CoversClass(InfoCommand::class)] -final class InfoCommandTest extends TestCase +final class InfoCommandTest extends IsolatedWorkingDirectoryTestCase { private CommandTester $commandTester; public function setUp(): void { + parent::setUp(); + $this->commandTester = new CommandTester(Container::testFactory()->get(InfoCommand::class)); } diff --git a/test/integration/Command/InstallCommandTest.php b/test/integration/Command/InstallCommandTest.php index 9ba63c84..18c4e05d 100644 --- a/test/integration/Command/InstallCommandTest.php +++ b/test/integration/Command/InstallCommandTest.php @@ -10,7 +10,6 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; -use PHPUnit\Framework\TestCase; use Symfony\Component\Console\Output\BufferedOutput; use Symfony\Component\Process\Process; @@ -19,26 +18,39 @@ use function array_key_exists; use function array_map; use function array_unshift; -use function assert; use function file_exists; use function is_executable; -use function is_file; -use function is_string; use function is_writable; use function Safe\preg_match; #[CoversClass(InstallCommand::class)] -class InstallCommandTest extends TestCase +class InstallCommandTest extends IsolatedWorkingDirectoryTestCase { private const TEST_PACKAGE = 'asgrim/example-pie-extension'; private CommandTester $commandTester; + private string|null $lastInstalledBinary = null; public function setUp(): void { + parent::setUp(); + $this->commandTester = new CommandTester(Container::testFactory()->get(InstallCommand::class)); } + protected function tearDown(): void + { + if ($this->lastInstalledBinary !== null && file_exists($this->lastInstalledBinary)) { + $rmCommand = ['rm', $this->lastInstalledBinary]; + if (! is_writable($this->lastInstalledBinary)) { + array_unshift($rmCommand, 'sudo'); + } + (new Process($rmCommand))->run(); + } + + parent::tearDown(); + } + /** @return array */ public static function phpPathProvider(): array { @@ -87,27 +99,16 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin $this->commandTester->assertCommandIsSuccessful(); $outputString = $this->commandTester->getDisplay(); - self::assertStringContainsString('Install complete: ', $outputString); - self::assertStringContainsString('You must now add "extension=example_pie_extension" to your php.ini', $outputString); - if ( - ! preg_match('#^Install complete: (.*)$#m', $outputString, $matches) - || ! array_key_exists(1, $matches) - || $matches[1] === '' - || ! file_exists($matches[1]) - || ! is_file($matches[1]) + if (preg_match('#^Install complete: (.*)$#m', $outputString, $matches) + && array_key_exists(1, $matches) + && $matches[1] !== '' ) { - return; + $this->lastInstalledBinary = $matches[1]; } - $fileToRemove = $matches[1]; - assert(is_string($fileToRemove)); - $rmCommand = ['rm', $fileToRemove]; - if (! is_writable($fileToRemove)) { - array_unshift($rmCommand, 'sudo'); - } - - (new Process($rmCommand))->mustRun(); + self::assertStringContainsString('Install complete: ', $outputString); + self::assertStringContainsString('You must now add "extension=example_pie_extension" to your php.ini', $outputString); } #[RequiresOperatingSystemFamily('Windows')] @@ -121,19 +122,15 @@ public function testInstallCommandWillInstallCompatibleExtensionWindows(): void $this->commandTester->assertCommandIsSuccessful(); $outputString = $this->commandTester->getDisplay(); - self::assertStringContainsString('Copied DLL to: ', $outputString); - self::assertStringContainsString('You must now add "extension=example_pie_extension" to your php.ini', $outputString); - if ( - ! preg_match('#^Copied DLL to: (.*)$#m', $outputString, $matches) - || ! array_key_exists(1, $matches) - || $matches[1] === '' - || ! file_exists($matches[1]) - || ! is_file($matches[1]) + if (preg_match('#^Copied DLL to: (.*)$#m', $outputString, $matches) + && array_key_exists(1, $matches) + && $matches[1] !== '' ) { - return; + $this->lastInstalledBinary = $matches[1]; } - (new Process(['rm', $matches[1]]))->mustRun(); + self::assertStringContainsString('Copied DLL to: ', $outputString); + self::assertStringContainsString('You must now add "extension=example_pie_extension" to your php.ini', $outputString); } } diff --git a/test/integration/Command/IsolatedWorkingDirectoryTestCase.php b/test/integration/Command/IsolatedWorkingDirectoryTestCase.php new file mode 100644 index 00000000..e3d7dec8 --- /dev/null +++ b/test/integration/Command/IsolatedWorkingDirectoryTestCase.php @@ -0,0 +1,33 @@ +tempPieDir = sys_get_temp_dir() . '/pie-test-' . uniqid(); + putenv('PIE_WORKING_DIRECTORY=' . $this->tempPieDir); + } + + protected function tearDown(): void + { + if (file_exists($this->tempPieDir)) { + (new Process(['rm', '-rf', $this->tempPieDir]))->run(); + } + + putenv('PIE_WORKING_DIRECTORY'); + } +} diff --git a/test/integration/Command/RepositoryManagementCommandsTest.php b/test/integration/Command/RepositoryManagementCommandsTest.php index b93c411e..3e31148b 100644 --- a/test/integration/Command/RepositoryManagementCommandsTest.php +++ b/test/integration/Command/RepositoryManagementCommandsTest.php @@ -9,7 +9,6 @@ use Php\Pie\Command\RepositoryRemoveCommand; use Php\Pie\Container; use PHPUnit\Framework\Attributes\CoversClass; -use PHPUnit\Framework\TestCase; use function array_filter; use function array_map; @@ -23,7 +22,7 @@ #[CoversClass(RepositoryListCommand::class)] #[CoversClass(RepositoryAddCommand::class)] #[CoversClass(RepositoryRemoveCommand::class)] -final class RepositoryManagementCommandsTest extends TestCase +final class RepositoryManagementCommandsTest extends IsolatedWorkingDirectoryTestCase { private const EXAMPLE_PATH_REPOSITORY_URL = __DIR__; private const EXAMPLE_VCS_REPOSITORY_URL = 'https://github.com/asgrim/example-pie-extension'; @@ -35,6 +34,8 @@ final class RepositoryManagementCommandsTest extends TestCase public function setUp(): void { + parent::setUp(); + $this->listCommand = new CommandTester(Container::testFactory()->get(RepositoryListCommand::class)); $this->addCommand = new CommandTester(Container::testFactory()->get(RepositoryAddCommand::class)); $this->removeCommand = new CommandTester(Container::testFactory()->get(RepositoryRemoveCommand::class)); From 43a360ad45d40474541672381c376e6abb4fa04e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 15 May 2026 12:38:55 +0100 Subject: [PATCH 10/15] 586: if package was not installed, don't try reinstalling it --- .../ComposerIntegrationHandler.php | 46 +++++++++++++++++-- .../InstalledJsonMetadata.php | 18 ++++++++ src/ComposerIntegration/UninstallProcess.php | 2 +- src/Util/Emoji.php | 1 + src/Util/PackageVerificationStatus.php | 12 +---- 5 files changed, 62 insertions(+), 17 deletions(-) diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index e4121037..265afbf8 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -7,6 +7,7 @@ use Composer\Composer; use Composer\Filter\PlatformRequirementFilter\PlatformRequirementFilterFactory; use Composer\Installer; +use Composer\IO\IOInterface; use Composer\Package\CompleteAliasPackage; use Composer\Package\CompletePackageInterface; use Php\Pie\DependencyResolver\Package; @@ -14,6 +15,7 @@ use Php\Pie\ExtensionName; use Php\Pie\Platform; use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Util\Emoji; use Php\Pie\Util\PackageVerificationStatus; use Psr\Container\ContainerInterface; @@ -77,20 +79,54 @@ public function runInstall( } assert($localRepoPackage instanceof CompletePackageInterface); - $piePackage = Package::fromComposerCompletePackage($localRepoPackage); - $status = $piePackage->verifyPackageStatus($targetPlatform); + $piePackage = Package::fromComposerCompletePackage($localRepoPackage); + $installedJsonMetadata = $piePackage->installedJsonMetadata(); + $status = $piePackage->verifyPackageStatus($targetPlatform); - $this->arrayCollectionIo->notice(sprintf( + $this->arrayCollectionIo->write(sprintf( 'Install status %s (%s) status=%s', $localRepoPackage->getName(), $extName->name(), $status->description(), - )); + ), verbosity: IOInterface::VERY_VERBOSE); + + if ($status->isVerified()) { + $this->arrayCollectionIo->write(sprintf( + '%s PIE package %s (%s) is already installed and verified.', + Emoji::GREEN_CHECKMARK, + $localRepoPackage->getName(), + $extName->name(), + ), verbosity: IOInterface::QUIET); + continue; + } + + if (! $installedJsonMetadata->isInstalled() && $installedJsonMetadata->isBuilt()) { + $this->arrayCollectionIo->write(sprintf( + '%s PIE package %s (%s) was previously built but not installed.', + Emoji::INFO, + $localRepoPackage->getName(), + $extName->name(), + ), verbosity: IOInterface::VERBOSE); + continue; + } - if ($status === PackageVerificationStatus::Verified) { + if (! $installedJsonMetadata->isInstalled() && ! $installedJsonMetadata->isBuilt() && $installedJsonMetadata->isDownloaded()) { + $this->arrayCollectionIo->write(sprintf( + '%s PIE package %s (%s) was previously downloaded but not built.', + Emoji::INFO, + $localRepoPackage->getName(), + $extName->name(), + ), verbosity: IOInterface::VERBOSE); continue; } + $this->arrayCollectionIo->write(sprintf( + '%s Package status of %s (%s) is not yet verified, adding to install candidates: %s', + Emoji::WARNING, + $localRepoPackage->getName(), + $extName->name(), + $status->description(), + )); $composer->getRepositoryManager()->getLocalRepository()->removePackage($localRepoPackage); } diff --git a/src/ComposerIntegration/InstalledJsonMetadata.php b/src/ComposerIntegration/InstalledJsonMetadata.php index 72cfdf3b..35d81159 100644 --- a/src/ComposerIntegration/InstalledJsonMetadata.php +++ b/src/ComposerIntegration/InstalledJsonMetadata.php @@ -165,4 +165,22 @@ public function phpizeBinary(): string|null { return $this->nonEmptyStringOrNull(self::KEY_PHPIZE_BINARY); } + + /** Has this package been downloaded, according to the metadata? (note: does not verifiy it is STILL downloaded - especially if vendor cleanup happened!) */ + public function isDownloaded(): bool + { + return $this->targetPlatformPhpVersion() !== null; + } + + /** Has this package been built, according to the metadata? (note: does not verify it is STILL built) */ + public function isBuilt(): bool + { + return $this->isDownloaded() && $this->builtBinary() !== null; + } + + /** Has this package been installed, according to the metadata (note: not verify it is STILL installed/verified) */ + public function isInstalled(): bool + { + return $this->isBuilt() && $this->installedBinary() !== null; + } } diff --git a/src/ComposerIntegration/UninstallProcess.php b/src/ComposerIntegration/UninstallProcess.php index 8b7b009d..80b10489 100644 --- a/src/ComposerIntegration/UninstallProcess.php +++ b/src/ComposerIntegration/UninstallProcess.php @@ -35,7 +35,7 @@ public function __invoke( $status = $piePackage->verifyPackageStatus($composerRequest->targetPlatform); - if ($status->isInstalled()) { + if ($status->isVerified()) { $io->write(sprintf('👋 Removed extension: %s', ($this->uninstall)($targetPlatform, $piePackage)->filePath)); } else { $io->writeError(sprintf('Did not remove extension file: %s', $status->description())); diff --git a/src/Util/Emoji.php b/src/Util/Emoji.php index 8e980019..82fab5f0 100644 --- a/src/Util/Emoji.php +++ b/src/Util/Emoji.php @@ -11,4 +11,5 @@ final class Emoji public const WARNING = 'âš ī¸ '; public const PROHIBITED = 'đŸšĢ'; public const CROSS = '❌'; + public const INFO = 'â„šī¸'; } diff --git a/src/Util/PackageVerificationStatus.php b/src/Util/PackageVerificationStatus.php index 301a61cb..272d8646 100644 --- a/src/Util/PackageVerificationStatus.php +++ b/src/Util/PackageVerificationStatus.php @@ -25,18 +25,8 @@ public function description(): string }; } - public function isInstalled(): bool + public function isVerified(): bool { return $this === self::Verified; } - - public function isBuilt(): bool - { - return $this->isInstalled() - || $this === self::ChecksumMismatch - || $this === self::ActualBinaryNotFound - || $this === self::InstalledBinaryMetadataMissing - || $this === self::ChecksumMetadataMissing - || $this === self::InstalledBinaryPathDoesNotMatchActualBinaryPath; - } } From 59b215465267080305fa817db0fda7818152162b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 28 May 2026 09:51:12 +0100 Subject: [PATCH 11/15] 586: remove todo for InstallAndBuildProcess Logic being; if a package was requested for install, we currently re-download the package anyway; AND even if we removed that logic, we should always do a `make clean` anyway to ensure a clean build. --- phpstan-baseline.neon | 12 ------------ .../ComposerIntegrationHandler.php | 1 - src/ComposerIntegration/InstallAndBuildProcess.php | 1 - src/Util/Emoji.php | 2 +- test/integration/Command/InstallCommandTest.php | 7 +++++-- 5 files changed, 6 insertions(+), 17 deletions(-) diff --git a/phpstan-baseline.neon b/phpstan-baseline.neon index 7a81d09b..19da6192 100644 --- a/phpstan-baseline.neon +++ b/phpstan-baseline.neon @@ -240,18 +240,6 @@ parameters: count: 2 path: test/integration/Command/InstallCommandTest.php - - - message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: test/integration/Command/InstallCommandTest.php - - - - message: '#^Call to function is_string\(\) with non\-empty\-string will always evaluate to true\.$#' - identifier: function.alreadyNarrowedType - count: 1 - path: test/integration/Command/InstallCommandTest.php - - message: '#^Call to function assert\(\) with true will always evaluate to true\.$#' identifier: function.alreadyNarrowedType diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index 265afbf8..eb0d12d7 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -16,7 +16,6 @@ use Php\Pie\Platform; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Emoji; -use Php\Pie\Util\PackageVerificationStatus; use Psr\Container\ContainerInterface; use function assert; diff --git a/src/ComposerIntegration/InstallAndBuildProcess.php b/src/ComposerIntegration/InstallAndBuildProcess.php index f0840344..45a97ab3 100644 --- a/src/ComposerIntegration/InstallAndBuildProcess.php +++ b/src/ComposerIntegration/InstallAndBuildProcess.php @@ -31,7 +31,6 @@ public function __invoke( CompletePackageInterface $composerPackage, string $installPath, ): void { - // @todo determine if we should build, determine if we should install etc $io = $composerRequest->pieOutput; $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( diff --git a/src/Util/Emoji.php b/src/Util/Emoji.php index 82fab5f0..7c160266 100644 --- a/src/Util/Emoji.php +++ b/src/Util/Emoji.php @@ -11,5 +11,5 @@ final class Emoji public const WARNING = 'âš ī¸ '; public const PROHIBITED = 'đŸšĢ'; public const CROSS = '❌'; - public const INFO = 'â„šī¸'; + public const INFO = 'â„šī¸ '; } diff --git a/test/integration/Command/InstallCommandTest.php b/test/integration/Command/InstallCommandTest.php index 18c4e05d..631325bc 100644 --- a/test/integration/Command/InstallCommandTest.php +++ b/test/integration/Command/InstallCommandTest.php @@ -45,6 +45,7 @@ protected function tearDown(): void if (! is_writable($this->lastInstalledBinary)) { array_unshift($rmCommand, 'sudo'); } + (new Process($rmCommand))->run(); } @@ -100,7 +101,8 @@ public function testInstallCommandWillInstallCompatibleExtensionNonWindows(strin $outputString = $this->commandTester->getDisplay(); - if (preg_match('#^Install complete: (.*)$#m', $outputString, $matches) + if ( + preg_match('#^Install complete: (.*)$#m', $outputString, $matches) && array_key_exists(1, $matches) && $matches[1] !== '' ) { @@ -123,7 +125,8 @@ public function testInstallCommandWillInstallCompatibleExtensionWindows(): void $outputString = $this->commandTester->getDisplay(); - if (preg_match('#^Copied DLL to: (.*)$#m', $outputString, $matches) + if ( + preg_match('#^Copied DLL to: (.*)$#m', $outputString, $matches) && array_key_exists(1, $matches) && $matches[1] !== '' ) { From 1767d02d31d5b9c4a0b7233d30a94f4ce2192d5e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 28 May 2026 10:02:00 +0100 Subject: [PATCH 12/15] 586: allow --force param to always re-install a package --- features/install-extensions.feature | 12 ++++++++++ .../ComposerIntegrationHandler.php | 2 +- test/behaviour/CliContext.php | 22 +++++++++++++++++++ 3 files changed, 35 insertions(+), 1 deletion(-) diff --git a/features/install-extensions.feature b/features/install-extensions.feature index 63d0b239..defa15f3 100644 --- a/features/install-extensions.feature +++ b/features/install-extensions.feature @@ -9,3 +9,15 @@ Feature: Extensions can be installed with PIE Example: An extension can be installed and enabled When I run a command to install an extension Then the extension should have been installed and enabled + + # pie install && pie install + Example: Re-installing an existing extension is a no-op + Given an extension was previously installed and enabled + When I run a command to install an extension + Then the extension should not have been re-installed + + # pie install && pie install --force + Example: Forcefully re-installing an existing extension should re-install the extension + Given an extension was previously installed and enabled + When I run a command to forcefully install an extension + Then the extension should have been installed and enabled diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index eb0d12d7..c30bfe56 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -89,7 +89,7 @@ public function runInstall( $status->description(), ), verbosity: IOInterface::VERY_VERBOSE); - if ($status->isVerified()) { + if ($status->isVerified() && ! $forceInstallPackageVersion) { $this->arrayCollectionIo->write(sprintf( '%s PIE package %s (%s) is already installed and verified.', Emoji::GREEN_CHECKMARK, diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index c466c0c0..6b960bba 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -191,6 +191,14 @@ public function iRunACommandToInstallAnExtension(): void $this->runPieCommand(['install', $this->thePackage]); } + #[When('I run a command to forcefully install an extension')] + public function iRunACommandToForcefullyInstallAnExtension(): void + { + $this->theExtension = 'example_pie_extension'; + $this->thePackage = 'asgrim/example-pie-extension'; + $this->runPieCommand(['install', '--force', $this->thePackage]); + } + #[When('I run a command to install an extension without enabling it')] public function iRunACommandToInstallAnExtensionWithoutEnabling(): void { @@ -265,6 +273,20 @@ public function theExtensionShouldHaveBeenInstalledAndEnabled(): void Assert::same($isExtEnabled, 'yes'); } + #[Then('the extension should not have been re-installed')] + public function theExtensionShouldNotHaveBeenReinstalled(): void + { + $this->assertCommandSuccessful(); + + Assert::contains($this->output, 'PIE package asgrim/example-pie-extension (example_pie_extension) is already installed and verified.'); + + $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("' . $this->theExtension . '")?"yes":"no";'])) + ->mustRun() + ->getOutput(); + + Assert::same($isExtEnabled, 'yes'); + } + #[Given('I have an invalid extension installed')] public function iHaveAnInvalidExtensionInstalled(): void { From cf065e4ae7a1f3a3b7fe3986a13bb41a7fbc3d62 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 28 May 2026 11:17:38 +0100 Subject: [PATCH 13/15] 586: match any package name in pie show for uninstall cleanup in Behat --- test/behaviour/CliContext.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 6b960bba..73fb65c4 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -41,7 +41,7 @@ class CliContext implements Context public function removeInstalledExtensions(): void { $this->runPieCommand(['show']); - if (! preg_match_all('#from đŸĨ§\s*([^/]+\/[^:]+)#', (string) $this->output, $installedExtensionPackageNames)) { + if (! preg_match_all('#([a-zA-Z0-9-_]+/[a-zA-Z0-9-_]+):#', (string) $this->output, $installedExtensionPackageNames)) { return; } From b258c8ad9be09657555b4e5d7fd4ecd28199f665 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 29 May 2026 11:07:07 +0100 Subject: [PATCH 14/15] 586: ensure (re)installing after download/build will actually install --- features/install-extensions.feature | 12 --------- features/reinstall-extensions.feature | 25 +++++++++++++++++++ .../ComposerIntegrationHandler.php | 2 -- test/behaviour/CliContext.php | 2 ++ 4 files changed, 27 insertions(+), 14 deletions(-) create mode 100644 features/reinstall-extensions.feature diff --git a/features/install-extensions.feature b/features/install-extensions.feature index defa15f3..63d0b239 100644 --- a/features/install-extensions.feature +++ b/features/install-extensions.feature @@ -9,15 +9,3 @@ Feature: Extensions can be installed with PIE Example: An extension can be installed and enabled When I run a command to install an extension Then the extension should have been installed and enabled - - # pie install && pie install - Example: Re-installing an existing extension is a no-op - Given an extension was previously installed and enabled - When I run a command to install an extension - Then the extension should not have been re-installed - - # pie install && pie install --force - Example: Forcefully re-installing an existing extension should re-install the extension - Given an extension was previously installed and enabled - When I run a command to forcefully install an extension - Then the extension should have been installed and enabled diff --git a/features/reinstall-extensions.feature b/features/reinstall-extensions.feature new file mode 100644 index 00000000..f5b643be --- /dev/null +++ b/features/reinstall-extensions.feature @@ -0,0 +1,25 @@ +Feature: Extensions can be re-installed with PIE, but only if needed + + # pie download && pie install + Example: Installing a previously downloaded extension should install the extension + Given an extension was previously downloaded but not built + When I run a command to install an extension + Then the extension should have been installed and enabled + + # pie build && pie install + Example: Installing a previously built extension should install the extension + Given an extension was previously built but not installed + When I run a command to install an extension + Then the extension should have been installed and enabled + + # pie install && pie install + Example: Re-installing an existing extension is a no-op + Given an extension was previously installed and enabled + When I run a command to install an extension + Then the extension should not have been re-installed + + # pie install && pie install --force + Example: Forcefully re-installing an existing extension should re-install the extension + Given an extension was previously installed and enabled + When I run a command to forcefully install an extension + Then the extension should have been installed and enabled diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index c30bfe56..6557487f 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -106,7 +106,6 @@ public function runInstall( $localRepoPackage->getName(), $extName->name(), ), verbosity: IOInterface::VERBOSE); - continue; } if (! $installedJsonMetadata->isInstalled() && ! $installedJsonMetadata->isBuilt() && $installedJsonMetadata->isDownloaded()) { @@ -116,7 +115,6 @@ public function runInstall( $localRepoPackage->getName(), $extName->name(), ), verbosity: IOInterface::VERBOSE); - continue; } $this->arrayCollectionIo->write(sprintf( diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 73fb65c4..fb8b1b59 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -55,6 +55,7 @@ public function removeInstalledExtensions(): void } #[When('I run a command to download the latest version of an extension')] + #[Given('an extension was previously downloaded but not built')] public function iRunACommandToDownloadTheLatestVersionOfAnExtension(): void { $this->runPieCommand(['download', 'asgrim/example-pie-extension']); @@ -125,6 +126,7 @@ public function versionOfTheExtensionShouldHaveBeen(string $version): void } #[When('I run a command to build an extension')] + #[Given('an extension was previously built but not installed')] public function iRunACommandToBuildAnExtension(): void { $this->runPieCommand(['build', 'asgrim/example-pie-extension']); From 690e6f42c42f78dd5b769941968131877cbab704 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 29 May 2026 12:51:54 +0100 Subject: [PATCH 15/15] 586: restore code that limits install packge to thae requested package --- .../OverrideDownloadUrlInstallListener.php | 6 +-- .../PieComposerFactory.php | 3 +- .../PiePackageInstaller.php | 48 +++++++++---------- 3 files changed, 29 insertions(+), 28 deletions(-) diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php index 3dc57cd1..e0f41347 100644 --- a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -66,9 +66,9 @@ function (OperationInterface $operation): void { } // Install requests for other packages than the one we want should be ignored - // if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { - // return; - // } + if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + return; + } $piePackage = Package::fromComposerCompletePackage($composerPackage); $targetPlatform = $this->composerRequest->targetPlatform; diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index ac45c495..b7c2bb05 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -13,6 +13,7 @@ use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; +use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\ExtensionType; use Php\Pie\Platform; use Psr\Container\ContainerInterface; @@ -69,7 +70,7 @@ public static function createPieComposer( )); OverrideDownloadUrlInstallListener::selfRegister($composer, $io, $container, $composerRequest); -// RemoveUnrelatedInstallOperations::selfRegister($composer, $composerRequest); + RemoveUnrelatedInstallOperations::selfRegister($composer, $composerRequest); $composer->getConfig()->merge(['config' => ['__PIE_REQUEST__' => $composerRequest]]); $io->loadConfiguration($composer->getConfig()); diff --git a/src/ComposerIntegration/PiePackageInstaller.php b/src/ComposerIntegration/PiePackageInstaller.php index 4ee777e1..bec8b961 100644 --- a/src/ComposerIntegration/PiePackageInstaller.php +++ b/src/ComposerIntegration/PiePackageInstaller.php @@ -39,18 +39,18 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa ?->then(function () use ($composerPackage) { $io = $this->composerRequest->pieOutput; -// if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { -// $io->write( -// sprintf( -// 'Skipping %s install request from Composer as it was not the expected PIE package %s', -// $composerPackage->getName(), -// $this->composerRequest->requestedPackage->package, -// ), -// verbosity: IOInterface::VERY_VERBOSE, -// ); -// -// return null; -// } + if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + $io->write( + sprintf( + 'Skipping %s install request from Composer as it was not the expected PIE package %s', + $composerPackage->getName(), + $this->composerRequest->requestedPackage->package, + ), + verbosity: IOInterface::VERY_VERBOSE, + ); + + return null; + } if (! $composerPackage instanceof CompletePackageInterface) { $io->writeError(sprintf( @@ -81,18 +81,18 @@ public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $ ?->then(function () use ($composerPackage) { $io = $this->composerRequest->pieOutput; -// if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { -// $io->write( -// sprintf( -// 'Skipping %s uninstall request from Composer as it was not the expected PIE package %s', -// $composerPackage->getName(), -// $this->composerRequest->requestedPackage->package, -// ), -// verbosity: IOInterface::VERY_VERBOSE, -// ); -// -// return null; -// } + if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + $io->write( + sprintf( + 'Skipping %s uninstall request from Composer as it was not the expected PIE package %s', + $composerPackage->getName(), + $this->composerRequest->requestedPackage->package, + ), + verbosity: IOInterface::VERY_VERBOSE, + ); + + return null; + } if (! $composerPackage instanceof CompletePackageInterface) { $io->writeError(sprintf(