aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates
diff options
context:
space:
mode:
Diffstat (limited to 'crates')
-rw-r--r--crates/shirabe-php-rpc/php/runtime/Composer/EventDispatcher/Event.php30
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Config.php212
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Downloader/DownloadManager.php128
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php23
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php20
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/IO/BufferIO.php9
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/IO/ConsoleIO.php9
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php22
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php20
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Package/Package.php6
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php20
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php20
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Repository/FilesystemRepository.php9
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php25
-rw-r--r--crates/shirabe-php-rpc/php/stubs/Composer/Script/Event.php6
-rw-r--r--crates/shirabe-php-rpc/php/worker.php55
-rw-r--r--crates/shirabe-php-rpc/src/lib.rs8
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs3
-rw-r--r--crates/shirabe/src/package/handle.rs5
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs895
20 files changed, 1403 insertions, 122 deletions
diff --git a/crates/shirabe-php-rpc/php/runtime/Composer/EventDispatcher/Event.php b/crates/shirabe-php-rpc/php/runtime/Composer/EventDispatcher/Event.php
index 0d35ac55..e2c63439 100644
--- a/crates/shirabe-php-rpc/php/runtime/Composer/EventDispatcher/Event.php
+++ b/crates/shirabe-php-rpc/php/runtime/Composer/EventDispatcher/Event.php
@@ -4,9 +4,10 @@
// 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.
+// faithful in-process port of the real base class. Proxy revival binds the handle without
+// running the constructor, so the constructor below is the native mode alone;
+// `__shirabeRustHandleDescriptor()` returns null there so the wire codec registers the object in
+// the P table instead of treating it as a Rust handle.
namespace Composer\EventDispatcher;
@@ -29,29 +30,20 @@ class Event implements \ShirabeRustStub
/** @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 = [])
+ public function __construct(string $name, array $args = [], array $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 __shirabeBind(int $rhandle, int $epoch): void
+ {
+ $this->__rhandle = $rhandle;
+ $this->__epoch = $epoch;
+ }
+
public function __destruct()
{
if ($this->__rhandle !== null) {
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Config.php b/crates/shirabe-php-rpc/php/stubs/Composer/Config.php
new file mode 100644
index 00000000..bccb8dcb
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/Config.php
@@ -0,0 +1,212 @@
+<?php
+
+// Generated by scripts/plugin-stub-generator; do not edit by hand.
+// Proxy stub for Composer\Config: the public surface forwards to the Rust-side entity over RPC.
+
+namespace Composer;
+
+use Composer\Advisory\Auditor;
+use Composer\Config\ConfigSourceInterface;
+use Composer\IO\IOInterface;
+use Composer\Util\ProcessExecutor;
+
+class Config implements \ShirabeRustStub
+{
+ /** @var int */
+ protected $__rhandle;
+ /** @var int */
+ protected $__epoch;
+
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
+ {
+ $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 __clone()
+ {
+ // PHP has already shallow-copied this stub, so both copies would point at one
+ // entity and release it twice. The Rust side clones the entity instead, applying
+ // whatever __clone semantics the real class defines, and this copy rebinds to the
+ // fresh handle. Entities without clone semantics answer with an explicit error.
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust($this->__rhandle, '__shirabeClone', []);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
+ public function __construct(bool $useEnvironment = true, ?string $baseDir = null)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$useEnvironment, $baseDir]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
+ public const SOURCE_DEFAULT = 'default';
+ public const SOURCE_COMMAND = 'command';
+ public const SOURCE_UNKNOWN = 'unknown';
+ public const RELATIVE_PATHS = 1;
+
+ public static $defaultConfig = [
+ 'process-timeout' => 300,
+ 'use-include-path' => false,
+ 'allow-plugins' => [],
+ 'use-parent-dir' => 'prompt',
+ 'preferred-install' => 'dist',
+ 'audit' => ['ignore' => [], 'abandoned' => Auditor::ABANDONED_FAIL],
+ 'notify-on-install' => true,
+ 'github-protocols' => ['https', 'ssh', 'git'],
+ 'gitlab-protocol' => null,
+ 'vendor-dir' => 'vendor',
+ 'bin-dir' => '{$vendor-dir}/bin',
+ 'cache-dir' => '{$home}/cache',
+ 'data-dir' => '{$home}',
+ 'cache-files-dir' => '{$cache-dir}/files',
+ 'cache-repo-dir' => '{$cache-dir}/repo',
+ 'cache-vcs-dir' => '{$cache-dir}/vcs',
+ 'cache-ttl' => 15552000, // 6 months
+ 'cache-files-ttl' => null, // fallback to cache-ttl
+ 'cache-files-maxsize' => '300MiB',
+ 'cache-read-only' => false,
+ 'bin-compat' => 'auto',
+ 'discard-changes' => false,
+ 'autoloader-suffix' => null,
+ 'sort-packages' => false,
+ 'optimize-autoloader' => false,
+ 'classmap-authoritative' => false,
+ 'apcu-autoloader' => false,
+ 'prepend-autoloader' => true,
+ 'update-with-minimal-changes' => false,
+ 'github-domains' => ['github.com'],
+ 'bitbucket-expose-hostname' => true,
+ 'disable-tls' => false,
+ 'secure-http' => true,
+ 'secure-svn-domains' => [],
+ 'cafile' => null,
+ 'capath' => null,
+ 'github-expose-hostname' => true,
+ 'gitlab-domains' => ['gitlab.com'],
+ 'store-auths' => 'prompt',
+ 'platform' => [],
+ 'archive-format' => 'tar',
+ 'archive-dir' => '.',
+ 'htaccess-protect' => true,
+ 'use-github-api' => true,
+ 'lock' => true,
+ 'platform-check' => 'php-only',
+ 'bitbucket-oauth' => [],
+ 'github-oauth' => [],
+ 'gitlab-oauth' => [],
+ 'gitlab-token' => [],
+ 'http-basic' => [],
+ 'bearer' => [],
+ 'custom-headers' => [],
+ 'bump-after-update' => false,
+ 'allow-missing-requirements' => false,
+ 'client-certificate' => [],
+ 'forgejo-domains' => ['codeberg.org'],
+ 'forgejo-token' => [],
+ ];
+ public static $defaultRepositories = [
+ 'packagist.org' => [
+ 'type' => 'composer',
+ 'url' => 'https://repo.packagist.org',
+ ],
+ ];
+
+ public static function disableProcessTimeout(): void
+ {
+ // Override global timeout set earlier by environment or config
+ ProcessExecutor::setTimeout(0);
+ }
+
+ public function setBaseDir(?string $baseDir): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'setBaseDir', [$baseDir]);
+ }
+
+ public function setConfigSource(ConfigSourceInterface $source): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'setConfigSource', [$source]);
+ }
+
+ public function getConfigSource(): ConfigSourceInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getConfigSource', []);
+ }
+
+ public function setAuthConfigSource(ConfigSourceInterface $source): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'setAuthConfigSource', [$source]);
+ }
+
+ public function getAuthConfigSource(): ConfigSourceInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getAuthConfigSource', []);
+ }
+
+ public function setLocalAuthConfigSource(ConfigSourceInterface $source): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'setLocalAuthConfigSource', [$source]);
+ }
+
+ public function getLocalAuthConfigSource(): ?ConfigSourceInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getLocalAuthConfigSource', []);
+ }
+
+ public function merge(array $config, string $source = self::SOURCE_UNKNOWN): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'merge', [$config, $source]);
+ }
+
+ public function getRepositories(): array
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getRepositories', []);
+ }
+
+ public function get(string $key, int $flags = 0)
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'get', [$key, $flags]);
+ }
+
+ public function all(int $flags = 0): array
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'all', [$flags]);
+ }
+
+ public function getSourceOfValue(string $key): string
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getSourceOfValue', [$key]);
+ }
+
+ public function raw(): array
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'raw', []);
+ }
+
+ public function has(string $key): bool
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'has', [$key]);
+ }
+
+ public function prohibitUrlByConfig(string $url, ?IOInterface $io = null, array $repoOptions = []): void
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'prohibitUrlByConfig', [$url, $io, $repoOptions]);
+ }
+}
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Downloader/DownloadManager.php b/crates/shirabe-php-rpc/php/stubs/Composer/Downloader/DownloadManager.php
new file mode 100644
index 00000000..3828ac4a
--- /dev/null
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/Downloader/DownloadManager.php
@@ -0,0 +1,128 @@
+<?php
+
+// Generated by scripts/plugin-stub-generator; do not edit by hand.
+// Proxy stub for Composer\Downloader\DownloadManager: the public surface forwards to the Rust-side entity over RPC.
+
+namespace Composer\Downloader;
+
+use Composer\Package\PackageInterface;
+use Composer\IO\IOInterface;
+use Composer\Util\Filesystem;
+use React\Promise\PromiseInterface;
+
+class DownloadManager implements \ShirabeRustStub
+{
+ /** @var int */
+ protected $__rhandle;
+ /** @var int */
+ protected $__epoch;
+
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
+ {
+ $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 __clone()
+ {
+ // PHP has already shallow-copied this stub, so both copies would point at one
+ // entity and release it twice. The Rust side clones the entity instead, applying
+ // whatever __clone semantics the real class defines, and this copy rebinds to the
+ // fresh handle. Entities without clone semantics answer with an explicit error.
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust($this->__rhandle, '__shirabeClone', []);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
+ public function __construct(IOInterface $io, bool $preferSource = false, ?Filesystem $filesystem = null)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$io, $preferSource, $filesystem]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
+ public function setPreferSource(bool $preferSource): self
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'setPreferSource', [$preferSource]);
+ return $this;
+ }
+
+ public function setPreferDist(bool $preferDist): self
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'setPreferDist', [$preferDist]);
+ return $this;
+ }
+
+ public function setPreferences(array $preferences): self
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'setPreferences', [$preferences]);
+ return $this;
+ }
+
+ public function setDownloader(string $type, DownloaderInterface $downloader): self
+ {
+ \ShirabeRpcRuntime::callRust($this->__rhandle, 'setDownloader', [$type, $downloader]);
+ return $this;
+ }
+
+ public function getDownloader(string $type): DownloaderInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getDownloader', [$type]);
+ }
+
+ public function getDownloaderForPackage(PackageInterface $package): ?DownloaderInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getDownloaderForPackage', [$package]);
+ }
+
+ public function getDownloaderType(DownloaderInterface $downloader): string
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getDownloaderType', [$downloader]);
+ }
+
+ public function download(PackageInterface $package, string $targetDir, ?PackageInterface $prevPackage = null): PromiseInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'download', [$package, $targetDir, $prevPackage]);
+ }
+
+ public function prepare(string $type, PackageInterface $package, string $targetDir, ?PackageInterface $prevPackage = null): PromiseInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'prepare', [$type, $package, $targetDir, $prevPackage]);
+ }
+
+ public function install(PackageInterface $package, string $targetDir): PromiseInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'install', [$package, $targetDir]);
+ }
+
+ public function update(PackageInterface $initial, PackageInterface $target, string $targetDir): PromiseInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'update', [$initial, $target, $targetDir]);
+ }
+
+ public function remove(PackageInterface $package, string $targetDir): PromiseInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'remove', [$package, $targetDir]);
+ }
+
+ public function cleanup(string $type, PackageInterface $package, string $targetDir, ?PackageInterface $prevPackage = null): PromiseInterface
+ {
+ return \ShirabeRpcRuntime::callRust($this->__rhandle, 'cleanup', [$type, $package, $targetDir, $prevPackage]);
+ }
+}
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php b/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php
index 30f8c0e8..e2369ed2 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php
@@ -6,8 +6,11 @@
namespace Composer\EventDispatcher;
use Composer\DependencyResolver\Transaction;
+use Composer\IO\IOInterface;
+use Composer\PartialComposer;
use Composer\DependencyResolver\Operation\OperationInterface;
use Composer\Repository\RepositoryInterface;
+use Composer\Util\ProcessExecutor;
class EventDispatcher implements \ShirabeRustStub
{
@@ -16,16 +19,12 @@ class EventDispatcher implements \ShirabeRustStub
/** @var int */
protected $__epoch;
- public function __construct(int $rhandle = 0, int $epoch = 0)
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
{
- 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;
}
@@ -54,6 +53,12 @@ class EventDispatcher implements \ShirabeRustStub
\ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
}
+ public function __construct(PartialComposer $composer, IOInterface $io, ?ProcessExecutor $process = null)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$composer, $io, $process]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public function setRunScripts(bool $runScripts = true): self
{
\ShirabeRpcRuntime::callRust($this->__rhandle, 'setRunScripts', [$runScripts]);
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php b/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php
index b3e0fb51..ecb4db2f 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php
@@ -14,16 +14,12 @@ abstract class BaseIO implements IOInterface, \ShirabeRustStub
/** @var int */
protected $__epoch;
- public function __construct(int $rhandle = 0, int $epoch = 0)
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
{
- 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;
}
@@ -52,6 +48,12 @@ abstract class BaseIO implements IOInterface, \ShirabeRustStub
\ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
}
+ public function __construct()
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, []]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public function isInteractive()
{
return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isInteractive', []);
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/IO/BufferIO.php b/crates/shirabe-php-rpc/php/stubs/Composer/IO/BufferIO.php
index d5e59e90..b2e84d2b 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/IO/BufferIO.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/IO/BufferIO.php
@@ -5,8 +5,17 @@
namespace Composer\IO;
+use Symfony\Component\Console\Output\StreamOutput;
+use Symfony\Component\Console\Formatter\OutputFormatterInterface;
+
class BufferIO extends ConsoleIO
{
+ public function __construct(string $input = '', int $verbosity = StreamOutput::VERBOSITY_NORMAL, ?OutputFormatterInterface $formatter = null)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$input, $verbosity, $formatter]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public function getOutput(): string
{
return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getOutput', []);
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/IO/ConsoleIO.php b/crates/shirabe-php-rpc/php/stubs/Composer/IO/ConsoleIO.php
index 0041363a..b68f50ff 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/IO/ConsoleIO.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/IO/ConsoleIO.php
@@ -6,10 +6,19 @@
namespace Composer\IO;
use Composer\Pcre\Preg;
+use Symfony\Component\Console\Helper\HelperSet;
use Symfony\Component\Console\Helper\Table;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
class ConsoleIO extends BaseIO
{
+ public function __construct(InputInterface $input, OutputInterface $output, HelperSet $helperSet)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$input, $output, $helperSet]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public static function sanitize($messages, bool $allowNewlines = true)
{
// Match ANSI escape sequences:
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php b/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php
index d458d30b..7f41f6b9 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php
@@ -13,6 +13,8 @@ use Composer\DependencyResolver\Operation\UpdateOperation;
use Composer\DependencyResolver\Operation\UninstallOperation;
use Composer\DependencyResolver\Operation\MarkAliasInstalledOperation;
use Composer\DependencyResolver\Operation\MarkAliasUninstalledOperation;
+use Composer\EventDispatcher\EventDispatcher;
+use Composer\Util\Loop;
use React\Promise\PromiseInterface;
class InstallationManager implements \ShirabeRustStub
@@ -22,16 +24,12 @@ class InstallationManager implements \ShirabeRustStub
/** @var int */
protected $__epoch;
- public function __construct(int $rhandle = 0, int $epoch = 0)
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
{
- 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;
}
@@ -60,6 +58,12 @@ class InstallationManager implements \ShirabeRustStub
\ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
}
+ public function __construct(Loop $loop, IOInterface $io, ?EventDispatcher $eventDispatcher = null)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$loop, $io, $eventDispatcher]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public function reset(): void
{
\ShirabeRpcRuntime::callRust($this->__rhandle, 'reset', []);
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php b/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php
index de8037ca..9fca286a 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php
@@ -14,16 +14,12 @@ abstract class BasePackage implements PackageInterface, \ShirabeRustStub
/** @var int */
protected $__epoch;
- public function __construct(int $rhandle = 0, int $epoch = 0)
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
{
- 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;
}
@@ -52,6 +48,12 @@ abstract class BasePackage implements PackageInterface, \ShirabeRustStub
\ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
}
+ public function __construct(string $name)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$name]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public const STABILITY_STABLE = 0;
public const STABILITY_RC = 5;
public const STABILITY_BETA = 10;
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Package/Package.php b/crates/shirabe-php-rpc/php/stubs/Composer/Package/Package.php
index 5b80060d..238dde3b 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/Package/Package.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/Package/Package.php
@@ -7,6 +7,12 @@ namespace Composer\Package;
class Package extends BasePackage
{
+ public function __construct(string $name, string $version, string $prettyVersion)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$name, $version, $prettyVersion]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public function setType(string $type): void
{
\ShirabeRpcRuntime::callRust($this->__rhandle, 'setType', [$type]);
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php b/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php
index c60edbb2..a48133cb 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php
@@ -18,16 +18,12 @@ class PartialComposer implements \ShirabeRustStub
/** @var int */
protected $__epoch;
- public function __construct(int $rhandle = 0, int $epoch = 0)
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
{
- 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;
}
@@ -56,6 +52,12 @@ class PartialComposer implements \ShirabeRustStub
\ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
}
+ public function __construct()
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, []]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public function setPackage(RootPackageInterface $package): void
{
\ShirabeRpcRuntime::callRust($this->__rhandle, 'setPackage', [$package]);
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php
index 51e6a462..a5358ff2 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php
@@ -14,16 +14,12 @@ class ArrayRepository implements RepositoryInterface, \ShirabeRustStub
/** @var int */
protected $__epoch;
- public function __construct(int $rhandle = 0, int $epoch = 0)
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
{
- 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;
}
@@ -52,6 +48,12 @@ class ArrayRepository implements RepositoryInterface, \ShirabeRustStub
\ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
}
+ public function __construct(array $packages = [])
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$packages]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public function hasPackage(PackageInterface $package)
{
return \ShirabeRpcRuntime::callRust($this->__rhandle, 'hasPackage', [$package]);
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/FilesystemRepository.php b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/FilesystemRepository.php
index 00e42ac6..99c225cf 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/FilesystemRepository.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/FilesystemRepository.php
@@ -5,10 +5,19 @@
namespace Composer\Repository;
+use Composer\Json\JsonFile;
+use Composer\Package\RootPackageInterface;
use Composer\Pcre\Preg;
+use Composer\Util\Filesystem;
class FilesystemRepository extends WritableArrayRepository
{
+ public function __construct(JsonFile $repositoryFile, bool $dumpVersions = false, ?RootPackageInterface $rootPackage = null, ?Filesystem $filesystem = null)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$repositoryFile, $dumpVersions, $rootPackage, $filesystem]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public static function safelyLoadInstalledVersions(string $path): bool
{
$installedVersionsData = @file_get_contents($path);
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php
index 5635c0dc..9f3567d3 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php
@@ -5,7 +5,12 @@
namespace Composer\Repository;
+use Composer\IO\IOInterface;
+use Composer\Config;
+use Composer\EventDispatcher\EventDispatcher;
use Composer\Package\PackageInterface;
+use Composer\Util\HttpDownloader;
+use Composer\Util\ProcessExecutor;
class RepositoryManager implements \ShirabeRustStub
{
@@ -14,16 +19,12 @@ class RepositoryManager implements \ShirabeRustStub
/** @var int */
protected $__epoch;
- public function __construct(int $rhandle = 0, int $epoch = 0)
+ /**
+ * Binds a stub the registry built for an existing entity. Proxy instantiation bypasses
+ * the constructor, which belongs to plugin code building a new entity instead.
+ */
+ public function __shirabeBind(int $rhandle, int $epoch): void
{
- 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;
}
@@ -52,6 +53,12 @@ class RepositoryManager implements \ShirabeRustStub
\ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
}
+ public function __construct(IOInterface $io, Config $config, HttpDownloader $httpDownloader, ?EventDispatcher $eventDispatcher = null, ?ProcessExecutor $process = null)
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$io, $config, $httpDownloader, $eventDispatcher, $process]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public function findPackage(string $name, $constraint): ?PackageInterface
{
return \ShirabeRpcRuntime::callRust($this->__rhandle, 'findPackage', [$name, $constraint]);
diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/Script/Event.php b/crates/shirabe-php-rpc/php/stubs/Composer/Script/Event.php
index a93d0dfc..065e79e2 100644
--- a/crates/shirabe-php-rpc/php/stubs/Composer/Script/Event.php
+++ b/crates/shirabe-php-rpc/php/stubs/Composer/Script/Event.php
@@ -11,6 +11,12 @@ use Composer\EventDispatcher\Event as BaseEvent;
class Event extends BaseEvent
{
+ public function __construct(string $name, Composer $composer, IOInterface $io, bool $devMode = false, array $args = [], array $flags = [])
+ {
+ [$this->__rhandle, $this->__epoch] = \ShirabeRpcRuntime::callRust(0, '__shirabeConstruct', [static::class, [$name, $composer, $io, $devMode, $args, $flags]]);
+ \ShirabeRustObjectRegistry::adopt($this->__rhandle, $this);
+ }
+
public function getComposer(): Composer
{
return \ShirabeRpcRuntime::callRust($this->__rhandle, 'getComposer', []);
diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php
index 12297c83..9a36abdd 100644
--- a/crates/shirabe-php-rpc/php/worker.php
+++ b/crates/shirabe-php-rpc/php/worker.php
@@ -25,6 +25,9 @@ interface ShirabeRustStub
* @return ?array{__rhandle: int, __class: string, __epoch: int}
*/
public function __shirabeRustHandleDescriptor(): ?array;
+
+ /** Binds this stub to an existing Rust entity, in place of running its constructor. */
+ public function __shirabeBind(int $rhandle, int $epoch): void;
}
/** Interns proxy stubs so the same Rust handle always yields the same stub instance. */
@@ -46,7 +49,10 @@ final class ShirabeRustObjectRegistry
"no proxy stub class is available for {$class}"
);
}
- $stub = new $class($rhandle, $epoch);
+ // The stub's constructor belongs to plugin code building a *new* entity; an entity that
+ // already exists is bound directly, so proxying never runs it.
+ $stub = (new ReflectionClass($class))->newInstanceWithoutConstructor();
+ $stub->__shirabeBind($rhandle, $epoch);
self::$internTable[$rhandle] = WeakReference::create($stub);
return $stub;
}
@@ -626,6 +632,53 @@ ShirabeRpcRuntime::$dispatch = [
}
return true;
},
+ // An already-fulfilled promise for a Rust-side call whose PHP signature declares
+ // PromiseInterface. The Rust future ran to completion before this is called, so there is
+ // nothing left to defer; see .ken/plugin-arch/design.md §10.1.6.
+ '__shirabe_resolved_promise' => static function ($args) {
+ if (!function_exists('React\\Promise\\resolve')) {
+ throw new RuntimeException(
+ 'react/promise is not loaded in the plugin process, so a PromiseInterface cannot be built'
+ );
+ }
+ return \React\Promise\resolve($args[0]);
+ },
+ // Drains a promise a plugin returned to the Rust side. React settles promises
+ // synchronously, so an already-settled one runs these handlers during then(); one that is
+ // still pending is an explicit error rather than a silently dropped continuation.
+ '__shirabe_settle_promise' => static function ($args) {
+ $promise = $args[0];
+ if (!$promise instanceof \React\Promise\PromiseInterface) {
+ throw new RuntimeException('__shirabe_settle_promise expects a promise handle');
+ }
+ $settled = false;
+ $value = null;
+ $rejected = false;
+ $reason = null;
+ $promise->then(
+ static function ($result) use (&$settled, &$value) {
+ $settled = true;
+ $value = $result;
+ },
+ static function ($error) use (&$settled, &$rejected, &$reason) {
+ $settled = true;
+ $rejected = true;
+ $reason = $error;
+ }
+ );
+ if (!$settled) {
+ throw new RuntimeException(
+ 'the promise returned to Shirabe is still pending; deferred resolution across the'
+ . ' RPC boundary is not implemented yet'
+ );
+ }
+ if ($rejected) {
+ throw $reason instanceof Throwable
+ ? $reason
+ : new RuntimeException('the promise returned to Shirabe was rejected with ' . gettype($reason));
+ }
+ return $value;
+ },
// For testing only: reads a public property of a P-table entity (PHPUnit asserts like
// `$plugins[0]->version` have no method to call).
'__shirabe_get_property' => static function ($args) {
diff --git a/crates/shirabe-php-rpc/src/lib.rs b/crates/shirabe-php-rpc/src/lib.rs
index ef11d542..4f9ba6b5 100644
--- a/crates/shirabe-php-rpc/src/lib.rs
+++ b/crates/shirabe-php-rpc/src/lib.rs
@@ -579,6 +579,14 @@ const STUB_FILES: &[(&str, &str)] = &[
include_str!("../php/stubs/Composer/Composer.php"),
),
(
+ "Composer/Config.php",
+ include_str!("../php/stubs/Composer/Config.php"),
+ ),
+ (
+ "Composer/Downloader/DownloadManager.php",
+ include_str!("../php/stubs/Composer/Downloader/DownloadManager.php"),
+ ),
+ (
"Composer/IO/BaseIO.php",
include_str!("../php/stubs/Composer/IO/BaseIO.php"),
),
diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
index fad6566e..4aff50c9 100644
--- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
+++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs
@@ -1686,6 +1686,9 @@ impl RustMethodDispatcher for ScriptRpcDispatcher<'_> {
},
);
}
+ if method_name == "__shirabeConstruct" {
+ return crate::plugin::php_plugin_proxy::construct_entity(&args);
+ }
return Err(runtime_throw(format!(
"unknown runtime service method `{method_name}`"
)));
diff --git a/crates/shirabe/src/package/handle.rs b/crates/shirabe/src/package/handle.rs
index 4d78734f..aed8be0f 100644
--- a/crates/shirabe/src/package/handle.rs
+++ b/crates/shirabe/src/package/handle.rs
@@ -64,8 +64,9 @@ impl AnyPackage {
}
}
- /// For testing only: reach the base `Package` of a real package variant.
- /// Crate-private; the public `__set_*` test hatches are built on top of it.
+ /// The base `Package` of a real package variant, for the members `PackageInterface` does not
+ /// carry (`Package`'s own setters, which the subclasses inherit). The public `__set_*` test
+ /// hatches are built on top of it too.
pub(crate) fn as_package_mut(&mut self) -> Option<&mut Package> {
match self {
Self::Package(p) => Some(p),
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index 701e83cc..2b3622f2 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -37,6 +37,10 @@ use shirabe_php_shim::PhpMixed;
#[derive(Debug, Clone)]
enum RustEntity {
Composer(ComposerHandle),
+ Config(std::rc::Rc<std::cell::RefCell<crate::config::Config>>),
+ DownloadManager(
+ std::rc::Rc<std::cell::RefCell<dyn crate::downloader::DownloadManagerInterface>>,
+ ),
Io(std::rc::Rc<std::cell::RefCell<dyn IOInterface>>),
InstallationManager(std::rc::Rc<std::cell::RefCell<dyn InstallationManagerInterface>>),
RepositoryManager(std::rc::Rc<std::cell::RefCell<dyn RepositoryManagerInterface>>),
@@ -54,6 +58,8 @@ fn entity_ptr_id(entity: &RustEntity) -> usize {
RustEntity::Composer(composer) => {
std::rc::Rc::as_ptr(composer.as_rc()) as *const () as usize
}
+ RustEntity::Config(config) => std::rc::Rc::as_ptr(config) as *const () as usize,
+ RustEntity::DownloadManager(dm) => std::rc::Rc::as_ptr(dm) as *const () as usize,
RustEntity::Io(io) => std::rc::Rc::as_ptr(io) as *const () as usize,
RustEntity::InstallationManager(im) => std::rc::Rc::as_ptr(im) as *const () as usize,
RustEntity::RepositoryManager(rm) => std::rc::Rc::as_ptr(rm) as *const () as usize,
@@ -278,6 +284,9 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
Err(e) => Err(runtime_throw(format!("{e:#}"))),
};
}
+ if method_name == "__shirabeConstruct" {
+ return construct_entity(&args);
+ }
return Err(runtime_throw(format!(
"unknown runtime service method `{method_name}`"
)));
@@ -303,6 +312,10 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
Some(RustEntity::Composer(composer)) => {
dispatch_composer_method(&composer, method_name)
}
+ Some(RustEntity::Config(config)) => dispatch_config_method(&config, method_name, &args),
+ Some(RustEntity::DownloadManager(dm)) => {
+ dispatch_download_manager_method(&dm, method_name, &args)
+ }
Some(RustEntity::InstallationManager(im)) => {
dispatch_installation_manager_method(&im, method_name, &args)
}
@@ -310,7 +323,7 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
dispatch_repository_manager_method(&rm, method_name)
}
Some(RustEntity::Repository(repository)) => {
- dispatch_repository_method(&repository, method_name)
+ dispatch_repository_method(&repository, method_name, &args)
}
Some(RustEntity::Package(package)) => {
dispatch_package_method(&package, method_name, &args)
@@ -323,6 +336,62 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
}
}
+/// Serves the constructor every proxy stub carries: plugin code writing `new SomeProxiedClass`
+/// gets a fresh Rust-side entity, since the proxied FQCN has no implementation of its own in the
+/// child. Classes whose entity cannot be built here are an explicit error, so a plugin never
+/// ends up holding a second, unconnected instance of a Composer service.
+pub(crate) fn construct_entity(args: &[PluginValue]) -> Result<PluginValue, PhpThrow> {
+ let (class, ctor_args) = match (args.first(), args.get(1)) {
+ // TODO(phase-e): lossy UTF-8; class names are bytes in PHP.
+ (Some(PluginValue::String(class)), Some(PluginValue::List(ctor_args))) => {
+ (String::from_utf8_lossy(class).into_owned(), ctor_args)
+ }
+ (Some(PluginValue::String(class)), None | Some(PluginValue::Array(_))) => {
+ // An empty PHP array crosses as a list; anything else keyed is a protocol error.
+ (String::from_utf8_lossy(class).into_owned(), &Vec::new())
+ }
+ other => {
+ return Err(runtime_throw(format!(
+ "__shirabeConstruct expects a class name and an argument list, got {other:?}"
+ )));
+ }
+ };
+ let string_arg = |position: usize| -> Result<String, PhpThrow> {
+ match ctor_args.get(position) {
+ Some(PluginValue::String(bytes)) => Ok(String::from_utf8_lossy(bytes).into_owned()),
+ other => Err(runtime_throw(format!(
+ "{class} expects a string constructor argument at position {position}, got {other:?}"
+ ))),
+ }
+ };
+ let entity = match class.as_str() {
+ "Composer\\Package\\Package" => AnyPackage::Package(crate::package::Package::new(
+ string_arg(0)?,
+ string_arg(1)?,
+ string_arg(2)?,
+ )),
+ "Composer\\Package\\CompletePackage" => AnyPackage::CompletePackage(
+ crate::package::CompletePackage::new(string_arg(0)?, string_arg(1)?, string_arg(2)?),
+ ),
+ // TODO(plugin): the remaining proxied classes get a construction story on demand,
+ // driven by explicit errors from real plugins. Each one has to decide what a
+ // plugin-built instance means for the Rust-side graph, which is why none of them is
+ // answered generically here.
+ other => {
+ return Err(runtime_throw(format!(
+ "Shirabe does not support constructing {other} inside the plugin process yet"
+ )));
+ }
+ };
+ let rhandle = register_entity(RustEntity::Package(std::rc::Rc::new(
+ std::cell::RefCell::new(entity),
+ )));
+ Ok(PluginValue::List(vec![
+ PluginValue::Int(rhandle as i64),
+ PluginValue::Int(0),
+ ]))
+}
+
/// Serves the `__clone` forwarder every proxy stub carries. Only entities whose Rust type
/// models the PHP clone answer; the rest are an explicit error, so a plugin cloning a live
/// service never silently ends up with two stubs over one entity.
@@ -337,6 +406,8 @@ fn clone_entity(entity: &RustEntity) -> Result<PluginValue, PhpThrow> {
)))
}
RustEntity::Composer(_)
+ | RustEntity::Config(_)
+ | RustEntity::DownloadManager(_)
| RustEntity::Io(_)
| RustEntity::InstallationManager(_)
| RustEntity::RepositoryManager(_)
@@ -386,7 +457,20 @@ fn dispatch_composer_method(
"Composer\\EventDispatcher\\EventDispatcher",
))
}
- // TODO(plugin): the remaining Composer object graph (getConfig, getLocker, ...)
+ "getConfig" => {
+ let config = composer.borrow().get_config();
+ let rhandle = register_entity(RustEntity::Config(config));
+ Ok(rust_handle_value(rhandle, "Composer\\Config"))
+ }
+ "getDownloadManager" => {
+ let dm = composer.borrow().get_download_manager();
+ let rhandle = register_entity(RustEntity::DownloadManager(dm));
+ Ok(rust_handle_value(
+ rhandle,
+ "Composer\\Downloader\\DownloadManager",
+ ))
+ }
+ // TODO(plugin): the remaining Composer object graph (getLocker, getPluginManager, ...)
// becomes reachable over RPC on demand, driven by explicit errors from real plugins.
other => Err(runtime_throw(format!(
"the Composer method `{other}` is not available over RPC yet"
@@ -394,6 +478,217 @@ fn dispatch_composer_method(
}
}
+fn dispatch_config_method(
+ config: &std::rc::Rc<std::cell::RefCell<crate::config::Config>>,
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<PluginValue, PhpThrow> {
+ let key = |position: usize| -> Result<String, PhpThrow> {
+ match args.get(position) {
+ // TODO(phase-e): lossy UTF-8; config keys are bytes in PHP.
+ Some(PluginValue::String(bytes)) => Ok(String::from_utf8_lossy(bytes).into_owned()),
+ other => Err(runtime_throw(format!(
+ "{method_name} expects a string key, got {other:?}"
+ ))),
+ }
+ };
+ let flags = |position: usize| -> Result<i64, PhpThrow> {
+ match args.get(position) {
+ None | Some(PluginValue::Null) => Ok(0),
+ Some(PluginValue::Int(flags)) => Ok(*flags),
+ other => Err(runtime_throw(format!(
+ "{method_name} expects int flags, got {other:?}"
+ ))),
+ }
+ };
+ match method_name {
+ "get" => {
+ let value = config
+ .borrow()
+ .get_with_flags(&key(0)?, flags(1)?)
+ .map_err(|error| runtime_throw(format!("get failed over RPC: {error}")))?;
+ Ok(PluginValue::from_php_mixed(&value))
+ }
+ "all" => {
+ let all = config
+ .borrow_mut()
+ .all(flags(0)?)
+ .map_err(|error| runtime_throw(format!("all failed over RPC: {error}")))?;
+ Ok(PluginValue::from_php_mixed(&PhpMixed::Array(all)))
+ }
+ "raw" => Ok(PluginValue::from_php_mixed(&PhpMixed::Array(
+ config.borrow().raw(),
+ ))),
+ "has" => Ok(PluginValue::Bool(config.borrow().has(&key(0)?))),
+ "getRepositories" => Ok(PluginValue::from_php_mixed(&PhpMixed::Array(
+ config.borrow().get_repositories(),
+ ))),
+ "getSourceOfValue" => Ok(PluginValue::string(
+ config.borrow_mut().get_source_of_value(&key(0)?),
+ )),
+ "merge" => {
+ let values = match args.first().map(PluginValue::to_php_mixed).transpose() {
+ Ok(Some(PhpMixed::Array(values))) => values,
+ Ok(Some(PhpMixed::List(items))) if items.is_empty() => IndexMap::new(),
+ Ok(other) => {
+ return Err(runtime_throw(format!(
+ "merge expects a config array, got {other:?}"
+ )));
+ }
+ Err(error) => {
+ return Err(runtime_throw(format!(
+ "merge could not decode its argument: {error:#}"
+ )));
+ }
+ };
+ let source = match args.get(1) {
+ None => crate::config::Config::SOURCE_UNKNOWN.to_string(),
+ Some(PluginValue::String(bytes)) => String::from_utf8_lossy(bytes).into_owned(),
+ other => {
+ return Err(runtime_throw(format!(
+ "merge expects a string source, got {other:?}"
+ )));
+ }
+ };
+ config.borrow_mut().merge(&values, &source);
+ Ok(PluginValue::Null)
+ }
+ // TODO(plugin): the config-source surface (getConfigSource / setAuthConfigSource / ...)
+ // needs proxy stubs for ConfigSourceInterface implementations, and prohibitUrlByConfig
+ // needs the IO argument decoded back into the Rust-side instance; both are widened on
+ // demand, driven by explicit errors from real plugins.
+ other => Err(runtime_throw(format!(
+ "the Config method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
+/// The download manager's contract is asynchronous on both sides: PHP declares a
+/// `PromiseInterface` return, Rust an `async fn`. The Rust future is driven to completion here
+/// and its value handed back as an already-settled React promise, which is the synchronous
+/// fallback of the promise design (`.ken/plugin-arch/design.md` §10.1.6) rather than the
+/// deferred resolution a concurrent engine would allow.
+fn dispatch_download_manager_method(
+ dm: &std::rc::Rc<std::cell::RefCell<dyn crate::downloader::DownloadManagerInterface>>,
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<PluginValue, PhpThrow> {
+ let string_arg = |position: usize| -> Result<String, PhpThrow> {
+ match args.get(position) {
+ // TODO(phase-e): lossy UTF-8; paths and types are bytes in PHP.
+ Some(PluginValue::String(bytes)) => Ok(String::from_utf8_lossy(bytes).into_owned()),
+ other => Err(runtime_throw(format!(
+ "{method_name} expects a string argument at position {position}, got {other:?}"
+ ))),
+ }
+ };
+ match method_name {
+ "setPreferSource" | "setPreferDist" => {
+ let preferred = match args.first() {
+ Some(PluginValue::Bool(preferred)) => *preferred,
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects a bool, got {other:?}"
+ )));
+ }
+ };
+ if method_name == "setPreferSource" {
+ dm.borrow_mut().set_prefer_source(preferred);
+ } else {
+ dm.borrow_mut().set_prefer_dist(preferred);
+ }
+ Ok(PluginValue::Null)
+ }
+ "download" | "prepare" | "install" | "update" | "remove" | "cleanup" => {
+ let resolved = crate::util::sync_executor::block_on(async {
+ let dm = dm.borrow();
+ match method_name {
+ "download" => {
+ dm.download(
+ package_from_arg(method_name, args.first())?,
+ &string_arg(1)?,
+ optional_package_from_arg(method_name, args.get(2))?,
+ )
+ .await
+ }
+ "prepare" => {
+ dm.prepare(
+ &string_arg(0)?,
+ package_from_arg(method_name, args.get(1))?,
+ &string_arg(2)?,
+ optional_package_from_arg(method_name, args.get(3))?,
+ )
+ .await
+ }
+ "install" => {
+ dm.install(
+ package_from_arg(method_name, args.first())?,
+ &string_arg(1)?,
+ )
+ .await
+ }
+ "update" => {
+ dm.update(
+ package_from_arg(method_name, args.first())?,
+ package_from_arg(method_name, args.get(1))?,
+ &string_arg(2)?,
+ )
+ .await
+ }
+ "remove" => {
+ dm.remove(
+ package_from_arg(method_name, args.first())?,
+ &string_arg(1)?,
+ )
+ .await
+ }
+ _ => {
+ dm.cleanup(
+ &string_arg(0)?,
+ package_from_arg(method_name, args.get(1))?,
+ &string_arg(2)?,
+ optional_package_from_arg(method_name, args.get(3))?,
+ )
+ .await
+ }
+ }
+ .map_err(|error| {
+ // TODO(plugin): the original exception class is collapsed to
+ // RuntimeException on this side of the boundary.
+ runtime_throw(format!("{method_name} failed over RPC: {error:#}"))
+ })
+ })?;
+ let value = match resolved {
+ Some(value) => PluginValue::from_php_mixed(&value),
+ None => PluginValue::Null,
+ };
+ resolved_promise(value)
+ }
+ // TODO(plugin): the downloader-facing surface (getDownloader / setDownloader /
+ // getDownloaderForPackage / getDownloaderType) needs proxy stubs for
+ // DownloaderInterface implementations before it can cross.
+ other => Err(runtime_throw(format!(
+ "the DownloadManager method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
+/// A `\React\Promise\PromiseInterface` already fulfilled with `value`, built in the worker so
+/// PHP callers get the object type their signatures declare.
+fn resolved_promise(value: PluginValue) -> Result<PluginValue, PhpThrow> {
+ match call_function_with_dispatcher(
+ "__shirabe_resolved_promise",
+ vec![value],
+ Some(&mut PluginRpcDispatcher::default()),
+ ) {
+ Ok(Ok(promise)) => Ok(promise),
+ Ok(Err(throw)) => Err(throw),
+ Err(error) => Err(runtime_throw(format!(
+ "creating a resolved promise in the plugin process failed: {error:#}"
+ ))),
+ }
+}
+
fn dispatch_event_dispatcher_method(
dispatcher: &std::rc::Rc<
std::cell::RefCell<dyn crate::event_dispatcher::EventDispatcherInterface>,
@@ -451,8 +746,40 @@ fn dispatch_repository_manager_method(
fn dispatch_repository_method(
repository: &RepositoryInterfaceHandle,
method_name: &str,
+ args: &[PluginValue],
) -> Result<PluginValue, PhpThrow> {
match method_name {
+ "hasPackage" => {
+ let package = package_from_arg(method_name, args.first())?;
+ let has = repository.has_package(package).map_err(|error| {
+ // TODO(plugin): the original exception class is collapsed to RuntimeException
+ // on this side of the boundary.
+ runtime_throw(format!("hasPackage failed over RPC: {error}"))
+ })?;
+ Ok(PluginValue::Bool(has))
+ }
+ "addPackage" | "removePackage" => {
+ let package = package_from_arg(method_name, args.first())?;
+ let mut borrowed = repository.borrow_mut();
+ let writable = borrowed
+ .as_writable_repository_interface_mut()
+ .ok_or_else(|| {
+ runtime_throw(format!(
+ "the repository behind this handle is not writable, so `{method_name}` cannot be called on it"
+ ))
+ })?;
+ let outcome = if method_name == "addPackage" {
+ writable.add_package(package)
+ } else {
+ writable.remove_package(package)
+ };
+ outcome.map_err(|error| {
+ // TODO(plugin): the original exception class is collapsed to RuntimeException
+ // on this side of the boundary.
+ runtime_throw(format!("{method_name} failed over RPC: {error}"))
+ })?;
+ Ok(PluginValue::Null)
+ }
"getPackages" => {
let packages = repository.borrow_mut().get_packages().map_err(|error| {
// TODO(plugin): the original exception class is collapsed to RuntimeException
@@ -555,6 +882,226 @@ fn decode_mirrors(
Ok(Some(mirrors))
}
+/// An `array<string, T>` of plain values as PHP shapes it, via the `PhpMixed` image of `T`.
+fn typed_map<T>(map: IndexMap<String, T>, to_mixed: impl Fn(T) -> PhpMixed) -> PluginValue {
+ string_keyed_map(
+ map.into_iter()
+ .map(|(key, value)| (key, to_mixed(value)))
+ .collect(),
+ )
+}
+
+/// A `list<array<string, T>>` as PHP shapes it.
+fn typed_map_list<T>(
+ rows: Vec<IndexMap<String, T>>,
+ to_mixed: impl Fn(T) -> PhpMixed + Copy,
+) -> PluginValue {
+ PluginValue::List(
+ rows.into_iter()
+ .map(|row| typed_map(row, to_mixed))
+ .collect(),
+ )
+}
+
+/// The `PhpMixed` image of an argument, as the wire codec decoded it.
+fn mixed_arg(method: &str, value: Option<&PluginValue>) -> Result<PhpMixed, PhpThrow> {
+ match value {
+ None => Ok(PhpMixed::Null),
+ Some(value) => value.to_php_mixed().map_err(|error| {
+ runtime_throw(format!("{method} could not decode its argument: {error:#}"))
+ }),
+ }
+}
+
+/// A PHP array argument as a map. An empty PHP array is indistinguishable from an empty list on
+/// the wire, so it decodes here as an empty map.
+fn map_arg(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<IndexMap<String, PhpMixed>, PhpThrow> {
+ match mixed_arg(method, value)? {
+ PhpMixed::Array(map) => Ok(map),
+ PhpMixed::List(items) if items.is_empty() => Ok(IndexMap::new()),
+ PhpMixed::List(items) => Ok(items
+ .into_iter()
+ .enumerate()
+ .map(|(index, item)| (index.to_string(), item))
+ .collect()),
+ other => Err(runtime_throw(format!(
+ "{method} expects an array, got {other:?}"
+ ))),
+ }
+}
+
+/// A PHP array argument as a list, accepting the keyed shape PHP allows anywhere a list is
+/// documented.
+fn list_arg(method: &str, value: Option<&PluginValue>) -> Result<Vec<PhpMixed>, PhpThrow> {
+ match mixed_arg(method, value)? {
+ PhpMixed::List(items) => Ok(items),
+ PhpMixed::Array(map) => Ok(map.into_values().collect()),
+ other => Err(runtime_throw(format!(
+ "{method} expects an array, got {other:?}"
+ ))),
+ }
+}
+
+fn as_string(method: &str, value: PhpMixed) -> Result<String, PhpThrow> {
+ match value {
+ PhpMixed::String(value) => Ok(value),
+ other => Err(runtime_throw(format!(
+ "{method} expects strings, got {other:?}"
+ ))),
+ }
+}
+
+fn as_int(method: &str, value: PhpMixed) -> Result<i64, PhpThrow> {
+ match value {
+ PhpMixed::Int(value) => Ok(value),
+ other => Err(runtime_throw(format!(
+ "{method} expects ints, got {other:?}"
+ ))),
+ }
+}
+
+fn string_map_arg(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<IndexMap<String, String>, PhpThrow> {
+ map_arg(method, value)?
+ .into_iter()
+ .map(|(key, value)| Ok((key, as_string(method, value)?)))
+ .collect()
+}
+
+fn int_map_arg(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<IndexMap<String, i64>, PhpThrow> {
+ map_arg(method, value)?
+ .into_iter()
+ .map(|(key, value)| Ok((key, as_int(method, value)?)))
+ .collect()
+}
+
+fn string_list_arg(method: &str, value: Option<&PluginValue>) -> Result<Vec<String>, PhpThrow> {
+ list_arg(method, value)?
+ .into_iter()
+ .map(|item| as_string(method, item))
+ .collect()
+}
+
+/// A `list<array<string, string>>` argument (`authors`, `aliases`).
+fn string_map_list_arg(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<Vec<IndexMap<String, String>>, PhpThrow> {
+ list_arg(method, value)?
+ .into_iter()
+ .map(|row| match row {
+ PhpMixed::Array(row) => row
+ .into_iter()
+ .map(|(key, value)| Ok((key, as_string(method, value)?)))
+ .collect(),
+ PhpMixed::List(items) if items.is_empty() => Ok(IndexMap::new()),
+ other => Err(runtime_throw(format!(
+ "{method} expects arrays of strings, got {other:?}"
+ ))),
+ })
+ .collect()
+}
+
+/// A `list<array<string, mixed>>` argument (`funding`).
+fn mixed_map_list_arg(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<Vec<IndexMap<String, PhpMixed>>, PhpThrow> {
+ list_arg(method, value)?
+ .into_iter()
+ .map(|row| match row {
+ PhpMixed::Array(row) => Ok(row),
+ PhpMixed::List(items) if items.is_empty() => Ok(IndexMap::new()),
+ other => Err(runtime_throw(format!(
+ "{method} expects arrays, got {other:?}"
+ ))),
+ })
+ .collect()
+}
+
+/// An `array<string, list<string>>` argument (`scripts`).
+fn string_list_map_arg(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<IndexMap<String, Vec<String>>, PhpThrow> {
+ map_arg(method, value)?
+ .into_iter()
+ .map(|(key, value)| {
+ let items = match value {
+ PhpMixed::List(items) => items,
+ PhpMixed::Array(map) => map.into_values().collect(),
+ other => {
+ return Err(runtime_throw(format!(
+ "{method} expects arrays of strings, got {other:?}"
+ )));
+ }
+ };
+ Ok((
+ key,
+ items
+ .into_iter()
+ .map(|item| as_string(method, item))
+ .collect::<Result<_, _>>()?,
+ ))
+ })
+ .collect()
+}
+
+fn bool_arg(method: &str, value: Option<&PluginValue>) -> Result<bool, PhpThrow> {
+ match value {
+ Some(PluginValue::Bool(value)) => Ok(*value),
+ other => Err(runtime_throw(format!(
+ "{method} expects a bool, got {other:?}"
+ ))),
+ }
+}
+
+fn required_string_arg(method: &str, value: Option<&PluginValue>) -> Result<String, PhpThrow> {
+ decode_optional_string(method, value)?
+ .ok_or_else(|| runtime_throw(format!("{method} expects a string")))
+}
+
+/// Resolves a package argument back to the Rust-side entity its proxy stub stands for.
+fn package_from_arg(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<PackageInterfaceHandle, PhpThrow> {
+ match value {
+ Some(PluginValue::RustHandle(handle)) => {
+ match R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned()) {
+ Some(RustEntity::Package(package)) => {
+ Ok(PackageInterfaceHandle::from_rc_unchecked(package))
+ }
+ _ => Err(runtime_throw(format!(
+ "{method} expects a package handle, got Rust handle {}",
+ handle.rhandle
+ ))),
+ }
+ }
+ other => Err(runtime_throw(format!(
+ "{method} expects a package argument, got {other:?}"
+ ))),
+ }
+}
+
+fn optional_package_from_arg(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<Option<PackageInterfaceHandle>, PhpThrow> {
+ match value {
+ None | Some(PluginValue::Null) => Ok(None),
+ other => Ok(Some(package_from_arg(method, other)?)),
+ }
+}
+
fn decode_optional_string(
method: &str,
value: Option<&PluginValue>,
@@ -568,6 +1115,75 @@ fn decode_optional_string(
}
}
+/// The read half of the `CompletePackage` / `RootPackage` surface, which `PackageInterface`
+/// does not carry. `None` means the method belongs to the base surface below.
+fn dispatch_complete_package_getter(
+ package: &AnyPackage,
+ method_name: &str,
+) -> Result<Option<PluginValue>, PhpThrow> {
+ let unavailable = || {
+ runtime_throw(format!(
+ "`{method_name}` is not available on this package over RPC"
+ ))
+ };
+ let value = match method_name {
+ "getScripts"
+ | "getRepositories"
+ | "getLicense"
+ | "getKeywords"
+ | "getDescription"
+ | "getHomepage"
+ | "getAuthors"
+ | "getSupport"
+ | "getFunding"
+ | "isAbandoned"
+ | "getReplacementPackage"
+ | "getArchiveName"
+ | "getArchiveExcludes" => {
+ let package = package
+ .as_complete_package_interface()
+ .ok_or_else(unavailable)?;
+ match method_name {
+ "getScripts" => typed_map(package.get_scripts(), |commands| {
+ PhpMixed::List(commands.into_iter().map(PhpMixed::String).collect())
+ }),
+ "getRepositories" => string_keyed_map(package.get_repositories()),
+ "getLicense" => string_list(package.get_license()),
+ "getKeywords" => string_list(package.get_keywords()),
+ "getDescription" => optional_string(package.get_description()),
+ "getHomepage" => optional_string(package.get_homepage()),
+ "getAuthors" => typed_map_list(package.get_authors(), PhpMixed::String),
+ "getSupport" => typed_map(package.get_support(), PhpMixed::String),
+ "getFunding" => typed_map_list(package.get_funding(), |value| value),
+ "isAbandoned" => PluginValue::Bool(package.is_abandoned()),
+ "getReplacementPackage" => optional_string(package.get_replacement_package()),
+ "getArchiveName" => optional_string(package.get_archive_name()),
+ _ => string_list(package.get_archive_excludes()),
+ }
+ }
+ "getAliases"
+ | "getMinimumStability"
+ | "getStabilityFlags"
+ | "getReferences"
+ | "getPreferStable"
+ | "getConfig" => {
+ let package = package
+ .as_root_package_interface()
+ .ok_or_else(unavailable)?;
+ match method_name {
+ "getAliases" => typed_map_list(package.get_aliases(), PhpMixed::String),
+ "getMinimumStability" => PluginValue::string(package.get_minimum_stability()),
+ "getStabilityFlags" => typed_map(package.get_stability_flags(), PhpMixed::Int),
+ "getReferences" => typed_map(package.get_references(), PhpMixed::String),
+ "getPreferStable" => PluginValue::Bool(package.get_prefer_stable()),
+ _ => string_keyed_map(package.get_config()),
+ }
+ }
+ _ => return Ok(None),
+ };
+ Ok(Some(value))
+}
+
fn dispatch_package_method(
package: &std::rc::Rc<std::cell::RefCell<AnyPackage>>,
method_name: &str,
@@ -685,9 +1301,214 @@ fn dispatch_package_method(
.set_transport_options(options);
return Ok(PluginValue::Null);
}
+ // `Package`'s own setters. The concrete subclasses inherit them (their PHP overrides in
+ // `RootPackage` delegate to the same base state), so the base package answers for every
+ // real variant.
+ "setType"
+ | "setTargetDir"
+ | "setExtra"
+ | "setBinaries"
+ | "setSourceType"
+ | "setDistSha1Checksum"
+ | "setSuggests"
+ | "setAutoload"
+ | "setDevAutoload"
+ | "setIncludePaths"
+ | "setPhpExt"
+ | "setNotificationUrl"
+ | "setIsDefaultBranch"
+ | "replaceVersion" => {
+ let mut borrowed = package.borrow_mut();
+ let package = borrowed.as_package_mut().ok_or_else(|| {
+ runtime_throw(format!(
+ "`{method_name}` is not available on an alias package over RPC"
+ ))
+ })?;
+ match method_name {
+ "setType" => package.set_type(required_string_arg(method_name, args.first())?),
+ "setTargetDir" => {
+ package.set_target_dir(decode_optional_string(method_name, args.first())?)
+ }
+ "setExtra" => package.set_extra(map_arg(method_name, args.first())?),
+ "setBinaries" => package.set_binaries(string_list_arg(method_name, args.first())?),
+ "setSourceType" => {
+ package.set_source_type(decode_optional_string(method_name, args.first())?)
+ }
+ "setDistSha1Checksum" => package
+ .set_dist_sha1_checksum(decode_optional_string(method_name, args.first())?),
+ "setSuggests" => package.set_suggests(string_map_arg(method_name, args.first())?),
+ "setAutoload" => package.set_autoload(map_arg(method_name, args.first())?),
+ "setDevAutoload" => package.set_dev_autoload(map_arg(method_name, args.first())?),
+ "setIncludePaths" => {
+ package.set_include_paths(string_list_arg(method_name, args.first())?)
+ }
+ "setPhpExt" => package.set_php_ext(match args.first() {
+ None | Some(PluginValue::Null) => None,
+ value => Some(map_arg(method_name, value)?),
+ }),
+ "setNotificationUrl" => {
+ package.set_notification_url(required_string_arg(method_name, args.first())?)
+ }
+ "setIsDefaultBranch" => {
+ package.set_is_default_branch(bool_arg(method_name, args.first())?)
+ }
+ _ => package.replace_version(
+ required_string_arg(method_name, args.first())?,
+ required_string_arg(method_name, args.get(1))?,
+ ),
+ }
+ return Ok(PluginValue::Null);
+ }
+ // TODO(plugin): the link setters take `array<string, Link>`, whose wire image is missing
+ // for the same reason the link getters below have none.
+ "setRequires" | "setConflicts" | "setProvides" | "setReplaces" | "setDevRequires" => {
+ if !list_arg(method_name, args.first())?.is_empty()
+ || !map_arg(method_name, args.first())?.is_empty()
+ {
+ return Err(runtime_throw(format!(
+ "the package method `{method_name}` takes Link values, whose encoding over RPC is not implemented yet"
+ )));
+ }
+ let mut borrowed = package.borrow_mut();
+ let package = borrowed.as_package_mut().ok_or_else(|| {
+ runtime_throw(format!(
+ "`{method_name}` is not available on an alias package over RPC"
+ ))
+ })?;
+ match method_name {
+ "setRequires" => package.set_requires(IndexMap::new()),
+ "setConflicts" => package.set_conflicts(IndexMap::new()),
+ "setProvides" => package.set_provides(IndexMap::new()),
+ "setReplaces" => package.set_replaces(IndexMap::new()),
+ _ => package.set_dev_requires(IndexMap::new()),
+ }
+ return Ok(PluginValue::Null);
+ }
+ // TODO(plugin): a \DateTimeInterface argument has to be decoded from a real PHP object in
+ // the child, which needs the value-object encoding `getReleaseDate` is missing too.
+ "setReleaseDate" => {
+ return match args.first() {
+ None | Some(PluginValue::Null) => {
+ let mut borrowed = package.borrow_mut();
+ let package = borrowed.as_package_mut().ok_or_else(|| {
+ runtime_throw(
+ "`setReleaseDate` is not available on an alias package over RPC"
+ .to_string(),
+ )
+ })?;
+ package.set_release_date(None);
+ Ok(PluginValue::Null)
+ }
+ _ => Err(runtime_throw(
+ "decoding a release date over RPC is not implemented yet".to_string(),
+ )),
+ };
+ }
+ "setScripts" | "setRepositories" | "setLicense" | "setKeywords" | "setDescription"
+ | "setHomepage" | "setAuthors" | "setSupport" | "setFunding" | "setAbandoned"
+ | "setArchiveName" | "setArchiveExcludes" => {
+ let mut borrowed = package.borrow_mut();
+ let package = borrowed
+ .as_complete_package_interface_mut()
+ .ok_or_else(|| {
+ runtime_throw(format!(
+ "`{method_name}` is not available on this package over RPC"
+ ))
+ })?;
+ match method_name {
+ "setScripts" => {
+ package.set_scripts(string_list_map_arg(method_name, args.first())?)
+ }
+ "setRepositories" => package.set_repositories(map_arg(method_name, args.first())?),
+ "setLicense" => package.set_license(string_list_arg(method_name, args.first())?),
+ "setKeywords" => package.set_keywords(string_list_arg(method_name, args.first())?),
+ "setDescription" => {
+ package.set_description(required_string_arg(method_name, args.first())?)
+ }
+ "setHomepage" => {
+ package.set_homepage(required_string_arg(method_name, args.first())?)
+ }
+ "setAuthors" => {
+ package.set_authors(string_map_list_arg(method_name, args.first())?)
+ }
+ "setSupport" => package.set_support(string_map_arg(method_name, args.first())?),
+ "setFunding" => package.set_funding(mixed_map_list_arg(method_name, args.first())?),
+ "setAbandoned" => package.set_abandoned(mixed_arg(method_name, args.first())?),
+ "setArchiveName" => {
+ package.set_archive_name(required_string_arg(method_name, args.first())?)
+ }
+ _ => package.set_archive_excludes(string_list_arg(method_name, args.first())?),
+ }
+ return Ok(PluginValue::Null);
+ }
+ "setStabilityFlags"
+ | "setMinimumStability"
+ | "setPreferStable"
+ | "setConfig"
+ | "setReferences"
+ | "setAliases" => {
+ let mut borrowed = package.borrow_mut();
+ let package = borrowed.as_root_package_interface_mut().ok_or_else(|| {
+ runtime_throw(format!(
+ "`{method_name}` is not available on this package over RPC"
+ ))
+ })?;
+ match method_name {
+ "setStabilityFlags" => {
+ package.set_stability_flags(int_map_arg(method_name, args.first())?)
+ }
+ "setMinimumStability" => {
+ package.set_minimum_stability(required_string_arg(method_name, args.first())?)
+ }
+ "setPreferStable" => {
+ package.set_prefer_stable(bool_arg(method_name, args.first())?)
+ }
+ "setConfig" => package.set_config(map_arg(method_name, args.first())?),
+ "setReferences" => {
+ package.set_references(string_map_arg(method_name, args.first())?)
+ }
+ _ => package.set_aliases(string_map_list_arg(method_name, args.first())?),
+ }
+ return Ok(PluginValue::Null);
+ }
+ // `BasePackage::$id` is the one public property of the package classes, so the stub's
+ // property forwarders only ever carry it.
+ "__get" | "__set" => {
+ let property = required_string_arg(method_name, args.first())?;
+ if property != "id" {
+ return Err(runtime_throw(format!(
+ "the package property `{property}` is not available over RPC"
+ )));
+ }
+ return if method_name == "__get" {
+ Ok(PluginValue::Int(
+ package.borrow().as_package_interface().get_id(),
+ ))
+ } else {
+ let id = match args.get(1) {
+ Some(PluginValue::Int(id)) => *id,
+ other => {
+ return Err(runtime_throw(format!(
+ "the package property `id` takes an int, got {other:?}"
+ )));
+ }
+ };
+ package.borrow_mut().as_package_interface_mut().set_id(id);
+ Ok(PluginValue::Null)
+ };
+ }
+ "equals" => {
+ let other = package_from_arg(method_name, args.first())?;
+ let this = PackageInterfaceHandle::from_rc_unchecked(package.clone());
+ return Ok(PluginValue::Bool(this.equals(&other)));
+ }
_ => {}
}
+ if let Some(value) = dispatch_complete_package_getter(&package.borrow(), method_name)? {
+ return Ok(value);
+ }
+
let borrowed = package.borrow();
let package = borrowed.as_package_interface();
match method_name {
@@ -788,6 +1609,16 @@ fn dispatch_package_method(
"__toString" => Ok(PluginValue::string(package.get_unique_name())),
"getPrettyString" => Ok(PluginValue::string(package.get_pretty_string())),
"isDefaultBranch" => Ok(PluginValue::Bool(package.is_default_branch())),
+ // `BasePackage`'s concrete methods are not forwarded by `PackageInterface`, so both are
+ // computed from the interface here, as `VersionSelector` already does for the second.
+ "isPlatform" => Ok(PluginValue::Bool(package.get_repository().is_some_and(
+ |repository| repository.is::<crate::repository::PlatformRepository>(),
+ ))),
+ "getStabilityPriority" => Ok(PluginValue::Int(
+ *crate::package::base_package::STABILITIES
+ .get(package.get_stability())
+ .unwrap_or(&crate::package::base_package::STABILITY_STABLE),
+ )),
"getTransportOptions" => Ok(string_keyed_map(package.get_transport_options())),
"getReleaseDate" => match package.get_release_date() {
None => Ok(PluginValue::Null),
@@ -797,9 +1628,6 @@ fn dispatch_package_method(
"encoding the release date over RPC is not implemented yet".to_string(),
)),
},
- // TODO(plugin): the concrete-class surface below PackageInterface (`Package`'s setters,
- // `CompletePackage`'s metadata, `RootPackage`'s root-only state) is widened on demand,
- // driven by explicit errors from real plugins.
other => Err(runtime_throw(format!(
"the package method `{other}` is not available over RPC yet"
))),
@@ -813,27 +1641,7 @@ fn dispatch_installation_manager_method(
) -> Result<PluginValue, PhpThrow> {
match method_name {
"getInstallPath" => {
- let package = match args.first() {
- Some(PluginValue::RustHandle(handle)) => {
- let entity = R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned());
- match entity {
- Some(RustEntity::Package(package)) => {
- PackageInterfaceHandle::from_rc_unchecked(package)
- }
- _ => {
- return Err(runtime_throw(format!(
- "getInstallPath expects a package handle, got Rust handle {}",
- handle.rhandle
- )));
- }
- }
- }
- other => {
- return Err(runtime_throw(format!(
- "getInstallPath expects a package argument, got {other:?}"
- )));
- }
- };
+ let package = package_from_arg(method_name, args.first())?;
Ok(match im.borrow().get_install_path(package) {
Some(path) => PluginValue::string(path),
None => PluginValue::Null,
@@ -1326,19 +2134,32 @@ impl PhpInstallerProxy {
Ok(repository_handle_value(&repo.as_repository_handle())?)
}
- /// The `?PromiseInterface` half of the installer contract. A plugin installer that returns
- /// a real promise needs the promise machinery the RPC boundary does not carry yet, so it is
- /// an explicit error rather than a silently dropped continuation.
+ /// The `?PromiseInterface` half of the installer contract. The Rust callers await the
+ /// installer's effects rather than chaining continuations, so a returned promise is drained
+ /// here: an already-settled one yields its value (or raises its rejection reason), while a
+ /// still-pending one is an explicit error — resolving it would need the concurrent execution
+ /// engine the boundary does not have (`.ken/plugin-arch/design.md` §10.1.6).
fn promise_result(&self, method: &str, value: PluginValue) -> anyhow::Result<Option<PhpMixed>> {
- match value {
+ let handle = match value {
+ PluginValue::Null => return Ok(None),
+ PluginValue::PhpHandle(handle) => handle,
+ other => return Err(self.unsupported_shape(method, &other)),
+ };
+ if !php_is_a(&handle, "React\\Promise\\PromiseInterface")? {
+ return Err(self.unsupported_shape(method, &PluginValue::PhpHandle(handle)));
+ }
+ let phandle = handle.phandle;
+ let settled = unwrap_php_result(call_function_with_dispatcher(
+ "__shirabe_settle_promise",
+ vec![PluginValue::PhpHandle(handle)],
+ Some(&mut PluginRpcDispatcher::default()),
+ ));
+ // The promise entity was interned in the worker's P table when it crossed; it has no
+ // owner on this side beyond this call, on the rejection path too.
+ let _ = release_php_handle(phandle);
+ match settled? {
PluginValue::Null => Ok(None),
- other => Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
- message: format!(
- "{}::{method}() returned a promise, which cannot cross the RPC boundary yet: {other:?}",
- self.handle.class
- ),
- code: 0,
- })),
+ other => Ok(Some(other.to_php_mixed()?)),
}
}