diff --git a/composer.lock b/composer.lock index c83d7d11..3db03adc 100644 --- a/composer.lock +++ b/composer.lock @@ -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" } diff --git a/phpunit/src/Backend/BackendTest.php b/phpunit/src/Backend/BackendTest.php index fa21d536..18997cf0 100644 --- a/phpunit/src/Backend/BackendTest.php +++ b/phpunit/src/Backend/BackendTest.php @@ -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'); diff --git a/phpunit/src/Build/NativeBuildConfigurationTest.php b/phpunit/src/Build/NativeBuildConfigurationTest.php new file mode 100644 index 00000000..8083bd35 --- /dev/null +++ b/phpunit/src/Build/NativeBuildConfigurationTest.php @@ -0,0 +1,175 @@ +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); + } +} diff --git a/src/Build/NativeBuildConfigurationTrait.php b/src/Build/NativeBuildConfigurationTrait.php index 3e67d90c..dbca0c71 100644 --- a/src/Build/NativeBuildConfigurationTrait.php +++ b/src/Build/NativeBuildConfigurationTrait.php @@ -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()); + } + } + /** * 解析库文件 */ diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index afff4720..7ca200d2 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -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:: 直接调用,无需动态符号表 diff --git a/src/CompilerBase.php b/src/CompilerBase.php index 2663aadf..8ac98b30 100644 --- a/src/CompilerBase.php +++ b/src/CompilerBase.php @@ -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) {