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/CommandHelper.php b/src/Command/CommandHelper.php
index b54ae8bb..ecd6c39f 100644
--- a/src/Command/CommandHelper.php
+++ b/src/Command/CommandHelper.php
@@ -31,7 +31,9 @@
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;
use Symfony\Component\Console\Input\InputArgument;
use Symfony\Component\Console\Input\InputInterface;
@@ -313,6 +315,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 || Realpath::compare($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/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);
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/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 @@
+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,
+ );
+ }
+
+ /** @return non-empty-string */
+ private function realTempDir(): string
+ {
+ $dir = sys_get_temp_dir() . DIRECTORY_SEPARATOR . uniqid('pie-test-extension-dir-', true);
+ mkdir($dir, 0777, true);
+
+ return $dir;
+ }
+
+ public function testAssertExtensionPathDoesNothingWhenPhpConfigNotUsed(): void
+ {
+ $targetPlatform = $this->targetPlatformWithExtensionPaths(null, $this->realTempDir());
+ $io = new BufferIO();
+
+ CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io);
+
+ self::assertSame('', $io->getOutput());
+ }
+
+ public function testAssertExtensionPathDoesNothingWhenPathsMatch(): void
+ {
+ $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
+ {
+ 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);
+ symlink($realDir, $symlinkPath);
+
+ $targetPlatform = $this->targetPlatformWithExtensionPaths($symlinkPath, $realDir);
+ $io = new BufferIO();
+
+ CommandHelper::assertExtensionPathIsConsistent($targetPlatform, new ArrayInput([]), $io);
+
+ self::assertSame('', $io->getOutput());
+ }
+
+ public function testAssertExtensionPathThrowsWhenPathsMatch(): void
+ {
+ $phpConfigExtensionPath = $this->realTempDir();
+ $iniExtensionPath = $this->realTempDir();
+ $targetPlatform = $this->targetPlatformWithExtensionPaths($phpConfigExtensionPath, $iniExtensionPath);
+
+ $this->expectException(RuntimeException::class);
+ $this->expectExceptionMessage(<<realTempDir();
+ $iniExtensionPath = $this->realTempDir();
+ $targetPlatform = $this->targetPlatformWithExtensionPaths($phpConfigExtensionPath, $iniExtensionPath);
+
+ $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(
+ <<getOutput(),
+ );
+ }
+
public function testListRepositories(): void
{
$io = new BufferIO();
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
diff --git a/test/unit/Util/RealpathTest.php b/test/unit/Util/RealpathTest.php
new file mode 100644
index 00000000..c459e5a2
--- /dev/null
+++ b/test/unit/Util/RealpathTest.php
@@ -0,0 +1,63 @@
+realTempDir();
+
+ self::assertTrue(Realpath::compare($dir, $dir));
+ }
+
+ public function testDifferentPathsAreNotEqual(): void
+ {
+ self::assertFalse(Realpath::compare($this->realTempDir(), $this->realTempDir()));
+ }
+
+ 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);
+ symlink($realDir, $symlinkPath);
+
+ self::assertTrue(Realpath::compare($symlinkPath, $realDir));
+ }
+
+ public function testTrailingDirectorySeparatorIsNormalised(): void
+ {
+ $dir = $this->realTempDir();
+
+ self::assertTrue(Realpath::compare($dir, $dir . DIRECTORY_SEPARATOR));
+ }
+}