修复 macOS 下的问题

pull/43/head
韩天峰 4 weeks ago
parent 852c70198b
commit ac795c9f8a
  1. 32
      composer.lock
  2. 14
      phpunit/src/Backend/BackendTest.php
  3. 175
      phpunit/src/Build/NativeBuildConfigurationTest.php
  4. 88
      src/Build/NativeBuildConfigurationTrait.php
  5. 7
      src/Build/SourcePipelineTrait.php
  6. 31
      src/CompilerBase.php

32
composer.lock generated

@ -4,7 +4,7 @@
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies",
"This file is @generated automatically"
],
"content-hash": "67fbe7eb507e9f89697eb3f1f0f1edda",
"content-hash": "230c2948c23aa6b500c006a3846c8acf",
"packages": [
{
"name": "ajaxray/ansikit",
@ -342,16 +342,16 @@
},
{
"name": "swoole/phpx",
"version": "v2.4.2",
"version": "v2.4.3",
"source": {
"type": "git",
"url": "https://github.com/swoole/phpx.git",
"reference": "8e7859f7155f5c8961cd04f2cb07751d6cd42885"
"reference": "6424c2b9bd4e910fc7e446b1ece5699818c08454"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/swoole/phpx/zipball/8e7859f7155f5c8961cd04f2cb07751d6cd42885",
"reference": "8e7859f7155f5c8961cd04f2cb07751d6cd42885",
"url": "https://api.github.com/repos/swoole/phpx/zipball/6424c2b9bd4e910fc7e446b1ece5699818c08454",
"reference": "6424c2b9bd4e910fc7e446b1ece5699818c08454",
"shasum": ""
},
"require": {
@ -381,9 +381,9 @@
],
"support": {
"issues": "https://github.com/swoole/phpx/issues",
"source": "https://github.com/swoole/phpx/tree/v2.4.2"
"source": "https://github.com/swoole/phpx/tree/v2.4.3"
},
"time": "2026-07-27T10:25:52+00:00"
"time": "2026-07-31T09:43:58+00:00"
},
{
"name": "symfony/polyfill-ctype",
@ -1180,16 +1180,16 @@
},
{
"name": "friendsofphp/php-cs-fixer",
"version": "v3.95.17",
"version": "v3.95.18",
"source": {
"type": "git",
"url": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer.git",
"reference": "0ee88422118f3cc59c8c3def222ba7f1493b6d5b"
"reference": "a8b4e4216faabf67f4e96110ee99a48c96e4e683"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/0ee88422118f3cc59c8c3def222ba7f1493b6d5b",
"reference": "0ee88422118f3cc59c8c3def222ba7f1493b6d5b",
"url": "https://api.github.com/repos/PHP-CS-Fixer/PHP-CS-Fixer/zipball/a8b4e4216faabf67f4e96110ee99a48c96e4e683",
"reference": "a8b4e4216faabf67f4e96110ee99a48c96e4e683",
"shasum": ""
},
"require": {
@ -1273,7 +1273,7 @@
],
"support": {
"issues": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/issues",
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.17"
"source": "https://github.com/PHP-CS-Fixer/PHP-CS-Fixer/tree/v3.95.18"
},
"funding": [
{
@ -1281,7 +1281,7 @@
"type": "github"
}
],
"time": "2026-07-24T13:54:39+00:00"
"time": "2026-07-30T15:46:02+00:00"
},
{
"name": "myclabs/deep-copy",
@ -4856,12 +4856,12 @@
],
"aliases": [],
"minimum-stability": "stable",
"stability-flags": {},
"stability-flags": [],
"prefer-stable": false,
"prefer-lowest": false,
"platform": {
"php": ">=8.4 <8.6"
},
"platform-dev": {},
"plugin-api-version": "2.9.0"
"platform-dev": [],
"plugin-api-version": "2.1.0"
}

@ -381,11 +381,25 @@ class BackendTest extends TestCase
}
};
// getLibraries() 必须先找到 phpx 库才能走到链接失败路径。
// 用临时目录提供静态库占位文件,使测试不依赖本机是否已构建 phpx。
$phpxHome = $dir . '/phpx';
mkdir($phpxHome . '/lib', 0777, true);
touch($phpxHome . '/lib/libphpx.a');
$previousPhpxHome = getenv('PHPX_HOME');
putenv('PHPX_HOME=' . $phpxHome);
try {
$compiler->build([$dir . '/missing.o']);
$this->fail('The failing linker command should abort the build');
} catch (TestError $e) {
$this->assertStringContainsString('link failed', $e->getMessage());
} finally {
if ($previousPhpxHome === false) {
putenv('PHPX_HOME');
} else {
putenv('PHPX_HOME=' . $previousPhpxHome);
}
}
$this->assertFileDoesNotExist($target . '.rsp');

@ -0,0 +1,175 @@
<?php
namespace TypePhp\Tests\Build;
use PHPUnit\Framework\TestCase;
use TypePhp\CompilerTest;
use TypePhp\Exception\TestError;
use TypePhp\Platform\Linux;
use TypePhp\Platform\Macos;
use TypePhp\Platform\PlatformBase;
use TypePhp\Platform\Windows;
final class NativeBuildConfigurationTest extends TestCase
{
private array $temporaryDirectories = [];
protected function tearDown(): void
{
parent::tearDown();
foreach ($this->temporaryDirectories as $dir) {
$this->removeDirectory($dir);
}
$this->temporaryDirectories = [];
}
/**
* 三个平台都必须能解析到对应名称的 phpx 库:
* Windows -> phpx.lib,Linux -> libphpx.so,macOS -> libphpx.dylib。
*/
public function testFindPhpxLibraryResolvesAllPlatforms(): void
{
$cases = [
'Linux' => [new Linux(), '/lib/libphpx.so'],
'macOS' => [new Macos(), '/lib/libphpx.dylib'],
'Windows' => [new Windows(), '\\lib\\phpx.lib'],
];
foreach ($cases as $label => [$platform, $relativeLib]) {
$phpxDir = $this->temporaryDirectory('phpx-' . strtolower($label));
// getPhpxDir() 会通过 realpath 规范化(macOS 上 /var -> /private/var),
// 这里按同样的规范化路径创建库文件,保证断言一致。
$phpxDir = realpath($phpxDir) ?: $phpxDir;
if ($platform instanceof Windows) {
mkdir($phpxDir . '\\lib', 0777, true);
$libPath = $phpxDir . '\\lib\\phpx.lib';
touch($libPath);
} else {
$libPath = $phpxDir . $relativeLib;
mkdir(dirname($libPath), 0777, true);
touch($libPath);
}
$restore = $this->withPhpxHome($phpxDir);
try {
$compiler = $this->newCompiler($platform);
$this->assertSame(
$libPath,
$compiler->findPhpxLibraryForTest(),
"{$label} 平台 phpx 库解析失败"
);
} finally {
$restore();
}
}
}
public function testValidatePhpxLibraryFailsFastWhenMissing(): void
{
$phpxDir = $this->temporaryDirectory('phpx-missing');
mkdir($phpxDir . '/lib', 0777, true);
$restore = $this->withPhpxHome($phpxDir);
try {
$compiler = $this->newCompiler(new Macos());
$this->expectException(TestError::class);
$this->expectExceptionMessage('phpx library not found');
$compiler->validatePhpxLibraryForTest();
} finally {
$restore();
}
}
public function testPhpxDirPrefersPhpxHomeOverVendor(): void
{
$root = $this->temporaryDirectory('phpx-priority-root');
mkdir($root . '/vendor/swoole/phpx', 0777, true);
$phpxHome = $this->temporaryDirectory('phpx-home');
$phpxHome = realpath($phpxHome) ?: $phpxHome;
$compiler = new class($root) extends CompilerTest {
public function __construct(string $rootPath)
{
parent::__construct($rootPath);
$this->forTest = true;
}
public function getPhpxDirForTest(): string
{
return $this->getPhpxDir();
}
};
$restore = $this->withPhpxHome($phpxHome);
try {
$this->assertSame($phpxHome, $compiler->getPhpxDirForTest());
} finally {
$restore();
}
}
private function newCompiler(PlatformBase $platform): object
{
$root = $this->temporaryDirectory('phpx-compiler-root');
$compiler = new class($root) extends CompilerTest {
public function __construct(string $rootPath)
{
parent::__construct($rootPath);
$this->forTest = true;
}
public function withPlatform(PlatformBase $platform): self
{
$this->platform = $platform;
return $this;
}
public function findPhpxLibraryForTest(): ?string
{
return $this->findPhpxLibrary();
}
public function validatePhpxLibraryForTest(): void
{
$this->validatePhpxLibrary();
}
};
return $compiler->withPlatform($platform);
}
private function withPhpxHome(string $dir): callable
{
$previous = getenv('PHPX_HOME');
putenv('PHPX_HOME=' . $dir);
return static function () use ($previous): void {
if ($previous === false) {
putenv('PHPX_HOME');
} else {
putenv('PHPX_HOME=' . $previous);
}
};
}
private function temporaryDirectory(string $prefix): string
{
$dir = sys_get_temp_dir() . '/' . $prefix . '_' . uniqid();
mkdir($dir, 0777, true);
$this->temporaryDirectories[] = $dir;
return $dir;
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
$files = array_diff(scandir($dir), ['.', '..']);
foreach ($files as $file) {
$path = $dir . DIRECTORY_SEPARATOR . $file;
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
}
rmdir($dir);
}
}

@ -85,32 +85,11 @@ trait NativeBuildConfigurationTrait
$libraries = [];
// phpx 库(根据平台使用不同的文件名格式)
if ($platform instanceof Windows) {
// Windows: phpx.lib (无 lib 前缀)
$phpxLibPath = $this->getPhpxDir() . '\\lib\\phpx.lib';
if (file_exists($phpxLibPath)) {
$libraries[] = $phpxLibPath; // 不添加引号,由 getLibraryFlags() 统一处理
} else {
$this->error('phpx.lib not found at: ' . $phpxLibPath);
}
} else {
// Linux/macOS: libphpx.so 或 libphpx.a
$sharedLibExt = $platform->getSharedLibraryExtension();
// getSharedLibraryExtension() 返回的值可能带点或不带点,需要统一处理
$extWithoutDot = ltrim($sharedLibExt, '.');
$phpxLibPath = $this->getPhpxDir() . '/lib/libphpx.' . $extWithoutDot;
if (file_exists($phpxLibPath)) {
$libraries[] = $phpxLibPath;
} else {
// 尝试静态库
$phpxStaticPath = $this->getPhpxDir() . '/lib/libphpx.a';
if (file_exists($phpxStaticPath)) {
$libraries[] = $phpxStaticPath;
} else {
$this->error('libphpx library not found');
}
}
$phpxLibPath = $this->findPhpxLibrary();
if ($phpxLibPath === null) {
$this->error($this->getPhpxLibraryErrorMessage());
}
$libraries[] = $phpxLibPath;
// extension 和 bin 模式都需要链接 PHP 库
if ($platform instanceof Windows) {
@ -153,6 +132,65 @@ trait NativeBuildConfigurationTrait
return $libraries;
}
/**
* 解析 phpx 库文件路径,库不存在时返回 null。
*
* Windows 使用 phpx.lib(无 lib 前缀);其他平台优先使用共享库
* (libphpx.so / libphpx.dylib),找不到时回退到静态库 libphpx.a。
*/
protected function findPhpxLibrary(): ?string
{
$platform = $this->getPlatform();
if ($platform instanceof Windows) {
$phpxLibPath = $this->getPhpxDir() . '\\lib\\phpx.lib';
return is_file($phpxLibPath) ? $phpxLibPath : null;
}
// Linux/macOS:共享库优先,静态库兜底
// getSharedLibraryExtension() 返回的值可能带点或不带点,需要统一处理
$sharedLibExt = ltrim($platform->getSharedLibraryExtension(), '.');
$phpxLibPath = $this->getPhpxDir() . '/lib/libphpx.' . $sharedLibExt;
if (is_file($phpxLibPath)) {
return $phpxLibPath;
}
$phpxStaticPath = $this->getPhpxDir() . '/lib/libphpx.a';
return is_file($phpxStaticPath) ? $phpxStaticPath : null;
}
/**
* 生成 phpx 库缺失时的错误信息
*/
protected function getPhpxLibraryErrorMessage(): string
{
$platform = $this->getPlatform();
if ($platform instanceof Windows) {
$expected = $this->getPhpxDir() . '\\lib\\phpx.lib';
$buildHint = 'Build PHPX first (for example, run `nmake phpx` in ' . $this->getPhpxDir() . '\\build)';
} else {
$sharedLibExt = ltrim($platform->getSharedLibraryExtension(), '.');
$expected = $this->getPhpxDir() . '/lib/libphpx.' . $sharedLibExt
. ' or ' . $this->getPhpxDir() . '/lib/libphpx.a';
$buildHint = 'Build phpx first (e.g. run `cmake --build ' . $this->getPhpxDir() . '/build`)';
}
return 'phpx library not found at: ' . $expected . PHP_EOL .
$buildHint . PHP_EOL .
'or set PHPX_HOME to a phpx installation that provides the library.';
}
/**
* 前置检测 phpx 库是否可用,在编译开始前报错,
* 避免所有源文件编译完成后才在链接阶段失败。
*/
protected function validatePhpxLibrary(): void
{
if ($this->findPhpxLibrary() === null) {
$this->error($this->getPhpxLibraryErrorMessage());
}
}
/**
* 解析库文件
*/

@ -95,6 +95,13 @@ trait SourcePipelineTrait
}
}
// 仅在 PHP 脚本入口(bin/tpc.php)前置检测 phpx 库:缺少库立即 fatal,
// 避免继续向下执行到文件处理/编译阶段才报错。已编译的 tpc 可执行文件
// 在进入 main() 前就由动态链接器加载 libphpx,无需(也无法)在此检测。
if (defined('TYPEPHP_PHP_SCRIPT_ENTRY')) {
$this->validatePhpxLibrary();
}
$this->validateCompilerToolchain();
// shell_exec 和 define 已通过 php::fn:: 直接调用,无需动态符号表

@ -488,10 +488,15 @@ class CompilerBase implements PropertyAccessContext
protected function getPhpxDir(): string
{
// 优先使用环境变量 PHPX_HOME
// 解析 phpx 目录,优先级(macOS / Windows / Linux 通用):
// 1. PHPX_HOME 环境变量指定的目录
// 2. vendor 目录(Composer 安装路径,或 TypePHP 源码检出下的 vendor/swoole/phpx)
$phpxDir = getenv('PHPX_HOME');
if ($phpxDir && is_dir($phpxDir)) {
return rtrim($phpxDir, '\/');
if ($phpxDir !== false && $phpxDir !== '') {
$phpxDir = rtrim($phpxDir, '\/');
if (is_dir($phpxDir)) {
return $this->normalizePhpxDir($phpxDir);
}
}
// Composer-installed TypePHP and PHPX are sibling packages. Ask
@ -500,21 +505,24 @@ class CompilerBase implements PropertyAccessContext
if (class_exists(\Composer\InstalledVersions::class)
&& \Composer\InstalledVersions::isInstalled('swoole/phpx')) {
$composerInstallPath = \Composer\InstalledVersions::getInstallPath('swoole/phpx');
if (is_string($composerInstallPath) && is_dir($composerInstallPath)) {
return rtrim($composerInstallPath, '\/');
if (is_string($composerInstallPath)) {
$composerInstallPath = rtrim($composerInstallPath, '\/');
if (is_dir($composerInstallPath)) {
return $this->normalizePhpxDir($composerInstallPath);
}
}
}
// TypePHP source checkout: phpx is installed below this repository.
$composerPhpxDir = $this->rootPath . self::PHPX_VENDOR_DIR;
if (is_dir($composerPhpxDir)) {
return $composerPhpxDir;
return $this->normalizePhpxDir($composerPhpxDir);
}
if (defined('ROOT_PATH')) {
$rootPhpxDir = ROOT_PATH . self::PHPX_VENDOR_DIR;
if (is_dir($rootPhpxDir)) {
return $rootPhpxDir;
return $this->normalizePhpxDir($rootPhpxDir);
}
}
@ -526,6 +534,15 @@ class CompilerBase implements PropertyAccessContext
);
}
/**
* 规范化 phpx 目录路径(解析相对路径与 ..,便于错误提示和后续路径拼接)
*/
private function normalizePhpxDir(string $dir): string
{
$real = realpath($dir);
return $real !== false ? $real : $dir;
}
protected function getPlatform(): PlatformBase
{
if ($this->platform === null) {

Loading…
Cancel
Save