aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-rpc
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 /crates/shirabe-php-rpc
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 'crates/shirabe-php-rpc')
-rw-r--r--crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php142
-rw-r--r--crates/shirabe-php-rpc/php/runtime/Composer/EventDispatcher/Event.php120
-rw-r--r--crates/shirabe-php-rpc/php/runtime/Shirabe/RustCommandStub.php34
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/Event.php67
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php92
-rw-r--r--crates/shirabe-php-rpc/php/worker.php70
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs24
7 files changed, 476 insertions, 73 deletions
diff --git a/crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php b/crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php
new file mode 100644
index 00000000..50ea31b1
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/runtime/Composer/Console/Application.php
@@ -0,0 +1,142 @@
+<?php
+
+// Shirabe's own definition of Composer\Console\Application for the plugin worker. The real
+// class file is never autoloaded in this process: plugin-provided commands run here under a
+// genuine Symfony console application, and this class supplies the Composer-specific surface
+// (getIO()/getComposer()/...) backed by the Rust process over RPC. Defining the same FQCN
+// instead of subclassing the real class keeps both `instanceof` and strict `get_class()`
+// comparisons identical to upstream.
+//
+// Unlike the files under stubs/, the classes under runtime/ are hand-written: they are
+// two-world implementations with behavior of their own, not mechanical proxies, so the stub
+// generator does not manage them.
+
+namespace Composer\Console;
+
+use Composer\Composer;
+use Composer\IO\IOInterface;
+use Symfony\Component\Console\Application as BaseApplication;
+use Symfony\Component\Console\Input\InputDefinition;
+use Symfony\Component\Console\Input\InputOption;
+
+class Application extends BaseApplication
+{
+ private static bool $shirabeConstructing = false;
+
+ /** @var Composer|null */
+ private $shirabeComposer = null;
+ /** @var IOInterface */
+ private $shirabeIo;
+ /** @var string|null */
+ private $shirabeInitialWorkingDirectory = null;
+ private bool $shirabeDisablePluginsByDefault = false;
+ private bool $shirabeDisableScriptsByDefault = false;
+
+ /**
+ * Builds the worker-side application from the Rust-side handoff. $config keys:
+ * `composer` (Composer proxy stub or null), `io` (IO proxy stub),
+ * `initialWorkingDirectory` (?string), `disablePluginsByDefault` (bool),
+ * `disableScriptsByDefault` (bool), `rustCommands` (metadata rows for
+ * \Shirabe\RustCommandStub), `pluginCommands` (live command entities from the P table).
+ */
+ public static function __shirabeBoot(array $config): self
+ {
+ self::$shirabeConstructing = true;
+ try {
+ $app = new self();
+ } finally {
+ self::$shirabeConstructing = false;
+ }
+ $app->setAutoExit(false);
+ // A command failure must reach the Rust side as an RPC throw, not get rendered here.
+ $app->setCatchExceptions(false);
+ $app->shirabeComposer = $config['composer'];
+ $app->shirabeIo = $config['io'];
+ $app->shirabeInitialWorkingDirectory = $config['initialWorkingDirectory'];
+ $app->shirabeDisablePluginsByDefault = $config['disablePluginsByDefault'];
+ $app->shirabeDisableScriptsByDefault = $config['disableScriptsByDefault'];
+ foreach ($config['rustCommands'] as $meta) {
+ $app->add(new \Shirabe\RustCommandStub($meta));
+ }
+ foreach ($config['pluginCommands'] as $command) {
+ $app->add($command);
+ }
+
+ return $app;
+ }
+
+ public function __construct(string $name = 'Composer', string $version = '')
+ {
+ if (!self::$shirabeConstructing) {
+ // TODO(plugin): a plugin constructing its own Composer\Console\Application would
+ // run commands outside the Rust-side orchestration; whether and how to support
+ // that is undecided, so fail loudly instead of handing out a half-wired instance.
+ throw new \RuntimeException(
+ 'Shirabe does not support constructing Composer\Console\Application inside the plugin process yet'
+ );
+ }
+ parent::__construct($name, $version !== '' ? $version : Composer::getVersion());
+ }
+
+ public function getIO(): IOInterface
+ {
+ return $this->shirabeIo;
+ }
+
+ public function getComposer(bool $required = true, ?bool $disablePlugins = null, ?bool $disableScripts = null): ?Composer
+ {
+ if (null === $this->shirabeComposer && $required) {
+ // TODO(plugin): creating a Composer instance from scratch here would need
+ // Factory::create over RPC; the handoff currently always carries the instance the
+ // Rust side already built, so this only fires after resetComposer-style flows.
+ throw new \RuntimeException(
+ 'Shirabe cannot create a new Composer instance inside the plugin process yet'
+ );
+ }
+
+ return $this->shirabeComposer;
+ }
+
+ public function resetComposer(): void
+ {
+ // TODO(plugin): the Composer instance is owned by the Rust process; dropping only the
+ // worker-side reference would desynchronize the two worlds, so this needs a
+ // reset-and-refetch round trip that does not exist yet.
+ throw new \RuntimeException(
+ 'Shirabe does not support resetComposer() inside the plugin process yet'
+ );
+ }
+
+ /**
+ * @return string|null
+ */
+ public function getInitialWorkingDirectory()
+ {
+ return $this->shirabeInitialWorkingDirectory;
+ }
+
+ public function getDisablePluginsByDefault(): bool
+ {
+ return $this->shirabeDisablePluginsByDefault;
+ }
+
+ public function getDisableScriptsByDefault(): bool
+ {
+ return $this->shirabeDisableScriptsByDefault;
+ }
+
+ // Same as the real class: without Composer's global options in the definition, binding a
+ // forwarded command line that carries e.g. --working-dir would fail here even though the
+ // Rust side already consumed the option.
+ protected function getDefaultInputDefinition(): InputDefinition
+ {
+ $definition = parent::getDefaultInputDefinition();
+ $definition->addOption(new InputOption('--profile', null, InputOption::VALUE_NONE, 'Display timing and memory usage information'));
+ $definition->addOption(new InputOption('--no-plugins', null, InputOption::VALUE_NONE, 'Whether to disable plugins.'));
+ $definition->addOption(new InputOption('--no-scripts', null, InputOption::VALUE_NONE, 'Skips the execution of all scripts defined in composer.json file.'));
+ $definition->addOption(new InputOption('--working-dir', '-d', InputOption::VALUE_REQUIRED, 'If specified, use the given directory as working directory.'));
+ $definition->addOption(new InputOption('--no-cache', null, InputOption::VALUE_NONE, 'Prevent use of the cache'));
+
+ return $definition;
+ }
+}
diff --git a/crates/shirabe-php-rpc/php/runtime/Composer/EventDispatcher/Event.php b/crates/shirabe-php-rpc/php/runtime/Composer/EventDispatcher/Event.php
new file mode 100644
index 00000000..0d35ac55
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/runtime/Composer/EventDispatcher/Event.php
@@ -0,0 +1,120 @@
+<?php
+
+// Shirabe's own definition of Composer\EventDispatcher\Event for the plugin worker. This class
+// lives in both worlds at once: an instance revived from a Rust handle proxies every call over
+// RPC (like a generated stub), while an instance constructed natively — real Composer code in
+// this process does `new PreCommandRunEvent(...)`, whose parent constructor lands here — is a
+// faithful in-process port of the real base class. The two modes are told apart by the
+// constructor arguments; `__shirabeRustHandleDescriptor()` returns null in native mode so the
+// wire codec registers the object in the P table instead of treating it as a Rust handle.
+
+namespace Composer\EventDispatcher;
+
+class Event implements \ShirabeRustStub
+{
+ /** @var int|null Null in native mode. */
+ protected $__rhandle;
+ /** @var int */
+ protected $__epoch = 0;
+
+ /** @var string This event's name (native mode) */
+ protected $name;
+
+ /** @var string[] Arguments passed by the user, these will be forwarded to CLI script handlers (native mode) */
+ protected $args;
+
+ /** @var mixed[] Flags usable in PHP script handlers (native mode) */
+ protected $flags;
+
+ /** @var bool Whether the event should not be passed to more listeners (native mode) */
+ private $propagationStopped = false;
+
+ /**
+ * Proxy revival passes (int $rhandle, int $epoch); the real class's constructor is
+ * (string $name, array $args = [], array $flags = []).
+ */
+ public function __construct($name = null, $args = [], $flags = [])
+ {
+ if (is_int($name) && func_num_args() === 2 && is_int($args)) {
+ $this->__rhandle = $name;
+ $this->__epoch = $args;
+
+ return;
+ }
+ if (!is_string($name)) {
+ throw new \RuntimeException(
+ 'Shirabe does not support constructing ' . static::class . ' inside the plugin process without an event name'
+ );
+ }
+ $this->__rhandle = null;
+ $this->name = $name;
+ $this->args = $args;
+ $this->flags = $flags;
+ }
+
+ public function __destruct()
+ {
+ if ($this->__rhandle !== null) {
+ \ShirabeRustObjectRegistry::release($this->__rhandle);
+ }
+ }
+
+ public function __shirabeRustHandleDescriptor(): ?array
+ {
+ if ($this->__rhandle === null) {
+ return null;
+ }
+
+ return [
+ '__rhandle' => $this->__rhandle,
+ '__class' => static::class,
+ '__epoch' => $this->__epoch,
+ ];
+ }
+
+ public function getName(): string
+ {
+ if ($this->__rhandle !== null) {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getName', []);
+ }
+
+ return $this->name;
+ }
+
+ public function getArguments(): array
+ {
+ if ($this->__rhandle !== null) {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getArguments', []);
+ }
+
+ return $this->args;
+ }
+
+ public function getFlags(): array
+ {
+ if ($this->__rhandle !== null) {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getFlags', []);
+ }
+
+ return $this->flags;
+ }
+
+ public function isPropagationStopped(): bool
+ {
+ if ($this->__rhandle !== null) {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isPropagationStopped', []);
+ }
+
+ return $this->propagationStopped;
+ }
+
+ public function stopPropagation(): void
+ {
+ if ($this->__rhandle !== null) {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'stopPropagation', []);
+
+ return;
+ }
+ $this->propagationStopped = true;
+ }
+}
diff --git a/crates/shirabe-php-rpc/php/runtime/Shirabe/RustCommandStub.php b/crates/shirabe-php-rpc/php/runtime/Shirabe/RustCommandStub.php
new file mode 100644
index 00000000..23ebfc04
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/runtime/Shirabe/RustCommandStub.php
@@ -0,0 +1,34 @@
+<?php
+
+// Worker-side stand-in for a Rust-implemented (built-in) command: it carries only the list
+// metadata, and running it forwards the raw input line back to the Rust process, where the
+// real implementation executes against its own application state (helper set included). This
+// is how a plugin command that invokes a built-in command crosses back over the process
+// boundary; each command always runs on the side that owns its implementation, so a helper
+// lookup always resolves against the helper set of the world the command was written for.
+
+namespace Shirabe;
+
+use Composer\Command\BaseCommand;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class RustCommandStub extends BaseCommand
+{
+ /** @param array{name: string, description: string, aliases: list<string>, hidden: bool} $meta */
+ public function __construct(array $meta)
+ {
+ parent::__construct($meta['name']);
+ $this->setDescription($meta['description']);
+ $this->setAliases($meta['aliases']);
+ $this->setHidden($meta['hidden']);
+ // The real input definition lives on the Rust side and the raw tokens are forwarded
+ // untouched, so nothing may be validated (or consumed) here.
+ $this->ignoreValidationErrors();
+ }
+
+ public function run(InputInterface $input, OutputInterface $output): int
+ {
+ return (int) \ShirabeRpcRuntime::callRust(0, '__shirabe_run_rust_command', [$this->getName(), (string) $input]);
+ }
+}
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/Event.php b/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/Event.php
deleted file mode 100644
index fea84c75..00000000
--- a/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/Event.php
+++ /dev/null
@@ -1,67 +0,0 @@
-<?php
-
-// Generated by scripts/plugin-stub-generator; do not edit by hand.
-// Proxy stub for Composer\EventDispatcher\Event: the public surface forwards to the Rust-side entity over RPC.
-
-namespace Composer\EventDispatcher;
-
-class Event implements \ShirabeRustStub
-{
- /** @var int */
- protected $__rhandle;
- /** @var int */
- protected $__epoch;
-
- public function __construct(int $rhandle = 0, int $epoch = 0)
- {
- if (func_num_args() < 2) {
- // Constructing the class from plugin code (a common idiom for e.g. `new BufferIO()`)
- // is an open question of the plugin design; only proxy instantiation passes a
- // Rust handle. Fail with a diagnosable message instead of an ArgumentCountError.
- throw new \RuntimeException(
- 'Shirabe does not support constructing ' . static::class . ' inside the plugin process yet'
- );
- }
- $this->__rhandle = $rhandle;
- $this->__epoch = $epoch;
- }
-
- public function __destruct()
- {
- \ShirabeRustObjectRegistry::release($this->__rhandle);
- }
-
- public function __shirabeRustHandleDescriptor(): array
- {
- return [
- '__rhandle' => $this->__rhandle,
- '__class' => static::class,
- '__epoch' => $this->__epoch,
- ];
- }
-
- public function getName(): string
- {
- return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getName', []);
- }
-
- public function getArguments(): array
- {
- return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getArguments', []);
- }
-
- public function getFlags(): array
- {
- return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getFlags', []);
- }
-
- public function isPropagationStopped(): bool
- {
- return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isPropagationStopped', []);
- }
-
- public function stopPropagation(): void
- {
- \ShirabeRpcRuntime::callRust($this->__rhandle, 'stopPropagation', []);
- }
-}
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php b/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php
new file mode 100644
index 00000000..c0651081
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php
@@ -0,0 +1,92 @@
+<?php
+
+// Generated by scripts/plugin-stub-generator; do not edit by hand.
+// Proxy stub for Composer\EventDispatcher\EventDispatcher: the public surface forwards to the Rust-side entity over RPC.
+
+namespace Composer\EventDispatcher;
+
+use Composer\DependencyResolver\Transaction;
+use Composer\DependencyResolver\Operation\OperationInterface;
+use Composer\Repository\RepositoryInterface;
+
+class EventDispatcher implements \ShirabeRustStub
+{
+ /** @var int */
+ protected $__rhandle;
+ /** @var int */
+ protected $__epoch;
+
+ public function __construct(int $rhandle = 0, int $epoch = 0)
+ {
+ if (func_num_args() < 2) {
+ // Constructing the class from plugin code (a common idiom for e.g. `new BufferIO()`)
+ // is an open question of the plugin design; only proxy instantiation passes a
+ // Rust handle. Fail with a diagnosable message instead of an ArgumentCountError.
+ throw new \RuntimeException(
+ 'Shirabe does not support constructing ' . static::class . ' inside the plugin process yet'
+ );
+ }
+ $this->__rhandle = $rhandle;
+ $this->__epoch = $epoch;
+ }
+
+ public function __destruct()
+ {
+ \ShirabeRustObjectRegistry::release($this->__rhandle);
+ }
+
+ public function __shirabeRustHandleDescriptor(): array
+ {
+ return [
+ '__rhandle' => $this->__rhandle,
+ '__class' => static::class,
+ '__epoch' => $this->__epoch,
+ ];
+ }
+
+ public function setRunScripts(bool $runScripts = true): self
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'setRunScripts', [$runScripts]);
+ return $this;
+ }
+
+ public function dispatch(?string $eventName, ?Event $event = null): int
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'dispatch', [$eventName, $event]);
+ }
+
+ public function dispatchScript(string $eventName, bool $devMode = false, array $additionalArgs = [], array $flags = []): int
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'dispatchScript', [$eventName, $devMode, $additionalArgs, $flags]);
+ }
+
+ public function dispatchPackageEvent(string $eventName, bool $devMode, RepositoryInterface $localRepo, array $operations, OperationInterface $operation): int
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'dispatchPackageEvent', [$eventName, $devMode, $localRepo, $operations, $operation]);
+ }
+
+ public function dispatchInstallerEvent(string $eventName, bool $devMode, bool $executeOperations, Transaction $transaction): int
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'dispatchInstallerEvent', [$eventName, $devMode, $executeOperations, $transaction]);
+ }
+
+ public function addListener(string $eventName, $listener, int $priority = 0): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'addListener', [$eventName, $listener, $priority]);
+ }
+
+ public function removeListener($listener): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'removeListener', [$listener]);
+ }
+
+ public function addSubscriber(EventSubscriberInterface $subscriber): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'addSubscriber', [$subscriber]);
+ }
+
+ public function hasEventListeners(Event $event): bool
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'hasEventListeners', [$event]);
+ }
+}
diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php
index 50e9ef76..241dad57 100644
--- a/crates/shirabe-php-rpc/php/worker.php
+++ b/crates/shirabe-php-rpc/php/worker.php
@@ -18,8 +18,13 @@ const SHIRABE_MAX_FRAME_LEN = 268435456; // 256 MiB, mirrored on the Rust side.
/** Marker interface every Rust-proxy stub class implements. */
interface ShirabeRustStub
{
- /** @return array{__rhandle: int, __class: string, __epoch: int} */
- public function __shirabeRustHandleDescriptor(): array;
+ /**
+ * Null when this instance holds no Rust handle: a dual-mode class (see php/runtime/) that
+ * was constructed natively in this process crosses the wire as a P-table entity instead.
+ *
+ * @return ?array{__rhandle: int, __class: string, __epoch: int}
+ */
+ public function __shirabeRustHandleDescriptor(): ?array;
}
/** Interns proxy stubs so the same Rust handle always yields the same stub instance. */
@@ -199,7 +204,11 @@ final class ShirabeRpcRuntime
public static function toWire($value)
{
if ($value instanceof ShirabeRustStub) {
- return $value->__shirabeRustHandleDescriptor();
+ $descriptor = $value->__shirabeRustHandleDescriptor();
+ if ($descriptor !== null) {
+ return $descriptor;
+ }
+ // A natively-constructed dual-mode instance falls through to the P table below.
}
if (is_object($value)) {
return ShirabePhpObjectRegistry::descriptor($value);
@@ -616,6 +625,61 @@ ShirabeRpcRuntime::$dispatch = [
}
return $obj->{$args[1]};
},
+ // Builds the worker-side Composer\Console\Application (the runtime/ definition, not the
+ // real class) from the Rust handoff; the caller keeps the returned handle and runs
+ // plugin-provided commands through __shirabe_run_console_application.
+ '__shirabe_console_application_boot' => static function ($args) {
+ return \Composer\Console\Application::__shirabeBoot($args[0]);
+ },
+ // Runs one command line (the stringified input of the Rust-side run) through a booted
+ // worker-side application; output goes to the inherited stdio, the exit code returns
+ // over the wire, and a command failure propagates as an RPC throw (catchExceptions is
+ // off on the booted application).
+ '__shirabe_run_console_application' => static function ($args) {
+ [$app, $inputString] = $args;
+ if (!$app instanceof \Composer\Console\Application) {
+ throw new RuntimeException('__shirabe_run_console_application expects an application handle');
+ }
+ return $app->run(new \Symfony\Component\Console\Input\StringInput($inputString));
+ },
+ // Reads a command's input definition (plus help text and extra usages) as plain data, so
+ // the Rust side can mirror it for `help`/`list` rendering without executing anything.
+ '__shirabe_read_command_definition' => static function ($args) {
+ $command = $args[0];
+ if (!$command instanceof \Symfony\Component\Console\Command\Command) {
+ throw new RuntimeException('__shirabe_read_command_definition expects a command handle');
+ }
+ $definition = $command->getDefinition();
+ $arguments = [];
+ foreach ($definition->getArguments() as $argument) {
+ $arguments[] = [
+ 'name' => $argument->getName(),
+ 'required' => $argument->isRequired(),
+ 'isArray' => $argument->isArray(),
+ 'description' => $argument->getDescription(),
+ 'default' => $argument->getDefault(),
+ ];
+ }
+ $options = [];
+ foreach ($definition->getOptions() as $option) {
+ $options[] = [
+ 'name' => $option->getName(),
+ 'shortcut' => $option->getShortcut(),
+ 'acceptValue' => $option->acceptValue(),
+ 'isValueRequired' => $option->isValueRequired(),
+ 'isArray' => $option->isArray(),
+ 'isNegatable' => $option->isNegatable(),
+ 'description' => $option->getDescription(),
+ 'default' => $option->getDefault(),
+ ];
+ }
+ return [
+ 'arguments' => $arguments,
+ 'options' => $options,
+ 'help' => $command->getHelp(),
+ 'usages' => $command->getUsages(),
+ ];
+ },
];
ShirabeRpcRuntime::serveForever();
diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs
index ba5443d2..ef11d542 100644
--- a/crates/shirabe-php-rpc/src/lib.rs
+++ b/crates/shirabe-php-rpc/src/lib.rs
@@ -563,8 +563,8 @@ const GLUE_SCRIPT: &str = include_str!("../php/worker.php");
/// generator's `--check` mode verifies both the file contents and this list).
const STUB_FILES: &[(&str, &str)] = &[
(
- "Composer/EventDispatcher/Event.php",
- include_str!("../php/stubs/Composer/EventDispatcher/Event.php"),
+ "Composer/EventDispatcher/EventDispatcher.php",
+ include_str!("../php/stubs/Composer/EventDispatcher/EventDispatcher.php"),
),
(
"Composer/Script/Event.php",
@@ -640,6 +640,24 @@ const STUB_FILES: &[(&str, &str)] = &[
),
];
+/// Hand-written worker-side classes (two-world implementations with behavior of their own, not
+/// mechanical proxies), written into the same autoload directory as the generated stubs so the
+/// prepended stub autoloader resolves their FQCNs ahead of any real class file.
+const RUNTIME_FILES: &[(&str, &str)] = &[
+ (
+ "Composer/Console/Application.php",
+ include_str!("../php/runtime/Composer/Console/Application.php"),
+ ),
+ (
+ "Composer/EventDispatcher/Event.php",
+ include_str!("../php/runtime/Composer/EventDispatcher/Event.php"),
+ ),
+ (
+ "Shirabe/RustCommandStub.php",
+ include_str!("../php/runtime/Shirabe/RustCommandStub.php"),
+ ),
+];
+
struct Worker {
stream: UnixStream,
// Also queried for its exit status when a socket read/write fails, to tell a dead worker
@@ -707,7 +725,7 @@ fn spawn_worker() -> anyhow::Result<Worker> {
std::fs::write(&script_path, GLUE_SCRIPT)?;
let stubs_dir = tempdir.path().join("stubs");
- for (relative_path, contents) in STUB_FILES {
+ for (relative_path, contents) in STUB_FILES.iter().chain(RUNTIME_FILES) {
let path = stubs_dir.join(relative_path);
std::fs::create_dir_all(path.parent().expect("stub paths have a parent"))?;
std::fs::write(&path, contents)?;