aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/plugin
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src/plugin')
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs360
-rw-r--r--crates/shirabe/src/plugin/plugin_interface.rs24
-rw-r--r--crates/shirabe/src/plugin/plugin_manager.rs439
3 files changed, 733 insertions, 90 deletions
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
new file mode 100644
index 00000000..87d67ea4
--- /dev/null
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -0,0 +1,360 @@
+//! PHP-backed `PluginInterface` adapter and the R table serving its RPC callbacks.
+//!
+//! This module has no Composer counterpart: it is the Rust half of the plugin runtime split
+//! (a real PHP child process executes the plugin code, see `docs/dev/php-rpc.md`). The plugin
+//! entity lives in the worker's P table; Rust-side entities the plugin can call back into
+//! (`$composer`, `$io`) live in the R table here.
+
+use crate::autoload::ClassLoader;
+use crate::composer::ComposerHandle;
+use crate::io::IOInterface;
+use crate::plugin::plugin_interface::PluginInterface;
+use indexmap::IndexMap;
+use shirabe_php_rpc::{
+ PhpThrow, PluginValue, RustMethodDispatcher, RustObjHandle, call_php_method, release_php_handle,
+};
+
+/// A Rust-side entity a PHP proxy stub points back to.
+#[derive(Debug)]
+enum RustEntity {
+ Composer(ComposerHandle),
+ Io(std::rc::Rc<std::cell::RefCell<dyn IOInterface>>),
+}
+
+thread_local! {
+ /// The R table. Entries are strong references kept for the worker's lifetime.
+ /// TODO(plugin): GC (dropping entries on ReleaseRustHandle) is not implemented yet;
+ /// until then entities registered here are intentionally never released.
+ static R_TABLE: std::cell::RefCell<IndexMap<u64, RustEntity>> =
+ std::cell::RefCell::new(IndexMap::new());
+}
+
+/// Registers the Composer instance in the R table, interned by shared-pointer identity so the
+/// same instance always crosses the boundary as the same handle (`===` in the child).
+pub(crate) fn register_composer_entity(composer: &ComposerHandle) -> u64 {
+ R_TABLE.with(|table| {
+ let mut table = table.borrow_mut();
+ let ptr = std::rc::Rc::as_ptr(composer.as_rc()) as *const () as usize;
+ for (rhandle, entity) in table.iter() {
+ if let RustEntity::Composer(existing) = entity
+ && std::rc::Rc::as_ptr(existing.as_rc()) as *const () as usize == ptr
+ {
+ return *rhandle;
+ }
+ }
+ let rhandle = shirabe_php_rpc::alloc_rhandle();
+ table.insert(rhandle, RustEntity::Composer(composer.clone()));
+ rhandle
+ })
+}
+
+/// Registers an IO instance in the R table, interned like [`register_composer_entity`].
+pub(crate) fn register_io_entity(io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>) -> u64 {
+ R_TABLE.with(|table| {
+ let mut table = table.borrow_mut();
+ let ptr = std::rc::Rc::as_ptr(io) as *const () as usize;
+ for (rhandle, entity) in table.iter() {
+ if let RustEntity::Io(existing) = entity
+ && std::rc::Rc::as_ptr(existing) as *const () as usize == ptr
+ {
+ return *rhandle;
+ }
+ }
+ let rhandle = shirabe_php_rpc::alloc_rhandle();
+ table.insert(rhandle, RustEntity::Io(io.clone()));
+ rhandle
+ })
+}
+
+/// The PHP class name (= proxy stub class) of a Rust IO instance, for the `__class` field of
+/// its handle descriptor.
+pub(crate) fn io_stub_class(
+ io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+) -> anyhow::Result<&'static str> {
+ let borrowed = io.borrow();
+ let any = borrowed.as_any();
+ if any
+ .downcast_ref::<crate::io::buffer_io::BufferIO>()
+ .is_some()
+ {
+ Ok("Composer\\IO\\BufferIO")
+ } else if any
+ .downcast_ref::<crate::io::console_io::ConsoleIO>()
+ .is_some()
+ {
+ Ok("Composer\\IO\\ConsoleIO")
+ } else if any.downcast_ref::<crate::io::null_io::NullIO>().is_some() {
+ Ok("Composer\\IO\\NullIO")
+ } else {
+ // TODO(plugin): only the IO classes with hand-written proxy stubs can cross the
+ // boundary until a stub generator exists.
+ Err(anyhow::anyhow!(
+ "no proxy stub class is available for this IO implementation"
+ ))
+ }
+}
+
+/// Looks a class up in every registered Rust-side `ClassLoader`, in registration order — the
+/// Rust mirror of what the PHP `spl_autoload_register` stack would do in-process.
+pub(crate) fn find_file_in_registered_loaders(class: &str) -> Option<String> {
+ for (_vendor_dir, mut loader) in ClassLoader::get_registered_loaders() {
+ if let Some(file) = loader.find_file(class) {
+ return Some(file);
+ }
+ }
+ None
+}
+
+/// Serves `CallRustMethod` requests from the child while a plugin-related call is in flight:
+/// rhandle 0 is the runtime service endpoint (autoload lookups), every other rhandle resolves
+/// through the R table. Unsupported methods are explicit errors, never silent fallbacks.
+#[derive(Debug)]
+pub(crate) struct PluginRpcDispatcher;
+
+impl RustMethodDispatcher for PluginRpcDispatcher {
+ fn dispatch(
+ &mut self,
+ rhandle: u64,
+ method_name: &str,
+ args: Vec<PluginValue>,
+ _out_param_positions: &[u32],
+ ) -> Result<PluginValue, PhpThrow> {
+ if rhandle == 0 {
+ if method_name == "__shirabe_find_file" {
+ let class = match args.first() {
+ Some(PluginValue::String(bytes)) => String::from_utf8_lossy(bytes).into_owned(),
+ other => {
+ return Err(runtime_throw(format!(
+ "__shirabe_find_file expects a class name argument, got {other:?}"
+ )));
+ }
+ };
+ return Ok(match find_file_in_registered_loaders(&class) {
+ Some(file) => PluginValue::string(file),
+ None => PluginValue::Null,
+ });
+ }
+ return Err(runtime_throw(format!(
+ "unknown runtime service method `{method_name}`"
+ )));
+ }
+
+ R_TABLE.with(|table| {
+ let table = table.borrow();
+ match table.get(&rhandle) {
+ Some(RustEntity::Io(io)) => dispatch_io_method(io, method_name, &args),
+ Some(RustEntity::Composer(_)) => {
+ // TODO(plugin): the Composer object graph (getConfig, getRepositoryManager,
+ // ...) becomes reachable over RPC later.
+ Err(runtime_throw(format!(
+ "the Composer method `{method_name}` is not available over RPC yet"
+ )))
+ }
+ None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
+ }
+ })
+ }
+}
+
+fn dispatch_io_method(
+ io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<PluginValue, PhpThrow> {
+ match method_name {
+ "write" | "writeError" => {
+ let (messages, newline, verbosity) = decode_write_args(method_name, args)?;
+ let borrowed = io.borrow();
+ for message in &messages {
+ if method_name == "write" {
+ borrowed.write3(message, newline, verbosity);
+ } else {
+ borrowed.write_error3(message, newline, verbosity);
+ }
+ }
+ Ok(PluginValue::Null)
+ }
+ "isInteractive" => Ok(PluginValue::Bool(io.borrow().is_interactive())),
+ "isVerbose" => Ok(PluginValue::Bool(io.borrow().is_verbose())),
+ "isVeryVerbose" => Ok(PluginValue::Bool(io.borrow().is_very_verbose())),
+ "isDebug" => Ok(PluginValue::Bool(io.borrow().is_debug())),
+ "isDecorated" => Ok(PluginValue::Bool(io.borrow().is_decorated())),
+ // TODO(plugin): the remaining IOInterface surface (ask*, authentications, ...) is
+ // widened on demand, driven by explicit errors from real plugins.
+ other => Err(runtime_throw(format!(
+ "the IO method `{other}` is not available over RPC yet"
+ ))),
+ }
+}
+
+/// Decodes `($messages, bool $newline, int $verbosity)`: `$messages` is a string or a list of
+/// strings in PHP.
+fn decode_write_args(
+ method_name: &str,
+ args: &[PluginValue],
+) -> Result<(Vec<String>, bool, i64), PhpThrow> {
+ let messages = match args.first() {
+ Some(PluginValue::String(bytes)) => vec![String::from_utf8_lossy(bytes).into_owned()],
+ Some(PluginValue::List(items)) => {
+ let mut messages = Vec::with_capacity(items.len());
+ for item in items {
+ match item {
+ PluginValue::String(bytes) => {
+ messages.push(String::from_utf8_lossy(bytes).into_owned());
+ }
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects string messages, got {other:?}"
+ )));
+ }
+ }
+ }
+ messages
+ }
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects a string or list of strings, got {other:?}"
+ )));
+ }
+ };
+ let newline = match args.get(1) {
+ Some(PluginValue::Bool(b)) => *b,
+ None => true,
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects a bool newline flag, got {other:?}"
+ )));
+ }
+ };
+ let verbosity = match args.get(2) {
+ Some(PluginValue::Int(v)) => *v,
+ None => crate::io::NORMAL,
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects an int verbosity, got {other:?}"
+ )));
+ }
+ };
+ Ok((messages, newline, verbosity))
+}
+
+fn runtime_throw(message: String) -> PhpThrow {
+ PhpThrow {
+ exception_class: "RuntimeException".to_string(),
+ message,
+ code: 0,
+ }
+}
+
+/// `PluginInterface` adapter for a plugin entity living in the PHP child process: every
+/// lifecycle call is forwarded as a `CallPhpMethod` RPC.
+#[derive(Debug)]
+pub struct PhpPluginProxy {
+ pub(crate) phandle: u64,
+ pub(crate) class: String,
+}
+
+impl PhpPluginProxy {
+ pub fn new(phandle: u64, class: String) -> Self {
+ Self { phandle, class }
+ }
+
+ fn forward_lifecycle_call(
+ &self,
+ method: &str,
+ composer: &ComposerHandle,
+ io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ ) -> anyhow::Result<()> {
+ let composer_rhandle = register_composer_entity(composer);
+ let io_rhandle = register_io_entity(io);
+ let io_class = io_stub_class(io)?;
+ let args = vec![
+ PluginValue::RustHandle(RustObjHandle {
+ rhandle: composer_rhandle,
+ class: "Composer\\Composer".to_string(),
+ epoch: 0,
+ snapshot: None,
+ }),
+ PluginValue::RustHandle(RustObjHandle {
+ rhandle: io_rhandle,
+ class: io_class.to_string(),
+ epoch: 0,
+ snapshot: None,
+ }),
+ ];
+ let outcome = call_php_method(self.phandle, method, args, Some(&mut PluginRpcDispatcher))?;
+ match outcome {
+ Ok(_) => Ok(()),
+ // TODO(plugin): the original exception class is collapsed to RuntimeException on
+ // this side of the boundary.
+ Err(throw) => Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: throw.message,
+ code: throw.code,
+ })),
+ }
+ }
+
+ /// For testing only: reads a public property of the plugin entity in the child, mirroring
+ /// PHPUnit assertions like `$plugins[0]->version`.
+ pub fn __get_property(&self, name: &str) -> anyhow::Result<shirabe_php_shim::PhpMixed> {
+ let outcome = shirabe_php_rpc::call_function(
+ "__shirabe_get_property",
+ vec![
+ PluginValue::PhpHandle(shirabe_php_rpc::PhpObjHandle {
+ phandle: self.phandle,
+ class: self.class.clone(),
+ implements: Vec::new(),
+ }),
+ PluginValue::string(name),
+ ],
+ )?;
+ match outcome {
+ Ok(value) => Ok(value.to_php_mixed()?),
+ Err(throw) => Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: throw.message,
+ code: throw.code,
+ })),
+ }
+ }
+}
+
+impl PluginInterface for PhpPluginProxy {
+ fn activate(
+ &mut self,
+ composer: ComposerHandle,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ ) -> anyhow::Result<()> {
+ self.forward_lifecycle_call("activate", &composer, &io)
+ }
+
+ fn deactivate(
+ &mut self,
+ composer: ComposerHandle,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ ) -> anyhow::Result<()> {
+ self.forward_lifecycle_call("deactivate", &composer, &io)
+ }
+
+ fn uninstall(
+ &mut self,
+ composer: ComposerHandle,
+ io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ ) -> anyhow::Result<()> {
+ self.forward_lifecycle_call("uninstall", &composer, &io)
+ }
+
+ fn get_class_name(&self) -> String {
+ self.class.clone()
+ }
+
+ fn as_php_plugin_proxy(&self) -> Option<&PhpPluginProxy> {
+ Some(self)
+ }
+}
+
+impl Drop for PhpPluginProxy {
+ fn drop(&mut self) {
+ // A dead worker has nothing left to release.
+ let _ = release_php_handle(self.phandle);
+ }
+}
diff --git a/crates/shirabe/src/plugin/plugin_interface.rs b/crates/shirabe/src/plugin/plugin_interface.rs
index 5a92a87a..54fa6009 100644
--- a/crates/shirabe/src/plugin/plugin_interface.rs
+++ b/crates/shirabe/src/plugin/plugin_interface.rs
@@ -7,23 +7,29 @@ use crate::plugin::Capable;
pub const PLUGIN_API_VERSION: &str = "2.9.0";
pub trait PluginInterface: std::fmt::Debug {
+ // The PHP methods return void but may throw; the owned `ComposerHandle` follows the shared
+ // handle policy — plugins typically store `$composer` beyond the call.
fn activate(
&mut self,
- composer: &ComposerHandle,
+ composer: ComposerHandle,
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- );
+ ) -> anyhow::Result<()>;
fn deactivate(
&mut self,
- composer: &ComposerHandle,
+ composer: ComposerHandle,
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- );
+ ) -> anyhow::Result<()>;
fn uninstall(
&mut self,
- composer: &ComposerHandle,
+ composer: ComposerHandle,
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- );
+ ) -> anyhow::Result<()>;
+
+ /// PHP: `get_class($plugin)`. Rust has no runtime class name, so each implementor carries
+ /// the name PHP would report.
+ fn get_class_name(&self) -> String;
// TODO(plugin): PHP-side `instanceof` checks for EventSubscriberInterface / Capable.
// EventSubscriberInterface is not dyn-compatible (its only method is associated, not
@@ -35,4 +41,10 @@ pub trait PluginInterface: std::fmt::Debug {
fn as_capable(&self) -> Option<&dyn Capable> {
None
}
+
+ /// For testing only: recovers the PHP-backed proxy so tests can read plugin properties the
+ /// way PHPUnit asserts `$plugins[0]->version`.
+ fn as_php_plugin_proxy(&self) -> Option<&crate::plugin::PhpPluginProxy> {
+ None
+ }
}
diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs
index 572bb3b6..6760bf20 100644
--- a/crates/shirabe/src/plugin/plugin_manager.rs
+++ b/crates/shirabe/src/plugin/plugin_manager.rs
@@ -6,6 +6,7 @@
use crate::composer::PartialComposerHandle;
use crate::composer::{ComposerHandle, ComposerWeakHandle};
+use crate::event_dispatcher::{EventDispatcher, unwrap_php_result};
use crate::factory::DisablePlugins;
use crate::installer::InstallerInterface;
use crate::io::IOInterface;
@@ -17,16 +18,20 @@ use crate::package::base_package::{self};
use crate::package::version::VersionParser;
use crate::plugin::PluginBlockedException;
use crate::plugin::capability::Capability;
+use crate::plugin::php_plugin_proxy::{PhpPluginProxy, PluginRpcDispatcher};
use crate::plugin::plugin_interface::{self, PluginInterface};
use crate::repository::InstalledRepository;
-use crate::repository::RepositoryInterface;
+use crate::repository::RepositoryInterfaceHandle;
use crate::repository::RepositoryUtils;
+use crate::repository::RootPackageRepository;
use crate::util::PackageSorter;
use indexmap::IndexMap;
use shirabe_external_packages::composer::pcre::Preg;
+use shirabe_php_rpc::{PluginValue, call_function_with_dispatcher};
use shirabe_php_shim::{
E_USER_DEPRECATED, PhpMixed, RuntimeException, UnexpectedValueException, array_key_exists,
- get_class_obj, implode, ksort, php_regex, trigger_error, trim, var_export_str, version_compare,
+ dirname, file_get_contents, implode, ksort, php_regex, preg_quote, strrpos, strtr_array,
+ substr, trigger_error, trim, var_export_str, version_compare,
};
use shirabe_semver::constraint::SimpleConstraint;
@@ -37,7 +42,9 @@ pub struct PluginManager {
pub(crate) global_composer: Option<PartialComposerHandle>,
pub(crate) version_parser: VersionParser,
pub(crate) disable_plugins: DisablePlugins,
- pub(crate) plugins: Vec<Box<dyn PluginInterface>>,
+ // PHP stores the same plugin instance in both $plugins and $registeredPlugins (reference
+ // semantics); shared handles preserve the identity comparisons that relies on.
+ pub(crate) plugins: Vec<std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>>,
pub(crate) registered_plugins: IndexMap<String, Vec<PluginOrInstaller>>,
allow_plugin_rules: Option<IndexMap<String, bool>>,
allow_global_plugin_rules: Option<IndexMap<String, bool>>,
@@ -46,11 +53,12 @@ pub struct PluginManager {
#[derive(Debug)]
pub enum PluginOrInstaller {
- Plugin(Box<dyn PluginInterface>),
+ Plugin(std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>),
Installer(Box<dyn InstallerInterface>),
}
-static mut CLASS_COUNTER: i64 = 0;
+/// PHP `private static $classCounter = 0;`.
+static CLASS_COUNTER: std::sync::atomic::AtomicI64 = std::sync::atomic::AtomicI64::new(0);
impl PluginManager {
pub fn new(
@@ -121,7 +129,7 @@ impl PluginManager {
.borrow()
.get_local_repository();
self.load_repository(
- &mut *repo.borrow_mut(),
+ &repo,
false,
Some(self.composer_full().borrow().get_package().clone()),
)?;
@@ -136,7 +144,7 @@ impl PluginManager {
.get_repository_manager()
.borrow()
.get_local_repository();
- self.load_repository(&mut *repo.borrow_mut(), true, None)?;
+ self.load_repository(&repo, true, None)?;
}
Ok(())
}
@@ -151,7 +159,7 @@ impl PluginManager {
.get_repository_manager()
.borrow()
.get_local_repository();
- self.deactivate_repository(&mut *repo.borrow_mut(), false)?;
+ self.deactivate_repository(&repo, false)?;
}
if self.global_composer.is_some() && !self.are_plugins_disabled("global") {
@@ -163,7 +171,7 @@ impl PluginManager {
.get_repository_manager()
.borrow()
.get_local_repository();
- self.deactivate_repository(&mut *repo.borrow_mut(), true)?;
+ self.deactivate_repository(&repo, true)?;
}
Ok(())
@@ -173,7 +181,7 @@ impl PluginManager {
///
/// PHP returns `$this->plugins` directly; the plugin objects are shared by reference, so this
/// borrows the stored instances rather than cloning them.
- pub fn get_plugins(&self) -> &[Box<dyn PluginInterface>] {
+ pub fn get_plugins(&self) -> &[std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>] {
&self.plugins
}
@@ -276,10 +284,7 @@ impl PluginManager {
return Ok(());
}
- // TODO(plugin): the rest of registerPackage performs class-level eval() to load the plugin source.
- // This is a runtime concern that requires PHP semantics; not portable to Rust without a PHP interpreter.
- // The remainder of the function is mirrored as references but performs no actual loading.
- let _old_installer_plugin = package.get_type() == "composer-installer";
+ let old_installer_plugin = package.get_type() == "composer-installer";
if self.registered_plugins.contains_key(&package.get_name()) {
return Ok(());
@@ -304,7 +309,7 @@ impl PluginManager {
code: 0,
}.into());
}
- let _classes: Vec<String> = if let Some(arr) = class_value.and_then(|v| v.as_list()) {
+ let classes: Vec<String> = if let Some(arr) = class_value.and_then(|v| v.as_list()) {
arr.iter()
.filter_map(|v| v.as_string().map(|s| s.to_string()))
.collect()
@@ -317,22 +322,260 @@ impl PluginManager {
]
};
- // TODO(plugin): everything below this point in the original PHP would create runtime instances:
- // - clone root package and clear `files` autoloads
- // - build a synthetic InstalledRepository for plugin dependencies
- // - parseAutoloads / createLoader on the autoload generator
- // - eval the plugin class source under a temporary class name
- // - call activate(...) and register subscribers
- // None of that is implementable without a PHP runtime, and so the body is intentionally left as a no-op stub.
- let _ = fail_on_missing_classes;
+ let composer = self.composer_full();
+ let local_repo = composer
+ .borrow()
+ .get_repository_manager()
+ .borrow()
+ .get_local_repository();
+ let global_repo = self.global_composer.as_ref().map(|gc| {
+ gc.borrow_partial()
+ .get_repository_manager()
+ .borrow()
+ .get_local_repository()
+ });
+
+ let root_package = RootPackageInterfaceHandle::dup(composer.borrow().get_package());
+
+ // clear files autoload rules from the root package as the root dependencies are not
+ // necessarily all present yet when booting this runtime autoloader
+ let mut root_package_autoloads = root_package.get_autoload();
+ root_package_autoloads.insert("files".to_string(), PhpMixed::List(vec![]));
+ root_package.set_autoload(root_package_autoloads);
+ let mut root_package_autoloads = root_package.get_dev_autoload();
+ root_package_autoloads.insert("files".to_string(), PhpMixed::List(vec![]));
+ root_package.set_dev_autoload(root_package_autoloads);
+
+ let root_package_repo =
+ RepositoryInterfaceHandle::new(RootPackageRepository::new(root_package.clone()));
+ let mut installed_repo =
+ InstalledRepository::new(vec![local_repo.clone(), root_package_repo]);
+ if let Some(global_repo) = &global_repo {
+ installed_repo.add_repository(global_repo.clone());
+ }
+
+ let mut autoload_packages: IndexMap<String, PackageInterfaceHandle> = IndexMap::new();
+ autoload_packages.insert(package.get_name().to_string(), package.clone());
+ let autoload_packages =
+ self.collect_dependencies(&installed_repo, autoload_packages, package.clone())?;
+
+ let generator = composer.borrow().get_autoload_generator();
+ let root_package_as_package: PackageInterfaceHandle = root_package.clone().into();
+ let mut autoloads: Vec<(PackageInterfaceHandle, Option<String>)> =
+ vec![(root_package_as_package.clone(), Some(String::new()))];
+ for (_name, autoload_package) in &autoload_packages {
+ if autoload_package.ptr_eq(&root_package_as_package) {
+ continue;
+ }
+
+ let is_global_package = match &global_repo {
+ Some(gr) => gr.borrow_mut().has_package(autoload_package.clone())?,
+ None => false,
+ };
+ let install_path = self.get_install_path(autoload_package.clone(), is_global_package);
+ let install_path = match install_path {
+ Some(p) => p,
+ None => continue,
+ };
+ autoloads.push((autoload_package.clone(), Some(install_path)));
+ }
+
+ let map =
+ generator
+ .borrow()
+ .parse_autoloads(autoloads, root_package, PhpMixed::Bool(false));
+ let vendor_dir = composer
+ .borrow()
+ .get_config()
+ .borrow()
+ .get("vendor-dir")
+ .as_string()
+ .map(|s| s.to_string());
+ let mut class_loader = generator.borrow().create_loader(&map, vendor_dir);
+ class_loader.register(false);
+
+ // The plugin code runs in the PHP worker: load the Composer PHP runtime (contracts like
+ // PluginInterface) and the reverse-RPC autoloader before touching plugin classes there.
+ EventDispatcher::ensure_composer_php_runtime()?;
+ EventDispatcher::ensure_script_autoloader()?;
+
+ if let Some(files) = map.get("files").and_then(|v| v.as_array()) {
+ for (file_identifier, file) in files {
+ // exclude laminas/laminas-zendframework-bridge:src/autoload.php as it breaks Composer in some conditions
+ // see https://github.com/composer/composer/issues/10349 and https://github.com/composer/composer/issues/10401
+ // this hack can be removed once this deprecated package stop being installed
+ if file_identifier == "7e9bd612cc444b3eed788ebbe46263a0" {
+ continue;
+ }
+ let file = file.as_string().unwrap_or_else(|| {
+ panic!("autoload files entry `{file_identifier}` is not a string path")
+ });
+ self.php_runtime_composer_require(file_identifier, file)?;
+ }
+ }
+
+ for class in classes {
+ let mut class = class;
+ if self.php_runtime_class_exists(&class, false)? {
+ class = trim(&class, Some("\\")).to_string();
+ let path = class_loader.find_file(&class).unwrap_or_else(|| {
+ panic!("plugin class `{class}` is already defined but has no autoloadable file")
+ });
+ let code = file_get_contents(&path)
+ .unwrap_or_else(|| panic!("unable to read the plugin class file `{path}`"));
+ let class_counter = CLASS_COUNTER.load(std::sync::atomic::Ordering::Relaxed);
+ let separator_pos = strrpos(&class, "\\");
+ let mut class_name = class.clone();
+ // PHP: `if ($separatorPos)` — position 0 is falsy and keeps the full name.
+ if let Some(separator_pos) = separator_pos
+ && separator_pos != 0
+ {
+ class_name = substr(&class, (separator_pos + 1) as i64, None);
+ }
+ let code = Preg::replace4(
+ format!(
+ "{{^((?:(?:final|readonly)\\s+)*(?:\\s*))class\\s+({})}}mi",
+ preg_quote(&class_name, None)
+ ),
+ &format!("$1class $2_composer_tmp{}", class_counter),
+ &code,
+ 1,
+ );
+ let mut replacements: IndexMap<String, String> = IndexMap::new();
+ replacements.insert("__FILE__".to_string(), var_export_str(&path, true));
+ replacements.insert("__DIR__".to_string(), var_export_str(&dirname(&path), true));
+ replacements.insert("__CLASS__".to_string(), var_export_str(&class, true));
+ let code = strtr_array(&code, &replacements);
+ let code = Preg::replace4(r"/^\s*<\?(php)?/i", "", &code, 1);
+ self.php_runtime_eval(&code)?;
+ class = format!("{}_composer_tmp{}", class, class_counter);
+ CLASS_COUNTER.store(class_counter + 1, std::sync::atomic::Ordering::Relaxed);
+ }
+
+ 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,
+ }
+ .into());
+ } else if self.php_runtime_class_exists(&class, true)? {
+ if !self.php_runtime_is_a(&class, "Composer\\Plugin\\PluginInterface")? {
+ return Err(RuntimeException {
+ message: format!(
+ "Could not activate plugin \"{}\" as \"{}\" does not implement Composer\\Plugin\\PluginInterface",
+ package.get_name(),
+ class
+ ),
+ code: 0,
+ }
+ .into());
+ }
+ let handle = self.php_runtime_new_object(&class)?;
+ let plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>> = std::rc::Rc::new(
+ std::cell::RefCell::new(PhpPluginProxy::new(handle.phandle, handle.class)),
+ );
+ self.add_plugin(plugin.clone(), is_global_plugin, Some(package.clone()))?;
+ self.registered_plugins
+ .entry(package.get_name().to_string())
+ .or_default()
+ .push(PluginOrInstaller::Plugin(plugin));
+ } else if fail_on_missing_classes {
+ return Err(UnexpectedValueException {
+ message: format!(
+ "Plugin {} could not be initialized, class not found: {}",
+ package.get_name(),
+ class
+ ),
+ code: 0,
+ }
+ .into());
+ }
+ }
Ok(())
}
+ /// Runs a boolean runtime query in the PHP worker with the plugin dispatcher active.
+ fn php_runtime_bool(&self, function: &str, args: Vec<PluginValue>) -> anyhow::Result<bool> {
+ let value = unwrap_php_result(call_function_with_dispatcher(
+ function,
+ args,
+ Some(&mut PluginRpcDispatcher),
+ ))?;
+ match value {
+ PluginValue::Bool(value) => Ok(value),
+ other => Err(anyhow::anyhow!(
+ "PHP runtime query `{function}` did not return a bool: {other:?}"
+ )),
+ }
+ }
+
+ fn php_runtime_class_exists(&self, class: &str, autoload: bool) -> anyhow::Result<bool> {
+ self.php_runtime_bool(
+ "class_exists",
+ vec![PluginValue::string(class), PluginValue::Bool(autoload)],
+ )
+ }
+
+ fn php_runtime_is_a(&self, class: &str, interface: &str) -> anyhow::Result<bool> {
+ self.php_runtime_bool(
+ "is_a",
+ vec![
+ PluginValue::string(class),
+ PluginValue::string(interface),
+ PluginValue::Bool(true),
+ ],
+ )
+ }
+
+ fn php_runtime_eval(&self, code: &str) -> anyhow::Result<()> {
+ unwrap_php_result(call_function_with_dispatcher(
+ "__shirabe_eval",
+ vec![PluginValue::string(code)],
+ Some(&mut PluginRpcDispatcher),
+ ))?;
+ Ok(())
+ }
+
+ fn php_runtime_composer_require(
+ &self,
+ file_identifier: &str,
+ file: &str,
+ ) -> anyhow::Result<()> {
+ unwrap_php_result(call_function_with_dispatcher(
+ "__shirabe_composer_require",
+ vec![
+ PluginValue::string(file_identifier),
+ PluginValue::string(file),
+ ],
+ Some(&mut PluginRpcDispatcher),
+ ))?;
+ Ok(())
+ }
+
+ /// 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> {
+ let value = unwrap_php_result(shirabe_php_rpc::new_object(
+ class,
+ vec![],
+ Some(&mut PluginRpcDispatcher),
+ ))?;
+ match value {
+ PluginValue::PhpHandle(handle) => Ok(handle),
+ other => Err(anyhow::anyhow!(
+ "instantiating `{class}` did not return a PHP handle: {other:?}"
+ )),
+ }
+ }
+
/// Deactivates a plugin package
- pub fn deactivate_package(&mut self, package: PackageInterfaceHandle) {
- // TODO(plugin): deactivation flow
+ pub fn deactivate_package(&mut self, package: PackageInterfaceHandle) -> anyhow::Result<()> {
if !self.registered_plugins.contains_key(&package.get_name()) {
- return;
+ return Ok(());
}
let plugins = self
@@ -349,17 +592,17 @@ impl PluginManager {
.remove_installer(&*inst);
}
PluginOrInstaller::Plugin(p) => {
- self.remove_plugin(&*p);
+ self.remove_plugin(&p)?;
}
}
}
+ Ok(())
}
/// Uninstall a plugin package
- pub fn uninstall_package(&mut self, package: PackageInterfaceHandle) {
- // TODO(plugin): uninstall flow
+ pub fn uninstall_package(&mut self, package: PackageInterfaceHandle) -> anyhow::Result<()> {
if !self.registered_plugins.contains_key(&package.get_name()) {
- return;
+ return Ok(());
}
let plugins = self
@@ -375,12 +618,13 @@ impl PluginManager {
.borrow_mut()
.remove_installer(&*inst);
}
- PluginOrInstaller::Plugin(mut p) => {
- self.remove_plugin(&*p);
- self.uninstall_plugin(&mut *p);
+ PluginOrInstaller::Plugin(p) => {
+ self.remove_plugin(&p)?;
+ self.uninstall_plugin(&p)?;
}
}
}
+ Ok(())
}
/// Returns the version of the internal composer-plugin-api package.
@@ -391,11 +635,10 @@ impl PluginManager {
/// Adds a plugin, activates it and registers it with the event dispatcher
pub fn add_plugin(
&mut self,
- mut plugin: Box<dyn PluginInterface>,
+ plugin: std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>,
is_global_plugin: bool,
source_package: Option<PackageInterfaceHandle>,
) -> anyhow::Result<()> {
- // TODO(plugin): plugin activation
if self.are_plugins_disabled(if is_global_plugin { "global" } else { "local" }) {
return Ok(());
}
@@ -419,16 +662,20 @@ impl PluginManager {
plugin_optional,
true,
)? {
- self.io.write_error(&format!(
- "Skipped loading \"{} from {}\" {} as it is not in config.allow-plugins",
- get_class_obj(&*plugin),
- sp.get_name(),
- if is_global_plugin || self.running_in_global_dir {
- "(installed globally) "
- } else {
- ""
- }
- ));
+ self.io.write_error3(
+ &format!(
+ "Skipped loading \"{} from {}\" {} as it is not in config.allow-plugins",
+ plugin.borrow().get_class_name(),
+ sp.get_name(),
+ if is_global_plugin || self.running_in_global_dir {
+ "(installed globally) "
+ } else {
+ ""
+ }
+ ),
+ true,
+ crate::io::DEBUG,
+ );
return Ok(());
}
}
@@ -441,65 +688,88 @@ impl PluginManager {
if is_global_plugin || self.running_in_global_dir {
details.push("installed globally".to_string());
}
- self.io.write_error(&format!(
- "Loading plugin {}{}",
- get_class_obj(&*plugin),
- if !details.is_empty() {
- format!(" ({})", implode(", ", &details))
- } else {
- String::new()
- }
- ));
- plugin.activate(&self.composer_full(), self.io.clone());
+ self.io.write_error3(
+ &format!(
+ "Loading plugin {}{}",
+ plugin.borrow().get_class_name(),
+ if !details.is_empty() {
+ format!(" ({})", implode(", ", &details))
+ } else {
+ String::new()
+ }
+ ),
+ true,
+ crate::io::DEBUG,
+ );
+ self.plugins.push(plugin.clone());
+ plugin
+ .borrow_mut()
+ .activate(self.composer_full(), self.io.clone())?;
// TODO(plugin): if plugin is EventSubscriberInterface, hook into the event dispatcher
// The PHP code calls $this->composer->getEventDispatcher()->addSubscriber($plugin);
// — add_subscriber here is generic over `S: EventSubscriberInterface` and cannot
// accept a `&dyn EventSubscriberInterface`. Skipped until subscriber dispatch is
// implemented dynamically.
- let _ = (*plugin).is_event_subscriber_interface();
- self.plugins.push(plugin);
+ let _ = plugin.borrow().is_event_subscriber_interface();
Ok(())
}
/// Removes a plugin, deactivates it and removes any listener the plugin has set on the plugin instance
- pub fn remove_plugin(&mut self, plugin: &dyn PluginInterface) {
- // TODO(plugin): plugin removal — PHP uses identity (`===`) comparison via array_search($plugin, $this->plugins, true).
- let plugin_addr = plugin as *const dyn PluginInterface as *const () as usize;
- let index = self.plugins.iter().position(|p| {
- (p.as_ref() as *const dyn PluginInterface as *const () as usize) == plugin_addr
- });
+ pub fn remove_plugin(
+ &mut self,
+ plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>,
+ ) -> anyhow::Result<()> {
+ // PHP uses identity (`===`) comparison via array_search($plugin, $this->plugins, true).
+ let index = self
+ .plugins
+ .iter()
+ .position(|p| std::rc::Rc::ptr_eq(p, plugin));
let index = match index {
Some(i) => i,
- None => return,
+ None => return Ok(()),
};
- self.io
- .write_error(&format!("Unloading plugin {}", get_class_obj(plugin)));
- let mut removed = self.plugins.remove(index);
- removed.deactivate(&self.composer_full(), self.io.clone());
+ self.io.write_error3(
+ &format!("Unloading plugin {}", plugin.borrow().get_class_name()),
+ true,
+ crate::io::DEBUG,
+ );
+ let removed = self.plugins.remove(index);
+ removed
+ .borrow_mut()
+ .deactivate(self.composer_full(), self.io.clone())?;
// TODO(plugin): remove_listener accepts any callable/object in PHP; here we have
// a plugin instance and need to translate to a Callable, which is not portable
// without runtime reflection.
- let _ = plugin;
+ Ok(())
}
/// Notifies a plugin it is being uninstalled and should clean up
- pub fn uninstall_plugin(&self, plugin: &mut dyn PluginInterface) {
- // TODO(plugin): plugin uninstall hook
- self.io
- .write_error(&format!("Uninstalling plugin {}", get_class_obj(plugin)));
- plugin.uninstall(&self.composer_full(), self.io.clone());
+ pub fn uninstall_plugin(
+ &self,
+ plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>,
+ ) -> anyhow::Result<()> {
+ self.io.write_error3(
+ &format!("Uninstalling plugin {}", plugin.borrow().get_class_name()),
+ true,
+ crate::io::DEBUG,
+ );
+ plugin
+ .borrow_mut()
+ .uninstall(self.composer_full(), self.io.clone())?;
+ Ok(())
}
+ // The repository stays behind its shared handle (borrowed only transiently) because
+ // register_package re-enters the same local repository through the RepositoryManager.
fn load_repository(
&mut self,
- repo: &mut dyn RepositoryInterface,
+ repo: &RepositoryInterfaceHandle,
is_global_repo: bool,
root_package: Option<RootPackageInterfaceHandle>,
) -> anyhow::Result<()> {
- // TODO(plugin): repository scan for plugin packages
let packages = repo.get_packages()?;
let mut weights: IndexMap<String, i64> = IndexMap::new();
@@ -570,10 +840,9 @@ impl PluginManager {
fn deactivate_repository(
&mut self,
- repo: &mut dyn RepositoryInterface,
+ repo: &RepositoryInterfaceHandle,
_is_global_repo: bool,
) -> anyhow::Result<()> {
- // TODO(plugin): deactivate plugins from a repository
let packages = repo.get_packages()?;
// PHP: $sortedPackages = array_reverse(PackageSorter::sortPackages($packages));
let mut sorted_packages = PackageSorter::sort_packages(packages.to_vec(), IndexMap::new());
@@ -584,10 +853,10 @@ impl PluginManager {
continue;
}
if "composer-plugin" == package.get_type() {
- self.deactivate_package(package.clone());
+ self.deactivate_package(package.clone())?;
// Backward compatibility
} else if "composer-installer" == package.get_type() {
- self.deactivate_package(package.clone());
+ self.deactivate_package(package.clone())?;
}
}
@@ -686,7 +955,7 @@ impl PluginManager {
return Err(UnexpectedValueException {
message: format!(
"Plugin {} provided invalid capability class name(s), got {}",
- get_class_obj(plugin),
+ plugin.get_class_name(),
var_export_str(capabilities.get(capability).unwrap(), true)
),
code: 0,
@@ -721,9 +990,11 @@ impl PluginManager {
// TODO(plugin): aggregate capabilities across all loaded plugins
let mut capabilities: Vec<Box<dyn Capability>> = vec![];
for plugin in self.get_plugins() {
- if let Ok(Some(capability)) =
- self.get_plugin_capability(&**plugin, capability_class_name, ctor_args.clone())
- {
+ if let Ok(Some(capability)) = self.get_plugin_capability(
+ &*plugin.borrow(),
+ capability_class_name,
+ ctor_args.clone(),
+ ) {
capabilities.push(capability);
}
}