aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src')
-rw-r--r--crates/shirabe/src/command/create_project_command.rs6
-rw-r--r--crates/shirabe/src/command/reinstall_command.rs43
-rw-r--r--crates/shirabe/src/event_dispatcher/event_dispatcher.rs14
-rw-r--r--crates/shirabe/src/factory.rs16
-rw-r--r--crates/shirabe/src/installer.rs8
-rw-r--r--crates/shirabe/src/installer/installation_manager.rs42
-rw-r--r--crates/shirabe/src/installer/installer_interface.rs17
-rw-r--r--crates/shirabe/src/installer/library_installer.rs12
-rw-r--r--crates/shirabe/src/installer/metapackage_installer.rs12
-rw-r--r--crates/shirabe/src/installer/noop_installer.rs12
-rw-r--r--crates/shirabe/src/installer/plugin_installer.rs47
-rw-r--r--crates/shirabe/src/installer/project_installer.rs10
-rw-r--r--crates/shirabe/src/plugin.rs2
-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
-rw-r--r--crates/shirabe/src/repository/filesystem_repository.rs18
-rw-r--r--crates/shirabe/src/repository/handle.rs60
-rw-r--r--crates/shirabe/src/repository/installed_array_repository.rs6
-rw-r--r--crates/shirabe/src/repository/installed_filesystem_repository.rs6
-rw-r--r--crates/shirabe/src/repository/repository_interface.rs6
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
}