diff --git a/phpunit/src/Backend/BackendOptionsTest.php b/phpunit/src/Backend/BackendOptionsTest.php index e30fff17..91f0eac0 100644 --- a/phpunit/src/Backend/BackendOptionsTest.php +++ b/phpunit/src/Backend/BackendOptionsTest.php @@ -121,6 +121,21 @@ class BackendOptionsTest extends TestCase $this->assertStringContainsString('/DPPROF_ON=1', $options); } + public function testMsvcCompileOptionsEscapesUnsafeDefines(): void + { + $platform = new Windows(); + $compiler = new Msvc($platform); + + $options = $compiler->buildCompileOptions([ + 'enable_profiler' => true, + 'prof_output' => 'profile output.log', + 'user_defines' => ['APP_NAME="hello world"'], + ]); + + $this->assertStringContainsString('/D' . escapeshellarg('APP_NAME="hello world"'), $options); + $this->assertStringContainsString('/D' . escapeshellarg('PROF_OUTPUT_FILE="profile output.log"'), $options); + } + /** * 测试 MSVC 编译选项 - 自定义标志 */ @@ -276,6 +291,21 @@ class BackendOptionsTest extends TestCase $this->assertStringContainsString('-fPIC', $options); } + public function testGccCompileOptionsEscapesUnsafeDefines(): void + { + $platform = new Linux(); + $compiler = new Gcc($platform); + + $options = $compiler->buildCompileOptions([ + 'enable_profiler' => true, + 'prof_output' => 'profile output.log', + 'user_defines' => ['APP_NAME="hello world"'], + ]); + + $this->assertStringContainsString('-D' . escapeshellarg('APP_NAME="hello world"'), $options); + $this->assertStringContainsString('-D' . escapeshellarg('PROF_OUTPUT_FILE="profile output.log"'), $options); + } + /** * 测试 GCC 链接选项 - 基本配置 */ diff --git a/phpunit/src/Backend/BackendTest.php b/phpunit/src/Backend/BackendTest.php index e35f7bdf..9316f0e5 100644 --- a/phpunit/src/Backend/BackendTest.php +++ b/phpunit/src/Backend/BackendTest.php @@ -13,6 +13,39 @@ use PhpAot\Php\Backend\CompilerFactory; class BackendTest extends TestCase { + private array $temporaryDirectories = []; + + protected function tearDown(): void + { + parent::tearDown(); + foreach ($this->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 编译器基本信息 */ @@ -103,6 +136,31 @@ class BackendTest extends TestCase $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, + ]); + + $this->assertStringContainsString('/TC', $cmd); + $this->assertStringContainsString('/fsanitize=address', $cmd); + $this->assertStringContainsString('/DPPROF_ON=1', $cmd); + $this->assertStringContainsString('/DPROF_OUTPUT_FILE=', $cmd); + $this->assertStringContainsString('/DFEATURE_X=1', $cmd); + $this->assertStringContainsString('/GL', $cmd); + $this->assertStringContainsString('/DZTS', $cmd); + $this->assertStringNotContainsString('/EHsc', $cmd); + $this->assertStringNotContainsString('/std:', $cmd); + } + /** * 测试 MSVC 完整链接命令 */ @@ -250,6 +308,32 @@ class BackendTest extends TestCase $this->assertStringContainsString('-fno-rtti', $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', + ]); + + $this->assertStringContainsString('-fsanitize=address', $cmd); + $this->assertStringContainsString('-DPPROF_ON=1', $cmd); + $this->assertStringContainsString('-DPROF_OUTPUT_FILE=', $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); + } + public function testGccBuildLinkCommandIncludesPlatformPathsOptionsAndLibraries(): void { $platform = new Linux(); @@ -270,6 +354,27 @@ class BackendTest extends TestCase $this->assertStringContainsString('-lphp', $cmd); } + 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]); + } + /** * 测试 GCC 完整编译选项 */ diff --git a/phpunit/src/CompilerBaseApiTest.php b/phpunit/src/CompilerBaseApiTest.php index a732b4f4..e7df6af8 100644 --- a/phpunit/src/CompilerBaseApiTest.php +++ b/phpunit/src/CompilerBaseApiTest.php @@ -12,12 +12,14 @@ class CompilerBaseApiTest extends TestCase private CompilerTest $compiler; private \ReflectionClass $ref; private array $originalArgv; + private string|false $originalPath; protected function setUp(): void { parent::setUp(); global $argv; $this->originalArgv = $argv ?? []; + $this->originalPath = getenv('PATH'); $this->testDir = sys_get_temp_dir() . '/compiler_api_test_' . uniqid(); mkdir($this->testDir, 0777, true); $this->compiler = CompilerTest::create($this->testDir); @@ -29,6 +31,11 @@ class CompilerBaseApiTest extends TestCase parent::tearDown(); global $argv; $argv = $this->originalArgv; + if ($this->originalPath === false) { + putenv('PATH'); + } else { + putenv('PATH=' . $this->originalPath); + } // Recursively remove the test directory (compiler creates build/ subdir) $this->removeDirectory($this->testDir); } @@ -67,17 +74,29 @@ class CompilerBaseApiTest extends TestCase return $m->invoke($this->compiler, ...$args); } - private function createProjectFile(string $yaml): string + private function createProjectFile(string $yaml, string $filename = 'project.yml', string $baseDir = ''): string { - $sourceFile = $this->testDir . '/main.php'; + $projectDir = $baseDir === '' ? $this->testDir : $this->testDir . '/' . trim($baseDir, '/'); + if (!is_dir($projectDir)) { + mkdir($projectDir, 0777, true); + } + + $sourceFile = $projectDir . '/main.php'; file_put_contents($sourceFile, "testDir . '/project.yml'; + $projectFile = $projectDir . '/' . $filename; file_put_contents($projectFile, $yaml); return $projectFile; } + private function createFakeClangFormat(string $binDir, string $logFile): void + { + mkdir($binDir, 0777, true); + file_put_contents($binDir . '/clang-format', "#!/bin/sh\nif [ \"$1\" = \"--version\" ]; then\n echo 'clang-format version test'\n exit 0\nfi\npwd > " . escapeshellarg($logFile) . "\nprintf '%s\\n' \"$@\" >> " . escapeshellarg($logFile) . "\n"); + chmod($binDir . '/clang-format', 0755); + } + // ======================================================================== // getTypeFromZendType // ======================================================================== @@ -154,6 +173,11 @@ class CompilerBaseApiTest extends TestCase $this->assertStringStartsWith($this->testDir, $buildDir); } + public function testFormatCodeDisabledByDefault(): void + { + $this->assertFalse($this->getPropertyValue('formatCode')); + } + public function testGetIncludeDir(): void { $includeDir = $this->compiler->getIncludeDir(); @@ -163,9 +187,24 @@ class CompilerBaseApiTest extends TestCase public function testParseProjectYamlLoadsDocumentedCompilerOptions(): void { + $binDir = $this->testDir . '/bin'; + $formatLog = $this->testDir . '/format.log'; + $this->createFakeClangFormat($binDir, $formatLog); + putenv('PATH=' . $binDir . ':' . ($this->originalPath ?: '')); + $projectFile = $this->createProjectFile(<<<'YAML' sources: - main.php +optimize: 2 +job: 8 +debug: true +profile: true +no-progress: true +no-console: true +no-literal-strings: true +sanitize: address +target-platform: aarch64-linux-gnu +build-dir: /tmp/project-build include-paths: - /opt/mylib/include - ../shared/headers @@ -173,6 +212,7 @@ defines: - ENABLE_LOGGING=1 - DEBUG_LEVEL=3 lto: true +format: true link-libs: - curl - ssl @@ -183,11 +223,56 @@ YAML); $this->invokeMethod('parseProjectYaml', $projectFile); + $this->assertSame(2, $this->getPropertyValue('optimizeLevel')); + $this->assertSame(8, $this->getPropertyValue('maxJob')); + $this->assertTrue($this->getPropertyValue('debug')); + $this->assertTrue($this->getPropertyValue('enableProfiler')); + $this->assertTrue($this->getPropertyValue('noProgress')); + $this->assertTrue($this->getPropertyValue('noConsole')); + $this->assertTrue($this->getPropertyValue('noLiteralStrings')); + $this->assertSame('address', $this->getPropertyValue('sanitize')); + $this->assertSame('aarch64-linux-gnu', $this->getPropertyValue('targetPlatform')); $this->assertSame(['/opt/mylib/include', '../shared/headers'], $this->compiler->getUserIncludePaths()); $this->assertSame(['ENABLE_LOGGING=1', 'DEBUG_LEVEL=3'], $this->compiler->getUserDefines()); $this->assertTrue($this->compiler->isLtoEnabled()); $this->assertSame(['curl', 'ssl'], $this->compiler->getLinkLibs()); $this->assertSame(['/usr/local/lib', '/opt/custom/lib'], $this->compiler->getLinkPaths()); + $this->assertSame('/tmp/project-build', $this->compiler->getBuildDir()); + $this->assertTrue($this->getPropertyValue('formatCode')); + } + + public function testParseProjectYamlSupportsCustomFilenameAndRelativeBuildDir(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - main.php +build-dir: build/output +YAML, 'myproject.yml', 'nested/config'); + + $this->invokeMethod('parseProjectYaml', $projectFile); + + $this->assertSame( + realpath($this->testDir . '/nested/config/build/output'), + $this->compiler->getBuildDir() + ); + } + + public function testParseProjectYamlSupportsCliStyleModeAndOutputAliases(): void + { + $projectFile = $this->createProjectFile(<<<'YAML' +sources: + - main.php +mode: ext +output: out/custom-ext +dry: true +YAML, 'custom-name.yml', 'yaml-alias'); + + $this->invokeMethod('parseProjectYaml', $projectFile); + + $this->assertSame(CompilerBase::BUILD_MODE_EXT, $this->getPropertyValue('buildMode')); + $this->assertTrue($this->getPropertyValue('dryRun')); + $this->assertSame('custom_ext', $this->getPropertyValue('targetName')); + $this->assertSame('out', $this->getPropertyValue('outputDir')); } public function testApplyCommandLineArgumentsDoesNotClearYamlRepeatableOptionsWhenCliAbsent(): void @@ -216,6 +301,37 @@ YAML); $this->assertSame(['/yaml/lib'], $this->compiler->getLinkPaths()); } + public function testFormatCppCodeEscapesPathsWithSpaces(): void + { + $spaceDir = sys_get_temp_dir() . '/compiler api format ' . uniqid(); + mkdir($spaceDir, 0777, true); + $binDir = $spaceDir . '/bin'; + $logFile = $spaceDir . '/format.log'; + $sourceFile = $spaceDir . '/hello world.cc'; + + file_put_contents($sourceFile, "int main() { return 0; }\n"); + $this->createFakeClangFormat($binDir, $logFile); + putenv('PATH=' . $binDir . ':' . ($this->originalPath ?: '')); + + $compiler = CompilerTest::create($spaceDir); + $ref = new \ReflectionClass($compiler); + $formatProp = $ref->getProperty('formatCode'); + $formatProp->setAccessible(true); + $formatProp->setValue($compiler, true); + + $method = $ref->getMethod('formatCppCode'); + $method->setAccessible(true); + $method->invoke($compiler, $sourceFile); + + $this->assertFileExists($logFile); + $lines = file($logFile, FILE_IGNORE_NEW_LINES); + $this->assertSame($spaceDir, $lines[0]); + $this->assertSame('-i', $lines[1]); + $this->assertSame($sourceFile, $lines[2]); + + $this->removeDirectory($spaceDir); + } + // ======================================================================== // isWindows / isLinux / isMacos // ======================================================================== diff --git a/src/Php/Backend/CompilerBackend.php b/src/Php/Backend/CompilerBackend.php index 23bcb466..a206749c 100644 --- a/src/Php/Backend/CompilerBackend.php +++ b/src/Php/Backend/CompilerBackend.php @@ -158,6 +158,15 @@ abstract class CompilerBackend return $this->platform->getLibraryFlags($libraries); } + protected function formatDefineFlag(string $define, string $prefix): string + { + if (preg_match('/^[A-Za-z_][A-Za-z0-9_]*(=(?:[A-Za-z0-9_.,:+\/@%-]+))?$/', $define) === 1) { + return $prefix . $define; + } + + return $prefix . escapeshellarg($define); + } + /** * 将目标文件列表写入 Response File,避免命令行参数过长超出 OS 限制(Windows 8191 字符) * @@ -178,7 +187,7 @@ abstract class CompilerBackend $lines[] = $file; } file_put_contents($rspFile, implode("\n", $lines)); - return '@' . $rspFile; + return escapeshellarg('@' . $rspFile); } /** diff --git a/src/Php/Backend/GccLikeBackend.php b/src/Php/Backend/GccLikeBackend.php index c31cf242..790d7e74 100644 --- a/src/Php/Backend/GccLikeBackend.php +++ b/src/Php/Backend/GccLikeBackend.php @@ -59,6 +59,63 @@ abstract class GccLikeBackend extends CompilerBackend return ''; } + /** 构建 GCC/Clang 共享编译选项,C 和 C++ 编译路径都复用这里 */ + protected function buildSharedCompileFlags(array $config, bool $includeCppStd = false): string + { + $cmd = ''; + + if (!empty($config['sanitize'])) { + $cmd .= ' ' . $this->formatSanitizerFlag($config['sanitize']); + } + + if (!empty($config['debug'])) { + $cmd .= ' -O0 -g'; + } else { + $optimizeLevel = $config['optimize'] ?? 2; + $cmd .= ' -O' . $optimizeLevel; + } + + $cmd .= ' -Wall'; + + if ($includeCppStd && !empty($config['cpp_std'])) { + $cmd .= ' -std=' . $config['cpp_std']; + } + + if (!empty($config['march'])) { + $cmd .= ' -march=' . $config['march']; + } + + if (!empty($config['target_platform'])) { + $cmd .= ' --target=' . $config['target_platform']; + } + + $cmd .= $this->getPICFlag($config); + + if (!empty($config['enable_profiler'])) { + $cmd .= ' ' . $this->formatDefineFlag('PPROF_ON=1', '-D'); + if (!empty($config['prof_output'])) { + $profOutput = addcslashes($config['prof_output'], "\\\""); + $cmd .= ' ' . $this->formatDefineFlag('PROF_OUTPUT_FILE="' . $profOutput . '"', '-D'); + } + } + + if ($includeCppStd && !empty($config['cxxflags'])) { + $cmd .= ' ' . $config['cxxflags']; + } + + if (!empty($config['user_defines'])) { + foreach ($config['user_defines'] as $define) { + $cmd .= ' ' . $this->formatDefineFlag($define, '-D'); + } + } + + if (!empty($config['lto'])) { + $cmd .= ' -flto'; + } + + return $cmd; + } + /** 获取平台特定的链接选项 */ protected function getPlatformLinkFlags(array $config): string { @@ -116,7 +173,7 @@ abstract class GccLikeBackend extends CompilerBackend } foreach ($defines as $define) { - $cmd .= ' -D' . $define; + $cmd .= ' ' . $this->formatDefineFlag($define, '-D'); } if (!empty($flags)) { @@ -181,15 +238,7 @@ abstract class GccLikeBackend extends CompilerBackend if (!empty($options['include_paths'])) { $cmd .= ' ' . $this->formatIncludePaths($options['include_paths']); } - - $optimizeLevel = $options['optimize'] ?? 0; - if (!empty($options['debug'])) { - $cmd .= ' -O0 -g'; - } else { - $cmd .= ' -O' . $optimizeLevel; - } - - $cmd .= ' -Wall'; + $cmd .= $this->buildSharedCompileFlags($options, false); return $cmd; } @@ -239,58 +288,8 @@ abstract class GccLikeBackend extends CompilerBackend public function buildCompileOptions(array $config = []): string { - $cmd = ''; - - $cmd .= $this->getCompilerPrefixFlags(); - - if (!empty($config['sanitize'])) { - $cmd .= ' ' . $this->formatSanitizerFlag($config['sanitize']); - } - - if (!empty($config['debug'])) { - $cmd .= ' -O0 -g'; - } else { - $optimizeLevel = $config['optimize'] ?? 2; - $cmd .= ' -O' . $optimizeLevel; - } - - $cmd .= ' -Wall'; - - if (!empty($config['cpp_std'])) { - $cmd .= ' -std=' . $config['cpp_std']; - } - - if (!empty($config['march'])) { - $cmd .= ' -march=' . $config['march']; - } - - if (!empty($config['target_platform'])) { - $cmd .= ' --target=' . $config['target_platform']; - } - - $cmd .= $this->getPICFlag($config); - - if (!empty($config['enable_profiler'])) { - $cmd .= ' -DPPROF_ON=1'; - if (!empty($config['prof_output'])) { - $cmd .= ' -DPROF_OUTPUT_FILE=\'"' . $config['prof_output'] . '"\''; - } - } - - if (!empty($config['cxxflags'])) { - $cmd .= ' ' . $config['cxxflags']; - } - - if (!empty($config['user_defines'])) { - foreach ($config['user_defines'] as $define) { - $cmd .= ' -D' . $define; - } - } - - if (!empty($config['lto'])) { - $cmd .= ' -flto'; - } - + $cmd = $this->getCompilerPrefixFlags(); + $cmd .= $this->buildSharedCompileFlags($config, true); return $cmd; } @@ -317,35 +316,8 @@ abstract class GccLikeBackend extends CompilerBackend public function buildFullCompileOptions(array $options = []): string { - $cmd = ''; - - $cmd .= $this->getCompilerPrefixFlags(); - - if (!empty($options['debug'])) { - $cmd .= ' -O0 -g'; - } else { - $optimizeLevel = $options['optimize'] ?? 2; - $cmd .= ' -O' . $optimizeLevel; - } - - $cmd .= ' -Wall'; - - if (!empty($options['cpp_std'])) { - $cmd .= ' -std=' . $options['cpp_std']; - } - - if (!empty($options['march'])) { - $cmd .= ' -march=' . $options['march']; - } - - if (!empty($options['sanitize'])) { - $cmd .= ' ' . $this->formatSanitizerFlag($options['sanitize']); - } - - if (!empty($options['pic'])) { - $cmd .= ' -fPIC'; - } - + $cmd = $this->getCompilerPrefixFlags(); + $cmd .= $this->buildSharedCompileFlags($options, true); return $cmd; } diff --git a/src/Php/Backend/Msvc.php b/src/Php/Backend/Msvc.php index 817632a5..9256d68e 100644 --- a/src/Php/Backend/Msvc.php +++ b/src/Php/Backend/Msvc.php @@ -34,6 +34,75 @@ class Msvc extends CompilerBackend return $this->linkerCommand; } + private function buildCommonCompileFlags(array $config, bool $includeCppOptions = true): string + { + $cmd = ''; + + $cmd .= ' /DZEND_WIN32 /DPHP_WIN32 /DZEND_DEBUG=0'; + + if (!empty($config['is_zts'])) { + $cmd .= ' /DZTS'; + } + + if (!empty($config['sanitize'])) { + if ($config['sanitize'] === 'address' || $config['sanitize'] === 'addr') { + $cmd .= ' /fsanitize=address'; + } + } + + if (!empty($config['debug'])) { + $cmd .= ' /Od /Zi'; + } else { + $optimizeLevel = $config['optimize'] ?? 2; + $optMap = [0 => '/Od', 1 => '/O1', 2 => '/O2', 3 => '/Ox']; + $cmd .= ' ' . ($optMap[$optimizeLevel] ?? '/O2'); + } + + $cmd .= ' /W3'; + + if (!empty($config['suppressed_warnings'])) { + foreach ($config['suppressed_warnings'] as $code => $description) { + $code = is_int($code) && $code < 100 ? $description : $code; + $cmd .= " /wd{$code}"; + } + } + + if (!empty($config['enable_profiler'])) { + $cmd .= ' ' . $this->formatDefineFlag('PPROF_ON=1', '/D'); + if (!empty($config['prof_output'])) { + $profOutput = addcslashes($config['prof_output'], "\\\""); + $cmd .= ' ' . $this->formatDefineFlag('PROF_OUTPUT_FILE="' . $profOutput . '"', '/D'); + } + } + + if (!empty($config['user_defines'])) { + foreach ($config['user_defines'] as $define) { + $cmd .= ' ' . $this->formatDefineFlag($define, '/D'); + } + } + + if (!empty($config['lto'])) { + $cmd .= ' /GL'; + } + + if ($includeCppOptions) { + $cmd .= ' /EHsc'; + if (!empty($config['cpp_std'])) { + $cmd .= ' /std:' . $config['cpp_std']; + } + + $cmd .= ' /MD'; + + if (!empty($config['cxxflags'])) { + $cmd .= ' ' . $config['cxxflags']; + } + } + + $cmd .= ' /nologo'; + + return $cmd; + } + public function compileFile( string $sourceFile, string $outputFile, @@ -52,7 +121,7 @@ class Msvc extends CompilerBackend // 添加宏定义 foreach ($defines as $define) { - $cmd .= ' /D' . $define; + $cmd .= ' ' . $this->formatDefineFlag($define, '/D'); } // 添加额外标志 @@ -129,29 +198,7 @@ class Msvc extends CompilerBackend } // 平台宏定义 - $cmd .= ' /DZEND_WIN32 /DPHP_WIN32 /DZEND_DEBUG=0'; - - // ZTS 支持 - if ($this->platform instanceof Windows && $this->platform->isZts()) { - $cmd .= ' /DZTS'; - } - - // 优化级别(C 文件通常使用较低的优化) - $optimizeLevel = $options['optimize'] ?? 0; - $optMap = [0 => '/Od', 1 => '/O1', 2 => '/O2', 3 => '/Ox']; - $cmd .= ' ' . ($optMap[$optimizeLevel] ?? '/O2'); - - // 警告级别 - $cmd .= ' /W3'; - - // 禁用常见警告 - $suppressedWarnings = $options['suppressed_warnings'] ?? ['4244', '4146']; - foreach ($suppressedWarnings as $code) { - $cmd .= " /wd{$code}"; - } - - // nologo - $cmd .= ' /nologo'; + $cmd .= $this->buildCommonCompileFlags($options, false); // 注意:C 文件不使用 /EHsc, /std:c++17, /MD 等 C++ 特定选项 @@ -220,7 +267,7 @@ class Msvc extends CompilerBackend // 宏定义 foreach ($defines as $define) { - $cmd .= ' /D' . $define; + $cmd .= ' ' . $this->formatDefineFlag($define, '/D'); } // 编译选项 @@ -322,86 +369,7 @@ class Msvc extends CompilerBackend */ public function buildCompileOptions(array $config = []): string { - $cmd = ''; - - // 平台宏定义 - $cmd .= ' /DZEND_WIN32 /DPHP_WIN32 /DZEND_DEBUG=0'; - - // ZTS - if (!empty($config['is_zts'])) { - $cmd .= ' /DZTS'; - } - - // Sanitizer - if (!empty($config['sanitize'])) { - if ($config['sanitize'] === 'address' || $config['sanitize'] === 'addr') { - $cmd .= ' /fsanitize=address'; - } - } - - // 优化和调试 - if (!empty($config['debug'])) { - $cmd .= ' /Od /Zi'; - } else { - $optimizeLevel = $config['optimize'] ?? 2; - $optMap = [0 => '/Od', 1 => '/O1', 2 => '/O2', 3 => '/Ox']; - $cmd .= ' ' . ($optMap[$optimizeLevel] ?? '/O2'); - } - - // 警告 - $cmd .= ' /W3'; - - // 禁用常见警告(只使用键,即警告代码) - if (!empty($config['suppressed_warnings'])) { - foreach ($config['suppressed_warnings'] as $code => $description) { - $code = is_int($code) && $code < 100 ? $description : $code; - $cmd .= " /wd{$code}"; - } - } - - // C++ 选项 - $cmd .= ' /EHsc'; - if (!empty($config['cpp_std'])) { - $cmd .= ' /std:' . $config['cpp_std']; - } - - // 扩展模块选项 - if (!empty($config['build_mode']) && $config['build_mode'] === 'ext') { - // MSVC 不需要 -fPIC,默认就是位置无关代码 - } - - // CRT - $cmd .= ' /MD'; - - // nologo - $cmd .= ' /nologo'; - - // 性能分析宏 - if (!empty($config['enable_profiler'])) { - $cmd .= ' /DPPROF_ON=1'; - if (!empty($config['prof_output'])) { - $cmd .= ' /DPROF_OUTPUT_FILE=\'"' . $config['prof_output'] . '"\''; - } - } - - // 用户自定义编译标志 - if (!empty($config['cxxflags'])) { - $cmd .= ' ' . $config['cxxflags']; - } - - // 用户自定义预处理器宏 - if (!empty($config['user_defines'])) { - foreach ($config['user_defines'] as $define) { - $cmd .= ' /D' . $define; - } - } - - // LTO(全程序优化 /GL + 链接时代码生成 /LTCG) - if (!empty($config['lto'])) { - $cmd .= ' /GL'; - } - - return $cmd; + return $this->buildCommonCompileFlags($config, true); } /** diff --git a/src/Php/CompilerBase.php b/src/Php/CompilerBase.php index 36ca49de..cda52ff0 100644 --- a/src/Php/CompilerBase.php +++ b/src/Php/CompilerBase.php @@ -251,7 +251,7 @@ class CompilerBase extends \PhpAot\Core\Translator protected array $linkPaths = []; // --link-path / -L: user-specified library search paths protected int $floatPrecision = 17; protected bool $debug = false; - protected bool $formatCode = true; // --format: enable clang-format (disabled by default) + protected bool $formatCode = false; // --format: enable clang-format (disabled by default) protected bool $printBacktraceOnError = true; protected bool $noLiteralStrings = false; protected bool $noConsole = false; // Windows: hide console window diff --git a/src/Php/Translator.php b/src/Php/Translator.php index 7f3dae2b..98bc550a 100644 --- a/src/Php/Translator.php +++ b/src/Php/Translator.php @@ -206,11 +206,11 @@ class Translator extends Preprocessor $cmd = $argv[0]; $climate->bold('USAGE:'); - $climate->tab()->out($cmd . ' [options]'); + $climate->tab()->out($cmd . ' [options]'); $climate->br(); $climate->bold('ARGUMENTS:'); - $climate->tab()->out(' Input PHP file/directory/project.yml to compile'); + $climate->tab()->out(' Input PHP file/directory/YAML config to compile'); $climate->br(); $climate->bold('EXAMPLES:'); @@ -354,12 +354,7 @@ class Translator extends Preprocessor // clang-format 代码格式化(默认关闭,需显式 --format 开启) if ($this->climate->arguments->defined('format')) { - $clangFormatVersion = shell_exec('clang-format --version'); - if (!empty($clangFormatVersion)) { - $this->formatCode = true; - } else { - $this->climate->warning('--format requested but clang-format not found, skipping formatting'); - } + $this->enableCodeFormattingIfAvailable('--format'); } // 用户自定义链接库(直接从 argv 解析以支持多值) @@ -450,13 +445,24 @@ class Translator extends Preprocessor $this->climate->bold()->out(self::APP_NAME . ' v' . self::VERSION); } + private function enableCodeFormattingIfAvailable(string $source): void + { + $clangFormatVersion = shell_exec('clang-format --version'); + if (!empty($clangFormatVersion)) { + $this->formatCode = true; + return; + } + + $this->climate->warning($source . ' requested but clang-format not found, skipping formatting'); + } + protected function formatCppCode(string $file): void { if (!$this->formatCode) { return; } - $cmd = 'cd ' . $this->rootPath . ' && clang-format -i ' . $file; + $cmd = 'cd ' . escapeshellarg($this->rootPath) . ' && clang-format -i ' . escapeshellarg($file); $this->climate->info('format: ' . $this->getRelativePath($file)); $this->climate->comment($cmd); shell_exec($cmd); @@ -1513,7 +1519,14 @@ CODE; 'include_paths' => $this->getIncludePaths(), 'optimize' => 0, 'debug' => $this->debug, + 'sanitize' => $this->sanitize, 'is_zts' => $this->isPhpZts, + 'enable_profiler' => $this->enableProfiler, + 'prof_output' => $this->targetName . '.prof', + 'user_defines' => $this->userDefines, + 'lto' => $this->enableLto, + 'march' => $this->march, + 'target_platform' => $this->targetPlatform, 'suppressed_warnings' => ['4244', '4146'], ]; } @@ -1927,17 +1940,33 @@ CODE; } protected function getAbsolutePath(string $path, string $projectDir): string + { + $absPath = $this->resolvePath($path, $projectDir, 'Source path'); + return realpath($absPath); + } + + protected function resolvePath(string $path, string $baseDir, string $label = 'Path'): string { $path = trim($path); if ($path === '') { - $this->error('Source path must not be empty'); + $this->error($label . ' must not be empty'); } - if ($path[0] !== '/') { - $absPath = $projectDir . '/' . $path; - } else { - $absPath = $path; + + if ($this->isAbsolutePath($path)) { + return $path; } - return realpath($absPath); + + return $baseDir . '/' . $path; + } + + protected function isAbsolutePath(string $path): bool + { + return $path !== '' + && ( + $path[0] === '/' + || $path[0] === '\\' + || preg_match('/^[A-Za-z]:[\\\\\\/]/', $path) === 1 + ); } protected function parseProjectYaml(string $path): array @@ -1969,6 +1998,43 @@ CODE; $list = $this->getFilesFromDir($projectDir); } + if (array_key_exists('optimize', $cfg)) { + $this->optimizeLevel = (int) $cfg['optimize']; + } + + if (array_key_exists('job', $cfg)) { + $this->maxJob = (int) $cfg['job']; + } + + if (!empty($cfg['debug'])) { + $this->debug = true; + } + + if (!empty($cfg['no-literal-strings'])) { + $this->noLiteralStrings = true; + } + + if (!empty($cfg['profile'])) { + if (!$this->isLinux()) { + $this->climate->error('`profile` in YAML is only supported on Linux (requires gperftools)'); + exit(1); + } + $this->enableProfiler = true; + } + + if (!empty($cfg['no-progress'])) { + $this->noProgress = true; + } + + if (!empty($cfg['no-console'])) { + $this->noConsole = true; + } + + $sanitize = $cfg['sanitize'] ?? null; + if (!empty($sanitize)) { + $this->sanitize = (string) $sanitize; + } + // 读取 cxx-flags $cxxFlags = $cfg['cxx-flags'] ?? null; if (!empty($cxxFlags)) { @@ -1991,6 +2057,22 @@ CODE; $this->march = $march; } + // 读取 target-platform + $targetPlatform = $cfg['target-platform'] ?? null; + if (!empty($targetPlatform)) { + $this->targetPlatform = (string) $targetPlatform; + } + + // 读取 build-dir + $buildDir = $cfg['build-dir'] ?? null; + if (!empty($buildDir)) { + $this->setBuildDir($this->resolvePath((string) $buildDir, $projectDir, 'Build path')); + } + + if (!empty($cfg['dry'])) { + $this->dryRun = true; + } + // 读取 ld-flags $ldflags = $cfg['ld-flags'] ?? null; if (!empty($ldflags)) { @@ -2022,6 +2104,11 @@ CODE; $this->enableLto = true; } + // 读取 format + if (!empty($cfg['format'])) { + $this->enableCodeFormattingIfAvailable('YAML format'); + } + // 读取 link-libs $linkLibs = $cfg['link-libs'] ?? null; if (!empty($linkLibs) && is_array($linkLibs)) { @@ -2038,9 +2125,10 @@ CODE; } } - // 读取 name - if (!empty($cfg['name'])) { - $this->setTargetName($cfg['name']); + // 读取 output/name + $output = $cfg['output'] ?? $cfg['name'] ?? null; + if (!empty($output)) { + $this->setTargetName((string) $output); } // 读取 cpp-compiler @@ -2049,8 +2137,8 @@ CODE; $this->setCppCompiler($cppCompiler); } - // 读取 type/build-mode(支持中横线和下划线) - $buildMode = $cfg['build-mode'] ?? $cfg['type'] ?? null; + // 读取 mode/type/build-mode(支持 CLI/YAML 两套命名) + $buildMode = $cfg['mode'] ?? $cfg['build-mode'] ?? $cfg['type'] ?? null; if (!empty($buildMode)) { // 映射常见的类型名称到内部 buildMode $modeMap = [