refactor(build): replace pcntl parallel compilation with process pool

- Removed pcntl dependency and related functions from PlatformBase and Windows classes
- Implemented proc_open-based process pool in NativeBuilder for cross-platform parallel compilation
- Updated dispatchParallel to dispatchProcessParallel with new process management logic
- Modified Translator to use process pool instead of pcntl for parallel compilation
- Added fallback to sequential compilation when proc_open is unavailable
- Updated ParallelCompileTest to use process-based tests instead of pcntl mocking
- Bumped version from 0.8.1 to 0.8.2 in project configuration files
master
韩天峰 4 weeks ago
parent a1efda0f7f
commit 1b79fcd2b4
  1. 3
      composer.json
  2. 174
      phpunit/src/ParallelCompileTest.php
  3. 6
      project.yml
  4. 181
      src/Build/NativeBuilder.php
  5. 8
      src/Platform/PlatformBase.php
  6. 5
      src/Platform/Windows.php
  7. 228
      src/Translator.php

@ -41,8 +41,5 @@
"cs-fix": "php-cs-fixer fix $1", "cs-fix": "php-cs-fixer fix $1",
"analyse": "phpstan analyse --memory-limit 1G -c phpstan.neon", "analyse": "phpstan analyse --memory-limit 1G -c phpstan.neon",
"rector": "rector process --clear-cache" "rector": "rector process --clear-cache"
},
"suggest": {
"ext-pcntl": "Required for process control features."
} }
} }

@ -5,137 +5,109 @@ namespace TypePhp\Tests;
use PHPUnit\Framework\TestCase; use PHPUnit\Framework\TestCase;
use TypePhp\Backend\CompilerBackend; use TypePhp\Backend\CompilerBackend;
use TypePhp\Build\NativeBuilder; use TypePhp\Build\NativeBuilder;
use TypePhp\CompilerTest;
class ParallelCompileTest extends TestCase final class ParallelCompileTest extends TestCase
{ {
public function testWaitRetriesWhenInterrupted(): void private string $directory;
{
$compiler = new ScriptedWaitCompiler([
[-1, 0, PCNTL_EINTR],
[123, 0, 0],
]);
$this->assertSame([123, 0], $compiler->waitForTest());
$this->assertSame(2, $compiler->getWaitCallCount());
}
public function testWaitFailureOtherThanInterruptionThrows(): void
{
$compiler = new ScriptedWaitCompiler([
[-1, 0, PCNTL_ECHILD],
]);
$this->expectException(\RuntimeException::class);
$this->expectExceptionMessage('Failed to wait for compiler process');
$compiler->waitForTest();
}
public function testSignaledChildIsNotSuccessful(): void protected function setUp(): void
{ {
$compiler = new ScriptedWaitCompiler([]); $this->directory = sys_get_temp_dir() . '/typephp_process_pool_' . bin2hex(random_bytes(6));
mkdir($this->directory, 0777, true);
$this->assertTrue($compiler->statusSucceeded(0));
$this->assertFalse($compiler->statusSucceeded(1 << 8));
$this->assertFalse($compiler->statusSucceeded(SIGTERM));
} }
public function testForkFailureStillReapsRunningCompilerProcesses(): void protected function tearDown(): void
{ {
$compiler = new ScriptedWaitCompiler( foreach (glob($this->directory . '/*') ?: [] as $file) {
[[101, 0, 0]], @unlink($file);
[101, -1]
);
try {
$compiler->compileInParallelForTest(['first.cc', 'second.cc', 'third.cc'], 2);
$this->fail('The fork failure should fail the parallel compilation');
} catch (\Exception $e) {
$this->assertStringContainsString('second.cc', $e->getMessage());
$this->assertStringContainsString('third.cc', $e->getMessage());
} }
@rmdir($this->directory);
$this->assertSame(1, $compiler->getWaitCallCount());
} }
public function testParallelDispatcherReportsEachCompletedTask(): void public function testProcessPoolRunsCompilerTasksConcurrently(): void
{ {
$builder = new NativeBuilder($this->createMock(CompilerBackend::class)); $firstMarker = $this->directory . '/first.ready';
$forkResults = [101, 102]; $secondMarker = $this->directory . '/second.ready';
$waitResults = [[102, 0], [101, 1 << 8]]; $firstObject = $this->directory . '/first.o';
$secondObject = $this->directory . '/second.o';
$completed = []; $completed = [];
$result = $builder->dispatchParallel( $builder = new NativeBuilder($this->createMock(CompilerBackend::class));
['first.cc', 'second.cc'], $result = $builder->dispatchProcessParallel(
[
$this->task('first.cc', $firstObject, $this->barrierCommand($firstMarker, $secondMarker, $firstObject)),
$this->task('second.cc', $secondObject, $this->barrierCommand($secondMarker, $firstMarker, $secondObject)),
],
2, 2,
static fn(string $source): string => $source . '.o', static function (
static function (): void {}, string $source,
static function () use (&$forkResults): int { string $object,
return array_shift($forkResults); string $command,
}, array $output,
static function () use (&$waitResults): array { int $status,
return array_shift($waitResults); bool $success,
}, int $count,
static fn(int $status): bool => $status === 0, ) use (&$completed): void {
static function (string $source, string $object, int $status, bool $success, int $count) use (&$completed): void { $completed[$source] = [$object, $command, $output, $status, $success, $count];
$completed[] = [$source, $object, $status, $success, $count];
}, },
); );
$this->assertSame(['second.cc.o'], $result['objects']); self::assertSame([], $result['failures'], var_export($completed, true));
$this->assertSame(['first.cc'], $result['failures']); self::assertEqualsCanonicalizing([$firstObject, $secondObject], $result['objects']);
$this->assertSame([ self::assertCount(2, $completed);
['second.cc', 'second.cc.o', 0, true, 1], self::assertTrue($completed['first.cc'][4]);
['first.cc', 'first.cc.o', 1 << 8, false, 2], self::assertTrue($completed['second.cc'][4]);
], $completed);
} }
}
class ScriptedWaitCompiler extends CompilerTest public function testProcessPoolCapturesCompilerFailureOutputAndStatus(): void
{
private int $waitCallCount = 0;
private int $lastWaitError = 0;
public function __construct(private array $waitResults, private array $forkResults = [])
{
parent::__construct(TYPEPHP_ROOT_PATH);
$this->noProgress = true;
}
protected function pcntlFork(): int
{ {
return array_shift($this->forkResults); $object = $this->directory . '/failed.o';
} $completion = null;
$command = $this->phpCommand("fwrite(STDERR, 'compiler-error'); exit(7);");
protected function pcntlWait(?int &$status): int $builder = new NativeBuilder($this->createMock(CompilerBackend::class));
{ $result = $builder->dispatchProcessParallel(
$this->waitCallCount++; [$this->task('failed.cc', $object, $command)],
[$pid, $status, $this->lastWaitError] = array_shift($this->waitResults); 4,
return $pid; static function (
} string $source,
string $object,
protected function pcntlLastError(): int string $command,
{ array $output,
return $this->lastWaitError; int $status,
} bool $success,
int $count,
) use (&$completion): void {
$completion = compact('source', 'object', 'command', 'output', 'status', 'success', 'count');
},
);
public function waitForTest(): array self::assertSame([], $result['objects']);
{ self::assertSame(['failed.cc'], $result['failures']);
return $this->waitForCompileChild(); self::assertSame(7, $completion['status']);
self::assertFalse($completion['success']);
self::assertStringContainsString('compiler-error', implode("\n", $completion['output']));
} }
public function statusSucceeded(int $status): bool /** @return array{source: string, object: string, command: string} */
private function task(string $source, string $object, string $command): array
{ {
return $this->compileChildSucceeded($status); return compact('source', 'object', 'command');
} }
public function getWaitCallCount(): int private function barrierCommand(string $ownMarker, string $otherMarker, string $object): string
{ {
return $this->waitCallCount; $code = 'file_put_contents(' . var_export($ownMarker, true) . ", 'ready');"
. '$deadline = microtime(true) + 5;'
. 'while (file_exists(' . var_export($otherMarker, true) . ') === false && microtime(true) < $deadline) {'
. ' usleep(10000); }'
. 'if (file_exists(' . var_export($otherMarker, true) . ') === false) { exit(9); }'
. 'file_put_contents(' . var_export($object, true) . ", 'object');";
return $this->phpCommand($code);
} }
public function compileInParallelForTest(array $sourceFiles, int $jobs): array private function phpCommand(string $code): string
{ {
return $this->compileWithPcntl($sourceFiles, $jobs); return escapeshellarg(PHP_BINARY) . ' -n -r ' . escapeshellarg($code);
} }
} }

@ -1,6 +1,6 @@
name: tpc name: tpc
build-mode: bin build-mode: bin
version: 0.8.1 version: 0.8.2
cxx-std: c++17 cxx-std: c++17
cxx-flags: cxx-flags:
- -Wall - -Wall
@ -12,8 +12,8 @@ resource:
icon: swoole-logo.ico icon: swoole-logo.ico
# 版本信息 # 版本信息
version-info: version-info:
file-version: 0.8.1.1112 file-version: 0.8.2.1112
product-version: 0.8.1 product-version: 0.8.2
company-name: "上海识沃网络科技有限公司" company-name: "上海识沃网络科技有限公司"
file-description: "TypePHP Compiler" file-description: "TypePHP Compiler"
internal-name: "typephp" internal-name: "typephp"

@ -58,86 +58,141 @@ final readonly class NativeBuilder
} }
/** /**
* @param Closure(string): string $objectFile * Run compiler commands concurrently through proc_open(). Keeping the
* @param Closure(string, string): void $worker * process pool independent of pcntl makes parallel builds available in a
* @param Closure(): int $fork * stock PHP installation on Linux, macOS, and Windows.
* @param Closure(): array{int, int} $wait *
* @param Closure(int): bool $succeeded * Output is redirected to one temporary file per process. This avoids the
* @param null|Closure(string, string, int, bool, int): void $completed * pipe-buffer deadlocks that can otherwise occur when a compiler emits a
* large diagnostic while the parent is waiting for another process.
*
* @param list<array{source: string, object: string, command: string}> $tasks
* @param null|Closure(string, string, string, list<string>, int, bool, int): void $completed
* @return array{objects: list<string>, failures: list<string>} * @return array{objects: list<string>, failures: list<string>}
*/ */
public function dispatchParallel( public function dispatchProcessParallel(
array $sources, array $tasks,
int $jobs, int $jobs,
Closure $objectFile,
Closure $worker,
Closure $fork,
Closure $wait,
Closure $succeeded,
?Closure $completed = null, ?Closure $completed = null,
): array { ): array {
// Keep most workers on the largest translation units to reduce the $jobs = max(1, $jobs);
// parallel tail, but reserve one fast lane for small files so progress $queue = $tasks;
// remains visible while the expensive units are still compiling.
$queue = SourceCompileQueue::largestFirst($sources);
$running = []; $running = [];
$objects = []; $objects = [];
$failures = []; $failures = [];
$completedCount = 0; $completedCount = 0;
$largeTaskCount = 0; $nullDevice = PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null';
$largeLaneLimit = max(1, $jobs - 1);
try {
while ($queue !== [] || $running !== []) { while ($queue !== [] || $running !== []) {
while (count($running) < $jobs && $queue !== []) { while (count($running) < $jobs && $queue !== []) {
if ($largeTaskCount < $largeLaneLimit) { $task = array_shift($queue);
$source = array_shift($queue); $logFile = tempnam(sys_get_temp_dir(), 'typephp-compile-');
$lane = 'large'; if ($logFile === false) {
} else { $failures[] = $task['source'];
$source = array_pop($queue); $completedCount++;
$lane = 'small'; $completed?->__invoke(
$task['source'],
$task['object'],
$task['command'],
['Unable to create compiler output file'],
1,
false,
$completedCount,
);
continue;
}
$process = @proc_open(
$task['command'],
[
0 => ['file', $nullDevice, 'r'],
1 => ['file', $logFile, 'a'],
2 => ['file', $logFile, 'a'],
],
$pipes,
);
if (!is_resource($process)) {
@unlink($logFile);
$failures[] = $task['source'];
$completedCount++;
$completed?->__invoke(
$task['source'],
$task['object'],
$task['command'],
['Unable to start compiler process'],
1,
false,
$completedCount,
);
continue;
}
$running[] = [
'task' => $task,
'process' => $process,
'log' => $logFile,
];
} }
$object = $objectFile($source);
$pid = $fork(); if ($running === []) {
if ($pid === -1) { continue;
$failures[] = $source;
array_push($failures, ...$queue);
$queue = [];
break;
} }
if ($pid === 0) {
try { $finished = false;
$worker($source, $object); foreach ($running as $index => $entry) {
exit(is_file($object) ? 0 : 1); $status = proc_get_status($entry['process']);
} catch (\Throwable) { if ($status['running']) {
exit(1); continue;
}
$finished = true;
$exitCode = (int) $status['exitcode'];
$closeCode = proc_close($entry['process']);
if ($exitCode < 0 && $closeCode >= 0) {
$exitCode = $closeCode;
} }
$contents = file_get_contents($entry['log']);
@unlink($entry['log']);
$output = $contents === false || $contents === ''
? []
: (preg_split('/\R/', rtrim($contents)) ?: []);
$task = $entry['task'];
$success = $exitCode === 0 && is_file($task['object']);
if ($success) {
$objects[] = $task['object'];
} else {
$failures[] = $task['source'];
}
$completedCount++;
unset($running[$index]);
$completed?->__invoke(
$task['source'],
$task['object'],
$task['command'],
$output,
$exitCode,
$success,
$completedCount,
);
} }
$running[$pid] = ['source' => $source, 'object' => $object, 'lane' => $lane]; $running = array_values($running);
if ($lane === 'large') {
$largeTaskCount++; if (!$finished) {
usleep(10_000);
} }
} }
if ($running === []) { } finally {
break; foreach ($running as $entry) {
} if (is_resource($entry['process'])) {
[$pid, $status] = $wait(); proc_terminate($entry['process']);
$task = $running[$pid] ?? null; proc_close($entry['process']);
unset($running[$pid]); }
if ($task === null) { @unlink($entry['log']);
continue;
}
if ($task['lane'] === 'large') {
$largeTaskCount--;
}
$success = $succeeded($status);
if ($success) {
$objects[] = $task['object'];
} else {
$failures[] = $task['source'];
} }
$completedCount++;
$completed?->__invoke($task['source'], $task['object'], $status, $success, $completedCount);
} }
return ['objects' => $objects, 'failures' => $failures]; return ['objects' => $objects, 'failures' => $failures];
} }

@ -140,14 +140,6 @@ abstract class PlatformBase
return $warnings; return $warnings;
} }
/**
* Whether this platform is suitable for parallel compilation using pcntl_fork.
*/
public function supportsPcntlParallelCompile(): bool
{
return true;
}
public function getIntegerLiteralSuffix(): string public function getIntegerLiteralSuffix(): string
{ {
return 'LL'; return 'LL';

@ -226,11 +226,6 @@ class Windows extends PlatformBase
return $warnings; return $warnings;
} }
public function supportsPcntlParallelCompile(): bool
{
return false;
}
/** /**
* Get the debug options. * Get the debug options.
*/ */

@ -24,6 +24,7 @@ use TypePhp\Build\NativeDependencyAuditor;
use TypePhp\Build\NanoSourceComposer; use TypePhp\Build\NanoSourceComposer;
use TypePhp\Build\PrecompiledHeaderManager; use TypePhp\Build\PrecompiledHeaderManager;
use TypePhp\Build\SourcePipelineTrait; use TypePhp\Build\SourcePipelineTrait;
use TypePhp\Build\SourceCompileQueue;
use TypePhp\Build\WasmInterfaceGenerator; use TypePhp\Build\WasmInterfaceGenerator;
use TypePhp\Config\ProjectYamlLoader; use TypePhp\Config\ProjectYamlLoader;
use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic; use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic;
@ -82,7 +83,7 @@ class Translator extends Preprocessor
use ResourceCompilationTrait; use ResourceCompilationTrait;
use ClassConstantValueTrait; use ClassConstantValueTrait;
public const string VERSION = '0.8.1'; public const string VERSION = '0.8.2';
public const string APP_NAME = 'TypePHP Compiler (AOT)'; public const string APP_NAME = 'TypePHP Compiler (AOT)';
protected bool $hasExplicitOutput = false; protected bool $hasExplicitOutput = false;
@ -1903,6 +1904,46 @@ CODE;
public function compileFile(string $cppFile, string $objectFile, bool $parallel = false): void public function compileFile(string $cppFile, string $objectFile, bool $parallel = false): void
{ {
$task = $this->prepareCompileFileTask($cppFile, $objectFile, $parallel);
if ($task === null) {
return;
}
$result = $this->getNativeBuilder()->compile(
$cppFile,
$objectFile,
$task['options'],
$task['language'],
$parallel,
);
if (!$parallel) {
$this->climate->comment($result['command']);
}
if ($result['status'] !== 0) {
if ($parallel && !empty($result['output'])) {
foreach ($result['output'] as $line) {
$this->climate->red($line);
}
}
$this->error('compile failed: ' . $cppFile);
}
$this->finalizeCompileFileTask($cppFile, $objectFile, $task);
}
/**
* @return null|array{
* language: ?string,
* options: CompileOptions,
* cacheable_misc: bool,
* nano_runtime: bool
* }
*/
private function prepareCompileFileTask(
string $cppFile,
string $objectFile,
bool $parallel,
): ?array {
$isCacheableMiscFile = $this->isPhpxMiscFile($cppFile) $isCacheableMiscFile = $this->isPhpxMiscFile($cppFile)
&& !$this->isProjectRuntimeEntryFile($cppFile); && !$this->isProjectRuntimeEntryFile($cppFile);
$isNanoRuntimeSource = isset($this->nanoRuntimeSources[$cppFile]); $isNanoRuntimeSource = isset($this->nanoRuntimeSources[$cppFile]);
@ -1910,10 +1951,10 @@ CODE;
if (!$parallel) { if (!$parallel) {
$this->climate->darkGray('[cache] skip: ' . $cppFile); $this->climate->darkGray('[cache] skip: ' . $cppFile);
} }
return; return null;
} }
if ($isNanoRuntimeSource && $this->hasNanoObjectFileCache($cppFile, $objectFile)) { if ($isNanoRuntimeSource && $this->hasNanoObjectFileCache($cppFile, $objectFile)) {
return; return null;
} }
if ($isCacheableMiscFile) { if ($isCacheableMiscFile) {
@ -1921,24 +1962,21 @@ CODE;
} }
$language = $this->getLanguageFromExtension($cppFile); $language = $this->getLanguageFromExtension($cppFile);
$options = $this->getSourceCompileCommandOptions($cppFile, $language); return [
$result = $this->getNativeBuilder()->compile($cppFile, $objectFile, $options, $language, $parallel); 'language' => $language,
if (!$parallel) { 'options' => $this->getSourceCompileCommandOptions($cppFile, $language),
$this->climate->comment($result['command']); 'cacheable_misc' => $isCacheableMiscFile,
} 'nano_runtime' => $isNanoRuntimeSource,
if ($result['status'] !== 0) { ];
if ($parallel && !empty($result['output'])) { }
foreach ($result['output'] as $line) {
$this->climate->red($line);
}
}
$this->error('compile failed: ' . $cppFile);
}
if ($isCacheableMiscFile) { /** @param array{cacheable_misc: bool, nano_runtime: bool} $task */
private function finalizeCompileFileTask(string $cppFile, string $objectFile, array $task): void
{
if ($task['cacheable_misc']) {
$this->writeMiscObjectCacheMetadata($cppFile, $objectFile); $this->writeMiscObjectCacheMetadata($cppFile, $objectFile);
} }
if ($isNanoRuntimeSource) { if ($task['nano_runtime']) {
$this->writeMiscObjectCacheMetadata($cppFile, $objectFile); $this->writeMiscObjectCacheMetadata($cppFile, $objectFile);
} }
} }
@ -2015,12 +2053,18 @@ CODE;
// Windows: compile the resource file (icon, version info, etc.) // Windows: compile the resource file (icon, version info, etc.)
$this->compileResourceFile(); $this->compileResourceFile();
if (!$this->getPlatform()->supportsPcntlParallelCompile() or $job <= 1) { if ($job <= 1) {
return $this->compileSourceFile($sourceFiles); return $this->compileSourceFile($sourceFiles);
} }
// Unix/Linux/macOS compile in parallel using pcntl if (function_exists('proc_open') && function_exists('proc_get_status')) {
return $this->compileWithPcntl($sourceFiles, $job); return $this->compileWithProcessPool($sourceFiles, $job);
}
$this->climate->warning(
'proc_open/proc_get_status unavailable, using sequential compilation',
);
return $this->compileSourceFile($sourceFiles);
} }
/** @param list<string> $generatedSources @return list<string> */ /** @param list<string> $generatedSources @return list<string> */
@ -2151,63 +2195,8 @@ CODE;
return $objectFiles; return $objectFiles;
} }
/** protected function compileWithProcessPool(array $sourceFiles, int $job): array
* Parallel compilation on Unix/Linux/macOS (using pcntl).
*/
protected function pcntlWait(?int &$status): int
{
return pcntl_wait($status);
}
protected function pcntlFork(): int
{
return pcntl_fork();
}
protected function pcntlLastError(): int
{
return pcntl_get_last_error();
}
protected function waitForCompileChild(): array
{ {
do {
$status = null;
$pid = $this->pcntlWait($status);
$error = $pid === -1 ? $this->pcntlLastError() : 0;
} while ($pid === -1 && defined('PCNTL_EINTR') && $error === PCNTL_EINTR);
if ($pid === -1) {
$message = function_exists('pcntl_strerror') ? pcntl_strerror($error) : 'error ' . $error;
throw new \RuntimeException('Failed to wait for compiler process: ' . $message);
}
return [$pid, (int) $status];
}
protected function compileChildSucceeded(int $status): bool
{
return pcntl_wifexited($status) && pcntl_wexitstatus($status) === 0;
}
protected function getCompileChildFailureReason(int $status): string
{
if (pcntl_wifsignaled($status)) {
return 'terminated by signal ' . pcntl_wtermsig($status);
}
if (pcntl_wifexited($status)) {
return 'exited with status ' . pcntl_wexitstatus($status);
}
return 'terminated abnormally';
}
protected function compileWithPcntl(array $sourceFiles, int $job): array
{
if (!function_exists('pcntl_fork')) {
$this->climate->warning('pcntl extension not available, using sequential compilation');
return $this->compileSourceFile($sourceFiles);
}
$totalFiles = count($sourceFiles); $totalFiles = count($sourceFiles);
$this->climate->lightBlue("Starting parallel compilation with {$job} jobs for {$totalFiles} files"); $this->climate->lightBlue("Starting parallel compilation with {$job} jobs for {$totalFiles} files");
$progress = null; $progress = null;
@ -2218,29 +2207,78 @@ CODE;
->labelStyle([AnsiTerminal::FG_CYAN]); ->labelStyle([AnsiTerminal::FG_CYAN]);
$progress->renderInPlace(0, $totalFiles, 'Compiling'); $progress->renderInPlace(0, $totalFiles, 'Compiling');
} }
$result = $this->getNativeBuilder()->dispatchParallel(
$sourceFiles, $tasks = [];
$cachedObjects = [];
$taskMetadata = [];
$completedBeforeDispatch = 0;
foreach (SourceCompileQueue::largestFirst($sourceFiles) as $source) {
$object = $this->getObjectFile($source);
$task = $this->prepareCompileFileTask($source, $object, true);
if ($task === null) {
$cachedObjects[] = $object;
$completedBeforeDispatch++;
if ($this->noProgress) {
$percent = $completedBeforeDispatch >= $totalFiles
? 100
: min(99, (int) ceil($completedBeforeDispatch / $totalFiles * 100));
$shortSource = $this->removeCommonPrefix($this->buildDir, $source);
$this->climate->white(
"[{$completedBeforeDispatch}/{$totalFiles}] {$percent}% {$shortSource} [cache]",
);
}
continue;
}
$tasks[] = [
'source' => $source,
'object' => $object,
'command' => $this->getNativeBuilder()->compileCommand(
$source,
$object,
$task['options'],
$task['language'],
),
];
$taskMetadata[$object] = $task;
}
if ($completedBeforeDispatch > 0 && !$this->noProgress) {
$progress->renderInPlace($completedBeforeDispatch, $totalFiles, 'Compiling');
}
$result = $this->getNativeBuilder()->dispatchProcessParallel(
$tasks,
$job, $job,
fn(string $source): string => $this->getObjectFile($source), function (
function (string $source, string $object): void { string $source,
$this->compileFile($source, $object, true); string $object,
}, string $command,
fn(): int => $this->pcntlFork(), array $output,
fn(): array => $this->waitForCompileChild(), int $status,
fn(int $status): bool => $this->compileChildSucceeded($status), bool $success,
function (string $source, string $object, int $status, bool $success, int $completed) use ($progress, $totalFiles): void { int $completed,
if (!$success) { ) use ($progress, $totalFiles, $completedBeforeDispatch, $taskMetadata): void {
echo PHP_EOL; $absoluteCompleted = $completedBeforeDispatch + $completed;
$this->climate->red("Compilation failed: {$source} ({$this->getCompileChildFailureReason($status)})"); if ($success) {
$this->finalizeCompileFileTask($source, $object, $taskMetadata[$object]);
} else {
if (!$this->noProgress) {
echo PHP_EOL;
}
foreach ($output as $line) {
$this->climate->red($line);
}
$this->climate->red("Compilation failed: {$source} (exit status {$status})");
$this->climate->comment($command);
} }
if ($this->noProgress) { if ($this->noProgress) {
$percent = $completed >= $totalFiles $percent = $absoluteCompleted >= $totalFiles
? 100 ? 100
: min(99, (int) ceil($completed / $totalFiles * 100)); : min(99, (int) ceil($absoluteCompleted / $totalFiles * 100));
$shortSource = $this->removeCommonPrefix($this->buildDir, $source); $shortSource = $this->removeCommonPrefix($this->buildDir, $source);
$this->climate->white("[{$completed}/{$totalFiles}] {$percent}% {$shortSource}"); $this->climate->white("[{$absoluteCompleted}/{$totalFiles}] {$percent}% {$shortSource}");
} else { } else {
$progress->renderInPlace($completed, $totalFiles, 'Compiling'); $progress->renderInPlace($absoluteCompleted, $totalFiles, 'Compiling');
} }
}, },
); );
@ -2253,7 +2291,7 @@ CODE;
throw new \Exception('Compilation failed for: ' . implode(', ', $result['failures'])); throw new \Exception('Compilation failed for: ' . implode(', ', $result['failures']));
} }
$this->climate->green("Successfully compiled {$totalFiles} files"); $this->climate->green("Successfully compiled {$totalFiles} files");
return $result['objects']; return [...$cachedObjects, ...$result['objects']];
} }
public function output(string $message, string $style = 'out'): void public function output(string $message, string $style = 'out'): void

Loading…
Cancel
Save