parent
5c3da4e406
commit
4216f21b02
4 changed files with 688 additions and 271 deletions
@ -0,0 +1,144 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/aot/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Tests\Build; |
||||||
|
|
||||||
|
use PHPUnit\Framework\TestCase; |
||||||
|
use TypePhp\Build\EmbeddedOpcodeGenerator; |
||||||
|
|
||||||
|
/** |
||||||
|
* @internal |
||||||
|
* @coversNothing |
||||||
|
*/ |
||||||
|
final class EmbeddedOpcodeGeneratorTest extends TestCase |
||||||
|
{ |
||||||
|
private string $directory; |
||||||
|
private string $buildDirectory; |
||||||
|
private string $vendorFile; |
||||||
|
private string $anonymousFile; |
||||||
|
private string $ordinaryFile; |
||||||
|
|
||||||
|
protected function setUp(): void |
||||||
|
{ |
||||||
|
$this->directory = sys_get_temp_dir() . '/typephp-opcode-generator-' . bin2hex(random_bytes(6)); |
||||||
|
$this->buildDirectory = $this->directory . '/build'; |
||||||
|
$vendor = $this->directory . '/vendor'; |
||||||
|
mkdir($vendor . '/package', 0777, true); |
||||||
|
file_put_contents($vendor . '/autoload.php', '<?php'); |
||||||
|
$this->vendorFile = $vendor . '/package/library.php'; |
||||||
|
$this->anonymousFile = $this->directory . '/anonymous.php'; |
||||||
|
$this->ordinaryFile = $this->directory . '/ordinary.php'; |
||||||
|
file_put_contents($this->vendorFile, '<?php return 1;'); |
||||||
|
file_put_contents($this->anonymousFile, '<?php return new class {};'); |
||||||
|
file_put_contents($this->ordinaryFile, '<?php return 2;'); |
||||||
|
} |
||||||
|
|
||||||
|
protected function tearDown(): void |
||||||
|
{ |
||||||
|
$iterator = new \RecursiveIteratorIterator( |
||||||
|
new \RecursiveDirectoryIterator($this->directory, \FilesystemIterator::SKIP_DOTS), |
||||||
|
\RecursiveIteratorIterator::CHILD_FIRST, |
||||||
|
); |
||||||
|
foreach ($iterator as $entry) { |
||||||
|
$entry->isDir() ? rmdir($entry->getPathname()) : unlink($entry->getPathname()); |
||||||
|
} |
||||||
|
rmdir($this->directory); |
||||||
|
} |
||||||
|
|
||||||
|
public function testPrepareReusesOnlyVendorAndAnonymousCaches(): void |
||||||
|
{ |
||||||
|
$files = [$this->vendorFile, $this->anonymousFile, $this->ordinaryFile]; |
||||||
|
$generator = $this->generator(); |
||||||
|
$first = $generator->prepare($files, [$this->anonymousFile => '@typephp:anon:test']); |
||||||
|
|
||||||
|
self::assertSame($files, $first->pendingFiles()); |
||||||
|
self::assertTrue($first->isVendorFile($this->vendorFile)); |
||||||
|
self::assertTrue($first->isAnonymousFile($this->anonymousFile)); |
||||||
|
self::assertNotSame( |
||||||
|
$first->cacheDirectory($this->vendorFile), |
||||||
|
$first->cacheDirectory($this->anonymousFile), |
||||||
|
); |
||||||
|
|
||||||
|
$vendorBlob = $this->writeBlob($first->cacheDirectory($this->vendorFile), $this->vendorFile, 'vendor'); |
||||||
|
$anonymousBlob = $this->writeBlob( |
||||||
|
$first->cacheDirectory($this->anonymousFile), |
||||||
|
$this->anonymousFile, |
||||||
|
'anonymous', |
||||||
|
); |
||||||
|
$ordinaryBlob = $this->writeBlob( |
||||||
|
$first->cacheDirectory($this->ordinaryFile), |
||||||
|
$this->ordinaryFile, |
||||||
|
'ordinary', |
||||||
|
); |
||||||
|
|
||||||
|
$second = $generator->prepare($files, [$this->anonymousFile => '@typephp:anon:test']); |
||||||
|
|
||||||
|
self::assertSame([ |
||||||
|
$this->vendorFile => $vendorBlob, |
||||||
|
$this->anonymousFile => $anonymousBlob, |
||||||
|
], $second->cachedBlobs()); |
||||||
|
self::assertSame([$this->ordinaryFile], $second->pendingFiles()); |
||||||
|
self::assertSame(1, $second->vendorHits()); |
||||||
|
self::assertSame(1, $second->anonymousHits()); |
||||||
|
self::assertFileDoesNotExist($ordinaryBlob); |
||||||
|
self::assertFileExists($this->buildDirectory . '/cache/opcache/compile-embedded-opcodes.php'); |
||||||
|
} |
||||||
|
|
||||||
|
public function testForceClearsPersistentOpcodeCaches(): void |
||||||
|
{ |
||||||
|
$files = [$this->vendorFile, $this->anonymousFile]; |
||||||
|
$anonymousKeys = [$this->anonymousFile => '@typephp:anon:test']; |
||||||
|
$first = $this->generator()->prepare($files, $anonymousKeys); |
||||||
|
$this->writeBlob($first->cacheDirectory($this->vendorFile), $this->vendorFile, 'vendor'); |
||||||
|
$this->writeBlob($first->cacheDirectory($this->anonymousFile), $this->anonymousFile, 'anonymous'); |
||||||
|
|
||||||
|
$forced = $this->generator(true)->prepare($files, $anonymousKeys); |
||||||
|
|
||||||
|
self::assertSame([], $forced->cachedBlobs()); |
||||||
|
self::assertSame($files, $forced->pendingFiles()); |
||||||
|
} |
||||||
|
|
||||||
|
public function testVendorSkipMarkerAvoidsRepeatedCompilation(): void |
||||||
|
{ |
||||||
|
$generator = $this->generator(); |
||||||
|
$first = $generator->prepare([$this->vendorFile], []); |
||||||
|
$cacheDirectory = $first->cacheDirectory($this->vendorFile); |
||||||
|
mkdir($cacheDirectory, 0777, true); |
||||||
|
file_put_contents( |
||||||
|
$cacheDirectory . '/skipped-' . hash('sha256', $this->vendorFile) . '.txt', |
||||||
|
$this->vendorFile, |
||||||
|
); |
||||||
|
|
||||||
|
$second = $generator->prepare([$this->vendorFile], []); |
||||||
|
|
||||||
|
self::assertSame([], $second->pendingFiles()); |
||||||
|
self::assertSame(1, $second->skippedVendorCount()); |
||||||
|
self::assertSame(0, $second->vendorHits()); |
||||||
|
} |
||||||
|
|
||||||
|
private function generator(bool $force = false): EmbeddedOpcodeGenerator |
||||||
|
{ |
||||||
|
return new EmbeddedOpcodeGenerator( |
||||||
|
$this->buildDirectory, |
||||||
|
'app', |
||||||
|
PHP_BINARY, |
||||||
|
[], |
||||||
|
PHP_VERSION, |
||||||
|
'build-signature', |
||||||
|
$force, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private function writeBlob(string $cacheDirectory, string $source, string $contents): string |
||||||
|
{ |
||||||
|
$blob = $cacheDirectory . '/system' . $source . '.bin'; |
||||||
|
mkdir(dirname($blob), 0777, true); |
||||||
|
file_put_contents($blob, $contents); |
||||||
|
return $blob; |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,88 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/aot/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Build; |
||||||
|
|
||||||
|
final readonly class EmbeddedOpcodeBatch |
||||||
|
{ |
||||||
|
/** |
||||||
|
* @param array<string, string> $cachedBlobs |
||||||
|
* @param list<string> $pendingFiles |
||||||
|
* @param array<string, string> $cacheDirectories |
||||||
|
* @param array<string, true> $vendorFiles |
||||||
|
* @param array<string, true> $anonymousFiles |
||||||
|
* @param array<string, true> $skippedVendorFiles |
||||||
|
*/ |
||||||
|
public function __construct( |
||||||
|
private array $cachedBlobs, |
||||||
|
private array $pendingFiles, |
||||||
|
private array $cacheDirectories, |
||||||
|
private array $vendorFiles, |
||||||
|
private array $anonymousFiles, |
||||||
|
private array $skippedVendorFiles, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
/** @return array<string, string> */ |
||||||
|
public function cachedBlobs(): array |
||||||
|
{ |
||||||
|
return $this->cachedBlobs; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return list<string> */ |
||||||
|
public function pendingFiles(): array |
||||||
|
{ |
||||||
|
return $this->pendingFiles; |
||||||
|
} |
||||||
|
|
||||||
|
public function pendingCount(): int |
||||||
|
{ |
||||||
|
return count($this->pendingFiles); |
||||||
|
} |
||||||
|
|
||||||
|
public function cacheDirectory(string $file): string |
||||||
|
{ |
||||||
|
return $this->cacheDirectories[$file] |
||||||
|
?? throw new \LogicException("Missing OPcache directory for: {$file}"); |
||||||
|
} |
||||||
|
|
||||||
|
public function isVendorFile(string $file): bool |
||||||
|
{ |
||||||
|
return isset($this->vendorFiles[$file]); |
||||||
|
} |
||||||
|
|
||||||
|
public function isAnonymousFile(string $file): bool |
||||||
|
{ |
||||||
|
return isset($this->anonymousFiles[$file]); |
||||||
|
} |
||||||
|
|
||||||
|
public function vendorHits(): int |
||||||
|
{ |
||||||
|
return count(array_intersect_key($this->cachedBlobs, $this->vendorFiles)); |
||||||
|
} |
||||||
|
|
||||||
|
public function anonymousHits(): int |
||||||
|
{ |
||||||
|
return count(array_intersect_key($this->cachedBlobs, $this->anonymousFiles)); |
||||||
|
} |
||||||
|
|
||||||
|
public function vendorCount(): int |
||||||
|
{ |
||||||
|
return count($this->vendorFiles); |
||||||
|
} |
||||||
|
|
||||||
|
public function anonymousCount(): int |
||||||
|
{ |
||||||
|
return count($this->anonymousFiles); |
||||||
|
} |
||||||
|
|
||||||
|
public function skippedVendorCount(): int |
||||||
|
{ |
||||||
|
return count($this->skippedVendorFiles); |
||||||
|
} |
||||||
|
} |
||||||
@ -0,0 +1,362 @@ |
|||||||
|
<?php |
||||||
|
/** |
||||||
|
* This file is part of TypePHP(AOT). |
||||||
|
* |
||||||
|
* @link https://www.swoole.com/aot/ |
||||||
|
* @contact service@swoole.com |
||||||
|
*/ |
||||||
|
|
||||||
|
namespace TypePhp\Build; |
||||||
|
|
||||||
|
final class EmbeddedOpcodeGenerator |
||||||
|
{ |
||||||
|
private const string DRIVER_SOURCE = <<<'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; |
||||||
|
|
||||||
|
/** @param list<string> $extensionArguments */ |
||||||
|
public function __construct( |
||||||
|
private readonly string $buildDirectory, |
||||||
|
private readonly string $targetName, |
||||||
|
private readonly string $php, |
||||||
|
private readonly array $extensionArguments, |
||||||
|
private readonly string $phpVersion, |
||||||
|
private readonly string $buildSignature, |
||||||
|
private readonly bool $force, |
||||||
|
) { |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Resolve persistent cache hits and produce an immutable compilation batch. |
||||||
|
* |
||||||
|
* @param list<string> $files |
||||||
|
* @param array<string, string> $anonymousKeys |
||||||
|
*/ |
||||||
|
public function prepare(array $files, array $anonymousKeys): EmbeddedOpcodeBatch |
||||||
|
{ |
||||||
|
$cacheDirectories = []; |
||||||
|
$vendorGroups = []; |
||||||
|
$vendorFiles = []; |
||||||
|
$anonymousFiles = []; |
||||||
|
$otherFiles = []; |
||||||
|
|
||||||
|
foreach ($files as $file) { |
||||||
|
if (isset($anonymousKeys[$file])) { |
||||||
|
$cacheDirectory = $this->anonymousCacheDirectory($file); |
||||||
|
if ($this->force) { |
||||||
|
$this->clearDirectory($cacheDirectory); |
||||||
|
} |
||||||
|
$cacheDirectories[$file] = $cacheDirectory; |
||||||
|
$anonymousFiles[$file] = true; |
||||||
|
continue; |
||||||
|
} |
||||||
|
|
||||||
|
$vendorRoot = $this->vendorRoot($file); |
||||||
|
if ($vendorRoot === null) { |
||||||
|
$otherFiles[] = $file; |
||||||
|
continue; |
||||||
|
} |
||||||
|
$vendorGroups[$vendorRoot][] = $file; |
||||||
|
$vendorFiles[$file] = true; |
||||||
|
} |
||||||
|
|
||||||
|
foreach ($vendorGroups as $vendorRoot => $vendorGroup) { |
||||||
|
$cacheDirectory = $this->vendorCacheDirectory($vendorRoot); |
||||||
|
if ($this->force) { |
||||||
|
$this->clearDirectory($cacheDirectory); |
||||||
|
} |
||||||
|
foreach ($vendorGroup as $file) { |
||||||
|
$cacheDirectories[$file] = $cacheDirectory; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
if ($otherFiles !== []) { |
||||||
|
// Ordinary directories have no reliable invalidation boundary. |
||||||
|
// Recreate their shared staging directory for every build. |
||||||
|
$cacheDirectory = $this->buildDirectory . '/cache/opcache/nonvendor-' |
||||||
|
. substr(hash('sha256', $this->targetName), 0, 20); |
||||||
|
$this->clearDirectory($cacheDirectory); |
||||||
|
foreach ($otherFiles as $file) { |
||||||
|
$cacheDirectories[$file] = $cacheDirectory; |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
$this->writeDriver(); |
||||||
|
return $this->resolve($files, $cacheDirectories, $vendorFiles, $anonymousFiles); |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* Compile only the cache misses from a prepared batch. |
||||||
|
* |
||||||
|
* @param callable(string): void|null $onSkipped |
||||||
|
* @param callable(int, int, string): void|null $onProgress |
||||||
|
* @return array<string, string> source file => OPcache blob |
||||||
|
*/ |
||||||
|
public function compile( |
||||||
|
EmbeddedOpcodeBatch $batch, |
||||||
|
?callable $onSkipped = null, |
||||||
|
?callable $onProgress = null, |
||||||
|
): array { |
||||||
|
$blobs = $batch->cachedBlobs(); |
||||||
|
$total = $batch->pendingCount(); |
||||||
|
foreach ($batch->pendingFiles() as $index => $file) { |
||||||
|
$cacheDirectory = $batch->cacheDirectory($file); |
||||||
|
$this->ensureDirectory($cacheDirectory); |
||||||
|
[$status, $stdout, $stderr] = $this->compileFile($file, $cacheDirectory); |
||||||
|
|
||||||
|
if ($status !== 0) { |
||||||
|
if ($batch->isAnonymousFile($file) |
||||||
|
|| !str_contains($stderr, "OPcache could not compile: {$file}")) { |
||||||
|
throw new \RuntimeException("Cannot compile embedded opcode {$file}:\n{$stderr}"); |
||||||
|
} |
||||||
|
if ($batch->isVendorFile($file)) { |
||||||
|
AtomicFile::write($this->skippedMarker($cacheDirectory, $file), $file . PHP_EOL, '.skip-'); |
||||||
|
} |
||||||
|
if ($onSkipped !== null) { |
||||||
|
$onSkipped($file); |
||||||
|
} |
||||||
|
} else { |
||||||
|
// Compiled files may emit PHP deprecation notices after the |
||||||
|
// version line printed by the driver. |
||||||
|
if (strtok($stdout, "\r\n") !== $this->phpVersion) { |
||||||
|
throw new \RuntimeException("Build PHP version changed while compiling {$file}"); |
||||||
|
} |
||||||
|
$blob = $this->findBlob($cacheDirectory, $file); |
||||||
|
if ($blob === null) { |
||||||
|
throw new \RuntimeException("Missing OPcache blob for {$file}"); |
||||||
|
} |
||||||
|
$blobs[$file] = $blob; |
||||||
|
} |
||||||
|
|
||||||
|
if ($onProgress !== null) { |
||||||
|
$onProgress($index + 1, $total, $file); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
uksort($blobs, static fn (string $left, string $right): int => strcmp($left, $right)); |
||||||
|
return $blobs; |
||||||
|
} |
||||||
|
|
||||||
|
/** |
||||||
|
* @param list<string> $files |
||||||
|
* @param array<string, string> $cacheDirectories |
||||||
|
* @param array<string, true> $vendorFiles |
||||||
|
* @param array<string, true> $anonymousFiles |
||||||
|
*/ |
||||||
|
private function resolve( |
||||||
|
array $files, |
||||||
|
array $cacheDirectories, |
||||||
|
array $vendorFiles, |
||||||
|
array $anonymousFiles, |
||||||
|
): EmbeddedOpcodeBatch { |
||||||
|
$cachedBlobs = []; |
||||||
|
$pendingFiles = []; |
||||||
|
$skippedVendorFiles = []; |
||||||
|
foreach ($files as $file) { |
||||||
|
$cacheDirectory = $cacheDirectories[$file]; |
||||||
|
if (isset($vendorFiles[$file]) |
||||||
|
&& is_file($this->skippedMarker($cacheDirectory, $file))) { |
||||||
|
$skippedVendorFiles[$file] = true; |
||||||
|
continue; |
||||||
|
} |
||||||
|
if ((isset($vendorFiles[$file]) || isset($anonymousFiles[$file])) |
||||||
|
&& ($blob = $this->findBlob($cacheDirectory, $file)) !== null) { |
||||||
|
$cachedBlobs[$file] = $blob; |
||||||
|
continue; |
||||||
|
} |
||||||
|
$pendingFiles[] = $file; |
||||||
|
} |
||||||
|
|
||||||
|
return new EmbeddedOpcodeBatch( |
||||||
|
$cachedBlobs, |
||||||
|
$pendingFiles, |
||||||
|
$cacheDirectories, |
||||||
|
$vendorFiles, |
||||||
|
$anonymousFiles, |
||||||
|
$skippedVendorFiles, |
||||||
|
); |
||||||
|
} |
||||||
|
|
||||||
|
private function anonymousCacheDirectory(string $file): string |
||||||
|
{ |
||||||
|
$fileHash = hash_file('sha256', $file); |
||||||
|
if (!is_string($fileHash)) { |
||||||
|
throw new \RuntimeException("Cannot hash anonymous class source: {$file}"); |
||||||
|
} |
||||||
|
$key = hash('sha256', implode("\n", [ |
||||||
|
'anonymous-opcodes-v1', |
||||||
|
$file, |
||||||
|
$fileHash, |
||||||
|
$this->buildSignature, |
||||||
|
])); |
||||||
|
return $this->buildDirectory . '/cache/opcache/anonymous-' . substr($key, 0, 20); |
||||||
|
} |
||||||
|
|
||||||
|
private function vendorCacheDirectory(string $vendorRoot): string |
||||||
|
{ |
||||||
|
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->buildSignature, |
||||||
|
])); |
||||||
|
return $this->buildDirectory . '/cache/opcache/vendor-' . substr($key, 0, 20); |
||||||
|
} |
||||||
|
|
||||||
|
/** Only files under a vendor directory with autoload.php use the persistent cache. */ |
||||||
|
private function vendorRoot(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; |
||||||
|
} |
||||||
|
|
||||||
|
/** @return array{int, string, string} */ |
||||||
|
private function compileFile(string $file, string $cacheDirectory): array |
||||||
|
{ |
||||||
|
$command = [ |
||||||
|
$this->php, |
||||||
|
'-n', |
||||||
|
...$this->extensionArguments, |
||||||
|
'-d', |
||||||
|
'opcache.enable_cli=1', |
||||||
|
'-d', |
||||||
|
'opcache.file_cache_only=1', |
||||||
|
'-d', |
||||||
|
'opcache.file_update_protection=0', |
||||||
|
'-d', |
||||||
|
'opcache.file_cache=' . $cacheDirectory, |
||||||
|
$this->driverPath(), |
||||||
|
$file, |
||||||
|
]; |
||||||
|
$process = proc_open($command, [ |
||||||
|
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]); |
||||||
|
return [proc_close($process), (string) $stdout, (string) $stderr]; |
||||||
|
} |
||||||
|
|
||||||
|
private function writeDriver(): void |
||||||
|
{ |
||||||
|
$path = $this->driverPath(); |
||||||
|
if (is_file($path) && file_get_contents($path) === self::DRIVER_SOURCE) { |
||||||
|
return; |
||||||
|
} |
||||||
|
AtomicFile::write($path, self::DRIVER_SOURCE, '.driver-'); |
||||||
|
} |
||||||
|
|
||||||
|
private function driverPath(): string |
||||||
|
{ |
||||||
|
return $this->buildDirectory . '/cache/opcache/compile-embedded-opcodes.php'; |
||||||
|
} |
||||||
|
|
||||||
|
private function skippedMarker(string $cacheDirectory, string $file): string |
||||||
|
{ |
||||||
|
return $cacheDirectory . '/skipped-' . hash('sha256', $file) . '.txt'; |
||||||
|
} |
||||||
|
|
||||||
|
private function findBlob(string $cacheDirectory, string $file): ?string |
||||||
|
{ |
||||||
|
$matches = []; |
||||||
|
foreach (glob($cacheDirectory . '/*', 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]; |
||||||
|
} |
||||||
|
if (DIRECTORY_SEPARATOR !== '\\' || !is_dir($cacheDirectory)) { |
||||||
|
return null; |
||||||
|
} |
||||||
|
|
||||||
|
// OPcache omits the drive colon from Windows cache paths (C:\foo |
||||||
|
// becomes C\foo) after two system-specific cache directories. |
||||||
|
$cacheSuffix = str_replace('/', '\\', preg_replace('/^([A-Za-z]):/', '$1', $file)) . '.bin'; |
||||||
|
foreach (glob($cacheDirectory . '/*', GLOB_ONLYDIR) ?: [] as $systemDirectory) { |
||||||
|
foreach (glob($systemDirectory . '/*', GLOB_ONLYDIR) ?: [] as $subDirectory) { |
||||||
|
$candidate = $subDirectory . DIRECTORY_SEPARATOR . $cacheSuffix; |
||||||
|
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]; |
||||||
|
} |
||||||
|
|
||||||
|
// Retain compatibility with PHP builds that use another number of |
||||||
|
// system-specific directories. |
||||||
|
$iterator = new \RecursiveIteratorIterator( |
||||||
|
new \RecursiveDirectoryIterator($cacheDirectory, \FilesystemIterator::SKIP_DOTS), |
||||||
|
); |
||||||
|
foreach ($iterator as $cached) { |
||||||
|
if ($cached->isFile() && $cached->getSize() > 0 |
||||||
|
&& str_ends_with($cached->getPathname(), $cacheSuffix)) { |
||||||
|
if ($matches !== []) { |
||||||
|
throw new \RuntimeException("Ambiguous OPcache blob for {$file}"); |
||||||
|
} |
||||||
|
$matches[] = $cached->getPathname(); |
||||||
|
} |
||||||
|
} |
||||||
|
return $matches[0] ?? null; |
||||||
|
} |
||||||
|
|
||||||
|
private function clearDirectory(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}"); |
||||||
|
} |
||||||
|
} |
||||||
|
|
||||||
|
private function ensureDirectory(string $directory): void |
||||||
|
{ |
||||||
|
if (!is_dir($directory) && !mkdir($directory, 0777, true) && !is_dir($directory)) { |
||||||
|
throw new \RuntimeException("Cannot create OPcache build directory: {$directory}"); |
||||||
|
} |
||||||
|
} |
||||||
|
} |
||||||
Loading…
Reference in new issue