From d0eb650420a30caffc5416be48f17ece53120391 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 29 Jan 2025 21:07:49 +0000 Subject: [PATCH 01/35] Move Composer event listeners into namespace --- .../{ => Listeners}/OverrideWindowsUrlInstallListener.php | 3 ++- .../{ => Listeners}/RemoveUnrelatedInstallOperations.php | 3 ++- src/ComposerIntegration/PieComposerFactory.php | 2 ++ .../{ => Listeners}/OverrideWindowsUrlInstallListenerTest.php | 4 ++-- .../{ => Listeners}/RemoveUnrelatedInstallOperationsTest.php | 4 ++-- 5 files changed, 10 insertions(+), 6 deletions(-) rename src/ComposerIntegration/{ => Listeners}/OverrideWindowsUrlInstallListener.php (96%) rename src/ComposerIntegration/{ => Listeners}/RemoveUnrelatedInstallOperations.php (96%) rename test/unit/ComposerIntegration/{ => Listeners}/OverrideWindowsUrlInstallListenerTest.php (97%) rename test/unit/ComposerIntegration/{ => Listeners}/RemoveUnrelatedInstallOperationsTest.php (97%) diff --git a/src/ComposerIntegration/OverrideWindowsUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php similarity index 96% rename from src/ComposerIntegration/OverrideWindowsUrlInstallListener.php rename to src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php index 43e064a3..657f6059 100644 --- a/src/ComposerIntegration/OverrideWindowsUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Pie\ComposerIntegration; +namespace Php\Pie\ComposerIntegration\Listeners; use Composer\Composer; use Composer\DependencyResolver\Operation\InstallOperation; @@ -12,6 +12,7 @@ use Composer\Package\CompletePackageInterface; use Composer\Util\AuthHelper; use Composer\Util\HttpDownloader; +use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Platform\OperatingSystem; diff --git a/src/ComposerIntegration/RemoveUnrelatedInstallOperations.php b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php similarity index 96% rename from src/ComposerIntegration/RemoveUnrelatedInstallOperations.php rename to src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php index 874ce7ee..eb7a8a44 100644 --- a/src/ComposerIntegration/RemoveUnrelatedInstallOperations.php +++ b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\Pie\ComposerIntegration; +namespace Php\Pie\ComposerIntegration\Listeners; use Closure; use Composer\Composer; @@ -11,6 +11,7 @@ use Composer\DependencyResolver\Transaction; use Composer\Installer\InstallerEvent; use Composer\Installer\InstallerEvents; +use Php\Pie\ComposerIntegration\PieComposerRequest; use Symfony\Component\Console\Output\OutputInterface; use function array_filter; diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index 279348df..2207b7f7 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -11,6 +11,8 @@ use Composer\PartialComposer; use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; +use Php\Pie\ComposerIntegration\Listeners\OverrideWindowsUrlInstallListener; +use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\ExtensionType; use Php\Pie\Platform; use Psr\Container\ContainerInterface; diff --git a/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php similarity index 97% rename from test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php rename to test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php index 8e9d2697..115600fe 100644 --- a/test/unit/ComposerIntegration/OverrideWindowsUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\PieUnitTest\ComposerIntegration; +namespace Php\PieUnitTest\ComposerIntegration\Listeners; use Composer\Composer; use Composer\DependencyResolver\Transaction; @@ -11,7 +11,7 @@ use Composer\Installer\InstallerEvents; use Composer\IO\IOInterface; use Composer\Package\CompletePackage; -use Php\Pie\ComposerIntegration\OverrideWindowsUrlInstallListener; +use Php\Pie\ComposerIntegration\Listeners\OverrideWindowsUrlInstallListener; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; diff --git a/test/unit/ComposerIntegration/RemoveUnrelatedInstallOperationsTest.php b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php similarity index 97% rename from test/unit/ComposerIntegration/RemoveUnrelatedInstallOperationsTest.php rename to test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php index 0703e3b0..44dd5a95 100644 --- a/test/unit/ComposerIntegration/RemoveUnrelatedInstallOperationsTest.php +++ b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php @@ -2,7 +2,7 @@ declare(strict_types=1); -namespace Php\PieUnitTest\ComposerIntegration; +namespace Php\PieUnitTest\ComposerIntegration\Listeners; use Composer\Composer; use Composer\DependencyResolver\Operation\InstallOperation; @@ -15,7 +15,7 @@ use Composer\Package\CompletePackage; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; -use Php\Pie\ComposerIntegration\RemoveUnrelatedInstallOperations; +use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; From fa79308655a67e8ead3394b3132fd51a9c4317e9 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 29 Jan 2025 22:22:07 +0000 Subject: [PATCH 02/35] Externalise the possible asset names when fetching release assets --- .../OverrideWindowsUrlInstallListener.php | 10 ++++- .../GithubPackageReleaseAssets.php | 32 +++++++-------- src/Downloading/PackageReleaseAssets.php | 9 ++++- .../GithubPackageReleaseAssetsTest.php | 7 +++- .../OverrideWindowsUrlInstallListenerTest.php | 2 +- .../RemoveUnrelatedInstallOperationsTest.php | 2 +- .../GithubPackageReleaseAssetsTest.php | 40 +++++++++++++++++-- 7 files changed, 76 insertions(+), 26 deletions(-) diff --git a/src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php index 657f6059..17df0bf4 100644 --- a/src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php +++ b/src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php @@ -16,6 +16,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\WindowsExtensionAssetName; use Psr\Container\ContainerInterface; use Webmozart\Assert\Assert; @@ -62,12 +63,17 @@ public function __invoke(InstallerEvent $installerEvent): void $composerPackage = $operation->getPackage(); Assert::isInstanceOf($composerPackage, CompletePackageInterface::class, 'I can only handle %2$s, got %s'); + $piePackage = Package::fromComposerCompletePackage($composerPackage); $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); - $url = $packageReleaseAssets->findWindowsDownloadUrlForPackage( + $url = $packageReleaseAssets->findMatchingReleaseAssetUrl( $this->composerRequest->targetPlatform, - Package::fromComposerCompletePackage($composerPackage), + $piePackage, new AuthHelper($this->io, $this->composer->getConfig()), new HttpDownloader($this->io, $this->composer->getConfig()), + WindowsExtensionAssetName::zipNames( + $this->composerRequest->targetPlatform, + $piePackage, + ), ); $this->composerRequest->pieOutput->writeln('Found prebuilt archive: ' . $url); diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index 69f338d0..d2cde988 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -9,7 +9,6 @@ use Composer\Util\HttpDownloader; use Php\Pie\DependencyResolver\Package; use Php\Pie\Platform\TargetPlatform; -use Php\Pie\Platform\WindowsExtensionAssetName; use Webmozart\Assert\Assert; use function array_map; @@ -25,47 +24,48 @@ public function __construct( ) { } - /** @return non-empty-string */ - public function findWindowsDownloadUrlForPackage( + /** + * @param non-empty-list $possibleReleaseAssetNames + * + * @return non-empty-string + */ + public function findMatchingReleaseAssetUrl( TargetPlatform $targetPlatform, Package $package, AuthHelper $authHelper, HttpDownloader $httpDownloader, + array $possibleReleaseAssetNames, ): string { $releaseAsset = $this->selectMatchingReleaseAsset( - $targetPlatform, $package, $this->getReleaseAssetsForPackage($package, $authHelper, $httpDownloader), + $possibleReleaseAssetNames, ); return $releaseAsset['browser_download_url']; } - /** @return non-empty-list */ - private function expectedWindowsAssetNames(TargetPlatform $targetPlatform, Package $package): array - { - return WindowsExtensionAssetName::zipNames($targetPlatform, $package); - } - /** @link https://github.com/squizlabs/PHP_CodeSniffer/issues/3734 */ // phpcs:disable Squiz.Commenting.FunctionComment.MissingParamName /** * @param list $releaseAssets + * @param non-empty-list $possibleReleaseAssetNames * * @return array{name: non-empty-string, browser_download_url: non-empty-string, ...} */ // phpcs:enable - private function selectMatchingReleaseAsset(TargetPlatform $targetPlatform, Package $package, array $releaseAssets): array - { - $expectedAssetNames = $this->expectedWindowsAssetNames($targetPlatform, $package); - + private function selectMatchingReleaseAsset( + Package $package, + array $releaseAssets, + array $possibleReleaseAssetNames, + ): array { foreach ($releaseAssets as $releaseAsset) { - if (in_array(strtolower($releaseAsset['name']), $expectedAssetNames, true)) { + if (in_array(strtolower($releaseAsset['name']), $possibleReleaseAssetNames, true)) { return $releaseAsset; } } - throw Exception\CouldNotFindReleaseAsset::forPackage($package, $expectedAssetNames); + throw Exception\CouldNotFindReleaseAsset::forPackage($package, $possibleReleaseAssetNames); } /** @return list */ diff --git a/src/Downloading/PackageReleaseAssets.php b/src/Downloading/PackageReleaseAssets.php index 5f79cc37..4e498858 100644 --- a/src/Downloading/PackageReleaseAssets.php +++ b/src/Downloading/PackageReleaseAssets.php @@ -12,11 +12,16 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface PackageReleaseAssets { - /** @return non-empty-string */ - public function findWindowsDownloadUrlForPackage( + /** + * @param non-empty-list $possibleReleaseAssetNames + * + * @return non-empty-string + */ + public function findMatchingReleaseAssetUrl( TargetPlatform $targetPlatform, Package $package, AuthHelper $authHelper, HttpDownloader $httpDownloader, + array $possibleReleaseAssetNames, ): string; } diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php index 1bd9d56a..664b1cb9 100644 --- a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php @@ -20,6 +20,7 @@ use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\ThreadSafetyMode; use Php\Pie\Platform\WindowsCompiler; +use Php\Pie\Platform\WindowsExtensionAssetName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -77,11 +78,15 @@ public function testDeterminingReleaseAssetUrlForWindows(): void self::assertSame( 'https://github.com/asgrim/example-pie-extension/releases/download/2.0.2/php_example_pie_extension-2.0.2-8.3-ts-vs16-x86_64.zip', (new GithubPackageReleaseAssets('https://api.github.com')) - ->findWindowsDownloadUrlForPackage( + ->findMatchingReleaseAssetUrl( $targetPlatform, $package, new AuthHelper($io, $config), new HttpDownloader($io, $config), + WindowsExtensionAssetName::zipNames( + $targetPlatform, + $package, + ), ), ); } diff --git a/test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php index 115600fe..942049c7 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php @@ -157,7 +157,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); $packageReleaseAssets ->expects(self::once()) - ->method('findWindowsDownloadUrlForPackage') + ->method('findMatchingReleaseAssetUrl') ->willReturn('https://example.com/windows-download-url'); $this->container diff --git a/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php index 44dd5a95..f251c42a 100644 --- a/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php +++ b/test/unit/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperationsTest.php @@ -13,9 +13,9 @@ use Composer\Installer\InstallerEvents; use Composer\IO\IOInterface; use Composer\Package\CompletePackage; +use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; -use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index 34d8e25a..a6cf7cf1 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -21,6 +21,7 @@ use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\ThreadSafetyMode; use Php\Pie\Platform\WindowsCompiler; +use Php\Pie\Platform\WindowsExtensionAssetName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -89,7 +90,19 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); - self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper, $httpDownloader)); + self::assertSame( + 'actual_download_url', + $releaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $package, + $authHelper, + $httpDownloader, + WindowsExtensionAssetName::zipNames( + $targetPlatform, + $package, + ), + ), + ); } public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThreadSafetySwapped(): void @@ -152,7 +165,19 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); - self::assertSame('actual_download_url', $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper, $httpDownloader)); + self::assertSame( + 'actual_download_url', + $releaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $package, + $authHelper, + $httpDownloader, + WindowsExtensionAssetName::zipNames( + $targetPlatform, + $package, + ), + ), + ); } public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotFound(): void @@ -197,6 +222,15 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); $this->expectException(CouldNotFindReleaseAsset::class); - $releaseAssets->findWindowsDownloadUrlForPackage($targetPlatform, $package, $authHelper, $httpDownloader); + $releaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $package, + $authHelper, + $httpDownloader, + WindowsExtensionAssetName::zipNames( + $targetPlatform, + $package, + ), + ); } } From 48cab2f113d58bf5412cea5ef13cad53e1811b2f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 30 Jan 2025 11:57:23 +0000 Subject: [PATCH 03/35] Update download URL override to support different DownloadUrlMethod values --- .../OverrideDownloadUrlInstallListener.php | 110 ++++++++++++++++++ .../OverrideWindowsUrlInstallListener.php | 82 ------------- .../PieComposerFactory.php | 4 +- src/Downloading/DownloadUrlMethod.php | 50 ++++++++ src/Platform/PrePackagedSourceAssetName.php | 42 +++++++ ...verrideDownloadUrlInstallListenerTest.php} | 90 ++++++++++++-- .../Downloading/DownloadUrlMethodTest.php | 28 +++++ .../PrePackagedSourceAssetNameTest.php | 18 +++ 8 files changed, 333 insertions(+), 91 deletions(-) create mode 100644 src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php delete mode 100644 src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php create mode 100644 src/Downloading/DownloadUrlMethod.php create mode 100644 src/Platform/PrePackagedSourceAssetName.php rename test/unit/ComposerIntegration/Listeners/{OverrideWindowsUrlInstallListenerTest.php => OverrideDownloadUrlInstallListenerTest.php} (67%) create mode 100644 test/unit/Downloading/DownloadUrlMethodTest.php create mode 100644 test/unit/Platform/PrePackagedSourceAssetNameTest.php diff --git a/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php new file mode 100644 index 00000000..fc8a6cdc --- /dev/null +++ b/src/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListener.php @@ -0,0 +1,110 @@ +getEventDispatcher() + ->addListener( + InstallerEvents::PRE_OPERATIONS_EXEC, + new self($composer, $io, $container, $composerRequest), + ); + } + + public function __invoke(InstallerEvent $installerEvent): void + { + /** @psalm-suppress InternalMethod */ + $operations = $installerEvent->getTransaction()?->getOperations() ?? []; + + array_walk( + $operations, + function (OperationInterface $operation): void { + if (! $operation instanceof InstallOperation) { + return; + } + + $composerPackage = $operation->getPackage(); + if (! $composerPackage instanceof CompletePackageInterface) { + return; + } + + // Install requests for other packages than the one we want should be ignored + if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + return; + } + + $piePackage = Package::fromComposerCompletePackage($composerPackage); + $targetPlatform = $this->composerRequest->targetPlatform; + $downloadUrlMethod = DownloadUrlMethod::fromPackage($piePackage, $targetPlatform); + + // Exit early if we should just use Composer's normal download + if ($downloadUrlMethod === DownloadUrlMethod::ComposerDefaultDownload) { + return; + } + + $possibleAssetNames = $downloadUrlMethod->possibleAssetNames($piePackage, $targetPlatform); + if ($possibleAssetNames === null) { + return; + } + + // @todo https://github.com/php/pie/issues/138 will need to depend on the repo type (GH/GL/BB/etc.) + $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); + + $url = $packageReleaseAssets->findMatchingReleaseAssetUrl( + $targetPlatform, + $piePackage, + new AuthHelper($this->io, $this->composer->getConfig()), + new HttpDownloader($this->io, $this->composer->getConfig()), + $possibleAssetNames, + ); + + $this->composerRequest->pieOutput->writeln('Found prebuilt archive: ' . $url); + $composerPackage->setDistUrl($url); + + if (pathinfo($url, PATHINFO_EXTENSION) !== 'tgz') { + return; + } + + $composerPackage->setDistType('tar'); + }, + ); + } +} diff --git a/src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php b/src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php deleted file mode 100644 index 17df0bf4..00000000 --- a/src/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListener.php +++ /dev/null @@ -1,82 +0,0 @@ -getEventDispatcher() - ->addListener( - InstallerEvents::PRE_OPERATIONS_EXEC, - new self($composer, $io, $container, $composerRequest), - ); - } - - public function __invoke(InstallerEvent $installerEvent): void - { - if ($this->composerRequest->targetPlatform->operatingSystem !== OperatingSystem::Windows) { - return; - } - - /** @psalm-suppress InternalMethod */ - $operations = $installerEvent->getTransaction()?->getOperations() ?? []; - - Assert::count($operations, 1, 'I can only do exactly %d thing at once, %d attempted'); - $operation = reset($operations); - Assert::isInstanceOf($operation, InstallOperation::class, 'I can only handle %2$s, got %s'); - - $composerPackage = $operation->getPackage(); - Assert::isInstanceOf($composerPackage, CompletePackageInterface::class, 'I can only handle %2$s, got %s'); - - $piePackage = Package::fromComposerCompletePackage($composerPackage); - $packageReleaseAssets = $this->container->get(PackageReleaseAssets::class); - $url = $packageReleaseAssets->findMatchingReleaseAssetUrl( - $this->composerRequest->targetPlatform, - $piePackage, - new AuthHelper($this->io, $this->composer->getConfig()), - new HttpDownloader($this->io, $this->composer->getConfig()), - WindowsExtensionAssetName::zipNames( - $this->composerRequest->targetPlatform, - $piePackage, - ), - ); - - $this->composerRequest->pieOutput->writeln('Found prebuilt archive: ' . $url); - $composerPackage->setDistUrl($url); - } -} diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index 2207b7f7..7acb2352 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -11,7 +11,7 @@ use Composer\PartialComposer; use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; -use Php\Pie\ComposerIntegration\Listeners\OverrideWindowsUrlInstallListener; +use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; use Php\Pie\ComposerIntegration\Listeners\RemoveUnrelatedInstallOperations; use Php\Pie\ExtensionType; use Php\Pie\Platform; @@ -61,7 +61,7 @@ public static function createPieComposer( true, ); - OverrideWindowsUrlInstallListener::selfRegister($composer, $io, $container, $composerRequest); + OverrideDownloadUrlInstallListener::selfRegister($composer, $io, $container, $composerRequest); RemoveUnrelatedInstallOperations::selfRegister($composer, $composerRequest); $composer->getConfig()->merge(['config' => ['__PIE_REQUEST__' => $composerRequest]]); diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php new file mode 100644 index 00000000..2b270858 --- /dev/null +++ b/src/Downloading/DownloadUrlMethod.php @@ -0,0 +1,50 @@ +|null */ + public function possibleAssetNames(Package $package, TargetPlatform $targetPlatform): array|null + { + return match ($this) { + self::WindowsBinaryDownload => WindowsExtensionAssetName::zipNames($targetPlatform, $package), + self::PrePackagedSourceDownload => PrePackagedSourceAssetName::packageNames($package), + self::ComposerDefaultDownload => null, + }; + } + + public static function fromPackage(Package $package, TargetPlatform $targetPlatform): self + { + /** + * PIE does not support building on Windows (yet, at least). Maintainers + * should provide pre-built Windows binaries. + */ + if ($targetPlatform->operatingSystem === OperatingSystem::Windows) { + return self::WindowsBinaryDownload; + } + + /** + * Some packages pre-package source code (e.g. mongodb) as there are + * external dependencies in Git submodules that otherwise aren't + * included in GitHub/Gitlab/etc "dist" downloads + */ + if (false) { // @todo check $package `php-ext` + return self::PrePackagedSourceDownload; + } + + return self::ComposerDefaultDownload; + } +} diff --git a/src/Platform/PrePackagedSourceAssetName.php b/src/Platform/PrePackagedSourceAssetName.php new file mode 100644 index 00000000..139e1aee --- /dev/null +++ b/src/Platform/PrePackagedSourceAssetName.php @@ -0,0 +1,42 @@ + */ + public static function packageNames(Package $package): array + { + return [ + strtolower(sprintf( + 'php_%s-%s-src.tgz', + $package->extensionName->name(), + $package->version, + )), + strtolower(sprintf( + 'php_%s-%s-src.zip', + $package->extensionName->name(), + $package->version, + )), + // @todo remove this: + strtolower(sprintf( + '%s-%s.tgz', + $package->extensionName->name(), + $package->version, + )), + ]; + } +} diff --git a/test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php similarity index 67% rename from test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php rename to test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 942049c7..4e215bd5 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideWindowsUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -11,7 +11,7 @@ use Composer\Installer\InstallerEvents; use Composer\IO\IOInterface; use Composer\Package\CompletePackage; -use Php\Pie\ComposerIntegration\Listeners\OverrideWindowsUrlInstallListener; +use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; @@ -29,8 +29,8 @@ use Psr\Container\ContainerInterface; use Symfony\Component\Console\Output\OutputInterface; -#[CoversClass(OverrideWindowsUrlInstallListener::class)] -final class OverrideWindowsUrlInstallListenerTest extends TestCase +#[CoversClass(OverrideDownloadUrlInstallListener::class)] +final class OverrideDownloadUrlInstallListenerTest extends TestCase { private Composer&MockObject $composer; private IOInterface&MockObject $io; @@ -53,7 +53,7 @@ public function testEventListenerRegistration(): void ->method('addListener') ->with( InstallerEvents::PRE_OPERATIONS_EXEC, - self::isInstanceOf(OverrideWindowsUrlInstallListener::class), + self::isInstanceOf(OverrideDownloadUrlInstallListener::class), ); $this->composer @@ -61,7 +61,7 @@ public function testEventListenerRegistration(): void ->method('getEventDispatcher') ->willReturn($eventDispatcher); - OverrideWindowsUrlInstallListener::selfRegister( + OverrideDownloadUrlInstallListener::selfRegister( $this->composer, $this->io, $this->container, @@ -85,6 +85,21 @@ public function testEventListenerRegistration(): void ); } + public function testNonInstallOperationsAreIgnored(): void + { + self::markTestIncomplete('todo'); // @todo + } + + public function testNonCompletePackagesAreIgnored(): void + { + self::markTestIncomplete('todo'); // @todo + } + + public function testInstallOperationsForDifferentPackagesAreIgnored(): void + { + self::markTestIncomplete('todo'); // @todo + } + public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void { $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); @@ -107,7 +122,7 @@ public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void ->expects(self::never()) ->method('get'); - (new OverrideWindowsUrlInstallListener( + (new OverrideDownloadUrlInstallListener( $this->composer, $this->io, $this->container, @@ -165,7 +180,7 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void ->with(PackageReleaseAssets::class) ->willReturn($packageReleaseAssets); - (new OverrideWindowsUrlInstallListener( + (new OverrideDownloadUrlInstallListener( $this->composer, $this->io, $this->container, @@ -193,4 +208,65 @@ public function testDistUrlIsUpdatedForWindowsInstallers(): void $composerPackage->getDistUrl(), ); } + + public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void + { + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistType('zip'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + $composerPackage->setPhpExt(['extension-name' => 'mongodb']); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $packageReleaseAssets = $this->createMock(PackageReleaseAssets::class); + $packageReleaseAssets + ->expects(self::once()) + ->method('findMatchingReleaseAssetUrl') + ->willReturn('https://example.com/pre-packaged-source-download-url.tgz'); + + $this->container + ->method('get') + ->with(PackageReleaseAssets::class) + ->willReturn($packageReleaseAssets); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); + + self::assertSame( + 'https://example.com/pre-packaged-source-download-url.tgz', + $composerPackage->getDistUrl(), + ); + self::assertSame('tar', $composerPackage->getDistType()); + } } diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php new file mode 100644 index 00000000..53da6c5d --- /dev/null +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -0,0 +1,28 @@ + Date: Fri, 31 Jan 2025 10:34:55 +0000 Subject: [PATCH 04/35] Refactor PIE Package VO to improve the API --- src/Building/UnixBuild.php | 2 +- src/Command/BuildCommand.php | 2 +- src/Command/CommandHelper.php | 4 +- src/Command/DownloadCommand.php | 2 +- src/Command/InfoCommand.php | 16 +- src/Command/InstallCommand.php | 2 +- src/Command/ShowCommand.php | 4 +- .../ComposerIntegrationHandler.php | 4 +- src/DependencyResolver/Package.php | 158 +++++++++++++----- .../ResolveDependencyWithComposer.php | 12 +- src/Downloading/DownloadedPackage.php | 4 +- .../GithubPackageReleaseAssets.php | 6 +- .../Ini/AddExtensionToTheIniFile.php | 14 +- .../Ini/CheckAndAddExtensionToIniIfNeeded.php | 6 +- src/Installing/Ini/DockerPhpExtEnable.php | 8 +- src/Installing/Ini/OndrejPhpenmod.php | 6 +- .../Ini/PreCheckExtensionAlreadyLoaded.php | 2 +- .../Ini/StandardAdditionalPhpIniDirectory.php | 4 +- src/Installing/SetupIniFile.php | 4 +- src/Installing/UnixInstall.php | 2 +- src/Installing/WindowsInstall.php | 8 +- src/Platform/PrePackagedSourceAssetName.php | 12 +- src/Platform/WindowsExtensionAssetName.php | 8 +- test/integration/Building/UnixBuildTest.php | 51 +----- .../ResolveDependencyWithComposerTest.php | 6 +- .../GithubPackageReleaseAssetsTest.php | 7 - .../Installing/UnixInstallTest.php | 8 - .../Installing/WindowsInstallTest.php | 7 - test/unit/Command/CommandHelperTest.php | 33 ++-- test/unit/DependencyResolver/PackageTest.php | 45 +++-- .../ResolveDependencyWithComposerTest.php | 8 +- .../Downloading/DownloadedPackageTest.php | 29 +--- .../CouldNotFindReleaseAssetTest.php | 14 -- .../GithubPackageReleaseAssetsTest.php | 21 --- .../Ini/AddExtensionToTheIniFileTest.php | 39 +---- .../CheckAndAddExtensionToIniIfNeededTest.php | 25 +-- .../Installing/Ini/DockerPhpExtEnableTest.php | 13 +- .../Installing/Ini/OndrejPhpenmodTest.php | 7 - .../Ini/PickBestSetupIniApproachTest.php | 21 --- .../PreCheckExtensionAlreadyLoadedTest.php | 13 +- .../StandardAdditionalPhpIniDirectoryTest.php | 13 +- .../Ini/StandardSinglePhpIniTest.php | 7 - .../WindowsExtensionAssetNameTest.php | 7 - 43 files changed, 251 insertions(+), 413 deletions(-) diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php index 6e7c9942..3130407d 100644 --- a/src/Building/UnixBuild.php +++ b/src/Building/UnixBuild.php @@ -76,7 +76,7 @@ public function __invoke( $this->make($targetPlatform, $downloadedPackage, $output, $outputCallback); - $expectedSoFile = $downloadedPackage->extractedSourcePath . '/modules/' . $downloadedPackage->package->extensionName->name() . '.so'; + $expectedSoFile = $downloadedPackage->extractedSourcePath . '/modules/' . $downloadedPackage->package->extensionName()->name() . '.so'; if (! file_exists($expectedSoFile)) { throw ExtensionBinaryNotFound::fromExpectedBinary($expectedSoFile); diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 09983648..b118d682 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -64,7 +64,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersion, $forceInstallPackageVersion, ); - $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); + $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); // Now we know what package we have, we can validate the configure options for the command and re-create the // Composer instance with the populated configure options diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 1fc4c7a9..8614fd2f 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -226,7 +226,7 @@ public static function requestedNameAndVersionPair(InputInterface $input): Reque public static function bindConfigureOptionsFromPackage(Command $command, Package $package, InputInterface $input): void { - foreach ($package->configureOptions as $configureOption) { + foreach ($package->configureOptions() as $configureOption) { $command->addOption( $configureOption->name, null, @@ -242,7 +242,7 @@ public static function bindConfigureOptionsFromPackage(Command $command, Package public static function processConfigureOptionsFromInput(Package $package, InputInterface $input): array { $configureOptionsValues = []; - foreach ($package->configureOptions as $configureOption) { + foreach ($package->configureOptions() as $configureOption) { if (! $input->hasOption($configureOption->name)) { continue; } diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 4a7439b1..7d27adde 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -66,7 +66,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersion, $forceInstallPackageVersion, ); - $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); + $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); try { ($this->composerIntegrationHandler)( diff --git a/src/Command/InfoCommand.php b/src/Command/InfoCommand.php index 0d03e934..4cb56949 100644 --- a/src/Command/InfoCommand.php +++ b/src/Command/InfoCommand.php @@ -64,17 +64,17 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersion, CommandHelper::determineForceInstallingPackageVersion($input), ); - $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); + $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); - $output->writeln(sprintf('Extension name: %s', $package->extensionName->name())); - $output->writeln(sprintf('Extension type: %s (%s)', $package->extensionType->value, $package->extensionType->name)); - $output->writeln(sprintf('Composer package name: %s', $package->name)); - $output->writeln(sprintf('Version: %s', $package->version)); - $output->writeln(sprintf('Download URL: %s', $package->downloadUrl ?? '(not specified)')); + $output->writeln(sprintf('Extension name: %s', $package->extensionName()->name())); + $output->writeln(sprintf('Extension type: %s (%s)', $package->extensionType()->value, $package->extensionType()->name)); + $output->writeln(sprintf('Composer package name: %s', $package->name())); + $output->writeln(sprintf('Version: %s', $package->version())); + $output->writeln(sprintf('Download URL: %s', $package->downloadUrl() ?? '(not specified)')); - if (count($package->configureOptions)) { + if (count($package->configureOptions())) { $output->writeln('Configure options:'); - foreach ($package->configureOptions as $configureOption) { + foreach ($package->configureOptions() as $configureOption) { $output->writeln(sprintf(' --%s%s (%s)', $configureOption->name, $configureOption->needsValue ? '=?' : '', $configureOption->description)); } } else { diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 0640b5e4..068f3c36 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -69,7 +69,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $requestedNameAndVersion, $forceInstallPackageVersion, ); - $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName->nameWithExtPrefix())); + $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); // Now we know what package we have, we can validate the configure options for the command and re-create the // Composer instance with the populated configure options diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index cb750f11..2c07282d 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -80,7 +80,7 @@ static function (string $version, string $phpExtensionName) use ($output, $piePa $extensionPath, $phpExtensionName, $extensionEnding, - PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($piePackage->composerPackage), + PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($piePackage->composerPackage()), ), )); }, @@ -152,7 +152,7 @@ static function (BasePackage $basePackage): bool { array_map( /** @return non-empty-string */ static function (Package $package): string { - return $package->extensionName->name(); + return $package->extensionName()->name(); }, $composerInstalledPackages, ), diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index 48d93aa8..d9b8f7b2 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -39,7 +39,7 @@ public function __invoke( // If user did not request a specific require version, use Composer to recommend one for the pie.json if ($recommendedRequireVersion === null) { - $recommendedRequireVersion = $versionSelector->findRecommendedRequireVersion($package->composerPackage); + $recommendedRequireVersion = $versionSelector->findRecommendedRequireVersion($package->composerPackage()); } // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file @@ -60,7 +60,7 @@ public function __invoke( $composerInstaller = PieComposerInstaller::createWithPhpBinary( $targetPlatform->phpBinaryPath, - $package->extensionName, + $package->extensionName(), $this->arrayCollectionIo, $composer, ); diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index d09a8a0d..94519ff0 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -26,53 +26,62 @@ * @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks * * @immutable + * + * @psalm-suppress PropertyNotSetInConstructor */ final class Package { - /** - * @param list $configureOptions - * @param non-empty-list|null $compatibleOsFamilies - * @param non-empty-list|null $incompatibleOsFamilies - */ + /** @var list */ + private array $configureOptions; + private int $priority; + private string|null $buildPath; + /** @var non-empty-list|null */ + private array|null $compatibleOsFamilies; + /** @var non-empty-list|null */ + private array|null $incompatibleOsFamilies; + private bool $supportZts; + private bool $supportNts; + public function __construct( - public readonly CompletePackageInterface $composerPackage, - public readonly ExtensionType $extensionType, - public readonly ExtensionName $extensionName, - public readonly string $name, - public readonly string $version, - public readonly string|null $downloadUrl, - public readonly array $configureOptions, - public readonly bool $supportZts, - public readonly bool $supportNts, - public readonly string|null $buildPath, - public readonly array|null $compatibleOsFamilies, - public readonly array|null $incompatibleOsFamilies, - public readonly int $priority, + private readonly CompletePackageInterface $composerPackage, + private readonly ExtensionType $extensionType, + private readonly ExtensionName $extensionName, + private readonly string $name, + private readonly string $version, + private readonly string|null $downloadUrl, ) { + $this->configureOptions = []; + $this->supportZts = true; + $this->supportNts = true; + $this->buildPath = null; + $this->compatibleOsFamilies = null; + $this->incompatibleOsFamilies = null; + $this->priority = 80; } public static function fromComposerCompletePackage(CompletePackageInterface $completePackage): self { + $package = new self( + $completePackage, + ExtensionType::tryFrom($completePackage->getType()) ?? ExtensionType::PhpModule, + ExtensionName::determineFromComposerPackage($completePackage), + $completePackage->getPrettyName(), + $completePackage->getPrettyVersion(), + $completePackage->getDistUrl(), + ); + $phpExtOptions = $completePackage->getPhpExt(); - $configureOptions = $phpExtOptions !== null && array_key_exists('configure-options', $phpExtOptions) + $package->configureOptions = $phpExtOptions !== null && array_key_exists('configure-options', $phpExtOptions) ? array_map( static fn (array $configureOption): ConfigureOption => ConfigureOption::fromComposerJsonDefinition($configureOption), $phpExtOptions['configure-options'], ) : []; - $supportZts = $phpExtOptions !== null && array_key_exists('support-zts', $phpExtOptions) - ? $phpExtOptions['support-zts'] - : true; - - $supportNts = $phpExtOptions !== null && array_key_exists('support-nts', $phpExtOptions) - ? $phpExtOptions['support-nts'] - : true; - - $buildPath = $phpExtOptions !== null && array_key_exists('build-path', $phpExtOptions) - ? $phpExtOptions['build-path'] - : null; + $package->supportZts = $phpExtOptions['support-zts'] ?? true; + $package->supportNts = $phpExtOptions['support-nts'] ?? true; + $package->buildPath = $phpExtOptions['build-path'] ?? null; $compatibleOsFamilies = $phpExtOptions['os-families'] ?? null; $incompatibleOsFamilies = $phpExtOptions['os-families-exclude'] ?? null; @@ -81,21 +90,12 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com throw new InvalidArgumentException('Cannot specify both "os-families" and "os-families-exclude" in composer.json'); } - return new self( - $completePackage, - ExtensionType::tryFrom($completePackage->getType()) ?? ExtensionType::PhpModule, - ExtensionName::determineFromComposerPackage($completePackage), - $completePackage->getPrettyName(), - $completePackage->getPrettyVersion(), - $completePackage->getDistUrl(), - $configureOptions, - $supportZts, - $supportNts, - $buildPath, - self::convertInputStringsToOperatingSystemFamilies($compatibleOsFamilies), - self::convertInputStringsToOperatingSystemFamilies($incompatibleOsFamilies), - $phpExtOptions['priority'] ?? 80, - ); + $package->compatibleOsFamilies = self::convertInputStringsToOperatingSystemFamilies($compatibleOsFamilies); + $package->incompatibleOsFamilies = self::convertInputStringsToOperatingSystemFamilies($incompatibleOsFamilies); + + $package->priority = $phpExtOptions['priority'] ?? 80; + + return $package; } public function prettyNameAndVersion(): string @@ -146,4 +146,72 @@ private static function convertInputStringsToOperatingSystemFamilies(array|null return $osFamilies; } + + public function composerPackage(): CompletePackageInterface + { + return $this->composerPackage; + } + + public function extensionType(): ExtensionType + { + return $this->extensionType; + } + + public function extensionName(): ExtensionName + { + return $this->extensionName; + } + + public function name(): string + { + return $this->name; + } + + public function version(): string + { + return $this->version; + } + + /** @return list */ + public function configureOptions(): array + { + return $this->configureOptions; + } + + public function downloadUrl(): string|null + { + return $this->downloadUrl; + } + + public function priority(): int + { + return $this->priority; + } + + public function buildPath(): string|null + { + return $this->buildPath; + } + + /** @return non-empty-list|null */ + public function compatibleOsFamilies(): array|null + { + return $this->compatibleOsFamilies; + } + + /** @return non-empty-list|null */ + public function incompatibleOsFamilies(): array|null + { + return $this->incompatibleOsFamilies; + } + + public function supportZts(): bool + { + return $this->supportZts; + } + + public function supportNts(): bool + { + return $this->supportNts; + } } diff --git a/src/DependencyResolver/ResolveDependencyWithComposer.php b/src/DependencyResolver/ResolveDependencyWithComposer.php index d2bdbd28..9e4fe603 100644 --- a/src/DependencyResolver/ResolveDependencyWithComposer.php +++ b/src/DependencyResolver/ResolveDependencyWithComposer.php @@ -70,27 +70,27 @@ public function __invoke( private function assertCompatibleThreadSafetyMode(ThreadSafetyMode $threadSafetyMode, Package $resolvedPackage): void { - if ($threadSafetyMode === ThreadSafetyMode::NonThreadSafe && ! $resolvedPackage->supportNts) { + if ($threadSafetyMode === ThreadSafetyMode::NonThreadSafe && ! $resolvedPackage->supportNts()) { throw IncompatibleThreadSafetyMode::ztsExtensionOnNtsPlatform(); } - if ($threadSafetyMode === ThreadSafetyMode::ThreadSafe && ! $resolvedPackage->supportZts) { + if ($threadSafetyMode === ThreadSafetyMode::ThreadSafe && ! $resolvedPackage->supportZts()) { throw IncompatibleThreadSafetyMode::ntsExtensionOnZtsPlatform(); } } private function assertCompatibleOsFamily(TargetPlatform $targetPlatform, Package $resolvedPackage): void { - if ($resolvedPackage->compatibleOsFamilies !== null && ! in_array($targetPlatform->operatingSystemFamily, $resolvedPackage->compatibleOsFamilies, true)) { + if ($resolvedPackage->compatibleOsFamilies() !== null && ! in_array($targetPlatform->operatingSystemFamily, $resolvedPackage->compatibleOsFamilies(), true)) { throw IncompatibleOperatingSystemFamily::notInCompatibleOperatingSystemFamilies( - $resolvedPackage->compatibleOsFamilies, + $resolvedPackage->compatibleOsFamilies(), $targetPlatform->operatingSystemFamily, ); } - if ($resolvedPackage->incompatibleOsFamilies !== null && in_array($targetPlatform->operatingSystemFamily, $resolvedPackage->incompatibleOsFamilies, true)) { + if ($resolvedPackage->incompatibleOsFamilies() !== null && in_array($targetPlatform->operatingSystemFamily, $resolvedPackage->incompatibleOsFamilies(), true)) { throw IncompatibleOperatingSystemFamily::inIncompatibleOperatingSystemFamily( - $resolvedPackage->incompatibleOsFamilies, + $resolvedPackage->incompatibleOsFamilies(), $targetPlatform->operatingSystemFamily, ); } diff --git a/src/Downloading/DownloadedPackage.php b/src/Downloading/DownloadedPackage.php index 3f6adeb8..ba57c4b7 100644 --- a/src/Downloading/DownloadedPackage.php +++ b/src/Downloading/DownloadedPackage.php @@ -26,8 +26,8 @@ private function __construct( public static function fromPackageAndExtractedPath(Package $package, string $extractedSourcePath): self { - if ($package->buildPath !== null) { - $extractedSourcePathWithBuildPath = realpath($extractedSourcePath . DIRECTORY_SEPARATOR . $package->buildPath); + if ($package->buildPath() !== null) { + $extractedSourcePathWithBuildPath = realpath($extractedSourcePath . DIRECTORY_SEPARATOR . $package->buildPath()); if (is_string($extractedSourcePathWithBuildPath)) { $extractedSourcePath = $extractedSourcePathWithBuildPath; diff --git a/src/Downloading/GithubPackageReleaseAssets.php b/src/Downloading/GithubPackageReleaseAssets.php index d2cde988..4b526ae0 100644 --- a/src/Downloading/GithubPackageReleaseAssets.php +++ b/src/Downloading/GithubPackageReleaseAssets.php @@ -74,16 +74,16 @@ private function getReleaseAssetsForPackage( AuthHelper $authHelper, HttpDownloader $httpDownloader, ): array { - Assert::notNull($package->downloadUrl); + Assert::notNull($package->downloadUrl()); try { $decodedRepsonse = $httpDownloader->get( - $this->githubApiBaseUrl . '/repos/' . $package->githubOrgAndRepository() . '/releases/tags/' . $package->version, + $this->githubApiBaseUrl . '/repos/' . $package->githubOrgAndRepository() . '/releases/tags/' . $package->version(), [ 'retry-auth-failure' => false, 'http' => [ 'method' => 'GET', - 'header' => $authHelper->addAuthenticationHeader([], $this->githubApiBaseUrl, $package->downloadUrl), + 'header' => $authHelper->addAuthenticationHeader([], $this->githubApiBaseUrl, $package->downloadUrl()), ], ], )->decodeJson(); diff --git a/src/Installing/Ini/AddExtensionToTheIniFile.php b/src/Installing/Ini/AddExtensionToTheIniFile.php index 73469e9e..f4cece32 100644 --- a/src/Installing/Ini/AddExtensionToTheIniFile.php +++ b/src/Installing/Ini/AddExtensionToTheIniFile.php @@ -63,7 +63,7 @@ public function __invoke( $output->writeln( sprintf( 'Enabled extension %s in the INI file %s', - $package->extensionName->name(), + $package->extensionName()->name(), $ini, ), OutputInterface::VERBOSITY_VERBOSE, @@ -73,7 +73,7 @@ public function __invoke( return false; } - $phpBinaryPath->assertExtensionIsLoadedInRuntime($package->extensionName, $output); + $phpBinaryPath->assertExtensionIsLoadedInRuntime($package->extensionName(), $output); return true; } catch (Throwable $anything) { @@ -81,7 +81,7 @@ public function __invoke( $output->writeln(sprintf( 'Something went wrong enabling the %s extension: %s', - $package->extensionName->name(), + $package->extensionName()->name(), $anything->getMessage(), )); @@ -93,10 +93,10 @@ public function __invoke( private function iniFileContent(Package $package): string { return PHP_EOL - . '; PIE automatically added this to enable the ' . $package->name . ' extension' . PHP_EOL - . '; priority=' . $package->priority . PHP_EOL - . ($package->extensionType === ExtensionType::PhpModule ? 'extension' : 'zend_extension') + . '; PIE automatically added this to enable the ' . $package->name() . ' extension' . PHP_EOL + . '; priority=' . $package->priority() . PHP_EOL + . ($package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension') . '=' - . $package->extensionName->name() . PHP_EOL; + . $package->extensionName()->name() . PHP_EOL; } } diff --git a/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php b/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php index eec39db0..1c3a57e7 100644 --- a/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php +++ b/src/Installing/Ini/CheckAndAddExtensionToIniIfNeeded.php @@ -45,7 +45,7 @@ public function __invoke( return false; } - if (($this->isExtensionAlreadyInTheIniFile)($iniFile, $downloadedPackage->package->extensionName)) { + if (($this->isExtensionAlreadyInTheIniFile)($iniFile, $downloadedPackage->package->extensionName())) { $output->writeln( sprintf( 'Extension is already enabled in the INI file %s', @@ -59,13 +59,13 @@ public function __invoke( } try { - $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime($downloadedPackage->package->extensionName, $output); + $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime($downloadedPackage->package->extensionName(), $output); return true; } catch (Throwable $anything) { $output->writeln(sprintf( 'Something went wrong verifying the %s extension is enabled: %s', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), $anything->getMessage(), )); diff --git a/src/Installing/Ini/DockerPhpExtEnable.php b/src/Installing/Ini/DockerPhpExtEnable.php index 2878af4d..195b80b1 100644 --- a/src/Installing/Ini/DockerPhpExtEnable.php +++ b/src/Installing/Ini/DockerPhpExtEnable.php @@ -41,12 +41,12 @@ public function setup( } try { - $enableOutput = Process::run([$dockerPhpExtEnable, $downloadedPackage->package->extensionName->name()]); + $enableOutput = Process::run([$dockerPhpExtEnable, $downloadedPackage->package->extensionName()->name()]); } catch (ProcessFailedException $processFailed) { $output->writeln( sprintf( 'Could not enable extension %s using %s. Exception was: %s', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), $this->dockerPhpExtEnableName, $processFailed->getMessage(), ), @@ -58,7 +58,7 @@ public function setup( try { $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime( - $downloadedPackage->package->extensionName, + $downloadedPackage->package->extensionName(), $output, ); @@ -67,7 +67,7 @@ public function setup( $output->writeln( sprintf( 'Asserting that extension %s was enabled using %s failed. Output was: %s', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), $this->dockerPhpExtEnableName, $enableOutput !== '' ? $enableOutput : '(empty)', ), diff --git a/src/Installing/Ini/OndrejPhpenmod.php b/src/Installing/Ini/OndrejPhpenmod.php index 051dfb65..7c52d7de 100644 --- a/src/Installing/Ini/OndrejPhpenmod.php +++ b/src/Installing/Ini/OndrejPhpenmod.php @@ -119,7 +119,7 @@ public function setup( '%s%s%s.ini', rtrim($expectedModsAvailablePath, DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR, - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), ); $pieCreatedTheIniFile = false; @@ -148,7 +148,7 @@ static function () use ($phpenmodPath, $targetPlatform, $downloadedPackage, $out $targetPlatform->phpBinaryPath->majorMinorVersion(), '-s', 'ALL', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), ]); return true; @@ -157,7 +157,7 @@ static function () use ($phpenmodPath, $targetPlatform, $downloadedPackage, $out sprintf( 'Failed to use %s to enable %s for PHP %s: %s', $phpenmodPath, - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionName()->name(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $processFailedException->getMessage(), ), diff --git a/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php index 89f36819..9ae8c55f 100644 --- a/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php +++ b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php @@ -26,7 +26,7 @@ public function setup( ): bool { try { $targetPlatform->phpBinaryPath->assertExtensionIsLoadedInRuntime( - $downloadedPackage->package->extensionName, + $downloadedPackage->package->extensionName(), $output, ); diff --git a/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php b/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php index 84a4b505..af94dab8 100644 --- a/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php +++ b/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php @@ -60,8 +60,8 @@ public function setup( '%s%s%d-%s.ini', rtrim($additionalIniFilesPath, DIRECTORY_SEPARATOR), DIRECTORY_SEPARATOR, - $downloadedPackage->package->priority, - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->priority(), + $downloadedPackage->package->extensionName()->name(), ); $pieCreatedTheIniFile = false; diff --git a/src/Installing/SetupIniFile.php b/src/Installing/SetupIniFile.php index d6ca58bb..35b11f5c 100644 --- a/src/Installing/SetupIniFile.php +++ b/src/Installing/SetupIniFile.php @@ -45,8 +45,8 @@ public function __invoke( $output->writeln('⚠️ Extension has NOT been automatically enabled.'); $output->writeln(sprintf( 'You must now add "%s=%s" to your php.ini', - $downloadedPackage->package->extensionType === ExtensionType::PhpModule ? 'extension' : 'zend_extension', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension', + $downloadedPackage->package->extensionName()->name(), )); } } diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php index da831eed..efe9beae 100644 --- a/src/Installing/UnixInstall.php +++ b/src/Installing/UnixInstall.php @@ -33,7 +33,7 @@ public function __invoke( ): BinaryFile { $targetExtensionPath = $targetPlatform->phpBinaryPath->extensionPath(); - $sharedObjectName = $downloadedPackage->package->extensionName->name() . '.so'; + $sharedObjectName = $downloadedPackage->package->extensionName()->name() . '.so'; $expectedSharedObjectLocation = sprintf( '%s/%s', $targetExtensionPath, diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php index 7ff5ef1d..29d4ef2b 100644 --- a/src/Installing/WindowsInstall.php +++ b/src/Installing/WindowsInstall.php @@ -86,8 +86,8 @@ public function __invoke( */ $output->writeln(sprintf( 'You must now add "%s=%s" to your php.ini', - $downloadedPackage->package->extensionType === ExtensionType::PhpModule ? 'extension' : 'zend_extension', - $downloadedPackage->package->extensionName->name(), + $downloadedPackage->package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension', + $downloadedPackage->package->extensionName()->name(), )); $binaryFile = BinaryFile::fromFileWithSha256Checksum($destinationDllName); @@ -122,7 +122,7 @@ private function normalisedPathsMatch(string $first, string $second): bool private function copyExtensionDll(TargetPlatform $targetPlatform, DownloadedPackage $downloadedPackage, string $sourceDllName): string { $destinationDllName = $targetPlatform->phpBinaryPath->extensionPath() . DIRECTORY_SEPARATOR - . 'php_' . $downloadedPackage->package->extensionName->name() . '.dll'; + . 'php_' . $downloadedPackage->package->extensionName()->name() . '.dll'; if (! copy($sourceDllName, $destinationDllName) || ! file_exists($destinationDllName) && ! is_file($destinationDllName)) { throw new RuntimeException('Failed to install DLL to ' . $destinationDllName); @@ -190,7 +190,7 @@ private function copyExtraFile(TargetPlatform $targetPlatform, DownloadedPackage { $destinationFullFilename = dirname($targetPlatform->phpBinaryPath->phpBinaryPath) . DIRECTORY_SEPARATOR . 'extras' . DIRECTORY_SEPARATOR - . $downloadedPackage->package->extensionName->name() . DIRECTORY_SEPARATOR + . $downloadedPackage->package->extensionName()->name() . DIRECTORY_SEPARATOR . substr($file->getPathname(), strlen($downloadedPackage->extractedSourcePath) + 1); $destinationPath = dirname($destinationFullFilename); diff --git a/src/Platform/PrePackagedSourceAssetName.php b/src/Platform/PrePackagedSourceAssetName.php index 139e1aee..13aea737 100644 --- a/src/Platform/PrePackagedSourceAssetName.php +++ b/src/Platform/PrePackagedSourceAssetName.php @@ -23,19 +23,19 @@ public static function packageNames(Package $package): array return [ strtolower(sprintf( 'php_%s-%s-src.tgz', - $package->extensionName->name(), - $package->version, + $package->extensionName()->name(), + $package->version(), )), strtolower(sprintf( 'php_%s-%s-src.zip', - $package->extensionName->name(), - $package->version, + $package->extensionName()->name(), + $package->version(), )), // @todo remove this: strtolower(sprintf( '%s-%s.tgz', - $package->extensionName->name(), - $package->version, + $package->extensionName()->name(), + $package->version(), )), ]; } diff --git a/src/Platform/WindowsExtensionAssetName.php b/src/Platform/WindowsExtensionAssetName.php index f2383c7e..9fe38f7f 100644 --- a/src/Platform/WindowsExtensionAssetName.php +++ b/src/Platform/WindowsExtensionAssetName.php @@ -36,8 +36,8 @@ private static function assetNames(TargetPlatform $targetPlatform, Package $pack return [ strtolower(sprintf( 'php_%s-%s-%s-%s-%s-%s.%s', - $package->extensionName->name(), - $package->version, + $package->extensionName()->name(), + $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), $targetPlatform->threadSafety->asShort(), strtolower($targetPlatform->windowsCompiler->name), @@ -46,8 +46,8 @@ private static function assetNames(TargetPlatform $targetPlatform, Package $pack )), strtolower(sprintf( 'php_%s-%s-%s-%s-%s-%s.%s', - $package->extensionName->name(), - $package->version, + $package->extensionName()->name(), + $package->version(), $targetPlatform->phpBinaryPath->majorMinorVersion(), strtolower($targetPlatform->windowsCompiler->name), $targetPlatform->threadSafety->asShort(), diff --git a/test/integration/Building/UnixBuildTest.php b/test/integration/Building/UnixBuildTest.php index 976ca7a0..1d6daa6c 100644 --- a/test/integration/Building/UnixBuildTest.php +++ b/test/integration/Building/UnixBuildTest.php @@ -8,7 +8,6 @@ use Composer\Util\Platform; use Php\Pie\Building\ExtensionBinaryNotFound; use Php\Pie\Building\UnixBuild; -use Php\Pie\ConfigureOption; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; @@ -44,13 +43,6 @@ public function testUnixBuildCanBuildExtension(): void 'pie_test_ext', '0.1.0', null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); @@ -100,13 +92,6 @@ public function testUnixBuildWillThrowExceptionWhenExpectedBinaryNameMismatches( 'pie_test_ext', '0.1.0', null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); @@ -136,22 +121,14 @@ public function testUnixBuildCanBuildExtensionWithBuildPath(): void $output = new BufferedOutput(); + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('myvendor/pie_test_ext'); + $composerPackage->method('getPrettyVersion')->willReturn('0.1.0'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['build-path' => 'pie_test_ext']); + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath( - new Package( - $this->createMock(CompletePackage::class), - ExtensionType::PhpModule, - ExtensionName::normaliseFromString('pie_test_ext'), - 'pie_test_ext', - '0.1.0', - null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - 'pie_test_ext', - null, - null, - 99, - ), + Package::fromComposerCompletePackage($composerPackage), dirname(self::TEST_EXTENSION_PATH), ); @@ -204,13 +181,6 @@ public function testCleanupDoesNotCleanWhenConfigureIsMissing(): void 'pie_test_ext', '0.1.0', null, - [], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); @@ -249,13 +219,6 @@ public function testVerboseOutputShowsCleanupMessages(): void 'pie_test_ext', '0.1.0', null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); diff --git a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php index 43d5799e..c250fbf5 100644 --- a/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/integration/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -98,8 +98,8 @@ public function testDependenciesAreResolvedToExpectedVersions( false, ); - self::assertSame($expectedVersion, $package->version); - self::assertNotNull($package->downloadUrl); - self::assertStringMatchesFormat($expectedDownloadUrl, $package->downloadUrl); + self::assertSame($expectedVersion, $package->version()); + self::assertNotNull($package->downloadUrl()); + self::assertStringMatchesFormat($expectedDownloadUrl, $package->downloadUrl()); } } diff --git a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php index 664b1cb9..4dd883c9 100644 --- a/test/integration/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/integration/Downloading/GithubPackageReleaseAssetsTest.php @@ -54,13 +54,6 @@ public function testDeterminingReleaseAssetUrlForWindows(): void 'asgrim/example-pie-extension', '2.0.2', 'https://api.github.com/repos/asgrim/example-pie-extension/zipball/f9ed13ea95dada34c6cc5a052da258dbda059d27', - [], - true, - true, - null, - null, - null, - 99, ); $io = $this->createMock(IOInterface::class); diff --git a/test/integration/Installing/UnixInstallTest.php b/test/integration/Installing/UnixInstallTest.php index ca28ee0d..1156eb69 100644 --- a/test/integration/Installing/UnixInstallTest.php +++ b/test/integration/Installing/UnixInstallTest.php @@ -7,7 +7,6 @@ use Composer\Package\CompletePackage; use Composer\Util\Platform; use Php\Pie\Building\UnixBuild; -use Php\Pie\ConfigureOption; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; @@ -89,13 +88,6 @@ public function testUnixInstallCanInstallExtension(string $phpConfig): void 'pie_test_ext', '0.1.0', null, - [ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-pie_test_ext'])], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); diff --git a/test/integration/Installing/WindowsInstallTest.php b/test/integration/Installing/WindowsInstallTest.php index 4d043415..58cb0653 100644 --- a/test/integration/Installing/WindowsInstallTest.php +++ b/test/integration/Installing/WindowsInstallTest.php @@ -53,13 +53,6 @@ public function testWindowsInstallCanInstallExtension(): void 'php/pie-test-ext', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), self::TEST_EXTENSION_PATH, ); diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index f70eb932..73f80d77 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -14,11 +14,8 @@ use Composer\Util\Platform; use InvalidArgumentException; use Php\Pie\Command\CommandHelper; -use Php\Pie\ConfigureOption; use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; -use Php\Pie\ExtensionName; -use Php\Pie\ExtensionType; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\Attributes\RequiresOperatingSystemFamily; @@ -102,27 +99,21 @@ public function testBindingConfigurationOptionsFromPackage(): void public function testProcessingConfigureOptionsFromInput(): void { - $package = new Package( - $this->createMock(CompletePackage::class), - ExtensionType::PhpModule, - ExtensionName::normaliseFromString('lolz'), - 'foo/bar', - '1.0.0', - null, - [ - ConfigureOption::fromComposerJsonDefinition([ + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.0.0'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn([ + 'configure-options' => [ + [ 'name' => 'with-stuff', 'needs-value' => true, - ]), - ConfigureOption::fromComposerJsonDefinition(['name' => 'enable-thing']), + ], + ['name' => 'enable-thing'], ], - true, - true, - null, - null, - null, - 99, - ); + ]); + $package = Package::fromComposerCompletePackage($composerPackage); + $inputDefinition = new InputDefinition(); $inputDefinition->addOption(new InputOption('with-stuff', null, InputOption::VALUE_REQUIRED)); $inputDefinition->addOption(new InputOption('enable-thing', null, InputOption::VALUE_NONE)); diff --git a/test/unit/DependencyResolver/PackageTest.php b/test/unit/DependencyResolver/PackageTest.php index fc99924a..47b3b10e 100644 --- a/test/unit/DependencyResolver/PackageTest.php +++ b/test/unit/DependencyResolver/PackageTest.php @@ -23,12 +23,12 @@ public function testFromComposerCompletePackage(): void new CompletePackage('vendor/foo', '1.2.3.0', '1.2.3'), ); - self::assertSame('foo', $package->extensionName->name()); - self::assertSame('vendor/foo', $package->name); - self::assertSame('1.2.3', $package->version); + self::assertSame('foo', $package->extensionName()->name()); + self::assertSame('vendor/foo', $package->name()); + self::assertSame('1.2.3', $package->version()); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertNull($package->downloadUrl); - self::assertNull($package->buildPath); + self::assertNull($package->downloadUrl()); + self::assertNull($package->buildPath()); } public function testFromComposerCompletePackageWithExtensionName(): void @@ -38,11 +38,11 @@ public function testFromComposerCompletePackageWithExtensionName(): void $package = Package::fromComposerCompletePackage($composerCompletePackage); - self::assertSame('something_else', $package->extensionName->name()); - self::assertSame('vendor/foo', $package->name); - self::assertSame('1.2.3', $package->version); + self::assertSame('something_else', $package->extensionName()->name()); + self::assertSame('vendor/foo', $package->name()); + self::assertSame('1.2.3', $package->version()); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertNull($package->downloadUrl); + self::assertNull($package->downloadUrl()); } public function testFromComposerCompletePackageWithExcludedOsFamilies(): void @@ -52,11 +52,11 @@ public function testFromComposerCompletePackageWithExcludedOsFamilies(): void $package = Package::fromComposerCompletePackage($composerCompletePackage); - self::assertSame([OperatingSystemFamily::Windows, OperatingSystemFamily::Darwin], $package->incompatibleOsFamilies); - self::assertSame('vendor/foo', $package->name); - self::assertSame('1.2.3', $package->version); + self::assertSame([OperatingSystemFamily::Windows, OperatingSystemFamily::Darwin], $package->incompatibleOsFamilies()); + self::assertSame('vendor/foo', $package->name()); + self::assertSame('1.2.3', $package->version()); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertNull($package->downloadUrl); + self::assertNull($package->downloadUrl()); } public function testFromComposerCompletePackageWithOsFamilies(): void @@ -66,12 +66,12 @@ public function testFromComposerCompletePackageWithOsFamilies(): void $package = Package::fromComposerCompletePackage($composerCompletePackage); - self::assertEmpty($package->incompatibleOsFamilies); - self::assertSame([OperatingSystemFamily::Windows, OperatingSystemFamily::Darwin], $package->compatibleOsFamilies); - self::assertSame('vendor/foo', $package->name); - self::assertSame('1.2.3', $package->version); + self::assertEmpty($package->incompatibleOsFamilies()); + self::assertSame([OperatingSystemFamily::Windows, OperatingSystemFamily::Darwin], $package->compatibleOsFamilies()); + self::assertSame('vendor/foo', $package->name()); + self::assertSame('1.2.3', $package->version()); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertNull($package->downloadUrl); + self::assertNull($package->downloadUrl()); } public function testFromComposerCompletePackageWithBothOsFamiliesAndExcludedOsFamiliesThrows(): void @@ -134,13 +134,6 @@ public function testGithubOrgAndRepo(string $composerPackageName, string|null $d $composerPackageName, '1.2.3', $downloadUrl, - [], - true, - true, - null, - null, - null, - 99, ); self::assertSame($expectedGithubOrgAndRepo, $package->githubOrgAndRepository()); @@ -154,6 +147,6 @@ public function testFromComposerCompletePackageWithBuildPath(): void $package = Package::fromComposerCompletePackage($composerCompletePackage); self::assertSame('vendor/foo:1.2.3', $package->prettyNameAndVersion()); - self::assertSame('some/subdirectory/path/', $package->buildPath); + self::assertSame('some/subdirectory/path/', $package->buildPath()); } } diff --git a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php index 6cdd9a4b..9e9f828e 100644 --- a/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php +++ b/test/unit/DependencyResolver/ResolveDependencyWithComposerTest.php @@ -66,8 +66,8 @@ public function testPackageThatCanBeResolved(): void $this->createMock(QuieterConsoleIO::class), ))($this->composer, $targetPlatform, new RequestedPackageAndVersion('asgrim/example-pie-extension', '^1.0'), false); - self::assertSame('asgrim/example-pie-extension', $package->name); - self::assertStringStartsWith('1.', $package->version); + self::assertSame('asgrim/example-pie-extension', $package->name()); + self::assertStringStartsWith('1.', $package->version()); } /** @@ -159,8 +159,8 @@ public function testUnresolvedPackageCanBeInstalledWithForceOption(array $platfo true, ); - self::assertSame('asgrim/example-pie-extension', $package->name); - self::assertStringStartsWith('1.', $package->version); + self::assertSame('asgrim/example-pie-extension', $package->name()); + self::assertStringStartsWith('1.', $package->version()); } public function testZtsOnlyPackageCannotBeInstalledOnNtsSystem(): void diff --git a/test/unit/Downloading/DownloadedPackageTest.php b/test/unit/Downloading/DownloadedPackageTest.php index c4edf0b2..409abf63 100644 --- a/test/unit/Downloading/DownloadedPackageTest.php +++ b/test/unit/Downloading/DownloadedPackageTest.php @@ -29,13 +29,6 @@ public function testFromPackageAndExtractedPath(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ); $extractedSourcePath = uniqid('/path/to/downloaded/package', true); @@ -48,21 +41,13 @@ public function testFromPackageAndExtractedPath(): void public function testFromPackageAndExtractedPathWithBuildPath(): void { - $package = new Package( - $this->createMock(CompletePackage::class), - ExtensionType::PhpModule, - ExtensionName::normaliseFromString('foo'), - 'foo/bar', - '1.2.3', - null, - [], - true, - true, - 'Downloading', - null, - null, - 99, - ); + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['build-path' => 'Downloading']); + + $package = Package::fromComposerCompletePackage($composerPackage); $extractedSourcePath = realpath(__DIR__ . '/../'); diff --git a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php index e25bcecb..86718a89 100644 --- a/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php +++ b/test/unit/Downloading/Exception/CouldNotFindReleaseAssetTest.php @@ -30,13 +30,6 @@ public function testForPackage(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ); $exception = CouldNotFindReleaseAsset::forPackage($package, ['something.zip', 'something2.zip']); @@ -53,13 +46,6 @@ public function testForPackageWithMissingTag(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ); $exception = CouldNotFindReleaseAsset::forPackageWithMissingTag($package); diff --git a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php index a6cf7cf1..58b99b16 100644 --- a/test/unit/Downloading/GithubPackageReleaseAssetsTest.php +++ b/test/unit/Downloading/GithubPackageReleaseAssetsTest.php @@ -79,13 +79,6 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrl(): void 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true), - [], - true, - true, - null, - null, - null, - 99, ); $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); @@ -154,13 +147,6 @@ public function testUrlIsReturnedWhenFindingWindowsDownloadUrlWithCompilerAndThr 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true), - [], - true, - true, - null, - null, - null, - 99, ); $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); @@ -210,13 +196,6 @@ public function testFindWindowsDownloadUrlForPackageThrowsExceptionWhenAssetNotF 'asgrim/example-pie-extension', '1.2.3', 'https://test-uri/' . uniqid('downloadUrl', true), - [], - true, - true, - null, - null, - null, - 99, ); $releaseAssets = new GithubPackageReleaseAssets('https://test-github-api-base-url.thephp.foundation'); diff --git a/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php b/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php index 3f6f3f2e..66820d77 100644 --- a/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php +++ b/test/unit/Installing/Ini/AddExtensionToTheIniFileTest.php @@ -63,13 +63,6 @@ public function testReturnsFalseWhenFileIsNotWritable(): void 'foo/bar', '1.0.0', null, - [], - true, - true, - null, - null, - null, - 99, ), $this->mockPhpBinary, $this->output, @@ -107,13 +100,6 @@ public function testReturnsFalseWhenExistingIniCouldNotBeRead(): void 'foo/bar', '1.0.0', null, - [], - true, - true, - null, - null, - null, - 99, ), $this->mockPhpBinary, $this->output, @@ -161,13 +147,6 @@ public function testReturnsFalseWhenExtensionWasAddedButPhpRuntimeDidNotLoadExte 'foo/bar', '1.0.0', null, - [], - true, - true, - null, - null, - null, - 99, ), $this->mockPhpBinary, $this->output, @@ -205,13 +184,6 @@ public function testReturnsTrueWhenExtensionAdded(): void 'foo/bar', '1.0.0', null, - [], - true, - true, - null, - null, - null, - 99, ), $this->mockPhpBinary, $this->output, @@ -221,7 +193,7 @@ public function testReturnsTrueWhenExtensionAdded(): void $iniContent = file_get_contents($iniFile); self::assertSame( PHP_EOL . '; PIE automatically added this to enable the foo/bar extension' . PHP_EOL - . '; priority=99' . PHP_EOL + . '; priority=80' . PHP_EOL . 'extension=foobar' . PHP_EOL, $iniContent, ); @@ -255,13 +227,6 @@ public function testReturnsTrueWhenExtensionAddedWithAdditionalStep(): void 'foo/bar', '1.0.0', null, - [], - true, - true, - null, - null, - null, - 99, ), $this->mockPhpBinary, $this->output, @@ -277,7 +242,7 @@ static function () use (&$additionalStepInvoked): bool { $iniContent = file_get_contents($iniFile); self::assertSame( PHP_EOL . '; PIE automatically added this to enable the foo/bar extension' . PHP_EOL - . '; priority=99' . PHP_EOL + . '; priority=80' . PHP_EOL . 'extension=foobar' . PHP_EOL, $iniContent, ); diff --git a/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php b/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php index ac31ecc2..b7ae9a8b 100644 --- a/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php +++ b/test/unit/Installing/Ini/CheckAndAddExtensionToIniIfNeededTest.php @@ -72,13 +72,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 66, ), '/path/to/extracted/source', ); @@ -122,16 +115,16 @@ public function testExtensionIsAlreadyEnabledButExtensionDoesNotLoad(): void $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(true); $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output) + ->with($this->downloadedPackage->package->extensionName(), $this->output) ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension( $this->mockPhpBinary, - $this->downloadedPackage->package->extensionName, + $this->downloadedPackage->package->extensionName(), )); $this->addExtensionToTheIniFile @@ -162,13 +155,13 @@ public function testExtensionIsAlreadyEnabledAndExtensionLoaded(): void $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(true); $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output); + ->with($this->downloadedPackage->package->extensionName(), $this->output); $this->addExtensionToTheIniFile ->expects(self::never()) @@ -194,13 +187,13 @@ public function testExtensionIsAlreadyEnabledWithAdditionalStepAndExtensionLoade $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(true); $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output); + ->with($this->downloadedPackage->package->extensionName(), $this->output); $this->addExtensionToTheIniFile ->expects(self::never()) @@ -233,7 +226,7 @@ public function testExtensionIsNotYetAdded(): void $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(false); $this->mockPhpBinary @@ -265,7 +258,7 @@ public function testExtensionIsNotYetAddedButFailsToBeAdded(): void $this->isExtensionAlreadyInTheIniFile ->expects(self::once()) ->method('__invoke') - ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName) + ->with(self::INI_FILE, $this->downloadedPackage->package->extensionName()) ->willReturn(false); $this->mockPhpBinary diff --git a/test/unit/Installing/Ini/DockerPhpExtEnableTest.php b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php index 410190a0..ab407afc 100644 --- a/test/unit/Installing/Ini/DockerPhpExtEnableTest.php +++ b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php @@ -70,13 +70,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); @@ -122,7 +115,7 @@ public function testReturnsTrueWhenDockerPhpExtEnableSuccessfullyEnablesExtensio $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output); + ->with($this->downloadedPackage->package->extensionName(), $this->output); self::assertTrue( (new DockerPhpExtEnable(self::GOOD_DOCKER_PHP_EXT_ENABLE)) @@ -157,10 +150,10 @@ public function testReturnsFalseWhenDockerPhpExtEnableFailsToAssertExtensionWasE $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output) + ->with($this->downloadedPackage->package->extensionName(), $this->output) ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension( $this->mockPhpBinary, - $this->downloadedPackage->package->extensionName, + $this->downloadedPackage->package->extensionName(), )); self::assertFalse( diff --git a/test/unit/Installing/Ini/OndrejPhpenmodTest.php b/test/unit/Installing/Ini/OndrejPhpenmodTest.php index 4fcecb4e..527fe426 100644 --- a/test/unit/Installing/Ini/OndrejPhpenmodTest.php +++ b/test/unit/Installing/Ini/OndrejPhpenmodTest.php @@ -83,13 +83,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); diff --git a/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php index 07d4bd68..1c35c02d 100644 --- a/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php +++ b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php @@ -97,13 +97,6 @@ public function testVerboseMessageIsEmittedSettingUpWithoutAnyApproaches(): void 'test-vendor/test-package', '1.2.3', 'https://test-uri/', - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ), @@ -139,13 +132,6 @@ public function testWorkingApproachIsUsed(): void 'test-vendor/test-package', '1.2.3', 'https://test-uri/', - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ), @@ -181,13 +167,6 @@ public function testSetupFailsWhenNoApproachesWork(): void 'test-vendor/test-package', '1.2.3', 'https://test-uri/', - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ), diff --git a/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php index de1795f3..6435e6bc 100644 --- a/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php +++ b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php @@ -65,13 +65,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); @@ -93,7 +86,7 @@ public function testSetupReturnsTrueWhenExtAlreadyRuntimeLoaded(): void $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output); + ->with($this->downloadedPackage->package->extensionName(), $this->output); self::assertTrue($this->preCheckExtensionAlreadyLoaded->setup( $this->targetPlatform, @@ -108,10 +101,10 @@ public function testSetupReturnsFalseWhenExtIsNotRuntimeLoaded(): void $this->mockPhpBinary ->expects(self::once()) ->method('assertExtensionIsLoadedInRuntime') - ->with($this->downloadedPackage->package->extensionName, $this->output) + ->with($this->downloadedPackage->package->extensionName(), $this->output) ->willThrowException(ExtensionIsNotLoaded::fromExpectedExtension( $this->mockPhpBinary, - $this->downloadedPackage->package->extensionName, + $this->downloadedPackage->package->extensionName(), )); self::assertFalse($this->preCheckExtensionAlreadyLoaded->setup( diff --git a/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php index 83f5b9e4..45a073d6 100644 --- a/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php +++ b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php @@ -77,13 +77,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); @@ -163,7 +156,7 @@ public function testReturnsTrueWhenCheckAndAddExtensionIsInvoked(): void unlink($additionalPhpIniDirectory); mkdir($additionalPhpIniDirectory, recursive: true); - $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini'; + $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '80-foobar.ini'; $this->mockPhpBinary ->expects(self::once()) @@ -199,7 +192,7 @@ public function testReturnsFalseAndRemovesPieCreatedIniFileWhenCheckAndAddExtens unlink($additionalPhpIniDirectory); mkdir($additionalPhpIniDirectory, recursive: true); - $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini'; + $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '80-foobar.ini'; $this->mockPhpBinary ->expects(self::once()) @@ -234,7 +227,7 @@ public function testReturnsFalseAndLeavesNonPieCreatedIniFileWhenCheckAndAddExte unlink($additionalPhpIniDirectory); mkdir($additionalPhpIniDirectory, recursive: true); - $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '99-foobar.ini'; + $expectedIniFile = $additionalPhpIniDirectory . DIRECTORY_SEPARATOR . '80-foobar.ini'; touch($expectedIniFile); $this->mockPhpBinary diff --git a/test/unit/Installing/Ini/StandardSinglePhpIniTest.php b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php index 34b19daa..21f47714 100644 --- a/test/unit/Installing/Ini/StandardSinglePhpIniTest.php +++ b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php @@ -70,13 +70,6 @@ public function setUp(): void 'foo/bar', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ), '/path/to/extracted/source', ); diff --git a/test/unit/Platform/WindowsExtensionAssetNameTest.php b/test/unit/Platform/WindowsExtensionAssetNameTest.php index 75e72a6b..77a77764 100644 --- a/test/unit/Platform/WindowsExtensionAssetNameTest.php +++ b/test/unit/Platform/WindowsExtensionAssetNameTest.php @@ -49,13 +49,6 @@ public function setUp(): void 'phpf/foo', '1.2.3', null, - [], - true, - true, - null, - null, - null, - 99, ); } From 011c5113872fc524a037dea56bf17f3c42f927db Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 3 Feb 2025 21:59:25 +0000 Subject: [PATCH 05/35] Add {version} template to download package path --- docs/extension-maintainers.md | 6 ++++++ src/Downloading/DownloadedPackage.php | 6 +++++- test/assets/package-1.2.3/.gitkeep | 0 .../unit/Downloading/DownloadedPackageTest.php | 18 ++++++++++++++++++ 4 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 test/assets/package-1.2.3/.gitkeep diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 610728bd..5a25c4c8 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -160,6 +160,12 @@ should specify this path in `build-path`, for example: } ``` +The `build-path` may contain some templated values which are replaced: + + * `{version}` to be replaced with the package version. For example a package + with version 1.2.3 with a `build-path` of `myext-{version}` the actual build + path would become `myext-{version}`. + ### Extension dependencies Extension authors may define some dependencies in `require`, but practically, diff --git a/src/Downloading/DownloadedPackage.php b/src/Downloading/DownloadedPackage.php index ba57c4b7..1ef0b252 100644 --- a/src/Downloading/DownloadedPackage.php +++ b/src/Downloading/DownloadedPackage.php @@ -27,7 +27,11 @@ private function __construct( public static function fromPackageAndExtractedPath(Package $package, string $extractedSourcePath): self { if ($package->buildPath() !== null) { - $extractedSourcePathWithBuildPath = realpath($extractedSourcePath . DIRECTORY_SEPARATOR . $package->buildPath()); + $extractedSourcePathWithBuildPath = realpath( + $extractedSourcePath + . DIRECTORY_SEPARATOR + . str_replace("{version}", $package->version(), $package->buildPath()) + ); if (is_string($extractedSourcePathWithBuildPath)) { $extractedSourcePath = $extractedSourcePathWithBuildPath; diff --git a/test/assets/package-1.2.3/.gitkeep b/test/assets/package-1.2.3/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/test/unit/Downloading/DownloadedPackageTest.php b/test/unit/Downloading/DownloadedPackageTest.php index 409abf63..dcdc5dd6 100644 --- a/test/unit/Downloading/DownloadedPackageTest.php +++ b/test/unit/Downloading/DownloadedPackageTest.php @@ -56,4 +56,22 @@ public function testFromPackageAndExtractedPathWithBuildPath(): void self::assertSame($extractedSourcePath . DIRECTORY_SEPARATOR . 'Downloading', $downloadedPackage->extractedSourcePath); self::assertSame($package, $downloadedPackage->package); } + + public function testFromPackageAndExtractedPathWithBuildPathWithVersionTemplate(): void + { + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['build-path' => 'package-{version}']); + + $package = Package::fromComposerCompletePackage($composerPackage); + + $extractedSourcePath = realpath(__DIR__ . '/../../assets'); + + $downloadedPackage = DownloadedPackage::fromPackageAndExtractedPath($package, $extractedSourcePath); + + self::assertSame($extractedSourcePath . DIRECTORY_SEPARATOR . 'package-1.2.3', $downloadedPackage->extractedSourcePath); + self::assertSame($package, $downloadedPackage->package); + } } From 7362b27b621774e1e8471af22353e20a37eae71f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 3 Feb 2025 22:08:39 +0000 Subject: [PATCH 06/35] Add handling for override-download-url-method configuration --- src/DependencyResolver/Package.php | 26 ++++++++++++++----- src/Downloading/DownloadUrlMethod.php | 10 +++---- ...OverrideDownloadUrlInstallListenerTest.php | 6 ++++- 3 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index 94519ff0..03263eeb 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -7,6 +7,7 @@ use Composer\Package\CompletePackageInterface; use InvalidArgumentException; use Php\Pie\ConfigureOption; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; use Php\Pie\Platform\OperatingSystemFamily; @@ -41,6 +42,7 @@ final class Package private array|null $incompatibleOsFamilies; private bool $supportZts; private bool $supportNts; + private DownloadUrlMethod|null $overrideDownloadUrlMethod; public function __construct( private readonly CompletePackageInterface $composerPackage, @@ -50,13 +52,14 @@ public function __construct( private readonly string $version, private readonly string|null $downloadUrl, ) { - $this->configureOptions = []; - $this->supportZts = true; - $this->supportNts = true; - $this->buildPath = null; - $this->compatibleOsFamilies = null; - $this->incompatibleOsFamilies = null; - $this->priority = 80; + $this->configureOptions = []; + $this->supportZts = true; + $this->supportNts = true; + $this->buildPath = null; + $this->compatibleOsFamilies = null; + $this->incompatibleOsFamilies = null; + $this->priority = 80; + $this->overrideDownloadUrlMethod = null; } public static function fromComposerCompletePackage(CompletePackageInterface $completePackage): self @@ -95,6 +98,10 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com $package->priority = $phpExtOptions['priority'] ?? 80; + if (array_key_exists('override-download-url-method', $phpExtOptions)) { + $package->overrideDownloadUrlMethod = DownloadUrlMethod::tryFrom($phpExtOptions['override-download-url-method']); + } + return $package; } @@ -214,4 +221,9 @@ public function supportNts(): bool { return $this->supportNts; } + + public function overrideDownloadUrlMethod(): DownloadUrlMethod|null + { + return $this->overrideDownloadUrlMethod; + } } diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php index 2b270858..ae1eadbd 100644 --- a/src/Downloading/DownloadUrlMethod.php +++ b/src/Downloading/DownloadUrlMethod.php @@ -10,11 +10,11 @@ use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\WindowsExtensionAssetName; -enum DownloadUrlMethod +enum DownloadUrlMethod: string { - case ComposerDefaultDownload; - case WindowsBinaryDownload; - case PrePackagedSourceDownload; + case ComposerDefaultDownload = 'composer-default'; + case WindowsBinaryDownload = 'windows-binary'; + case PrePackagedSourceDownload = 'pre-packaged-source'; /** @return non-empty-list|null */ public function possibleAssetNames(Package $package, TargetPlatform $targetPlatform): array|null @@ -41,7 +41,7 @@ public static function fromPackage(Package $package, TargetPlatform $targetPlatf * external dependencies in Git submodules that otherwise aren't * included in GitHub/Gitlab/etc "dist" downloads */ - if (false) { // @todo check $package `php-ext` + if ($package->overrideDownloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload) { return self::PrePackagedSourceDownload; } diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 4e215bd5..97de0073 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -15,6 +15,7 @@ use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; @@ -214,7 +215,10 @@ public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); $composerPackage->setDistType('zip'); $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); - $composerPackage->setPhpExt(['extension-name' => 'mongodb']); + $composerPackage->setPhpExt([ + 'extension-name' => 'foobar', + 'override-download-url-method' => 'pre-packaged-source', + ]); /** * @psalm-suppress InternalClass From 205d4780510ca9522eabff42c15ed94577b830ea Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 3 Feb 2025 22:15:49 +0000 Subject: [PATCH 07/35] Move php-ext schema from pie-design repo --- resources/composer-json-php-ext-schema.json | 106 ++++++++++++++++++++ 1 file changed, 106 insertions(+) create mode 100644 resources/composer-json-php-ext-schema.json diff --git a/resources/composer-json-php-ext-schema.json b/resources/composer-json-php-ext-schema.json new file mode 100644 index 00000000..b41199db --- /dev/null +++ b/resources/composer-json-php-ext-schema.json @@ -0,0 +1,106 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://raw.githubusercontent.com/php/pie/main/composer-json-php-ext-schema.json", + "title": "composer.json php-ext schema", + "description": "Schema for the proposed php-ext section in composer.json that the new PECL will use to build packages", + "type": "object", + "properties": { + "php-ext": { + "type": "object", + "description": "Settings for PHP extension packages.", + "properties": { + "extension-name": { + "type": "string", + "description": "If specified, this will be used as the name of the extension, where needed by tooling. If this is not specified, the extension name will be derived from the Composer package name (e.g. `vendor/name` would become `ext-name`). The extension name may be specified with or without the `ext-` prefix, and tools that use this must normalise this appropriately.", + "example": "ext-xdebug" + }, + "priority": { + "type": "integer", + "description": "This is used to add a prefix to the INI file, e.g. `90-xdebug.ini` which affects the loading order. The priority is a number in the range 10-99 inclusive, with 10 being the highest priority (i.e. will be processed first), and 99 being the lowest priority (i.e. will be processed last). There are two digits so that the files sort correctly on any platform, whether the sorting is natural or not.", + "minimum": 10, + "maximum": 99, + "example": 80, + "default": 80 + }, + "support-zts": { + "type": "boolean", + "description": "Does this package support Zend Thread Safety", + "example": false, + "default": true + }, + "support-nts": { + "type": "boolean", + "description": "Does this package support non-Thread Safe mode", + "example": false, + "default": true + }, + "build-path": { + "type": ["string", "null"], + "description": "If specified, this is the subdirectory that will be used to build the extension instead of the root of the project.", + "example": "my-extension-source", + "default": null + }, + "override-download-url-method": { + "type": "string", + "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", + "enum": ["composer-default", "override-download-url-method"], + "example": "composer-default" + }, + "os-families": { + "type": "array", + "minItems": 1, + "description": "An array of OS families to mark as compatible with the extension. Specifying this property will mean this package is not installable with PIE on any OS family not listed here. Must not be specified alongside os-families-exclude.", + "items": { + "type": "string", + "enum": ["windows", "bsd", "darwin", "solaris", "linux", "unknown"], + "description": "The name of the OS family to mark as compatible." + } + }, + "os-families-exclude": { + "type": "array", + "minItems": 1, + "description": "An array of OS families to mark as incompatible with the extension. Specifying this property will mean this package is installable on any OS family except those listed here. Must not be specified alongside os-families.", + "items": { + "type": "string", + "enum": ["windows", "bsd", "darwin", "solaris", "linux", "unknown"], + "description": "The name of the OS family to exclude." + } + }, + "configure-options": { + "type": "array", + "description": "These configure options make up the flags that can be passed to ./configure when installing the extension.", + "items": { + "type": "object", + "required": ["name"], + "properties": { + "name": { + "type": "string", + "description": "The name of the flag, this would typically be prefixed with `--`, for example, the value 'the-flag' would be passed as `./configure --the-flag`.", + "example": "without-xdebug-compression", + "pattern": "^[a-zA-Z0-9][a-zA-Z0-9-_]*$" + }, + "needs-value": { + "type": "boolean", + "description": "If this is set to true, the flag needs a value (e.g. --with-somelib=), otherwise it is a flag without a value (e.g. --enable-some-feature).", + "example": false, + "default": false + }, + "description": { + "type": "string", + "description": "The description of what the flag does or means.", + "example": "Disable compression through zlib" + } + } + } + } + }, + "allOf": [ + { + "not": { + "required": ["os-families", "os-families-exclude"] + } + } + ] + } + } +} From 1806bb91d7c6883f048eb57c942dd828e16d812f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 4 Feb 2025 21:08:03 +0000 Subject: [PATCH 08/35] CS and SA fixes --- src/DependencyResolver/Package.php | 7 ++++++- src/Downloading/DownloadedPackage.php | 3 ++- .../Listeners/OverrideDownloadUrlInstallListenerTest.php | 1 - 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index 03263eeb..331b14ce 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -18,6 +18,7 @@ use function array_slice; use function explode; use function implode; +use function is_string; use function parse_url; use function str_contains; use function str_starts_with; @@ -98,7 +99,11 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com $package->priority = $phpExtOptions['priority'] ?? 80; - if (array_key_exists('override-download-url-method', $phpExtOptions)) { + if ( + $phpExtOptions !== null + && array_key_exists('override-download-url-method', $phpExtOptions) + && is_string($phpExtOptions['override-download-url-method']) + ) { $package->overrideDownloadUrlMethod = DownloadUrlMethod::tryFrom($phpExtOptions['override-download-url-method']); } diff --git a/src/Downloading/DownloadedPackage.php b/src/Downloading/DownloadedPackage.php index 1ef0b252..7836d3d0 100644 --- a/src/Downloading/DownloadedPackage.php +++ b/src/Downloading/DownloadedPackage.php @@ -8,6 +8,7 @@ use function is_string; use function realpath; +use function str_replace; use const DIRECTORY_SEPARATOR; @@ -30,7 +31,7 @@ public static function fromPackageAndExtractedPath(Package $package, string $ext $extractedSourcePathWithBuildPath = realpath( $extractedSourcePath . DIRECTORY_SEPARATOR - . str_replace("{version}", $package->version(), $package->buildPath()) + . str_replace('{version}', $package->version(), $package->buildPath()), ); if (is_string($extractedSourcePathWithBuildPath)) { diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 97de0073..2ca8671d 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -15,7 +15,6 @@ use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; -use Php\Pie\Downloading\DownloadUrlMethod; use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; From 68b3428a52d51ff5b52128ecad13cac4bb8d0d27 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 4 Feb 2025 21:28:20 +0000 Subject: [PATCH 09/35] Documentation for override-download-url-method --- docs/extension-maintainers.md | 14 ++++++++++++++ resources/composer-json-php-ext-schema.json | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 5a25c4c8..4bbfd439 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -166,6 +166,20 @@ The `build-path` may contain some templated values which are replaced: with version 1.2.3 with a `build-path` of `myext-{version}` the actual build path would become `myext-{version}`. +#### `override-download-url-method` + +The `override-download-url-method` directive allows extension maintainers to +change the behaviour of downloading the source package. + + * Setting this to `composer-default`, which is the default value if not + specified, will use the default behaviour implemented by Composer, which is + to use the standard ZIP archive from the GitHub API (or other source control + system). + * Using `pre-packaged-source` will locate a source code package in the release + assets list based matching one of the following naming conventions: + * `php_{ExtensionName}-{Version}-src.tgz` (e.g. `php_myext-1.20.1-src.tgz`) + * `php_{ExtensionName}-{Version}-src.zip` (e.g. `php_myext-1.20.1-src.zip`) + ### Extension dependencies Extension authors may define some dependencies in `require`, but practically, diff --git a/resources/composer-json-php-ext-schema.json b/resources/composer-json-php-ext-schema.json index b41199db..22ca3706 100644 --- a/resources/composer-json-php-ext-schema.json +++ b/resources/composer-json-php-ext-schema.json @@ -43,7 +43,7 @@ "override-download-url-method": { "type": "string", "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", - "enum": ["composer-default", "override-download-url-method"], + "enum": ["composer-default", "pre-packaged-source"], "example": "composer-default" }, "os-families": { From edcc851592639ed1022ab1041702b1ea689c60a9 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 5 Feb 2025 09:46:30 +0000 Subject: [PATCH 10/35] Update docs with actual expected build path Co-authored-by: Christophe Coevoet --- docs/extension-maintainers.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index 4bbfd439..bd704975 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -164,7 +164,7 @@ The `build-path` may contain some templated values which are replaced: * `{version}` to be replaced with the package version. For example a package with version 1.2.3 with a `build-path` of `myext-{version}` the actual build - path would become `myext-{version}`. + path would become `myext-1.2.3`. #### `override-download-url-method` From 7e3b16f0313fe253478fbb12c12ea9fb6b74689f Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 5 Feb 2025 10:26:17 +0000 Subject: [PATCH 11/35] Update to remove superfluous override- prefix and pull latest composer/composer --- composer.json | 2 +- composer.lock | 21 +++++++------ docs/extension-maintainers.md | 4 +-- resources/composer-json-php-ext-schema.json | 2 +- src/DependencyResolver/Package.php | 31 ++++++++----------- src/Downloading/DownloadUrlMethod.php | 2 +- ...OverrideDownloadUrlInstallListenerTest.php | 2 +- 7 files changed, 31 insertions(+), 33 deletions(-) diff --git a/composer.json b/composer.json index ba043ff4..037a249d 100644 --- a/composer.json +++ b/composer.json @@ -28,7 +28,7 @@ ], "require": { "php": "8.1.*||8.2.*||8.3.*||8.4.*", - "composer/composer": "^2.8.5", + "composer/composer": "dev-main", "composer/pcre": "^3.3.2", "composer/semver": "^3.4.3", "fidry/cpu-core-counter": "^1.2", diff --git a/composer.lock b/composer.lock index c6ecdfbd..fa318734 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "819abdc1a00f374dc389cde52c096979", + "content-hash": "010234aff469fe878aace1bf281c1dff", "packages": [ { "name": "composer/ca-bundle", @@ -157,16 +157,16 @@ }, { "name": "composer/composer", - "version": "2.8.5", + "version": "dev-main", "source": { "type": "git", "url": "https://github.com/composer/composer.git", - "reference": "ae208dc1e182bd45d99fcecb956501da212454a1" + "reference": "b70b6bd0709f6b2071935ee2f2b6061b812724d0" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/composer/composer/zipball/ae208dc1e182bd45d99fcecb956501da212454a1", - "reference": "ae208dc1e182bd45d99fcecb956501da212454a1", + "url": "https://api.github.com/repos/composer/composer/zipball/b70b6bd0709f6b2071935ee2f2b6061b812724d0", + "reference": "b70b6bd0709f6b2071935ee2f2b6061b812724d0", "shasum": "" }, "require": { @@ -205,6 +205,7 @@ "ext-zip": "Enabling the zip extension allows you to unzip archives", "ext-zlib": "Allow gzip compression of HTTP requests" }, + "default-branch": true, "bin": [ "bin/composer" ], @@ -251,7 +252,7 @@ "irc": "ircs://irc.libera.chat:6697/composer", "issues": "https://github.com/composer/composer/issues", "security": "https://github.com/composer/composer/security/policy", - "source": "https://github.com/composer/composer/tree/2.8.5" + "source": "https://github.com/composer/composer/tree/main" }, "funding": [ { @@ -267,7 +268,7 @@ "type": "tidelift" } ], - "time": "2025-01-21T14:23:40+00:00" + "time": "2025-02-05T10:20:56+00:00" }, { "name": "composer/metadata-minifier", @@ -7003,13 +7004,15 @@ ], "aliases": [], "minimum-stability": "stable", - "stability-flags": [], + "stability-flags": { + "composer/composer": 20 + }, "prefer-stable": false, "prefer-lowest": false, "platform": { "php": "8.1.*||8.2.*||8.3.*||8.4.*" }, - "platform-dev": [], + "platform-dev": {}, "platform-overrides": { "php": "8.1.99" }, diff --git a/docs/extension-maintainers.md b/docs/extension-maintainers.md index bd704975..7f34de30 100644 --- a/docs/extension-maintainers.md +++ b/docs/extension-maintainers.md @@ -166,9 +166,9 @@ The `build-path` may contain some templated values which are replaced: with version 1.2.3 with a `build-path` of `myext-{version}` the actual build path would become `myext-1.2.3`. -#### `override-download-url-method` +#### `download-url-method` -The `override-download-url-method` directive allows extension maintainers to +The `download-url-method` directive allows extension maintainers to change the behaviour of downloading the source package. * Setting this to `composer-default`, which is the default value if not diff --git a/resources/composer-json-php-ext-schema.json b/resources/composer-json-php-ext-schema.json index 22ca3706..ca283510 100644 --- a/resources/composer-json-php-ext-schema.json +++ b/resources/composer-json-php-ext-schema.json @@ -40,7 +40,7 @@ "example": "my-extension-source", "default": null }, - "override-download-url-method": { + "download-url-method": { "type": "string", "description": "If specified, this technique will be used to override the URL that PIE uses to download the asset. The default, if not specified, is composer-default.", "enum": ["composer-default", "pre-packaged-source"], diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index 331b14ce..42bee00b 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -18,7 +18,6 @@ use function array_slice; use function explode; use function implode; -use function is_string; use function parse_url; use function str_contains; use function str_starts_with; @@ -43,7 +42,7 @@ final class Package private array|null $incompatibleOsFamilies; private bool $supportZts; private bool $supportNts; - private DownloadUrlMethod|null $overrideDownloadUrlMethod; + private DownloadUrlMethod|null $downloadUrlMethod; public function __construct( private readonly CompletePackageInterface $composerPackage, @@ -53,14 +52,14 @@ public function __construct( private readonly string $version, private readonly string|null $downloadUrl, ) { - $this->configureOptions = []; - $this->supportZts = true; - $this->supportNts = true; - $this->buildPath = null; - $this->compatibleOsFamilies = null; - $this->incompatibleOsFamilies = null; - $this->priority = 80; - $this->overrideDownloadUrlMethod = null; + $this->configureOptions = []; + $this->supportZts = true; + $this->supportNts = true; + $this->buildPath = null; + $this->compatibleOsFamilies = null; + $this->incompatibleOsFamilies = null; + $this->priority = 80; + $this->downloadUrlMethod = null; } public static function fromComposerCompletePackage(CompletePackageInterface $completePackage): self @@ -99,12 +98,8 @@ public static function fromComposerCompletePackage(CompletePackageInterface $com $package->priority = $phpExtOptions['priority'] ?? 80; - if ( - $phpExtOptions !== null - && array_key_exists('override-download-url-method', $phpExtOptions) - && is_string($phpExtOptions['override-download-url-method']) - ) { - $package->overrideDownloadUrlMethod = DownloadUrlMethod::tryFrom($phpExtOptions['override-download-url-method']); + if ($phpExtOptions !== null && array_key_exists('download-url-method', $phpExtOptions)) { + $package->downloadUrlMethod = DownloadUrlMethod::tryFrom($phpExtOptions['download-url-method']); } return $package; @@ -227,8 +222,8 @@ public function supportNts(): bool return $this->supportNts; } - public function overrideDownloadUrlMethod(): DownloadUrlMethod|null + public function downloadUrlMethod(): DownloadUrlMethod|null { - return $this->overrideDownloadUrlMethod; + return $this->downloadUrlMethod; } } diff --git a/src/Downloading/DownloadUrlMethod.php b/src/Downloading/DownloadUrlMethod.php index ae1eadbd..397de170 100644 --- a/src/Downloading/DownloadUrlMethod.php +++ b/src/Downloading/DownloadUrlMethod.php @@ -41,7 +41,7 @@ public static function fromPackage(Package $package, TargetPlatform $targetPlatf * external dependencies in Git submodules that otherwise aren't * included in GitHub/Gitlab/etc "dist" downloads */ - if ($package->overrideDownloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload) { + if ($package->downloadUrlMethod() === DownloadUrlMethod::PrePackagedSourceDownload) { return self::PrePackagedSourceDownload; } diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 2ca8671d..237cbb6a 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -216,7 +216,7 @@ public function testDistUrlIsUpdatedForPrePackagedTgzSource(): void $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); $composerPackage->setPhpExt([ 'extension-name' => 'foobar', - 'override-download-url-method' => 'pre-packaged-source', + 'download-url-method' => 'pre-packaged-source', ]); /** From 52ca6667dcea23141b8bd5751b4ab54b15b346f0 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 6 Feb 2025 08:00:15 +0000 Subject: [PATCH 12/35] Specify Package VO defaults in properties --- src/DependencyResolver/Package.php | 46 +++++++++++++----------------- 1 file changed, 20 insertions(+), 26 deletions(-) diff --git a/src/DependencyResolver/Package.php b/src/DependencyResolver/Package.php index 42bee00b..2e4d31e8 100644 --- a/src/DependencyResolver/Package.php +++ b/src/DependencyResolver/Package.php @@ -33,16 +33,16 @@ final class Package { /** @var list */ - private array $configureOptions; - private int $priority; - private string|null $buildPath; + private array $configureOptions = []; + private int $priority = 80; + private string|null $buildPath = null; /** @var non-empty-list|null */ - private array|null $compatibleOsFamilies; + private array|null $compatibleOsFamilies = null; /** @var non-empty-list|null */ - private array|null $incompatibleOsFamilies; - private bool $supportZts; - private bool $supportNts; - private DownloadUrlMethod|null $downloadUrlMethod; + private array|null $incompatibleOsFamilies = null; + private bool $supportZts = true; + private bool $supportNts = true; + private DownloadUrlMethod|null $downloadUrlMethod = null; public function __construct( private readonly CompletePackageInterface $composerPackage, @@ -52,14 +52,6 @@ public function __construct( private readonly string $version, private readonly string|null $downloadUrl, ) { - $this->configureOptions = []; - $this->supportZts = true; - $this->supportNts = true; - $this->buildPath = null; - $this->compatibleOsFamilies = null; - $this->incompatibleOsFamilies = null; - $this->priority = 80; - $this->downloadUrlMethod = null; } public static function fromComposerCompletePackage(CompletePackageInterface $completePackage): self @@ -140,18 +132,20 @@ private static function convertInputStringsToOperatingSystemFamilies(array|null return null; } - $osFamilies = []; - foreach ($input as $value) { - $valueToTry = strtolower($value); + Assert::isNonEmptyList($input, 'Expected operating systems families to be a non-empty list.'); - Assert::inArray($valueToTry, OperatingSystemFamily::asValuesList(), 'Expected operating system family to be one of: %2$s. Got: %s'); + return array_map( + static function ($value): OperatingSystemFamily { + Assert::inArray( + strtolower($value), + OperatingSystemFamily::asValuesList(), + 'Expected operating system family to be one of: %2$s. Got: %s', + ); - $osFamilies[] = OperatingSystemFamily::from($valueToTry); - } - - Assert::isNonEmptyList($osFamilies, 'Expected operating systems families to be a non-empty list.'); - - return $osFamilies; + return OperatingSystemFamily::from(strtolower($value)); + }, + $input, + ); } public function composerPackage(): CompletePackageInterface From 1cb972b579aaf4a644774858dddb091f6258c04a Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 6 Feb 2025 08:16:54 +0000 Subject: [PATCH 13/35] Removed testing package name format --- src/Platform/PrePackagedSourceAssetName.php | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Platform/PrePackagedSourceAssetName.php b/src/Platform/PrePackagedSourceAssetName.php index 13aea737..3261b690 100644 --- a/src/Platform/PrePackagedSourceAssetName.php +++ b/src/Platform/PrePackagedSourceAssetName.php @@ -31,12 +31,6 @@ public static function packageNames(Package $package): array $package->extensionName()->name(), $package->version(), )), - // @todo remove this: - strtolower(sprintf( - '%s-%s.tgz', - $package->extensionName()->name(), - $package->version(), - )), ]; } } From e9eb4ff663a607e2ad3435a9fa280dbfa73e6587 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 6 Feb 2025 09:18:53 +0000 Subject: [PATCH 14/35] Add remaining unit tests for download URL override --- ...OverrideDownloadUrlInstallListenerTest.php | 130 +++++++++++++++++- .../Downloading/DownloadUrlMethodTest.php | 103 +++++++++++++- .../PrePackagedSourceAssetNameTest.php | 21 ++- 3 files changed, 247 insertions(+), 7 deletions(-) diff --git a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php index 237cbb6a..859bdb9a 100644 --- a/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php +++ b/test/unit/ComposerIntegration/Listeners/OverrideDownloadUrlInstallListenerTest.php @@ -11,6 +11,7 @@ use Composer\Installer\InstallerEvents; use Composer\IO\IOInterface; use Composer\Package\CompletePackage; +use Composer\Package\Package; use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; @@ -87,17 +88,140 @@ public function testEventListenerRegistration(): void public function testNonInstallOperationsAreIgnored(): void { - self::markTestIncomplete('todo'); // @todo + $composerPackage = new CompletePackage('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([$composerPackage], []), + ); + + $this->container + ->expects(self::never()) + ->method('get'); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); } public function testNonCompletePackagesAreIgnored(): void { - self::markTestIncomplete('todo'); // @todo + $composerPackage = new Package('foo/bar', '1.2.3.0', '1.2.3'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $this->container + ->expects(self::never()) + ->method('get'); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); } public function testInstallOperationsForDifferentPackagesAreIgnored(): void { - self::markTestIncomplete('todo'); // @todo + $composerPackage = new CompletePackage('different/package', '1.2.3.0', '1.2.3'); + $composerPackage->setDistUrl('https://example.com/git-archive-zip-url'); + + /** + * @psalm-suppress InternalClass + * @psalm-suppress InternalMethod + */ + $installerEvent = new InstallerEvent( + InstallerEvents::PRE_OPERATIONS_EXEC, + $this->composer, + $this->io, + false, + true, + new Transaction([], [$composerPackage]), + ); + + $this->container + ->expects(self::never()) + ->method('get'); + + (new OverrideDownloadUrlInstallListener( + $this->composer, + $this->io, + $this->container, + new PieComposerRequest( + $this->createMock(OutputInterface::class), + new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ), + new RequestedPackageAndVersion('foo/bar', '^1.1'), + PieOperation::Install, + [], + null, + false, + ), + ))($installerEvent); } public function testWindowsUrlInstallerDoesNotRunOnNonWindows(): void diff --git a/test/unit/Downloading/DownloadUrlMethodTest.php b/test/unit/Downloading/DownloadUrlMethodTest.php index 53da6c5d..f48b8e86 100644 --- a/test/unit/Downloading/DownloadUrlMethodTest.php +++ b/test/unit/Downloading/DownloadUrlMethodTest.php @@ -4,7 +4,19 @@ namespace Php\PieUnitTest\Downloading; +use Composer\Package\CompletePackage; +use Composer\Package\CompletePackageInterface; +use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadUrlMethod; +use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; +use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\OperatingSystemFamily; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Platform\ThreadSafetyMode; +use Php\Pie\Platform\WindowsCompiler; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -13,16 +25,101 @@ final class DownloadUrlMethodTest extends TestCase { public function testWindowsPackages(): void { - self::markTestIncomplete('todo'); // @todo + $package = new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/foo', + '1.2.3', + null, + ); + + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath + ->method('majorMinorVersion') + ->willReturn('8.1'); + + $targetPlatform = new TargetPlatform( + OperatingSystem::Windows, + OperatingSystemFamily::Windows, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + WindowsCompiler::VC15, + ); + + $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + + self::assertSame(DownloadUrlMethod::WindowsBinaryDownload, $downloadUrlMethod); + + self::assertSame( + [ + 'php_foo-1.2.3-8.1-nts-vc15-x86_64.zip', + 'php_foo-1.2.3-8.1-vc15-nts-x86_64.zip', + ], + $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), + ); } public function testPrePackagedSourceDownloads(): void { - self::markTestIncomplete('todo'); // @todo + $composerPackage = $this->createMock(CompletePackage::class); + $composerPackage->method('getPrettyName')->willReturn('foo/bar'); + $composerPackage->method('getPrettyVersion')->willReturn('1.2.3'); + $composerPackage->method('getType')->willReturn('php-ext'); + $composerPackage->method('getPhpExt')->willReturn(['download-url-method' => 'pre-packaged-source']); + + $package = Package::fromComposerCompletePackage($composerPackage); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + + self::assertSame(DownloadUrlMethod::PrePackagedSourceDownload, $downloadUrlMethod); + + self::assertSame( + [ + 'php_bar-1.2.3-src.tgz', + 'php_bar-1.2.3-src.zip', + ], + $downloadUrlMethod->possibleAssetNames($package, $targetPlatform), + ); } public function testComposerDefaultDownload(): void { - self::markTestIncomplete('todo'); // @todo + $package = new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foo'), + 'foo/foo', + '1.2.3', + null, + ); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + PhpBinaryPath::fromCurrentProcess(), + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + ); + + $downloadUrlMethod = DownloadUrlMethod::fromPackage($package, $targetPlatform); + + self::assertSame(DownloadUrlMethod::ComposerDefaultDownload, $downloadUrlMethod); + + self::assertNull($downloadUrlMethod->possibleAssetNames($package, $targetPlatform)); } } diff --git a/test/unit/Platform/PrePackagedSourceAssetNameTest.php b/test/unit/Platform/PrePackagedSourceAssetNameTest.php index 426aebc8..5a47f146 100644 --- a/test/unit/Platform/PrePackagedSourceAssetNameTest.php +++ b/test/unit/Platform/PrePackagedSourceAssetNameTest.php @@ -4,6 +4,10 @@ namespace Php\PieUnitTest\Platform; +use Composer\Package\CompletePackageInterface; +use Php\Pie\DependencyResolver\Package; +use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use Php\Pie\Platform\PrePackagedSourceAssetName; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -13,6 +17,21 @@ final class PrePackagedSourceAssetNameTest extends TestCase { public function testPackageNames(): void { - self::markTestIncomplete('todo'); // @todo + self::assertSame( + [ + 'php_foobar-1.2.3-src.tgz', + 'php_foobar-1.2.3-src.zip', + ], + PrePackagedSourceAssetName::packageNames( + new Package( + $this->createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ), + ), + ); } } From afd8a8ca95639171ce0d673fa1f9e09c348946a5 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 7 Feb 2025 12:04:43 +0000 Subject: [PATCH 15/35] Fix help for package+version command hint --- src/Command/CommandHelper.php | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index 8614fd2f..a4563145 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -64,6 +64,12 @@ public static function configurePhpConfigOptions(Command $command): void InputOption::VALUE_REQUIRED, 'The path to the `php` binary to use as the target PHP platform on ' . OperatingSystem::Windows->asFriendlyName() . ', e.g. --' . self::OPTION_WITH_PHP_PATH . '=C:\usr\php7.4.33\php.exe', ); + $command->addOption( + self::OPTION_WITH_PHPIZE_PATH, + null, + InputOption::VALUE_REQUIRED, + 'The path to the `phpize` binary to use as the target PHP platform, e.g. --' . self::OPTION_WITH_PHPIZE_PATH . '=/usr/bin/phpize7.4', + ); } public static function configureDownloadBuildInstallOptions(Command $command): void @@ -71,7 +77,7 @@ public static function configureDownloadBuildInstallOptions(Command $command): v $command->addArgument( self::ARG_REQUESTED_PACKAGE_AND_VERSION, InputArgument::REQUIRED, - 'The extension name and version constraint to use, in the format {ext-name}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.', + 'The PIE package name and version constraint to use, in the format {vendor/package}{?:{?version-constraint}{?@stability}}, for example `xdebug/xdebug:^3.4@alpha`, `xdebug/xdebug:@alpha`, `xdebug/xdebug:^3.4`, etc.', ); $command->addOption( self::OPTION_MAKE_PARALLEL_JOBS, @@ -79,12 +85,6 @@ public static function configureDownloadBuildInstallOptions(Command $command): v InputOption::VALUE_REQUIRED, 'Override many jobs to run in parallel when running compiling (this is passed to "make -jN" during build). PIE will try to detect this by default.', ); - $command->addOption( - self::OPTION_WITH_PHPIZE_PATH, - null, - InputOption::VALUE_REQUIRED, - 'The path to the `phpize` binary to use as the target PHP platform, e.g. --' . self::OPTION_WITH_PHPIZE_PATH . '=/usr/bin/phpize7.4', - ); $command->addOption( self::OPTION_SKIP_ENABLE_EXTENSION, null, From eccc8acc9016e11798f86a4f71c71b85bcf959e3 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Fri, 7 Feb 2025 12:12:05 +0000 Subject: [PATCH 16/35] Split out PIE package installed.json processor --- src/Command/ShowCommand.php | 53 +------------- src/Platform/InstalledPiePackages.php | 71 +++++++++++++++++++ .../Platform/InstalledPiePackagesTest.php | 18 +++++ 3 files changed, 92 insertions(+), 50 deletions(-) create mode 100644 src/Platform/InstalledPiePackages.php create mode 100644 test/unit/Platform/InstalledPiePackagesTest.php diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index 2c07282d..ad9c9dd8 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -4,25 +4,16 @@ namespace Php\Pie\Command; -use Composer\Package\BasePackage; -use Composer\Package\CompletePackageInterface; use Php\Pie\BinaryFile; -use Php\Pie\ComposerIntegration\PieComposerFactory; -use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; -use Php\Pie\DependencyResolver\Package; +use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\OperatingSystem; -use Php\Pie\Platform\TargetPlatform; -use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; -use function array_combine; -use function array_filter; use function array_key_exists; -use function array_map; use function array_walk; use function file_exists; use function sprintf; @@ -38,7 +29,7 @@ final class ShowCommand extends Command { public function __construct( - private readonly ContainerInterface $container, + private readonly InstalledPiePackages $installedPiePackages, ) { parent::__construct(); } @@ -54,7 +45,7 @@ public function execute(InputInterface $input, OutputInterface $output): int { $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); - $piePackages = $this->buildListOfPieInstalledPackages($output, $targetPlatform); + $piePackages = $this->installedPiePackages->allPiePackages($targetPlatform); $phpEnabledExtensions = $targetPlatform->phpBinaryPath->extensions(); $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); $extensionEnding = $targetPlatform->operatingSystem === OperatingSystem::Windows ? '.dll' : '.so'; @@ -121,42 +112,4 @@ private static function verifyChecksumInformation( return ' ✅'; } - - /** @return array */ - private function buildListOfPieInstalledPackages( - OutputInterface $output, - TargetPlatform $targetPlatform, - ): array { - $composerInstalledPackages = array_map( - static function (CompletePackageInterface $package): Package { - return Package::fromComposerCompletePackage($package); - }, - array_filter( - PieComposerFactory::createPieComposer( - $this->container, - PieComposerRequest::noOperation( - $output, - $targetPlatform, - ), - ) - ->getRepositoryManager() - ->getLocalRepository() - ->getPackages(), - static function (BasePackage $basePackage): bool { - return $basePackage instanceof CompletePackageInterface; - }, - ), - ); - - return array_combine( - array_map( - /** @return non-empty-string */ - static function (Package $package): string { - return $package->extensionName()->name(); - }, - $composerInstalledPackages, - ), - $composerInstalledPackages, - ); - } } diff --git a/src/Platform/InstalledPiePackages.php b/src/Platform/InstalledPiePackages.php new file mode 100644 index 00000000..aa1e496d --- /dev/null +++ b/src/Platform/InstalledPiePackages.php @@ -0,0 +1,71 @@ + + */ +class InstalledPiePackages +{ + /** @psalm-suppress PossiblyUnusedMethod no direct reference; used in service locator */ + public function __construct(private readonly ContainerInterface $container) + { + } + + /** + * Returns a list of PIE packages according to PIE; this does NOT check if + * the extension is actually enabled in the target PHP. + * + * @return ListOfPiePackages + */ + public function allPiePackages(TargetPlatform $targetPlatform): array + { + $composerInstalledPackages = array_map( + static function (CompletePackageInterface $package): Package { + return Package::fromComposerCompletePackage($package); + }, + array_filter( + PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + new NullOutput(), + $targetPlatform, + ), + ) + ->getRepositoryManager() + ->getLocalRepository() + ->getPackages(), + static function (BasePackage $basePackage): bool { + return $basePackage instanceof CompletePackageInterface; + }, + ), + ); + + return array_combine( + array_map( + /** @return non-empty-string */ + static function (Package $package): string { + return $package->extensionName()->name(); + }, + $composerInstalledPackages, + ), + $composerInstalledPackages, + ); + } +} diff --git a/test/unit/Platform/InstalledPiePackagesTest.php b/test/unit/Platform/InstalledPiePackagesTest.php new file mode 100644 index 00000000..628ca7d2 --- /dev/null +++ b/test/unit/Platform/InstalledPiePackagesTest.php @@ -0,0 +1,18 @@ + Date: Fri, 7 Feb 2025 12:12:34 +0000 Subject: [PATCH 17/35] Do not purge packages so they remain available in pie show even after vendor cleanup --- src/ComposerIntegration/PieComposerFactory.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index 7acb2352..d3ace61c 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -9,6 +9,7 @@ use Composer\Installer; use Composer\IO\IOInterface; use Composer\PartialComposer; +use Composer\Repository\InstalledRepositoryInterface; use Composer\Util\Filesystem; use Composer\Util\ProcessExecutor; use Php\Pie\ComposerIntegration\Listeners\OverrideDownloadUrlInstallListener; @@ -70,6 +71,21 @@ public static function createPieComposer( return $composer; } + protected function purgePackages(InstalledRepositoryInterface $repo, Installer\InstallationManager $im): void + { + /** + * This is intentionally a no-op in PIE.... + * + * Why not purge packages? + * + * We have a post install job in {@see VendorCleanup} that cleans up the vendor directory to remove all the + * actual package files; however, this means that Composer thinks they are not installed after that. When + * creating the Composer instance, the last step is to purge packages from the + * {@see InstalledRepositoryInterface} if they no longer exist on disk. But, that means we can't list the + * packages installed with PIE any more! So, we override this method to become a no-op ✅ + */ + } + public static function recreatePieComposer( ContainerInterface $container, Composer $existingComposer, From b3ade18c3275b8320fbb1f27ea2756409a7769b1 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 12 Feb 2025 08:55:46 +0000 Subject: [PATCH 18/35] Improve BinaryFile with verify methods --- src/BinaryFile.php | 24 +++++++ src/Command/ShowCommand.php | 21 ++++-- .../BinaryFileFailedVerification.php | 33 +++++++++ src/Util/FileNotFound.php | 20 ++++++ test/unit/BinaryFileTest.php | 67 +++++++++++++++++++ 5 files changed, 159 insertions(+), 6 deletions(-) create mode 100644 src/Installing/BinaryFileFailedVerification.php create mode 100644 src/Util/FileNotFound.php create mode 100644 test/unit/BinaryFileTest.php diff --git a/src/BinaryFile.php b/src/BinaryFile.php index 54611187..50cac2ed 100644 --- a/src/BinaryFile.php +++ b/src/BinaryFile.php @@ -4,6 +4,9 @@ namespace Php\Pie; +use Php\Pie\Installing\BinaryFileFailedVerification; + +use function file_exists; use function hash_file; /** @@ -33,4 +36,25 @@ public static function fromFileWithSha256Checksum(string $filePath): self hash_file(self::HASH_TYPE_SHA256, $filePath), ); } + + public function verify(): void + { + if (! file_exists($this->filePath)) { + throw Util\FileNotFound::fromFilename($this->filePath); + } + + self::verifyAgainstOther(self::fromFileWithSha256Checksum($this->filePath)); + } + + /** @throws BinaryFileFailedVerification */ + public function verifyAgainstOther(self $other): void + { + if ($this->filePath !== $other->filePath) { + throw BinaryFileFailedVerification::fromFilenameMismatch($this, $other); + } + + if ($other->checksum !== $this->checksum) { + throw BinaryFileFailedVerification::fromChecksumMismatch($this, $other); + } + } } diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index ad9c9dd8..6986784e 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -6,6 +6,7 @@ use Php\Pie\BinaryFile; use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; +use Php\Pie\Installing\BinaryFileFailedVerification; use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\OperatingSystem; use Symfony\Component\Console\Attribute\AsCommand; @@ -90,10 +91,10 @@ private static function verifyChecksumInformation( string $extensionEnding, array $installedJsonMetadata, ): string { - $expectedConventionalBinaryPath = $extensionPath . DIRECTORY_SEPARATOR . $phpExtensionName . $extensionEnding; + $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($expectedConventionalBinaryPath)) { + if (! file_exists($actualBinaryPathByConvention)) { return ''; } @@ -101,13 +102,21 @@ private static function verifyChecksumInformation( $pieExpectedChecksum = array_key_exists(PieInstalledJsonMetadataKeys::BinaryChecksum->value, $installedJsonMetadata) ? $installedJsonMetadata[PieInstalledJsonMetadataKeys::BinaryChecksum->value] : null; // Some other kind of mismatch of file path, or we don't have a stored checksum available - if ($expectedConventionalBinaryPath !== $pieExpectedBinaryPath || $pieExpectedChecksum === null) { + if ( + $pieExpectedBinaryPath === null + || $pieExpectedChecksum === null + || $pieExpectedBinaryPath !== $actualBinaryPathByConvention + ) { return ''; } - $actualInstalledBinary = BinaryFile::fromFileWithSha256Checksum($expectedConventionalBinaryPath); - if ($actualInstalledBinary->checksum !== $pieExpectedChecksum) { - return ' ⚠️ was ' . substr($actualInstalledBinary->checksum, 0, 8) . '..., expected ' . substr($pieExpectedChecksum, 0, 8) . '...'; + $expectedBinaryFileFromMetadata = new BinaryFile($pieExpectedBinaryPath, $pieExpectedChecksum); + $actualBinaryFile = BinaryFile::fromFileWithSha256Checksum($actualBinaryPathByConvention); + + try { + $expectedBinaryFileFromMetadata->verifyAgainstOther($actualBinaryFile); + } catch (BinaryFileFailedVerification) { + return ' ⚠️ was ' . substr($actualBinaryFile->checksum, 0, 8) . '..., expected ' . substr($expectedBinaryFileFromMetadata->checksum, 0, 8) . '...'; } return ' ✅'; diff --git a/src/Installing/BinaryFileFailedVerification.php b/src/Installing/BinaryFileFailedVerification.php new file mode 100644 index 00000000..dbd8f228 --- /dev/null +++ b/src/Installing/BinaryFileFailedVerification.php @@ -0,0 +1,33 @@ +filePath, + $actual->filePath, + )); + } + + public static function fromChecksumMismatch(BinaryFile $expected, BinaryFile $actual): self + { + return new self(sprintf( + 'File "%s" failed checksum verification. Expected %s..., was %s...', + $expected->filePath, + substr($expected->checksum, 0, 8), + substr($actual->checksum, 0, 8), + )); + } +} diff --git a/src/Util/FileNotFound.php b/src/Util/FileNotFound.php new file mode 100644 index 00000000..445bfe50 --- /dev/null +++ b/src/Util/FileNotFound.php @@ -0,0 +1,20 @@ +expectNotToPerformAssertions(); + $expectation->verify(); + } + + public function testVerifyFailsWithFileThatDoesNotExist(): void + { + $expectation = new BinaryFile( + '/path/to/a/file/that/does/not/exist', + self::TEST_FILE_HASH, + ); + + $this->expectException(FileNotFound::class); + $expectation->verify(); + } + + public function testVerifyFailsWithWrongHash(): void + { + $expectation = new BinaryFile( + self::TEST_FILE, + 'another hash that is wrong', + ); + + $this->expectException(BinaryFileFailedVerification::class); + $this->expectExceptionMessageMatches('/File "[^"]+" failed checksum verification\. Expected [^\.]+\.\.\., was [^\.]+\.\.\./'); + $expectation->verify(); + } + + public function testVerifyFailsWithDifferentFile(): void + { + $expectation = new BinaryFile( + self::TEST_FILE, + self::TEST_FILE_HASH, + ); + + $this->expectException(BinaryFileFailedVerification::class); + $this->expectExceptionMessageMatches('/Expected file "[^"]+" but actual file was "[^"]+"/'); + $expectation->verifyAgainstOther(new BinaryFile( + __FILE__, + self::TEST_FILE_HASH, + )); + } +} From 21214108f47fabe58d95a07dd2dacc933386e129 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 12 Feb 2025 09:51:55 +0000 Subject: [PATCH 19/35] Added component to remove INI entries for a package --- src/Container.php | 2 + src/Installing/Ini/RemoveIniEntry.php | 15 ++++ .../Ini/RemoveIniEntryWithFileGetContents.php | 89 +++++++++++++++++++ .../RemoveIniEntryWithFileGetContentsTest.php | 18 ++++ 4 files changed, 124 insertions(+) create mode 100644 src/Installing/Ini/RemoveIniEntry.php create mode 100644 src/Installing/Ini/RemoveIniEntryWithFileGetContents.php create mode 100644 test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php diff --git a/src/Container.php b/src/Container.php index 6f8ef418..8ddcae7d 100644 --- a/src/Container.php +++ b/src/Container.php @@ -107,6 +107,8 @@ static function (ContainerInterface $container): Install { }, ); + $container->alias(Ini\RemoveIniEntryWithFileGetContents::class, Ini\RemoveIniEntry::class); + return $container; } } diff --git a/src/Installing/Ini/RemoveIniEntry.php b/src/Installing/Ini/RemoveIniEntry.php new file mode 100644 index 00000000..fb5c7fd5 --- /dev/null +++ b/src/Installing/Ini/RemoveIniEntry.php @@ -0,0 +1,15 @@ + Returns a list of INI files that were updated to remove the extension */ + public function __invoke(Package $package, TargetPlatform $targetPlatform): array; +} diff --git a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php new file mode 100644 index 00000000..df8579f1 --- /dev/null +++ b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php @@ -0,0 +1,89 @@ + Returns a list of INI files that were updated to remove the extension */ + public function __invoke(Package $package, TargetPlatform $targetPlatform): array + { + $allIniFiles = [$targetPlatform->phpBinaryPath->loadedIniConfigurationFile()]; + + $additionalIniDirectory = $targetPlatform->phpBinaryPath->additionalIniDirectory(); + if ($additionalIniDirectory !== null) { + $allIniFiles = array_merge( + array_map( + static function (string $path) use ($additionalIniDirectory): string { + return $additionalIniDirectory . DIRECTORY_SEPARATOR . $path; + }, + array_filter( + scandir($additionalIniDirectory), + static function (string $path) use ($additionalIniDirectory): bool { + if (in_array($path, ['.', '..'])) { + return false; + } + + return file_exists($additionalIniDirectory . DIRECTORY_SEPARATOR . $path); + }, + ), + ), + $allIniFiles, + ); + } + + $regex = sprintf( + '/^(%s\w*=\w*%s)$/m', + $package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension', + $package->extensionName()->name(), + ); + + $updatedIniFiles = []; + array_walk( + $allIniFiles, + static function (string $iniFile) use (&$updatedIniFiles, $regex): void { + $currentContent = file_get_contents($iniFile); + + if ($currentContent === false || $currentContent === '') { + return; + } + + $replacedContent = preg_replace( + $regex, + '; $1 ; removed by PIE', + $currentContent, + ); + + if ($replacedContent === null || $replacedContent === $currentContent) { + return; + } + + // @todo verify it was written; permissions may have failed etc + file_put_contents($iniFile, $replacedContent); + $updatedIniFiles[] = $iniFile; + }, + ); + + return $updatedIniFiles; + } +} diff --git a/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php b/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php new file mode 100644 index 00000000..f658f2f6 --- /dev/null +++ b/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php @@ -0,0 +1,18 @@ + Date: Wed, 12 Feb 2025 10:52:50 +0000 Subject: [PATCH 20/35] Added component to uninstall the installed binary --- src/Container.php | 4 ++ src/Installing/PackageMetadataMissing.php | 33 ++++++++++++++ src/Installing/Uninstall.php | 13 ++++++ src/Installing/UninstallUsingUnlink.php | 45 +++++++++++++++++++ .../Installing/PackageMetadataMissingTest.php | 43 ++++++++++++++++++ .../Installing/UninstallUsingUnlinkTest.php | 23 ++++++++++ 6 files changed, 161 insertions(+) create mode 100644 src/Installing/PackageMetadataMissing.php create mode 100644 src/Installing/Uninstall.php create mode 100644 src/Installing/UninstallUsingUnlink.php create mode 100644 test/unit/Installing/PackageMetadataMissingTest.php create mode 100644 test/unit/Installing/UninstallUsingUnlinkTest.php diff --git a/src/Container.php b/src/Container.php index 8ddcae7d..e13ad639 100644 --- a/src/Container.php +++ b/src/Container.php @@ -25,6 +25,8 @@ use Php\Pie\Downloading\PackageReleaseAssets; use Php\Pie\Installing\Ini; use Php\Pie\Installing\Install; +use Php\Pie\Installing\Uninstall; +use Php\Pie\Installing\UninstallUsingUnlink; use Php\Pie\Installing\UnixInstall; use Php\Pie\Installing\WindowsInstall; use Psr\Container\ContainerInterface; @@ -107,6 +109,8 @@ static function (ContainerInterface $container): Install { }, ); + $container->alias(UninstallUsingUnlink::class, Uninstall::class); + $container->alias(Ini\RemoveIniEntryWithFileGetContents::class, Ini\RemoveIniEntry::class); return $container; diff --git a/src/Installing/PackageMetadataMissing.php b/src/Installing/PackageMetadataMissing.php new file mode 100644 index 00000000..50c5ff7a --- /dev/null +++ b/src/Installing/PackageMetadataMissing.php @@ -0,0 +1,33 @@ + $actualMetadata + * @param list $wantedKeys + */ + public static function duringUninstall(Package $package, array $actualMetadata, array $wantedKeys): self + { + $missingKeys = array_diff($wantedKeys, array_keys($actualMetadata)); + + return new self(sprintf( + 'PIE metadata was missing for package %s. Missing metadata key%s: %s', + $package->name(), + count($missingKeys) === 1 ? '' : 's', + implode(', ', $missingKeys), + )); + } +} diff --git a/src/Installing/Uninstall.php b/src/Installing/Uninstall.php new file mode 100644 index 00000000..16ffac48 --- /dev/null +++ b/src/Installing/Uninstall.php @@ -0,0 +1,13 @@ +composerPackage()); + + if ( + ! array_key_exists(PieInstalledJsonMetadataKeys::InstalledBinary->value, $pieMetadata) + || ! array_key_exists(PieInstalledJsonMetadataKeys::BinaryChecksum->value, $pieMetadata) + ) { + throw PackageMetadataMissing::duringUninstall( + $package, + $pieMetadata, + [ + PieInstalledJsonMetadataKeys::InstalledBinary->value, + PieInstalledJsonMetadataKeys::BinaryChecksum->value, + ], + ); + } + + $expectedBinaryFile = new BinaryFile( + $pieMetadata[PieInstalledJsonMetadataKeys::InstalledBinary->value], + $pieMetadata[PieInstalledJsonMetadataKeys::BinaryChecksum->value], + ); + + $expectedBinaryFile->verify(); + + unlink($expectedBinaryFile->filePath); + // @todo verify the unlink worked etc, maybe permissions failed + } +} diff --git a/test/unit/Installing/PackageMetadataMissingTest.php b/test/unit/Installing/PackageMetadataMissingTest.php new file mode 100644 index 00000000..5fdc5a78 --- /dev/null +++ b/test/unit/Installing/PackageMetadataMissingTest.php @@ -0,0 +1,43 @@ +createMock(CompletePackageInterface::class), + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foo/bar', + '1.2.3', + null, + ); + + $exception = PackageMetadataMissing::duringUninstall( + $package, + [ + 'a' => 'something', + 'b' => 'something else', + ], + ['b', 'c', 'd'], + ); + + self::assertSame( + 'PIE metadata was missing for package foo/bar. Missing metadata keys: c, d', + $exception->getMessage(), + ); + } +} diff --git a/test/unit/Installing/UninstallUsingUnlinkTest.php b/test/unit/Installing/UninstallUsingUnlinkTest.php new file mode 100644 index 00000000..e6a8e546 --- /dev/null +++ b/test/unit/Installing/UninstallUsingUnlinkTest.php @@ -0,0 +1,23 @@ + Date: Mon, 17 Feb 2025 20:38:55 +0000 Subject: [PATCH 21/35] ComposerIntegrationHandler no longer __invoke --- src/Command/BuildCommand.php | 2 +- src/Command/DownloadCommand.php | 2 +- src/Command/InstallCommand.php | 2 +- src/ComposerIntegration/ComposerIntegrationHandler.php | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index b118d682..fd5588fa 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -85,7 +85,7 @@ public function execute(InputInterface $input, OutputInterface $output): int ); try { - ($this->composerIntegrationHandler)( + $this->composerIntegrationHandler->runInstall( $package, $composer, $targetPlatform, diff --git a/src/Command/DownloadCommand.php b/src/Command/DownloadCommand.php index 7d27adde..9c156459 100644 --- a/src/Command/DownloadCommand.php +++ b/src/Command/DownloadCommand.php @@ -69,7 +69,7 @@ public function execute(InputInterface $input, OutputInterface $output): int $output->writeln(sprintf('Found package: %s which provides %s', $package->prettyNameAndVersion(), $package->extensionName()->nameWithExtPrefix())); try { - ($this->composerIntegrationHandler)( + $this->composerIntegrationHandler->runInstall( $package, $composer, $targetPlatform, diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index 068f3c36..ae405949 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -90,7 +90,7 @@ public function execute(InputInterface $input, OutputInterface $output): int ); try { - ($this->composerIntegrationHandler)( + $this->composerIntegrationHandler->runInstall( $package, $composer, $targetPlatform, diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index d9b8f7b2..ce9543d2 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -26,7 +26,7 @@ public function __construct( ) { } - public function __invoke( + public function runInstall( Package $package, Composer $composer, TargetPlatform $targetPlatform, From 876206569b72310f088ca599e3d0435261511d0e Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 17 Feb 2025 21:06:44 +0000 Subject: [PATCH 22/35] Add method to remove a require from pie.json --- src/ComposerIntegration/PieJsonEditor.php | 20 +++++++++++++++++++ .../ComposerIntegration/PieJsonEditorTest.php | 6 ++++++ 2 files changed, 26 insertions(+) diff --git a/src/ComposerIntegration/PieJsonEditor.php b/src/ComposerIntegration/PieJsonEditor.php index 973bfbff..475bfc47 100644 --- a/src/ComposerIntegration/PieJsonEditor.php +++ b/src/ComposerIntegration/PieJsonEditor.php @@ -82,6 +82,26 @@ public function addRequire(string $package, string $version): string return $originalPieJsonContent; } + /** + * Remove a package from the `require` section of the given `pie.json`. + * Returns the original `pie.json` content, in case it needs to be + * restored later. + * + * @param non-empty-string $package + */ + public function removeRequire(string $package): string + { + $originalPieJsonContent = file_get_contents($this->pieJsonFilename); + + (new JsonConfigSource( + new JsonFile( + $this->pieJsonFilename, + ), + ))->removeLink('require', $package); + + return $originalPieJsonContent; + } + public function revert(string $originalPieJsonContent): void { file_put_contents($this->pieJsonFilename, $originalPieJsonContent); diff --git a/test/unit/ComposerIntegration/PieJsonEditorTest.php b/test/unit/ComposerIntegration/PieJsonEditorTest.php index 29edc044..164ad9e2 100644 --- a/test/unit/ComposerIntegration/PieJsonEditorTest.php +++ b/test/unit/ComposerIntegration/PieJsonEditorTest.php @@ -53,6 +53,12 @@ public function testCanAddRequire(): void EOF), $this->normaliseJson(file_get_contents($testPieJson)), ); + + $editor->removeRequire('foo/bar'); + self::assertSame( + $this->normaliseJson('{}'), + $this->normaliseJson(file_get_contents($testPieJson)), + ); } public function testCanRevert(): void From 9fac585fcafd38416f0ab434c6d3dd0edc52c01b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 17 Feb 2025 21:07:59 +0000 Subject: [PATCH 23/35] Added uninstall to PieOperation enum --- src/ComposerIntegration/PieOperation.php | 1 + test/unit/ComposerIntegration/PieOperationTest.php | 2 ++ 2 files changed, 3 insertions(+) diff --git a/src/ComposerIntegration/PieOperation.php b/src/ComposerIntegration/PieOperation.php index a3345d51..1967ab2e 100644 --- a/src/ComposerIntegration/PieOperation.php +++ b/src/ComposerIntegration/PieOperation.php @@ -11,6 +11,7 @@ enum PieOperation case Download; case Build; case Install; + case Uninstall; public function shouldBuild(): bool { diff --git a/test/unit/ComposerIntegration/PieOperationTest.php b/test/unit/ComposerIntegration/PieOperationTest.php index 8387800a..a6f00dd2 100644 --- a/test/unit/ComposerIntegration/PieOperationTest.php +++ b/test/unit/ComposerIntegration/PieOperationTest.php @@ -17,6 +17,7 @@ public function testShouldBuild(): void self::assertFalse(PieOperation::Download->shouldBuild()); self::assertTrue(PieOperation::Build->shouldBuild()); self::assertTrue(PieOperation::Install->shouldBuild()); + self::assertFalse(PieOperation::Uninstall->shouldBuild()); } public function testShouldInstall(): void @@ -25,5 +26,6 @@ public function testShouldInstall(): void self::assertFalse(PieOperation::Download->shouldInstall()); self::assertFalse(PieOperation::Build->shouldInstall()); self::assertTrue(PieOperation::Install->shouldInstall()); + self::assertFalse(PieOperation::Uninstall->shouldBuild()); } } From 742d0ca34f68f27c37168993c7e01d8ed0f74d3c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 17 Feb 2025 21:30:08 +0000 Subject: [PATCH 24/35] Added ComposerIntegrationHandler#runUninstall --- .../ComposerIntegrationHandler.php | 42 +++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/src/ComposerIntegration/ComposerIntegrationHandler.php b/src/ComposerIntegration/ComposerIntegrationHandler.php index ce9543d2..4c51f724 100644 --- a/src/ComposerIntegration/ComposerIntegrationHandler.php +++ b/src/ComposerIntegration/ComposerIntegrationHandler.php @@ -88,4 +88,46 @@ public function runInstall( ($this->vendorCleanup)($composer); } + + public function runUninstall( + Package $packageToRemove, + Composer $composer, + TargetPlatform $targetPlatform, + RequestedPackageAndVersion $requestedPackageAndVersionToRemove, + ): void { + // Write the new requirement to pie.json; because we later essentially just do a `composer install` using that file + $pieComposerJson = Platform::getPieJsonFilename($targetPlatform); + $pieJsonEditor = PieJsonEditor::fromTargetPlatform($targetPlatform); + $originalPieJsonContent = $pieJsonEditor->removeRequire($requestedPackageAndVersionToRemove->package); + + // Refresh the Composer instance so it re-reads the updated pie.json + $composer = PieComposerFactory::recreatePieComposer($this->container, $composer); + + $composerInstaller = PieComposerInstaller::createWithPhpBinary( + $targetPlatform->phpBinaryPath, + $packageToRemove->extensionName(), + $this->arrayCollectionIo, + $composer, + ); + $composerInstaller + ->setAllowedTypes(['php-ext', 'php-ext-zend']) + ->setInstall(true) + ->setIgnoredTypes([]) + ->setDryRun(false) + ->setDownloadOnly(false); + + if (file_exists(PieComposerFactory::getLockFile($pieComposerJson))) { + $composerInstaller->setUpdate(true); + $composerInstaller->setUpdateAllowList([$requestedPackageAndVersionToRemove->package]); + } + + $resultCode = $composerInstaller->run(); + + if ($resultCode !== Installer::ERROR_NONE) { + // Revert composer.json change + $pieJsonEditor->revert($originalPieJsonContent); + + throw ComposerRunFailed::fromExitCode($resultCode); + } + } } From 058a5ef874ac50d340bb707f47e199104f4172a1 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 17 Feb 2025 22:09:10 +0000 Subject: [PATCH 25/35] Added Composer uninstall processor for PiePackageInstaller --- bin/pie | 2 + src/Command/UninstallCommand.php | 103 ++++++++++++++++++ .../PieComposerFactory.php | 1 + .../PiePackageInstaller.php | 42 +++++++ src/ComposerIntegration/UninstallProcess.php | 48 ++++++++ src/Container.php | 2 + 6 files changed, 198 insertions(+) create mode 100644 src/Command/UninstallCommand.php create mode 100644 src/ComposerIntegration/UninstallProcess.php diff --git a/bin/pie b/bin/pie index 2b907bb1..caa0a2f4 100755 --- a/bin/pie +++ b/bin/pie @@ -13,6 +13,7 @@ use Php\Pie\Command\RepositoryAddCommand; use Php\Pie\Command\RepositoryListCommand; use Php\Pie\Command\RepositoryRemoveCommand; use Php\Pie\Command\ShowCommand; +use Php\Pie\Command\UninstallCommand; use Php\Pie\Util\PieVersion; use Symfony\Component\Console\Application; use Symfony\Component\Console\CommandLoader\ContainerCommandLoader; @@ -37,6 +38,7 @@ $application->setCommandLoader(new ContainerCommandLoader( 'repository:list' => RepositoryListCommand::class, 'repository:add' => RepositoryAddCommand::class, 'repository:remove' => RepositoryRemoveCommand::class, + 'uninstall' => UninstallCommand::class, ] )); diff --git a/src/Command/UninstallCommand.php b/src/Command/UninstallCommand.php new file mode 100644 index 00000000..71f041d0 --- /dev/null +++ b/src/Command/UninstallCommand.php @@ -0,0 +1,103 @@ +addArgument( + self::ARG_PACKAGE_NAME, + InputArgument::REQUIRED, + 'The package name to remove, in the format {vendor/package}, for example `xdebug/xdebug`', + ); + + CommandHelper::configurePhpConfigOptions($this); + } + + public function execute(InputInterface $input, OutputInterface $output): int + { + $packageToRemove = (string) $input->getArgument(self::ARG_PACKAGE_NAME); + Assert::stringNotEmpty($packageToRemove); + $requestedPackageAndVersionToRemove = new RequestedPackageAndVersion($packageToRemove, null); + + $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); + + $piePackage = $this->findPiePackageByPackageName($packageToRemove, $targetPlatform); + + if ($piePackage === null) { + $output->writeln('No package found: ' . $packageToRemove . ''); + + return 1; + } + + $composer = PieComposerFactory::createPieComposer( + $this->container, + new PieComposerRequest( + $output, + $targetPlatform, + $requestedPackageAndVersionToRemove, + PieOperation::Uninstall, + [], // Configure options are not needed for uninstall + null, + true, + ), + ); + + $this->composerIntegrationHandler->runUninstall( + $piePackage, + $composer, + $targetPlatform, + $requestedPackageAndVersionToRemove, + ); + + return 0; + } + + private function findPiePackageByPackageName(string $packageToRemove, TargetPlatform $targetPlatform): Package|null + { + $piePackages = $this->installedPiePackages->allPiePackages($targetPlatform); + + foreach ($piePackages as $piePackage) { + if ($piePackage->name() === $packageToRemove) { + return $piePackage; + } + } + + return null; + } +} diff --git a/src/ComposerIntegration/PieComposerFactory.php b/src/ComposerIntegration/PieComposerFactory.php index d3ace61c..ba9dcd28 100644 --- a/src/ComposerIntegration/PieComposerFactory.php +++ b/src/ComposerIntegration/PieComposerFactory.php @@ -39,6 +39,7 @@ protected function createDefaultInstallers(Installer\InstallationManager $im, Pa $type, $fs, $this->container->get(InstallAndBuildProcess::class), + $this->container->get(UninstallProcess::class), $this->composerRequest, ); }; diff --git a/src/ComposerIntegration/PiePackageInstaller.php b/src/ComposerIntegration/PiePackageInstaller.php index d2185cc9..66cdc766 100644 --- a/src/ComposerIntegration/PiePackageInstaller.php +++ b/src/ComposerIntegration/PiePackageInstaller.php @@ -25,6 +25,7 @@ public function __construct( ExtensionType $type, Filesystem $filesystem, private readonly InstallAndBuildProcess $installAndBuildProcess, + private readonly UninstallProcess $uninstallProcess, private readonly PieComposerRequest $composerRequest, ) { parent::__construct($io, $composer, $type->value, $filesystem); @@ -71,4 +72,45 @@ public function install(InstalledRepositoryInterface $repo, PackageInterface $pa return null; }); } + + /** @inheritDoc */ + public function uninstall(InstalledRepositoryInterface $repo, PackageInterface $package) + { + $composerPackage = $package; + + // @todo check into why not being removed from `vendor/composer/installed.json` + return parent::uninstall($repo, $composerPackage) + ?->then(function () use ($composerPackage) { + $output = $this->composerRequest->pieOutput; + + if ($this->composerRequest->requestedPackage->package !== $composerPackage->getName()) { + $output->writeln( + sprintf( + 'Skipping %s uninstall request from Composer as it was not the expected PIE package %s', + $composerPackage->getName(), + $this->composerRequest->requestedPackage->package, + ), + OutputInterface::VERBOSITY_VERY_VERBOSE, + ); + + return null; + } + + if (! $composerPackage instanceof CompletePackage) { + $output->writeln(sprintf( + 'Not using PIE to install %s as it was not a Complete Package', + $composerPackage->getName(), + )); + + return null; + } + + ($this->uninstallProcess)( + $this->composerRequest, + $composerPackage, + ); + + return null; + }); + } } diff --git a/src/ComposerIntegration/UninstallProcess.php b/src/ComposerIntegration/UninstallProcess.php new file mode 100644 index 00000000..06f1d821 --- /dev/null +++ b/src/ComposerIntegration/UninstallProcess.php @@ -0,0 +1,48 @@ +pieOutput; + + $piePackage = Package::fromComposerCompletePackage($composerPackage); + + $affectedIniFiles = ($this->removeIniEntry)($piePackage, $composerRequest->targetPlatform); + + if (count($affectedIniFiles) === 1) { + $output->writeln(sprintf('INI file "%s" was updated to remove the extension.', reset($affectedIniFiles))); + } elseif (count($affectedIniFiles) === 0) { + $output->writeln('No INI files were updated to remove the extension.'); + } else { + $output->writeln('The following INI files were updated to remove the extnesion:'); + array_walk($affectedIniFiles, static fn (string $ini) => $output->writeln(' - ' . $ini)); + } + + ($this->uninstall)($piePackage); + } +} diff --git a/src/Container.php b/src/Container.php index e13ad639..351db890 100644 --- a/src/Container.php +++ b/src/Container.php @@ -17,6 +17,7 @@ use Php\Pie\Command\RepositoryListCommand; use Php\Pie\Command\RepositoryRemoveCommand; use Php\Pie\Command\ShowCommand; +use Php\Pie\Command\UninstallCommand; use Php\Pie\ComposerIntegration\MinimalHelperSet; use Php\Pie\ComposerIntegration\QuieterConsoleIO; use Php\Pie\DependencyResolver\DependencyResolver; @@ -54,6 +55,7 @@ public static function factory(): ContainerInterface $container->singleton(RepositoryListCommand::class); $container->singleton(RepositoryAddCommand::class); $container->singleton(RepositoryRemoveCommand::class); + $container->singleton(UninstallCommand::class); $container->singleton(QuieterConsoleIO::class, static function (ContainerInterface $container): QuieterConsoleIO { return new QuieterConsoleIO( From f305935cb4e8d0e1752dc172b950474d1c22ae4c Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 20 Feb 2025 20:21:58 +0000 Subject: [PATCH 26/35] Use Composer in InstalledPiePackages --- src/Command/ShowCommand.php | 15 ++++++++++- src/Command/UninstallCommand.php | 17 +++++++++--- src/Platform/InstalledPiePackages.php | 20 +++----------- .../Platform/InstalledPiePackagesTest.php | 26 ++++++++++++++++++- 4 files changed, 55 insertions(+), 23 deletions(-) diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index 6986784e..01e8c7f6 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -5,13 +5,17 @@ namespace Php\Pie\Command; use Php\Pie\BinaryFile; +use Php\Pie\ComposerIntegration\PieComposerFactory; +use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; use Php\Pie\Installing\BinaryFileFailedVerification; use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\OperatingSystem; +use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use function array_key_exists; @@ -31,6 +35,7 @@ final class ShowCommand extends Command { public function __construct( private readonly InstalledPiePackages $installedPiePackages, + private readonly ContainerInterface $container, ) { parent::__construct(); } @@ -46,7 +51,15 @@ public function execute(InputInterface $input, OutputInterface $output): int { $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); - $piePackages = $this->installedPiePackages->allPiePackages($targetPlatform); + $composer = PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + new NullOutput(), + $targetPlatform, + ), + ); + + $piePackages = $this->installedPiePackages->allPiePackages($composer); $phpEnabledExtensions = $targetPlatform->phpBinaryPath->extensions(); $extensionPath = $targetPlatform->phpBinaryPath->extensionPath(); $extensionEnding = $targetPlatform->operatingSystem === OperatingSystem::Windows ? '.dll' : '.so'; diff --git a/src/Command/UninstallCommand.php b/src/Command/UninstallCommand.php index 71f041d0..cb0c1dcb 100644 --- a/src/Command/UninstallCommand.php +++ b/src/Command/UninstallCommand.php @@ -4,6 +4,7 @@ namespace Php\Pie\Command; +use Composer\Composer; use Php\Pie\ComposerIntegration\ComposerIntegrationHandler; use Php\Pie\ComposerIntegration\PieComposerFactory; use Php\Pie\ComposerIntegration\PieComposerRequest; @@ -11,12 +12,12 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\Platform\InstalledPiePackages; -use Php\Pie\Platform\TargetPlatform; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; +use Symfony\Component\Console\Output\NullOutput; use Symfony\Component\Console\Output\OutputInterface; use Webmozart\Assert\Assert; @@ -57,7 +58,15 @@ public function execute(InputInterface $input, OutputInterface $output): int $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $output); - $piePackage = $this->findPiePackageByPackageName($packageToRemove, $targetPlatform); + $composer = PieComposerFactory::createPieComposer( + $this->container, + PieComposerRequest::noOperation( + new NullOutput(), + $targetPlatform, + ), + ); + + $piePackage = $this->findPiePackageByPackageName($packageToRemove, $composer); if ($piePackage === null) { $output->writeln('No package found: ' . $packageToRemove . ''); @@ -88,9 +97,9 @@ public function execute(InputInterface $input, OutputInterface $output): int return 0; } - private function findPiePackageByPackageName(string $packageToRemove, TargetPlatform $targetPlatform): Package|null + private function findPiePackageByPackageName(string $packageToRemove, Composer $composer): Package|null { - $piePackages = $this->installedPiePackages->allPiePackages($targetPlatform); + $piePackages = $this->installedPiePackages->allPiePackages($composer); foreach ($piePackages as $piePackage) { if ($piePackage->name() === $packageToRemove) { diff --git a/src/Platform/InstalledPiePackages.php b/src/Platform/InstalledPiePackages.php index aa1e496d..d303d9a8 100644 --- a/src/Platform/InstalledPiePackages.php +++ b/src/Platform/InstalledPiePackages.php @@ -4,13 +4,10 @@ namespace Php\Pie\Platform; +use Composer\Composer; use Composer\Package\BasePackage; use Composer\Package\CompletePackageInterface; -use Php\Pie\ComposerIntegration\PieComposerFactory; -use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\DependencyResolver\Package; -use Psr\Container\ContainerInterface; -use Symfony\Component\Console\Output\NullOutput; use function array_combine; use function array_filter; @@ -23,31 +20,20 @@ */ class InstalledPiePackages { - /** @psalm-suppress PossiblyUnusedMethod no direct reference; used in service locator */ - public function __construct(private readonly ContainerInterface $container) - { - } - /** * Returns a list of PIE packages according to PIE; this does NOT check if * the extension is actually enabled in the target PHP. * * @return ListOfPiePackages */ - public function allPiePackages(TargetPlatform $targetPlatform): array + public function allPiePackages(Composer $composer): array { $composerInstalledPackages = array_map( static function (CompletePackageInterface $package): Package { return Package::fromComposerCompletePackage($package); }, array_filter( - PieComposerFactory::createPieComposer( - $this->container, - PieComposerRequest::noOperation( - new NullOutput(), - $targetPlatform, - ), - ) + $composer ->getRepositoryManager() ->getLocalRepository() ->getPackages(), diff --git a/test/unit/Platform/InstalledPiePackagesTest.php b/test/unit/Platform/InstalledPiePackagesTest.php index 628ca7d2..22e6357a 100644 --- a/test/unit/Platform/InstalledPiePackagesTest.php +++ b/test/unit/Platform/InstalledPiePackagesTest.php @@ -4,6 +4,10 @@ namespace Php\PieUnitTest\Platform; +use Composer\Composer; +use Composer\Package\CompletePackage; +use Composer\Repository\InstalledRepositoryInterface; +use Composer\Repository\RepositoryManager; use Php\Pie\Platform\InstalledPiePackages; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -13,6 +17,26 @@ final class InstalledPiePackagesTest extends TestCase { public function testAllPiePackages(): void { - self::fail('to be implemented'); // @todo implement this test + $localRepo = $this->createMock(InstalledRepositoryInterface::class); + $localRepo->method('getPackages')->willReturn([ + new CompletePackage('foo/bar1', '1.2.3.0', '1.2.3'), + new CompletePackage('foo/bar2', '1.2.3.0', '1.2.3'), + ]); + + $repoManager = $this->createMock(RepositoryManager::class); + $repoManager->method('getLocalRepository')->willReturn($localRepo); + + $composer = $this->createMock(Composer::class); + $composer->method('getRepositoryManager')->willReturn($repoManager); + + $packages = (new InstalledPiePackages())->allPiePackages($composer); + + self::assertArrayHasKey('bar1', $packages); + self::assertArrayHasKey('bar2', $packages); + + self::assertSame('bar1', $packages['bar1']->extensionName()->name()); + self::assertSame('foo/bar1', $packages['bar1']->name()); + self::assertSame('bar2', $packages['bar2']->extensionName()->name()); + self::assertSame('foo/bar2', $packages['bar2']->name()); } } From 4b334704cd57c1992ebc6c00649bf8fa591845ed Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Mon, 24 Feb 2025 11:20:43 +0000 Subject: [PATCH 27/35] Fix uninstall and complete tests --- features/uninstall-extensions.feature | 6 + .../RemoveUnrelatedInstallOperations.php | 3 +- src/ComposerIntegration/UninstallProcess.php | 18 ++- .../Ini/RemoveIniEntryWithFileGetContents.php | 7 +- src/Installing/Uninstall.php | 3 +- src/Installing/UninstallUsingUnlink.php | 4 +- test/behaviour/CliContext.php | 21 ++++ .../RemoveIniEntryWithFileGetContentsTest.php | 112 +++++++++++++++++- .../Installing/UninstallUsingUnlinkTest.php | 57 ++++++++- 9 files changed, 219 insertions(+), 12 deletions(-) create mode 100644 features/uninstall-extensions.feature diff --git a/features/uninstall-extensions.feature b/features/uninstall-extensions.feature new file mode 100644 index 00000000..67d8c2dc --- /dev/null +++ b/features/uninstall-extensions.feature @@ -0,0 +1,6 @@ +Feature: Extensions can be uninstalled with PIE + + Example: The latest version of an extension can be downloaded + Given an extension was previously installed + When I run a command to uninstall an extension + Then the extension should not be installed anymore diff --git a/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php index eb7a8a44..905a276f 100644 --- a/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php +++ b/src/ComposerIntegration/Listeners/RemoveUnrelatedInstallOperations.php @@ -8,6 +8,7 @@ use Composer\Composer; use Composer\DependencyResolver\Operation\InstallOperation; use Composer\DependencyResolver\Operation\OperationInterface; +use Composer\DependencyResolver\Operation\UninstallOperation; use Composer\DependencyResolver\Transaction; use Composer\Installer\InstallerEvent; use Composer\Installer\InstallerEvents; @@ -49,7 +50,7 @@ public function __invoke(InstallerEvent $installerEvent): void $newOperations = array_filter( $installerEvent->getTransaction()?->getOperations() ?? [], function (OperationInterface $operation) use ($pieOutput): bool { - if (! $operation instanceof InstallOperation) { + if (! $operation instanceof InstallOperation && ! $operation instanceof UninstallOperation) { $pieOutput->writeln( sprintf( 'Unexpected operation during installer: %s', diff --git a/src/ComposerIntegration/UninstallProcess.php b/src/ComposerIntegration/UninstallProcess.php index 06f1d821..781fd0b8 100644 --- a/src/ComposerIntegration/UninstallProcess.php +++ b/src/ComposerIntegration/UninstallProcess.php @@ -8,6 +8,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Installing\Ini\RemoveIniEntry; use Php\Pie\Installing\Uninstall; +use Symfony\Component\Console\Output\OutputInterface; use function array_walk; use function count; @@ -35,14 +36,23 @@ public function __invoke( $affectedIniFiles = ($this->removeIniEntry)($piePackage, $composerRequest->targetPlatform); if (count($affectedIniFiles) === 1) { - $output->writeln(sprintf('INI file "%s" was updated to remove the extension.', reset($affectedIniFiles))); + $output->writeln( + sprintf('INI file "%s" was updated to remove the extension.', reset($affectedIniFiles)), + OutputInterface::VERBOSITY_VERBOSE, + ); } elseif (count($affectedIniFiles) === 0) { - $output->writeln('No INI files were updated to remove the extension.'); + $output->writeln( + 'No INI files were updated to remove the extension.', + OutputInterface::VERBOSITY_VERBOSE, + ); } else { - $output->writeln('The following INI files were updated to remove the extnesion:'); + $output->writeln( + 'The following INI files were updated to remove the extnesion:', + OutputInterface::VERBOSITY_VERBOSE, + ); array_walk($affectedIniFiles, static fn (string $ini) => $output->writeln(' - ' . $ini)); } - ($this->uninstall)($piePackage); + $output->writeln(sprintf('👋 Removed extension: %s', ($this->uninstall)($piePackage)->filePath)); } } diff --git a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php index df8579f1..279a4d65 100644 --- a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php +++ b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php @@ -28,7 +28,12 @@ class RemoveIniEntryWithFileGetContents implements RemoveIniEntry /** @return list Returns a list of INI files that were updated to remove the extension */ public function __invoke(Package $package, TargetPlatform $targetPlatform): array { - $allIniFiles = [$targetPlatform->phpBinaryPath->loadedIniConfigurationFile()]; + $allIniFiles = []; + + $mainIni = $targetPlatform->phpBinaryPath->loadedIniConfigurationFile(); + if ($mainIni !== null) { + $allIniFiles[] = $mainIni; + } $additionalIniDirectory = $targetPlatform->phpBinaryPath->additionalIniDirectory(); if ($additionalIniDirectory !== null) { diff --git a/src/Installing/Uninstall.php b/src/Installing/Uninstall.php index 16ffac48..06584b09 100644 --- a/src/Installing/Uninstall.php +++ b/src/Installing/Uninstall.php @@ -4,10 +4,11 @@ namespace Php\Pie\Installing; +use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface Uninstall { - public function __invoke(Package $package): void; + public function __invoke(Package $package): BinaryFile; } diff --git a/src/Installing/UninstallUsingUnlink.php b/src/Installing/UninstallUsingUnlink.php index 2811d505..76d77a5e 100644 --- a/src/Installing/UninstallUsingUnlink.php +++ b/src/Installing/UninstallUsingUnlink.php @@ -14,7 +14,7 @@ /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ class UninstallUsingUnlink implements Uninstall { - public function __invoke(Package $package): void + public function __invoke(Package $package): BinaryFile { $pieMetadata = PieInstalledJsonMetadataKeys::pieMetadataFromComposerPackage($package->composerPackage()); @@ -40,6 +40,8 @@ public function __invoke(Package $package): void $expectedBinaryFile->verify(); unlink($expectedBinaryFile->filePath); + // @todo verify the unlink worked etc, maybe permissions failed + return $expectedBinaryFile; } } diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 31b2f9a5..8f111b0a 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -114,11 +114,32 @@ public function theExtensionShouldHaveBeenBuiltWithOptions(): void } #[When('I run a command to install an extension')] + #[Given('an extension was previously installed')] public function iRunACommandToInstallAnExtension(): void { $this->runPieCommand(['install', 'asgrim/example-pie-extension']); } + #[When('I run a command to uninstall an extension')] + public function iRunACommandToUninstallAnExtension(): void + { + $this->runPieCommand(['uninstall', 'asgrim/example-pie-extension']); + } + + #[Then('the extension should not be installed anymore')] + public function theExtensionShouldNotBeInstalled(): void + { + $this->assertCommandSuccessful(); + + Assert::regex($this->output, '#👋 Removed extension: [-_a-zA-Z0-9/]+/example_pie_extension.so#'); + + $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("example_pie_extension")?"yes":"no";'])) + ->mustRun() + ->getOutput(); + + Assert::same('no', $isExtEnabled); + } + #[Then('the extension should have been installed')] public function theExtensionShouldHaveBeenInstalled(): void { diff --git a/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php b/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php index f658f2f6..3aae85b7 100644 --- a/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php +++ b/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php @@ -4,15 +4,123 @@ namespace Php\PieUnitTest\Installing\Ini; +use Composer\Package\CompletePackageInterface; +use Composer\Util\Filesystem; +use Php\Pie\DependencyResolver\Package; +use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; use Php\Pie\Installing\Ini\RemoveIniEntryWithFileGetContents; +use Php\Pie\Platform\Architecture; +use Php\Pie\Platform\OperatingSystem; +use Php\Pie\Platform\OperatingSystemFamily; +use Php\Pie\Platform\TargetPhp\PhpBinaryPath; +use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Platform\ThreadSafetyMode; use PHPUnit\Framework\Attributes\CoversClass; +use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Webmozart\Assert\Assert; + +use function file_get_contents; +use function file_put_contents; +use function mkdir; +use function sys_get_temp_dir; +use function uniqid; + +use const DIRECTORY_SEPARATOR; #[CoversClass(RemoveIniEntryWithFileGetContents::class)] final class RemoveIniEntryWithFileGetContentsTest extends TestCase { - public function testRelevantIniFilesHaveExtensionRemoved(): void + private const INI_WITH_COMMENTED_EXTS = ";extension=foobar\n;zend_extension=foobar\n"; + private const INI_WITH_ACTIVE_EXTS = "extension=foobar\nzend_extension=foobar\n"; + + private string $iniFilePath; + + public function setUp(): void { - self::fail('to be implemented'); // @todo + parent::setUp(); + + $this->iniFilePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_remove_ini_test', true); + mkdir($this->iniFilePath); + Assert::positiveInteger(file_put_contents( + $this->iniFilePath . DIRECTORY_SEPARATOR . 'with_commented_exts.ini', + self::INI_WITH_COMMENTED_EXTS, + )); + Assert::positiveInteger(file_put_contents( + $this->iniFilePath . DIRECTORY_SEPARATOR . 'with_active_exts.ini', + self::INI_WITH_ACTIVE_EXTS, + )); + } + + public function tearDown(): void + { + parent::tearDown(); + + (new Filesystem())->remove($this->iniFilePath); + } + + /** + * @return array + * + * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 + */ + public function extensionTypeProvider(): array + { + return [ + 'phpModule' => [ExtensionType::PhpModule, "; extension=foobar ; removed by PIE\nzend_extension=foobar\n"], + 'zendExtension' => [ExtensionType::ZendExtension, "extension=foobar\n; zend_extension=foobar ; removed by PIE\n"], + ]; + } + + #[DataProvider('extensionTypeProvider')] + public function testRelevantIniFilesHaveExtensionRemoved(ExtensionType $extensionType, string $expectedActiveContent): void + { + $phpBinaryPath = $this->createMock(PhpBinaryPath::class); + $phpBinaryPath + ->method('loadedIniConfigurationFile') + ->willReturn(null); + $phpBinaryPath + ->method('additionalIniDirectory') + ->willReturn($this->iniFilePath); + + $package = new Package( + $this->createMock(CompletePackageInterface::class), + $extensionType, + ExtensionName::normaliseFromString('foobar'), + 'foobar/foobar', + '1.2.3', + null, + ); + + $targetPlatform = new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinaryPath, + Architecture::x86_64, + ThreadSafetyMode::ThreadSafe, + 1, + null, + ); + + $affectedFiles = (new RemoveIniEntryWithFileGetContents())( + $package, + $targetPlatform, + ); + + self::assertSame( + [$this->iniFilePath . DIRECTORY_SEPARATOR . 'with_active_exts.ini'], + $affectedFiles, + ); + + self::assertSame( + self::INI_WITH_COMMENTED_EXTS, + file_get_contents($this->iniFilePath . DIRECTORY_SEPARATOR . 'with_commented_exts.ini'), + ); + + self::assertSame( + $expectedActiveContent, + file_get_contents($this->iniFilePath . DIRECTORY_SEPARATOR . 'with_active_exts.ini'), + ); } } diff --git a/test/unit/Installing/UninstallUsingUnlinkTest.php b/test/unit/Installing/UninstallUsingUnlinkTest.php index e6a8e546..8b928d14 100644 --- a/test/unit/Installing/UninstallUsingUnlinkTest.php +++ b/test/unit/Installing/UninstallUsingUnlinkTest.php @@ -4,20 +4,73 @@ namespace Php\PieUnitTest\Installing; +use Composer\Package\CompletePackageInterface; +use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; +use Php\Pie\DependencyResolver\Package; +use Php\Pie\ExtensionName; +use Php\Pie\ExtensionType; +use Php\Pie\Installing\PackageMetadataMissing; use Php\Pie\Installing\UninstallUsingUnlink; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; +use function file_put_contents; +use function hash_file; +use function sys_get_temp_dir; +use function uniqid; + +use const DIRECTORY_SEPARATOR; + #[CoversClass(UninstallUsingUnlink::class)] final class UninstallUsingUnlinkTest extends TestCase { public function testMissingMetadataThrowsException(): void { - self::fail('to be implemented'); // @todo + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([]); + + $package = new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foobar/foobar', + '1.2.3', + null, + ); + + $this->expectException(PackageMetadataMissing::class); + $this->expectExceptionMessage('PIE metadata was missing for package foobar/foobar. Missing metadata keys: pie-installed-binary, pie-installed-binary-checksum'); + (new UninstallUsingUnlink())($package); } public function testBinaryFileIsRemoved(): void { - self::fail('to be implemented'); // @todo + $testFilename = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_uninstall_binary_test_', true); + file_put_contents($testFilename, 'test content'); + $testHash = hash_file('sha256', $testFilename); + + $composerPackage = $this->createMock(CompletePackageInterface::class); + $composerPackage + ->method('getExtra') + ->willReturn([ + PieInstalledJsonMetadataKeys::InstalledBinary->value => $testFilename, + PieInstalledJsonMetadataKeys::BinaryChecksum->value => $testHash, + ]); + + $package = new Package( + $composerPackage, + ExtensionType::PhpModule, + ExtensionName::normaliseFromString('foobar'), + 'foobar/foobar', + '1.2.3', + null, + ); + + $uninstalled = (new UninstallUsingUnlink())($package); + + self::assertSame($testFilename, $uninstalled->filePath); + self::assertFileDoesNotExist($testFilename); } } From 6cda93b2717fe39c1692f563549c5133d9512f73 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 25 Feb 2025 09:11:37 +0000 Subject: [PATCH 28/35] Disable uninstall command for Windows See https://github.com/php/pie/issues/190 for details. --- features/uninstall-extensions.feature | 4 +++- src/Command/UninstallCommand.php | 11 +++++++++++ src/Installing/FailedToRemoveExtension.php | 21 +++++++++++++++++++++ src/Installing/UninstallUsingUnlink.php | 5 +++-- test/behaviour/CliContext.php | 10 +++++++--- 5 files changed, 45 insertions(+), 6 deletions(-) create mode 100644 src/Installing/FailedToRemoveExtension.php diff --git a/features/uninstall-extensions.feature b/features/uninstall-extensions.feature index 67d8c2dc..154c10cf 100644 --- a/features/uninstall-extensions.feature +++ b/features/uninstall-extensions.feature @@ -1,6 +1,8 @@ Feature: Extensions can be uninstalled with PIE - Example: The latest version of an extension can be downloaded + # See https://github.com/php/pie/issues/190 for why this is non-Windows + @non-windows + Example: An extension can be uninstalled Given an extension was previously installed When I run a command to uninstall an extension Then the extension should not be installed anymore diff --git a/src/Command/UninstallCommand.php b/src/Command/UninstallCommand.php index cb0c1dcb..82d00f91 100644 --- a/src/Command/UninstallCommand.php +++ b/src/Command/UninstallCommand.php @@ -5,6 +5,7 @@ namespace Php\Pie\Command; use Composer\Composer; +use Composer\Util\Platform; use Php\Pie\ComposerIntegration\ComposerIntegrationHandler; use Php\Pie\ComposerIntegration\PieComposerFactory; use Php\Pie\ComposerIntegration\PieComposerRequest; @@ -52,6 +53,16 @@ public function configure(): void public function execute(InputInterface $input, OutputInterface $output): int { + if (Platform::isWindows()) { + /** + * @todo add support for uninstalling in Windows - see + * {@link https://github.com/php/pie/issues/190} for details + */ + $output->writeln('Uninstalling extensions on Windows is not currently supported.'); + + return 1; + } + $packageToRemove = (string) $input->getArgument(self::ARG_PACKAGE_NAME); Assert::stringNotEmpty($packageToRemove); $requestedPackageAndVersionToRemove = new RequestedPackageAndVersion($packageToRemove, null); diff --git a/src/Installing/FailedToRemoveExtension.php b/src/Installing/FailedToRemoveExtension.php new file mode 100644 index 00000000..8b5abaed --- /dev/null +++ b/src/Installing/FailedToRemoveExtension.php @@ -0,0 +1,21 @@ +filePath, + )); + } +} diff --git a/src/Installing/UninstallUsingUnlink.php b/src/Installing/UninstallUsingUnlink.php index 76d77a5e..0d9a9239 100644 --- a/src/Installing/UninstallUsingUnlink.php +++ b/src/Installing/UninstallUsingUnlink.php @@ -39,9 +39,10 @@ public function __invoke(Package $package): BinaryFile $expectedBinaryFile->verify(); - unlink($expectedBinaryFile->filePath); + if (! unlink($expectedBinaryFile->filePath)) { + throw FailedToRemoveExtension::withFilename($expectedBinaryFile); + } - // @todo verify the unlink worked etc, maybe permissions failed return $expectedBinaryFile; } } diff --git a/test/behaviour/CliContext.php b/test/behaviour/CliContext.php index 8f111b0a..78391cf3 100644 --- a/test/behaviour/CliContext.php +++ b/test/behaviour/CliContext.php @@ -131,13 +131,17 @@ public function theExtensionShouldNotBeInstalled(): void { $this->assertCommandSuccessful(); - Assert::regex($this->output, '#👋 Removed extension: [-_a-zA-Z0-9/]+/example_pie_extension.so#'); + if (Platform::isWindows()) { + Assert::regex($this->output, '#👋 Removed extension: [-\\\_:.a-zA-Z0-9]+\\\php_example_pie_extension.dll#'); + } else { + Assert::regex($this->output, '#👋 Removed extension: [-_a-zA-Z0-9/]+/example_pie_extension.so#'); + } $isExtEnabled = (new Process([self::PHP_BINARY, '-r', 'echo extension_loaded("example_pie_extension")?"yes":"no";'])) ->mustRun() ->getOutput(); - Assert::same('no', $isExtEnabled); + Assert::same($isExtEnabled, 'no'); } #[Then('the extension should have been installed')] @@ -159,7 +163,7 @@ public function theExtensionShouldHaveBeenInstalled(): void ->mustRun() ->getOutput(); - Assert::same('yes', $isExtEnabled); + Assert::same($isExtEnabled, 'yes'); } #[Given('I have an invalid extension installed')] From 64c05ce92c0c76e71d8093b1ee71910cfb416503 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Tue, 25 Feb 2025 10:45:17 +0000 Subject: [PATCH 29/35] Use sudo to remove the binary if no permissions given --- src/Command/UninstallCommand.php | 5 +++++ .../Ini/RemoveIniEntryWithFileGetContents.php | 6 ++++-- src/Installing/UninstallUsingUnlink.php | 13 ++++++++++++- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/src/Command/UninstallCommand.php b/src/Command/UninstallCommand.php index 82d00f91..03f66226 100644 --- a/src/Command/UninstallCommand.php +++ b/src/Command/UninstallCommand.php @@ -13,6 +13,7 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; use Php\Pie\Platform\InstalledPiePackages; +use Php\Pie\Platform\TargetPlatform; use Psr\Container\ContainerInterface; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; @@ -63,6 +64,10 @@ public function execute(InputInterface $input, OutputInterface $output): int return 1; } + if (! TargetPlatform::isRunningAsRoot()) { + $output->writeln('This command may need elevated privileges, and may prompt you for your password.'); + } + $packageToRemove = (string) $input->getArgument(self::ARG_PACKAGE_NAME); Assert::stringNotEmpty($packageToRemove); $requestedPackageAndVersionToRemove = new RequestedPackageAndVersion($packageToRemove, null); diff --git a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php index 279a4d65..2ec292a8 100644 --- a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php +++ b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php @@ -83,8 +83,10 @@ static function (string $iniFile) use (&$updatedIniFiles, $regex): void { return; } - // @todo verify it was written; permissions may have failed etc - file_put_contents($iniFile, $replacedContent); + if (! file_put_contents($iniFile, $replacedContent)) { + return; + } + $updatedIniFiles[] = $iniFile; }, ); diff --git a/src/Installing/UninstallUsingUnlink.php b/src/Installing/UninstallUsingUnlink.php index 0d9a9239..87d98e2a 100644 --- a/src/Installing/UninstallUsingUnlink.php +++ b/src/Installing/UninstallUsingUnlink.php @@ -8,6 +8,7 @@ use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; use Php\Pie\DependencyResolver\Package; +use Php\Pie\Util\Process; use function array_key_exists; use function unlink; @@ -39,7 +40,17 @@ public function __invoke(Package $package): BinaryFile $expectedBinaryFile->verify(); - if (! unlink($expectedBinaryFile->filePath)) { + // If the target directory isn't writable, or a .so file already exists and isn't writable, try to use sudo + if (file_exists($expectedBinaryFile->filePath) && ! is_writable($expectedBinaryFile->filePath)) { + Process::run(['sudo', 'rm', $expectedBinaryFile->filePath]); + + // Removal worked, bail out + if (! file_exists($expectedBinaryFile->filePath)) { + return $expectedBinaryFile; + } + } + + if (file_exists($expectedBinaryFile->filePath) && ! unlink($expectedBinaryFile->filePath)) { throw FailedToRemoveExtension::withFilename($expectedBinaryFile); } From e835b5ef46fd3fe7d263b67c848b7b86ff9ec612 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 26 Feb 2025 07:35:55 +0000 Subject: [PATCH 30/35] Move BinaryFile into new File namespace --- src/Building/Build.php | 2 +- src/Building/UnixBuild.php | 2 +- src/Building/WindowsBuild.php | 2 +- src/Command/ShowCommand.php | 4 ++-- src/ComposerIntegration/InstalledJsonMetadata.php | 2 +- src/{ => File}/BinaryFile.php | 4 ++-- src/{Installing => File}/BinaryFileFailedVerification.php | 3 +-- src/Installing/FailedToRemoveExtension.php | 2 +- src/Installing/Ini/DockerPhpExtEnable.php | 2 +- src/Installing/Ini/OndrejPhpenmod.php | 2 +- src/Installing/Ini/PickBestSetupIniApproach.php | 2 +- src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php | 2 +- src/Installing/Ini/SetupIniApproach.php | 2 +- src/Installing/Ini/StandardAdditionalPhpIniDirectory.php | 2 +- src/Installing/Ini/StandardSinglePhpIni.php | 2 +- src/Installing/Install.php | 2 +- src/Installing/SetupIniFile.php | 2 +- src/Installing/Uninstall.php | 2 +- src/Installing/UninstallUsingUnlink.php | 6 ++++-- src/Installing/UnixInstall.php | 2 +- src/Installing/WindowsInstall.php | 2 +- .../ComposerIntegration/InstallAndBuildProcessTest.php | 2 +- .../ComposerIntegration/InstalledJsonMetadataTest.php | 2 +- test/unit/{ => File}/BinaryFileTest.php | 8 ++++---- test/unit/Installing/Ini/DockerPhpExtEnableTest.php | 2 +- test/unit/Installing/Ini/OndrejPhpenmodTest.php | 2 +- test/unit/Installing/Ini/PickBestSetupIniApproachTest.php | 2 +- .../Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php | 2 +- .../Ini/StandardAdditionalPhpIniDirectoryTest.php | 2 +- test/unit/Installing/Ini/StandardSinglePhpIniTest.php | 2 +- 30 files changed, 38 insertions(+), 37 deletions(-) rename src/{ => File}/BinaryFile.php (95%) rename src/{Installing => File}/BinaryFileFailedVerification.php (93%) rename test/unit/{ => File}/BinaryFileTest.php (90%) diff --git a/src/Building/Build.php b/src/Building/Build.php index 884b170b..4ec00668 100644 --- a/src/Building/Build.php +++ b/src/Building/Build.php @@ -4,8 +4,8 @@ namespace Php\Pie\Building; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Building/UnixBuild.php b/src/Building/UnixBuild.php index 3130407d..ec91385f 100644 --- a/src/Building/UnixBuild.php +++ b/src/Building/UnixBuild.php @@ -4,8 +4,8 @@ namespace Php\Pie\Building; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; diff --git a/src/Building/WindowsBuild.php b/src/Building/WindowsBuild.php index 991ac77d..66cbff49 100644 --- a/src/Building/WindowsBuild.php +++ b/src/Building/WindowsBuild.php @@ -4,8 +4,8 @@ namespace Php\Pie\Building; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\WindowsExtensionAssetName; diff --git a/src/Command/ShowCommand.php b/src/Command/ShowCommand.php index 01e8c7f6..6bf3a9a7 100644 --- a/src/Command/ShowCommand.php +++ b/src/Command/ShowCommand.php @@ -4,11 +4,11 @@ namespace Php\Pie\Command; -use Php\Pie\BinaryFile; use Php\Pie\ComposerIntegration\PieComposerFactory; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; -use Php\Pie\Installing\BinaryFileFailedVerification; +use Php\Pie\File\BinaryFile; +use Php\Pie\File\BinaryFileFailedVerification; use Php\Pie\Platform\InstalledPiePackages; use Php\Pie\Platform\OperatingSystem; use Psr\Container\ContainerInterface; diff --git a/src/ComposerIntegration/InstalledJsonMetadata.php b/src/ComposerIntegration/InstalledJsonMetadata.php index 61bc857f..e91aaaaa 100644 --- a/src/ComposerIntegration/InstalledJsonMetadata.php +++ b/src/ComposerIntegration/InstalledJsonMetadata.php @@ -7,8 +7,8 @@ use Composer\Package\CompletePackage; use Composer\Package\CompletePackageInterface; use Composer\PartialComposer; -use Php\Pie\BinaryFile; use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys as MetadataKey; +use Php\Pie\File\BinaryFile; use Webmozart\Assert\Assert; use function array_merge; diff --git a/src/BinaryFile.php b/src/File/BinaryFile.php similarity index 95% rename from src/BinaryFile.php rename to src/File/BinaryFile.php index 50cac2ed..db3ae942 100644 --- a/src/BinaryFile.php +++ b/src/File/BinaryFile.php @@ -2,9 +2,9 @@ declare(strict_types=1); -namespace Php\Pie; +namespace Php\Pie\File; -use Php\Pie\Installing\BinaryFileFailedVerification; +use Php\Pie\Util; use function file_exists; use function hash_file; diff --git a/src/Installing/BinaryFileFailedVerification.php b/src/File/BinaryFileFailedVerification.php similarity index 93% rename from src/Installing/BinaryFileFailedVerification.php rename to src/File/BinaryFileFailedVerification.php index dbd8f228..7dd141bb 100644 --- a/src/Installing/BinaryFileFailedVerification.php +++ b/src/File/BinaryFileFailedVerification.php @@ -2,9 +2,8 @@ declare(strict_types=1); -namespace Php\Pie\Installing; +namespace Php\Pie\File; -use Php\Pie\BinaryFile; use RuntimeException; use function sprintf; diff --git a/src/Installing/FailedToRemoveExtension.php b/src/Installing/FailedToRemoveExtension.php index 8b5abaed..f0f4afe2 100644 --- a/src/Installing/FailedToRemoveExtension.php +++ b/src/Installing/FailedToRemoveExtension.php @@ -4,7 +4,7 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; +use Php\Pie\File\BinaryFile; use RuntimeException; use function sprintf; diff --git a/src/Installing/Ini/DockerPhpExtEnable.php b/src/Installing/Ini/DockerPhpExtEnable.php index 195b80b1..588a5e02 100644 --- a/src/Installing/Ini/DockerPhpExtEnable.php +++ b/src/Installing/Ini/DockerPhpExtEnable.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\Exception\ExtensionIsNotLoaded; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; diff --git a/src/Installing/Ini/OndrejPhpenmod.php b/src/Installing/Ini/OndrejPhpenmod.php index 7c52d7de..92bb5ebc 100644 --- a/src/Installing/Ini/OndrejPhpenmod.php +++ b/src/Installing/Ini/OndrejPhpenmod.php @@ -5,8 +5,8 @@ namespace Php\Pie\Installing\Ini; use Composer\Util\Platform; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Ini/PickBestSetupIniApproach.php b/src/Installing/Ini/PickBestSetupIniApproach.php index 83c719d3..a122491e 100644 --- a/src/Installing/Ini/PickBestSetupIniApproach.php +++ b/src/Installing/Ini/PickBestSetupIniApproach.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use ReflectionClass; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php index 9ae8c55f..23c8b0ce 100644 --- a/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php +++ b/src/Installing/Ini/PreCheckExtensionAlreadyLoaded.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPhp\Exception\ExtensionIsNotLoaded; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Ini/SetupIniApproach.php b/src/Installing/Ini/SetupIniApproach.php index 37870e90..0e003e0d 100644 --- a/src/Installing/Ini/SetupIniApproach.php +++ b/src/Installing/Ini/SetupIniApproach.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php b/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php index af94dab8..3202450e 100644 --- a/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php +++ b/src/Installing/Ini/StandardAdditionalPhpIniDirectory.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Ini/StandardSinglePhpIni.php b/src/Installing/Ini/StandardSinglePhpIni.php index 0eb046e8..decaf6da 100644 --- a/src/Installing/Ini/StandardSinglePhpIni.php +++ b/src/Installing/Ini/StandardSinglePhpIni.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing\Ini; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Install.php b/src/Installing/Install.php index 17982b41..0e1b3fee 100644 --- a/src/Installing/Install.php +++ b/src/Installing/Install.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/SetupIniFile.php b/src/Installing/SetupIniFile.php index 35b11f5c..e169246c 100644 --- a/src/Installing/SetupIniFile.php +++ b/src/Installing/SetupIniFile.php @@ -4,9 +4,9 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\SetupIniApproach; use Php\Pie\Platform\TargetPlatform; use Symfony\Component\Console\Output\OutputInterface; diff --git a/src/Installing/Uninstall.php b/src/Installing/Uninstall.php index 06584b09..1c72af83 100644 --- a/src/Installing/Uninstall.php +++ b/src/Installing/Uninstall.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; +use Php\Pie\File\BinaryFile; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface Uninstall diff --git a/src/Installing/UninstallUsingUnlink.php b/src/Installing/UninstallUsingUnlink.php index 87d98e2a..4e3699ad 100644 --- a/src/Installing/UninstallUsingUnlink.php +++ b/src/Installing/UninstallUsingUnlink.php @@ -4,12 +4,14 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\ComposerIntegration\PieInstalledJsonMetadataKeys; use Php\Pie\DependencyResolver\Package; - +use Php\Pie\File\BinaryFile; use Php\Pie\Util\Process; + use function array_key_exists; +use function file_exists; +use function is_writable; use function unlink; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ diff --git a/src/Installing/UnixInstall.php b/src/Installing/UnixInstall.php index efe9beae..fa179ffb 100644 --- a/src/Installing/UnixInstall.php +++ b/src/Installing/UnixInstall.php @@ -4,8 +4,8 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Util\Process; use RuntimeException; diff --git a/src/Installing/WindowsInstall.php b/src/Installing/WindowsInstall.php index 29d4ef2b..23c8f2a9 100644 --- a/src/Installing/WindowsInstall.php +++ b/src/Installing/WindowsInstall.php @@ -4,9 +4,9 @@ namespace Php\Pie\Installing; -use Php\Pie\BinaryFile; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\TargetPlatform; use Php\Pie\Platform\WindowsExtensionAssetName; use RecursiveDirectoryIterator; diff --git a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php index a0137f5b..5d3f5a94 100644 --- a/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php +++ b/test/unit/ComposerIntegration/InstallAndBuildProcessTest.php @@ -6,13 +6,13 @@ use Composer\Package\CompletePackage; use Composer\PartialComposer; -use Php\Pie\BinaryFile; use Php\Pie\Building\Build; 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; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Install; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; diff --git a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php b/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php index 48790e74..a4df418d 100644 --- a/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php +++ b/test/unit/ComposerIntegration/InstalledJsonMetadataTest.php @@ -8,11 +8,11 @@ use Composer\Package\CompletePackage; use Composer\Repository\InstalledArrayRepository; use Composer\Repository\RepositoryManager; -use Php\Pie\BinaryFile; use Php\Pie\ComposerIntegration\InstalledJsonMetadata; use Php\Pie\ComposerIntegration\PieComposerRequest; use Php\Pie\ComposerIntegration\PieOperation; use Php\Pie\DependencyResolver\RequestedPackageAndVersion; +use Php\Pie\File\BinaryFile; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; use Php\Pie\Platform\OperatingSystemFamily; diff --git a/test/unit/BinaryFileTest.php b/test/unit/File/BinaryFileTest.php similarity index 90% rename from test/unit/BinaryFileTest.php rename to test/unit/File/BinaryFileTest.php index 27400b21..0b7fc210 100644 --- a/test/unit/BinaryFileTest.php +++ b/test/unit/File/BinaryFileTest.php @@ -2,10 +2,10 @@ declare(strict_types=1); -namespace Php\PieUnitTest; +namespace Php\PieUnitTest\File; -use Php\Pie\BinaryFile; -use Php\Pie\Installing\BinaryFileFailedVerification; +use Php\Pie\File\BinaryFile; +use Php\Pie\File\BinaryFileFailedVerification; use Php\Pie\Util\FileNotFound; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -13,7 +13,7 @@ #[CoversClass(BinaryFile::class)] final class BinaryFileTest extends TestCase { - private const TEST_FILE = __DIR__ . '/../assets/test-zip.zip'; + private const TEST_FILE = __DIR__ . '/../../assets/test-zip.zip'; private const TEST_FILE_HASH = '64e40b4a66831437a3cc6b899ea71a36765ccb435f8831ab20d49f8ce3f806fa'; public function testVerifySucceedsWithGoodHash(): void diff --git a/test/unit/Installing/Ini/DockerPhpExtEnableTest.php b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php index ab407afc..e118096d 100644 --- a/test/unit/Installing/Ini/DockerPhpExtEnableTest.php +++ b/test/unit/Installing/Ini/DockerPhpExtEnableTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\DockerPhpExtEnable; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; diff --git a/test/unit/Installing/Ini/OndrejPhpenmodTest.php b/test/unit/Installing/Ini/OndrejPhpenmodTest.php index 527fe426..efa76863 100644 --- a/test/unit/Installing/Ini/OndrejPhpenmodTest.php +++ b/test/unit/Installing/Ini/OndrejPhpenmodTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\CheckAndAddExtensionToIniIfNeeded; use Php\Pie\Installing\Ini\OndrejPhpenmod; use Php\Pie\Platform\Architecture; diff --git a/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php index 1c35c02d..0786ee7f 100644 --- a/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php +++ b/test/unit/Installing/Ini/PickBestSetupIniApproachTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackage; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\PickBestSetupIniApproach; use Php\Pie\Installing\Ini\SetupIniApproach; use Php\Pie\Platform\Architecture; diff --git a/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php index 6435e6bc..69b71db3 100644 --- a/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php +++ b/test/unit/Installing/Ini/PreCheckExtensionAlreadyLoadedTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\PreCheckExtensionAlreadyLoaded; use Php\Pie\Platform\Architecture; use Php\Pie\Platform\OperatingSystem; diff --git a/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php index 45a073d6..8380d211 100644 --- a/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php +++ b/test/unit/Installing/Ini/StandardAdditionalPhpIniDirectoryTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\CheckAndAddExtensionToIniIfNeeded; use Php\Pie\Installing\Ini\StandardAdditionalPhpIniDirectory; use Php\Pie\Platform\Architecture; diff --git a/test/unit/Installing/Ini/StandardSinglePhpIniTest.php b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php index 21f47714..3d35fd0e 100644 --- a/test/unit/Installing/Ini/StandardSinglePhpIniTest.php +++ b/test/unit/Installing/Ini/StandardSinglePhpIniTest.php @@ -5,11 +5,11 @@ namespace Php\PieUnitTest\Installing\Ini; use Composer\Package\CompletePackageInterface; -use Php\Pie\BinaryFile; use Php\Pie\DependencyResolver\Package; use Php\Pie\Downloading\DownloadedPackage; use Php\Pie\ExtensionName; use Php\Pie\ExtensionType; +use Php\Pie\File\BinaryFile; use Php\Pie\Installing\Ini\CheckAndAddExtensionToIniIfNeeded; use Php\Pie\Installing\Ini\StandardSinglePhpIni; use Php\Pie\Platform\Architecture; From 80f835f4be71933e491f53a86087c7ecfc719ec2 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 26 Feb 2025 10:34:15 +0000 Subject: [PATCH 31/35] Util to capture and return errors running a function --- src/Util/CaptureErrors.php | 44 ++++++++++++++++++++++++++++ test/unit/Util/CaptureErrorsTest.php | 38 ++++++++++++++++++++++++ 2 files changed, 82 insertions(+) create mode 100644 src/Util/CaptureErrors.php create mode 100644 test/unit/Util/CaptureErrorsTest.php diff --git a/src/Util/CaptureErrors.php b/src/Util/CaptureErrors.php new file mode 100644 index 00000000..4d706a48 --- /dev/null +++ b/src/Util/CaptureErrors.php @@ -0,0 +1,44 @@ + + */ +final class CaptureErrors +{ + /** + * @param callable():T $code + * @param CapturedErrorList $captured + * + * @return T + * + * @template T + */ + public static function for(callable $code, array &$captured): mixed + { + set_error_handler(static function (int $level, string $message, string $filename, int $line) use (&$captured): bool { + $captured[] = [ + 'level' => $level, + 'message' => $message, + 'filename' => $filename, + 'line' => $line, + ]; + + return true; + }); + + $returnValue = $code(); + + restore_error_handler(); + + return $returnValue; + } +} diff --git a/test/unit/Util/CaptureErrorsTest.php b/test/unit/Util/CaptureErrorsTest.php new file mode 100644 index 00000000..a1cddea9 --- /dev/null +++ b/test/unit/Util/CaptureErrorsTest.php @@ -0,0 +1,38 @@ + Date: Wed, 26 Feb 2025 10:43:09 +0000 Subject: [PATCH 32/35] Added Sudo locator --- src/File/Sudo.php | 48 +++++++++++++++++++++++++++++++ src/File/SudoNotFoundOnSystem.php | 15 ++++++++++ 2 files changed, 63 insertions(+) create mode 100644 src/File/Sudo.php create mode 100644 src/File/SudoNotFoundOnSystem.php diff --git a/src/File/Sudo.php b/src/File/Sudo.php new file mode 100644 index 00000000..b3041b40 --- /dev/null +++ b/src/File/Sudo.php @@ -0,0 +1,48 @@ +find('sudo'); + + if ($sudo === null || $sudo === '') { + throw SudoNotFoundOnSystem::new(); + } + + self::$memoizedSudo = $sudo; + } + + return self::$memoizedSudo; + } + + public static function exists(): bool + { + try { + self::find(); + + return true; + } catch (Throwable) { + return false; + } + } +} diff --git a/src/File/SudoNotFoundOnSystem.php b/src/File/SudoNotFoundOnSystem.php new file mode 100644 index 00000000..e3818aa3 --- /dev/null +++ b/src/File/SudoNotFoundOnSystem.php @@ -0,0 +1,15 @@ + Date: Wed, 26 Feb 2025 10:45:49 +0000 Subject: [PATCH 33/35] Added file util to write to a file with sudo --- src/File/FailedToWriteFile.php | 26 +++++++++++++ src/File/SudoFilePut.php | 60 ++++++++++++++++++++++++++++++ test/unit/File/SudoFilePutTest.php | 33 ++++++++++++++++ 3 files changed, 119 insertions(+) create mode 100644 src/File/FailedToWriteFile.php create mode 100644 src/File/SudoFilePut.php create mode 100644 test/unit/File/SudoFilePutTest.php diff --git a/src/File/FailedToWriteFile.php b/src/File/FailedToWriteFile.php new file mode 100644 index 00000000..b856ea48 --- /dev/null +++ b/src/File/FailedToWriteFile.php @@ -0,0 +1,26 @@ + file_put_contents($filename, $content), + $capturedErrors, + ); + + if ($writeSuccessful === false) { + throw FailedToWriteFile::fromFilePutContentErrors($filename, $capturedErrors); + } + + if (! $didChangePermissions || ! Sudo::exists()) { + return; + } + + Process::run([Sudo::find(), 'chmod', $previousPermissions, $filename]); + } + + private static function attemptToMakeFileEditable(string $filename): bool + { + if (! Sudo::exists()) { + return false; + } + + if (! is_writable($filename)) { + try { + Process::run([Sudo::find(), 'chmod', '0777', $filename]); + + return true; + } catch (ProcessFailedException) { + return false; + } + } + + return false; + } +} diff --git a/test/unit/File/SudoFilePutTest.php b/test/unit/File/SudoFilePutTest.php new file mode 100644 index 00000000..7d909dfb --- /dev/null +++ b/test/unit/File/SudoFilePutTest.php @@ -0,0 +1,33 @@ + Date: Wed, 26 Feb 2025 11:01:03 +0000 Subject: [PATCH 34/35] Use sudo to write INI file to remove the INI entry --- src/Command/UninstallCommand.php | 2 +- src/ComposerIntegration/UninstallProcess.php | 2 +- src/Installing/Ini/RemoveIniEntry.php | 3 ++- .../Ini/RemoveIniEntryWithFileGetContents.php | 20 ++++++++++++++----- .../RemoveIniEntryWithFileGetContentsTest.php | 4 +++- 5 files changed, 22 insertions(+), 9 deletions(-) diff --git a/src/Command/UninstallCommand.php b/src/Command/UninstallCommand.php index 03f66226..0e6997bf 100644 --- a/src/Command/UninstallCommand.php +++ b/src/Command/UninstallCommand.php @@ -59,7 +59,7 @@ public function execute(InputInterface $input, OutputInterface $output): int * @todo add support for uninstalling in Windows - see * {@link https://github.com/php/pie/issues/190} for details */ - $output->writeln('Uninstalling extensions on Windows is not currently supported.'); + $output->writeln('Uninstalling extensions on Windows is not currently supported.'); return 1; } diff --git a/src/ComposerIntegration/UninstallProcess.php b/src/ComposerIntegration/UninstallProcess.php index 781fd0b8..6f4d747a 100644 --- a/src/ComposerIntegration/UninstallProcess.php +++ b/src/ComposerIntegration/UninstallProcess.php @@ -33,7 +33,7 @@ public function __invoke( $piePackage = Package::fromComposerCompletePackage($composerPackage); - $affectedIniFiles = ($this->removeIniEntry)($piePackage, $composerRequest->targetPlatform); + $affectedIniFiles = ($this->removeIniEntry)($piePackage, $composerRequest->targetPlatform, $output); if (count($affectedIniFiles) === 1) { $output->writeln( diff --git a/src/Installing/Ini/RemoveIniEntry.php b/src/Installing/Ini/RemoveIniEntry.php index fb5c7fd5..347c9168 100644 --- a/src/Installing/Ini/RemoveIniEntry.php +++ b/src/Installing/Ini/RemoveIniEntry.php @@ -6,10 +6,11 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\Platform\TargetPlatform; +use Symfony\Component\Console\Output\OutputInterface; /** @internal This is not public API for PIE, so should not be depended upon unless you accept the risk of BC breaks */ interface RemoveIniEntry { /** @return list Returns a list of INI files that were updated to remove the extension */ - public function __invoke(Package $package, TargetPlatform $targetPlatform): array; + public function __invoke(Package $package, TargetPlatform $targetPlatform, OutputInterface $output): array; } diff --git a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php index 2ec292a8..6b4235c5 100644 --- a/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php +++ b/src/Installing/Ini/RemoveIniEntryWithFileGetContents.php @@ -6,7 +6,10 @@ use Php\Pie\DependencyResolver\Package; use Php\Pie\ExtensionType; +use Php\Pie\File\FailedToWriteFile; +use Php\Pie\File\SudoFilePut; use Php\Pie\Platform\TargetPlatform; +use Symfony\Component\Console\Output\OutputInterface; use function array_filter; use function array_map; @@ -14,7 +17,6 @@ use function array_walk; use function file_exists; use function file_get_contents; -use function file_put_contents; use function in_array; use function preg_replace; use function scandir; @@ -26,7 +28,7 @@ class RemoveIniEntryWithFileGetContents implements RemoveIniEntry { /** @return list Returns a list of INI files that were updated to remove the extension */ - public function __invoke(Package $package, TargetPlatform $targetPlatform): array + public function __invoke(Package $package, TargetPlatform $targetPlatform, OutputInterface $output): array { $allIniFiles = []; @@ -58,7 +60,7 @@ static function (string $path) use ($additionalIniDirectory): bool { } $regex = sprintf( - '/^(%s\w*=\w*%s)$/m', + '/^(%s\w*=\w*%s)/m', $package->extensionType() === ExtensionType::PhpModule ? 'extension' : 'zend_extension', $package->extensionName()->name(), ); @@ -66,7 +68,7 @@ static function (string $path) use ($additionalIniDirectory): bool { $updatedIniFiles = []; array_walk( $allIniFiles, - static function (string $iniFile) use (&$updatedIniFiles, $regex): void { + static function (string $iniFile) use (&$updatedIniFiles, $regex, $package, $output): void { $currentContent = file_get_contents($iniFile); if ($currentContent === false || $currentContent === '') { @@ -83,7 +85,15 @@ static function (string $iniFile) use (&$updatedIniFiles, $regex): void { return; } - if (! file_put_contents($iniFile, $replacedContent)) { + try { + SudoFilePut::contents($iniFile, $replacedContent); + } catch (FailedToWriteFile) { + $output->writeln(sprintf( + 'Failed to remove extension "%s" from INI file "%s"', + $package->extensionName()->name(), + $iniFile, + )); + return; } diff --git a/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php b/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php index 3aae85b7..d08fb494 100644 --- a/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php +++ b/test/unit/Installing/Ini/RemoveIniEntryWithFileGetContentsTest.php @@ -19,6 +19,7 @@ use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\Attributes\DataProvider; use PHPUnit\Framework\TestCase; +use Symfony\Component\Console\Output\OutputInterface; use Webmozart\Assert\Assert; use function file_get_contents; @@ -65,7 +66,7 @@ public function tearDown(): void * * @psalm-suppress PossiblyUnusedMethod https://github.com/psalm/psalm-plugin-phpunit/issues/131 */ - public function extensionTypeProvider(): array + public static function extensionTypeProvider(): array { return [ 'phpModule' => [ExtensionType::PhpModule, "; extension=foobar ; removed by PIE\nzend_extension=foobar\n"], @@ -106,6 +107,7 @@ public function testRelevantIniFilesHaveExtensionRemoved(ExtensionType $extensio $affectedFiles = (new RemoveIniEntryWithFileGetContents())( $package, $targetPlatform, + $this->createMock(OutputInterface::class), ); self::assertSame( From 2a3e8a94a59b19662a027e71fa093744f48ad00b Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Wed, 26 Feb 2025 11:11:37 +0000 Subject: [PATCH 35/35] Only run SudoFilePutTest if allowed --- test/unit/File/SudoFilePutTest.php | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test/unit/File/SudoFilePutTest.php b/test/unit/File/SudoFilePutTest.php index 7d909dfb..64707640 100644 --- a/test/unit/File/SudoFilePutTest.php +++ b/test/unit/File/SudoFilePutTest.php @@ -4,6 +4,7 @@ namespace Php\PieUnitTest\File; +use Php\Pie\File\Sudo; use Php\Pie\File\SudoFilePut; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -21,6 +22,10 @@ final class SudoFilePutTest extends TestCase { public function testSudoFilePutContents(): void { + if (! Sudo::exists()) { + self::markTestSkipped('Cannot test sudo file_put_contents without sudo'); + } + $file = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie_test_sudo_file_put_contents_', true); touch($file); chmod($file, 0000);