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)); + } +}