From 1b79fcd2b4e48a1b596df11f37f752bf1b67463a Mon Sep 17 00:00:00 2001 From: tianfenghan Date: Mon, 14 Sep 2026 14:36:16 +0800 Subject: [PATCH] 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 --- composer.json | 3 - phpunit/src/ParallelCompileTest.php | 174 +++++++++------------ project.yml | 6 +- src/Build/NativeBuilder.php | 181 ++++++++++++++-------- src/Platform/PlatformBase.php | 8 - src/Platform/Windows.php | 5 - src/Translator.php | 228 ++++++++++++++++------------ 7 files changed, 327 insertions(+), 278 deletions(-) diff --git a/composer.json b/composer.json index 5f17769a..577a6527 100644 --- a/composer.json +++ b/composer.json @@ -41,8 +41,5 @@ "cs-fix": "php-cs-fixer fix $1", "analyse": "phpstan analyse --memory-limit 1G -c phpstan.neon", "rector": "rector process --clear-cache" - }, - "suggest": { - "ext-pcntl": "Required for process control features." } } diff --git a/phpunit/src/ParallelCompileTest.php b/phpunit/src/ParallelCompileTest.php index 69149347..97c13626 100644 --- a/phpunit/src/ParallelCompileTest.php +++ b/phpunit/src/ParallelCompileTest.php @@ -5,137 +5,109 @@ namespace TypePhp\Tests; use PHPUnit\Framework\TestCase; use TypePhp\Backend\CompilerBackend; use TypePhp\Build\NativeBuilder; -use TypePhp\CompilerTest; -class ParallelCompileTest extends TestCase +final class ParallelCompileTest extends TestCase { - public function testWaitRetriesWhenInterrupted(): void - { - $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(); - } + private string $directory; - public function testSignaledChildIsNotSuccessful(): void + protected function setUp(): void { - $compiler = new ScriptedWaitCompiler([]); - - $this->assertTrue($compiler->statusSucceeded(0)); - $this->assertFalse($compiler->statusSucceeded(1 << 8)); - $this->assertFalse($compiler->statusSucceeded(SIGTERM)); + $this->directory = sys_get_temp_dir() . '/typephp_process_pool_' . bin2hex(random_bytes(6)); + mkdir($this->directory, 0777, true); } - public function testForkFailureStillReapsRunningCompilerProcesses(): void + protected function tearDown(): void { - $compiler = new ScriptedWaitCompiler( - [[101, 0, 0]], - [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()); + foreach (glob($this->directory . '/*') ?: [] as $file) { + @unlink($file); } - - $this->assertSame(1, $compiler->getWaitCallCount()); + @rmdir($this->directory); } - public function testParallelDispatcherReportsEachCompletedTask(): void + public function testProcessPoolRunsCompilerTasksConcurrently(): void { - $builder = new NativeBuilder($this->createMock(CompilerBackend::class)); - $forkResults = [101, 102]; - $waitResults = [[102, 0], [101, 1 << 8]]; + $firstMarker = $this->directory . '/first.ready'; + $secondMarker = $this->directory . '/second.ready'; + $firstObject = $this->directory . '/first.o'; + $secondObject = $this->directory . '/second.o'; $completed = []; - $result = $builder->dispatchParallel( - ['first.cc', 'second.cc'], + $builder = new NativeBuilder($this->createMock(CompilerBackend::class)); + $result = $builder->dispatchProcessParallel( + [ + $this->task('first.cc', $firstObject, $this->barrierCommand($firstMarker, $secondMarker, $firstObject)), + $this->task('second.cc', $secondObject, $this->barrierCommand($secondMarker, $firstMarker, $secondObject)), + ], 2, - static fn(string $source): string => $source . '.o', - static function (): void {}, - static function () use (&$forkResults): int { - return array_shift($forkResults); - }, - static function () use (&$waitResults): array { - return array_shift($waitResults); - }, - static fn(int $status): bool => $status === 0, - static function (string $source, string $object, int $status, bool $success, int $count) use (&$completed): void { - $completed[] = [$source, $object, $status, $success, $count]; + static function ( + string $source, + string $object, + string $command, + array $output, + int $status, + bool $success, + int $count, + ) use (&$completed): void { + $completed[$source] = [$object, $command, $output, $status, $success, $count]; }, ); - $this->assertSame(['second.cc.o'], $result['objects']); - $this->assertSame(['first.cc'], $result['failures']); - $this->assertSame([ - ['second.cc', 'second.cc.o', 0, true, 1], - ['first.cc', 'first.cc.o', 1 << 8, false, 2], - ], $completed); + self::assertSame([], $result['failures'], var_export($completed, true)); + self::assertEqualsCanonicalizing([$firstObject, $secondObject], $result['objects']); + self::assertCount(2, $completed); + self::assertTrue($completed['first.cc'][4]); + self::assertTrue($completed['second.cc'][4]); } -} -class ScriptedWaitCompiler extends CompilerTest -{ - 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 + public function testProcessPoolCapturesCompilerFailureOutputAndStatus(): void { - 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 - { - $this->waitCallCount++; - [$pid, $status, $this->lastWaitError] = array_shift($this->waitResults); - return $pid; - } - - protected function pcntlLastError(): int - { - return $this->lastWaitError; - } + $builder = new NativeBuilder($this->createMock(CompilerBackend::class)); + $result = $builder->dispatchProcessParallel( + [$this->task('failed.cc', $object, $command)], + 4, + static function ( + string $source, + string $object, + string $command, + array $output, + int $status, + bool $success, + int $count, + ) use (&$completion): void { + $completion = compact('source', 'object', 'command', 'output', 'status', 'success', 'count'); + }, + ); - public function waitForTest(): array - { - return $this->waitForCompileChild(); + self::assertSame([], $result['objects']); + self::assertSame(['failed.cc'], $result['failures']); + 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); } } diff --git a/project.yml b/project.yml index 11dd8e82..3b560d4b 100644 --- a/project.yml +++ b/project.yml @@ -1,6 +1,6 @@ name: tpc build-mode: bin -version: 0.8.1 +version: 0.8.2 cxx-std: c++17 cxx-flags: - -Wall @@ -12,8 +12,8 @@ resource: icon: swoole-logo.ico # 版本信息 version-info: - file-version: 0.8.1.1112 - product-version: 0.8.1 + file-version: 0.8.2.1112 + product-version: 0.8.2 company-name: "上海识沃网络科技有限公司" file-description: "TypePHP Compiler" internal-name: "typephp" diff --git a/src/Build/NativeBuilder.php b/src/Build/NativeBuilder.php index 30108615..3dfb8d28 100644 --- a/src/Build/NativeBuilder.php +++ b/src/Build/NativeBuilder.php @@ -58,86 +58,141 @@ final readonly class NativeBuilder } /** - * @param Closure(string): string $objectFile - * @param Closure(string, string): void $worker - * @param Closure(): int $fork - * @param Closure(): array{int, int} $wait - * @param Closure(int): bool $succeeded - * @param null|Closure(string, string, int, bool, int): void $completed + * Run compiler commands concurrently through proc_open(). Keeping the + * process pool independent of pcntl makes parallel builds available in a + * stock PHP installation on Linux, macOS, and Windows. + * + * Output is redirected to one temporary file per process. This avoids the + * pipe-buffer deadlocks that can otherwise occur when a compiler emits a + * large diagnostic while the parent is waiting for another process. + * + * @param list $tasks + * @param null|Closure(string, string, string, list, int, bool, int): void $completed * @return array{objects: list, failures: list} */ - public function dispatchParallel( - array $sources, + public function dispatchProcessParallel( + array $tasks, int $jobs, - Closure $objectFile, - Closure $worker, - Closure $fork, - Closure $wait, - Closure $succeeded, ?Closure $completed = null, ): array { - // Keep most workers on the largest translation units to reduce the - // parallel tail, but reserve one fast lane for small files so progress - // remains visible while the expensive units are still compiling. - $queue = SourceCompileQueue::largestFirst($sources); + $jobs = max(1, $jobs); + $queue = $tasks; $running = []; $objects = []; $failures = []; $completedCount = 0; - $largeTaskCount = 0; - $largeLaneLimit = max(1, $jobs - 1); - - while ($queue !== [] || $running !== []) { - while (count($running) < $jobs && $queue !== []) { - if ($largeTaskCount < $largeLaneLimit) { - $source = array_shift($queue); - $lane = 'large'; - } else { - $source = array_pop($queue); - $lane = 'small'; + $nullDevice = PHP_OS_FAMILY === 'Windows' ? 'NUL' : '/dev/null'; + + try { + while ($queue !== [] || $running !== []) { + while (count($running) < $jobs && $queue !== []) { + $task = array_shift($queue); + $logFile = tempnam(sys_get_temp_dir(), 'typephp-compile-'); + if ($logFile === false) { + $failures[] = $task['source']; + $completedCount++; + $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 ($pid === -1) { - $failures[] = $source; - array_push($failures, ...$queue); - $queue = []; - break; + + if ($running === []) { + continue; } - if ($pid === 0) { - try { - $worker($source, $object); - exit(is_file($object) ? 0 : 1); - } catch (\Throwable) { - exit(1); + + $finished = false; + foreach ($running as $index => $entry) { + $status = proc_get_status($entry['process']); + if ($status['running']) { + 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]; - if ($lane === 'large') { - $largeTaskCount++; + $running = array_values($running); + + if (!$finished) { + usleep(10_000); } } - if ($running === []) { - break; - } - [$pid, $status] = $wait(); - $task = $running[$pid] ?? null; - unset($running[$pid]); - if ($task === null) { - continue; - } - if ($task['lane'] === 'large') { - $largeTaskCount--; - } - $success = $succeeded($status); - if ($success) { - $objects[] = $task['object']; - } else { - $failures[] = $task['source']; + } finally { + foreach ($running as $entry) { + if (is_resource($entry['process'])) { + proc_terminate($entry['process']); + proc_close($entry['process']); + } + @unlink($entry['log']); } - $completedCount++; - $completed?->__invoke($task['source'], $task['object'], $status, $success, $completedCount); } + return ['objects' => $objects, 'failures' => $failures]; } diff --git a/src/Platform/PlatformBase.php b/src/Platform/PlatformBase.php index 91dd8e91..d1f9c256 100644 --- a/src/Platform/PlatformBase.php +++ b/src/Platform/PlatformBase.php @@ -140,14 +140,6 @@ abstract class PlatformBase return $warnings; } - /** - * Whether this platform is suitable for parallel compilation using pcntl_fork. - */ - public function supportsPcntlParallelCompile(): bool - { - return true; - } - public function getIntegerLiteralSuffix(): string { return 'LL'; diff --git a/src/Platform/Windows.php b/src/Platform/Windows.php index 208c586c..55589387 100644 --- a/src/Platform/Windows.php +++ b/src/Platform/Windows.php @@ -226,11 +226,6 @@ class Windows extends PlatformBase return $warnings; } - public function supportsPcntlParallelCompile(): bool - { - return false; - } - /** * Get the debug options. */ diff --git a/src/Translator.php b/src/Translator.php index 85bd7332..5be496df 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -24,6 +24,7 @@ use TypePhp\Build\NativeDependencyAuditor; use TypePhp\Build\NanoSourceComposer; use TypePhp\Build\PrecompiledHeaderManager; use TypePhp\Build\SourcePipelineTrait; +use TypePhp\Build\SourceCompileQueue; use TypePhp\Build\WasmInterfaceGenerator; use TypePhp\Config\ProjectYamlLoader; use TypePhp\Diagnostics\CompileTimeAttributeDiagnostic; @@ -82,7 +83,7 @@ class Translator extends Preprocessor use ResourceCompilationTrait; use ClassConstantValueTrait; - public const string VERSION = '0.8.1'; + public const string VERSION = '0.8.2'; public const string APP_NAME = 'TypePHP Compiler (AOT)'; protected bool $hasExplicitOutput = false; @@ -1903,6 +1904,46 @@ CODE; 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) && !$this->isProjectRuntimeEntryFile($cppFile); $isNanoRuntimeSource = isset($this->nanoRuntimeSources[$cppFile]); @@ -1910,10 +1951,10 @@ CODE; if (!$parallel) { $this->climate->darkGray('[cache] skip: ' . $cppFile); } - return; + return null; } if ($isNanoRuntimeSource && $this->hasNanoObjectFileCache($cppFile, $objectFile)) { - return; + return null; } if ($isCacheableMiscFile) { @@ -1921,24 +1962,21 @@ CODE; } $language = $this->getLanguageFromExtension($cppFile); - $options = $this->getSourceCompileCommandOptions($cppFile, $language); - $result = $this->getNativeBuilder()->compile($cppFile, $objectFile, $options, $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); - } + return [ + 'language' => $language, + 'options' => $this->getSourceCompileCommandOptions($cppFile, $language), + 'cacheable_misc' => $isCacheableMiscFile, + 'nano_runtime' => $isNanoRuntimeSource, + ]; + } - 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); } - if ($isNanoRuntimeSource) { + if ($task['nano_runtime']) { $this->writeMiscObjectCacheMetadata($cppFile, $objectFile); } } @@ -2015,12 +2053,18 @@ CODE; // Windows: compile the resource file (icon, version info, etc.) $this->compileResourceFile(); - if (!$this->getPlatform()->supportsPcntlParallelCompile() or $job <= 1) { + if ($job <= 1) { return $this->compileSourceFile($sourceFiles); } - // Unix/Linux/macOS compile in parallel using pcntl - return $this->compileWithPcntl($sourceFiles, $job); + if (function_exists('proc_open') && function_exists('proc_get_status')) { + return $this->compileWithProcessPool($sourceFiles, $job); + } + + $this->climate->warning( + 'proc_open/proc_get_status unavailable, using sequential compilation', + ); + return $this->compileSourceFile($sourceFiles); } /** @param list $generatedSources @return list */ @@ -2151,63 +2195,8 @@ CODE; return $objectFiles; } - /** - * 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 + protected function compileWithProcessPool(array $sourceFiles, int $job): 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); $this->climate->lightBlue("Starting parallel compilation with {$job} jobs for {$totalFiles} files"); $progress = null; @@ -2218,29 +2207,78 @@ CODE; ->labelStyle([AnsiTerminal::FG_CYAN]); $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, - fn(string $source): string => $this->getObjectFile($source), - function (string $source, string $object): void { - $this->compileFile($source, $object, true); - }, - fn(): int => $this->pcntlFork(), - fn(): array => $this->waitForCompileChild(), - fn(int $status): bool => $this->compileChildSucceeded($status), - function (string $source, string $object, int $status, bool $success, int $completed) use ($progress, $totalFiles): void { - if (!$success) { - echo PHP_EOL; - $this->climate->red("Compilation failed: {$source} ({$this->getCompileChildFailureReason($status)})"); + function ( + string $source, + string $object, + string $command, + array $output, + int $status, + bool $success, + int $completed, + ) use ($progress, $totalFiles, $completedBeforeDispatch, $taskMetadata): void { + $absoluteCompleted = $completedBeforeDispatch + $completed; + 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) { - $percent = $completed >= $totalFiles + $percent = $absoluteCompleted >= $totalFiles ? 100 - : min(99, (int) ceil($completed / $totalFiles * 100)); + : min(99, (int) ceil($absoluteCompleted / $totalFiles * 100)); $shortSource = $this->removeCommonPrefix($this->buildDir, $source); - $this->climate->white("[{$completed}/{$totalFiles}] {$percent}% {$shortSource}"); + $this->climate->white("[{$absoluteCompleted}/{$totalFiles}] {$percent}% {$shortSource}"); } 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'])); } $this->climate->green("Successfully compiled {$totalFiles} files"); - return $result['objects']; + return [...$cachedObjects, ...$result['objects']]; } public function output(string $message, string $style = 'out'): void