diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-06 01:50:34 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-06 01:50:34 +0900 |
| commit | 4de018826e9dce90fd5cb78d468641478327ec99 (patch) | |
| tree | 0843f7e3e8c50886c8470ace17fcb6fed937be4f | |
| parent | da602f1cb1d555c7826fa3d026df66b82061cda4 (diff) | |
| download | php-shirabe-4de018826e9dce90fd5cb78d468641478327ec99.tar.gz php-shirabe-4de018826e9dce90fd5cb78d468641478327ec99.tar.zst php-shirabe-4de018826e9dce90fd5cb78d468641478327ec99.zip | |
feat(plugin): run plugin-provided installers through the RPC worker
A plugin can now hand an InstallerInterface implementation to
InstallationManager::addInstaller across the wire, and a legacy
composer-installer package is loaded as one; both are backed by a
PhpInstallerProxy forwarding the whole installer contract to the entity in
the PHP worker. An installer returning a real promise is an explicit error
until promises can cross the boundary.
InstallationManager takes installers as shared handles instead of boxes, so
the object identity removeInstaller and PluginManager's registeredPlugins
compare against survives registration, and holds them in a RefCell:
Installer::run keeps a shared borrow of the manager for the whole run, and a
plugin activated inside it registers its installer from there. The type
cache keys on the installer itself, like upstream, so re-entrant
registration cannot leave a stale index behind. InstallerInterface::supports
is fallible for the same reason getCapabilities and getCommands are: it
answers over RPC.
Cloning a proxy stub clones the Rust-side entity and rebinds the copy to the
fresh handle. Previously only the classes declaring __clone got a throwing
body, and the rest let two stubs share (and twice release) one handle.
Package entities answer with AnyPackage::dup, which already carries
BasePackage::__clone and the RootAliasPackage override; the others are an
explicit error.
The package proxy covers the whole PackageInterface surface; only the link
maps and the release date still lack a wire image for their value objects.
PluginManager gains a test-only seam for the reported Plugin API version,
and the three PluginInstallerTest cases that need it are ported.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
28 files changed, 948 insertions, 173 deletions
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 c0651081..30f8c0e8 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/EventDispatcher/EventDispatcher.php @@ -44,6 +44,16 @@ class EventDispatcher implements \ShirabeRustStub ]; } + 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 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 55da6415..b3e0fb51 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/IO/BaseIO.php @@ -42,6 +42,16 @@ abstract class BaseIO implements IOInterface, \ShirabeRustStub ]; } + 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 isInteractive() { return \ShirabeRpcRuntime::callRust($this->__rhandle, 'isInteractive', []); 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 35682561..d458d30b 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Installer/InstallationManager.php @@ -50,6 +50,16 @@ class InstallationManager implements \ShirabeRustStub ]; } + 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 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 e55c3fb5..de8037ca 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Package/BasePackage.php @@ -42,6 +42,16 @@ abstract class BasePackage implements PackageInterface, \ShirabeRustStub ]; } + 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 const STABILITY_STABLE = 0; public const STABILITY_RC = 5; public const STABILITY_BETA = 10; @@ -75,13 +85,6 @@ abstract class BasePackage implements PackageInterface, \ShirabeRustStub \ShirabeRpcRuntime::callRust($this->__rhandle, '__set', [$name, $value]); } - public function __clone() - { - // Cloning a proxy is an open design question; fail instead of silently sharing - // the Rust-side entity between two stub instances. - throw new \RuntimeException('Shirabe does not support cloning ' . static::class . ' inside the plugin process yet'); - } - public static function packageNameToRegexp(string $allowPattern, string $wrap = '{^%s$}i'): string { $cleanedAllowPattern = str_replace('\\*', '.*', preg_quote($allowPattern)); diff --git a/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php b/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php index 588800dc..c60edbb2 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/PartialComposer.php @@ -46,6 +46,16 @@ class PartialComposer implements \ShirabeRustStub ]; } + 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 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 33454fa2..51e6a462 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/ArrayRepository.php @@ -42,6 +42,16 @@ class ArrayRepository implements RepositoryInterface, \ShirabeRustStub ]; } + 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 hasPackage(PackageInterface $package) { return \ShirabeRpcRuntime::callRust($this->__rhandle, 'hasPackage', [$package]); 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 252bab4d..5635c0dc 100644 --- a/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php +++ b/crates/shirabe-php-rpc/php/stubs/Composer/Repository/RepositoryManager.php @@ -42,6 +42,16 @@ class RepositoryManager implements \ShirabeRustStub ]; } + 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 findPackage(string $name, $constraint): ?PackageInterface { return \ShirabeRpcRuntime::callRust($this->__rhandle, 'findPackage', [$name, $constraint]); diff --git a/crates/shirabe-php-rpc/php/worker.php b/crates/shirabe-php-rpc/php/worker.php index 241dad57..12297c83 100644 --- a/crates/shirabe-php-rpc/php/worker.php +++ b/crates/shirabe-php-rpc/php/worker.php @@ -51,6 +51,16 @@ final class ShirabeRustObjectRegistry return $stub; } + /** + * Interns a stub the registry did not build: a `__clone` forwarder rebinds the copy PHP + * made to a freshly cloned entity, and that pairing has to be visible to later crossings + * of the same handle. + */ + public static function adopt(int $rhandle, object $stub): void + { + self::$internTable[$rhandle] = WeakReference::create($stub); + } + /** Invoked when an EpochBump frame arrives. No-op if the stub already died. */ public static function bumpEpoch(int $rhandle, int $epoch): void { diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs index 1e289b08..756ebfac 100644 --- a/crates/shirabe/src/command/create_project_command.rs +++ b/crates/shirabe/src/command/create_project_command.rs @@ -1011,7 +1011,7 @@ impl CreateProjectCommand { { let mut im = installation_manager.borrow_mut(); im.set_output_progress(!no_progress); - im.add_installer(Box::new(project_installer)); + im.add_installer(std::rc::Rc::new(project_installer)); } let installed_repo = crate::repository::InstalledRepositoryInterfaceHandle::new( InstalledArrayRepository::new()?, diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs index fe3b5289..a1fda63b 100644 --- a/crates/shirabe/src/factory.rs +++ b/crates/shirabe/src/factory.rs @@ -1291,7 +1291,7 @@ impl Factory { ))); im.borrow_mut() - .add_installer(Box::new(crate::installer::LibraryInstaller::new( + .add_installer(std::rc::Rc::new(crate::installer::LibraryInstaller::new( io.clone(), composer.clone(), None, @@ -1299,14 +1299,15 @@ impl Factory { Some(binary_installer.clone()), ))); im.borrow_mut() - .add_installer(Box::new(crate::installer::PluginInstaller::new( + .add_installer(std::rc::Rc::new(crate::installer::PluginInstaller::new( io.clone(), composer, Some(fs), Some(binary_installer), ))); - im.borrow_mut() - .add_installer(Box::new(crate::installer::MetapackageInstaller::new(io))); + im.borrow_mut().add_installer(std::rc::Rc::new( + crate::installer::MetapackageInstaller::new(io), + )); } fn purge_packages( diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs index 31bfaa2c..439026b0 100644 --- a/crates/shirabe/src/installer/installation_manager.rs +++ b/crates/shirabe/src/installer/installation_manager.rs @@ -33,12 +33,12 @@ pub struct InstallationManager { /// Rc rather than Box so `get_installer` can hand out shareable handles: the download/cleanup /// futures collected for Loop::wait must own their installer beyond the loop iteration that /// created them (PHP closures capture $installer the same way). - installers: Vec<std::rc::Rc<dyn InstallerInterface>>, - /// Maps a package type to the index of its installer in `installers`. PHP caches the installer - /// instance itself; here we store an index instead. The index never dangles because both - /// `add_installer` and `remove_installer` clear the cache whenever `installers` changes. + /// RefCell so a plugin activated from inside `execute` — which holds a shared borrow of this + /// manager for the whole run — can still register its own installer. + installers: std::cell::RefCell<Vec<std::rc::Rc<dyn InstallerInterface>>>, + /// Maps a package type to its installer. /// RefCell so lookups can populate the cache through `&self` from concurrent operation chains. - cache: std::cell::RefCell<IndexMap<String, usize>>, + cache: std::cell::RefCell<IndexMap<String, std::rc::Rc<dyn InstallerInterface>>>, /// RefCell so mark_for_notification works through `&self` from concurrent operation chains. notifiable_packages: std::cell::RefCell<IndexMap<String, Vec<PackageInterfaceHandle>>>, loop_: std::rc::Rc<std::cell::RefCell<Loop>>, @@ -69,7 +69,7 @@ impl InstallationManager { event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>, ) -> Self { Self { - installers: vec![], + installers: std::cell::RefCell::new(vec![]), cache: std::cell::RefCell::new(IndexMap::new()), notifiable_packages: std::cell::RefCell::new(IndexMap::new()), loop_, @@ -133,30 +133,25 @@ impl InstallationManager { } /// Adds installer - pub fn add_installer(&mut self, installer: Box<dyn InstallerInterface>) { - array_unshift(&mut self.installers, std::rc::Rc::from(installer)); - self.cache = std::cell::RefCell::new(IndexMap::new()); - } - - /// For testing only: adds an installer as a pre-built shared handle, so the caller keeps an - /// identity handle usable for PHP `assertSame`-style comparisons (`Rc::ptr_eq`) and for - /// `remove_installer`. `add_installer` cannot serve because `Rc::from(Box)` reallocates, - /// losing the caller's pointer identity. - pub fn __add_installer(&mut self, installer: std::rc::Rc<dyn InstallerInterface>) { - array_unshift(&mut self.installers, installer); - self.cache = std::cell::RefCell::new(IndexMap::new()); + /// + /// The installer is taken as a shared handle: PHP hands over an object reference and both + /// sides keep the same identity afterwards, which `removeInstaller` and the plugin + /// manager's `registeredPlugins` bookkeeping compare against. + pub fn add_installer(&self, installer: std::rc::Rc<dyn InstallerInterface>) { + array_unshift(&mut self.installers.borrow_mut(), installer); + self.cache.borrow_mut().clear(); } /// Removes installer - pub fn remove_installer(&mut self, installer: &dyn InstallerInterface) { + pub fn remove_installer(&self, installer: &dyn InstallerInterface) { let target = installer as *const dyn InstallerInterface as *const (); - let key = self - .installers + let mut installers = self.installers.borrow_mut(); + let key = installers .iter() .position(|inst| &**inst as *const dyn InstallerInterface as *const () == target); if let Some(k) = key { - array_splice(&mut self.installers, k as i64, Some(1), vec![]); - self.cache = std::cell::RefCell::new(IndexMap::new()); + array_splice(&mut installers, k as i64, Some(1), vec![]); + self.cache.borrow_mut().clear(); } } @@ -166,7 +161,9 @@ impl InstallationManager { /// disabling the PluginManager. This ensures that no third-party /// code is ever executed. pub fn disable_plugins(&mut self) { - for installer in self.installers.iter() { + // Cloned out: `disablePlugins` reaches into the plugin manager, which may reach back. + let installers = self.installers.borrow().clone(); + for installer in installers.iter() { if let Some(plugin_installer) = installer.as_plugin_installer() { plugin_installer.disable_plugins(); } @@ -180,17 +177,18 @@ impl InstallationManager { ) -> anyhow::Result<std::rc::Rc<dyn InstallerInterface>> { let r#type = strtolower(r#type); - if let Some(&index) = self.cache.borrow().get(&r#type) { - return Ok(self.installers[index].clone()); + if let Some(installer) = self.cache.borrow().get(&r#type) { + return Ok(installer.clone()); } - let index = self - .installers - .iter() - .position(|installer| installer.supports(&r#type)); - if let Some(index) = index { - self.cache.borrow_mut().insert(r#type, index); - return Ok(self.installers[index].clone()); + // Cloned out: a PHP-backed installer answers `supports` over RPC, and the plugin behind + // it can register a further installer from that call. + let installers = self.installers.borrow().clone(); + for installer in installers { + if installer.supports(&r#type)? { + self.cache.borrow_mut().insert(r#type, installer.clone()); + return Ok(installer); + } } Err(InvalidArgumentException { @@ -1020,8 +1018,8 @@ pub trait InstallationManagerInterface: std::fmt::Debug { unimplemented!("as_any is only implemented for the concrete InstallationManager") } - fn add_installer(&mut self, installer: Box<dyn InstallerInterface>); - fn remove_installer(&mut self, installer: &dyn InstallerInterface); + fn add_installer(&self, installer: std::rc::Rc<dyn InstallerInterface>); + fn remove_installer(&self, installer: &dyn InstallerInterface); fn disable_plugins(&mut self); fn is_package_installed( &mut self, @@ -1047,12 +1045,12 @@ impl InstallationManagerInterface for InstallationManager { self } - fn add_installer(&mut self, installer: Box<dyn InstallerInterface>) { - self.add_installer(installer); + fn add_installer(&self, installer: std::rc::Rc<dyn InstallerInterface>) { + InstallationManager::add_installer(self, installer); } - fn remove_installer(&mut self, installer: &dyn InstallerInterface) { - self.remove_installer(installer); + fn remove_installer(&self, installer: &dyn InstallerInterface) { + InstallationManager::remove_installer(self, installer); } fn disable_plugins(&mut self) { diff --git a/crates/shirabe/src/installer/installer_interface.rs b/crates/shirabe/src/installer/installer_interface.rs index bc464735..fc273916 100644 --- a/crates/shirabe/src/installer/installer_interface.rs +++ b/crates/shirabe/src/installer/installer_interface.rs @@ -8,7 +8,7 @@ use shirabe_php_shim::PhpMixed; #[async_trait::async_trait(?Send)] pub trait InstallerInterface: std::fmt::Debug { - fn supports(&self, package_type: &str) -> bool; + fn supports(&self, package_type: &str) -> anyhow::Result<bool>; fn is_installed( &self, diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs index c5a9608e..479fc6d5 100644 --- a/crates/shirabe/src/installer/library_installer.rs +++ b/crates/shirabe/src/installer/library_installer.rs @@ -232,11 +232,11 @@ impl LibraryInstaller { #[async_trait::async_trait(?Send)] impl InstallerInterface for LibraryInstaller { - fn supports(&self, package_type: &str) -> bool { - match &self.r#type { + fn supports(&self, package_type: &str) -> anyhow::Result<bool> { + Ok(match &self.r#type { Some(t) => package_type == t, None => true, - } + }) } fn is_installed( diff --git a/crates/shirabe/src/installer/metapackage_installer.rs b/crates/shirabe/src/installer/metapackage_installer.rs index e2821fd3..52c152ea 100644 --- a/crates/shirabe/src/installer/metapackage_installer.rs +++ b/crates/shirabe/src/installer/metapackage_installer.rs @@ -24,8 +24,8 @@ impl MetapackageInstaller { #[async_trait::async_trait(?Send)] impl InstallerInterface for MetapackageInstaller { - fn supports(&self, package_type: &str) -> bool { - package_type == "metapackage" + fn supports(&self, package_type: &str) -> anyhow::Result<bool> { + Ok(package_type == "metapackage") } fn is_installed( diff --git a/crates/shirabe/src/installer/noop_installer.rs b/crates/shirabe/src/installer/noop_installer.rs index 68a2b981..95f31f2e 100644 --- a/crates/shirabe/src/installer/noop_installer.rs +++ b/crates/shirabe/src/installer/noop_installer.rs @@ -10,8 +10,8 @@ pub struct NoopInstaller; #[async_trait::async_trait(?Send)] impl InstallerInterface for NoopInstaller { - fn supports(&self, _package_type: &str) -> bool { - true + fn supports(&self, _package_type: &str) -> anyhow::Result<bool> { + Ok(true) } fn is_installed( diff --git a/crates/shirabe/src/installer/plugin_installer.rs b/crates/shirabe/src/installer/plugin_installer.rs index 1e7a2636..7683a7d5 100644 --- a/crates/shirabe/src/installer/plugin_installer.rs +++ b/crates/shirabe/src/installer/plugin_installer.rs @@ -73,8 +73,8 @@ impl PluginInstaller { #[async_trait::async_trait(?Send)] impl InstallerInterface for PluginInstaller { - fn supports(&self, package_type: &str) -> bool { - package_type == "composer-plugin" || package_type == "composer-installer" + fn supports(&self, package_type: &str) -> anyhow::Result<bool> { + Ok(package_type == "composer-plugin" || package_type == "composer-installer") } fn is_installed( diff --git a/crates/shirabe/src/installer/project_installer.rs b/crates/shirabe/src/installer/project_installer.rs index 6fee682f..59d44410 100644 --- a/crates/shirabe/src/installer/project_installer.rs +++ b/crates/shirabe/src/installer/project_installer.rs @@ -31,8 +31,8 @@ impl ProjectInstaller { #[async_trait::async_trait(?Send)] impl InstallerInterface for ProjectInstaller { - fn supports(&self, _package_type: &str) -> bool { - true + fn supports(&self, _package_type: &str) -> anyhow::Result<bool> { + Ok(true) } fn is_installed( diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs index d13019e3..701e83cc 100644 --- a/crates/shirabe/src/plugin/php_plugin_proxy.rs +++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs @@ -20,8 +20,8 @@ use crate::plugin::capability::{Capability, CommandProvider}; use crate::plugin::capable::Capable; use crate::plugin::plugin_interface::PluginInterface; use crate::repository::{ - InstalledArrayRepository, InstalledFilesystemRepository, RepositoryInterfaceHandle, - RepositoryManagerInterface, + InstalledArrayRepository, InstalledFilesystemRepository, InstalledRepositoryInterfaceHandle, + RepositoryInterfaceHandle, RepositoryManagerInterface, }; use indexmap::IndexMap; use shirabe_external_packages::symfony::console::command::command::Command; @@ -292,6 +292,12 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> { // The entity is cloned out so no table borrow is held while the handler runs (a // handler that re-enters register_*_entity would otherwise panic on the RefCell). let entity = R_TABLE.with(|table| table.borrow().get(&rhandle).cloned()); + if method_name == "__shirabeClone" { + return match entity { + Some(entity) => clone_entity(&entity), + None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))), + }; + } match entity { Some(RustEntity::Io(io)) => dispatch_io_method(&io, method_name, &args), Some(RustEntity::Composer(composer)) => { @@ -317,6 +323,36 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> { } } +/// 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. +fn clone_entity(entity: &RustEntity) -> Result<PluginValue, PhpThrow> { + let rhandle = match entity { + // `AnyPackage::dup` carries `BasePackage::__clone` (repository reset, id = -1) and the + // `RootAliasPackage::__clone` override. + RustEntity::Package(package) => { + let cloned = package.borrow().dup(); + register_entity(RustEntity::Package(std::rc::Rc::new( + std::cell::RefCell::new(cloned), + ))) + } + RustEntity::Composer(_) + | RustEntity::Io(_) + | RustEntity::InstallationManager(_) + | RustEntity::RepositoryManager(_) + | RustEntity::Repository(_) + | RustEntity::EventDispatcher(_) => { + return Err(runtime_throw( + "cloning this Rust-side entity over RPC is not supported".to_string(), + )); + } + }; + Ok(PluginValue::List(vec![ + PluginValue::Int(rhandle as i64), + PluginValue::Int(0), + ])) +} + fn dispatch_composer_method( composer: &ComposerHandle, method_name: &str, @@ -437,22 +473,259 @@ fn dispatch_repository_method( } } +/// A PHP string-or-null wire value. +fn optional_string(value: Option<String>) -> PluginValue { + match value { + Some(value) => PluginValue::string(value), + None => PluginValue::Null, + } +} + +fn string_list(values: Vec<String>) -> PluginValue { + PluginValue::List(values.into_iter().map(PluginValue::string).collect()) +} + +/// `?list<array{url: string, preferred: bool}>` as PHP shapes it. +fn mirror_list(mirrors: Option<Vec<crate::package::Mirror>>) -> PluginValue { + match mirrors { + None => PluginValue::Null, + Some(mirrors) => PluginValue::List( + mirrors + .into_iter() + .map(|mirror| { + PluginValue::Array(IndexMap::from([ + (b"url".to_vec(), PluginValue::string(mirror.url)), + (b"preferred".to_vec(), PluginValue::Bool(mirror.preferred)), + ])) + }) + .collect(), + ), + } +} + +/// An `array<string, mixed>` as PHP shapes it: empty maps cross as a list, since an empty PHP +/// array is indistinguishable from an empty list on the wire. +fn string_keyed_map(map: IndexMap<String, PhpMixed>) -> PluginValue { + if map.is_empty() { + PluginValue::List(Vec::new()) + } else { + PluginValue::from_php_mixed(&PhpMixed::Array(map)) + } +} + +/// The inverse of `mirror_list`. +fn decode_mirrors( + method: &str, + value: Option<&PluginValue>, +) -> Result<Option<Vec<crate::package::Mirror>>, PhpThrow> { + let rows = match value { + None | Some(PluginValue::Null) => return Ok(None), + Some(PluginValue::List(rows)) => rows.clone(), + Some(PluginValue::Array(rows)) => rows.values().cloned().collect(), + other => { + return Err(runtime_throw(format!( + "{method} expects a list of mirrors or null, got {other:?}" + ))); + } + }; + let mut mirrors = Vec::with_capacity(rows.len()); + for row in rows { + let row = match row { + PluginValue::Array(row) => row, + other => { + return Err(runtime_throw(format!( + "{method} expects mirror maps, got {other:?}" + ))); + } + }; + let url = match row.get(b"url".as_slice()) { + Some(PluginValue::String(url)) => String::from_utf8_lossy(url).into_owned(), + other => { + return Err(runtime_throw(format!( + "{method} expects a string `url` in every mirror, got {other:?}" + ))); + } + }; + let preferred = matches!( + row.get(b"preferred".as_slice()), + Some(PluginValue::Bool(true)) + ); + mirrors.push(crate::package::Mirror { url, preferred }); + } + Ok(Some(mirrors)) +} + +fn decode_optional_string( + method: &str, + value: Option<&PluginValue>, +) -> Result<Option<String>, PhpThrow> { + match value { + None | Some(PluginValue::Null) => Ok(None), + Some(PluginValue::String(bytes)) => Ok(Some(String::from_utf8_lossy(bytes).into_owned())), + other => Err(runtime_throw(format!( + "{method} expects a string or null, got {other:?}" + ))), + } +} + fn dispatch_package_method( package: &std::rc::Rc<std::cell::RefCell<AnyPackage>>, method_name: &str, args: &[PluginValue], ) -> Result<PluginValue, PhpThrow> { - let package = package.borrow(); - let package = package.as_package_interface(); + // The link getters return `array<string, Link>`; only the empty case has a wire image so + // far (an empty PHP array crosses as a list). + // + // TODO(plugin): Link is a rust-snapshot value whose constraint field must materialize as a + // real composer/semver object in the child; the snapshot encoding does not exist yet. + let links = |links: IndexMap<String, crate::package::Link>| -> Result<PluginValue, PhpThrow> { + if links.is_empty() { + Ok(PluginValue::List(Vec::new())) + } else { + Err(runtime_throw(format!( + "the package method `{method_name}` returns Link values, whose encoding over RPC is not implemented yet" + ))) + } + }; + + // Mutators borrow mutably and must not hold the borrow across the shared-borrow arms. + match method_name { + "setId" => { + let id = match args.first() { + Some(PluginValue::Int(id)) => *id, + other => { + return Err(runtime_throw(format!( + "setId expects an int, got {other:?}" + ))); + } + }; + package.borrow_mut().as_package_interface_mut().set_id(id); + return Ok(PluginValue::Null); + } + "setInstallationSource" + | "setSourceReference" + | "setSourceUrl" + | "setDistUrl" + | "setDistType" + | "setDistReference" + | "setSourceDistReferences" => { + let value = decode_optional_string(method_name, args.first())?; + let mut borrowed = package.borrow_mut(); + let package = borrowed.as_package_interface_mut(); + match method_name { + "setInstallationSource" => package.set_installation_source(value), + "setSourceReference" => package.set_source_reference(value), + "setSourceUrl" => package.set_source_url(value), + "setDistUrl" => package.set_dist_url(value), + "setDistType" => package.set_dist_type(value), + "setDistReference" => package.set_dist_reference(value), + _ => package.set_source_dist_references(value.ok_or_else(|| { + runtime_throw("setSourceDistReferences expects a reference string".to_string()) + })?), + } + return Ok(PluginValue::Null); + } + "setSourceMirrors" | "setDistMirrors" => { + let mirrors = decode_mirrors(method_name, args.first())?; + let mut borrowed = package.borrow_mut(); + let package = borrowed.as_package_interface_mut(); + if method_name == "setSourceMirrors" { + package.set_source_mirrors(mirrors); + } else { + package.set_dist_mirrors(mirrors); + } + return Ok(PluginValue::Null); + } + "setRepository" => { + let repository = match args.first() { + Some(PluginValue::RustHandle(handle)) => { + match R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned()) { + Some(RustEntity::Repository(repository)) => repository, + _ => { + return Err(runtime_throw(format!( + "setRepository expects a repository handle, got Rust handle {}", + handle.rhandle + ))); + } + } + } + other => { + return Err(runtime_throw(format!( + "setRepository expects a repository argument, got {other:?}" + ))); + } + }; + package + .borrow_mut() + .as_package_interface_mut() + .set_repository(repository) + .map_err(|error| runtime_throw(format!("setRepository failed: {error}")))?; + return Ok(PluginValue::Null); + } + "setTransportOptions" => { + let options = match args.first() { + Some(value) => match value.to_php_mixed().map_err(|error| { + runtime_throw(format!( + "setTransportOptions could not decode its argument: {error:#}" + )) + })? { + PhpMixed::Array(options) => options, + PhpMixed::List(items) if items.is_empty() => IndexMap::new(), + other => { + return Err(runtime_throw(format!( + "setTransportOptions expects an array, got {other:?}" + ))); + } + }, + None => IndexMap::new(), + }; + package + .borrow_mut() + .as_package_interface_mut() + .set_transport_options(options); + return Ok(PluginValue::Null); + } + _ => {} + } + + let borrowed = package.borrow(); + let package = borrowed.as_package_interface(); match method_name { "getName" => Ok(PluginValue::string(package.get_name().to_string())), + "getPrettyName" => Ok(PluginValue::string(package.get_pretty_name().to_string())), + "getNames" => { + let provides = match args.first() { + None => true, + Some(PluginValue::Bool(provides)) => *provides, + other => { + return Err(runtime_throw(format!( + "getNames expects a bool provides flag, got {other:?}" + ))); + } + }; + Ok(string_list(package.get_names(provides))) + } + "getId" => Ok(PluginValue::Int(package.get_id())), + "isDev" => Ok(PluginValue::Bool(package.is_dev())), "getType" => Ok(PluginValue::string(package.get_type())), + "getTargetDir" => Ok(optional_string(package.get_target_dir())), + "getExtra" => Ok(string_keyed_map(package.get_extra())), + "getInstallationSource" => Ok(optional_string(package.get_installation_source())), + "getSourceType" => Ok(optional_string(package.get_source_type())), + "getSourceUrl" => Ok(optional_string(package.get_source_url())), + "getSourceUrls" => Ok(string_list(package.get_source_urls())), + "getSourceReference" => Ok(optional_string(package.get_source_reference())), + "getSourceMirrors" => Ok(mirror_list(package.get_source_mirrors())), + "getDistType" => Ok(optional_string(package.get_dist_type())), + "getDistUrl" => Ok(optional_string(package.get_dist_url())), + "getDistUrls" => Ok(string_list(package.get_dist_urls())), + "getDistReference" => Ok(optional_string(package.get_dist_reference())), + "getDistSha1Checksum" => Ok(optional_string(package.get_dist_sha1_checksum())), + "getDistMirrors" => Ok(mirror_list(package.get_dist_mirrors())), + "getVersion" => Ok(PluginValue::string(package.get_version().to_string())), "getPrettyVersion" => Ok(PluginValue::string( package.get_pretty_version().to_string(), )), - "getExtra" => Ok(PluginValue::from_php_mixed(&PhpMixed::Array( - package.get_extra(), - ))), "getFullPrettyVersion" => { let truncate = match args.first() { None => true, @@ -477,22 +750,56 @@ fn dispatch_package_method( package.get_full_pretty_version(truncate, display_mode), )) } - "getRequires" => { - let requires = package.get_requires(); - if requires.is_empty() { - // An empty PHP array crosses the wire as a list. + "getStability" => Ok(PluginValue::string(package.get_stability().to_string())), + "getRequires" => links(package.get_requires()), + "getConflicts" => links(package.get_conflicts()), + "getProvides" => links(package.get_provides()), + "getReplaces" => links(package.get_replaces()), + "getDevRequires" => links(package.get_dev_requires()), + "getSuggests" => { + let suggests = package.get_suggests(); + if suggests.is_empty() { Ok(PluginValue::List(Vec::new())) } else { - // TODO(plugin): Link is a rust-snapshot value whose constraint field must - // materialize as a real composer/semver object in the child; the snapshot - // encoding does not exist yet. - Err(runtime_throw( - "encoding Link values over RPC is not implemented yet".to_string(), + Ok(PluginValue::Array( + suggests + .into_iter() + .map(|(name, description)| { + (name.into_bytes(), PluginValue::string(description)) + }) + .collect(), )) } } - // TODO(plugin): the remaining PackageInterface surface (setters included) is widened - // on demand, driven by explicit errors from real plugins. + "getAutoload" => Ok(string_keyed_map(package.get_autoload())), + "getDevAutoload" => Ok(string_keyed_map(package.get_dev_autoload())), + "getIncludePaths" => Ok(string_list(package.get_include_paths())), + "getPhpExt" => Ok(match package.get_php_ext() { + Some(config) => string_keyed_map(config), + None => PluginValue::Null, + }), + "getRepository" => match package.get_repository() { + Some(repository) => repository_handle_value(&repository), + None => Ok(PluginValue::Null), + }, + "getBinaries" => Ok(string_list(package.get_binaries())), + "getUniqueName" => Ok(PluginValue::string(package.get_unique_name())), + "getNotificationUrl" => Ok(optional_string(package.get_notification_url())), + "__toString" => Ok(PluginValue::string(package.get_unique_name())), + "getPrettyString" => Ok(PluginValue::string(package.get_pretty_string())), + "isDefaultBranch" => Ok(PluginValue::Bool(package.is_default_branch())), + "getTransportOptions" => Ok(string_keyed_map(package.get_transport_options())), + "getReleaseDate" => match package.get_release_date() { + None => Ok(PluginValue::Null), + // TODO(plugin): a \DateTimeInterface has to materialize as a real PHP object in the + // child, which needs a snapshot encoding for value objects. + Some(_) => Err(runtime_throw( + "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" ))), @@ -532,6 +839,35 @@ fn dispatch_installation_manager_method( None => PluginValue::Null, }) } + "addInstaller" | "removeInstaller" => { + let handle = match args.first() { + Some(PluginValue::PhpHandle(handle)) => handle.clone(), + other => { + return Err(runtime_throw(format!( + "{method_name} expects an installer object, got {other:?}" + ))); + } + }; + if !php_is_a(&handle, "Composer\\Installer\\InstallerInterface").map_err(|error| { + runtime_throw(format!( + "{method_name} could not type-check its argument: {error:#}" + )) + })? { + return Err(runtime_throw(format!( + "{method_name} expects a Composer\\Installer\\InstallerInterface, got {}", + handle.class + ))); + } + let phandle = handle.phandle; + let installer = php_installer_proxy(handle); + if method_name == "addInstaller" { + im.borrow().add_installer(installer); + } else { + im.borrow().remove_installer(&*installer); + forget_php_installer_proxy(phandle); + } + Ok(PluginValue::Null) + } // TODO(plugin): the remaining InstallationManager surface is widened on demand, // driven by explicit errors from real plugins. other => Err(runtime_throw(format!( @@ -942,6 +1278,243 @@ pub(crate) fn php_is_a(handle: &PhpObjHandle, class: &str) -> anyhow::Result<boo Ok(matches!(value, PluginValue::Bool(true))) } +/// Wire value handing a Rust-side repository to the child, interned in the R table. +pub(crate) fn repository_handle_value( + repository: &RepositoryInterfaceHandle, +) -> Result<PluginValue, PhpThrow> { + let class = repository_stub_class(repository)?; + let rhandle = register_entity(RustEntity::Repository(repository.clone())); + Ok(rust_handle_value(rhandle, class)) +} + +/// `InstallerInterface` adapter for an installer entity living in the PHP child process: the +/// installer a plugin hands to `InstallationManager::addInstaller`, or the class a legacy +/// `composer-installer` package names. Every call is forwarded as a `CallPhpMethod` RPC. +#[derive(Debug)] +pub struct PhpInstallerProxy { + pub(crate) handle: PhpObjHandle, +} + +impl PhpInstallerProxy { + pub(crate) fn new(handle: PhpObjHandle) -> Self { + Self { handle } + } + + fn call(&self, method: &str, args: Vec<PluginValue>) -> anyhow::Result<PluginValue> { + unwrap_php_result(call_php_method( + self.handle.phandle, + method, + args, + Some(&mut PluginRpcDispatcher::default()), + )) + } + + fn package_arg(package: &PackageInterfaceHandle) -> anyhow::Result<PluginValue> { + Ok(package_handle_value(package.as_rc())?) + } + + fn optional_package_arg( + package: &Option<PackageInterfaceHandle>, + ) -> anyhow::Result<PluginValue> { + match package { + Some(package) => Self::package_arg(package), + None => Ok(PluginValue::Null), + } + } + + fn repo_arg(repo: &InstalledRepositoryInterfaceHandle) -> anyhow::Result<PluginValue> { + 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. + fn promise_result(&self, method: &str, value: PluginValue) -> anyhow::Result<Option<PhpMixed>> { + match value { + 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, + })), + } + } + + fn unsupported_shape(&self, method: &str, value: &PluginValue) -> anyhow::Error { + anyhow::anyhow!(shirabe_php_shim::RuntimeException { + message: format!( + "{}::{method}() returned an unsupported shape over RPC: {value:?}", + self.handle.class + ), + code: 0, + }) + } +} + +#[async_trait::async_trait(?Send)] +impl crate::installer::InstallerInterface for PhpInstallerProxy { + fn supports(&self, package_type: &str) -> anyhow::Result<bool> { + match self.call("supports", vec![PluginValue::string(package_type)])? { + PluginValue::Bool(supports) => Ok(supports), + other => Err(self.unsupported_shape("supports", &other)), + } + } + + fn is_installed( + &self, + repo: &InstalledRepositoryInterfaceHandle, + package: PackageInterfaceHandle, + ) -> anyhow::Result<bool> { + let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)?]; + match self.call("isInstalled", args)? { + PluginValue::Bool(installed) => Ok(installed), + other => Err(self.unsupported_shape("isInstalled", &other)), + } + } + + async fn download( + &self, + package: PackageInterfaceHandle, + prev_package: Option<PackageInterfaceHandle>, + ) -> anyhow::Result<Option<PhpMixed>> { + let args = vec![ + Self::package_arg(&package)?, + Self::optional_package_arg(&prev_package)?, + ]; + let value = self.call("download", args)?; + self.promise_result("download", value) + } + + async fn prepare( + &self, + r#type: &str, + package: PackageInterfaceHandle, + prev_package: Option<PackageInterfaceHandle>, + ) -> anyhow::Result<Option<PhpMixed>> { + let args = vec![ + PluginValue::string(r#type), + Self::package_arg(&package)?, + Self::optional_package_arg(&prev_package)?, + ]; + let value = self.call("prepare", args)?; + self.promise_result("prepare", value) + } + + async fn install( + &self, + repo: &InstalledRepositoryInterfaceHandle, + package: PackageInterfaceHandle, + ) -> anyhow::Result<Option<PhpMixed>> { + let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)?]; + let value = self.call("install", args)?; + self.promise_result("install", value) + } + + async fn update( + &self, + repo: &InstalledRepositoryInterfaceHandle, + initial: PackageInterfaceHandle, + target: PackageInterfaceHandle, + ) -> anyhow::Result<Option<PhpMixed>> { + let args = vec![ + Self::repo_arg(repo)?, + Self::package_arg(&initial)?, + Self::package_arg(&target)?, + ]; + let value = self.call("update", args)?; + self.promise_result("update", value) + } + + async fn uninstall( + &self, + repo: &InstalledRepositoryInterfaceHandle, + package: PackageInterfaceHandle, + ) -> anyhow::Result<Option<PhpMixed>> { + let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)?]; + let value = self.call("uninstall", args)?; + self.promise_result("uninstall", value) + } + + async fn cleanup( + &self, + r#type: &str, + package: PackageInterfaceHandle, + prev_package: Option<PackageInterfaceHandle>, + ) -> anyhow::Result<Option<PhpMixed>> { + let args = vec![ + PluginValue::string(r#type), + Self::package_arg(&package)?, + Self::optional_package_arg(&prev_package)?, + ]; + let value = self.call("cleanup", args)?; + self.promise_result("cleanup", value) + } + + fn get_install_path(&self, package: PackageInterfaceHandle) -> Option<String> { + // PHP declares `getInstallPath(): string`; a failure here is a plugin error the + // infallible signature cannot carry, so it aborts rather than answering a path that + // would silently install the package in the wrong place. + let args = vec![Self::package_arg(&package).unwrap_or_else(|error| { + panic!( + "{}::getInstallPath argument failed: {error:#}", + self.handle.class + ) + })]; + let value = self.call("getInstallPath", args).unwrap_or_else(|error| { + panic!( + "{}::getInstallPath failed over RPC: {error:#}", + self.handle.class + ) + }); + match value { + PluginValue::Null => None, + PluginValue::String(path) => Some(String::from_utf8_lossy(&path).into_owned()), + other => panic!("{}", self.unsupported_shape("getInstallPath", &other)), + } + } +} + +impl Drop for PhpInstallerProxy { + fn drop(&mut self) { + let _ = release_php_handle(self.handle.phandle); + } +} + +thread_local! { + /// The installer adapters handed to `InstallationManager::addInstaller` over RPC, keyed by + /// the entity's phandle. `removeInstaller` arrives carrying the same entity, and the + /// manager compares installers by identity, so the adapter it was given has to be found + /// again rather than rebuilt. + static PHP_INSTALLER_PROXIES: std::cell::RefCell<IndexMap<u64, std::rc::Rc<dyn crate::installer::InstallerInterface>>> = + std::cell::RefCell::new(IndexMap::new()); +} + +/// The adapter for an installer entity, building it on first sight. +pub(crate) fn php_installer_proxy( + handle: PhpObjHandle, +) -> std::rc::Rc<dyn crate::installer::InstallerInterface> { + PHP_INSTALLER_PROXIES.with(|proxies| { + let mut proxies = proxies.borrow_mut(); + if let Some(existing) = proxies.get(&handle.phandle) { + return existing.clone(); + } + let phandle = handle.phandle; + let proxy: std::rc::Rc<dyn crate::installer::InstallerInterface> = + std::rc::Rc::new(PhpInstallerProxy::new(handle)); + proxies.insert(phandle, proxy.clone()); + proxy + }) +} + +/// Drops the adapter bookkeeping for an installer entity that left the manager. +fn forget_php_installer_proxy(phandle: u64) { + PHP_INSTALLER_PROXIES.with(|proxies| { + proxies.borrow_mut().shift_remove(&phandle); + }); +} + /// `Capability` adapter for a capability entity living in the PHP child process, for /// capability interfaces that add no methods of their own (the plain /// `Composer\Plugin\Capability\Capability` marker). diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs index 9e52c01f..8f005079 100644 --- a/crates/shirabe/src/plugin/plugin_manager.rs +++ b/crates/shirabe/src/plugin/plugin_manager.rs @@ -47,12 +47,13 @@ pub struct PluginManager { allow_plugin_rules: Option<IndexMap<String, bool>>, allow_global_plugin_rules: Option<IndexMap<String, bool>>, running_in_global_dir: bool, + plugin_api_version_override: Option<String>, } #[derive(Debug)] pub enum PluginOrInstaller { Plugin(std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>), - Installer(Box<dyn InstallerInterface>), + Installer(std::rc::Rc<dyn InstallerInterface>), } /// PHP `private static $classCounter = 0;`. @@ -101,9 +102,17 @@ impl PluginManager { allow_plugin_rules, allow_global_plugin_rules, running_in_global_dir: false, + plugin_api_version_override: None, } } + /// For testing only: makes `get_plugin_api_version` report `version` instead of the + /// compiled-in constant, the seam PHPUnit obtains from + /// `getMockBuilder(PluginManager::class)->onlyMethods(['getPluginApiVersion'])`. + pub fn __set_plugin_api_version(&mut self, version: &str) { + self.plugin_api_version_override = Some(version.to_string()); + } + pub fn set_running_in_global_dir(&mut self, running_in_global_dir: bool) { self.running_in_global_dir = running_in_global_dir; } @@ -460,16 +469,46 @@ impl PluginManager { } if old_installer_plugin { - // TODO(plugin): legacy composer-installer plugins need the InstallerInterface - // reverse adapter, which does not exist yet; explicit error until then. - return Err(RuntimeException { - message: format!( - "Shirabe cannot load \"{}\": legacy composer-installer plugins are not supported yet", - package.get_name() - ), - code: 0, + if !self.php_runtime_is_a(&class, "Composer\\Installer\\InstallerInterface")? { + return Err(RuntimeException { + message: format!( + "Could not activate plugin \"{}\" as \"{}\" does not implement Composer\\Installer\\InstallerInterface", + package.get_name(), + class + ), + code: 0, + } + .into()); } - .into()); + self.io.write_error(&format!( + "<warning>Loading \"{}\" {}which is a legacy composer-installer built for Composer 1.x, it is likely to cause issues as you are running Composer 2.x.</warning>", + package.get_name(), + if is_global_plugin || self.running_in_global_dir { + "(installed globally) " + } else { + "" + } + )); + let composer = self.composer_full(); + let handle = self.php_runtime_new_object_with_args( + &class, + vec![ + crate::plugin::io_handle_value(&self.io)?, + crate::plugin::composer_handle_value(&composer), + ], + )?; + let installer = crate::plugin::php_installer_proxy(handle); + // A shared borrow: this runs inside `InstallationManager::execute`, which holds + // one of its own for the whole run. + composer + .borrow() + .get_installation_manager() + .borrow() + .add_installer(installer.clone()); + self.registered_plugins + .entry(package.get_name().to_string()) + .or_default() + .push(PluginOrInstaller::Installer(installer)); } else if self.php_runtime_class_exists(&class, true)? { if !self.php_runtime_is_a(&class, "Composer\\Plugin\\PluginInterface")? { return Err(RuntimeException { @@ -569,9 +608,17 @@ impl PluginManager { /// PHP `new $class()` in the worker, returning the P-table handle of the new entity. fn php_runtime_new_object(&self, class: &str) -> anyhow::Result<shirabe_php_rpc::PhpObjHandle> { + self.php_runtime_new_object_with_args(class, vec![]) + } + + fn php_runtime_new_object_with_args( + &self, + class: &str, + args: Vec<PluginValue>, + ) -> anyhow::Result<shirabe_php_rpc::PhpObjHandle> { let value = unwrap_php_result(shirabe_php_rpc::new_object( class, - vec![], + args, Some(&mut PluginRpcDispatcher::default()), ))?; match value { @@ -610,7 +657,7 @@ impl PluginManager { if let PluginOrInstaller::Installer(inst) = &self.registered_plugins.get(&name).unwrap()[index] { - installation_manager.borrow_mut().remove_installer(&**inst); + installation_manager.borrow().remove_installer(&**inst); } } } @@ -648,7 +695,7 @@ impl PluginManager { if let PluginOrInstaller::Installer(inst) = &self.registered_plugins.get(&name).unwrap()[index] { - installation_manager.borrow_mut().remove_installer(&**inst); + installation_manager.borrow().remove_installer(&**inst); } } } @@ -659,7 +706,10 @@ impl PluginManager { /// Returns the version of the internal composer-plugin-api package. pub(crate) fn get_plugin_api_version(&self) -> String { - plugin_interface::PLUGIN_API_VERSION.to_string() + match &self.plugin_api_version_override { + Some(version) => version.clone(), + None => plugin_interface::PLUGIN_API_VERSION.to_string(), + } } /// Adds a plugin, activates it and registers it with the event dispatcher diff --git a/crates/shirabe/tests/autoload/autoload_generator_test.rs b/crates/shirabe/tests/autoload/autoload_generator_test.rs index 85e98d81..81f508cc 100644 --- a/crates/shirabe/tests/autoload/autoload_generator_test.rs +++ b/crates/shirabe/tests/autoload/autoload_generator_test.rs @@ -33,8 +33,8 @@ struct InstallPathStubInstaller { #[async_trait::async_trait(?Send)] impl InstallerInterface for InstallPathStubInstaller { - fn supports(&self, _package_type: &str) -> bool { - true + fn supports(&self, _package_type: &str) -> anyhow::Result<bool> { + Ok(true) } fn is_installed( @@ -155,8 +155,8 @@ fn make_installation_manager( ))); let loop_ = std::rc::Rc::new(std::cell::RefCell::new(Loop::new(http_downloader, None))); - let mut im = InstallationManager::new(loop_, io, None); - im.add_installer(Box::new(InstallPathStubInstaller { + let im = InstallationManager::new(loop_, io, None); + im.add_installer(std::rc::Rc::new(InstallPathStubInstaller { vendor_dir: vendor_dir.to_string(), })); im @@ -491,7 +491,7 @@ fn test_vendor_dir_same_as_working_dir() { let mut s = set_up(); s.vendor_dir = s.working_dir.clone(); // Re-register the install-path stub so getInstallPath uses the new vendor dir. - s.im.add_installer(Box::new(InstallPathStubInstaller { + s.im.add_installer(std::rc::Rc::new(InstallPathStubInstaller { vendor_dir: s.vendor_dir.clone(), })); @@ -551,7 +551,7 @@ fn test_root_package_autoloading_alternative_vendor_dir() { ])); s.vendor_dir = format!("{}/subdir", s.vendor_dir); - s.im.add_installer(Box::new(InstallPathStubInstaller { + s.im.add_installer(std::rc::Rc::new(InstallPathStubInstaller { vendor_dir: s.vendor_dir.clone(), })); diff --git a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs index 5a6c93e5..674c4a80 100644 --- a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs +++ b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs @@ -421,8 +421,8 @@ mockall::mock! { #[derive(Debug)] pub InstallationManager {} impl InstallationManagerInterface for InstallationManager { - fn add_installer(&mut self, installer: Box<dyn InstallerInterface>); - fn remove_installer(&mut self, installer: &dyn InstallerInterface); + fn add_installer(&self, installer: std::rc::Rc<dyn InstallerInterface>); + fn remove_installer(&self, installer: &dyn InstallerInterface); fn disable_plugins(&mut self); fn is_package_installed( &mut self, diff --git a/crates/shirabe/tests/installer/installation_manager_test.rs b/crates/shirabe/tests/installer/installation_manager_test.rs index 8f2d7d4f..c511703e 100644 --- a/crates/shirabe/tests/installer/installation_manager_test.rs +++ b/crates/shirabe/tests/installer/installation_manager_test.rs @@ -67,8 +67,8 @@ mockall::mock! { #[async_trait::async_trait(?Send)] impl InstallerInterface for MockInstaller { - fn supports(&self, package_type: &str) -> bool { - MockInstaller::supports(self, package_type) + fn supports(&self, package_type: &str) -> anyhow::Result<bool> { + Ok(MockInstaller::supports(self, package_type)) } fn is_installed( @@ -163,12 +163,12 @@ impl BinaryInstaller { #[async_trait::async_trait(?Send)] impl InstallerInterface for BinaryInstaller { - fn supports(&self, package_type: &str) -> bool { + fn supports(&self, package_type: &str) -> anyhow::Result<bool> { self.calls .borrow_mut() .supports_args .push(package_type.to_string()); - package_type == "library" + Ok(package_type == "library") } fn is_installed( @@ -272,10 +272,10 @@ fn test_add_get_installer() { .times(2) .returning(|arg| arg == "vendor"); - let mut manager = + let manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); - manager.add_installer(Box::new(installer)); + manager.add_installer(std::rc::Rc::new(installer)); assert!(manager.get_installer("vendor").is_ok()); assert!(manager.get_installer("unregistered").is_err()); @@ -290,7 +290,7 @@ fn test_add_remove_installer() { .times(2) .returning(|arg| arg == "vendor"); // The manager stores installers as Rc, so the PHP object-identity semantics (assertSame, - // removeInstaller) map to Rc::ptr_eq on a handle registered via __add_installer. + // removeInstaller) map to Rc::ptr_eq on the handle the caller keeps. let installer: std::rc::Rc<dyn InstallerInterface> = std::rc::Rc::new(installer); let mut installer2 = MockInstaller::new(); @@ -300,15 +300,15 @@ fn test_add_remove_installer() { .returning(|arg| arg == "vendor"); let installer2: std::rc::Rc<dyn InstallerInterface> = std::rc::Rc::new(installer2); - let mut manager = + let manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); - manager.__add_installer(installer.clone()); + manager.add_installer(installer.clone()); assert!(std::rc::Rc::ptr_eq( &installer, &manager.get_installer("vendor").unwrap() )); - manager.__add_installer(installer2.clone()); + manager.add_installer(installer2.clone()); assert!(std::rc::Rc::ptr_eq( &installer2, &manager.get_installer("vendor").unwrap() @@ -352,9 +352,9 @@ fn test_install() { .withf_st(move |package| same_handle(package, &expected)) .returning(|_| Ok(None)); - let mut manager = + let manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); - manager.add_installer(Box::new(installer)); + manager.add_installer(std::rc::Rc::new(installer)); let operation = InstallOperation::new(package); @@ -385,9 +385,9 @@ fn test_update_with_equal_types() { }) .returning(|_, _| Ok(None)); - let mut manager = + let manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); - manager.add_installer(Box::new(installer)); + manager.add_installer(std::rc::Rc::new(installer)); let operation = UpdateOperation::new(initial, target); @@ -427,10 +427,10 @@ fn test_update_with_not_equal_types() { .withf_st(move |package| same_handle(package, &expected_target)) .returning(|_| Ok(None)); - let mut manager = + let manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); - manager.add_installer(Box::new(lib_installer)); - manager.add_installer(Box::new(bundle_installer)); + manager.add_installer(std::rc::Rc::new(lib_installer)); + manager.add_installer(std::rc::Rc::new(bundle_installer)); let operation = UpdateOperation::new(initial, target); @@ -457,9 +457,9 @@ fn test_uninstall() { .withf_st(move |package| same_handle(package, &expected)) .returning(|_| Ok(None)); - let mut manager = + let manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); - manager.add_installer(Box::new(installer)); + manager.add_installer(std::rc::Rc::new(installer)); let operation = UninstallOperation::new(package); @@ -472,9 +472,9 @@ fn test_uninstall() { fn test_install_binary() { let set_up = set_up(); let (installer, calls) = BinaryInstaller::new(); - let mut manager = + let manager = shirabe::installer::InstallationManager::new(set_up.loop_.clone(), set_up.io.clone(), None); - manager.add_installer(Box::new(installer)); + manager.add_installer(std::rc::Rc::new(installer)); let package = get_package("test/pkg", "1.0.0"); manager.ensure_binaries_presence(package.clone()); diff --git a/crates/shirabe/tests/plugin/plugin_installer_test.rs b/crates/shirabe/tests/plugin/plugin_installer_test.rs index 35088c8d..06cd2ce9 100644 --- a/crates/shirabe/tests/plugin/plugin_installer_test.rs +++ b/crates/shirabe/tests/plugin/plugin_installer_test.rs @@ -16,7 +16,9 @@ use shirabe::io::IOInterface; use shirabe::io::buffer_io::BufferIO; use shirabe::json::JsonFile; use shirabe::package::loader::{ArrayLoader, JsonLoader, JsonLoaderInput}; -use shirabe::package::{Locker, LockerInterface, PackageInterfaceHandle, RootPackageHandle}; +use shirabe::package::{ + CompletePackageHandle, Locker, LockerInterface, PackageInterfaceHandle, RootPackageHandle, +}; use shirabe::plugin::plugin_interface::PluginInterface; use shirabe::plugin::{Capable, PluginManager, composer_handle_value, io_handle_value}; use shirabe::repository::{ @@ -30,6 +32,7 @@ use shirabe::util::process_executor::ProcessExecutor; use shirabe_external_packages::symfony::console::output::output_interface::VERBOSITY_NORMAL; use shirabe_external_packages::symfony::process::PhpExecutableFinder; use shirabe_php_shim::PhpMixed; +use shirabe_semver::VersionParser; use tempfile::TempDir; /// The register/activate flow runs the plugin in the real PHP worker; without a PHP binary the @@ -166,9 +169,9 @@ impl RepositoryManagerInterface for MockRepositoryManager { struct MockInstallationManager; impl InstallationManagerInterface for MockInstallationManager { - fn add_installer(&mut self, _installer: Box<dyn InstallerInterface>) {} + fn add_installer(&self, _installer: std::rc::Rc<dyn InstallerInterface>) {} - fn remove_installer(&mut self, _installer: &dyn InstallerInterface) {} + fn remove_installer(&self, _installer: &dyn InstallerInterface) {} fn disable_plugins(&mut self) {} @@ -606,31 +609,114 @@ fn test_register_plugin_only_one_time() { assert_eq!("activate v1\n", set_up.io.borrow().get_output()); } -// PluginManager::get_plugin_api_version returns a hardcoded constant -// (plugin_interface::PLUGIN_API_VERSION) with no seam to override it per-test the way PHP's -// `getMockBuilder(PluginManager::class)->onlyMethods(['getPluginApiVersion'])` does. -#[ignore = "Requires mocking getPluginApiVersion; PluginManager has no such seam (TODO(plugin))"] +/// PHP `setPluginApiVersionWithPlugins`: swaps in a plugin manager reporting +/// `new_plugin_api_version` (PHP mocks `getPluginApiVersion`; the Rust seam is +/// `__set_plugin_api_version`) and a local repository holding the internal composer-plugin-api +/// package plus `plugins` (PHP mocks `getPackages`), then loads the installed plugins. +fn set_plugin_api_version_with_plugins( + set_up: &SetUp, + new_plugin_api_version: &str, + plugins: Vec<PackageInterfaceHandle>, +) -> std::rc::Rc<std::cell::RefCell<PluginManager>> { + let pm = std::rc::Rc::new(std::cell::RefCell::new(PluginManager::new( + set_up.io_dyn.clone(), + set_up.composer.downgrade(), + None, + DisablePlugins::None, + ))); + pm.borrow_mut() + .__set_plugin_api_version(new_plugin_api_version); + set_up.composer.borrow_mut().set_plugin_manager(pm.clone()); + + let plug_api_internal_package: PackageInterfaceHandle = CompletePackageHandle::new( + "composer-plugin-api".to_string(), + VersionParser + .normalize(new_plugin_api_version, None) + .unwrap(), + new_plugin_api_version.to_string(), + ) + .into(); + let repository = + InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap()); + repository + .borrow_mut() + .add_package(plug_api_internal_package) + .unwrap(); + for plugin in plugins { + repository.borrow_mut().add_package(plugin).unwrap(); + } + set_up + .composer + .borrow() + .get_repository_manager() + .borrow_mut() + .set_local_repository(repository.as_repository_handle()); + + pm.borrow_mut().load_installed_plugins().unwrap(); + pm +} + #[test] fn test_star_plugin_version_works_with_any_api_version() { - // TODO(phase-d): requires mocking getPluginApiVersion; PluginManager has no such seam - // (TODO(plugin)). - todo!() + if !php_runtime_available() { + return; + } + let _worker = lock_php_worker(); + let set_up = set_up(); + let star_version_plugin = || vec![PackageInterfaceHandle::dup(&set_up.packages[4])]; + + let pm = set_plugin_api_version_with_plugins(&set_up, "1.0.0", star_version_plugin()); + assert_eq!(1, pm.borrow().get_plugins().len()); + + let pm = set_plugin_api_version_with_plugins(&set_up, "1.9.9", star_version_plugin()); + assert_eq!(1, pm.borrow().get_plugins().len()); + + let pm = set_plugin_api_version_with_plugins(&set_up, "2.0.0-dev", star_version_plugin()); + assert_eq!(1, pm.borrow().get_plugins().len()); + + let pm = set_plugin_api_version_with_plugins(&set_up, "100.0.0-stable", star_version_plugin()); + assert_eq!(1, pm.borrow().get_plugins().len()); } -#[ignore = "Requires mocking getPluginApiVersion; PluginManager has no such seam (TODO(plugin))"] #[test] fn test_plugin_constraint_works_only_with_certain_api_version() { - // TODO(phase-d): requires mocking getPluginApiVersion; PluginManager has no such seam - // (TODO(plugin)). - todo!() + if !php_runtime_available() { + return; + } + let _worker = lock_php_worker(); + let set_up = set_up(); + let plugin_with_api_constraint = || vec![PackageInterfaceHandle::dup(&set_up.packages[5])]; + + let pm = set_plugin_api_version_with_plugins(&set_up, "1.0.0", plugin_with_api_constraint()); + assert_eq!(0, pm.borrow().get_plugins().len()); + + let pm = set_plugin_api_version_with_plugins(&set_up, "1.1.9", plugin_with_api_constraint()); + assert_eq!(0, pm.borrow().get_plugins().len()); + + let pm = set_plugin_api_version_with_plugins(&set_up, "1.2.0", plugin_with_api_constraint()); + assert_eq!(1, pm.borrow().get_plugins().len()); + + let pm = set_plugin_api_version_with_plugins(&set_up, "1.9.9", plugin_with_api_constraint()); + assert_eq!(1, pm.borrow().get_plugins().len()); } -#[ignore = "Requires mocking getPluginApiVersion; PluginManager has no such seam (TODO(plugin))"] #[test] fn test_plugin_range_constraints_work_only_with_certain_api_version() { - // TODO(phase-d): requires mocking getPluginApiVersion; PluginManager has no such seam - // (TODO(plugin)). - todo!() + if !php_runtime_available() { + return; + } + let _worker = lock_php_worker(); + let set_up = set_up(); + let plugin_with_api_constraint = || vec![PackageInterfaceHandle::dup(&set_up.packages[6])]; + + let pm = set_plugin_api_version_with_plugins(&set_up, "1.0.0", plugin_with_api_constraint()); + assert_eq!(0, pm.borrow().get_plugins().len()); + + let pm = set_plugin_api_version_with_plugins(&set_up, "3.0.0", plugin_with_api_constraint()); + assert_eq!(1, pm.borrow().get_plugins().len()); + + let pm = set_plugin_api_version_with_plugins(&set_up, "5.5.0", plugin_with_api_constraint()); + assert_eq!(0, pm.borrow().get_plugins().len()); } #[test] diff --git a/crates/shirabe/tests/repository/filesystem_repository_test.rs b/crates/shirabe/tests/repository/filesystem_repository_test.rs index bcdb766e..3279f29e 100644 --- a/crates/shirabe/tests/repository/filesystem_repository_test.rs +++ b/crates/shirabe/tests/repository/filesystem_repository_test.rs @@ -93,8 +93,8 @@ mockall::mock! { #[derive(Debug)] pub InstallationManager {} impl InstallationManagerInterface for InstallationManager { - fn add_installer(&mut self, installer: Box<dyn InstallerInterface>); - fn remove_installer(&mut self, installer: &dyn InstallerInterface); + fn add_installer(&self, installer: std::rc::Rc<dyn InstallerInterface>); + fn remove_installer(&self, installer: &dyn InstallerInterface); fn disable_plugins(&mut self); fn is_package_installed( &mut self, diff --git a/docs/dev/php-rpc.md b/docs/dev/php-rpc.md index 298b4059..e6797533 100644 --- a/docs/dev/php-rpc.md +++ b/docs/dev/php-rpc.md @@ -168,7 +168,9 @@ Both sets are written into the same autoload directory at worker spawn and resol highest priority, so these FQCNs can never be shadowed by the real implementation; `__shirabe_require` restores that priority after loading code that prepends its own autoloader. Stubs are interned per rhandle (`WeakReference`-based registry) so identity (`===`) holds, and -their destructors send `ReleaseRustHandle`. +their destructors send `ReleaseRustHandle`. `clone` on a stub calls `__shirabeClone` on the +entity and rebinds the copy to the handle that answers, so the two stubs never share (and never +double-release) one entity; entities with no clone semantics answer with an explicit error. ## The P table diff --git a/docs/dev/plugin-class-classification.md b/docs/dev/plugin-class-classification.md index ab91ccda..ddba364d 100644 --- a/docs/dev/plugin-class-classification.md +++ b/docs/dev/plugin-class-classification.md @@ -436,13 +436,6 @@ which can cross the wire as values. The stub needs a bespoke story (e.g. a local Table bound to a proxying `OutputInterface`); until one is designed, both members raise explicit errors. -### Proxy clone semantics - -`clone $package` is a common plugin idiom (and `NoopInstaller::install` -does `$repo->addPackage(clone $package)`), but PHP `clone` on a proxy stub -copies the handle, not the Rust entity. The stub generator needs a -`__clone` that RPCs a clone of the entity. Undecided. - ## The classifier tool ### Dependencies and layout diff --git a/docs/dev/plugin-stub-generation.md b/docs/dev/plugin-stub-generation.md index e10c0e9c..33025df7 100644 --- a/docs/dev/plugin-stub-generation.md +++ b/docs/dev/plugin-stub-generation.md @@ -72,9 +72,13 @@ the generator's vendor directory or the classifier report is unavailable. * **Public instance properties** are not declared on the stub; `__get`/`__set` forwarders carry every access (including dynamic-property writes) to the Rust side, where an unsupported name is an explicit error. -* **`__toString`** is forwarded like any other method. **`__clone`** emits a - throwing body: proxy clone semantics are an open design question, and - cloning must not silently share the Rust handle between two stubs. +* **`__toString`** is forwarded like any other method. **`__clone`** is part of + the boilerplate on every stub, whether or not the real class declares one: + PHP has already copied the stub by the time it runs, so the copy asks the + Rust side for a clone of the entity and rebinds itself to the fresh handle + (`__shirabeClone`, answered with `[rhandle, epoch]`). The clone semantics of + the real class live on the Rust side with the entity; entities that model no + clone answer with an explicit error. * **Imports**: the original file's `use` statements are kept in their original order, restricted to names the emitted stub references; signatures declared elsewhere (interface files) are re-spelled through that import table. diff --git a/scripts/plugin-stub-generator/src/Generator.php b/scripts/plugin-stub-generator/src/Generator.php index 1e41eed8..ce8f612b 100644 --- a/scripts/plugin-stub-generator/src/Generator.php +++ b/scripts/plugin-stub-generator/src/Generator.php @@ -50,6 +50,16 @@ final class Generator '__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); + } PHP; private const PROPERTY_FORWARDERS = <<<'PHP' @@ -65,15 +75,6 @@ final class Generator } PHP; - private const CLONE_THROW = <<<'PHP' - public function __clone() - { - // Cloning a proxy is an open design question; fail instead of silently sharing - // the Rust-side entity between two stub instances. - throw new \RuntimeException('Shirabe does not support cloning ' . static::class . ' inside the plugin process yet'); - } - PHP; - private Project $project; private NamePrinter $printer; @@ -234,7 +235,6 @@ final class Generator $publicStatics = []; $nonPublicStatics = []; $ownInstanceMethods = []; - $cloneThrows = false; foreach ($class->getMethods() as $method) { $name = $method->name->toString(); if ($name === '__construct') { @@ -245,14 +245,12 @@ final class Generator $this->errors[] = "$fqcn::$name: magic methods cannot be proxied"; } // __toString is an ordinary zero-argument call under a magic name and is - // forwarded below; __clone semantics are an open design question and the - // emitted body throws instead of silently sharing the Rust handle. + // forwarded below. A real __clone declaration needs no counterpart here: the + // boilerplate's forwarder delegates cloning to the entity, whose Rust-side + // clone carries the declared semantics. if ($method->isPublic() && $name === '__toString') { $ownInstanceMethods[$name] = $method; } - if ($method->isPublic() && $name === '__clone') { - $cloneThrows = true; - } continue; } if ($method->isStatic()) { @@ -343,9 +341,6 @@ final class Generator if ($hasPublicInstanceProperties) { $members[] = self::PROPERTY_FORWARDERS; } - if ($cloneThrows) { - $members[] = self::CLONE_THROW; - } $members = array_merge($members, $staticMethods, $methodTexts); $body = implode("\n\n", $members); |
