diff options
Diffstat (limited to 'crates/shirabe-php-rpc/php/runtime')
3 files changed, 296 insertions, 0 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]); + } +} |
