aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe')
-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
-rw-r--r--crates/shirabe/tests/autoload/autoload_generator_test.rs10
-rw-r--r--crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs4
-rw-r--r--crates/shirabe/tests/installer/installation_manager_test.rs58
-rw-r--r--crates/shirabe/tests/installer/library_installer_test.rs120
-rw-r--r--crates/shirabe/tests/installer/metapackage_installer_test.rs80
-rw-r--r--crates/shirabe/tests/plugin/main.rs2
-rw-r--r--crates/shirabe/tests/plugin/plugin_installer_test.rs665
-rw-r--r--crates/shirabe/tests/repository/filesystem_repository_test.rs5
29 files changed, 1593 insertions, 511 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
}
diff --git a/crates/shirabe/tests/autoload/autoload_generator_test.rs b/crates/shirabe/tests/autoload/autoload_generator_test.rs
index 5f0629dd..85e98d81 100644
--- a/crates/shirabe/tests/autoload/autoload_generator_test.rs
+++ b/crates/shirabe/tests/autoload/autoload_generator_test.rs
@@ -13,7 +13,7 @@ use shirabe::io::{BufferIO, IOInterface};
use shirabe::package::handle::{AliasPackageHandle, PackageHandle, RootPackageHandle};
use shirabe::package::{Link, PackageInterfaceHandle, RootPackageInterfaceHandle};
use shirabe::repository::{
- InstalledArrayRepository, InstalledRepositoryInterface, WritableRepositoryInterface,
+ InstalledArrayRepository, InstalledRepositoryInterfaceHandle, WritableRepositoryInterface,
};
use shirabe::util::http_downloader::HttpDownloader;
use shirabe::util::r#loop::Loop;
@@ -39,7 +39,7 @@ impl InstallerInterface for InstallPathStubInstaller {
fn is_installed(
&self,
- _repo: &mut dyn InstalledRepositoryInterface,
+ _repo: &InstalledRepositoryInterfaceHandle,
_package: PackageInterfaceHandle,
) -> anyhow::Result<bool> {
Ok(true)
@@ -64,7 +64,7 @@ impl InstallerInterface for InstallPathStubInstaller {
async fn install(
&self,
- _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>,
+ _repo: &InstalledRepositoryInterfaceHandle,
_package: PackageInterfaceHandle,
) -> anyhow::Result<Option<PhpMixed>> {
Ok(None)
@@ -72,7 +72,7 @@ impl InstallerInterface for InstallPathStubInstaller {
async fn update(
&self,
- _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>,
+ _repo: &InstalledRepositoryInterfaceHandle,
_initial: PackageInterfaceHandle,
_target: PackageInterfaceHandle,
) -> anyhow::Result<Option<PhpMixed>> {
@@ -81,7 +81,7 @@ impl InstallerInterface for InstallPathStubInstaller {
async fn uninstall(
&self,
- _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>,
+ _repo: &InstalledRepositoryInterfaceHandle,
_package: PackageInterfaceHandle,
) -> anyhow::Result<Option<PhpMixed>> {
Ok(None)
diff --git a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs
index e9620158..9cd51a7b 100644
--- a/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs
+++ b/crates/shirabe/tests/event_dispatcher/event_dispatcher_test.rs
@@ -426,13 +426,13 @@ mockall::mock! {
fn disable_plugins(&mut self);
fn is_package_installed(
&mut self,
- repo: &mut dyn InstalledRepositoryInterface,
+ repo: &shirabe::repository::InstalledRepositoryInterfaceHandle,
package: PackageInterfaceHandle,
) -> anyhow::Result<bool>;
fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle);
fn execute(
&mut self,
- repo: &mut dyn InstalledRepositoryInterface,
+ repo: &shirabe::repository::InstalledRepositoryInterfaceHandle,
operations: Vec<AnyOperation>,
dev_mode: bool,
run_scripts: bool,
diff --git a/crates/shirabe/tests/installer/installation_manager_test.rs b/crates/shirabe/tests/installer/installation_manager_test.rs
index 6cc231f5..8f2d7d4f 100644
--- a/crates/shirabe/tests/installer/installation_manager_test.rs
+++ b/crates/shirabe/tests/installer/installation_manager_test.rs
@@ -10,7 +10,7 @@ use shirabe::io::IOInterface;
use shirabe::io::null_io::NullIO;
use shirabe::package::PackageInterfaceHandle;
use shirabe::package::handle::CompletePackageHandle;
-use shirabe::repository::{InstalledArrayRepository, InstalledRepositoryInterface};
+use shirabe::repository::{InstalledArrayRepository, InstalledRepositoryInterfaceHandle};
use shirabe::util::http_downloader::HttpDownloader;
use shirabe::util::r#loop::Loop;
use shirabe_php_shim::PhpMixed;
@@ -39,11 +39,9 @@ fn set_up() -> SetUp {
SetUp { loop_, io }
}
-// Equivalent to `getMockBuilder(InstallerInterface::class)->getMock()`. mockall cannot generate an
-// `#[async_trait]` impl for the async methods that take `&mut dyn InstalledRepositoryInterface`
-// (the object lifetime async_trait inserts clashes with mockall's generated lifetimes), so the
-// expectations live on inherent methods and a thin hand-written InstallerInterface impl forwards to
-// them, dropping the unused `repo` argument exactly as the PHPUnit mock ignores it. The methods not
+// Equivalent to `getMockBuilder(InstallerInterface::class)->getMock()`. The expectations live on
+// inherent methods and a thin hand-written InstallerInterface impl forwards to them, dropping the
+// unused `repo` argument exactly as the PHPUnit mock ignores it. The methods not
// configured by any test (is_installed/download/prepare/cleanup/get_install_path, and the defaulted
// as_binary_presence_interface/as_plugin_installer_mut) return the same defaults as an unconfigured
// PHPUnit mock.
@@ -75,7 +73,7 @@ impl InstallerInterface for MockInstaller {
fn is_installed(
&self,
- _repo: &mut dyn InstalledRepositoryInterface,
+ _repo: &InstalledRepositoryInterfaceHandle,
_package: PackageInterfaceHandle,
) -> anyhow::Result<bool> {
Ok(false)
@@ -100,7 +98,7 @@ impl InstallerInterface for MockInstaller {
async fn install(
&self,
- _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>,
+ _repo: &InstalledRepositoryInterfaceHandle,
package: PackageInterfaceHandle,
) -> anyhow::Result<Option<PhpMixed>> {
MockInstaller::install(self, package)
@@ -108,7 +106,7 @@ impl InstallerInterface for MockInstaller {
async fn update(
&self,
- _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>,
+ _repo: &InstalledRepositoryInterfaceHandle,
initial: PackageInterfaceHandle,
target: PackageInterfaceHandle,
) -> anyhow::Result<Option<PhpMixed>> {
@@ -117,7 +115,7 @@ impl InstallerInterface for MockInstaller {
async fn uninstall(
&self,
- _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>,
+ _repo: &InstalledRepositoryInterfaceHandle,
package: PackageInterfaceHandle,
) -> anyhow::Result<Option<PhpMixed>> {
MockInstaller::uninstall(self, package)
@@ -175,7 +173,7 @@ impl InstallerInterface for BinaryInstaller {
fn is_installed(
&self,
- _repo: &mut dyn InstalledRepositoryInterface,
+ _repo: &InstalledRepositoryInterfaceHandle,
_package: PackageInterfaceHandle,
) -> anyhow::Result<bool> {
Ok(false)
@@ -200,7 +198,7 @@ impl InstallerInterface for BinaryInstaller {
async fn install(
&self,
- _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>,
+ _repo: &InstalledRepositoryInterfaceHandle,
_package: PackageInterfaceHandle,
) -> anyhow::Result<Option<PhpMixed>> {
Ok(None)
@@ -208,7 +206,7 @@ impl InstallerInterface for BinaryInstaller {
async fn update(
&self,
- _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>,
+ _repo: &InstalledRepositoryInterfaceHandle,
_initial: PackageInterfaceHandle,
_target: PackageInterfaceHandle,
) -> anyhow::Result<Option<PhpMixed>> {
@@ -217,7 +215,7 @@ impl InstallerInterface for BinaryInstaller {
async fn uninstall(
&self,
- _repo: &std::cell::RefCell<&mut dyn InstalledRepositoryInterface>,
+ _repo: &InstalledRepositoryInterfaceHandle,
_package: PackageInterfaceHandle,
) -> anyhow::Result<Option<PhpMixed>> {
Ok(None)
@@ -360,11 +358,9 @@ fn test_install() {
let operation = InstallOperation::new(package);
- let mut repository = InstalledArrayRepository::new().unwrap();
- run(manager.install(
- &std::cell::RefCell::new(&mut repository as &mut dyn InstalledRepositoryInterface),
- &operation,
- ));
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
+ run(manager.install(&repository, &operation));
}
#[test]
@@ -395,11 +391,9 @@ fn test_update_with_equal_types() {
let operation = UpdateOperation::new(initial, target);
- let mut repository = InstalledArrayRepository::new().unwrap();
- run(manager.update(
- &std::cell::RefCell::new(&mut repository as &mut dyn InstalledRepositoryInterface),
- &operation,
- ));
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
+ run(manager.update(&repository, &operation));
}
#[test]
@@ -440,11 +434,9 @@ fn test_update_with_not_equal_types() {
let operation = UpdateOperation::new(initial, target);
- let mut repository = InstalledArrayRepository::new().unwrap();
- run(manager.update(
- &std::cell::RefCell::new(&mut repository as &mut dyn InstalledRepositoryInterface),
- &operation,
- ));
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
+ run(manager.update(&repository, &operation));
}
#[test]
@@ -471,11 +463,9 @@ fn test_uninstall() {
let operation = UninstallOperation::new(package);
- let mut repository = InstalledArrayRepository::new().unwrap();
- run(manager.uninstall(
- &std::cell::RefCell::new(&mut repository as &mut dyn InstalledRepositoryInterface),
- &operation,
- ));
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
+ run(manager.uninstall(&repository, &operation));
}
#[test]
diff --git a/crates/shirabe/tests/installer/library_installer_test.rs b/crates/shirabe/tests/installer/library_installer_test.rs
index baa4f13a..6402d903 100644
--- a/crates/shirabe/tests/installer/library_installer_test.rs
+++ b/crates/shirabe/tests/installer/library_installer_test.rs
@@ -12,9 +12,7 @@ use shirabe::installer::{BinaryInstallerInterface, InstallerInterface, LibraryIn
use shirabe::io::IOInterface;
use shirabe::io::null_io::NullIO;
use shirabe::package::PackageInterfaceHandle;
-use shirabe::repository::InstalledArrayRepository;
-use shirabe::repository::RepositoryInterface;
-use shirabe::repository::WritableRepositoryInterface;
+use shirabe::repository::{InstalledArrayRepository, InstalledRepositoryInterfaceHandle};
use shirabe::util::filesystem::Filesystem;
use shirabe_php_shim::PhpMixed;
use std::fs;
@@ -199,32 +197,27 @@ fn test_is_installed() {
let library = LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None);
let package = get_package("test/pkg", "1.0.0");
- let mut repository = InstalledArrayRepository::new().unwrap();
- assert!(
- !library
- .is_installed(&mut repository, package.clone())
- .unwrap()
- );
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
+ assert!(!library.is_installed(&repository, package.clone()).unwrap());
// package being in repo is not enough to be installed
- repository.add_package(package.clone()).unwrap();
- assert!(
- !library
- .is_installed(&mut repository, package.clone())
- .unwrap()
- );
+ repository
+ .borrow_mut()
+ .add_package(package.clone())
+ .unwrap();
+ assert!(!library.is_installed(&repository, package.clone()).unwrap());
// package being in repo and vendor/pkg/foo dir present means it is seen as installed
let pkg_dir = format!("{}/{}", setup.vendor_dir, package.get_pretty_name());
fs::create_dir_all(&pkg_dir).unwrap();
- assert!(
- library
- .is_installed(&mut repository, package.clone())
- .unwrap()
- );
+ assert!(library.is_installed(&repository, package.clone()).unwrap());
- repository.remove_package(package.clone()).unwrap();
- assert!(!library.is_installed(&mut repository, package).unwrap());
+ repository
+ .borrow_mut()
+ .remove_package(package.clone())
+ .unwrap();
+ assert!(!library.is_installed(&repository, package).unwrap());
tear_down(&mut setup);
}
@@ -250,18 +243,13 @@ fn test_install() {
let library = LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None);
- let mut repository = InstalledArrayRepository::new().unwrap();
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
- run(library.install(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface,
- ),
- package.clone(),
- ))
- .unwrap();
+ run(library.install(&repository, package.clone())).unwrap();
// PHP asserts repository->addPackage was called once with $package.
- assert!(repository.has_package(package).unwrap());
+ assert!(repository.borrow_mut().has_package(package).unwrap());
assert!(
std::path::Path::new(&setup.vendor_dir).exists(),
@@ -308,20 +296,17 @@ fn test_update() {
.returning(|_, _, _| Ok(None));
set_download_manager(&setup, dm);
- let mut repository = InstalledArrayRepository::new().unwrap();
- repository.add_package(initial.clone()).unwrap();
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
+ repository
+ .borrow_mut()
+ .add_package(initial.clone())
+ .unwrap();
// The default Filesystem is fine; the LibraryInstaller's own filesystem performs the rename.
let library = LibraryInstaller::new(setup.io.clone(), setup.composer.clone(), None, None, None);
- run(library.update(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface,
- ),
- initial.clone(),
- target.clone(),
- ))
- .unwrap();
+ run(library.update(&repository, initial.clone(), target.clone())).unwrap();
assert!(
std::path::Path::new(&new_target_dir).exists(),
@@ -329,8 +314,13 @@ fn test_update() {
);
assert!(!std::path::Path::new(&old_target_dir).exists());
- assert!(!repository.has_package(initial.clone()).unwrap());
- assert!(repository.has_package(target.clone()).unwrap());
+ assert!(
+ !repository
+ .borrow_mut()
+ .has_package(initial.clone())
+ .unwrap()
+ );
+ assert!(repository.borrow_mut().has_package(target.clone()).unwrap());
assert!(
std::path::Path::new(&setup.vendor_dir).exists(),
@@ -342,16 +332,7 @@ fn test_update() {
);
// Updating again, with the initial package no longer installed, fails.
- assert!(
- run(library.update(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface
- ),
- initial,
- target
- ))
- .is_err()
- );
+ assert!(run(library.update(&repository, initial, target)).is_err());
tear_down(&mut setup);
}
@@ -380,30 +361,25 @@ fn test_uninstall() {
// PHP mocks hasPackage to return (true, false) over two calls; a real repository
// seeded with the package reproduces this naturally: present, then absent after
// the first uninstall removes it.
- let mut repository = InstalledArrayRepository::new().unwrap();
- repository.add_package(package.clone()).unwrap();
-
- run(library.uninstall(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface,
- ),
- package.clone(),
- ))
- .unwrap();
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
+ repository
+ .borrow_mut()
+ .add_package(package.clone())
+ .unwrap();
- assert!(!repository.has_package(package.clone()).unwrap());
+ run(library.uninstall(&repository, package.clone())).unwrap();
- // Uninstalling again, with the package no longer installed, fails.
assert!(
- run(library.uninstall(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface
- ),
- package
- ))
- .is_err()
+ !repository
+ .borrow_mut()
+ .has_package(package.clone())
+ .unwrap()
);
+ // Uninstalling again, with the package no longer installed, fails.
+ assert!(run(library.uninstall(&repository, package)).is_err());
+
tear_down(&mut setup);
}
diff --git a/crates/shirabe/tests/installer/metapackage_installer_test.rs b/crates/shirabe/tests/installer/metapackage_installer_test.rs
index b87fdebd..8675c00e 100644
--- a/crates/shirabe/tests/installer/metapackage_installer_test.rs
+++ b/crates/shirabe/tests/installer/metapackage_installer_test.rs
@@ -8,7 +8,7 @@ use crate::test_case::get_package;
use shirabe::installer::{InstallerInterface, MetapackageInstaller};
use shirabe::io::IOInterface;
use shirabe::io::null_io::NullIO;
-use shirabe::repository::{InstalledArrayRepository, RepositoryInterface};
+use shirabe::repository::{InstalledArrayRepository, InstalledRepositoryInterfaceHandle};
fn installer() -> MetapackageInstaller {
let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> =
@@ -20,17 +20,13 @@ fn installer() -> MetapackageInstaller {
fn test_install() {
let package = get_package("test/pkg", "1.0.0");
let installer = installer();
- let mut repository = InstalledArrayRepository::new_with_packages(vec![]).unwrap();
+ let repository = InstalledRepositoryInterfaceHandle::new(
+ InstalledArrayRepository::new_with_packages(vec![]).unwrap(),
+ );
- run(installer.install(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface,
- ),
- package.clone(),
- ))
- .unwrap();
+ run(installer.install(&repository, package.clone())).unwrap();
- assert!(repository.has_package(package).unwrap());
+ assert!(repository.borrow_mut().has_package(package).unwrap());
}
#[test]
@@ -38,59 +34,41 @@ fn test_update() {
let initial = get_package("test/initial", "1.0.0");
let target = get_package("test/target", "1.0.1");
let installer = installer();
- let mut repository =
- InstalledArrayRepository::new_with_packages(vec![initial.clone()]).unwrap();
-
- run(installer.update(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface,
- ),
- initial.clone(),
- target.clone(),
- ))
- .unwrap();
+ let repository = InstalledRepositoryInterfaceHandle::new(
+ InstalledArrayRepository::new_with_packages(vec![initial.clone()]).unwrap(),
+ );
- assert!(!repository.has_package(initial.clone()).unwrap());
- assert!(repository.has_package(target.clone()).unwrap());
+ run(installer.update(&repository, initial.clone(), target.clone())).unwrap();
- // Updating again, with the initial package no longer installed, fails.
assert!(
- run(installer.update(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface
- ),
- initial,
- target
- ))
- .is_err()
+ !repository
+ .borrow_mut()
+ .has_package(initial.clone())
+ .unwrap()
);
+ assert!(repository.borrow_mut().has_package(target.clone()).unwrap());
+
+ // Updating again, with the initial package no longer installed, fails.
+ assert!(run(installer.update(&repository, initial, target)).is_err());
}
#[test]
fn test_uninstall() {
let package = get_package("test/pkg", "1.0.0");
let installer = installer();
- let mut repository =
- InstalledArrayRepository::new_with_packages(vec![package.clone()]).unwrap();
-
- run(installer.uninstall(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface,
- ),
- package.clone(),
- ))
- .unwrap();
+ let repository = InstalledRepositoryInterfaceHandle::new(
+ InstalledArrayRepository::new_with_packages(vec![package.clone()]).unwrap(),
+ );
- assert!(!repository.has_package(package.clone()).unwrap());
+ run(installer.uninstall(&repository, package.clone())).unwrap();
- // Uninstalling again, with the package no longer installed, fails.
assert!(
- run(installer.uninstall(
- &std::cell::RefCell::new(
- &mut repository as &mut dyn shirabe::repository::InstalledRepositoryInterface
- ),
- package
- ))
- .is_err()
+ !repository
+ .borrow_mut()
+ .has_package(package.clone())
+ .unwrap()
);
+
+ // Uninstalling again, with the package no longer installed, fails.
+ assert!(run(installer.uninstall(&repository, package)).is_err());
}
diff --git a/crates/shirabe/tests/plugin/main.rs b/crates/shirabe/tests/plugin/main.rs
index 5cb77de4..62c8cfa8 100644
--- a/crates/shirabe/tests/plugin/main.rs
+++ b/crates/shirabe/tests/plugin/main.rs
@@ -1,3 +1,5 @@
+#[path = "../common/async_runtime.rs"]
+mod async_runtime;
#[path = "../common/config_stub.rs"]
mod config_stub;
diff --git a/crates/shirabe/tests/plugin/plugin_installer_test.rs b/crates/shirabe/tests/plugin/plugin_installer_test.rs
index ca2dd879..e3f89851 100644
--- a/crates/shirabe/tests/plugin/plugin_installer_test.rs
+++ b/crates/shirabe/tests/plugin/plugin_installer_test.rs
@@ -1,28 +1,201 @@
//! ref: composer/tests/Composer/Test/Plugin/PluginInstallerTest.php
-use crate::config_stub::ConfigStubBuilder;
+use crate::async_runtime::run;
use indexmap::IndexMap;
+use shirabe::autoload::AutoloadGenerator;
use shirabe::composer::{Composer, ComposerHandle, PartialOrFullComposer};
use shirabe::config::Config;
+use shirabe::dependency_resolver::operation::AnyOperation;
+use shirabe::downloader::{DownloadManagerInterface, DownloaderInterface};
+use shirabe::event_dispatcher::EventDispatcher;
use shirabe::factory::DisablePlugins;
-use shirabe::installer::InstallationManager;
+use shirabe::installer::{
+ InstallationManager, InstallationManagerInterface, InstallerInterface, PluginInstaller,
+};
use shirabe::io::IOInterface;
use shirabe::io::buffer_io::BufferIO;
use shirabe::json::JsonFile;
-use shirabe::package::{Locker, LockerInterface};
+use shirabe::package::loader::{ArrayLoader, JsonLoader, JsonLoaderInput};
+use shirabe::package::{Locker, LockerInterface, PackageInterfaceHandle, RootPackageHandle};
use shirabe::plugin::plugin_interface::PluginInterface;
use shirabe::plugin::{Capable, PluginManager};
+use shirabe::repository::{
+ InstalledArrayRepository, InstalledRepositoryInterfaceHandle, RepositoryInterfaceHandle,
+ RepositoryManagerInterface,
+};
use shirabe::util::Platform;
use shirabe::util::http_downloader::HttpDownloader;
use shirabe::util::r#loop::Loop;
use shirabe::util::process_executor::ProcessExecutor;
use shirabe_external_packages::symfony::console::output::output_interface::VERBOSITY_NORMAL;
+use shirabe_external_packages::symfony::process::PhpExecutableFinder;
use shirabe_php_shim::PhpMixed;
+use tempfile::TempDir;
+
+/// The register/activate flow runs the plugin in the real PHP worker; without a PHP binary the
+/// worker cannot start. Tests exercising it return early, following the convention of the
+/// non-mock tests in `shirabe-php-rpc`.
+fn php_runtime_available() -> bool {
+ PhpExecutableFinder::new().find(false).is_some()
+}
+
+/// All tests in this binary share the single PHP worker, whose loaded-class table persists
+/// across tests just like PHPUnit's single-process runs (that sharing is what exercises the
+/// `_composer_tmp` rename path). Interleaving two tests would let one test's class definitions
+/// race the other's `class_exists` checks, so the worker-touching tests run serialized.
+static PHP_WORKER_TESTS: std::sync::Mutex<()> = std::sync::Mutex::new(());
+
+fn lock_php_worker() -> std::sync::MutexGuard<'static, ()> {
+ PHP_WORKER_TESTS
+ .lock()
+ .unwrap_or_else(|poisoned| poisoned.into_inner())
+}
+
+/// `__DIR__ . '/Fixtures'` of the upstream test class.
+fn fixtures_dir() -> String {
+ let dir = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
+ .join("../../composer/tests/Composer/Test/Plugin/Fixtures");
+ dir.canonicalize()
+ .expect("the Composer checkout must provide the plugin fixtures")
+ .to_str()
+ .unwrap()
+ .to_string()
+}
+
+// PHP mocks `Composer\Downloader\DownloadManager`; install/update/remove resolve to null and the
+// other methods are never reached by these tests.
+mockall::mock! {
+ #[derive(Debug)]
+ pub DownloadManager {}
+ #[async_trait::async_trait(?Send)]
+ impl DownloadManagerInterface for DownloadManager {
+ fn set_prefer_source(&mut self, prefer_source: bool);
+ fn set_prefer_dist(&mut self, prefer_dist: bool);
+ fn get_downloader_for_package(
+ &self,
+ package: PackageInterfaceHandle,
+ ) -> anyhow::Result<Option<std::rc::Rc<std::cell::RefCell<dyn DownloaderInterface>>>>;
+ async fn download(
+ &self,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn prepare(
+ &self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn install(
+ &self,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn update(
+ &self,
+ initial: PackageInterfaceHandle,
+ target: PackageInterfaceHandle,
+ target_dir: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn remove(
+ &self,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ async fn cleanup(
+ &self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ target_dir: &str,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>>;
+ }
+}
+
+/// PHP mocks `Composer\Repository\RepositoryManager` so that getLocalRepository returns the
+/// test repository; the other methods are never reached.
+#[derive(Debug)]
+struct MockRepositoryManager {
+ local: RepositoryInterfaceHandle,
+ repositories: Vec<RepositoryInterfaceHandle>,
+}
+
+impl RepositoryManagerInterface for MockRepositoryManager {
+ fn get_local_repository(&self) -> RepositoryInterfaceHandle {
+ self.local.clone()
+ }
+
+ fn get_repositories(&self) -> &Vec<RepositoryInterfaceHandle> {
+ &self.repositories
+ }
+
+ fn create_repository(
+ &self,
+ _type: &str,
+ _config: IndexMap<String, PhpMixed>,
+ _name: Option<&str>,
+ ) -> anyhow::Result<RepositoryInterfaceHandle> {
+ unimplemented!("not exercised by PluginInstallerTest")
+ }
+
+ fn add_repository(&mut self, _repository: RepositoryInterfaceHandle) {
+ unimplemented!("not exercised by PluginInstallerTest")
+ }
+
+ fn set_local_repository(&mut self, repository: RepositoryInterfaceHandle) {
+ self.local = repository;
+ }
+}
+
+/// PHP mocks `Composer\Installer\InstallationManager` so that getInstallPath maps a package to
+/// `__DIR__.'/Fixtures/'.$package->getPrettyName()`; every other method keeps the PHPUnit mock
+/// default (no-op / falsy).
+#[derive(Debug)]
+struct MockInstallationManager;
+
+impl InstallationManagerInterface for MockInstallationManager {
+ fn add_installer(&mut self, _installer: Box<dyn InstallerInterface>) {}
+
+ fn remove_installer(&mut self, _installer: &dyn InstallerInterface) {}
+
+ fn disable_plugins(&mut self) {}
+
+ fn is_package_installed(
+ &mut self,
+ _repo: &InstalledRepositoryInterfaceHandle,
+ _package: PackageInterfaceHandle,
+ ) -> anyhow::Result<bool> {
+ Ok(false)
+ }
+
+ fn ensure_binaries_presence(&mut self, _package: PackageInterfaceHandle) {}
+
+ fn execute(
+ &mut self,
+ _repo: &InstalledRepositoryInterfaceHandle,
+ _operations: Vec<AnyOperation>,
+ _dev_mode: bool,
+ _run_scripts: bool,
+ _download_only: bool,
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+
+ fn get_install_path(&self, package: PackageInterfaceHandle) -> Option<String> {
+ Some(format!("{}/{}", fixtures_dir(), package.get_pretty_name()))
+ }
+
+ fn set_output_progress(&mut self, _output_progress: bool) {}
+
+ fn notify_installs(&mut self, _io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>) {}
+}
/// Equivalent to PHP setUp()'s `new InstallationManager(...)`, used only to satisfy
-/// `Locker::new`'s constructor argument; it is never exercised by the currently-portable
-/// tests below.
-fn installation_manager(
+/// `Locker::new`'s concrete constructor argument (PHP hands the same InstallationManager mock to
+/// the Locker, which never touches it in these tests).
+fn locker_installation_manager(
io: &std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
) -> std::rc::Rc<std::cell::RefCell<InstallationManager>> {
let config = std::rc::Rc::new(std::cell::RefCell::new(Config::new(false, None)));
@@ -42,175 +215,411 @@ fn installation_manager(
#[derive(Debug)]
struct SetUp {
- #[allow(dead_code)]
- io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
+ io: std::rc::Rc<std::cell::RefCell<BufferIO>>,
+ io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
pm: std::rc::Rc<std::cell::RefCell<PluginManager>>,
+ autoload_generator: std::rc::Rc<std::cell::RefCell<AutoloadGenerator>>,
+ packages: Vec<PackageInterfaceHandle>,
+ repository: InstalledRepositoryInterfaceHandle,
// Keeps the Composer alive; PluginManager only holds a weak back-reference to it.
- #[allow(dead_code)]
composer: ComposerHandle,
+ // PHP's tearDown() removes this directory; TempDir does the same on drop.
+ _directory: TempDir,
}
-/// Builds a `Composer` the way PHP's setUp() does (config with `allow-plugins => true`
-/// and a `Locker` backed by /dev/null) and constructs a `PluginManager` from it.
-///
-/// PHP's setUp() additionally mocks DownloadManager/RepositoryManager/InstallationManager/
-/// EventDispatcher, loads 8 plugin-vN fixture packages, and creates a temp fixtures
-/// directory. None of that is reproduced here: every test that would exercise it depends on
-/// `PluginManager::register_package` actually instantiating a plugin class, which is an
-/// unported runtime concern (`TODO(plugin)` in `plugin/plugin_manager.rs`) — those tests stay
-/// `#[ignore]` below. Only the tests that call `PluginManager::get_plugin_capability` directly
-/// with a hand-built plugin object are portable, and they need nothing more than `pm` itself.
fn set_up() -> SetUp {
- let io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = std::rc::Rc::new(
- std::cell::RefCell::new(BufferIO::new(String::new(), VERBOSITY_NORMAL, None).unwrap()),
- );
+ let loader = JsonLoader::new(Box::new(ArrayLoader::new(None, false)));
+ let mut packages = vec![];
+ let directory = TempDir::new().unwrap();
+ let directory_path = directory.path().to_str().unwrap().to_string();
+ for i in 1..=8 {
+ std::fs::create_dir_all(format!("{}/Fixtures/plugin-v{}", directory_path, i)).unwrap();
+ packages.push(
+ loader
+ .load(JsonLoaderInput::String(format!(
+ "{}/plugin-v{}/composer.json",
+ fixtures_dir(),
+ i
+ )))
+ .unwrap(),
+ );
+ }
- let config = ConfigStubBuilder::new()
- .with("allow-plugins", PhpMixed::Bool(true))
- .build_shared();
+ let mut dm = MockDownloadManager::new();
+ dm.expect_install().returning(|_, _| Ok(None));
+ dm.expect_update().returning(|_, _, _| Ok(None));
+ dm.expect_remove().returning(|_, _| Ok(None));
- let json_file = JsonFile::new(Platform::get_dev_null(), None, Some(io.clone())).unwrap();
- let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some(
- io.clone(),
- ))));
- let locker: std::rc::Rc<std::cell::RefCell<dyn LockerInterface>> =
- std::rc::Rc::new(std::cell::RefCell::new(Locker::new(
- io.clone(),
- json_file,
- installation_manager(&io),
- "{}",
- process,
- )));
+ let repository =
+ InstalledRepositoryInterfaceHandle::new(InstalledArrayRepository::new().unwrap());
+
+ let repository_manager = MockRepositoryManager {
+ local: repository.as_repository_handle(),
+ repositories: vec![],
+ };
+
+ let installation_manager = MockInstallationManager;
+
+ let io = std::rc::Rc::new(std::cell::RefCell::new(
+ BufferIO::new(String::new(), VERBOSITY_NORMAL, None).unwrap(),
+ ));
+ let io_dyn: std::rc::Rc<std::cell::RefCell<dyn IOInterface>> = io.clone();
+
+ let composer_rc = std::rc::Rc::new(std::cell::RefCell::new(PartialOrFullComposer::Full(
+ Composer::new(),
+ )));
+ let composer = ComposerHandle::from_rc_unchecked(composer_rc.clone());
- let mut composer = Composer::new();
- composer.set_config(config);
- composer.set_locker(locker);
+ // PHP hands the AutoloadGenerator a mocked EventDispatcher (disabled constructor); the Rust
+ // AutoloadGenerator requires a concrete EventDispatcher, and none of these tests reach it.
+ let dispatcher = std::rc::Rc::new(std::cell::RefCell::new(EventDispatcher::new(
+ composer.upcast().downgrade(),
+ io_dyn.clone(),
+ None,
+ )));
+ let autoload_generator = std::rc::Rc::new(std::cell::RefCell::new(AutoloadGenerator::new(
+ dispatcher,
+ Some(io_dyn.clone()),
+ )));
- let composer = ComposerHandle::from_rc_unchecked(std::rc::Rc::new(std::cell::RefCell::new(
- PartialOrFullComposer::Full(composer),
+ let mut config = Config::new(false, None);
+ let mut config_section: IndexMap<String, PhpMixed> = IndexMap::new();
+ config_section.insert(
+ "vendor-dir".to_string(),
+ PhpMixed::String(format!("{}/Fixtures/", directory_path)),
+ );
+ config_section.insert(
+ "home".to_string(),
+ PhpMixed::String(format!("{}/Fixtures", directory_path)),
+ );
+ config_section.insert(
+ "bin-dir".to_string(),
+ PhpMixed::String(format!("{}/Fixtures/bin", directory_path)),
+ );
+ config_section.insert("allow-plugins".to_string(), PhpMixed::Bool(true));
+ let mut merged: IndexMap<String, PhpMixed> = IndexMap::new();
+ merged.insert("config".to_string(), PhpMixed::Array(config_section));
+ config.merge(&merged, Config::SOURCE_UNKNOWN);
+
+ {
+ let mut c = composer.borrow_mut();
+ c.set_config(std::rc::Rc::new(std::cell::RefCell::new(config)));
+ c.set_download_manager(std::rc::Rc::new(std::cell::RefCell::new(dm)));
+ c.set_repository_manager(std::rc::Rc::new(std::cell::RefCell::new(
+ repository_manager,
+ )));
+ c.set_installation_manager(std::rc::Rc::new(std::cell::RefCell::new(
+ installation_manager,
+ )));
+ c.set_autoload_generator(autoload_generator.clone());
+ }
+ let real_dispatcher = std::rc::Rc::new(std::cell::RefCell::new(EventDispatcher::new(
+ composer.upcast().downgrade(),
+ io_dyn.clone(),
+ None,
)));
+ composer.borrow_mut().set_event_dispatcher(real_dispatcher);
+ composer.borrow_mut().set_package(
+ RootPackageHandle::new(
+ "dummy/root".to_string(),
+ "1.0.0.0".to_string(),
+ "1.0.0".to_string(),
+ )
+ .into(),
+ );
+ {
+ let json_file =
+ JsonFile::new(Platform::get_dev_null(), None, Some(io_dyn.clone())).unwrap();
+ let process = std::rc::Rc::new(std::cell::RefCell::new(ProcessExecutor::new(Some(
+ io_dyn.clone(),
+ ))));
+ let locker: std::rc::Rc<std::cell::RefCell<dyn LockerInterface>> =
+ std::rc::Rc::new(std::cell::RefCell::new(Locker::new(
+ io_dyn.clone(),
+ json_file,
+ locker_installation_manager(&io_dyn),
+ "{}",
+ process,
+ )));
+ composer.borrow_mut().set_locker(locker);
+ }
- let pm = PluginManager::new(io.clone(), composer.downgrade(), None, DisablePlugins::None);
+ let pm = std::rc::Rc::new(std::cell::RefCell::new(PluginManager::new(
+ io_dyn.clone(),
+ composer.downgrade(),
+ None,
+ DisablePlugins::None,
+ )));
+ composer.borrow_mut().set_plugin_manager(pm.clone());
SetUp {
io,
- pm: std::rc::Rc::new(std::cell::RefCell::new(pm)),
+ io_dyn,
+ pm,
+ autoload_generator,
+ packages,
+ repository,
composer,
+ _directory: directory,
}
}
-/// PHP's tearDown() removes the temp fixtures directory created by setUp(); `set_up` above
-/// creates no such directory, so there is nothing to clean up.
-fn tear_down() {}
-
-struct TearDown;
-
-impl Drop for TearDown {
- fn drop(&mut self) {
- tear_down();
+/// PHPUnit asserts `$plugins[$i]->version` etc.; the plugin entity lives in the PHP child, so
+/// the property is read over RPC through the proxy's test helper.
+fn plugin_property(
+ plugin: &std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>,
+ name: &str,
+) -> String {
+ let plugin = plugin.borrow();
+ let proxy = plugin
+ .as_php_plugin_proxy()
+ .expect("registered plugins are PHP-backed proxies");
+ match proxy.__get_property(name).unwrap() {
+ PhpMixed::String(s) => s,
+ other => panic!("property `{name}` is not a string: {other:?}"),
}
}
-// The plugin system requires the PHP runtime to load and instantiate plugin classes.
-// `PluginInstaller::install`/`update` never call `PluginManager::register_package` (the calls
-// are commented out in installer/plugin_installer.rs, TODO(plugin)), and `register_package`
-// itself never instantiates a plugin class or calls `add_plugin` (TODO(plugin) in
-// plugin/plugin_manager.rs). So `PluginManager::get_plugins()` can never contain the plugin
-// instances these tests assert on.
-#[ignore = "PluginInstaller and runtime loading of fixture plugin PHP classes (plugin-v1) are not implemented (TODO(plugin))"]
+fn new_installer(set_up: &SetUp) -> PluginInstaller {
+ PluginInstaller::new(
+ set_up.io_dyn.clone(),
+ set_up.composer.upcast().downgrade(),
+ None,
+ None,
+ )
+}
+
#[test]
fn test_install_new_plugin() {
- // TODO(phase-d): PluginInstaller and runtime loading of fixture plugin PHP classes (plugin-v1)
- // are not implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: $this->repository->getPackages() returns [].
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.install(&set_up.repository, set_up.packages[0].clone())).unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!("installer-v1", plugin_property(&plugins[0], "version"));
+ assert_eq!("activate v1\n", set_up.io.borrow().get_output());
}
-#[ignore = "PluginInstaller and runtime loading of fixture plugin PHP classes are not implemented (TODO(plugin))"]
#[test]
fn test_install_plugin_with_root_package_having_files_autoload() {
- // TODO(phase-d): PluginInstaller and runtime loading of fixture plugin PHP classes are not
- // implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: $this->repository->getPackages() returns [].
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ set_up.autoload_generator.borrow_mut().set_dev_mode(true);
+ let files_autoload = format!("{}/files_autoload_which_should_not_run.php", fixtures_dir());
+ let mut autoload: IndexMap<String, PhpMixed> = IndexMap::new();
+ autoload.insert(
+ "files".to_string(),
+ PhpMixed::List(vec![PhpMixed::String(files_autoload)]),
+ );
+ let root = set_up.composer.borrow().get_package().clone();
+ root.set_autoload(autoload.clone());
+ root.set_dev_autoload(autoload);
+
+ run(installer.install(&set_up.repository, set_up.packages[0].clone())).unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!("activate v1\n", set_up.io.borrow().get_output());
+ assert_eq!("installer-v1", plugin_property(&plugins[0], "version"));
}
-#[ignore = "PluginInstaller and runtime loading of fixture plugin PHP classes (plugin-v4) are not implemented (TODO(plugin))"]
#[test]
fn test_install_multiple_plugins() {
- // TODO(phase-d): PluginInstaller and runtime loading of fixture plugin PHP classes (plugin-v4)
- // are not implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: $this->repository->getPackages() returns [$this->packages[3]].
+ set_up
+ .repository
+ .borrow_mut()
+ .add_package(set_up.packages[3].clone())
+ .unwrap();
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.install(&set_up.repository, set_up.packages[3].clone())).unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!("plugin1", plugin_property(&plugins[0], "name"));
+ assert_eq!("installer-v4", plugin_property(&plugins[0], "version"));
+ assert_eq!("plugin2", plugin_property(&plugins[1], "name"));
+ assert_eq!("installer-v4", plugin_property(&plugins[1], "version"));
+ assert_eq!(
+ "activate v4-plugin1\nactivate v4-plugin2\n",
+ set_up.io.borrow().get_output()
+ );
}
-#[ignore = "PluginInstaller.update and runtime plugin class loading/deactivation are not implemented (TODO(plugin))"]
#[test]
fn test_upgrade_with_new_class_name() {
- // TODO(phase-d): PluginInstaller.update and runtime plugin class loading/deactivation are not
- // implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: getPackages returns [$this->packages[0]]; hasPackage answers (true, false), which a
+ // real repository seeded with the initial package reproduces naturally.
+ set_up
+ .repository
+ .borrow_mut()
+ .add_package(set_up.packages[0].clone())
+ .unwrap();
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.update(
+ &set_up.repository,
+ set_up.packages[0].clone(),
+ set_up.packages[1].clone(),
+ ))
+ .unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ // PHP: assertCount(1, $plugins); $plugins[1]->version — unset() keeps array keys, so the
+ // remaining plugin sits at key 1. The Vec port reindexes; the remaining plugin is [0].
+ assert_eq!(1, plugins.len());
+ assert_eq!("installer-v2", plugin_property(&plugins[0], "version"));
+ assert_eq!(
+ "activate v1\ndeactivate v1\nactivate v2\n",
+ set_up.io.borrow().get_output()
+ );
}
-#[ignore = "PluginInstaller.uninstall and runtime plugin class loading/uninstall hook are not implemented (TODO(plugin))"]
#[test]
fn test_uninstall() {
- // TODO(phase-d): PluginInstaller.uninstall and runtime plugin class loading/uninstall hook are
- // not implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ set_up
+ .repository
+ .borrow_mut()
+ .add_package(set_up.packages[0].clone())
+ .unwrap();
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.uninstall(&set_up.repository, set_up.packages[0].clone())).unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!(0, plugins.len());
+ assert_eq!(
+ "activate v1\ndeactivate v1\nuninstall v1\n",
+ set_up.io.borrow().get_output()
+ );
}
-#[ignore = "PluginInstaller.update and runtime plugin class loading/deactivation are not implemented (TODO(plugin))"]
#[test]
fn test_upgrade_with_same_class_name() {
- // TODO(phase-d): PluginInstaller.update and runtime plugin class loading/deactivation are not
- // implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: getPackages returns [$this->packages[1]]; hasPackage answers (true, false).
+ set_up
+ .repository
+ .borrow_mut()
+ .add_package(set_up.packages[1].clone())
+ .unwrap();
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.update(
+ &set_up.repository,
+ set_up.packages[1].clone(),
+ set_up.packages[2].clone(),
+ ))
+ .unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!("installer-v3", plugin_property(&plugins[0], "version"));
+ assert_eq!(
+ "activate v2\ndeactivate v2\nactivate v3\n",
+ set_up.io.borrow().get_output()
+ );
}
-#[ignore = "PluginInstaller and runtime loading of fixture plugin PHP classes are not implemented (TODO(plugin))"]
#[test]
fn test_register_plugin_only_one_time() {
- // TODO(phase-d): PluginInstaller and runtime loading of fixture plugin PHP classes are not
- // implemented (TODO(plugin)).
- todo!()
+ if !php_runtime_available() {
+ return;
+ }
+ let _worker = lock_php_worker();
+ let set_up = set_up();
+ // PHP: $this->repository->getPackages() returns [].
+ let installer = new_installer(&set_up);
+ set_up.pm.borrow_mut().load_installed_plugins().unwrap();
+
+ run(installer.install(&set_up.repository, set_up.packages[0].clone())).unwrap();
+ run(installer.install(
+ &set_up.repository,
+ PackageInterfaceHandle::dup(&set_up.packages[0]),
+ ))
+ .unwrap();
+
+ let pm = set_up.pm.borrow();
+ let plugins = pm.get_plugins();
+ assert_eq!(1, plugins.len());
+ assert_eq!("installer-v1", plugin_property(&plugins[0], "version"));
+ assert_eq!("activate v1\n", set_up.io.borrow().get_output());
}
-// PluginManager::register_package's version-constraint check against composer-plugin-api is
-// fully ported and does gate loading correctly, but `getPluginApiVersion()` returns a hardcoded
-// constant (plugin_interface::PLUGIN_API_VERSION) with no seam to override it per-test the way
-// PHP's `getMockBuilder(PluginManager::class)->onlyMethods(['getPluginApiVersion'])` does, and
-// even a matching version can never produce a registered plugin (register_package's
-// instantiate-and-add_plugin step is an unported TODO(plugin) stub). Both blockers must be
-// resolved together; a partial port (e.g. only the count==0 branches) would drop assertions the
-// test relies on, which is disallowed.
-#[ignore = "Requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP classes; not implemented (TODO(plugin))"]
+// PluginManager::get_plugin_api_version returns a hardcoded constant
+// (plugin_interface::PLUGIN_API_VERSION) with no seam to override it per-test the way PHP's
+// `getMockBuilder(PluginManager::class)->onlyMethods(['getPluginApiVersion'])` does.
+#[ignore = "Requires mocking getPluginApiVersion; PluginManager has no such seam (TODO(plugin))"]
#[test]
fn test_star_plugin_version_works_with_any_api_version() {
- // TODO(phase-d): requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP
- // classes; not implemented (TODO(plugin)).
+ // TODO(phase-d): requires mocking getPluginApiVersion; PluginManager has no such seam
+ // (TODO(plugin)).
todo!()
}
-#[ignore = "Requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP classes; not implemented (TODO(plugin))"]
+#[ignore = "Requires mocking getPluginApiVersion; PluginManager has no such seam (TODO(plugin))"]
#[test]
fn test_plugin_constraint_works_only_with_certain_api_version() {
- // TODO(phase-d): requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP
- // classes; not implemented (TODO(plugin)).
+ // TODO(phase-d): requires mocking getPluginApiVersion; PluginManager has no such seam
+ // (TODO(plugin)).
todo!()
}
-#[ignore = "Requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP classes; not implemented (TODO(plugin))"]
+#[ignore = "Requires mocking getPluginApiVersion; PluginManager has no such seam (TODO(plugin))"]
#[test]
fn test_plugin_range_constraints_work_only_with_certain_api_version() {
- // TODO(phase-d): requires mocking getPluginApiVersion and runtime loading of fixture plugin PHP
- // classes; not implemented (TODO(plugin)).
+ // TODO(phase-d): requires mocking getPluginApiVersion; PluginManager has no such seam
+ // (TODO(plugin)).
todo!()
}
-#[ignore = "get_plugin_capabilities requires a registered plugin, which register_package never produces (TODO(plugin) in plugin/plugin_manager.rs); Capability::CommandProvider/BaseCommand runtime instantiation is also unported"]
+#[ignore = "get_plugin_capability never instantiates a capability class (TODO(plugin) in plugin/plugin_manager.rs); Capability::CommandProvider/BaseCommand runtime instantiation is unported"]
#[test]
fn test_command_provider_capability() {
- // TODO(phase-d): get_plugin_capabilities requires a registered plugin, which register_package
- // never produces (TODO(plugin) in plugin/plugin_manager.rs); Capability::CommandProvider/
- // BaseCommand runtime instantiation is also unported.
+ // TODO(phase-d): get_plugin_capability never instantiates a capability class (TODO(plugin)
+ // in plugin/plugin_manager.rs); Capability::CommandProvider/BaseCommand runtime
+ // instantiation is also unported.
todo!()
}
@@ -224,30 +633,36 @@ struct NoopPlugin;
impl PluginInterface for NoopPlugin {
fn activate(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
}
fn deactivate(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
}
fn uninstall(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+
+ fn get_class_name(&self) -> String {
+ "NoopPlugin".to_string()
}
}
#[test]
fn test_incapable_plugin_is_correctly_detected() {
let set_up = set_up();
- let _tear_down = TearDown;
let plugin = NoopPlugin;
let result = set_up
@@ -293,23 +708,30 @@ struct CapablePlugin {
impl PluginInterface for CapablePlugin {
fn activate(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
}
fn deactivate(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
}
fn uninstall(
&mut self,
- _composer: &ComposerHandle,
+ _composer: ComposerHandle,
_io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
- ) {
+ ) -> anyhow::Result<()> {
+ Ok(())
+ }
+
+ fn get_class_name(&self) -> String {
+ "CapablePlugin".to_string()
}
fn as_capable(&self) -> Option<&dyn Capable> {
@@ -327,7 +749,6 @@ impl Capable for CapablePlugin {
#[test]
fn test_querying_non_provided_capability_returns_null_safely() {
let set_up = set_up();
- let _tear_down = TearDown;
let plugin = CapablePlugin {
get_capabilities_calls: std::cell::RefCell::new(0),
diff --git a/crates/shirabe/tests/repository/filesystem_repository_test.rs b/crates/shirabe/tests/repository/filesystem_repository_test.rs
index c7b8f9a6..323aff9f 100644
--- a/crates/shirabe/tests/repository/filesystem_repository_test.rs
+++ b/crates/shirabe/tests/repository/filesystem_repository_test.rs
@@ -10,7 +10,6 @@ use shirabe::io::IOInterface;
use shirabe::json::json_file::JsonFile;
use shirabe::package::loader::ArrayLoader;
use shirabe::package::{Link, PackageInterfaceHandle, RootAliasPackageHandle, RootPackageHandle};
-use shirabe::repository::InstalledRepositoryInterface;
use shirabe::repository::RepositoryInterface;
use shirabe::repository::filesystem_repository::FilesystemRepository;
use shirabe::util::filesystem::Filesystem;
@@ -99,13 +98,13 @@ mockall::mock! {
fn disable_plugins(&mut self);
fn is_package_installed(
&mut self,
- repo: &mut dyn InstalledRepositoryInterface,
+ repo: &shirabe::repository::InstalledRepositoryInterfaceHandle,
package: PackageInterfaceHandle,
) -> anyhow::Result<bool>;
fn ensure_binaries_presence(&mut self, package: PackageInterfaceHandle);
fn execute(
&mut self,
- repo: &mut dyn InstalledRepositoryInterface,
+ repo: &shirabe::repository::InstalledRepositoryInterfaceHandle,
operations: Vec<AnyOperation>,
dev_mode: bool,
run_scripts: bool,