diff options
| author | nsfisis <nsfisis@gmail.com> | 2026-08-04 02:25:28 +0900 |
|---|---|---|
| committer | nsfisis <nsfisis@gmail.com> | 2026-08-04 05:43:18 +0900 |
| commit | a02fc7d728a9973a3275a0f47604081c4439b424 (patch) | |
| tree | b1a73d2eb6b17bb6c204318b58d75ae6cf46cc7a /crates/shirabe/src | |
| parent | 20f7a7826ae048d249e0d837ca6393a5f09c9ba6 (diff) | |
| download | php-shirabe-a02fc7d728a9973a3275a0f47604081c4439b424.tar.gz php-shirabe-a02fc7d728a9973a3275a0f47604081c4439b424.tar.zst php-shirabe-a02fc7d728a9973a3275a0f47604081c4439b424.zip | |
feat(plugin): activate plugins through the PHP RPC worker
Implement the remainder of PluginManager::registerPackage: the plugin
autoload map is built by the ported createLoader/parseAutoloads and
served to the worker over the existing reverse-RPC autoloader, files
entries go through a composerRequire-equivalent glue call, and
already-defined classes take the upstream _composer_tmp rename/eval
path. Instantiation uses the new NewObject/CallPhpMethod lanes backed
by a P table in the worker; PhpPluginProxy adapts the resulting handle
to PluginInterface, with $composer/$io exposed to plugin callbacks via
an R table (unsupported methods stay explicit errors). Hand-written
proxy stubs cover Composer, PartialComposer and the IO hierarchy, and
the stub autoloader is re-prepended after loading the Composer PHP
runtime so its vendor autoloader cannot shadow proxied FQCNs.
FilesystemRepository::write now mirrors InstalledVersions::reload into
a running worker (class_exists-guarded, so an unloaded class keeps its
upstream lazy-load behavior), removing the previously undefined
observation window.
The installer pipeline passes the installed repository as a shared
handle instead of a long-lived `&mut dyn`: plugin registration runs
inside InstallationManager::execute and re-enters the same local
repository through the RepositoryManager, which would panic on the
RefCell re-borrow under the old shape.
PluginInterface lifecycle methods now take an owned ComposerHandle
(plugins retain $composer past the call) and return anyhow::Result
(PHP plugin code may throw); the plugin list uses shared ownership so
the identity comparison of removePlugin survives the dual storage in
registeredPlugins, matching PHP reference semantics.
Ports the activate/upgrade/uninstall tests of PluginInstallerTest,
serialized across the shared worker process whose persistent class
table is exactly what exercises the rename path.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe/src')
21 files changed, 938 insertions, 222 deletions
diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs index 7312a60b..a907bc09 100644 --- a/crates/shirabe/src/command/create_project_command.rs +++ b/crates/shirabe/src/command/create_project_command.rs @@ -1011,9 +1011,11 @@ impl CreateProjectCommand { let mut im = installation_manager.borrow_mut(); im.set_output_progress(!no_progress); im.add_installer(Box::new(project_installer)); - let mut installed_repo = InstalledArrayRepository::new()?; + let installed_repo = crate::repository::InstalledRepositoryInterfaceHandle::new( + InstalledArrayRepository::new()?, + ); im.execute( - &mut installed_repo, + &installed_repo, vec![InstallOperation::new(package.clone()).into()], true, true, diff --git a/crates/shirabe/src/command/reinstall_command.rs b/crates/shirabe/src/command/reinstall_command.rs index 95fde5ae..0d3567a2 100644 --- a/crates/shirabe/src/command/reinstall_command.rs +++ b/crates/shirabe/src/command/reinstall_command.rs @@ -244,32 +244,23 @@ impl Command for ReinstallCommand { let uninstall_operations: Vec<AnyOperation> = uninstall_operations.into_iter().map(Into::into).collect(); - { - let mut local_repo_ref = local_repo.borrow_mut(); - let repo = local_repo_ref - .as_installed_repository_interface_mut() - .expect("local repository must be an InstalledRepositoryInterface"); - installation_manager.borrow_mut().execute( - repo, - uninstall_operations, - dev_mode, - true, - false, - )?; - } - { - let mut local_repo_ref = local_repo.borrow_mut(); - let repo = local_repo_ref - .as_installed_repository_interface_mut() - .expect("local repository must be an InstalledRepositoryInterface"); - installation_manager.borrow_mut().execute( - repo, - install_operations.clone(), - dev_mode, - true, - false, - )?; - } + let repo = crate::repository::InstalledRepositoryInterfaceHandle::from_repository_handle( + &local_repo, + ); + installation_manager.borrow_mut().execute( + &repo, + uninstall_operations, + dev_mode, + true, + false, + )?; + installation_manager.borrow_mut().execute( + &repo, + install_operations.clone(), + dev_mode, + true, + false, + )?; if !input .borrow() diff --git a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs index 2097c83d..f5b85ca1 100644 --- a/crates/shirabe/src/event_dispatcher/event_dispatcher.rs +++ b/crates/shirabe/src/event_dispatcher/event_dispatcher.rs @@ -596,7 +596,7 @@ impl EventDispatcher { // The user's command class extends Symfony's Command, so the child // process needs the real symfony/console classes before it can even // autoload the user class. - self.ensure_composer_php_runtime()?; + Self::ensure_composer_php_runtime()?; if !self.php_runtime_bool( "class_exists", vec![PluginValue::string(class_name.clone())], @@ -699,7 +699,7 @@ try {{ "false" }, ); - self.ensure_script_autoloader()?; + Self::ensure_script_autoloader()?; let mut dispatcher = ScriptRpcDispatcher { loader: self.loader.clone(), event: None, @@ -1054,7 +1054,7 @@ try {{ })); }; - self.ensure_script_autoloader()?; + Self::ensure_script_autoloader()?; let rhandle = shirabe_php_rpc::alloc_rhandle(); let mut dispatcher = ScriptRpcDispatcher { loader: self.loader.clone(), @@ -1435,7 +1435,7 @@ try {{ /// Makes the worker's script-class autoloader active, so class queries and script execution /// in the child can resolve classes through the Rust-side [`ClassLoader`] built by /// [`Self::make_autoloader`]. - fn ensure_script_autoloader(&self) -> anyhow::Result<()> { + pub(crate) fn ensure_script_autoloader() -> anyhow::Result<()> { unwrap_php_result(call_function( "__shirabe_enable_script_autoloader", Vec::new(), @@ -1445,7 +1445,7 @@ try {{ /// Loads the Composer PHP runtime (symfony/console and friends) into the worker, needed /// before a `scripts` Command class can be autoloaded and hosted. - fn ensure_composer_php_runtime(&self) -> anyhow::Result<()> { + pub(crate) fn ensure_composer_php_runtime() -> anyhow::Result<()> { // TODO(plugin): the real PHP classes are taken from a Composer checkout for now; how // they ship with a released Shirabe binary is part of the plugin distribution work. let autoload = Self::composer_php_runtime_autoload().ok_or_else(|| { @@ -1484,7 +1484,7 @@ try {{ /// Runs a boolean runtime query (`class_exists`, `is_a`, ...) inside the PHP worker, with /// the script autoloader active so the query can trigger class loading. fn php_runtime_bool(&self, function: &str, args: Vec<PluginValue>) -> anyhow::Result<bool> { - self.ensure_script_autoloader()?; + Self::ensure_script_autoloader()?; let mut dispatcher = ScriptRpcDispatcher { loader: self.loader.clone(), event: None, @@ -1616,7 +1616,7 @@ fn runtime_throw(message: String) -> PhpThrow { /// Collapses the two failure lanes of an RPC call into `anyhow`: the callers here treat a PHP /// exception raised during a runtime query as fatal for the current dispatch. -fn unwrap_php_result( +pub(crate) fn unwrap_php_result( outcome: anyhow::Result<Result<PluginValue, PhpThrow>>, ) -> anyhow::Result<PluginValue> { match outcome? { diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs index 29dc3e05..903f6d35 100644 --- a/crates/shirabe/src/factory.rs +++ b/crates/shirabe/src/factory.rs @@ -42,7 +42,6 @@ use crate::plugin::PluginEvents; use crate::plugin::PluginManager; use crate::repository::FilesystemRepository; use crate::repository::InstalledFilesystemRepository; -use crate::repository::InstalledRepositoryInterface; use crate::repository::RepositoryFactory; use crate::repository::RepositoryManager; use crate::util::Filesystem; @@ -892,13 +891,9 @@ impl Factory { // once everything is initialized we can // purge packages from local repos if they have been deleted on the filesystem // PHP: $this->purgePackages($rm->getLocalRepository(), $im); - // TODO(phase-c): the rm/im locals are still in scope (Rc-shared with composer), but - // purge_packages wants `&mut dyn InstalledRepositoryInterface` and - // RepositoryManager::get_local_repository yields a RepositoryInterfaceHandle that - // exposes no raw &mut InstalledRepositoryInterface view (only per-method helpers that - // borrow internally). Wiring this needs such an accessor plus completing - // purge_packages' removal body (repo.removePackage), which is itself still a stub. - // self.purge_packages(rm.get_local_repository(), &mut im)?; + // TODO(phase-c): purge_packages' removal body (repo.removePackage for packages + // deleted on the filesystem) is still a stub; wire this call once implemented. + // self.purge_packages(&InstalledRepositoryInterfaceHandle::from_repository_handle(&rm.get_local_repository()), &mut im)?; } Ok(PartialComposerHandle::from_rc(composer)) @@ -1316,10 +1311,11 @@ impl Factory { fn purge_packages( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &crate::repository::InstalledRepositoryInterfaceHandle, im: &mut InstallationManager, ) -> anyhow::Result<()> { - for package in repo.get_packages()? { + let packages = repo.borrow_mut().get_packages()?; + for package in packages { if !im.is_package_installed(repo, package.clone())? { let _ = package; } diff --git a/crates/shirabe/src/installer.rs b/crates/shirabe/src/installer.rs index d46d693d..f68ab35f 100644 --- a/crates/shirabe/src/installer.rs +++ b/crates/shirabe/src/installer.rs @@ -1257,17 +1257,15 @@ impl Installer { if self.execute_operations { local_repo.set_dev_package_names(self.locker.borrow_mut().get_dev_package_names()?); - let mut local_repo_ref = local_repo.borrow_mut(); self.installation_manager.borrow_mut().execute( - local_repo_ref - .as_installed_repository_interface_mut() - .unwrap(), + &crate::repository::InstalledRepositoryInterfaceHandle::from_repository_handle( + &local_repo, + ), local_repo_transaction.get_operations().clone(), self.dev_mode, self.run_scripts, self.download_only, )?; - drop(local_repo_ref); // see https://github.com/composer/composer/issues/2764 if !local_repo_transaction.get_operations().is_empty() { diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs index cf75469c..8d248a76 100644 --- a/crates/shirabe/src/installer/installation_manager.rs +++ b/crates/shirabe/src/installer/installation_manager.rs @@ -16,6 +16,7 @@ use crate::io::IOInterfaceImmutable; use crate::io::io_interface; use crate::package::PackageInterfaceHandle; use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use crate::util::Platform; use crate::util::r#loop::Loop; use crate::util::sync_executor; @@ -198,17 +199,18 @@ impl InstallationManager { /// Checks whether provided package is installed in one of the registered installers. pub fn is_package_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { // For testing only (ref InstallationManagerMock::isPackageInstalled). if self.mock.is_some() { - return repo.has_package(package); + return repo.borrow_mut().has_package(package); } if let Some(alias) = package.as_alias() { let alias_of: PackageInterfaceHandle = alias.get_alias_of().into(); - return Ok(repo.has_package(package)? && self.is_package_installed(repo, alias_of)?); + return Ok(repo.borrow_mut().has_package(package)? + && self.is_package_installed(repo, alias_of)?); } self.get_installer(&package.get_type())? @@ -236,7 +238,7 @@ impl InstallationManager { /// Executes solver operation. pub fn execute( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, operations: Vec<AnyOperation>, dev_mode: bool, run_scripts: bool, @@ -248,6 +250,7 @@ impl InstallationManager { // borrowed across the loop without also borrowing `&self`. if let Some(mock) = self.mock.as_mut() { let _ = (dev_mode, run_scripts, download_only); + let mut repo = repo.borrow_mut(); for operation in operations { let trace = shirabe_php_shim::strip_tags(&operation.to_string()); match operation { @@ -318,11 +321,6 @@ impl InstallationManager { let all_operations: Vec<AnyOperation> = operations.clone(); - // The concurrent operation chains share the repository; each chain borrows it only in - // synchronous sections, never across an await. - let repo_cell: std::cell::RefCell<&mut dyn InstalledRepositoryInterface> = - std::cell::RefCell::new(repo); - let result: anyhow::Result<()> = (|| -> anyhow::Result<()> { // execute operations in batches to make sure download-modifying-plugins are installed // before the other packages get downloaded @@ -363,7 +361,7 @@ impl InstallationManager { for batch_to_execute in batches { sync_executor::block_on(self.download_and_execute_batch( - &repo_cell, + repo, batch_to_execute, &mut cleanup_promises, dev_mode, @@ -394,7 +392,7 @@ impl InstallationManager { // do a last write so that we write the repository even if nothing changed // as that can trigger an update of some files like InstalledVersions.php if // running a new composer version - repo_cell.into_inner().write(dev_mode, self); + repo.borrow_mut().write(dev_mode, self); Ok(()) } @@ -402,7 +400,7 @@ impl InstallationManager { #[allow(clippy::too_many_arguments, reason = "to keep PHP signature")] async fn download_and_execute_batch( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operations: IndexMap<i64, AnyOperation>, cleanup_promises: &mut IndexMap< i64, @@ -544,7 +542,7 @@ impl InstallationManager { async fn execute_batch( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operations: IndexMap<i64, AnyOperation>, cleanup_promises: &IndexMap< i64, @@ -577,10 +575,10 @@ impl InstallationManager { } match &operation { AnyOperation::MarkAliasInstalled(op) => { - self.mark_alias_installed(&mut **repo.borrow_mut(), op)?; + self.mark_alias_installed(&mut *repo.borrow_mut(), op)?; } AnyOperation::MarkAliasUninstalled(op) => { - self.mark_alias_uninstalled(&mut **repo.borrow_mut(), op); + self.mark_alias_uninstalled(&mut *repo.borrow_mut(), op); } _ => {} } @@ -701,7 +699,7 @@ impl InstallationManager { /// Executes install operation. pub async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operation: &InstallOperation, ) -> anyhow::Result<Option<PhpMixed>> { let package = operation.get_package(); @@ -716,7 +714,7 @@ impl InstallationManager { /// Executes update operation. pub async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operation: &UpdateOperation, ) -> anyhow::Result<Option<PhpMixed>> { let initial = operation.get_initial_package().clone(); @@ -744,7 +742,7 @@ impl InstallationManager { /// Uninstalls package. pub async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, operation: &UninstallOperation, ) -> anyhow::Result<Option<PhpMixed>> { let package = operation.get_package(); @@ -1017,13 +1015,13 @@ pub trait InstallationManagerInterface: std::fmt::Debug { fn disable_plugins(&mut self); fn is_package_installed( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool>; fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle); fn execute( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, operations: Vec<AnyOperation>, dev_mode: bool, run_scripts: bool, @@ -1053,7 +1051,7 @@ impl InstallationManagerInterface for InstallationManager { fn is_package_installed( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { InstallationManager::is_package_installed(self, repo, package) @@ -1065,7 +1063,7 @@ impl InstallationManagerInterface for InstallationManager { fn execute( &mut self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, operations: Vec<AnyOperation>, dev_mode: bool, run_scripts: bool, diff --git a/crates/shirabe/src/installer/installer_interface.rs b/crates/shirabe/src/installer/installer_interface.rs index f2c0cd7c..bc464735 100644 --- a/crates/shirabe/src/installer/installer_interface.rs +++ b/crates/shirabe/src/installer/installer_interface.rs @@ -3,7 +3,7 @@ use crate::installer::BinaryPresenceInterface; use crate::installer::PluginInstaller; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use shirabe_php_shim::PhpMixed; #[async_trait::async_trait(?Send)] @@ -12,7 +12,7 @@ pub trait InstallerInterface: std::fmt::Debug { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool>; @@ -29,25 +29,26 @@ pub trait InstallerInterface: std::fmt::Debug { prev_package: Option<PackageInterfaceHandle>, ) -> anyhow::Result<Option<PhpMixed>>; - // install/update/uninstall take the repository behind a RefCell: the concurrent operation - // chains share it, and implementations must borrow it only in synchronous sections (never - // across an await). + // install/update/uninstall take the repository as a shared handle: the concurrent operation + // chains share it (and re-entrant flows like plugin registration reach the same repository + // through RepositoryManager), so implementations must borrow it only in synchronous + // sections (never across an await). async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>>; async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>>; async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>>; diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs index 976c5a1f..c5a9608e 100644 --- a/crates/shirabe/src/installer/library_installer.rs +++ b/crates/shirabe/src/installer/library_installer.rs @@ -8,7 +8,7 @@ use crate::installer::BinaryPresenceInterface; use crate::installer::InstallerInterface; use crate::io::IOInterface; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use crate::util::Filesystem; use crate::util::Platform; use crate::util::Silencer; @@ -241,10 +241,10 @@ impl InstallerInterface for LibraryInstaller { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { - if !repo.has_package(package.clone())? { + if !repo.borrow_mut().has_package(package.clone())? { return Ok(false); } @@ -315,7 +315,7 @@ impl InstallerInterface for LibraryInstaller { async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { self.initialize_vendor_dir(); @@ -346,7 +346,7 @@ impl InstallerInterface for LibraryInstaller { async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -380,7 +380,7 @@ impl InstallerInterface for LibraryInstaller { async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { if !repo.borrow_mut().has_package(package.clone())? { diff --git a/crates/shirabe/src/installer/metapackage_installer.rs b/crates/shirabe/src/installer/metapackage_installer.rs index 26032a6d..e2821fd3 100644 --- a/crates/shirabe/src/installer/metapackage_installer.rs +++ b/crates/shirabe/src/installer/metapackage_installer.rs @@ -8,7 +8,7 @@ use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::io::io_interface; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use shirabe_php_shim::{InvalidArgumentException, PhpMixed}; #[derive(Debug)] @@ -30,10 +30,10 @@ impl InstallerInterface for MetapackageInstaller { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { - repo.has_package(package) + repo.borrow_mut().has_package(package) } async fn download( @@ -64,7 +64,7 @@ impl InstallerInterface for MetapackageInstaller { async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { self.io.write_error3( @@ -81,7 +81,7 @@ impl InstallerInterface for MetapackageInstaller { async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -111,7 +111,7 @@ impl InstallerInterface for MetapackageInstaller { async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { if !repo.borrow_mut().has_package(package.clone())? { diff --git a/crates/shirabe/src/installer/noop_installer.rs b/crates/shirabe/src/installer/noop_installer.rs index d76ad4ad..68a2b981 100644 --- a/crates/shirabe/src/installer/noop_installer.rs +++ b/crates/shirabe/src/installer/noop_installer.rs @@ -2,7 +2,7 @@ use crate::installer::InstallerInterface; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use shirabe_php_shim::{InvalidArgumentException, PhpMixed}; #[derive(Debug)] @@ -16,10 +16,10 @@ impl InstallerInterface for NoopInstaller { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { - repo.has_package(package) + repo.borrow_mut().has_package(package) } async fn download( @@ -50,7 +50,7 @@ impl InstallerInterface for NoopInstaller { async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { let mut repo = repo.borrow_mut(); @@ -63,7 +63,7 @@ impl InstallerInterface for NoopInstaller { async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -86,7 +86,7 @@ impl InstallerInterface for NoopInstaller { async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { let mut repo = repo.borrow_mut(); diff --git a/crates/shirabe/src/installer/plugin_installer.rs b/crates/shirabe/src/installer/plugin_installer.rs index c0473fd0..1e7a2636 100644 --- a/crates/shirabe/src/installer/plugin_installer.rs +++ b/crates/shirabe/src/installer/plugin_installer.rs @@ -8,7 +8,7 @@ use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::package::PackageInterfaceHandle; use crate::plugin::PluginManager; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use crate::util::Filesystem; use crate::util::Platform; use shirabe_php_shim::{PhpMixed, UnexpectedValueException, empty}; @@ -46,7 +46,7 @@ impl PluginInstaller { async fn rollback_install( &self, e: anyhow::Error, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<()> { self.inner.io.write_error(&format!( @@ -79,7 +79,7 @@ impl InstallerInterface for PluginInstaller { fn is_installed( &self, - repo: &mut dyn InstalledRepositoryInterface, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { self.inner.is_installed(repo, package) @@ -135,43 +135,56 @@ impl InstallerInterface for PluginInstaller { async fn install( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { - self.inner.install(repo, package).await?; + self.inner.install(repo, package.clone()).await?; - // TODO(plugin): register package in plugin manager after install, rollback on failure Platform::workaround_filesystem_issues(); - // self.get_plugin_manager().register_package(package, true)?; - // On error: self.rollback_install(e, repo, package)?; + let result = + self.get_plugin_manager() + .borrow_mut() + .register_package(package.clone(), true, false); + if let Err(e) = result { + self.rollback_install(e, repo, package).await?; + } Ok(None) } async fn update( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, initial: PackageInterfaceHandle, target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { - self.inner.update(repo, initial, target).await?; + self.inner + .update(repo, initial.clone(), target.clone()) + .await?; - // TODO(plugin): deactivate initial and register target in plugin manager after update, rollback on failure Platform::workaround_filesystem_issues(); - // self.get_plugin_manager().deactivate_package(initial); - // self.get_plugin_manager().register_package(target, true)?; - // On error: self.rollback_install(e, repo, target)?; + let result = (|| -> anyhow::Result<()> { + self.get_plugin_manager() + .borrow_mut() + .deactivate_package(initial)?; + self.get_plugin_manager() + .borrow_mut() + .register_package(target.clone(), true, false)?; + Ok(()) + })(); + if let Err(e) = result { + self.rollback_install(e, repo, target).await?; + } Ok(None) } async fn uninstall( &self, - repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { - // TODO(plugin): uninstall package from plugin manager self.get_plugin_manager() .borrow_mut() - .uninstall_package(package.clone()); + .uninstall_package(package.clone())?; self.inner.uninstall(repo, package).await } diff --git a/crates/shirabe/src/installer/project_installer.rs b/crates/shirabe/src/installer/project_installer.rs index 628e4252..6fee682f 100644 --- a/crates/shirabe/src/installer/project_installer.rs +++ b/crates/shirabe/src/installer/project_installer.rs @@ -3,7 +3,7 @@ use crate::downloader::DownloadManagerInterface; use crate::installer::InstallerInterface; use crate::package::PackageInterfaceHandle; -use crate::repository::InstalledRepositoryInterface; +use crate::repository::InstalledRepositoryInterfaceHandle; use crate::util::Filesystem; use shirabe_php_shim::{InvalidArgumentException, PhpMixed}; @@ -37,7 +37,7 @@ impl InstallerInterface for ProjectInstaller { fn is_installed( &self, - _repo: &mut dyn InstalledRepositoryInterface, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<bool> { Ok(false) @@ -94,7 +94,7 @@ impl InstallerInterface for ProjectInstaller { async fn install( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { self.download_manager @@ -105,7 +105,7 @@ impl InstallerInterface for ProjectInstaller { async fn update( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _initial: PackageInterfaceHandle, _target: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { @@ -118,7 +118,7 @@ impl InstallerInterface for ProjectInstaller { async fn uninstall( &self, - _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>, + _repo: &InstalledRepositoryInterfaceHandle, _package: PackageInterfaceHandle, ) -> anyhow::Result<Option<PhpMixed>> { Err(InvalidArgumentException { diff --git a/crates/shirabe/src/plugin.rs b/crates/shirabe/src/plugin.rs index a26e1173..a629e4c0 100644 --- a/crates/shirabe/src/plugin.rs +++ b/crates/shirabe/src/plugin.rs @@ -1,6 +1,7 @@ pub mod capability; pub mod capable; pub mod command_event; +pub mod php_plugin_proxy; pub mod plugin_blocked_exception; pub mod plugin_events; pub mod plugin_interface; @@ -13,6 +14,7 @@ pub mod pre_pool_create_event; pub use capability::*; pub use capable::*; pub use command_event::*; +pub use php_plugin_proxy::*; pub use plugin_blocked_exception::*; pub use plugin_events::*; pub use plugin_interface::*; 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); } } diff --git a/crates/shirabe/src/repository/filesystem_repository.rs b/crates/shirabe/src/repository/filesystem_repository.rs index 32e4022a..b2a2530e 100644 --- a/crates/shirabe/src/repository/filesystem_repository.rs +++ b/crates/shirabe/src/repository/filesystem_repository.rs @@ -343,10 +343,20 @@ impl FilesystemRepository { ); // make sure the in memory state is up to date with on disk - // TODO(plugin): whether this reload must also be pushed to the plugin PHP child - // process is undecided; the InstalledVersions state a plugin observes after this - // dump is undefined (docs/dev/plugin-class-classification.md, "Bootstrap classes - // cannot be stub-shadowed"). + // The upstream in-process reload is split in two here: the Rust-side mirror below, + // and a push into the PHP worker where the real observers (plugins) live. The push + // is skipped when no worker is running — with no child there is nothing that could + // observe the stale state, and the worker glue additionally ignores it while the + // class is not loaded there (a later lazy load reads the freshly written + // installed.php, matching upstream observations in every case). + if shirabe_php_rpc::worker_is_running() { + crate::event_dispatcher::unwrap_php_result(shirabe_php_rpc::call_function( + "__shirabe_installed_versions_reload", + vec![shirabe_php_rpc::PluginValue::from_php_mixed( + &PhpMixed::Array(versions.clone()), + )], + ))?; + } InstalledVersions::reload(versions); // make sure the selfDir matches the expected data at runtime if the class was loaded from the vendor dir, as it may have been diff --git a/crates/shirabe/src/repository/handle.rs b/crates/shirabe/src/repository/handle.rs index 2a7239b9..1e24085b 100644 --- a/crates/shirabe/src/repository/handle.rs +++ b/crates/shirabe/src/repository/handle.rs @@ -3,8 +3,8 @@ use crate::package::BasePackageHandle; use crate::package::PackageInterfaceHandle; use crate::repository::{ - FindPackageConstraint, LoadPackagesResult, LockArrayRepository, PlatformRepository, - ProviderInfo, RepositoryInterface, SearchResult, + FindPackageConstraint, InstalledRepositoryInterface, LoadPackagesResult, LockArrayRepository, + PlatformRepository, ProviderInfo, RepositoryInterface, SearchResult, }; use indexmap::IndexMap; use shirabe_semver::constraint::AnyConstraint; @@ -192,6 +192,62 @@ impl RepositoryInterfaceHandle { } } +/// Shared handle over a repository known to implement `InstalledRepositoryInterface`. +/// +/// The installer pipeline passes this instead of a long-lived `&mut dyn +/// InstalledRepositoryInterface` so that re-entrant access to the same repository through +/// `RepositoryManager::get_local_repository()` — e.g. `PluginManager::register_package` running +/// inside `InstallationManager::execute` — borrows the shared `RefCell` only transiently. +#[derive(Debug, Clone)] +pub struct InstalledRepositoryInterfaceHandle( + std::rc::Rc<std::cell::RefCell<dyn RepositoryInterface>>, +); + +impl InstalledRepositoryInterfaceHandle { + pub fn new<T: RepositoryInterface + 'static>(repository: T) -> Self { + Self::from_repository_handle(&RepositoryInterfaceHandle::new(repository)) + } + + /// PHP has no counterpart for this narrowing: parameters typed + /// `InstalledRepositoryInterface` simply receive such an instance. Handing over a + /// repository that is not one is a programming error. + pub fn from_repository_handle(handle: &RepositoryInterfaceHandle) -> Self { + assert!( + handle.is_installed_repository_interface(), + "repository does not implement InstalledRepositoryInterface" + ); + Self(handle.as_rc().clone()) + } + + pub fn as_repository_handle(&self) -> RepositoryInterfaceHandle { + RepositoryInterfaceHandle::from_rc(self.0.clone()) + } + + pub fn borrow(&self) -> Ref<'_, dyn InstalledRepositoryInterface> { + Ref::map(self.0.borrow(), |r| { + r.as_installed_repository_interface() + .expect("checked at handle construction") + }) + } + + pub fn borrow_mut(&self) -> RefMut<'_, dyn InstalledRepositoryInterface> { + RefMut::map(self.0.borrow_mut(), |r| { + r.as_installed_repository_interface_mut() + .expect("checked at handle construction") + }) + } + + /// PHP `===` (reference identity). + pub fn ptr_eq(&self, other: &Self) -> bool { + std::rc::Rc::ptr_eq(&self.0, &other.0) + } + + /// Stable identity usable as a map key (PHP `spl_object_hash`). + pub fn ptr_id(&self) -> usize { + std::rc::Rc::as_ptr(&self.0) as *const () as usize + } +} + impl PartialEq for RepositoryInterfaceHandle { fn eq(&self, other: &Self) -> bool { std::rc::Rc::ptr_eq(&self.0, &other.0) diff --git a/crates/shirabe/src/repository/installed_array_repository.rs b/crates/shirabe/src/repository/installed_array_repository.rs index c0c0fbb7..9483a1a9 100644 --- a/crates/shirabe/src/repository/installed_array_repository.rs +++ b/crates/shirabe/src/repository/installed_array_repository.rs @@ -137,12 +137,14 @@ impl RepositoryInterface for InstalledArrayRepository { fn as_advisory_provider(&self) -> Option<&dyn AdvisoryProviderInterface> { None } - fn as_installed_repository_interface(&self) -> Option<&dyn InstalledRepositoryInterface> { + fn as_installed_repository_interface( + &self, + ) -> Option<&(dyn InstalledRepositoryInterface + 'static)> { Some(self) } fn as_installed_repository_interface_mut( &mut self, - ) -> Option<&mut dyn InstalledRepositoryInterface> { + ) -> Option<&mut (dyn InstalledRepositoryInterface + 'static)> { Some(self) } fn as_writable_repository_interface_mut( diff --git a/crates/shirabe/src/repository/installed_filesystem_repository.rs b/crates/shirabe/src/repository/installed_filesystem_repository.rs index 0bb962fd..1986f32a 100644 --- a/crates/shirabe/src/repository/installed_filesystem_repository.rs +++ b/crates/shirabe/src/repository/installed_filesystem_repository.rs @@ -171,12 +171,14 @@ impl RepositoryInterface for InstalledFilesystemRepository { fn as_advisory_provider(&self) -> Option<&dyn AdvisoryProviderInterface> { None } - fn as_installed_repository_interface(&self) -> Option<&dyn InstalledRepositoryInterface> { + fn as_installed_repository_interface( + &self, + ) -> Option<&(dyn InstalledRepositoryInterface + 'static)> { Some(self) } fn as_installed_repository_interface_mut( &mut self, - ) -> Option<&mut dyn InstalledRepositoryInterface> { + ) -> Option<&mut (dyn InstalledRepositoryInterface + 'static)> { Some(self) } fn as_writable_repository_interface_mut( diff --git a/crates/shirabe/src/repository/repository_interface.rs b/crates/shirabe/src/repository/repository_interface.rs index cece0d6f..ed1022f4 100644 --- a/crates/shirabe/src/repository/repository_interface.rs +++ b/crates/shirabe/src/repository/repository_interface.rs @@ -107,15 +107,17 @@ pub trait RepositoryInterface: std::fmt::Debug { None } + // The `+ 'static` object bound lets `InstalledRepositoryInterfaceHandle` project a + // `Ref`/`RefMut` through this method (`Ref::map` needs a lifetime-independent target). fn as_installed_repository_interface( &self, - ) -> Option<&dyn crate::repository::InstalledRepositoryInterface> { + ) -> Option<&(dyn crate::repository::InstalledRepositoryInterface + 'static)> { None } fn as_installed_repository_interface_mut( &mut self, - ) -> Option<&mut dyn crate::repository::InstalledRepositoryInterface> { + ) -> Option<&mut (dyn crate::repository::InstalledRepositoryInterface + 'static)> { None } |
