temporaryDirectories as $dir) { $this->removeDirectory($dir); } $this->temporaryDirectories = []; } private function createTemporaryDirectory(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); } /** * 测试 MSVC 编译器基本信息 */ public function testMsvcBasic(): void { $platform = new Windows(); $compiler = new Msvc($platform); $this->assertEquals('MSVC', $compiler->getName()); $this->assertEquals('cl', $compiler->getCompilerCommand()); $this->assertEquals('link', $compiler->getLinkerCommand()); } /** * 测试 MSVC 完整编译命令 */ public function testMsvcBuildCompileCommand(): void { $platform = new Windows([], true); // ZTS mode $compiler = new Msvc($platform); $cmd = $compiler->buildCompileCommand( 'test.cpp', 'test.obj', [ 'optimize' => 2, 'cpp_std' => 'c++17', ] ); $this->assertStringContainsString('cl', $cmd); $this->assertStringContainsString('/c', $cmd); $this->assertStringContainsString('/DZEND_WIN32', $cmd); $this->assertStringContainsString('/DPHP_WIN32', $cmd); $this->assertStringContainsString('/DZTS', $cmd); // ZTS enabled $this->assertStringContainsString('/O2', $cmd); $this->assertStringContainsString('/W3', $cmd); $this->assertStringContainsString('/std:c++17', $cmd); $this->assertStringContainsString('/EHsc', $cmd); $this->assertStringContainsString('/MD', $cmd); $this->assertStringContainsString('/nologo', $cmd); } public function testMsvcBuildCCompileCommandKeepsSharedCompilerOptions(): void { $platform = new Windows([], true); $compiler = new Msvc($platform); $cmd = $compiler->buildCCompileCommand('misc.c', 'misc.obj', [ 'sanitize' => 'address', 'enable_profiler' => true, 'prof_output' => 'app.prof', 'user_defines' => ['FEATURE_X=1'], 'lto' => true, 'is_zts' => true, 'cflags' => '/experimental:c11atomics', ]); $this->assertStringContainsString('/TC', $cmd); $this->assertStringContainsString('/fsanitize=address', $cmd); $this->assertStringContainsString('/DPPROF_ON=1', $cmd); $this->assertStringContainsString('/D' . escapeshellarg('PROF_OUTPUT_FILE="app.prof"'), $cmd); $this->assertStringContainsString('/DFEATURE_X=1', $cmd); $this->assertStringContainsString('/GL', $cmd); $this->assertStringContainsString('/DZTS', $cmd); $this->assertStringContainsString('/experimental:c11atomics', $cmd); $this->assertStringNotContainsString('/EHsc', $cmd); $this->assertStringNotContainsString('/std:', $cmd); } public function testMsvcLargeObjectFormatAppliesToGeneratedAndNativeCommands(): void { $compiler = new Msvc(new Windows()); foreach ([false, true] as $debug) { $options = ['debug' => $debug]; $this->assertStringContainsString('/bigobj', $compiler->buildCompileCommand('generated.cc', 'generated.obj', $options)); $this->assertStringContainsString('/bigobj', $compiler->buildNativeCompileCommand('native.c', 'native.obj', $options, 'c')); } $this->assertStringNotContainsString('/bigobj', $compiler->buildLinkOptions()); } public function testMsvcDebugPdbOptionsApplyToCppAndCCommands(): void { $compiler = new Msvc(new Windows()); $pdb = 'C:\\build output\\cache\\msvc\\app.compile.pdb'; $options = ['debug' => true, 'compiler_pdb' => $pdb]; $cpp = $compiler->buildCompileCommand('app.cpp', 'app.obj', $options); $c = $compiler->buildCCompileCommand('helper.c', 'helper.obj', $options); foreach ([$cpp, $c] as $command) { $this->assertStringContainsString('/Fd' . escapeshellarg($pdb), $command); $this->assertStringContainsString('/FS', $command); } } /** * 测试 MSVC 完整链接命令 */ public function testMsvcBuildLinkCommand(): void { $platform = new Windows(); $compiler = new Msvc($platform); $cmd = $compiler->buildLinkCommand( ['test.obj'], 'output.exe', [ 'debug' => true, 'no_console' => false, ] ); $this->assertStringContainsString('link', $cmd); $this->assertStringContainsString('/OUT:', $cmd); $this->assertStringContainsString('/DEBUG', $cmd); $this->assertStringContainsString('/PDB:' . escapeshellarg('output.pdb'), $cmd); $this->assertStringContainsString('/NODEFAULTLIB:LIBCMT', $cmd); $this->assertStringContainsString('/nologo', $cmd); $compiler->cleanupResponseFile(); } public function testMsvcDebugLinkPdbFollowsOutputPath(): void { $compiler = new Msvc(new Windows()); $output = 'C:\\build output\\app.dll'; $cmd = $compiler->buildLinkCommand(['app.obj'], $output, ['debug' => true]); $this->assertStringContainsString( '/PDB:' . escapeshellarg('C:\\build output\\app.pdb'), $cmd ); $compiler->cleanupResponseFile(); } /** * 测试 GCC 编译器基本信息 */ public function testGccBasic(): void { $platform = new Linux(); $compiler = new Gcc($platform); $this->assertEquals('GCC', $compiler->getName()); $this->assertEquals('g++', $compiler->getCompilerCommand()); $this->assertEquals('g++', $compiler->getLinkerCommand()); } public function testGccBuildCompileCommandUsesCustomCompilerAndIncludes(): void { $platform = new Linux(); $compiler = new Gcc($platform, '/opt/toolchain/bin/g++'); $cmd = $compiler->buildCompileCommand('test.cpp', 'test.o', [ 'include_paths' => ['/usr/include/php'], 'cpp_std' => 'c++20', 'cxxflags' => '-fno-rtti', ]); $this->assertStringStartsWith('/opt/toolchain/bin/g++', $cmd); $this->assertStringContainsString('-I' . escapeshellarg('/usr/include/php'), $cmd); $this->assertStringContainsString('-std=c++20', $cmd); $this->assertStringContainsString('-fno-rtti', $cmd); } public function testGccOnWindowsUsesGccIncludeSyntaxForAllSourceTypes(): void { $compiler = new Gcc(new Windows()); $includePaths = ['C:\\phpx\\include', 'C:\\Program Files\\PHP\\include']; $options = [ 'include_paths' => $includePaths, ]; $commands = [ $compiler->buildCompileCommand('test.cpp', 'test.obj', $options), $compiler->buildCCompileCommand('test.c', 'test.obj', $options), $compiler->buildNativeCompileCommand('test.S', 'test.obj', $options, 'assembler'), ]; foreach ($commands as $cmd) { foreach ($includePaths as $path) { $this->assertStringContainsString('-I' . escapeshellarg($path), $cmd); } $this->assertStringNotContainsString('/I ', $cmd); } } public function testGccBuildCCompileCommandKeepsSharedCompilerOptions(): void { $platform = new Linux(); $compiler = new Gcc($platform); $cmd = $compiler->buildCCompileCommand('misc.c', 'misc.o', [ 'sanitize' => 'address', 'enable_profiler' => true, 'prof_output' => 'app.prof', 'user_defines' => ['FEATURE_X=1'], 'lto' => true, 'march' => 'native', 'target_platform' => 'aarch64-linux-gnu', 'build_mode' => 'ext', 'cflags' => '-ffreestanding -fno-builtin', ]); $this->assertStringContainsString('-fsanitize=address', $cmd); $this->assertStringContainsString('-DPPROF_ON=1', $cmd); $this->assertStringContainsString('-D' . escapeshellarg('PROF_OUTPUT_FILE="app.prof"'), $cmd); $this->assertStringContainsString('-DFEATURE_X=1', $cmd); $this->assertStringContainsString('-flto', $cmd); $this->assertStringContainsString('-march=native', $cmd); $this->assertStringContainsString('--target=aarch64-linux-gnu', $cmd); $this->assertStringContainsString('-fPIC', $cmd); $this->assertStringContainsString('-ffreestanding -fno-builtin', $cmd); } public function testGccBuildAssemblerCommandUsesNativeFlags(): void { $compiler = new Gcc(new Linux()); $cmd = $compiler->buildNativeCompileCommand('entry.S', 'entry.o', [ 'nativeflags' => '-m64 -mno-red-zone', ], 'assembler'); $this->assertStringContainsString('-x assembler', $cmd); $this->assertStringContainsString('-m64 -mno-red-zone', $cmd); } public function testGccBuildLinkCommandIncludesPlatformPathsOptionsAndLibraries(): void { $platform = new Linux(); $compiler = new Gcc($platform, 'g++'); $cmd = $compiler->buildLinkCommand(['a.o', 'b.o'], 'app', [ 'library_paths' => ['/usr/lib'], 'libraries' => ['/usr/lib/libphpx.so', 'php'], 'ldflags' => '-Wl,--as-needed', 'build_mode' => 'ext', ]); $this->assertStringStartsWith('g++', $cmd); $this->assertStringContainsString('-L' . escapeshellarg('/usr/lib'), $cmd); $this->assertStringContainsString('-Wl,--as-needed', $cmd); $this->assertStringContainsString('-shared', $cmd); $this->assertStringContainsString('-lphpx', $cmd); $this->assertStringContainsString('-lphp', $cmd); $compiler->cleanupResponseFile(); } public function testBuildCleansResponseFileWhenLinkFails(): void { $dir = $this->createTemporaryDirectory('backend_link_failure'); $target = $dir . '/app'; $backend = new Gcc(new Linux(), 'false'); $compiler = new class(TYPEPHP_ROOT_PATH, $target, $backend) extends CompilerTest { public function __construct( string $rootPath, private readonly string $testTarget, \TypePhp\Backend\CompilerBackend $backend ) { parent::__construct($rootPath); $this->forTest = true; $this->compilerBackend = $backend; } protected function getTargetFileName(): string { return $this->testTarget; } }; // 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'); } public function testResponseFileArgumentIsEscapedForPathsWithSpaces(): void { $platform = new Linux(); $compiler = new Gcc($platform, 'g++'); $dir = $this->createTemporaryDirectory('backend link path'); $target = $dir . '/my app'; $rspFile = $target . '.rsp'; $objectWithSpace = $dir . '/object one.o'; $objectWithoutSpace = $dir . '/object_two.o'; $cmd = $compiler->buildLinkCommand([$objectWithSpace, $objectWithoutSpace], $target); $this->assertStringContainsString(escapeshellarg('@' . $rspFile), $cmd); $this->assertStringContainsString('-o ' . escapeshellarg($target), $cmd); $this->assertFileExists($rspFile); $lines = file($rspFile, FILE_IGNORE_NEW_LINES); $this->assertSame('"' . $objectWithSpace . '"', $lines[0]); $this->assertSame('"' . $objectWithoutSpace . '"', $lines[1]); } public function testResponseFileCanBePlacedInTheBuildDirectory(): void { $platform = new Linux(); $compiler = new Gcc($platform, 'g++'); $dir = $this->createTemporaryDirectory('backend response build directory'); $target = $dir . '/output/my app'; $rspFile = $dir . '/build/my app.rsp'; $object = $dir . '/object.o'; $cmd = $compiler->buildLinkCommand( [$object], $target, ['response_file' => $rspFile], ); $this->assertStringContainsString(escapeshellarg('@' . $rspFile), $cmd); $this->assertFileExists($rspFile); $this->assertFileDoesNotExist($target . '.rsp'); } /** * 测试 Clang 编译器基本信息 */ public function testClangBasic(): void { $platform = new Linux(); $compiler = new Clang($platform); $this->assertEquals('Clang', $compiler->getName()); $this->assertEquals('clang++', $compiler->getCompilerCommand()); $this->assertEquals('clang++', $compiler->getLinkerCommand()); } /** * 测试 Clang Windows 平台链接器 */ public function testClangWindowsLinker(): void { $platform = new Windows(); $compiler = new Clang($platform); // Windows 下 Clang 使用 link.exe $this->assertEquals('link', $compiler->getLinkerCommand()); } public function testCompilerFactoryKeepsConfiguredCompilerCommand(): void { $compiler = CompilerFactory::createByName('/opt/llvm/bin/clang++', new Linux()); $this->assertInstanceOf(Clang::class, $compiler); $this->assertSame('/opt/llvm/bin/clang++', $compiler->getCompilerCommand()); $this->assertSame('/opt/llvm/bin/clang++', $compiler->getLinkerCommand()); } }