fix: resolve php-config from selected PHP home

master^2
韩天峰 4 hours ago
parent 2ef53e7eeb
commit f2db98b7da
  1. 84
      phpunit/src/Platform/PlatformTest.php
  2. 27
      src/Installer/LibPhpInstaller.php
  3. 114
      src/Platform/UnixPlatform.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 平台基本功能
*/

@ -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');
}

@ -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');

Loading…
Cancel
Save