@ -495,8 +495,10 @@ class Translator extends Preprocessor
public function save(string $code, string $file): void
public function save(string $code, string $file): void
{
{
$this->writeFile($file, $code);
$written = $this->writeFile($file, $code);
$this->formatCppCode($file);
if ($written) {
$this->formatCppCode($file);
}
}
}
public function convertFile(string $file): string
public function convertFile(string $file): string
@ -1095,8 +1097,9 @@ CODE;
$this->indentLevel--;
$this->indentLevel--;
$this->writeFile($file, $code);
if ($this->writeFile($file, $code)) {
$this->formatCppCode($file);
$this->formatCppCode($file);
}
$this->localHeaders = [];
$this->localHeaders = [];
return $file;
return $file;
}
}
@ -1191,6 +1194,116 @@ CODE;
}
}
}
}
/**
* Check whether a compiler-generated .cc file has a valid object cache.
* Mirrors hasMiscObjectFileCache() but applies to generated translation
* units, whose compiled output depends on the generated headers they
* include (func_decl.h / data_decl.h / arginfo / extension).
*
* The cache key incorporates the compile command, the PHP ABI, and the
* content of every generated header, so any change to the generated
* surface (including headers another file changed) invalidates it.
*/
public function hasGeneratedObjectFileCache(string $cppFile): bool
{
if ($this->climate->arguments->defined('force') || $this->enableProfiler) {
return false;
}
$objectFile = $this->getObjectFile($cppFile);
if (!is_file($objectFile)) {
return false;
}
$metadataFile = $this->getMiscObjectCacheMetadataFile($objectFile);
if (!is_file($metadataFile)) {
return false;
}
$cachedKey = file_get_contents($metadataFile);
if ($cachedKey === false || trim($cachedKey) !== $this->getGeneratedObjectCacheKey($cppFile, $objectFile)) {
return false;
}
$objectMtime = filemtime($objectFile);
if ($objectMtime < = filemtime($cppFile)) {
return false;
}
foreach ($this->getGeneratedHeaderDependencies() as $header) {
if (is_file($header) & & filemtime($header) > $objectMtime) {
return false;
}
}
return true;
}
/**
* @return list< string > generated headers that every generated .cc depends on.
*
* Every generated translation unit includes the project func_decl.h and
* data_decl.h (plus the phpx headers handled by the misc cache). The
* per-file _arginfo.h headers are included by the extension unit, so they
* are part of the shared surface too. extension-< target > .cc is NOT listed:
* its content tracks any class change and would invalidate every .cc,
* defeating incremental builds. It is itself a generated unit and gets its
* own cache entry via isGeneratedSourceFile().
*/
protected function getGeneratedHeaderDependencies(): array
{
$includeDir = $this->getIncludeDir();
$headers = [
$includeDir . '/php_' . $this->targetName . '_func_decl.h',
$includeDir . '/php_' . $this->targetName . '_data_decl.h',
];
if (is_dir($includeDir)) {
$iterator = new \FilesystemIterator($includeDir, \FilesystemIterator::SKIP_DOTS);
foreach ($iterator as $entry) {
if ($entry->isFile() & & str_ends_with($entry->getFilename(), '_arginfo.h')) {
$headers[] = $entry->getPathname();
}
}
}
sort($headers, SORT_STRING);
return $headers;
}
protected function getGeneratedObjectCacheKey(string $sourceFile, string $objectFile): string
{
$abi = [
'php_version_id' => PHP_VERSION_ID,
'php_api_version' => defined('PHP_API_VERSION') ? constant('PHP_API_VERSION') : null,
'zend_module_api' => defined('ZEND_MODULE_API_NO') ? constant('ZEND_MODULE_API_NO') : null,
'php_zts' => defined('PHP_ZTS') ? PHP_ZTS : null,
'php_debug' => defined('PHP_DEBUG') ? PHP_DEBUG : null,
'integer_size' => PHP_INT_SIZE,
];
$context = hash_init('sha256');
hash_update($context, $this->buildCompileFileCommand($sourceFile, $objectFile) . "\0" . serialize($abi));
foreach ($this->getGeneratedHeaderDependencies() as $header) {
hash_update($context, "\0" . $header . "\0");
if (is_file($header)) {
hash_update_file($context, $header);
}
}
return hash_final($context);
}
protected function writeGeneratedObjectCacheMetadata(string $sourceFile, string $objectFile): void
{
$metadataFile = $this->getMiscObjectCacheMetadataFile($objectFile);
if (file_put_contents($metadataFile, $this->getGeneratedObjectCacheKey($sourceFile, $objectFile) . PHP_EOL) === false) {
throw new \RuntimeException('Cannot write generated object cache metadata: ' . $metadataFile);
}
}
protected function invalidateGeneratedObjectCache(string $objectFile): void
{
$this->invalidateMiscObjectCache($objectFile);
}
public function isPhpxMiscFile(string $cppFile): bool
public function isPhpxMiscFile(string $cppFile): bool
{
{
$miscDir = $this->getPhpxDir() . '/src/misc/';
$miscDir = $this->getPhpxDir() . '/src/misc/';
@ -1235,16 +1348,21 @@ CODE;
public function compileFile(string $cppFile, string $objectFile, bool $parallel = false): void
public function compileFile(string $cppFile, string $objectFile, bool $parallel = false): void
{
{
if ($this->isPhpxMiscFile($cppFile) & & $this->hasMiscObjectFileCache($cppFile)) {
$isMiscFile = $this->isPhpxMiscFile($cppFile);
$isGenerated = $this->isGeneratedSourceFile($cppFile);
if (($isMiscFile & & $this->hasMiscObjectFileCache($cppFile))
|| ($isGenerated & & $this->hasGeneratedObjectFileCache($cppFile))) {
if (!$parallel) {
if (!$parallel) {
$this->climate->darkGray('[cache] skip: ' . $cppFile);
$this->climate->darkGray('[cache] skip: ' . $cppFile);
}
}
return;
return;
}
}
$isMiscFile = $this->isPhpxMiscFile($cppFile);
if ($isMiscFile) {
if ($isMiscFile) {
$this->invalidateMiscObjectCache($objectFile);
$this->invalidateMiscObjectCache($objectFile);
} elseif ($isGenerated) {
$this->invalidateGeneratedObjectCache($objectFile);
}
}
$language = $this->getLanguageFromExtension($cppFile);
$language = $this->getLanguageFromExtension($cppFile);
@ -1268,7 +1386,23 @@ CODE;
if ($isMiscFile) {
if ($isMiscFile) {
$this->writeMiscObjectCacheMetadata($cppFile, $objectFile);
$this->writeMiscObjectCacheMetadata($cppFile, $objectFile);
} elseif ($isGenerated) {
$this->writeGeneratedObjectCacheMetadata($cppFile, $objectFile);
}
}
/**
* True for compiler-generated .cc translation units (as opposed to phpx
* misc sources and user-provided native sources). These live under the
* build directory and are safe to cache by generated-header fingerprint.
*/
protected function isGeneratedSourceFile(string $cppFile): bool
{
if ($this->isPhpxMiscFile($cppFile) || !$this->isCppFile($cppFile)) {
return false;
}
}
$buildDir = rtrim($this->getBuildDir(), '/\\') . DIRECTORY_SEPARATOR;
return str_starts_with($cppFile, $buildDir);
}
}
protected function buildCompileFileCommand(string $sourceFile, string $objectFile): string
protected function buildCompileFileCommand(string $sourceFile, string $objectFile): string
@ -2322,7 +2456,7 @@ CODE;
{
{
$this->climate->info('convert: ' . $this->getRelativePath($this->file));
$this->climate->info('convert: ' . $this->getRelativePath($this->file));
$ast = $this->parser->parse ($phpCode);
$ast = $this->parseCachedAst ($phpCode);
$traverser = new NodeTraverser();
$traverser = new NodeTraverser();
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new NameResolver(null, ['replaceNodes' => false]));
$traverser->addVisitor(new Visitor(sourceFile: $this->file));
$traverser->addVisitor(new Visitor(sourceFile: $this->file));