From 4de018826e9dce90fd5cb78d468641478327ec99 Mon Sep 17 00:00:00 2001 From: nsfisis Date: Thu, 6 Aug 2026 01:50:34 +0900 Subject: 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) --- .../shirabe/src/installer/installation_manager.rs | 76 +++++++++++----------- .../shirabe/src/installer/installer_interface.rs | 2 +- crates/shirabe/src/installer/library_installer.rs | 6 +- .../shirabe/src/installer/metapackage_installer.rs | 4 +- crates/shirabe/src/installer/noop_installer.rs | 4 +- crates/shirabe/src/installer/plugin_installer.rs | 4 +- crates/shirabe/src/installer/project_installer.rs | 4 +- 7 files changed, 49 insertions(+), 51 deletions(-) (limited to 'crates/shirabe/src/installer') 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>, - /// 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>>, + /// Maps a package type to its installer. /// RefCell so lookups can populate the cache through `&self` from concurrent operation chains. - cache: std::cell::RefCell>, + cache: std::cell::RefCell>>, /// RefCell so mark_for_notification works through `&self` from concurrent operation chains. notifiable_packages: std::cell::RefCell>>, loop_: std::rc::Rc>, @@ -69,7 +69,7 @@ impl InstallationManager { event_dispatcher: Option>>, ) -> 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) { - 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) { - 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) { + 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> { 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); - fn remove_installer(&mut self, installer: &dyn InstallerInterface); + fn add_installer(&self, installer: std::rc::Rc); + 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) { - self.add_installer(installer); + fn add_installer(&self, installer: std::rc::Rc) { + 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; 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 { + 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 { + 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 { + 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 { + 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 { + Ok(true) } fn is_installed( -- cgit v1.3.1