diff --git a/src/Build/ProjectConversion.php b/src/Build/ProjectConversion.php new file mode 100644 index 00000000..8eb08769 --- /dev/null +++ b/src/Build/ProjectConversion.php @@ -0,0 +1,40 @@ + $files + * @param list $sourceFiles + */ + public function __construct( + private array $files, + private array $sourceFiles, + private int $validSourceCount, + ) { + } + + /** @return list */ + public function files(): array + { + return $this->files; + } + + /** @return list */ + public function sourceFiles(): array + { + return $this->sourceFiles; + } + + public function hasValidSources(): bool + { + return $this->validSourceCount !== 0; + } +} diff --git a/src/Build/SourcePipelineTrait.php b/src/Build/SourcePipelineTrait.php index bb9afb68..aea74774 100644 --- a/src/Build/SourcePipelineTrait.php +++ b/src/Build/SourcePipelineTrait.php @@ -464,37 +464,46 @@ trait SourcePipelineTrait public function getFiles(string $path): array { $this->applyPhpVersionCommandLineArgument(); - $realpath = realpath($path); - if ($realpath === false) { + $projectPath = realpath($path); + if ($projectPath === false) { $this->error("path not exists: {$path}"); } - $path = $realpath; + $files = $this->discoverProjectFiles($projectPath); + // Command-line values have the highest precedence and therefore apply + // only after a YAML project has loaded all included configuration. + $this->applyCommandLineArguments(); + $this->validateLoadedProjectConfiguration(); + $files = $this->excludeGeneratedLibraryStub($files); + return $this->filterIgnoredFiles($files); + } + + /** @return list */ + private function discoverProjectFiles(string $path): array + { if (is_dir($path)) { // Directory mode: no YAML parsing - $list = $this->getFilesFromDir($path); - $targetName = basename($path); - $this->setTargetName($targetName); + $files = $this->getFilesFromDir($path); + $this->setTargetName(basename($path)); $this->sourceDirs[] = $path; - } else { - $ext = pathinfo($path, PATHINFO_EXTENSION); - if ($ext === 'yml' || $ext === 'yaml') { - // YAML config mode: parse the YAML first - $list = $this->parseProjectYaml($path); - } elseif ($ext === 'php') { - // Single-file mode: no YAML parsing - $list = [$path]; - $targetName = FileScanner::getFileName($path); - $this->setTargetName($targetName); - $this->sourceDirs[] = dirname($path); - } else { - $this->error('Unsupported file type: ' . $path); - } + return $files; } - // Apply command-line arguments after all configuration is loaded (so they - // take the highest precedence) - $this->applyCommandLineArguments(); + $extension = pathinfo($path, PATHINFO_EXTENSION); + if ($extension === 'yml' || $extension === 'yaml') { + return $this->parseProjectYaml($path); + } + if ($extension === 'php') { + // Single-file mode: no YAML parsing + $this->setTargetName(FileScanner::getFileName($path)); + $this->sourceDirs[] = dirname($path); + return [$path]; + } + $this->error('Unsupported file type: ' . $path); + } + + private function validateLoadedProjectConfiguration(): void + { if ($this->embeddedFiles !== [] && !$this->isBuildModeBin()) { $this->error('`embedded-files` requires `mode: bin`'); } @@ -505,65 +514,88 @@ trait SourcePipelineTrait $this->clearIncrementalBuildCache(); } $this->validateProjectObjectFiles(); + } + /** @param list $files @return list */ + private function excludeGeneratedLibraryStub(array $files): array + { // The generated public import stub is an output artifact, not an input // of the library that produced it. Exclude a previous build's copy when // a project scans its output directory recursively. - if ($this->isBuildModeLib()) { - $generatedStub = realpath($this->getLibraryImportStubFile()); - if ($generatedStub !== false) { - $list = array_values(array_filter( - $list, - static fn(string $file): bool => realpath($file) !== $generatedStub, - )); - } + if (!$this->isBuildModeLib()) { + return $files; } - - return $this->filterIgnoredFiles($list); + $generatedStub = realpath($this->getLibraryImportStubFile()); + if ($generatedStub === false) { + return $files; + } + return array_values(array_filter( + $files, + static fn(string $file): bool => realpath($file) !== $generatedStub, + )); } public function prepare(string $path): array { $files = $this->getFiles($path); + $this->prepareBuildEnvironment(); + $preparedKey = $this->preparedProjectKey($files); + if (!$this->restorePreparedProject($preparedKey)) { + $files = $this->preprocessProjectFiles($files, $preparedKey); + } + return $this->finalizePreparedProject($files); + } + private function prepareBuildEnvironment(): void + { + $this->prepareRuntimeDependencies(); + $this->validateCompilerToolchain(); + $this->reportBuildLibraryWarnings(); + } + + private function prepareRuntimeDependencies(): void + { // Source-composed Nano does not consume the host PHP/PHPX runtime. // Windows Nano deliberately leaves nanoMode=false and therefore keeps // this original DLL/import-library validation path. - if (!$this->isNanoMode()) { - if ($this->isBuildModeEmbed() && $this->getPlatform() instanceof Linux) { - try { - $phpDir = (new LibPhpInstaller())->ensure($this->getPhpDir()) ?? $this->getPhpDir(); - } catch (\Throwable $e) { - $this->error('Unable to install libphp.so: ' . $e->getMessage()); - } - } else { - $phpDir = $this->getPhpDir(); - } + if ($this->isNanoMode()) { + return; + } - if (!($this->getPlatform() instanceof Wasi)) { - $this->validatePhpRuntimeMinimum($phpDir); + $platform = $this->getPlatform(); + $phpDir = $this->getPhpDir(); + if ($this->isBuildModeEmbed() && $platform instanceof Linux) { + try { + $phpDir = (new LibPhpInstaller())->ensure($phpDir) ?? $phpDir; + } catch (\Throwable $e) { + $this->error('Unable to install libphp.so: ' . $e->getMessage()); } + } - if ($this->getPlatform() instanceof Linux) { - try { - (new LibPhpxInstaller())->ensure($this->getPhpxDir(), $phpDir); - } catch (\Throwable $e) { - $this->error('Unable to build libphpx.so: ' . $e->getMessage()); - } - } + if (!($platform instanceof Wasi)) { + $this->validatePhpRuntimeMinimum($phpDir); + } - // Pre-check the phpx library only at the PHP script entry (bin/tpc.php): - // a missing library fails immediately rather than surfacing later during - // file processing/compilation. The compiled tpc executable has libphpx - // loaded by the dynamic linker before entering main(), so checking here - // is neither needed nor possible. - if (defined('TYPEPHP_PHP_SCRIPT_ENTRY') && !($this->getPlatform() instanceof Wasi)) { - $this->validatePhpxLibrary(); + if ($platform instanceof Linux) { + try { + (new LibPhpxInstaller())->ensure($this->getPhpxDir(), $phpDir); + } catch (\Throwable $e) { + $this->error('Unable to build libphpx.so: ' . $e->getMessage()); } } - $this->validateCompilerToolchain(); + // Pre-check the phpx library only at the PHP script entry (bin/tpc.php): + // a missing library fails immediately rather than surfacing later during + // file processing/compilation. The compiled tpc executable has libphpx + // loaded by the dynamic linker before entering main(), so checking here + // is neither needed nor possible. + if (defined('TYPEPHP_PHP_SCRIPT_ENTRY') && !($platform instanceof Wasi)) { + $this->validatePhpxLibrary(); + } + } + private function reportBuildLibraryWarnings(): void + { // shell_exec and define are already called directly via php::fn::, so no // dynamic symbol table is needed @@ -590,15 +622,11 @@ trait SourcePipelineTrait } } } + } - $files = $this->filterIgnoredFiles($files); - $preparedKey = $this->preparedProjectKey($files); - if ($this->restorePreparedProject($preparedKey)) { - $this->embeddedOpcodeFiles = array_values(array_diff($this->embeddedPhpFiles, $files)); - $files = $this->getSortedFiles($files); - $this->initializeIncrementalCompilation($files); - return $files; - } + /** @param list $files @return list */ + private function preprocessProjectFiles(array $files, string $preparedKey): array + { $warningsBefore = $this->preprocessingWarningCount; $inputCount = count($files); $this->discoverNativeClassDeclarations($files); @@ -627,6 +655,12 @@ trait SourcePipelineTrait if (count($files) === $inputCount && $this->preprocessingWarningCount === $warningsBefore) { $this->storePreparedProject($preparedKey); } + return array_values($files); + } + + /** @param list $files @return list */ + private function finalizePreparedProject(array $files): array + { $files = $this->getSortedFiles($files); $this->embeddedOpcodeFiles = array_values(array_diff($this->embeddedPhpFiles, $files)); $this->initializeIncrementalCompilation($files); @@ -738,171 +772,217 @@ trait SourcePipelineTrait try { $this->composeTraitDeclarations($files); $previousPhase = $this->enterCompilerPhase(self::PHASE_CONVERT); - // Hydrate persistent literal/resource IDs before any unchanged - // translation unit or declaration header is reused. - $this->getStableIdRegistry(); - // All declarations are now known. Lower declaration constant - // expressions before translating any function body so cache IDs - // are assigned exclusively in the convert phase. - $this->finalizeDeclarationExpressions($this->getDeclarationInputFiles($files)); - // Whole-program extension generation must not depend on conversion - // side effects from dirty files. Clean incremental files are not - // converted, but their non-empty property defaults still require a - // custom allocation path in the regenerated module entry. - $this->finalizeRequestArrayDefaultMetadata(); - $this->initializeDeclarationHeaderFiles($files); - $this->restoreCleanIncrementalMetadata($files); - - // Native/import stubs are declaration inputs, not ordinary PHP - // bodies. Their Zend metadata still belongs to the module entry. - foreach ($this->getDeclarationInputFiles($files) as $file) { - if ($this->isStubFile($file) && $this->shouldRegeneratePhpFile($file)) { - $this->genStubFile($file); - } + $this->initializeProjectConversion($files); + return $this->finalizeProjectConversion($this->convertProjectFiles($files)); + } finally { + $this->splitTranslationUnitsEnabled = $previousSplitSetting; + if ($previousPhase !== null) { + $this->restoreCompilerPhase($previousPhase); } + $this->compilationStatistics->finish(); + } + } - $sourceFiles = []; - $validSourceCount = 0; - // Generate the C++ files - foreach ($files as $k => $file) { - try { - if (FileScanner::isPhpFile($file)) { - $path = realpath($file) ?: $file; - $anonymousManifest = $this->anonymousManifestPath($path); - $legacyAnonymousManifest = $this->getBuildDir() . '/anonymous-' - . substr(hash('sha256', $path), 0, 20) . '.json'; - if (is_file($legacyAnonymousManifest) - && file_get_contents($legacyAnonymousManifest) === '[]') { - unlink($legacyAnonymousManifest); - } - // Older builds wrote [] for every PHP file. Those files - // carry no cache data and can be removed on a cache hit. - if (is_file($anonymousManifest) - && file_get_contents($anonymousManifest) === '[]') { - unlink($anonymousManifest); - } - $shouldRegenerate = $this->shouldRegeneratePhpFile($path); - $hasAnonymousManifest = is_file($anonymousManifest); - $needsAnonymousRefresh = !$shouldRegenerate - && (is_file($legacyAnonymousManifest) - || ($hasAnonymousManifest - ? !$this->canEmbedAnonymousClassOpcode() - : (preg_match('/new\s+class\b/', (string) file_get_contents($path)) === 1 - && $this->canEmbedAnonymousClassOpcode()))); - if (!$shouldRegenerate && !$needsAnonymousRefresh) { - if ($hasAnonymousManifest) { - $this->restoreAnonymousManifest($path); - } - $validSourceCount++; - if ($this->incrementalTranslationUnitWasEmitted($path)) { - $cppFile = $this->getCppFile($path); - $this->registerGeneratedProjectSource($cppFile); - $sourceFiles[] = $cppFile; - foreach ($this->getSplitTranslationUnits($path) as $part) { - $this->registerGeneratedProjectSource($part); - $sourceFiles[] = $part; - } - } - $this->climate->darkGray( - '[cached] ' . $this->getRelativePath($path), - ); - continue; - } - $statisticsBefore = $this->compilationStatistics->all(); - $this->currentAnonymousFiles = []; - // A dirty dependency means code generation must run; - // it does not mean the generated bytes changed. Keep - // an existing translation unit's timestamp when the - // output is identical, matching CMake/Ninja's restat - // model and preventing needless native recompilation. - $cppFile = $this->convertFile($path); - if ($this->currentAnonymousFiles !== []) { - $this->writeFile($anonymousManifest, json_encode( - array_values(array_unique($this->currentAnonymousFiles)), - JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR, - )); - } elseif (is_file($anonymousManifest)) { - unlink($anonymousManifest); - } - if (is_file($legacyAnonymousManifest)) { - unlink($legacyAnonymousManifest); - } - $this->recordIncrementalConversion( - $path, - $cppFile !== null, - $this->compilationStatistics->delta($statisticsBefore), - ); - } elseif (FileScanner::isNativeSourceFile($file)) { - $cppFile = $file; - } else { - continue; - } - $validSourceCount++; - if ($cppFile !== null) { - $sourceFiles[] = $cppFile; - if (FileScanner::isPhpFile($file)) { - foreach ($this->getSplitTranslationUnits($file) as $part) { - $this->registerGeneratedProjectSource($part); - $sourceFiles[] = $part; - } - } - } - } catch (Unsupported $e) { - echo ' unsupported syntax: ' . $e->getMessage() . "\n"; - echo ' skip: ' . $file . "\n"; - if (in_array($file, $this->embeddedPhpFiles, true) - && !in_array($file, $this->embeddedOpcodeFiles, true)) { - $this->embeddedOpcodeFiles[] = $file; - } - unset($files[$k]); - } - } - $this->finalizeIncrementalConversionMetadata($files); - - // A valid PHP input may intentionally emit no standalone translation - // unit (for example a compile-time trait or an interface). The shared - // extension source still carries its runtime metadata, so only reject - // an input set in which no supported source was converted at all. - if ($validSourceCount === 0) { - $this->stop('No valid source file found'); + /** @param list $files */ + private function initializeProjectConversion(array $files): void + { + // Persistent IDs must be hydrated before any unchanged translation + // unit or declaration header is reused. + $this->getStableIdRegistry(); + // Declaration constants are lowered before bodies so cache IDs are + // assigned exclusively in the convert phase. + $declarationFiles = $this->getDeclarationInputFiles($files); + $this->finalizeDeclarationExpressions($declarationFiles); + $this->finalizeRequestArrayDefaultMetadata(); + $this->initializeDeclarationHeaderFiles($files); + $this->restoreCleanIncrementalMetadata($files); + + // Import stubs contribute Zend metadata but have no ordinary PHP body. + foreach ($declarationFiles as $file) { + if ($this->isStubFile($file) && $this->shouldRegeneratePhpFile($file)) { + $this->genStubFile($file); } + } + } - // A WASI library publishes WIT/Component exports rather than a native - // TypePHP shared-library ABI, so a PHP import stub would be misleading. - if ($this->isBuildModeLib() && !$this->isWasiTarget()) { - $this->genLibraryImportStub($files); + /** @param list $files */ + private function convertProjectFiles(array $files): ProjectConversion + { + $sourceFiles = []; + $validSourceCount = 0; + foreach ($files as $key => $file) { + try { + $generated = $this->convertProjectFile($file); + if ($generated === null) { + continue; + } + ++$validSourceCount; + array_push($sourceFiles, ...$generated); + } catch (Unsupported $error) { + $this->reportUnsupportedProjectFile($file, $error); + unset($files[$key]); } + } + return new ProjectConversion(array_values($files), $sourceFiles, $validSourceCount); + } - // Function and data declarations are emitted together, one header - // per PHP source, plus a small project-runtime ABI header. - $this->genDeclarationHeaders($files); - // Large array-valued class constants used to make module_init() one - // enormous GCC optimization unit. Emit their request-lifecycle - // helpers as independent, cacheable translation units while the - // arginfo-backed class registration remains in extension-*.cc. - foreach ($this->genClassArrayConstantLifecycleSources() as $lifecycleSource) { - $sourceFiles[] = $lifecycleSource; - } - // Nano keeps the ordinary statically registered Zend class/module - // metadata, then adds a direct native process entry beside it. - $sourceFiles[] = $this->genExtension(); - if ($this->isBuildModeEmbed() && !$this->isNanoMode() - && ($this->embeddedFiles !== [] || $this->embeddedOpcodeFiles !== [])) { - array_push($sourceFiles, ...$this->genEmbeddedOpcodeTable()); - } - if ($this->isNanoMode()) { - $sourceFiles[] = $this->genNanoEntrypoint(); - } - $this->getStableIdRegistry()->flush(); - $this->saveIncrementalCompilationState($files); + /** @return list|null */ + private function convertProjectFile(string $file): ?array + { + if (FileScanner::isPhpFile($file)) { + return $this->convertPhpProjectFile(realpath($file) ?: $file); + } + return FileScanner::isNativeSourceFile($file) ? [$file] : null; + } - return $sourceFiles; - } finally { - $this->splitTranslationUnitsEnabled = $previousSplitSetting; - if ($previousPhase !== null) { - $this->restoreCompilerPhase($previousPhase); + /** @return list */ + private function convertPhpProjectFile(string $file): array + { + $manifest = $this->anonymousManifestPath($file); + $legacyManifest = $this->legacyAnonymousManifestPath($file); + $this->removeEmptyAnonymousManifest($manifest); + $this->removeEmptyAnonymousManifest($legacyManifest); + + $shouldRegenerate = $this->shouldRegeneratePhpFile($file); + if (!$shouldRegenerate && !$this->anonymousManifestNeedsRefresh($file, $manifest, $legacyManifest)) { + return $this->restoreCachedPhpProjectFile($file, $manifest); + } + return $this->regeneratePhpProjectFile($file, $manifest, $legacyManifest); + } + + private function legacyAnonymousManifestPath(string $file): string + { + return $this->getBuildDir() . '/anonymous-' + . substr(hash('sha256', $file), 0, 20) . '.json'; + } + + private function removeEmptyAnonymousManifest(string $manifest): void + { + if (is_file($manifest) && file_get_contents($manifest) === '[]') { + unlink($manifest); + } + } + + private function anonymousManifestNeedsRefresh( + string $file, + string $manifest, + string $legacyManifest, + ): bool { + if (is_file($legacyManifest)) { + return true; + } + if (is_file($manifest)) { + return !$this->canEmbedAnonymousClassOpcode(); + } + return preg_match('/new\s+class\b/', (string) file_get_contents($file)) === 1 + && $this->canEmbedAnonymousClassOpcode(); + } + + /** @return list */ + private function restoreCachedPhpProjectFile(string $file, string $manifest): array + { + if (is_file($manifest)) { + $this->restoreAnonymousManifest($file); + } + $sourceFiles = []; + if ($this->incrementalTranslationUnitWasEmitted($file)) { + $cppFile = $this->getCppFile($file); + $this->registerGeneratedProjectSource($cppFile); + $sourceFiles[] = $cppFile; + array_push($sourceFiles, ...$this->getRegisteredSplitTranslationUnits($file)); + } + $this->climate->darkGray('[cached] ' . $this->getRelativePath($file)); + return $sourceFiles; + } + + /** @return list */ + private function regeneratePhpProjectFile( + string $file, + string $manifest, + string $legacyManifest, + ): array { + $statisticsBefore = $this->compilationStatistics->all(); + $this->currentAnonymousFiles = []; + // Dirty dependencies require code generation, but byte-identical output + // keeps its timestamp so native compilation can still hit its cache. + $cppFile = $this->convertFile($file); + $this->storeAnonymousManifest($manifest); + if (is_file($legacyManifest)) { + unlink($legacyManifest); + } + $this->recordIncrementalConversion( + $file, + $cppFile !== null, + $this->compilationStatistics->delta($statisticsBefore), + ); + if ($cppFile === null) { + return []; + } + return [$cppFile, ...$this->getRegisteredSplitTranslationUnits($file)]; + } + + private function storeAnonymousManifest(string $manifest): void + { + if ($this->currentAnonymousFiles === []) { + if (is_file($manifest)) { + unlink($manifest); } - $this->compilationStatistics->finish(); + return; + } + $this->writeFile($manifest, json_encode( + array_values(array_unique($this->currentAnonymousFiles)), + JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR, + )); + } + + /** @return list */ + private function getRegisteredSplitTranslationUnits(string $file): array + { + $parts = $this->getSplitTranslationUnits($file); + foreach ($parts as $part) { + $this->registerGeneratedProjectSource($part); + } + return $parts; + } + + private function reportUnsupportedProjectFile(string $file, Unsupported $error): void + { + echo ' unsupported syntax: ' . $error->getMessage() . "\n"; + echo ' skip: ' . $file . "\n"; + if (in_array($file, $this->embeddedPhpFiles, true) + && !in_array($file, $this->embeddedOpcodeFiles, true)) { + $this->embeddedOpcodeFiles[] = $file; + } + } + + /** @return list */ + private function finalizeProjectConversion(ProjectConversion $conversion): array + { + $files = $conversion->files(); + $this->finalizeIncrementalConversionMetadata($files); + // Trait and interface inputs may emit no standalone translation unit, + // but at least one supported input must participate in the project. + if (!$conversion->hasValidSources()) { + $this->stop('No valid source file found'); + } + + if ($this->isBuildModeLib() && !$this->isWasiTarget()) { + $this->genLibraryImportStub($files); + } + $this->genDeclarationHeaders($files); + + $sourceFiles = $conversion->sourceFiles(); + array_push($sourceFiles, ...$this->genClassArrayConstantLifecycleSources()); + $sourceFiles[] = $this->genExtension(); + if ($this->isBuildModeEmbed() && !$this->isNanoMode() + && ($this->embeddedFiles !== [] || $this->embeddedOpcodeFiles !== [])) { + array_push($sourceFiles, ...$this->genEmbeddedOpcodeTable()); + } + if ($this->isNanoMode()) { + $sourceFiles[] = $this->genNanoEntrypoint(); } + $this->getStableIdRegistry()->flush(); + $this->saveIncrementalCompilationState($files); + return $sourceFiles; } } diff --git a/src/Translator.php b/src/Translator.php index 76cac356..f83e62f9 100644 --- a/src/Translator.php +++ b/src/Translator.php @@ -2292,56 +2292,79 @@ CODE; public function compile(array $sourceFiles): array { - $job = $this->maxJob; + $sourceFiles = $this->prepareNativeSourceFiles($sourceFiles); + $this->prepareNativeCompilationEnvironment(); + return $this->dispatchNativeCompilation($sourceFiles); + } + /** @param list $sourceFiles @return list */ + private function prepareNativeSourceFiles(array $sourceFiles): array + { if ($this->isNanoMode()) { - $sourceFiles = $this->composeNanoRuntimeSources($sourceFiles); - // The embed build needs the main function and the CLI's built-in function definitions. - } elseif ($this->isBuildModeEmbed()) { - $runtimeSource = $this->getPhpxDir() . '/src/misc/typephp_runtime.cc'; - // PHPX 2.6.3 keeps the common runtime in typephp_main.cc. Newer - // PHPX versions split it out so the object can be shared across - // projects. Keep the old layout buildable during release rollout. - if (is_file($runtimeSource)) { - $sourceFiles[] = $runtimeSource; - } - $sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_main.cc'; - if ($this->embeddedFiles !== [] || $this->embeddedOpcodeFiles !== []) { - $sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_opcode_table.cc'; - // Select the decoder for the PHP CLI that generated the blobs. - // Distribution PHP headers need not live under the PHP prefix. - $this->getOpcodeBuildExtensionArgs(); - if (!preg_match('/^8\.(4|5)\./', $this->opcodeBuildPhpVersion, $versionMatch)) { - throw new \RuntimeException( - "Unsupported opcode decoder PHP version: {$this->opcodeBuildPhpVersion}", - ); - } - $decoder = 'opcode_unserialize_8' . $versionMatch[1] . '.c'; - $sourceFiles[] = $this->getPhpxDir() . '/thirdparty/opcache/' . $decoder; - } + return $this->composeNanoRuntimeSources($sourceFiles); + } + if ($this->isBuildModeEmbed()) { + array_push($sourceFiles, ...$this->getEmbeddedRuntimeSources()); } - if (!$this->isNanoMode() - && $this->isBuildModeBin() + if ($this->isBuildModeBin() && !$this->isWasiTarget() && !$this->isIosTarget()) { $sourceFiles[] = $this->getPhpxDir() . '/src/misc/php_cli_process_title.c'; $sourceFiles[] = $this->getPhpxDir() . '/src/misc/ps_title.c'; } + return $sourceFiles; + } + /** @return list */ + private function getEmbeddedRuntimeSources(): array + { + $sources = []; + $runtimeSource = $this->getPhpxDir() . '/src/misc/typephp_runtime.cc'; + // PHPX 2.6.3 keeps the common runtime in typephp_main.cc. Newer PHPX + // versions split it out so the object can be shared across projects. + if (is_file($runtimeSource)) { + $sources[] = $runtimeSource; + } + $sources[] = $this->getPhpxDir() . '/src/misc/typephp_main.cc'; + if ($this->embeddedFiles !== [] || $this->embeddedOpcodeFiles !== []) { + $sources[] = $this->getPhpxDir() . '/src/misc/typephp_opcode_table.cc'; + $sources[] = $this->getEmbeddedOpcodeDecoderSource(); + } + return $sources; + } + + private function getEmbeddedOpcodeDecoderSource(): string + { + // Select the decoder for the PHP CLI that generated the blobs. + // Distribution PHP headers need not live under the PHP prefix. + $this->getOpcodeBuildExtensionArgs(); + if (!preg_match('/^8\.(4|5)\./', $this->opcodeBuildPhpVersion, $versionMatch)) { + throw new \RuntimeException( + "Unsupported opcode decoder PHP version: {$this->opcodeBuildPhpVersion}", + ); + } + return $this->getPhpxDir() . '/thirdparty/opcache/opcode_unserialize_8' + . $versionMatch[1] . '.c'; + } + + private function prepareNativeCompilationEnvironment(): void + { if (!$this->isNanoMode()) { $this->preparePhpXPrecompiledHeader(); } - - // Windows: compile the resource file (icon, version info, etc.) $this->compileResourceFile(); + } - if ($job <= 1) { + /** @param list $sourceFiles @return list */ + private function dispatchNativeCompilation(array $sourceFiles): array + { + if ($this->maxJob <= 1) { return $this->compileSourceFile($sourceFiles); } if (function_exists('proc_open') && function_exists('proc_get_status')) { - return $this->compileWithProcessPool($sourceFiles, $job); + return $this->compileWithProcessPool($sourceFiles, $this->maxJob); } $this->climate->warning(