#!/usr/bin/env php 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); try { $generator = new Generator($composerRoot, Report::load($reportPath), $targets, $runtimeProvided); $files = $generator->generate(); } catch (GenerationError $e) { foreach ($e->errors as $error) { fwrite(STDERR, "error: $error\n"); } exit(1); } $problems = []; // 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 = (new Project($composerRoot))->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"; } } $iterator = new RecursiveIteratorIterator( new RecursiveDirectoryIterator($stubsDir, FilesystemIterator::SKIP_DOTS) ); foreach ($iterator as $entry) { if ($entry->isFile() && str_ends_with($entry->getFilename(), '.php')) { $relative = substr($entry->getPathname(), strlen($stubsDir) + 1); if (!isset($files[$relative])) { $problems[] = "stale stub not covered by targets.list: $stubsDir/$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"; } } if ($check) { foreach ($files as $relative => $content) { $current = @file_get_contents("$stubsDir/$relative"); if ($current === false) { $problems[] = "missing stub (regenerate): $stubsDir/$relative"; } elseif ($current !== $content) { $problems[] = "stale stub (regenerate): $stubsDir/$relative"; } } } else { foreach ($files as $relative => $content) { $path = "$stubsDir/$relative"; if (!is_dir(dirname($path))) { mkdir(dirname($path), 0777, true); } file_put_contents($path, $content); } fwrite(STDERR, count($files) . " stubs written to $stubsDir\n"); } foreach ($problems as $problem) { fwrite(STDERR, "error: $problem\n"); } exit($problems === [] ? 0 : 1);