feat(compiler): implement embedded opcode compilation for bundled files

- Replace resolveComposerAutoloader function with direct vendor/autoload.php path
- Add anonymous class opcode embedding functionality with conditional logic
- Implement bundled-files feature for packaging vendor dependencies
- Create object caching system under build/cache directory structure
- Add OPcache integration for generating embedded opcode blobs
- Support vendor directory caching with mtime-based invalidation
- Add progress reporting for opcode generation process
- Move legacy build cache to new cache directory structure
- Update documentation for bundled-files configuration option
- Add error handling for opcode build environment requirements
master
韩天峰 3 weeks ago
parent b9c42eed1f
commit 3f159a8c05
  1. 22
      README-CN.md
  2. 28
      README.md
  3. 3
      project.yml
  4. 1
      src/Build/IncrementalCompilationTrait.php
  5. 3
      src/Build/NativeCommandOptionsTrait.php
  6. 550
      src/Build/SourcePipelineTrait.php
  7. 22
      src/Build/TranslationUnitSplitTrait.php
  8. 21
      src/CompilerBase.php
  9. 66
      src/Preprocessor.php
  10. 82
      src/Translator.php
  11. 39
      src/compiler.php

@ -304,6 +304,10 @@ sources:
- path: src/windows - path: src/windows
if: PHP_OS_FAMILY == "Windows" if: PHP_OS_FAMILY == "Windows"
# 将文件打包到二进制,并为 ZendVM 提供字节码及内存文件读取。
bundled-files:
- vendor
# 由项目自身的原生构建流程预编译。 # 由项目自身的原生构建流程预编译。
objects: objects:
- native/build/startup.o - native/build/startup.o
@ -333,6 +337,19 @@ ext-deps:
`PHP_VERSION`、`PHP_VERSION_ID` 和 `PHP_OS_FAMILY`。命令行参数优先于 YAML `PHP_VERSION`、`PHP_VERSION_ID` 和 `PHP_OS_FAMILY`。命令行参数优先于 YAML
中的同名配置。原生链接依赖应写入 `link-libs`;`ext-deps` 会生成 中的同名配置。原生链接依赖应写入 `link-libs`;`ext-deps` 会生成
`ZEND_MOD_REQUIRED`,缺少所需 PHP 扩展时由 Zend 拒绝加载模块。 `ZEND_MOD_REQUIRED`,缺少所需 PHP 扩展时由 Zend 拒绝加载模块。
`bundled-files` 支持与 `sources` 相同的文件、目录及条件写法,仅在显式配置时启用。
所列文件全部打包进二进制;未通过 `sources` 成功原生编译的 PHP 文件由 OPcache
生成字节码。运行时的 `require` 和 `require_once` 从内存交给 ZendVM 执行,
无需读取磁盘上的 PHP 文件。构建字节码的 PHP CLI、OPcache 与目标 PHP 运行时需匹配。
运行二进制文件时无需 Composer 安装、磁盘 vendor 文件或 OPcache 扩展;Composer
自动加载文件也已嵌入,仍按需加载类。构建时有 OPcache 的情况下,匿名类在首次执行
对应 `new class` 表达式时从同一字节码表加载。未配置 `bundled-files` 且缺少
OPcache 时,匿名类退回内嵌 PHP 代码方式。
仅当目录名为 `vendor` 且目录下存在 `autoload.php` 时,才会使用字节码缓存;
目录 mtime 及构建用的 PHP/OPcache 未变化时复用字节码。其他 `bundled-files`
文件(包括缺少 `autoload.php` 的同名目录)每次构建都重新生成。
修改目录下已有文件不会更新目录 mtime;这种情况可用 `--force` 重新生成
vendor 字节码。
通用的 `objects` 列表会把已有 `.o`/`.obj` 文件直接加入链接步骤。TypePHP 通用的 `objects` 列表会把已有 `.o`/`.obj` 文件直接加入链接步骤。TypePHP
不会重新编译这些文件;原生编译器、目标架构、编译参数和增量构建均由项目负责。 不会重新编译这些文件;原生编译器、目标架构、编译参数和增量构建均由项目负责。
使用目标通用参数的原生文件仍应放入 `sources`;仅当某个编译单元需要不同参数且 使用目标通用参数的原生文件仍应放入 `sources`;仅当某个编译单元需要不同参数且
@ -341,8 +358,9 @@ ext-deps:
项目级 `cxx-flags`、`c-flags`、`asm-flags` 和 `ld-flags` 分别应用于 项目级 `cxx-flags`、`c-flags`、`asm-flags` 和 `ld-flags` 分别应用于
C++、C、汇编和链接命令。 C++、C、汇编和链接命令。
构建目录保存生成的 C++、依赖对象和预编译头缓存。复用同一个构建目录可以显著加快 构建目录中的可读 C++ 和头文件与内部产物分开存放。对象文件、opcode 字节码、
增量构建;仅在确实需要重编 PHPX 公共对象时使用 `--force`。 二进制归档、清单、链接响应文件和预编译头均放在 `build-dir/cache` 下。复用同一个
构建目录可以显著加快增量构建;仅在确实需要重编 PHPX 公共对象时使用 `--force`。
全部项目配置项及命令行优先级详见[编译器命令行](docs/zh-cn/COMPILER_CLI.md)。 全部项目配置项及命令行优先级详见[编译器命令行](docs/zh-cn/COMPILER_CLI.md)。

@ -336,6 +336,10 @@ sources:
- path: src/windows - path: src/windows
if: PHP_OS_FAMILY == "Windows" if: PHP_OS_FAMILY == "Windows"
# Embed files for ZendVM execution and virtual file reads.
bundled-files:
- vendor
# Precompiled by the project's external native build. # Precompiled by the project's external native build.
objects: objects:
- native/build/startup.o - native/build/startup.o
@ -366,6 +370,24 @@ directory; conditional entries support `PHP_VERSION`, `PHP_VERSION_ID`, and
`PHP_OS_FAMILY`. CLI arguments override their YAML counterparts. Native linker `PHP_OS_FAMILY`. CLI arguments override their YAML counterparts. Native linker
dependencies belong in `link-libs`; `ext-deps` writes `ZEND_MOD_REQUIRED` dependencies belong in `link-libs`; `ext-deps` writes `ZEND_MOD_REQUIRED`
entries so Zend can reject loading when a required PHP extension is missing. entries so Zend can reject loading when a required PHP extension is missing.
`bundled-files` accepts files or directories with the same conditional syntax.
It is opt-in for embedded binary builds: all listed files are packed into the
binary, and PHP files not successfully compiled from `sources` are stored as
OPcache bytecode. `require` and `require_once` load those scripts through
ZendVM without reading their PHP files from disk. The PHP CLI and OPcache used
to build the blobs must match the target PHP runtime. The resulting binary
does not need Composer installation, vendor files, or an OPcache extension at
runtime: Composer's autoload files are embedded and still resolve classes on
demand. When OPcache is available at build time, anonymous classes use the
same opcode table when their `new class` expression is first executed. Without
`bundled-files`, builds lacking OPcache use embedded PHP code for anonymous
classes instead.
Only a `vendor` directory containing `autoload.php` uses the opcode cache.
Its blobs are reused while the directory mtime and build PHP/OPcache remain
unchanged. All other `bundled-files` files, including files in a `vendor`
directory without `autoload.php`, are regenerated on every build. Directory
mtime does not change when an existing nested file is edited; use `--force`
to regenerate vendor opcodes in that case.
The generic `objects` list adds existing `.o`/`.obj` files directly to the The generic `objects` list adds existing `.o`/`.obj` files directly to the
link step. TypePHP never recompiles these files; the project owns their native link step. TypePHP never recompiles these files; the project owns their native
compiler, architecture, flags, and incremental build. Keep native files that compiler, architecture, flags, and incremental build. Keep native files that
@ -376,8 +398,10 @@ post-link packaging step after tpc emits its ELF.
Project-wide `cxx-flags`, `c-flags`, `asm-flags`, and `ld-flags` are applied to Project-wide `cxx-flags`, `c-flags`, `asm-flags`, and `ld-flags` are applied to
C++, C, assembler, and link commands respectively. C++, C, assembler, and link commands respectively.
The build directory contains generated C++, dependency objects, and the The build directory keeps readable generated C++ and headers separate from
precompiled-header cache. Reusing it makes incremental builds much faster; internal artifacts. Objects, opcode blobs, binary archives, manifests, linker
response files, and precompiled headers live under `build-dir/cache`. Reusing
the build directory makes incremental builds much faster;
use `--force` only when the reusable PHPX objects must be rebuilt. use `--force` only when the reusable PHPX objects must be rebuilt.
See [Compiler CLI](docs/en/COMPILER_CLI.md) for all project keys and command-line See [Compiler CLI](docs/en/COMPILER_CLI.md) for all project keys and command-line

@ -35,6 +35,9 @@ sources:
- vendor/nikic/php-parser/lib/PhpParser/NodeVisitor.php - vendor/nikic/php-parser/lib/PhpParser/NodeVisitor.php
- vendor/nikic/php-parser/lib/PhpParser/NodeVisitorAbstract.php - vendor/nikic/php-parser/lib/PhpParser/NodeVisitorAbstract.php
bundled-files:
- ./vendor
ignore: ignore:
- ./src/Assert.php - ./src/Assert.php
- ./src/polyfills.php - ./src/polyfills.php

@ -365,6 +365,7 @@ trait IncrementalCompilationTrait
'platform' => $this->targetPlatform, 'platform' => $this->targetPlatform,
'literalStrings' => !$this->noLiteralStrings, 'literalStrings' => !$this->noLiteralStrings,
'debug' => $this->debug, 'debug' => $this->debug,
'anonymousOpcode' => $this->canEmbedAnonymousClassOpcode(),
])); ]));
// New self-hosted compiler builds embed their immutable source snapshot. // New self-hosted compiler builds embed their immutable source snapshot.

@ -187,7 +187,8 @@ trait NativeCommandOptionsTrait
'sanitize' => $this->sanitize, 'sanitize' => $this->sanitize,
'lto' => $this->enableLto, 'lto' => $this->enableLto,
'target_platform' => $targetPlatform, 'target_platform' => $targetPlatform,
'response_file' => $this->getBuildDir() . DIRECTORY_SEPARATOR 'response_file' => $this->getBuildDir() . DIRECTORY_SEPARATOR . 'cache'
. DIRECTORY_SEPARATOR . 'link' . DIRECTORY_SEPARATOR
. basename($this->getTargetFileName()) . '.rsp', . basename($this->getTargetFileName()) . '.rsp',
]; ];

@ -8,6 +8,8 @@
namespace TypePhp\Build; namespace TypePhp\Build;
use Ajaxray\AnsiKit\AnsiTerminal;
use Ajaxray\AnsiKit\Components\Progressbar;
use TypePhp\Backend\CompilerFactory; use TypePhp\Backend\CompilerFactory;
use TypePhp\Exception\SyntaxError; use TypePhp\Exception\SyntaxError;
use TypePhp\Exception\Unsupported; use TypePhp\Exception\Unsupported;
@ -20,6 +22,30 @@ use TypePhp\Platform\Windows;
trait SourcePipelineTrait trait SourcePipelineTrait
{ {
use PreparedProjectCacheTrait; use PreparedProjectCacheTrait;
/** @var list<string> PHP files selected by bundled-files. */
private array $bundledPhpFiles = [];
/** @var list<string> All regular files selected by bundled-files. */
private array $bundledFiles = [];
/** @var list<string> Selected PHP files not translated to native code. */
private array $embeddedOpcodeFiles = [];
/** @var list<string> Anonymous classes generated from the current source file. */
private array $currentAnonymousFiles = [];
/** @var array<string, string> Build-time PHP path to runtime anonymous-class key. */
private array $anonymousOpcodeKeys = [];
/** @var ?list<string> Zend extension arguments for the build PHP CLI. */
private ?array $opcodeBuildExtensionArgs = null;
private bool $opcodeBuildChecked = false;
private string $opcodeBuildProbeError = '';
private string $opcodeBuildPhpVersion = '';
private string $opcodeBuildSignature = '';
/** /**
* Prepare PHP inputs for the Composer php-nano source-composition build. * Prepare PHP inputs for the Composer php-nano source-composition build.
* *
@ -100,6 +126,481 @@ trait SourcePipelineTrait
$this->sourceDirs = array_merge($this->sourceDirs, $files); $this->sourceDirs = array_merge($this->sourceDirs, $files);
} }
protected function embedAnonymousClassCode(string $name, string $code): string
{
$path = $this->getBuildDir() . '/cache/anonymous/source/anonymous-' . $this->targetName . '-'
. substr(hash('sha256', $this->file), 0, 12) . '-' . $name . '.php';
$this->writeFile($path, "<?php\n" . $code . "\n");
$this->embeddedOpcodeFiles[] = $path;
$this->anonymousOpcodeKeys[$path] = $this->anonymousOpcodeKey($path);
$this->currentAnonymousFiles[] = $path;
return $this->anonymousOpcodeKeys[$path];
}
private function anonymousOpcodeKey(string $path): string
{
return '@typephp:anon:' . substr(hash('sha256', $path), 0, 32);
}
private function anonymousManifestPath(string $source): string
{
return $this->getBuildDir() . '/cache/anonymous/manifests/'
. substr(hash('sha256', $source), 0, 20) . '.json';
}
private function moveLegacyBuildCache(string $legacy, string $current): void
{
if (!file_exists($legacy)) {
return;
}
$destination = $current;
if (file_exists($destination)) {
$base = $this->getBuildDir() . '/cache/legacy/' . basename($legacy);
$destination = $base;
for ($suffix = 1; file_exists($destination); ++$suffix) {
$destination = $base . '-' . $suffix;
}
}
$directory = dirname($destination);
if (!is_dir($directory) && !mkdir($directory, 0777, true) && !is_dir($directory)) {
throw new \RuntimeException("Cannot create build cache directory: {$directory}");
}
if (!rename($legacy, $destination)) {
throw new \RuntimeException("Cannot move build cache into {$directory}: {$legacy}");
}
}
private function restoreAnonymousManifest(string $source): void
{
$manifest = $this->anonymousManifestPath($source);
if (!is_file($manifest)) {
return;
}
$paths = json_decode((string) file_get_contents($manifest), true);
if (!is_array($paths)) {
throw new \RuntimeException("Invalid anonymous class manifest: {$manifest}");
}
foreach ($paths as $path) {
if (!is_string($path) || !is_file($path)) {
throw new \RuntimeException("Missing anonymous class source: {$path}");
}
$this->embeddedOpcodeFiles[] = $path;
$this->anonymousOpcodeKeys[$path] = $this->anonymousOpcodeKey($path);
}
}
/** Check whether the target PHP CLI can generate OPcache file-cache blobs. */
private function probeOpcodeBuildExtensionArgs(): ?array
{
if ($this->opcodeBuildChecked) {
return $this->opcodeBuildExtensionArgs;
}
$this->opcodeBuildChecked = true;
$php = $this->getPhpDir() . '/bin/php';
if (!is_file($php) || !is_executable($php)) {
$this->opcodeBuildProbeError = "Build PHP CLI is not executable: {$php}";
return null;
}
foreach ([[], ['-d', 'zend_extension=opcache']] as $extensionArgs) {
$process = proc_open(
[
$php, '-n', ...$extensionArgs, '-d', 'opcache.enable_cli=1',
'-r', 'if (!extension_loaded("Zend OPcache") || !function_exists("opcache_compile_file")) exit(1); '
. '$extension = ini_get("extension_dir") . DIRECTORY_SEPARATOR . "opcache." . PHP_SHLIB_SUFFIX; '
. 'echo json_encode(["php" => PHP_VERSION, "opcache" => phpversion("Zend OPcache"), '
. '"binary" => [PHP_BINARY, hash_file("sha256", PHP_BINARY)], '
. '"extension" => [$extension, is_file($extension) ? hash_file("sha256", $extension) : null], '
. '"zts" => PHP_ZTS, "debug" => PHP_DEBUG, "int_size" => PHP_INT_SIZE]);',
],
[0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w']],
$pipes,
);
if (!is_resource($process)) {
$this->opcodeBuildProbeError = "Cannot start the build PHP CLI: {$php}";
return null;
}
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
if (proc_close($process) === 0) {
$signature = json_decode((string) $stdout, true);
if (!is_array($signature) || !is_string($signature['php'] ?? null)) {
throw new \RuntimeException("Invalid OPcache build PHP signature from {$php}");
}
$this->opcodeBuildPhpVersion = $signature['php'];
$this->opcodeBuildSignature = (string) $stdout;
return $this->opcodeBuildExtensionArgs = $extensionArgs;
}
if ($stderr !== false && trim($stderr) !== '') {
$this->opcodeBuildProbeError = trim($stderr);
}
}
return null;
}
private function getOpcodeBuildExtensionArgs(): array
{
$extensionArgs = $this->probeOpcodeBuildExtensionArgs();
if ($extensionArgs !== null) {
return $extensionArgs;
}
$php = $this->getPhpDir() . '/bin/php';
$detail = $this->opcodeBuildProbeError === '' ? '' : "\n" . $this->opcodeBuildProbeError;
$this->error("Embedded opcode generation requires Zend OPcache for the build PHP CLI: {$php}{$detail}");
}
protected function canEmbedAnonymousClassOpcode(): bool
{
return $this->isBuildModeBin() && !$this->isNanoMode()
&& !$this->isIosTarget() && !$this->isAndroidTarget() && !$this->isWasiTarget()
&& $this->probeOpcodeBuildExtensionArgs() !== null;
}
private function startOpcodeProgress(string $label, int $total): ?Progressbar
{
if ($this->noProgress || $total === 0) {
return null;
}
$progress = new Progressbar();
$progress->barStyle([AnsiTerminal::FG_GREEN])
->percentageStyle([AnsiTerminal::TEXT_BOLD])
->labelStyle([AnsiTerminal::FG_CYAN]);
$progress->renderInPlace(0, $total, $label);
return $progress;
}
private function updateOpcodeProgress(
?Progressbar $progress,
string $label,
int $completed,
int $total,
?string $file = null,
): void {
if ($progress !== null) {
$progress->renderInPlace($completed, $total, $label);
} elseif ($this->noProgress && ($file !== null || $completed % 100 === 0 || $completed === $total)) {
$percent = (int) ceil($completed / $total * 100);
$detail = $file === null ? '' : " {$file}";
$this->output("[{$completed}/{$total}] {$percent}% {$label}{$detail}", 'white');
}
}
/** Only files under a vendor directory with autoload.php use the mtime cache. */
private function vendorRootForOpcodeFile(string $file): ?string
{
for ($directory = dirname($file); $directory !== dirname($directory); $directory = dirname($directory)) {
if (basename($directory) === 'vendor' && is_file($directory . '/autoload.php')) {
return $directory;
}
}
return null;
}
private function findOpcodeBlob(string $cacheDir, string $file): ?string
{
$matches = [];
foreach (glob($cacheDir . '/*', GLOB_ONLYDIR) ?: [] as $systemDirectory) {
$candidate = $systemDirectory . $file . '.bin';
if (is_file($candidate) && filesize($candidate) > 0) {
$matches[] = $candidate;
}
}
if (count($matches) > 1) {
throw new \RuntimeException("Ambiguous OPcache blob for {$file}");
}
if ($matches !== []) {
return $matches[0];
}
// OPcache uses a different file-cache path layout on Windows.
if (DIRECTORY_SEPARATOR === '\\' && is_dir($cacheDir)) {
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($cacheDir, \FilesystemIterator::SKIP_DOTS),
);
foreach ($iterator as $cached) {
if ($cached->isFile() && $cached->getSize() > 0
&& str_ends_with($cached->getPathname(), $file . '.bin')) {
if ($matches !== []) {
throw new \RuntimeException("Ambiguous OPcache blob for {$file}");
}
$matches[] = $cached->getPathname();
}
}
}
return $matches[0] ?? null;
}
private function clearOpcodeCacheDirectory(string $directory): void
{
if (!is_dir($directory)) {
return;
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS),
\RecursiveIteratorIterator::CHILD_FIRST,
);
foreach ($iterator as $entry) {
$path = $entry->getPathname();
if ($entry->isDir() ? !rmdir($path) : !unlink($path)) {
throw new \RuntimeException("Cannot clear opcode build directory: {$path}");
}
}
if (!rmdir($directory)) {
throw new \RuntimeException("Cannot clear opcode build directory: {$directory}");
}
}
/** Embed OPcache's file-cache bytes for scripts left to ZendVM. */
private function genEmbeddedOpcodeTable(): array
{
$output = $this->getBuildDir() . '/embedded-opcodes-' . $this->targetName . '.cc';
foreach (glob($this->getBuildDir() . '/opcache-*') ?: [] as $legacyCache) {
if (is_dir($legacyCache)) {
$this->moveLegacyBuildCache(
$legacyCache,
$this->getBuildDir() . '/cache/opcache/' . basename($legacyCache),
);
}
}
$files = array_values(array_unique($this->embeddedOpcodeFiles));
sort($files, SORT_STRING);
$blobs = [];
$phpVersion = PHP_VERSION;
if ($files !== []) {
if ($this->isIosTarget() || $this->isAndroidTarget() || $this->isWasiTarget()) {
throw new \RuntimeException('Embedded opcodes require a native PHP build host matching the target');
}
$this->output('Generating embedded opcodes for ' . count($files) . ' PHP files', 'lightBlue');
$php = $this->getPhpDir() . '/bin/php';
$extensionArgs = $this->getOpcodeBuildExtensionArgs();
$phpVersion = $this->opcodeBuildPhpVersion;
$otherFiles = [];
$vendorRoots = [];
$vendorFiles = [];
$fileCacheDirs = [];
foreach ($files as $file) {
$vendorRoot = $this->vendorRootForOpcodeFile($file);
if ($vendorRoot === null) {
$otherFiles[] = $file;
continue;
}
$vendorRoots[$vendorRoot][] = $file;
$vendorFiles[$file] = true;
}
foreach ($vendorRoots as $vendorRoot => $filesInVendor) {
clearstatcache(true, $vendorRoot);
$mtime = filemtime($vendorRoot);
if ($mtime === false) {
throw new \RuntimeException("Cannot read vendor directory mtime: {$vendorRoot}");
}
$key = hash('sha256', implode("\n", [
'vendor-directory-mtime-opcodes-v2', $vendorRoot,
(string) $mtime, $this->opcodeBuildSignature,
]));
$cacheDir = $this->getBuildDir() . '/cache/opcache/vendor-' . substr($key, 0, 20);
if ($this->climate->arguments->defined('force')) {
$this->clearOpcodeCacheDirectory($cacheDir);
}
foreach ($filesInVendor as $file) {
$fileCacheDirs[$file] = $cacheDir;
}
}
if ($otherFiles !== []) {
// Recreate this staging directory on every build so OPcache
// cannot reuse bytecode for non-vendor or anonymous sources.
$cacheDir = $this->getBuildDir() . '/cache/opcache/nonvendor-'
. substr(hash('sha256', $this->targetName), 0, 20);
$this->clearOpcodeCacheDirectory($cacheDir);
foreach ($otherFiles as $file) {
$fileCacheDirs[$file] = $cacheDir;
}
}
$driver = $this->getBuildDir() . '/cache/opcache/compile-embedded-opcodes.php';
$this->moveLegacyBuildCache(
$this->getBuildDir() . '/compile-embedded-opcodes.php',
$driver,
);
$this->writeFile($driver, <<<'PHP'
<?php
echo PHP_VERSION, "\n";
foreach (array_slice($argv, 1) as $path) {
if (!opcache_compile_file($path)) {
fwrite(STDERR, "OPcache could not compile: {$path}\n");
exit(1);
}
}
PHP
);
$command = [
$php, '-n', ...$extensionArgs,
'-d', 'opcache.enable_cli=1',
'-d', 'opcache.file_cache_only=1',
'-d', 'opcache.file_update_protection=0',
];
// OPcache installs declarations in the compiling CLI process.
// Isolate files so unrelated scripts with the same global
// function names do not conflict while creating their bytecode.
$pending = [];
$vendorHits = 0;
foreach ($files as $file) {
$cacheDir = $fileCacheDirs[$file];
if (isset($vendorFiles[$file])
&& ($blob = $this->findOpcodeBlob($cacheDir, $file)) !== null) {
$blobs[$file] = $blob;
++$vendorHits;
} else {
$pending[] = $file;
}
}
if ($vendorRoots !== []) {
$vendorCount = array_sum(array_map('count', $vendorRoots));
$this->output(
'Vendor opcode cache: ' . $vendorHits . ' reused, '
. ($vendorCount - $vendorHits) . ' to generate',
'lightBlue',
);
}
$progress = $this->startOpcodeProgress('Opcodes', count($pending));
$completed = 0;
foreach ($pending as $file) {
$cacheDir = $fileCacheDirs[$file];
if (!is_dir($cacheDir) && !mkdir($cacheDir, 0777, true) && !is_dir($cacheDir)) {
throw new \RuntimeException("Cannot create OPcache build directory: {$cacheDir}");
}
$process = proc_open([...$command, '-d', 'opcache.file_cache=' . $cacheDir, $driver, $file], [
0 => ['pipe', 'r'], 1 => ['pipe', 'w'], 2 => ['pipe', 'w'],
], $pipes);
if (!is_resource($process)) {
throw new \RuntimeException('Cannot start the PHP OPcache build process');
}
fclose($pipes[0]);
$stdout = stream_get_contents($pipes[1]);
$stderr = stream_get_contents($pipes[2]);
fclose($pipes[1]);
fclose($pipes[2]);
if (proc_close($process) !== 0) {
throw new \RuntimeException("Cannot compile embedded opcode {$file}:\n{$stderr}");
}
if (trim($stdout) !== $phpVersion) {
throw new \RuntimeException("Build PHP version changed while compiling {$file}");
}
$blob = $this->findOpcodeBlob($cacheDir, $file);
if ($blob === null) {
throw new \RuntimeException("Missing OPcache blob for {$file}");
}
$blobs[$file] = $blob;
$this->updateOpcodeProgress($progress, 'Opcodes', ++$completed, count($pending), $this->noProgress ? $file : null);
}
if ($progress !== null) {
echo PHP_EOL;
}
uksort($blobs, static fn(string $left, string $right): int => strcmp($left, $right));
}
$archive = $this->getBuildDir() . '/cache/embedded/embedded-files-' . $this->targetName . '.bin';
$this->moveLegacyBuildCache(
$this->getBuildDir() . '/embedded-files-' . $this->targetName . '.bin',
$archive,
);
$rawIndex = [];
$opcodeIndex = [];
$sources = [$output];
if ($this->bundledFiles !== [] || $files !== []) {
$temporaryArchive = $archive . '.tmp';
if (!is_dir(dirname($archive))
&& !mkdir(dirname($archive), 0777, true) && !is_dir(dirname($archive))) {
throw new \RuntimeException('Cannot create embedded archive directory: ' . dirname($archive));
}
$stream = fopen($temporaryArchive, 'wb');
if ($stream === false) {
throw new \RuntimeException("Cannot create embedded file archive: {$temporaryArchive}");
}
foreach ($this->bundledFiles as $file) {
$offset = ftell($stream);
$input = fopen($file, 'rb');
if ($input === false) {
throw new \RuntimeException("Cannot read embedded file: {$file}");
}
$length = stream_copy_to_stream($input, $stream);
fclose($input);
if ($length === false) {
throw new \RuntimeException("Cannot copy embedded file: {$file}");
}
$rawIndex[$file] = [$offset, $length];
}
foreach ($blobs as $file => $blobFile) {
$offset = ftell($stream);
$input = fopen($blobFile, 'rb');
if ($input === false) {
throw new \RuntimeException("Cannot read opcode blob: {$blobFile}");
}
$length = stream_copy_to_stream($input, $stream);
fclose($input);
if ($length === false) {
throw new \RuntimeException("Cannot copy opcode blob: {$blobFile}");
}
$opcodeIndex[$this->anonymousOpcodeKeys[$file] ?? $file] = [$offset, $length];
}
fclose($stream);
if (!is_file($archive) || hash_file('sha256', $archive) !== hash_file('sha256', $temporaryArchive)) {
rename($temporaryArchive, $archive);
} else {
unlink($temporaryArchive);
}
$assembly = $this->getBuildDir() . '/embedded-files-' . $this->targetName . '.S';
$quotedArchive = json_encode($archive, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
$archiveHash = hash_file('sha256', $archive);
$asmCode = "# archive-sha256: {$archiveHash}\n.section .rodata\n#ifdef __APPLE__\n.globl _typephp_embedded_archive_start\n.p2align 4\n_typephp_embedded_archive_start:\n#else\n.globl typephp_embedded_archive_start\n.p2align 4\ntypephp_embedded_archive_start:\n#endif\n.incbin {$quotedArchive}\n";
$this->writeFile($assembly, $asmCode);
$this->generatedProjectSources[$assembly] = true;
$sources[] = $assembly;
$this->output(
'Packed ' . count($rawIndex) . ' files and ' . count($opcodeIndex) . ' opcode blobs',
'green',
);
}
$code = '#include <typephp_opcode_table.h>' . PHP_EOL;
if ($this->bundledFiles !== [] || $files !== []) {
$code .= 'extern "C" const uint8_t typephp_embedded_archive_start[];' . PHP_EOL;
}
$code .= 'static const typephp_opcode_entry typephp_opcodes[] = {' . PHP_EOL;
foreach ($opcodeIndex as $file => [$offset, $length]) {
$path = json_encode($file, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
$code .= " {{$path}, typephp_embedded_archive_start + {$offset}, {$length}}," . PHP_EOL;
}
$code .= ' {nullptr, nullptr, 0},' . PHP_EOL . '};' . PHP_EOL;
$code .= 'extern "C" const typephp_opcode_entry *typephp_project_opcode_table(size_t *count) {' . PHP_EOL;
$code .= ' *count = ' . count($opcodeIndex) . '; return typephp_opcodes;' . PHP_EOL . '}' . PHP_EOL;
$code .= 'static const typephp_embedded_file_entry typephp_embedded_files[] = {' . PHP_EOL;
foreach ($rawIndex as $file => [$offset, $length]) {
$path = json_encode($file, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
$code .= " {{$path}, typephp_embedded_archive_start + {$offset}, {$length}}," . PHP_EOL;
}
$code .= ' {nullptr, nullptr, 0},' . PHP_EOL . '};' . PHP_EOL;
$code .= 'extern "C" const typephp_embedded_file_entry *typephp_project_embedded_file_table(size_t *count) {' . PHP_EOL;
$code .= ' *count = ' . count($rawIndex) . '; return typephp_embedded_files;' . PHP_EOL . '}' . PHP_EOL;
$version = json_encode($phpVersion, JSON_THROW_ON_ERROR);
$code .= 'extern "C" const char *typephp_project_php_version(void) { return ' . $version . '; }' . PHP_EOL;
if ($rawIndex === [] && $opcodeIndex === []) {
$code .= 'extern "C" void typephp_opcode_table_install(void) {}' . PHP_EOL;
$code .= 'extern "C" void typephp_opcode_table_uninstall(void) {}' . PHP_EOL;
}
$this->writeFile($output, $code);
$this->generatedProjectSources[$output] = true;
foreach (glob($this->getBuildDir() . '/anonymous-*.json') ?: [] as $legacyManifest) {
unlink($legacyManifest);
}
foreach (glob($this->getBuildDir() . '/anonymous-' . $this->targetName . '-*.php') ?: [] as $legacySource) {
unlink($legacySource);
}
return $sources;
}
public function getFiles(string $path): array public function getFiles(string $path): array
{ {
$this->applyPhpVersionCommandLineArgument(); $this->applyPhpVersionCommandLineArgument();
@ -134,6 +635,12 @@ trait SourcePipelineTrait
// Apply command-line arguments after all configuration is loaded (so they // Apply command-line arguments after all configuration is loaded (so they
// take the highest precedence) // take the highest precedence)
$this->applyCommandLineArguments(); $this->applyCommandLineArguments();
if ($this->bundledFiles !== [] && !$this->isBuildModeBin()) {
$this->error('`bundled-files` requires `mode: bin`');
}
if ($this->bundledFiles !== []) {
$this->getOpcodeBuildExtensionArgs();
}
if ($this->climate->arguments->defined('force')) { if ($this->climate->arguments->defined('force')) {
$this->clearIncrementalBuildCache(); $this->clearIncrementalBuildCache();
} }
@ -227,6 +734,7 @@ trait SourcePipelineTrait
$files = $this->filterIgnoredFiles($files); $files = $this->filterIgnoredFiles($files);
$preparedKey = $this->preparedProjectKey($files); $preparedKey = $this->preparedProjectKey($files);
if ($this->restorePreparedProject($preparedKey)) { if ($this->restorePreparedProject($preparedKey)) {
$this->embeddedOpcodeFiles = array_values(array_diff($this->bundledPhpFiles, $files));
$files = $this->getSortedFiles($files); $files = $this->getSortedFiles($files);
$this->initializeIncrementalCompilation($files); $this->initializeIncrementalCompilation($files);
return $files; return $files;
@ -260,6 +768,7 @@ trait SourcePipelineTrait
$this->storePreparedProject($preparedKey); $this->storePreparedProject($preparedKey);
} }
$files = $this->getSortedFiles($files); $files = $this->getSortedFiles($files);
$this->embeddedOpcodeFiles = array_values(array_diff($this->bundledPhpFiles, $files));
$this->initializeIncrementalCompilation($files); $this->initializeIncrementalCompilation($files);
return $files; return $files;
} }
@ -392,7 +901,27 @@ trait SourcePipelineTrait
try { try {
if (FileScanner::isPhpFile($file)) { if (FileScanner::isPhpFile($file)) {
$path = realpath($file) ?: $file; $path = realpath($file) ?: $file;
if (!$this->shouldRegeneratePhpFile($path)) { $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);
}
$needsAnonymousRefresh = is_file($legacyAnonymousManifest)
|| ($this->canEmbedAnonymousClassOpcode()
&& !is_file($anonymousManifest)
&& preg_match('/new\s+class\b/', (string) file_get_contents($path)) === 1);
if (!$this->shouldRegeneratePhpFile($path) && !$needsAnonymousRefresh) {
if ($this->canEmbedAnonymousClassOpcode()) {
$this->restoreAnonymousManifest($path);
}
$validSourceCount++; $validSourceCount++;
if ($this->incrementalTranslationUnitWasEmitted($path)) { if ($this->incrementalTranslationUnitWasEmitted($path)) {
$cppFile = $this->getCppFile($path); $cppFile = $this->getCppFile($path);
@ -409,12 +938,24 @@ trait SourcePipelineTrait
continue; continue;
} }
$statisticsBefore = $this->compilationStatistics->all(); $statisticsBefore = $this->compilationStatistics->all();
$this->currentAnonymousFiles = [];
// A dirty dependency means code generation must run; // A dirty dependency means code generation must run;
// it does not mean the generated bytes changed. Keep // it does not mean the generated bytes changed. Keep
// an existing translation unit's timestamp when the // an existing translation unit's timestamp when the
// output is identical, matching CMake/Ninja's restat // output is identical, matching CMake/Ninja's restat
// model and preventing needless native recompilation. // model and preventing needless native recompilation.
$cppFile = $this->convertFile($path); $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( $this->recordIncrementalConversion(
$path, $path,
$cppFile !== null, $cppFile !== null,
@ -438,6 +979,10 @@ trait SourcePipelineTrait
} catch (Unsupported $e) { } catch (Unsupported $e) {
echo ' unsupported syntax: ' . $e->getMessage() . "\n"; echo ' unsupported syntax: ' . $e->getMessage() . "\n";
echo ' skip: ' . $file . "\n"; echo ' skip: ' . $file . "\n";
if (in_array($file, $this->bundledPhpFiles, true)
&& !in_array($file, $this->embeddedOpcodeFiles, true)) {
$this->embeddedOpcodeFiles[] = $file;
}
unset($files[$k]); unset($files[$k]);
} }
} }
@ -470,6 +1015,9 @@ trait SourcePipelineTrait
// Nano keeps the ordinary statically registered Zend class/module // Nano keeps the ordinary statically registered Zend class/module
// metadata, then adds a direct native process entry beside it. // metadata, then adds a direct native process entry beside it.
$sourceFiles[] = $this->genExtension(); $sourceFiles[] = $this->genExtension();
if ($this->isBuildModeEmbed() && !$this->isNanoMode()) {
array_push($sourceFiles, ...$this->genEmbeddedOpcodeTable());
}
if ($this->isNanoMode()) { if ($this->isNanoMode()) {
$sourceFiles[] = $this->genNanoEntrypoint(); $sourceFiles[] = $this->genNanoEntrypoint();
} }

@ -12,7 +12,15 @@ trait TranslationUnitSplitTrait
protected function getSplitTranslationUnits(string $source): array protected function getSplitTranslationUnits(string $source): array
{ {
$primary = $this->getCppFile($source); $primary = $this->getCppFile($source);
$manifest = $primary . '.parts.json'; $manifest = $this->splitTranslationUnitManifestPath($primary);
$legacy = $primary . '.parts.json';
if (is_file($legacy)) {
$contents = file_get_contents($legacy);
if (!is_file($manifest) && is_string($contents) && $contents !== '[]') {
$this->writeFile($manifest, $contents);
}
unlink($legacy);
}
$parts = is_file($manifest) ? json_decode((string) file_get_contents($manifest), true) : []; $parts = is_file($manifest) ? json_decode((string) file_get_contents($manifest), true) : [];
if (!is_array($parts)) { if (!is_array($parts)) {
return []; return [];
@ -23,6 +31,11 @@ trait TranslationUnitSplitTrait
&& preg_match('/^' . preg_quote($primary, '/') . '\\.part-[0-9]+\\.cc$/D', $part) === 1)); && preg_match('/^' . preg_quote($primary, '/') . '\\.part-[0-9]+\\.cc$/D', $part) === 1));
} }
private function splitTranslationUnitManifestPath(string $primary): string
{
return $this->getBuildDir() . '/cache/parts/' . hash('sha256', $primary) . '.json';
}
private function splitLargeTranslationUnit(string $code, string $primary, bool $force): string private function splitLargeTranslationUnit(string $code, string $primary, bool $force): string
{ {
$oldParts = $this->getSplitTranslationUnits($this->file); $oldParts = $this->getSplitTranslationUnits($this->file);
@ -64,8 +77,11 @@ trait TranslationUnitSplitTrait
$parts[] = $part; $parts[] = $part;
} }
} }
if ($this->splitTranslationUnitsEnabled || $oldParts !== []) { $manifest = $this->splitTranslationUnitManifestPath($primary);
$this->writeFile($primary . '.parts.json', json_encode($parts, JSON_THROW_ON_ERROR)); if ($parts !== []) {
$this->writeFile($manifest, json_encode($parts, JSON_THROW_ON_ERROR));
} elseif (is_file($manifest)) {
unlink($manifest);
} }
foreach (array_diff($oldParts, $parts) as $obsolete) { foreach (array_diff($oldParts, $parts) as $obsolete) {
foreach ([$obsolete, $this->getObjectFile($obsolete), foreach ([$obsolete, $this->getObjectFile($obsolete),

@ -4279,15 +4279,26 @@ class CompilerBase implements PropertyAccessContext
} }
} }
$this->flattenEmbeddedClassTraits($classDef); $this->flattenEmbeddedClassTraits($classDef);
// Anonymous classes are defined by eval in the root namespace, // Embedded classes are defined in the root namespace,
// so symbols imported inside them must be converted to // so symbols imported inside them must be converted to
// fully-qualified names. // fully-qualified names.
$this->resolveAnonClassNames($classDef); $this->resolveAnonClassNames($classDef);
$this->context->beforeStmtLines[] = 'static THREAD_LOCAL bool ' . $className . '_defined = false;';
$classCode = $this->genEmbeddedCode($classDef); $classCode = $this->genEmbeddedCode($classDef);
$this->addConstData($className . '_code', $classCode); if ($this->isBuildModeBin() && !$this->isNanoMode()
$this->context->beforeStmtLines[] = 'if (!' . $className . '_defined) {' && !$this->isIosTarget() && !$this->isAndroidTarget() && !$this->isWasiTarget()
. $className . '_defined = true; php::eval((const char *)' . $className . '_code);}'; && method_exists($this, 'embedAnonymousClassCode')
&& method_exists($this, 'canEmbedAnonymousClassOpcode')
&& $this->canEmbedAnonymousClassOpcode()) {
$opcodePath = $this->embedAnonymousClassCode($className, $classCode);
$this->localHeaders[] = 'typephp_opcode_table.h';
$this->context->beforeStmtLines[] = 'typephp_opcode_table_require("'
. $this->escapeString($opcodePath) . '");';
} else {
$this->context->beforeStmtLines[] = 'static THREAD_LOCAL bool ' . $className . '_defined = false;';
$this->addConstData($className . '_code', $classCode);
$this->context->beforeStmtLines[] = 'if (!' . $className . '_defined) {'
. $className . '_defined = true; php::eval((const char *)' . $className . '_code);}';
}
$className = '\\' . $className; $className = '\\' . $className;
$cePtr = $this->getClassEntryPtr($className); $cePtr = $this->getClassEntryPtr($className);
$ctorClassName = $className; $ctorClassName = $className;

@ -342,15 +342,15 @@ class Preprocessor extends CompilerBase
$ext = $this->getPlatform()->getObjectExtension(); $ext = $this->getPlatform()->getObjectExtension();
// Composer packages are immutable build inputs. Keep their objects in // Composer packages are immutable build inputs. Keep their objects in
// the project build directory instead of writing beside vendor sources. // the project cache instead of writing beside vendor sources.
if (isset($this->nanoRuntimeSources[$cppFile])) { if (isset($this->nanoRuntimeSources[$cppFile])) {
$separator = $this->getPlatform()->getPathSeparator(); $separator = $this->getPlatform()->getPathSeparator();
$objectDir = $this->buildDir . $separator . 'nano-objects'; $name = $info['filename'] . '-' . substr(sha1($cppFile), 0, 12) . $ext;
if (!is_dir($objectDir)) { return $this->cacheObjectFile(
mkdir($objectDir, 0777, true); $this->buildDir . $separator . 'cache' . $separator . 'objects'
} . $separator . 'nano' . $separator . $name,
return $objectDir . $separator . $info['filename'] . '-' $this->buildDir . $separator . 'nano-objects' . $separator . $name,
. substr(sha1($cppFile), 0, 12) . $ext; );
} }
// Keep the same path separator as cppFile // Keep the same path separator as cppFile
@ -362,11 +362,12 @@ class Preprocessor extends CompilerBase
// other PHPX misc sources are target-independent and share their // other PHPX misc sources are target-independent and share their
// cached object files within the build directory. // cached object files within the build directory.
$cacheScope = $this->isProjectRuntimeEntryFile($cppFile) ? $this->targetName : 'shared'; $cacheScope = $this->isProjectRuntimeEntryFile($cppFile) ? $this->targetName : 'shared';
$objectDir = $this->buildDir . $separator . 'phpx-misc' . $separator . $cacheScope; $name = $info['filename'] . $ext;
if (!is_dir($objectDir)) { return $this->cacheObjectFile(
mkdir($objectDir, 0777, true); $this->buildDir . $separator . 'cache' . $separator . 'objects'
} . $separator . 'phpx-misc' . $separator . $cacheScope . $separator . $name,
return $objectDir . $separator . $info['filename'] . $ext; $this->buildDir . $separator . 'phpx-misc' . $separator . $cacheScope . $separator . $name,
);
} }
$filename = $info['filename']; $filename = $info['filename'];
@ -376,7 +377,46 @@ class Preprocessor extends CompilerBase
$filename .= '.' . $info['extension']; $filename .= '.' . $info['extension'];
} }
return $info['dirname'] . $this->getPlatform()->getPathSeparator() . $filename . $ext; $separator = $this->getPlatform()->getPathSeparator();
$legacy = $info['dirname'] . $separator . $filename . $ext;
$normalizedBuildDir = rtrim(str_replace('\\', '/', $this->buildDir), '/') . '/';
if (str_starts_with($normalizedFile, $normalizedBuildDir)) {
$relative = substr($normalizedFile, strlen($normalizedBuildDir));
$relativeDir = dirname($relative);
$objectDir = $this->buildDir . $separator . 'cache' . $separator . 'objects'
. $separator . 'generated';
if ($relativeDir !== '.') {
$objectDir .= $separator . str_replace('/', $separator, $relativeDir);
}
} else {
$objectDir = $this->buildDir . $separator . 'cache' . $separator . 'objects'
. $separator . 'external' . $separator . substr(sha1($info['dirname']), 0, 16);
// A legacy object beside an external source may belong to a
// different build directory. Leave that file alone.
$legacy = null;
}
return $this->cacheObjectFile($objectDir . $separator . $filename . $ext, $legacy);
}
private function cacheObjectFile(string $object, ?string $legacy): string
{
$directory = dirname($object);
if (!is_dir($directory) && !mkdir($directory, 0777, true) && !is_dir($directory)) {
throw new \RuntimeException('Cannot create object cache directory: ' . $directory);
}
if ($legacy !== null) {
foreach (['', '.typephp-cache'] as $suffix) {
if (!is_file($legacy . $suffix)) {
continue;
}
if (is_file($object . $suffix)) {
unlink($legacy . $suffix);
} else {
rename($legacy . $suffix, $object . $suffix);
}
}
}
return $object;
} }
private function isGeneratedCppFile(string $file): bool private function isGeneratedCppFile(string $file): bool

@ -762,6 +762,9 @@ class Translator extends Preprocessor
try { try {
$cppCode = $this->doConvert($phpCode); $cppCode = $this->doConvert($phpCode);
$cppFile = $this->getCppFile($file); $cppFile = $this->getCppFile($file);
if ($this->isIncludeOnlyTranslationUnit($cppCode)) {
$cppCode = '';
}
$this->recordEmittedTypes($cppCode); $this->recordEmittedTypes($cppCode);
$cppCode = $this->splitLargeTranslationUnit($cppCode, $cppFile, $forceWrite); $cppCode = $this->splitLargeTranslationUnit($cppCode, $cppFile, $forceWrite);
if ($cppCode === '') { if ($cppCode === '') {
@ -788,6 +791,17 @@ class Translator extends Preprocessor
} }
} }
private function isIncludeOnlyTranslationUnit(string $code): bool
{
foreach (preg_split('/\R/', $code) as $line) {
$line = trim($line);
if ($line !== '' && !str_starts_with($line, '#include ')) {
return false;
}
}
return true;
}
/** /**
* Remove artifacts left by an earlier build when a PHP source no longer * Remove artifacts left by an earlier build when a PHP source no longer
* emits a C++ translation unit. Trait-only files are the common case. * emits a C++ translation unit. Trait-only files are the common case.
@ -2196,6 +2210,21 @@ CODE;
$sourceFiles[] = $runtimeSource; $sourceFiles[] = $runtimeSource;
} }
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_main.cc'; $sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_main.cc';
if ($this->bundledFiles !== [] || $this->embeddedOpcodeFiles !== []) {
$sourceFiles[] = $this->getPhpxDir() . '/src/misc/typephp_opcode_table.cc';
$versionHeader = $this->getPhpDir() . '/include/php/main/php_version.h';
$versionText = is_file($versionHeader) ? file_get_contents($versionHeader) : '';
if (!preg_match('/#define PHP_VERSION_ID\s+(\d+)/', $versionText, $versionMatch)) {
throw new \RuntimeException("Cannot determine the target PHP version from {$versionHeader}");
}
$targetPhpVersion = (int) $versionMatch[1];
$decoder = match (true) {
$targetPhpVersion >= 80400 && $targetPhpVersion < 80500 => 'opcode_unserialize_84.c',
$targetPhpVersion >= 80500 && $targetPhpVersion < 80600 => 'opcode_unserialize_85.c',
default => throw new \RuntimeException("Unsupported opcode decoder PHP version: {$targetPhpVersion}"),
};
$sourceFiles[] = $this->getPhpxDir() . '/thirdparty/opcache/' . $decoder;
}
} }
if (!$this->isNanoMode() if (!$this->isNanoMode()
@ -2525,7 +2554,8 @@ CODE;
private function getLinkCacheMetadataFile(string $targetFile): string private function getLinkCacheMetadataFile(string $targetFile): string
{ {
return $this->getBuildDir() . DIRECTORY_SEPARATOR return $this->getBuildDir() . DIRECTORY_SEPARATOR . 'cache'
. DIRECTORY_SEPARATOR . 'link' . DIRECTORY_SEPARATOR
. basename($targetFile) . '.typephp-link-cache'; . basename($targetFile) . '.typephp-link-cache';
} }
@ -2533,6 +2563,10 @@ CODE;
private function writeLinkCache(array $objectFiles, string $targetFile): void private function writeLinkCache(array $objectFiles, string $targetFile): void
{ {
$metadata = $this->getLinkCacheMetadataFile($targetFile); $metadata = $this->getLinkCacheMetadataFile($targetFile);
if (!is_dir(dirname($metadata)) && !mkdir(dirname($metadata), 0777, true)
&& !is_dir(dirname($metadata))) {
throw new \RuntimeException('Cannot create link cache directory: ' . dirname($metadata));
}
if (file_put_contents($metadata, $this->getLinkCacheKey($objectFiles, $targetFile) . PHP_EOL) === false) { if (file_put_contents($metadata, $this->getLinkCacheKey($objectFiles, $targetFile) . PHP_EOL) === false) {
throw new \RuntimeException('Cannot write link cache metadata: ' . $metadata); throw new \RuntimeException('Cannot write link cache metadata: ' . $metadata);
} }
@ -3832,6 +3866,52 @@ CODE;
$list = $this->getFilesFromDir($projectDir); $list = $this->getFilesFromDir($projectDir);
} }
// Raw files are bundled; PHP files not emitted by `sources` are also
// compiled into OPcache blobs for ZendVM execution at runtime.
if (array_key_exists('opcode-sources', $cfg)) {
$this->error('`opcode-sources` has been renamed to `bundled-files`');
}
if (array_key_exists('bundled-files', $cfg)) {
if (!is_array($cfg['bundled-files'])) {
$this->error('`bundled-files` must be an array');
}
foreach ($cfg['bundled-files'] as $entry) {
[$src, $condition] = $this->parseProjectYamlSourceEntry($entry);
if ($condition !== null && !$this->evaluateProjectYamlCondition($condition)) {
continue;
}
$resolved = $this->getAbsolutePath($src, $projectDir);
if (!$resolved) {
$this->error('Bundled file or directory does not exist: `' . $src . '`');
}
if (is_file($resolved)) {
$this->bundledFiles[] = $resolved;
continue;
}
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($resolved, \FilesystemIterator::SKIP_DOTS),
);
foreach ($iterator as $file) {
if ($file->isFile()) {
$this->bundledFiles[] = $file->getPathname();
}
}
}
$this->bundledFiles = array_values(array_unique($this->bundledFiles));
sort($this->bundledFiles, SORT_STRING);
$this->bundledPhpFiles = array_values(array_filter(
$this->bundledFiles,
static fn(string $file): bool => FileScanner::isPhpFile($file),
));
if ($this->bundledFiles !== []) {
$this->output(
'bundled-files: found ' . count($this->bundledFiles)
. ' files (' . count($this->bundledPhpFiles) . ' PHP)',
'lightBlue',
);
}
}
if (array_key_exists('optimize', $cfg)) { if (array_key_exists('optimize', $cfg)) {
$this->optimizeLevel = (int) $cfg['optimize']; $this->optimizeLevel = (int) $cfg['optimize'];
} }

@ -32,7 +32,7 @@ function main(int $argc, array $argv): void
// in bin/bootstrap.php. The AOT compiler starts here directly and therefore // in bin/bootstrap.php. The AOT compiler starts here directly and therefore
// must load the dependencies packaged alongside tpc itself. // must load the dependencies packaged alongside tpc itself.
if (!defined('TYPEPHP_PHP_SCRIPT_ENTRY')) { if (!defined('TYPEPHP_PHP_SCRIPT_ENTRY')) {
require_once resolveComposerAutoloader(); require_once dirname(__DIR__) . '/vendor/autoload.php';
} }
$completionStatus = CompletionCommand::execute($argv); $completionStatus = CompletionCommand::execute($argv);
@ -136,43 +136,6 @@ function main(int $argc, array $argv): void
} }
} }
/**
* Locate the Composer autoloader that ships with the compiler.
*
* A source checkout and a Unix-like package keep it below TYPEPHP_ROOT_PATH,
* while a packaged SDK extracts it below PHP_HOME.
*/
function resolveComposerAutoloader(): string
{
$candidates = [];
$roots = [TYPEPHP_ROOT_PATH, getenv('PHP_HOME') ?: null];
foreach ($roots as $root) {
if (!is_string($root) || $root === '') {
continue;
}
$autoloadFile = rtrim($root, '/\\') . '/vendor/autoload.php';
if (!in_array($autoloadFile, $candidates, true)) {
$candidates[] = $autoloadFile;
}
}
foreach ($candidates as $candidate) {
if (is_file($candidate)) {
return $candidate;
}
}
fwrite(STDERR, "Unable to find the Composer autoloader (vendor/autoload.php).\n");
fwrite(STDERR, "Searched in:\n");
foreach ($candidates as $candidate) {
fwrite(STDERR, " - {$candidate}\n");
}
fwrite(STDERR, "\nInstall the TypePHP dependencies with Composer first:\n");
fwrite(STDERR, " cd " . TYPEPHP_ROOT_PATH . " && composer install\n");
fwrite(STDERR, "Or point PHP_HOME at a TypePHP installation that already contains vendor/autoload.php.\n");
exit(1);
}
function shouldCompileNativeSourceProject(array $argv): bool function shouldCompileNativeSourceProject(array $argv): bool
{ {
foreach (array_slice($argv, 1) as $argument) { foreach (array_slice($argv, 1) as $argument) {

Loading…
Cancel
Save