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 /crates/shirabe/src/plugin/plugin_manager.rs | |
| 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>
Diffstat (limited to 'crates/shirabe/src/plugin/plugin_manager.rs')
| -rw-r--r-- | crates/shirabe/src/plugin/plugin_manager.rs | 78 |
1 files changed, 64 insertions, 14 deletions
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 |
