#!/usr/bin/env php */ $readList = static function (string $path): array { $entries = []; foreach (file($path, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES) as $line) { $line = trim($line); if ($line !== '' && !str_starts_with($line, '#')) { $entries[] = $line; } } return $entries; }; $targets = $readList(__DIR__ . '/targets.list'); $exemptions = $readList(__DIR__ . '/guard-exemptions.list'); // Hand-written dual-mode classes (php/runtime/) resolve through the same worker autoloader as // the generated stubs; the generator must know them so they can serve as stub base classes and // so a targets.list entry can never shadow one. $runtimeDir = $repoRoot . '/crates/shirabe-php-rpc/php/runtime'; $runtimeProvided = []; $runtimeFiles = []; if (is_dir($runtimeDir)) { $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($runtimeDir, FilesystemIterator::SKIP_DOTS) ); foreach ($iterator as $entry) { if ($entry->isFile() && str_ends_with($entry->getFilename(), '.php')) { $relative = substr($entry->getPathname(), strlen($runtimeDir) + 1); $runtimeFiles[] = $relative; $runtimeProvided[] = str_replace('/', '\\', substr($relative, 0, -strlen('.php'))); } } } sort($runtimeProvided); sort($runtimeFiles); $problems = []; $report = Report::load($reportPath); $project = new Project($composerRoot); // Every class the Rust side owns and neither a stub nor a runtime class shadows gets a guard, so // that the worker cannot fall through to the real implementation and run a second instance. // Exempt entries stay real because the worker has a mechanism that makes a native instance // correct; each one is justified where it is listed. $exempt = array_flip($exemptions); $guardTargets = []; foreach ($report->inCategories(['rust-proxy', 'rust-snapshot', 'unsupported']) as $fqcn) { if (in_array($fqcn, $targets, true) || in_array($fqcn, $runtimeProvided, true)) { continue; } if (isset($exempt[$fqcn])) { unset($exempt[$fqcn]); continue; } if ($report->kind($fqcn) !== 'class') { $problems[] = "$fqcn is a {$report->kind($fqcn)} in a Rust-owned category; a guard can only" . ' shadow a class'; continue; } $guardTargets[] = $fqcn; } foreach (array_keys($exempt) as $fqcn) { $problems[] = "stale guard exemption in guard-exemptions.list: $fqcn needs no guard"; } try { $generator = new Generator($composerRoot, $report, $targets, $runtimeProvided, $exemptions); $files = $generator->generate(); $guardGenerator = new GuardGenerator( $project, new NamePrinter(), $guardTargets, array_merge($targets, $runtimeProvided), ); $guardFiles = $guardGenerator->generate(); } catch (GenerationError $e) { foreach ($e->errors as $error) { fwrite(STDERR, "error: $error\n"); } exit(1); } // Handoff classification of Composer\Console\Application's declared properties. The worker-side // runtime definition (php/runtime/Composer/Console/Application.php) hands off exactly the state // a plugin-visible application exposes; a property the upstream class declares and this table // does not know means the handoff decision has not been made, and generation fails so a // Composer version bump surfaces it explicitly instead of silently dropping state. $applicationPropertyTable = [ 'composer' => 'handoff', // carried by __shirabe_console_application_boot 'io' => 'handoff', // the IO seam proxy 'initialWorkingDirectory' => 'handoff', // copied once at boot 'disablePluginsByDefault' => 'handoff', // copied once at boot 'disableScriptsByDefault' => 'handoff', // copied once at boot 'hasPluginCommands' => 'rust-local', // Rust-side runtime state, never shared 'logo' => 'static-config', // static rendering data, the worker never needs it ]; $applicationFile = $project->sourceFor('Composer\\Console\\Application'); $declaredProperties = []; foreach ($applicationFile->classLike->getProperties() as $property) { foreach ($property->props as $prop) { $declaredProperties[$prop->name->toString()] = true; } } foreach ($declaredProperties as $name => $_) { if (!isset($applicationPropertyTable[$name])) { $problems[] = "Composer\\Console\\Application::\$$name is not classified in the handoff property table in generate-stubs"; } } foreach ($applicationPropertyTable as $name => $_) { if (!isset($declaredProperties[$name])) { $problems[] = "the handoff property table in generate-stubs classifies Composer\\Console\\Application::\$$name, which the class no longer declares"; } } $generatedSets = [ [$stubsDir, $files, 'targets.list'], [$guardsDir, $guardFiles, 'the classifier report'], ]; foreach ($generatedSets as [$dir, $generated, $source]) { if (!is_dir($dir)) { continue; } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($dir, FilesystemIterator::SKIP_DOTS) ); foreach ($iterator as $entry) { if ($entry->isFile() && str_ends_with($entry->getFilename(), '.php')) { $relative = substr($entry->getPathname(), strlen($dir) + 1); if (!isset($generated[$relative])) { $problems[] = "stale file not covered by $source: $dir/$relative"; } } } } // The Rust side embeds every stub with include_str!; the two lists must not drift apart. The // whitespace is squeezed out first because rustfmt wraps a long include_str! across lines. $libRsText = preg_replace('/\s+/', '', (string) file_get_contents($libRs)); foreach ($files as $relative => $_) { if (!str_contains($libRsText, "include_str!(\"../php/stubs/$relative\")")) { $problems[] = "STUB_FILES in $libRs does not embed $relative"; } } foreach ($runtimeFiles as $relative) { if (!str_contains($libRsText, "include_str!(\"../php/runtime/$relative\")")) { $problems[] = "RUNTIME_FILES in $libRs does not embed $relative"; } } foreach ([[$stubsDir, $files, 'stubs'], [$guardsDir, $guardFiles, 'guards']] as [$dir, $generated, $what]) { if ($check) { foreach ($generated as $relative => $content) { $current = @file_get_contents("$dir/$relative"); if ($current === false) { $problems[] = "missing file (regenerate): $dir/$relative"; } elseif ($current !== $content) { $problems[] = "stale file (regenerate): $dir/$relative"; } } continue; } foreach ($generated as $relative => $content) { $path = "$dir/$relative"; if (!is_dir(dirname($path))) { mkdir(dirname($path), 0777, true); } file_put_contents($path, $content); } fwrite(STDERR, count($generated) . " $what written to $dir\n"); } foreach ($problems as $problem) { fwrite(STDERR, "error: $problem\n"); } exit($problems === [] ? 0 : 1);