- Add --entry option to specify PHP entry file for CLI SAPI execution
- Make --php-builder option optional with default empty configuration {}
- Update documentation to reflect new --entry and optional --php-builder options
- Add CompilerToolchain class to manage C/C++ compiler and archiver tools
- Implement response file support for long object lists in archiving operations
- Add progress tracking and detailed build stage reporting for PHP compilation
- Support MSVC, GCC and Clang compiler toolchains with proper companion tools
- Add test cases for new entry point and toolchain functionality
- Adjust build pipeline to exclude entry file from source compilation
- Improve build logging and error handling during compilation processes
master
parent
6e6e596661
commit
b893d865d1
15 changed files with 869 additions and 34 deletions
@ -0,0 +1,60 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/aot/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Tests\Build; |
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use TypePhp\Backend\Clang; |
||||||
|
use TypePhp\Backend\Gcc; |
||||||
|
use TypePhp\Backend\Msvc; |
||||||
|
use TypePhp\Build\CompilerToolchain; |
||||||
|
use TypePhp\Platform\Linux; |
||||||
|
use TypePhp\Platform\Macos; |
||||||
|
use TypePhp\Platform\Windows; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* @coversNothing |
||||||
|
*/ |
||||||
|
final class CompilerToolchainTest extends TestCase |
||||||
|
{ |
||||||
|
public function testDerivesCrossGccCompanionTools(): void |
||||||
|
{ |
||||||
|
$toolchain = CompilerToolchain::fromBackend(new Gcc( |
||||||
|
new Linux(), |
||||||
|
'/opt/cross/bin/aarch64-linux-gnu-g++', |
||||||
|
)); |
||||||
|
|
||||||
|
self::assertSame('/opt/cross/bin/aarch64-linux-gnu-gcc', $toolchain->cCompiler); |
||||||
|
self::assertSame('/opt/cross/bin/aarch64-linux-gnu-g++', $toolchain->cxxCompiler); |
||||||
|
self::assertSame('/opt/cross/bin/aarch64-linux-gnu-gcc-ar', $toolchain->archiver); |
||||||
|
self::assertSame(CompilerToolchain::ARCHIVER_UNIX, $toolchain->archiverStyle); |
||||||
|
} |
||||||
|
|
||||||
|
public function testDerivesVersionedClangCompanionTools(): void |
||||||
|
{ |
||||||
|
$toolchain = CompilerToolchain::fromBackend(new Clang( |
||||||
|
new Macos(), |
||||||
|
'/opt/llvm/bin/clang++-18', |
||||||
|
)); |
||||||
|
|
||||||
|
self::assertSame('/opt/llvm/bin/clang-18', $toolchain->cCompiler); |
||||||
|
self::assertSame('/opt/llvm/bin/clang++-18', $toolchain->cxxCompiler); |
||||||
|
self::assertSame('/opt/llvm/bin/llvm-ar-18', $toolchain->archiver); |
||||||
|
} |
||||||
|
|
||||||
|
public function testSelectsMsvcLibraryManager(): void |
||||||
|
{ |
||||||
|
$toolchain = CompilerToolchain::fromBackend(new Msvc(new Windows(), 'cl')); |
||||||
|
|
||||||
|
self::assertSame('cl', $toolchain->cCompiler); |
||||||
|
self::assertSame('cl', $toolchain->cxxCompiler); |
||||||
|
self::assertContains(strtolower(basename($toolchain->archiver)), ['lib', 'lib.exe']); |
||||||
|
self::assertSame(CompilerToolchain::ARCHIVER_MSVC, $toolchain->archiverStyle); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,119 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/aot/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Tests\Build; |
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use TypePhp\Build\CompilerToolchain; |
||||||
|
use TypePhp\Build\SapiPhpBuilder; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* @coversNothing |
||||||
|
*/ |
||||||
|
final class SapiPhpBuilderTest extends TestCase |
||||||
|
{ |
||||||
|
public function testExtractsUniqueMakeObjectTargetsForProgress(): void |
||||||
|
{ |
||||||
|
$builder = new SapiPhpBuilder(__DIR__, static function (string $message): void {}); |
||||||
|
$method = new \ReflectionMethod($builder, 'extractObjectTargets'); |
||||||
|
|
||||||
|
self::assertSame([ |
||||||
|
'ext/json/json.lo', |
||||||
|
'Zend/zend.lo', |
||||||
|
'main/main.lo', |
||||||
|
], $method->invoke($builder, implode("\n", [ |
||||||
|
'cc -c ext/json/json.c -o ext/json/json.lo -MMD', |
||||||
|
"cc -c Zend/zend.c -o 'Zend/zend.lo' -MMD", |
||||||
|
'cc -c main/main.c -o "main/main.lo" -MMD', |
||||||
|
'cc -c ext/json/json.c -o ext/json/json.lo -MMD', |
||||||
|
'cc objects.o -o sapi/cli/php', |
||||||
|
]))); |
||||||
|
} |
||||||
|
|
||||||
|
public function testAbbreviatesLongArchiverCommands(): void |
||||||
|
{ |
||||||
|
$builder = new SapiPhpBuilder(__DIR__, static function (string $message): void {}); |
||||||
|
$method = new \ReflectionMethod($builder, 'displayCommand'); |
||||||
|
|
||||||
|
self::assertSame( |
||||||
|
"'ar' 'rcs' '/tmp/libphp.a' '@<6 object files>'", |
||||||
|
$method->invoke($builder, [ |
||||||
|
'ar', |
||||||
|
'rcs', |
||||||
|
'/tmp/libphp.a', |
||||||
|
'one.o', |
||||||
|
'two.o', |
||||||
|
'three.o', |
||||||
|
'four.o', |
||||||
|
'five.o', |
||||||
|
'six.o', |
||||||
|
]), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public function testBuildsArchiveCommandForSelectedToolchain(): void |
||||||
|
{ |
||||||
|
$gcc = new SapiPhpBuilder( |
||||||
|
__DIR__, |
||||||
|
static function (string $message): void {}, |
||||||
|
toolchain: new CompilerToolchain( |
||||||
|
'/opt/gcc/bin/gcc', |
||||||
|
'/opt/gcc/bin/g++', |
||||||
|
'/opt/gcc/bin/gcc-ar', |
||||||
|
CompilerToolchain::ARCHIVER_UNIX, |
||||||
|
), |
||||||
|
); |
||||||
|
$method = new \ReflectionMethod($gcc, 'archiveCommand'); |
||||||
|
self::assertSame( |
||||||
|
['/opt/gcc/bin/gcc-ar', 'rcs', '/tmp/libphp.a', '@/tmp/objects.rsp'], |
||||||
|
$method->invoke($gcc, '/tmp/libphp.a', '@/tmp/objects.rsp'), |
||||||
|
); |
||||||
|
|
||||||
|
$msvc = new SapiPhpBuilder( |
||||||
|
__DIR__, |
||||||
|
static function (string $message): void {}, |
||||||
|
toolchain: new CompilerToolchain('cl', 'cl', 'lib.exe', CompilerToolchain::ARCHIVER_MSVC), |
||||||
|
); |
||||||
|
self::assertSame( |
||||||
|
['lib.exe', '/NOLOGO', '/OUT:C:\\tmp\\php.lib', '@C:\\tmp\\objects.rsp'], |
||||||
|
$method->invoke($msvc, 'C:\\tmp\\php.lib', '@C:\\tmp\\objects.rsp'), |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
public function testReservesOneProgressStepUntilMakeActuallyFinishes(): void |
||||||
|
{ |
||||||
|
$builder = new SapiPhpBuilder(__DIR__, static function (string $message): void {}); |
||||||
|
$method = new \ReflectionMethod($builder, 'liveProgressValue'); |
||||||
|
|
||||||
|
self::assertSame(630, $method->invoke($builder, 634, 634)); |
||||||
|
self::assertSame(512, $method->invoke($builder, 512, 634)); |
||||||
|
self::assertSame(0, $method->invoke($builder, 1, 1)); |
||||||
|
} |
||||||
|
|
||||||
|
public function testDetectsPostCompilationBuildStages(): void |
||||||
|
{ |
||||||
|
$builder = new SapiPhpBuilder(__DIR__, static function (string $message): void {}); |
||||||
|
$method = new \ReflectionMethod($builder, 'detectBuildStage'); |
||||||
|
|
||||||
|
self::assertSame( |
||||||
|
'Linking OPcache', |
||||||
|
$method->invoke($builder, 'libtool --mode=link gcc -shared -o ext/opcache/opcache.la objects.o'), |
||||||
|
); |
||||||
|
self::assertSame( |
||||||
|
'Linking PHP CLI', |
||||||
|
$method->invoke($builder, 'libtool --mode=link gcc objects.o -o sapi/cli/php'), |
||||||
|
); |
||||||
|
self::assertSame( |
||||||
|
'Linking PHP FPM', |
||||||
|
$method->invoke($builder, 'libtool --mode=link gcc objects.o -o sapi/fpm/php-fpm'), |
||||||
|
); |
||||||
|
self::assertSame('Generating phar.php', $method->invoke($builder, 'Generating phar.php')); |
||||||
|
self::assertSame('Generating phar.phar', $method->invoke($builder, 'Generating phar.phar')); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,161 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/aot/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Build; |
||||||
|
|
||||||
|
use TypePhp\Backend\Clang; |
||||||
|
use TypePhp\Backend\CompilerBackend; |
||||||
|
use TypePhp\Backend\Gcc; |
||||||
|
use TypePhp\Backend\Msvc; |
||||||
|
use TypePhp\Installer\PhpBuildConfiguration; |
||||||
|
|
||||||
|
final readonly class CompilerToolchain |
||||||
|
{ |
||||||
|
public const string ARCHIVER_UNIX = 'unix'; |
||||||
|
public const string ARCHIVER_MSVC = 'msvc'; |
||||||
|
|
||||||
|
public function __construct( |
||||||
|
public string $cCompiler, |
||||||
|
public string $cxxCompiler, |
||||||
|
public string $archiver, |
||||||
|
public string $archiverStyle, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
public static function fromBackend(CompilerBackend $backend): self |
||||||
|
{ |
||||||
|
$cxx = $backend->getCompilerCommand(); |
||||||
|
[$words, $compilerIndex] = self::compilerWords($cxx); |
||||||
|
$compiler = $words[$compilerIndex]; |
||||||
|
|
||||||
|
if ($backend instanceof Gcc) { |
||||||
|
$cCompiler = self::replaceCompilerWord($words, $compilerIndex, self::gccCCompiler($compiler)); |
||||||
|
$archiver = self::resolveCompanion($compiler, self::gccArchiverNames($compiler)); |
||||||
|
return new self($cCompiler, $cxx, $archiver, self::ARCHIVER_UNIX); |
||||||
|
} |
||||||
|
if ($backend instanceof Clang) { |
||||||
|
$cCompiler = self::replaceCompilerWord($words, $compilerIndex, self::clangCCompiler($compiler)); |
||||||
|
$archiver = self::resolveCompanion($compiler, self::clangArchiverNames($compiler)); |
||||||
|
return new self($cCompiler, $cxx, $archiver, self::ARCHIVER_UNIX); |
||||||
|
} |
||||||
|
if ($backend instanceof Msvc) { |
||||||
|
$archiver = self::resolveCompanion($compiler, ['lib.exe', 'lib']); |
||||||
|
return new self($cxx, $cxx, $archiver, self::ARCHIVER_MSVC); |
||||||
|
} |
||||||
|
throw new \RuntimeException('Unsupported PHP builder compiler toolchain: ' . $backend->getName()); |
||||||
|
} |
||||||
|
|
||||||
|
/** @return array{list<string>, int} */ |
||||||
|
private static function compilerWords(string $command): array |
||||||
|
{ |
||||||
|
$words = PhpBuildConfiguration::parseShellWords($command); |
||||||
|
if ($words === []) { |
||||||
|
throw new \RuntimeException('The compiler command is empty'); |
||||||
|
} |
||||||
|
for ($index = count($words) - 1; $index >= 0; --$index) { |
||||||
|
$name = strtolower(preg_replace('/\.exe$/i', '', basename(str_replace('\\', '/', $words[$index]))) ?? ''); |
||||||
|
if (preg_match('/(?:^|-)clang(?:\+\+|-cl)?(?:-\d+)?$|(?:^|-)(?:g\+\+|gcc|c\+\+|cl)(?:-\d+)?$/', $name) === 1) { |
||||||
|
return [$words, $index]; |
||||||
|
} |
||||||
|
} |
||||||
|
return [$words, 0]; |
||||||
|
} |
||||||
|
|
||||||
|
/** @param list<string> $words */ |
||||||
|
private static function replaceCompilerWord(array $words, int $index, string $compiler): string |
||||||
|
{ |
||||||
|
$words[$index] = $compiler; |
||||||
|
if (count($words) === 1) { |
||||||
|
return $compiler; |
||||||
|
} |
||||||
|
return implode(' ', array_map('escapeshellarg', $words)); |
||||||
|
} |
||||||
|
|
||||||
|
private static function gccCCompiler(string $compiler): string |
||||||
|
{ |
||||||
|
return self::replaceBasename($compiler, static function (string $name): string { |
||||||
|
$name = preg_replace('/g\+\+/', 'gcc', $name, 1, $count) ?? $name; |
||||||
|
if ($count === 0) { |
||||||
|
$name = preg_replace('/c\+\+/', 'cc', $name, 1) ?? $name; |
||||||
|
} |
||||||
|
return $name; |
||||||
|
}); |
||||||
|
} |
||||||
|
|
||||||
|
private static function clangCCompiler(string $compiler): string |
||||||
|
{ |
||||||
|
return self::replaceBasename( |
||||||
|
$compiler, |
||||||
|
static fn (string $name): string => preg_replace('/clang\+\+/', 'clang', $name, 1) ?? $name, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
/** @return list<string> */ |
||||||
|
private static function gccArchiverNames(string $compiler): array |
||||||
|
{ |
||||||
|
$name = basename(str_replace('\\', '/', $compiler)); |
||||||
|
$gccAr = preg_replace('/g\+\+|gcc|c\+\+/', 'gcc-ar', $name, 1, $count) ?? $name; |
||||||
|
if ($count === 0) { |
||||||
|
$gccAr = 'gcc-ar'; |
||||||
|
} |
||||||
|
$plainAr = preg_replace('/g\+\+|gcc|c\+\+/', 'ar', $name, 1, $plainCount) ?? $name; |
||||||
|
if ($plainCount === 0) { |
||||||
|
$plainAr = 'ar'; |
||||||
|
} |
||||||
|
return array_values(array_unique([$gccAr, 'gcc-ar', $plainAr, 'ar'])); |
||||||
|
} |
||||||
|
|
||||||
|
/** @return list<string> */ |
||||||
|
private static function clangArchiverNames(string $compiler): array |
||||||
|
{ |
||||||
|
$name = strtolower(preg_replace('/\.exe$/i', '', basename(str_replace('\\', '/', $compiler))) ?? ''); |
||||||
|
$suffix = preg_match('/clang(?:\+\+)?-(\d+)$/', $name, $match) === 1 ? '-' . $match[1] : ''; |
||||||
|
$names = ['llvm-ar' . $suffix, 'llvm-ar']; |
||||||
|
if (PHP_OS_FAMILY === 'Darwin') { |
||||||
|
// Xcode ships the LLVM archiver as /usr/bin/ar on some releases. |
||||||
|
$names[] = 'ar'; |
||||||
|
} |
||||||
|
return array_values(array_unique($names)); |
||||||
|
} |
||||||
|
|
||||||
|
/** @param list<string> $names */ |
||||||
|
private static function resolveCompanion(string $compiler, array $names): string |
||||||
|
{ |
||||||
|
$normalized = str_replace('\\', '/', $compiler); |
||||||
|
$hasDirectory = str_contains($normalized, '/'); |
||||||
|
if ($hasDirectory) { |
||||||
|
$directory = dirname($normalized); |
||||||
|
foreach ($names as $name) { |
||||||
|
$resolved = ExecutableLocator::resolve($directory . DIRECTORY_SEPARATOR . $name); |
||||||
|
if ($resolved !== null) { |
||||||
|
return $resolved; |
||||||
|
} |
||||||
|
} |
||||||
|
// An explicitly selected compiler directory defines the toolchain |
||||||
|
// boundary. Do not silently take an unrelated archiver from PATH. |
||||||
|
return $directory . DIRECTORY_SEPARATOR . $names[0]; |
||||||
|
} |
||||||
|
foreach ($names as $name) { |
||||||
|
$resolved = ExecutableLocator::resolve($name); |
||||||
|
if ($resolved !== null) { |
||||||
|
return $resolved; |
||||||
|
} |
||||||
|
} |
||||||
|
return $names[0]; |
||||||
|
} |
||||||
|
|
||||||
|
private static function replaceBasename(string $path, callable $replace): string |
||||||
|
{ |
||||||
|
$normalized = str_replace('\\', '/', $path); |
||||||
|
$name = $replace(basename($normalized)); |
||||||
|
if (!str_contains($normalized, '/')) { |
||||||
|
return $name; |
||||||
|
} |
||||||
|
return dirname($normalized) . DIRECTORY_SEPARATOR . $name; |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue