aboutsummaryrefslogtreecommitdiffhomepage
path: root/scripts
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-05 03:58:03 +0900
committernsfisis <nsfisis@gmail.com>2026-08-05 03:58:03 +0900
commit9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec (patch)
treeb1041b55bc3ed36a391370cc2f5ececc8cb386b2 /scripts
parentbe3458128ef09b3d1074b134f2822162e077e165 (diff)
downloadphp-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.tar.gz
php-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.tar.zst
php-shirabe-9b444a9a879b75a6af3d3c7ba8b9a4294574c3ec.zip
feat(plugin): run plugin-provided commands in a worker-side application
A same-FQCN Composer\Console\Application, hand-written under the new php/runtime/ tree, hosts CommandProvider commands inside the PHP worker: PhpCommandProxy overrides run() and forwards the stringified input, so the real Symfony machinery binds, validates and executes against the live command object, while help/list render Rust-side from a definition read back at construction. Reverse \Shirabe\RustCommandStub rows let a plugin command invoke built-in commands back in the Rust process, keeping every command on the side whose helper set it was written for. Composer\EventDispatcher\Event moves from a generated stub to a dual-mode runtime class: the real BaseCommand::initialize constructs a PreCommandRunEvent natively in the worker, which a proxy-only constructor guard rejected. Its PRE_COMMAND_RUN dispatch reaches a new EventDispatcher stub whose dispatch supports the observably-no-op no-listener case and fails explicitly otherwise. The stub generator now accepts runtime-provided classes as stub bases (never as targets) and cross-checks the Application handoff property table against the real class. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'scripts')
-rwxr-xr-xscripts/plugin-stub-generator/generate-stubs61
-rw-r--r--scripts/plugin-stub-generator/src/Generator.php67
-rw-r--r--scripts/plugin-stub-generator/targets.list2
3 files changed, 125 insertions, 5 deletions
diff --git a/scripts/plugin-stub-generator/generate-stubs b/scripts/plugin-stub-generator/generate-stubs
index c6b03c97..934577f3 100755
--- a/scripts/plugin-stub-generator/generate-stubs
+++ b/scripts/plugin-stub-generator/generate-stubs
@@ -7,6 +7,7 @@ require __DIR__ . '/vendor/autoload.php';
use Shirabe\PluginStubGenerator\GenerationError;
use Shirabe\PluginStubGenerator\Generator;
+use Shirabe\PluginStubGenerator\Project;
use Shirabe\PluginStubGenerator\Report;
$repoRoot = dirname(__DIR__, 2);
@@ -39,8 +40,29 @@ foreach (file(__DIR__ . '/targets.list', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY
}
}
+// 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);
+
try {
- $generator = new Generator($composerRoot, Report::load($reportPath), $targets);
+ $generator = new Generator($composerRoot, Report::load($reportPath), $targets, $runtimeProvided);
$files = $generator->generate();
} catch (GenerationError $e) {
foreach ($e->errors as $error) {
@@ -51,6 +73,38 @@ try {
$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)
);
@@ -70,6 +124,11 @@ foreach ($files as $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) {
diff --git a/scripts/plugin-stub-generator/src/Generator.php b/scripts/plugin-stub-generator/src/Generator.php
index b7cfc859..1e41eed8 100644
--- a/scripts/plugin-stub-generator/src/Generator.php
+++ b/scripts/plugin-stub-generator/src/Generator.php
@@ -93,22 +93,40 @@ final class Generator
*/
private array $surfaces = [];
- /** @param list<string> $targets */
+ /** @var array<string, true> */
+ private array $runtimeSet = [];
+
+ /**
+ * @param list<string> $targets
+ * @param list<string> $runtimeProvided FQCNs of the hand-written dual-mode classes under
+ * php/runtime/; they may serve as stub base classes
+ * but must never be generation targets themselves
+ */
public function __construct(
string $composerRoot,
private readonly Report $report,
private readonly array $targets,
+ private readonly array $runtimeProvided = [],
) {
$this->project = new Project($composerRoot);
$this->printer = new NamePrinter();
foreach ($targets as $fqcn) {
$this->targetSet[$fqcn] = true;
}
+ foreach ($runtimeProvided as $fqcn) {
+ $this->runtimeSet[$fqcn] = true;
+ }
}
/** @return array<string, string> relative stub path => file content */
public function generate(): array
{
+ foreach ($this->runtimeProvided as $fqcn) {
+ if (isset($this->targetSet[$fqcn])) {
+ $this->errors[] = "$fqcn is both a stub target and provided by php/runtime/;"
+ . ' the runtime definition would be shadowed by the generated stub';
+ }
+ }
$files = [];
foreach ($this->targets as $fqcn) {
$files[str_replace('\\', '/', $fqcn) . '.php'] = $this->emitClass($fqcn);
@@ -119,6 +137,45 @@ final class Generator
return $files;
}
+ /**
+ * The stub surface a runtime-provided (hand-written, dual-mode) base class exposes,
+ * computed from the real Composer class the runtime file mirrors, so a generated subclass
+ * stub can omit the methods it inherits — the same records emitClass builds for generated
+ * parents.
+ *
+ * @return array<string, list<array{string, bool, string, bool, bool}>>
+ */
+ private function surfaceFromRealClass(string $fqcn): array
+ {
+ $file = $this->project->sourceFor($fqcn);
+ $class = $file->classLike;
+ if (!$class instanceof Class_) {
+ $this->errors[] = "$fqcn is not a class";
+ return [];
+ }
+ $surface = [];
+ $parentFqcn = $class->extends === null ? null : $this->resolvedName($class->extends);
+ if ($parentFqcn !== null) {
+ $surface = $this->surfaces[$parentFqcn] ?? $this->surfaceFromRealClass($parentFqcn);
+ }
+ foreach ($this->interfaceClosure($class) as $interface) {
+ foreach ($interface->classLike->getMethods() as $method) {
+ if ($method->isStatic()) {
+ continue;
+ }
+ $surface[$method->name->toString()] ??= $this->fingerprint($method, $file);
+ }
+ }
+ foreach ($class->getMethods() as $method) {
+ $name = $method->name->toString();
+ if ($method->isStatic() || !$method->isPublic() || str_starts_with($name, '__')) {
+ continue;
+ }
+ $surface[$name] = $this->fingerprint($method, $file);
+ }
+ return $surface;
+ }
+
private function emitClass(string $fqcn): string
{
$file = $this->project->sourceFor($fqcn);
@@ -136,8 +193,12 @@ final class Generator
$parentFqcn = $class->extends === null ? null : $this->resolvedName($class->extends);
$isRoot = $parentFqcn === null;
if ($parentFqcn !== null && !isset($this->targetSet[$parentFqcn])) {
- $this->errors[] = "$fqcn extends $parentFqcn, which is not a stub target";
- $isRoot = true;
+ if (isset($this->runtimeSet[$parentFqcn])) {
+ $this->surfaces[$parentFqcn] ??= $this->surfaceFromRealClass($parentFqcn);
+ } else {
+ $this->errors[] = "$fqcn extends $parentFqcn, which is not a stub target";
+ $isRoot = true;
+ }
}
if (!$isRoot && !isset($this->surfaces[$parentFqcn])) {
$this->errors[] = "$fqcn must come after its base class $parentFqcn in targets.list";
diff --git a/scripts/plugin-stub-generator/targets.list b/scripts/plugin-stub-generator/targets.list
index 59656c6a..17f14615 100644
--- a/scripts/plugin-stub-generator/targets.list
+++ b/scripts/plugin-stub-generator/targets.list
@@ -2,7 +2,7 @@
# line; the output path is derived from the FQCN. Every entry must be classified as
# rust-proxy or contract in the classifier report, and stub base classes must precede
# their subclasses.
-Composer\EventDispatcher\Event
+Composer\EventDispatcher\EventDispatcher
Composer\Script\Event
Composer\PartialComposer
Composer\Composer