- 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