feat(compiler): add compiler command validation and path resolution

- Implemented isCommandExecutable method to check if compiler commands are executable
- Added getCommandProgram method to extract program name from quoted/argumented commands
- Created isPathLikeCommand helper for path-based command detection
- Added compiler toolchain validation in Translator prepare phase
- Implemented proper PATH environment lookup with extension handling on Windows
- Added comprehensive unit tests for command parsing and executability checks
- Enhanced error reporting when compiler/linker executables are not found
- Moved file collection before compiler validation in translation process
pull/15/head
韩天峰 2 months ago
parent be4835991e
commit 75bf9f4113
  1. 66
      phpunit/src/FactoryTest.php
  2. 70
      src/Php/Backend/CompilerFactory.php
  3. 28
      src/Php/Translator.php

@ -11,6 +11,48 @@ use PhpAot\Php\Platform\Macos;
class FactoryTest extends TestCase class FactoryTest extends TestCase
{ {
private string|false $originalPath;
private string $tmpDir;
protected function setUp(): void
{
parent::setUp();
$this->originalPath = getenv('PATH');
$this->tmpDir = sys_get_temp_dir() . '/compiler_factory_test_' . uniqid();
mkdir($this->tmpDir, 0777, true);
}
protected function tearDown(): void
{
parent::tearDown();
if ($this->originalPath === false) {
putenv('PATH');
} else {
putenv('PATH=' . $this->originalPath);
}
$this->removeDirectory($this->tmpDir);
}
private function removeDirectory(string $dir): void
{
if (!is_dir($dir)) {
return;
}
foreach (array_diff(scandir($dir), ['.', '..']) as $file) {
$path = $dir . DIRECTORY_SEPARATOR . $file;
is_dir($path) ? $this->removeDirectory($path) : unlink($path);
}
rmdir($dir);
}
private function createFakeExecutable(string $name): string
{
$path = $this->tmpDir . DIRECTORY_SEPARATOR . $name;
file_put_contents($path, "#!/bin/sh\nexit 0\n");
chmod($path, 0755);
return $path;
}
/** /**
* 测试 PlatformFactory 自动检测 * 测试 PlatformFactory 自动检测
*/ */
@ -172,4 +214,28 @@ class FactoryTest extends TestCase
$this->assertSame($platform, $retrievedPlatform); $this->assertSame($platform, $retrievedPlatform);
} }
public function testCompilerCommandProgramParsesArgumentsAndQuotes(): void
{
$this->assertSame('clang++', CompilerFactory::getCommandProgram('clang++ -stdlib=libc++'));
$this->assertSame('/opt/llvm/bin/clang++', CompilerFactory::getCommandProgram('"/opt/llvm/bin/clang++" -O2'));
$this->assertSame('C:\\LLVM\\bin\\clang++.exe', CompilerFactory::getCommandProgram('"C:\\LLVM\\bin\\clang++.exe" -O2'));
$this->assertSame('', CompilerFactory::getCommandProgram(' '));
}
public function testCompilerCommandExecutableUsesPathAndIgnoresArguments(): void
{
$this->createFakeExecutable('fake-g++');
putenv('PATH=' . $this->tmpDir);
$this->assertTrue(CompilerFactory::isCommandExecutable('fake-g++ -std=c++20'));
$this->assertFalse(CompilerFactory::isCommandExecutable('missing-g++ -std=c++20'));
}
public function testCompilerCommandExecutableAcceptsQuotedAbsolutePath(): void
{
$compiler = $this->createFakeExecutable('quoted-clang++');
$this->assertTrue(CompilerFactory::isCommandExecutable('"' . $compiler . '" -O2'));
}
} }

@ -110,10 +110,69 @@ class CompilerFactory
]; ];
} }
public static function isCommandExecutable(string $command): bool
{
$program = self::getCommandProgram($command);
if ($program === '') {
return false;
}
if (self::isPathLikeCommand($program)) {
return is_file($program) && is_executable($program);
}
$path = getenv('PATH');
if ($path === false || $path === '') {
return false;
}
$extensions = [''];
if (DIRECTORY_SEPARATOR === '\\') {
$pathext = getenv('PATHEXT') ?: '.COM;.EXE;.BAT;.CMD';
$extensions = array_filter(array_map('strtolower', explode(';', $pathext)));
if (preg_match('/\.[A-Za-z0-9]+$/', $program)) {
array_unshift($extensions, '');
}
}
foreach (explode(PATH_SEPARATOR, $path) as $dir) {
if ($dir === '') {
continue;
}
foreach ($extensions as $extension) {
$candidate = rtrim($dir, DIRECTORY_SEPARATOR . '/\\') . DIRECTORY_SEPARATOR . $program . $extension;
if (is_file($candidate) && is_executable($candidate)) {
return true;
}
}
}
return false;
}
public static function getCommandProgram(string $command): string
{
$command = trim($command);
if ($command === '') {
return '';
}
if ($command[0] === '"' || $command[0] === "'") {
$quote = $command[0];
$end = strpos($command, $quote, 1);
if ($end !== false) {
return substr($command, 1, $end - 1);
}
}
$firstToken = strtok($command, " \t\r\n");
return $firstToken === false ? '' : $firstToken;
}
private static function normalizeCompilerName(string $compilerName): string private static function normalizeCompilerName(string $compilerName): string
{ {
$firstToken = strtok(trim($compilerName), ' '); $firstToken = self::getCommandProgram($compilerName);
if ($firstToken === false || $firstToken === '') { if ($firstToken === '') {
return ''; return '';
} }
@ -122,4 +181,11 @@ class CompilerFactory
return preg_replace('/\.exe$/', '', $name); return preg_replace('/\.exe$/', '', $name);
} }
private static function isPathLikeCommand(string $program): bool
{
return str_contains($program, '/')
|| str_contains($program, '\\')
|| preg_match('/^[A-Za-z]:[\/\\\\]/', $program) === 1;
}
} }

@ -636,6 +636,9 @@ class Translator extends Preprocessor
public function prepare(string $path): array public function prepare(string $path): array
{ {
$files = $this->getFiles($path);
$this->validateCompilerToolchain();
// shell_exec 和 define 已通过 php::fn:: 直接调用,无需动态符号表 // shell_exec 和 define 已通过 php::fn:: 直接调用,无需动态符号表
// 根据平台检查库文件(仅在构建二进制文件时需要) // 根据平台检查库文件(仅在构建二进制文件时需要)
@ -648,7 +651,6 @@ class Translator extends Preprocessor
} }
} }
$files = $this->getFiles($path);
$files = $this->filterIgnoredFiles($files); $files = $this->filterIgnoredFiles($files);
// 分析 PHP 文件,预处理 // 分析 PHP 文件,预处理
foreach ($files as $k => $file) { foreach ($files as $k => $file) {
@ -668,6 +670,30 @@ class Translator extends Preprocessor
return $files; return $files;
} }
protected function validateCompilerToolchain(): void
{
$backend = $this->getCompilerBackend();
$compilerCommand = $backend->getCompilerCommand();
if (!CompilerFactory::isCommandExecutable($compilerCommand)) {
$program = CompilerFactory::getCommandProgram($compilerCommand);
$this->error(
"C/C++ compiler executable not found: {$program}\n" .
"Configured compiler command: {$compilerCommand}\n" .
"Install a supported compiler or set `cpp-compiler` in project.yml / PHPX_CC / CXX."
);
}
$linkerCommand = $backend->getLinkerCommand();
if ($linkerCommand !== $compilerCommand && !CompilerFactory::isCommandExecutable($linkerCommand)) {
$program = CompilerFactory::getCommandProgram($linkerCommand);
$this->error(
"Linker executable not found: {$program}\n" .
"Configured linker command: {$linkerCommand}\n" .
"Install the required linker or update compiler configuration."
);
}
}
protected function shouldIgnoreFile(string $file): bool protected function shouldIgnoreFile(string $file): bool
{ {
foreach ($this->ignorePaths as $ignorePath) { foreach ($this->ignorePaths as $ignorePath) {

Loading…
Cancel
Save