From 58a60ff52e5ac0375ab3b3b9e5893da5365928e2 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 10 Sep 2026 12:13:14 +0100 Subject: [PATCH 1/5] 730: add phpConfigExtensionPath to get the extension path according to php-config (if present) --- src/Platform/TargetPhp/PhpBinaryPath.php | 12 ++++++++++++ .../unit/Platform/TargetPhp/PhpBinaryPathTest.php | 15 +++++++++++++++ 2 files changed, 27 insertions(+) diff --git a/src/Platform/TargetPhp/PhpBinaryPath.php b/src/Platform/TargetPhp/PhpBinaryPath.php index 7f001a4f..cd2f78d0 100644 --- a/src/Platform/TargetPhp/PhpBinaryPath.php +++ b/src/Platform/TargetPhp/PhpBinaryPath.php @@ -154,6 +154,18 @@ public function extensionPath(string|null $prefixInstallRoot = null): string throw ExtensionPathProblem::new($this, $extensionPath); } + /** @return non-empty-string|null */ + public function phpConfigExtensionPath(): string|null + { + if ($this->phpConfigPath === null) { + return null; + } + + $extensionDir = self::cleanWarningAndDeprecationsFromOutput(Process::run([$this->phpConfigPath, '--extension-dir'])); + + return $extensionDir !== '' ? $extensionDir : null; + } + public function assertExtensionIsLoadedInRuntime(ExtensionName $extension, IOInterface|null $io = null): void { if (! in_array(strtolower($extension->name()), array_map('strtolower', array_keys($this->extensions())))) { diff --git a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php index 95275522..096918b0 100644 --- a/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php +++ b/test/unit/Platform/TargetPhp/PhpBinaryPathTest.php @@ -194,6 +194,21 @@ public function testFromPhpConfigExecutable(string $phpConfigPath, string $expec ); self::assertSame($phpConfigPath, $phpBinary->phpConfigPath()); + + self::assertSame( + Process::run([$phpConfigPath, '--extension-dir']), + $phpBinary->phpConfigExtensionPath(), + ); + } + + public function testPhpConfigExtensionPathIsNullWhenPhpConfigIsNotPresent(): void + { + $phpExecutable = trim((string) (new PhpExecutableFinder())->find()); + assert($phpExecutable !== ''); + + $phpBinary = PhpBinaryPath::fromPhpBinaryPath($phpExecutable); + + self::assertNull($phpBinary->phpConfigExtensionPath()); } public function testExtensions(): void From 5a3247904df43d3552933ed9ed4733ed821acb37 Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 10 Sep 2026 13:57:53 +0100 Subject: [PATCH 2/5] 730: add assertExtensionPathIsConsistent helper --- src/Command/CommandHelper.php | 29 +++++++++ test/unit/Command/CommandHelperTest.php | 85 +++++++++++++++++++++++++ 2 files changed, 114 insertions(+) diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index b54ae8bb..f4d78510 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -32,6 +32,7 @@ use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; use Psr\Container\ContainerInterface; +use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; @@ -313,6 +314,34 @@ public static function determineForceInstallingPackageVersion(InputInterface $in return $input->hasOption(self::OPTION_FORCE) && $input->getOption(self::OPTION_FORCE); } + public static function assertExtensionPathIsConsistent(TargetPlatform $targetPlatform, InputInterface $input, IOInterface $io): void + { + $phpConfigExtensionPath = $targetPlatform->phpBinaryPath->phpConfigExtensionPath(); + $iniExtensionPath = $targetPlatform->phpBinaryPath->extensionPath(); + + if ($phpConfigExtensionPath === null || $phpConfigExtensionPath === $iniExtensionPath) { + return; + } + + $message = sprintf( + <<<'ERROR' + The php.ini `extension_dir` directive (%s) does not match `php-config --extension-dir` (%s). This means + that installs will likely fail (as the extension will be installed in one place, but PHP is looking in + another place). + + + ERROR, + $iniExtensionPath, + $phpConfigExtensionPath, + ); + + if (! self::determineForceInstallingPackageVersion($input)) { + throw new RuntimeException($message . 'Re-run with --force to attempt the install anyway.'); + } + + $io->writeError('Warning: ' . $message . 'Proceeding anyway because --force was used.'); + } + public static function noDev(InputInterface $input): bool { return $input->hasOption(self::OPTION_NO_DEV) && $input->getOption(self::OPTION_NO_DEV); diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index eeb3c985..c79695fa 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -24,11 +24,17 @@ use Php\Pie\DependencyResolver\ResolvedPackageRequest; use Php\Pie\DependencyResolver\UnableToResolveRequirement; use Php\Pie\Downloading\DownloadUrlMethod; +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\Attributes\RequiresOperatingSystemFamily; use PHPUnit\Framework\TestCase; +use RuntimeException; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\ArrayInput; use Symfony\Component\Console\Input\InputDefinition; @@ -376,6 +382,85 @@ public function testDetermineSuppressedDownloadUrlMethodsThrowsForInvalidValue() CommandHelper::determineSuppressedDownloadUrlMethods($input); } + private function targetPlatformWithExtensionPaths(string|null $phpConfigExtensionPath, string $iniExtensionPath): TargetPlatform + { + $phpBinary = $this->createMock(PhpBinaryPath::class); + $phpBinary->method('phpConfigExtensionPath')->willReturn($phpConfigExtensionPath); + $phpBinary->method('extensionPath')->willReturn($iniExtensionPath); + + return new TargetPlatform( + OperatingSystem::NonWindows, + OperatingSystemFamily::Linux, + $phpBinary, + Architecture::x86_64, + ThreadSafetyMode::NonThreadSafe, + 1, + null, + null, + ); + } + + public function testAssertExtensionPathDoesNothingWhenPhpConfigNotUsed(): void + { + $targetPlatform = $this->targetPlatformWithExtensionPaths(null, '/ini/extension/dir'); + $io = new BufferIO(); + + CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io); + + self::assertSame('', $io->getOutput()); + } + + public function testAssertExtensionPathDoesNothingWhenPathsMatch(): void + { + $targetPlatform = $this->targetPlatformWithExtensionPaths('/same/extension/dir', '/same/extension/dir'); + $io = new BufferIO(); + + CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io); + + self::assertSame('', $io->getOutput()); + } + + public function testAssertExtensionPathThrowsWhenPathsMatch(): void + { + $targetPlatform = $this->targetPlatformWithExtensionPaths('/php-config/extension/dir', '/ini/extension/dir'); + + $this->expectException(RuntimeException::class); + $this->expectExceptionMessage(<<<'EXCEPTION' + The php.ini `extension_dir` directive (/ini/extension/dir) does not match `php-config --extension-dir` (/php-config/extension/dir). This means + that installs will likely fail (as the extension will be installed in one place, but PHP is looking in + another place). + + Re-run with --force to attempt the install anyway. + EXCEPTION); + + CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), new BufferIO()); + } + + public function testAssertExtensionPathWarnsWhenPathsMatchButLukeUsesTheForce(): void + { + $targetPlatform = $this->targetPlatformWithExtensionPaths('/php-config/extension/dir', '/ini/extension/dir'); + + $command = new Command(); + CommandHelper::configureDownloadBuildInstallOptions($command); + $input = new ArrayInput(['--force' => true]); + CommandHelper::validateInput($input, $command); + + $io = new BufferIO(); + + CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $io); + + self::assertStringContainsString( + <<<'WARNING' + Warning: The php.ini `extension_dir` directive (/ini/extension/dir) does not match `php-config --extension-dir` (/php-config/extension/dir). This means + that installs will likely fail (as the extension will be installed in one place, but PHP is looking in + another place). + + Proceeding anyway because --force was used. + WARNING, + $io->getOutput(), + ); + } + public function testListRepositories(): void { $io = new BufferIO(); From 715cc6efe7bb7b4a1a051f341f8929958dd5b1ae Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 10 Sep 2026 13:58:34 +0100 Subject: [PATCH 3/5] 730: wire in new assertExtensionPathIsConsistent helper to commands --- src/Command/BuildCommand.php | 1 + src/Command/InstallCommand.php | 1 + src/Command/InstallExtensionsForProjectCommand.php | 1 + src/Command/UpgradeCommand.php | 1 + 4 files changed, 4 insertions(+) diff --git a/src/Command/BuildCommand.php b/src/Command/BuildCommand.php index 8fd323dc..8f0b967f 100644 --- a/src/Command/BuildCommand.php +++ b/src/Command/BuildCommand.php @@ -54,6 +54,7 @@ public function configure(): void public function execute(InputInterface $input, OutputInterface $output): int { $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); + CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $this->io); try { $requestedNamesAndVersions = CommandHelper::requestedNameAndVersionPairs($input); } catch (InvalidPackageName $invalidPackageName) { diff --git a/src/Command/InstallCommand.php b/src/Command/InstallCommand.php index c978c0fe..d75bb2dc 100644 --- a/src/Command/InstallCommand.php +++ b/src/Command/InstallCommand.php @@ -92,6 +92,7 @@ public function execute(InputInterface $input, OutputInterface $output): int } $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); + CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $this->io); try { $requestedNamesAndVersions = CommandHelper::requestedNameAndVersionPairs($input); } catch (InvalidPackageName $invalidPackageName) { diff --git a/src/Command/InstallExtensionsForProjectCommand.php b/src/Command/InstallExtensionsForProjectCommand.php index da46af88..6c2bf9ad 100644 --- a/src/Command/InstallExtensionsForProjectCommand.php +++ b/src/Command/InstallExtensionsForProjectCommand.php @@ -109,6 +109,7 @@ private function handlePhpProject(InputInterface $input, RootPackageInterface $r { $extensionToPackageSelections = CommandHelper::determineExtensionToPackageSelections($input); $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); + CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $this->io); $allowNonInteractive = $input->hasOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL) && $input->getOption(CommandHelper::OPTION_ALLOW_NON_INTERACTIVE_PROJECT_INSTALL); if ($allowNonInteractive) { diff --git a/src/Command/UpgradeCommand.php b/src/Command/UpgradeCommand.php index d644d4a1..f69cf87e 100644 --- a/src/Command/UpgradeCommand.php +++ b/src/Command/UpgradeCommand.php @@ -55,6 +55,7 @@ public function execute(InputInterface $input, OutputInterface $output): int } $targetPlatform = CommandHelper::determineTargetPlatformFromInputs($input, $this->io); + CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $this->io); $forceInstallPackageVersion = CommandHelper::determineForceInstallingPackageVersion($input); CommandHelper::applyNoCacheOptionIfSet($input, $this->io); From aa2cc1e60aa1b34f304a085add2dc5888acbbf5d Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 10 Sep 2026 15:27:02 +0100 Subject: [PATCH 4/5] 730: allow for symlinks in extension dir comparisons --- src/Command/CommandHelper.php | 3 +- src/Util/Realpath.php | 15 +++++++ test/unit/Command/CommandHelperTest.php | 51 ++++++++++++++++++---- test/unit/Util/RealpathTest.php | 58 +++++++++++++++++++++++++ 4 files changed, 118 insertions(+), 9 deletions(-) create mode 100644 src/Util/Realpath.php create mode 100644 test/unit/Util/RealpathTest.php diff --git a/src/Command/CommandHelper.php b/src/Command/CommandHelper.php index f4d78510..ecd6c39f 100644 --- a/src/Command/CommandHelper.php +++ b/src/Command/CommandHelper.php @@ -31,6 +31,7 @@ use Php\Pie\Platform\TargetPhp\PhpBinaryPath; use Php\Pie\Platform\TargetPhp\PhpizePath; use Php\Pie\Platform\TargetPlatform; +use Php\Pie\Util\Realpath; use Psr\Container\ContainerInterface; use RuntimeException; use Symfony\Component\Console\Command\Command; @@ -319,7 +320,7 @@ public static function assertExtensionPathIsConsistent(TargetPlatform $targetPla $phpConfigExtensionPath = $targetPlatform->phpBinaryPath->phpConfigExtensionPath(); $iniExtensionPath = $targetPlatform->phpBinaryPath->extensionPath(); - if ($phpConfigExtensionPath === null || $phpConfigExtensionPath === $iniExtensionPath) { + if ($phpConfigExtensionPath === null || Realpath::compare($phpConfigExtensionPath, $iniExtensionPath)) { return; } diff --git a/src/Util/Realpath.php b/src/Util/Realpath.php new file mode 100644 index 00000000..ec30a090 --- /dev/null +++ b/src/Util/Realpath.php @@ -0,0 +1,15 @@ +targetPlatformWithExtensionPaths(null, '/ini/extension/dir'); + $targetPlatform = $this->targetPlatformWithExtensionPaths(null, $this->realTempDir()); $io = new BufferIO(); CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io); @@ -412,7 +427,23 @@ public function testAssertExtensionPathDoesNothingWhenPhpConfigNotUsed(): void public function testAssertExtensionPathDoesNothingWhenPathsMatch(): void { - $targetPlatform = $this->targetPlatformWithExtensionPaths('/same/extension/dir', '/same/extension/dir'); + $dir = $this->realTempDir(); + $targetPlatform = $this->targetPlatformWithExtensionPaths($dir, $dir); + $io = new BufferIO(); + + CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io); + + self::assertSame('', $io->getOutput()); + } + + public function testAssertExtensionPathDoesNothingWhenPathsAreSymlinkedToTheSameRealPath(): void + { + $realDir = $this->realTempDir(); + + $symlinkPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie-test-extension-dir-symlink-', true); + symlink($realDir, $symlinkPath); + + $targetPlatform = $this->targetPlatformWithExtensionPaths($symlinkPath, $realDir); $io = new BufferIO(); CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io); @@ -422,11 +453,13 @@ public function testAssertExtensionPathDoesNothingWhenPathsMatch(): void public function testAssertExtensionPathThrowsWhenPathsMatch(): void { - $targetPlatform = $this->targetPlatformWithExtensionPaths('/php-config/extension/dir', '/ini/extension/dir'); + $phpConfigExtensionPath = $this->realTempDir(); + $iniExtensionPath = $this->realTempDir(); + $targetPlatform = $this->targetPlatformWithExtensionPaths($phpConfigExtensionPath, $iniExtensionPath); $this->expectException(RuntimeException::class); - $this->expectExceptionMessage(<<<'EXCEPTION' - The php.ini `extension_dir` directive (/ini/extension/dir) does not match `php-config --extension-dir` (/php-config/extension/dir). This means + $this->expectExceptionMessage(<<targetPlatformWithExtensionPaths('/php-config/extension/dir', '/ini/extension/dir'); + $phpConfigExtensionPath = $this->realTempDir(); + $iniExtensionPath = $this->realTempDir(); + $targetPlatform = $this->targetPlatformWithExtensionPaths($phpConfigExtensionPath, $iniExtensionPath); $command = new Command(); CommandHelper::configureDownloadBuildInstallOptions($command); @@ -450,8 +485,8 @@ public function testAssertExtensionPathWarnsWhenPathsMatchButLukeUsesTheForce(): CommandHelper::assertExtensionPathIsConsistent($targetPlatform, $input, $io); self::assertStringContainsString( - <<<'WARNING' - Warning: The php.ini `extension_dir` directive (/ini/extension/dir) does not match `php-config --extension-dir` (/php-config/extension/dir). This means + <<realTempDir(); + + self::assertTrue(Realpath::compare($dir, $dir)); + } + + public function testDifferentPathsAreNotEqual(): void + { + self::assertFalse(Realpath::compare($this->realTempDir(), $this->realTempDir())); + } + + public function testSymlinkedPathIsEqualToItsRealTarget(): void + { + $realDir = $this->realTempDir(); + + $symlinkPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie-test-realpath-symlink-', true); + symlink($realDir, $symlinkPath); + + self::assertTrue(Realpath::compare($symlinkPath, $realDir)); + } + + public function testTrailingDirectorySeparatorIsNormalised(): void + { + $dir = $this->realTempDir(); + + self::assertTrue(Realpath::compare($dir, $dir . DIRECTORY_SEPARATOR)); + } +} From b95f24d72aca485f05f22c405a497de2eaeb126d Mon Sep 17 00:00:00 2001 From: James Titcumb Date: Thu, 10 Sep 2026 15:58:06 +0100 Subject: [PATCH 5/5] 730: skip realpath symlink CI tests as eleveted privs needed --- test/unit/Command/CommandHelperTest.php | 4 ++++ test/unit/Util/RealpathTest.php | 5 +++++ 2 files changed, 9 insertions(+) diff --git a/test/unit/Command/CommandHelperTest.php b/test/unit/Command/CommandHelperTest.php index d4333423..f0a87101 100644 --- a/test/unit/Command/CommandHelperTest.php +++ b/test/unit/Command/CommandHelperTest.php @@ -438,6 +438,10 @@ public function testAssertExtensionPathDoesNothingWhenPathsMatch(): void public function testAssertExtensionPathDoesNothingWhenPathsAreSymlinkedToTheSameRealPath(): void { + if (Platform::isWindows()) { + self::markTestSkipped('Skipping for Windows as ineffective'); + } + $realDir = $this->realTempDir(); $symlinkPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie-test-extension-dir-symlink-', true); diff --git a/test/unit/Util/RealpathTest.php b/test/unit/Util/RealpathTest.php index ebb4650b..c459e5a2 100644 --- a/test/unit/Util/RealpathTest.php +++ b/test/unit/Util/RealpathTest.php @@ -4,6 +4,7 @@ namespace Php\PieUnitTest\Util; +use Composer\Util\Platform; use Php\Pie\Util\Realpath; use PHPUnit\Framework\Attributes\CoversClass; use PHPUnit\Framework\TestCase; @@ -41,6 +42,10 @@ public function testDifferentPathsAreNotEqual(): void public function testSymlinkedPathIsEqualToItsRealTarget(): void { + if (Platform::isWindows()) { + self::markTestSkipped('Skipping for Windows as ineffective'); + } + $realDir = $this->realTempDir(); $symlinkPath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie-test-realpath-symlink-', true);