From f2db98b7da5f0d2a43380e1d5378ff7ba47c77ee Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Fri, 28 Aug 2026 18:36:57 +0800 Subject: [PATCH] fix: resolve php-config from selected PHP home --- phpunit/src/Platform/PlatformTest.php | 84 +++++++++++++++++++ src/Installer/LibPhpInstaller.php | 27 ++++-- src/Platform/UnixPlatform.php | 114 +++++++++++++++++++++++++- 3 files changed, 214 insertions(+), 11 deletions(-) diff --git a/phpunit/src/Platform/PlatformTest.php b/phpunit/src/Platform/PlatformTest.php index f499bb77..3106e85d 100644 --- a/phpunit/src/Platform/PlatformTest.php +++ b/phpunit/src/Platform/PlatformTest.php @@ -315,6 +315,90 @@ class PlatformTest extends TestCase $this->assertSame('', $platform->getCrtConfig()); } + public function testLinuxPhpHomeSelectsMatchingVersionedPhpConfig(): void + { + if (PHP_OS_FAMILY === 'Windows') { + $this->markTestSkipped('Unix php-config lookup test'); + } + + $root = sys_get_temp_dir() . '/typephp-php-home-' . bin2hex(random_bytes(6)); + $phpHome = $root . '/php'; + $rightInclude = $phpHome . '/include/right'; + $wrongInclude = $phpHome . '/include/wrong'; + mkdir($phpHome . '/bin', 0755, true); + mkdir($rightInclude, 0755, true); + mkdir($wrongInclude, 0755, true); + + $versionedConfig = $phpHome . '/bin/php-config' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; + $wrongMinor = PHP_MINOR_VERSION === 4 ? 5 : 4; + $this->writePhpConfig($versionedConfig, PHP_VERSION, $phpHome, $rightInclude); + $this->writePhpConfig( + $phpHome . '/bin/php-config', + PHP_MAJOR_VERSION . '.' . $wrongMinor . '.0', + $phpHome, + $wrongInclude, + ); + + $previousPhpHome = getenv('PHP_HOME'); + try { + putenv('PHP_HOME=' . $phpHome); + $platform = new Linux(); + + $this->assertSame($phpHome, $platform->getPhpDir()); + $this->assertSame([$rightInclude], $platform->buildPhpIncludePaths($phpHome)); + } finally { + $previousPhpHome === false + ? putenv('PHP_HOME') + : putenv('PHP_HOME=' . $previousPhpHome); + unlink($versionedConfig); + unlink($phpHome . '/bin/php-config'); + rmdir($rightInclude); + rmdir($wrongInclude); + rmdir($phpHome . '/include'); + rmdir($phpHome . '/bin'); + rmdir($phpHome); + rmdir($root); + } + } + + public function testLinuxPhpHomeDoesNotFallBackToPathPhpConfig(): void + { + if (PHP_OS_FAMILY === 'Windows') { + $this->markTestSkipped('Unix php-config lookup test'); + } + + $root = sys_get_temp_dir() . '/typephp-php-home-missing-' . bin2hex(random_bytes(6)); + $phpHome = $root . '/php'; + mkdir($phpHome . '/bin', 0755, true); + $previousPhpHome = getenv('PHP_HOME'); + + try { + putenv('PHP_HOME=' . $phpHome); + $this->expectException(\RuntimeException::class); + $this->expectExceptionMessage('PHP_HOME does not provide an executable bin/php-config'); + (new Linux())->buildPhpIncludePaths($phpHome); + } finally { + $previousPhpHome === false + ? putenv('PHP_HOME') + : putenv('PHP_HOME=' . $previousPhpHome); + rmdir($phpHome . '/bin'); + rmdir($phpHome); + rmdir($root); + } + } + + private function writePhpConfig(string $path, string $version, string $prefix, string $include): void + { + $script = sprintf( + "#!/bin/sh\ncase \"\$1\" in\n --version) printf '%%s\\n' %s ;;\n --prefix) printf '%%s\\n' %s ;;\n --includes) printf '%%s\\n' %s ;;\nesac\n", + escapeshellarg($version), + escapeshellarg($prefix), + escapeshellarg('-I' . $include), + ); + file_put_contents($path, $script); + chmod($path, 0755); + } + /** * 测试 macOS 平台基本功能 */ diff --git a/src/Installer/LibPhpInstaller.php b/src/Installer/LibPhpInstaller.php index cbe06e76..2f082bf4 100644 --- a/src/Installer/LibPhpInstaller.php +++ b/src/Installer/LibPhpInstaller.php @@ -176,13 +176,10 @@ final class LibPhpInstaller private function currentConfigureOptions(): string { - $phpConfig = $this->sourcePhpDir !== null && is_executable($this->sourcePhpDir . '/bin/php-config') - ? $this->sourcePhpDir . '/bin/php-config' - : trim((string) shell_exec('command -v php-config 2>/dev/null')); - if ($phpConfig !== '') { - return trim($this->capture([$phpConfig, '--configure-options'])); - } - + // 优先使用 PHP_BINARY -i 的 Configure Command:输出保留每个参数的 + // 引号,能正确处理 `CFLAGS=-g -O2` 这类含空格的值。而 + // `php-config --configure-options` 会丢失引号,导致含空格的值被 + // 错误拆分(例如 `-O2` 被当作独立参数传给 configure)。 $info = $this->capture([PHP_BINARY, '-n', '-i']); if (preg_match('/^Configure Command =>\s*(.+)$/mi', $info, $match)) { $words = PhpBuildConfiguration::parseShellWords(trim($match[1])); @@ -191,6 +188,22 @@ final class LibPhpInstaller } return implode(' ', array_map('escapeshellarg', $words)); } + + // 后备:php-config --configure-options。PPA 的多版本 PHP 共用 + // /usr 前缀,因此 PHP_HOME=/usr 时必须优先 php-config8.x。 + $versionedPhpConfig = $this->sourcePhpDir . '/bin/php-config' + . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; + if ($this->sourcePhpDir !== null && is_executable($versionedPhpConfig)) { + $phpConfig = $versionedPhpConfig; + } elseif ($this->sourcePhpDir !== null && is_executable($this->sourcePhpDir . '/bin/php-config')) { + $phpConfig = $this->sourcePhpDir . '/bin/php-config'; + } else { + $phpConfig = trim((string) shell_exec('command -v php-config 2>/dev/null')); + } + if ($phpConfig !== '') { + return trim($this->capture([$phpConfig, '--configure-options'])); + } + throw new \RuntimeException('Unable to determine the current PHP configure options from php-config or php -i'); } diff --git a/src/Platform/UnixPlatform.php b/src/Platform/UnixPlatform.php index e2f101f5..9d7ef36b 100644 --- a/src/Platform/UnixPlatform.php +++ b/src/Platform/UnixPlatform.php @@ -99,8 +99,23 @@ abstract class UnixPlatform extends PlatformBase public function getPhpDir(): string { $phpDir = getenv('PHP_HOME'); - if ($phpDir && is_dir($phpDir)) { - return rtrim($phpDir, '\/'); + if (is_string($phpDir) && $phpDir !== '') { + $phpDir = rtrim($phpDir, '\/'); + if (!is_dir($phpDir)) { + throw new \RuntimeException("PHP_HOME is not a directory: {$phpDir}"); + } + return $phpDir; + } + + // Ubuntu/PPA 多版本环境下 php8.4 与 php-config8.4 并存,而 + // php-config 可能被 update-alternatives 指向其它版本。优先依据 + // PHP_BINARY 的版本后缀定位版本化 php-config,避免 ABI 错配。 + $versionedConfig = $this->findVersionedPhpConfig(dirname(realpath(PHP_BINARY) ?: PHP_BINARY)); + if ($versionedConfig !== null) { + $prefix = $this->getPhpConfigValue($versionedConfig, '--prefix'); + if ($prefix !== null && is_dir($prefix)) { + return rtrim($prefix, '/'); + } } // Composer executes tpc.php with an already selected PHP binary. Use @@ -198,24 +213,115 @@ abstract class UnixPlatform extends PlatformBase */ protected function findPhpConfig(string $phpDir): ?string { + $candidates = []; + $phpDir = rtrim($phpDir, '/'); + + $phpHome = getenv('PHP_HOME'); + if (is_string($phpHome) && $phpHome !== '') { + $phpHome = rtrim($phpHome, '/'); + $expected = realpath($phpHome) ?: $phpHome; + $actual = realpath($phpDir) ?: $phpDir; + if ($actual === $expected) { + // PHP_HOME is authoritative. Ubuntu/PPA installs several PHP + // versions under /usr, so prefer php-config8.x over the + // unversioned php-config selected by update-alternatives. + $versioned = $this->findVersionedPhpConfig($phpDir . '/bin'); + if ($versioned !== null) { + $candidates[] = $versioned; + } + $candidate = $phpDir . '/bin/php-config'; + if (is_executable($candidate)) { + $candidates[] = $candidate; + } + + if ($candidates === []) { + throw new \RuntimeException( + "PHP_HOME does not provide an executable bin/php-config: {$phpDir}" + ); + } + + foreach (array_unique($candidates) as $config) { + if ($this->phpConfigMatchesCurrentPhp($config)) { + return $config; + } + } + $this->reportPhpConfigVersionMismatch($candidates[0]); + } + } + + // Prefer the config belonging to the requested installation. If its + // unversioned config belongs to another PHP, the version check below + // will continue with the config beside the running PHP binary. $candidate = $phpDir . '/bin/php-config'; if (is_executable($candidate)) { - return $candidate; + $candidates[] = $candidate; + } + + $versioned = $this->findVersionedPhpConfig(dirname(realpath(PHP_BINARY) ?: PHP_BINARY)); + if ($versioned !== null) { + $candidates[] = $versioned; } + // PATH is only a fallback, and its prefix must match the selected PHP. $whichResult = trim(shell_exec('which php-config 2>/dev/null')); if ($whichResult && is_executable($whichResult)) { $prefix = $this->getPhpConfigValue($whichResult, '--prefix'); $expected = realpath($phpDir) ?: rtrim($phpDir, '/'); $actual = $prefix === null ? null : (realpath($prefix) ?: rtrim($prefix, '/')); if ($actual === $expected) { - return $whichResult; + $candidates[] = $whichResult; } } + // 依次返回第一个与当前 PHP 主次版本匹配的候选 + foreach (array_unique($candidates) as $config) { + if ($this->phpConfigMatchesCurrentPhp($config)) { + return $config; + } + } + + // 存在候选但版本均不匹配时给出明确错误 + if ($candidates !== []) { + $this->reportPhpConfigVersionMismatch($candidates[0]); + } + return null; } + /** Find php-config8.x for the PHP version executing the compiler. */ + private function findVersionedPhpConfig(string $binDir): ?string + { + $candidate = rtrim($binDir, '/') . '/php-config' . PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; + return is_executable($candidate) ? $candidate : null; + } + + /** + * 校验 php-config 的主次版本号是否与当前运行的 PHP 一致。 + */ + private function phpConfigMatchesCurrentPhp(string $phpConfig): bool + { + $version = $this->getPhpConfigValue($phpConfig, '--version'); + if ($version === null) { + return false; + } + if (preg_match('/^(\d+\.\d+)\.\d+/', $version, $match)) { + return $match[1] === PHP_MAJOR_VERSION . '.' . PHP_MINOR_VERSION; + } + return false; + } + + private function reportPhpConfigVersionMismatch(string $phpConfig): void + { + $version = $this->getPhpConfigValue($phpConfig, '--version') ?? 'unknown'; + throw new \RuntimeException(sprintf( + "The `php-config` (%s) reports PHP %s, but the running PHP is %s. " . + 'Set PHP_HOME to the matching PHP installation.', + $phpConfig, + $version, + PHP_VERSION, + )); + } + protected function getPhpConfigValue(string $phpConfig, string $option): ?string { $value = shell_exec(escapeshellarg($phpConfig) . ' ' . escapeshellarg($option) . ' 2>/dev/null');