aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src')
-rw-r--r--crates/shirabe/src/command/create_project_command.rs2
-rw-r--r--crates/shirabe/src/factory.rs9
-rw-r--r--crates/shirabe/src/installer/installation_manager.rs76
-rw-r--r--crates/shirabe/src/installer/installer_interface.rs2
-rw-r--r--crates/shirabe/src/installer/library_installer.rs6
-rw-r--r--crates/shirabe/src/installer/metapackage_installer.rs4
-rw-r--r--crates/shirabe/src/installer/noop_installer.rs4
-rw-r--r--crates/shirabe/src/installer/plugin_installer.rs4
-rw-r--r--crates/shirabe/src/installer/project_installer.rs4
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs609
-rw-r--r--crates/shirabe/src/plugin/plugin_manager.rs78
11 files changed, 710 insertions, 88 deletions
diff --git a/crates/shirabe/src/command/create_project_command.rs b/crates/shirabe/src/command/create_project_command.rs
index 1e289b08..756ebfac 100644
--- a/crates/shirabe/src/command/create_project_command.rs
+++ b/crates/shirabe/src/command/create_project_command.rs
@@ -1011,7 +1011,7 @@ impl CreateProjectCommand {
{
let mut im = installation_manager.borrow_mut();
im.set_output_progress(!no_progress);
- im.add_installer(Box::new(project_installer));
+ im.add_installer(std::rc::Rc::new(project_installer));
}
let installed_repo = crate::repository::InstalledRepositoryInterfaceHandle::new(
InstalledArrayRepository::new()?,
diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs
index fe3b5289..a1fda63b 100644
--- a/crates/shirabe/src/factory.rs
+++ b/crates/shirabe/src/factory.rs
@@ -1291,7 +1291,7 @@ impl Factory {
)));
im.borrow_mut()
- .add_installer(Box::new(crate::installer::LibraryInstaller::new(
+ .add_installer(std::rc::Rc::new(crate::installer::LibraryInstaller::new(
io.clone(),
composer.clone(),
None,
@@ -1299,14 +1299,15 @@ impl Factory {
Some(binary_installer.clone()),
)));
im.borrow_mut()
- .add_installer(Box::new(crate::installer::PluginInstaller::new(
+ .add_installer(std::rc::Rc::new(crate::installer::PluginInstaller::new(
io.clone(),
composer,
Some(fs),
Some(binary_installer),
)));
- im.borrow_mut()
- .add_installer(Box::new(crate::installer::MetapackageInstaller::new(io)));
+ im.borrow_mut().add_installer(std::rc::Rc::new(
+ crate::installer::MetapackageInstaller::new(io),
+ ));
}
fn purge_packages(
diff --git a/crates/shirabe/src/installer/installation_manager.rs b/crates/shirabe/src/installer/installation_manager.rs
index 31bfaa2c..439026b0 100644
--- a/crates/shirabe/src/installer/installation_manager.rs
+++ b/crates/shirabe/src/installer/installation_manager.rs
@@ -33,12 +33,12 @@ pub struct InstallationManager {
/// Rc rather than Box so `get_installer` can hand out shareable handles: the download/cleanup
/// futures collected for Loop::wait must own their installer beyond the loop iteration that
/// created them (PHP closures capture $installer the same way).
- installers: Vec<std::rc::Rc<dyn InstallerInterface>>,
- /// Maps a package type to the index of its installer in `installers`. PHP caches the installer
- /// instance itself; here we store an index instead. The index never dangles because both
- /// `add_installer` and `remove_installer` clear the cache whenever `installers` changes.
+ /// RefCell so a plugin activated from inside `execute` — which holds a shared borrow of this
+ /// manager for the whole run — can still register its own installer.
+ installers: std::cell::RefCell<Vec<std::rc::Rc<dyn InstallerInterface>>>,
+ /// Maps a package type to its installer.
/// RefCell so lookups can populate the cache through `&self` from concurrent operation chains.
- cache: std::cell::RefCell<IndexMap<String, usize>>,
+ cache: std::cell::RefCell<IndexMap<String, std::rc::Rc<dyn InstallerInterface>>>,
/// RefCell so mark_for_notification works through `&self` from concurrent operation chains.
notifiable_packages: std::cell::RefCell<IndexMap<String, Vec<PackageInterfaceHandle>>>,
loop_: std::rc::Rc<std::cell::RefCell<Loop>>,
@@ -69,7 +69,7 @@ impl InstallationManager {
event_dispatcher: Option<std::rc::Rc<std::cell::RefCell<EventDispatcher>>>,
) -> Self {
Self {
- installers: vec![],
+ installers: std::cell::RefCell::new(vec![]),
cache: std::cell::RefCell::new(IndexMap::new()),
notifiable_packages: std::cell::RefCell::new(IndexMap::new()),
loop_,
@@ -133,30 +133,25 @@ impl InstallationManager {
}
/// Adds installer
- pub fn add_installer(&mut self, installer: Box<dyn InstallerInterface>) {
- array_unshift(&mut self.installers, std::rc::Rc::from(installer));
- self.cache = std::cell::RefCell::new(IndexMap::new());
- }
-
- /// For testing only: adds an installer as a pre-built shared handle, so the caller keeps an
- /// identity handle usable for PHP `assertSame`-style comparisons (`Rc::ptr_eq`) and for
- /// `remove_installer`. `add_installer` cannot serve because `Rc::from(Box)` reallocates,
- /// losing the caller's pointer identity.
- pub fn __add_installer(&mut self, installer: std::rc::Rc<dyn InstallerInterface>) {
- array_unshift(&mut self.installers, installer);
- self.cache = std::cell::RefCell::new(IndexMap::new());
+ ///
+ /// The installer is taken as a shared handle: PHP hands over an object reference and both
+ /// sides keep the same identity afterwards, which `removeInstaller` and the plugin
+ /// manager's `registeredPlugins` bookkeeping compare against.
+ pub fn add_installer(&self, installer: std::rc::Rc<dyn InstallerInterface>) {
+ array_unshift(&mut self.installers.borrow_mut(), installer);
+ self.cache.borrow_mut().clear();
}
/// Removes installer
- pub fn remove_installer(&mut self, installer: &dyn InstallerInterface) {
+ pub fn remove_installer(&self, installer: &dyn InstallerInterface) {
let target = installer as *const dyn InstallerInterface as *const ();
- let key = self
- .installers
+ let mut installers = self.installers.borrow_mut();
+ let key = installers
.iter()
.position(|inst| &**inst as *const dyn InstallerInterface as *const () == target);
if let Some(k) = key {
- array_splice(&mut self.installers, k as i64, Some(1), vec![]);
- self.cache = std::cell::RefCell::new(IndexMap::new());
+ array_splice(&mut installers, k as i64, Some(1), vec![]);
+ self.cache.borrow_mut().clear();
}
}
@@ -166,7 +161,9 @@ impl InstallationManager {
/// disabling the PluginManager. This ensures that no third-party
/// code is ever executed.
pub fn disable_plugins(&mut self) {
- for installer in self.installers.iter() {
+ // Cloned out: `disablePlugins` reaches into the plugin manager, which may reach back.
+ let installers = self.installers.borrow().clone();
+ for installer in installers.iter() {
if let Some(plugin_installer) = installer.as_plugin_installer() {
plugin_installer.disable_plugins();
}
@@ -180,17 +177,18 @@ impl InstallationManager {
) -> anyhow::Result<std::rc::Rc<dyn InstallerInterface>> {
let r#type = strtolower(r#type);
- if let Some(&index) = self.cache.borrow().get(&r#type) {
- return Ok(self.installers[index].clone());
+ if let Some(installer) = self.cache.borrow().get(&r#type) {
+ return Ok(installer.clone());
}
- let index = self
- .installers
- .iter()
- .position(|installer| installer.supports(&r#type));
- if let Some(index) = index {
- self.cache.borrow_mut().insert(r#type, index);
- return Ok(self.installers[index].clone());
+ // Cloned out: a PHP-backed installer answers `supports` over RPC, and the plugin behind
+ // it can register a further installer from that call.
+ let installers = self.installers.borrow().clone();
+ for installer in installers {
+ if installer.supports(&r#type)? {
+ self.cache.borrow_mut().insert(r#type, installer.clone());
+ return Ok(installer);
+ }
}
Err(InvalidArgumentException {
@@ -1020,8 +1018,8 @@ pub trait InstallationManagerInterface: std::fmt::Debug {
unimplemented!("as_any is only implemented for the concrete InstallationManager")
}
- fn add_installer(&mut self, installer: Box<dyn InstallerInterface>);
- fn remove_installer(&mut self, installer: &dyn InstallerInterface);
+ fn add_installer(&self, installer: std::rc::Rc<dyn InstallerInterface>);
+ fn remove_installer(&self, installer: &dyn InstallerInterface);
fn disable_plugins(&mut self);
fn is_package_installed(
&mut self,
@@ -1047,12 +1045,12 @@ impl InstallationManagerInterface for InstallationManager {
self
}
- fn add_installer(&mut self, installer: Box<dyn InstallerInterface>) {
- self.add_installer(installer);
+ fn add_installer(&self, installer: std::rc::Rc<dyn InstallerInterface>) {
+ InstallationManager::add_installer(self, installer);
}
- fn remove_installer(&mut self, installer: &dyn InstallerInterface) {
- self.remove_installer(installer);
+ fn remove_installer(&self, installer: &dyn InstallerInterface) {
+ InstallationManager::remove_installer(self, installer);
}
fn disable_plugins(&mut self) {
diff --git a/crates/shirabe/src/installer/installer_interface.rs b/crates/shirabe/src/installer/installer_interface.rs
index bc464735..fc273916 100644
--- a/crates/shirabe/src/installer/installer_interface.rs
+++ b/crates/shirabe/src/installer/installer_interface.rs
@@ -8,7 +8,7 @@ use shirabe_php_shim::PhpMixed;
#[async_trait::async_trait(?Send)]
pub trait InstallerInterface: std::fmt::Debug {
- fn supports(&self, package_type: &str) -> bool;
+ fn supports(&self, package_type: &str) -> anyhow::Result<bool>;
fn is_installed(
&self,
diff --git a/crates/shirabe/src/installer/library_installer.rs b/crates/shirabe/src/installer/library_installer.rs
index c5a9608e..479fc6d5 100644
--- a/crates/shirabe/src/installer/library_installer.rs
+++ b/crates/shirabe/src/installer/library_installer.rs
@@ -232,11 +232,11 @@ impl LibraryInstaller {
#[async_trait::async_trait(?Send)]
impl InstallerInterface for LibraryInstaller {
- fn supports(&self, package_type: &str) -> bool {
- match &self.r#type {
+ fn supports(&self, package_type: &str) -> anyhow::Result<bool> {
+ Ok(match &self.r#type {
Some(t) => package_type == t,
None => true,
- }
+ })
}
fn is_installed(
diff --git a/crates/shirabe/src/installer/metapackage_installer.rs b/crates/shirabe/src/installer/metapackage_installer.rs
index e2821fd3..52c152ea 100644
--- a/crates/shirabe/src/installer/metapackage_installer.rs
+++ b/crates/shirabe/src/installer/metapackage_installer.rs
@@ -24,8 +24,8 @@ impl MetapackageInstaller {
#[async_trait::async_trait(?Send)]
impl InstallerInterface for MetapackageInstaller {
- fn supports(&self, package_type: &str) -> bool {
- package_type == "metapackage"
+ fn supports(&self, package_type: &str) -> anyhow::Result<bool> {
+ Ok(package_type == "metapackage")
}
fn is_installed(
diff --git a/crates/shirabe/src/installer/noop_installer.rs b/crates/shirabe/src/installer/noop_installer.rs
index 68a2b981..95f31f2e 100644
--- a/crates/shirabe/src/installer/noop_installer.rs
+++ b/crates/shirabe/src/installer/noop_installer.rs
@@ -10,8 +10,8 @@ pub struct NoopInstaller;
#[async_trait::async_trait(?Send)]
impl InstallerInterface for NoopInstaller {
- fn supports(&self, _package_type: &str) -> bool {
- true
+ fn supports(&self, _package_type: &str) -> anyhow::Result<bool> {
+ Ok(true)
}
fn is_installed(
diff --git a/crates/shirabe/src/installer/plugin_installer.rs b/crates/shirabe/src/installer/plugin_installer.rs
index 1e7a2636..7683a7d5 100644
--- a/crates/shirabe/src/installer/plugin_installer.rs
+++ b/crates/shirabe/src/installer/plugin_installer.rs
@@ -73,8 +73,8 @@ impl PluginInstaller {
#[async_trait::async_trait(?Send)]
impl InstallerInterface for PluginInstaller {
- fn supports(&self, package_type: &str) -> bool {
- package_type == "composer-plugin" || package_type == "composer-installer"
+ fn supports(&self, package_type: &str) -> anyhow::Result<bool> {
+ Ok(package_type == "composer-plugin" || package_type == "composer-installer")
}
fn is_installed(
diff --git a/crates/shirabe/src/installer/project_installer.rs b/crates/shirabe/src/installer/project_installer.rs
index 6fee682f..59d44410 100644
--- a/crates/shirabe/src/installer/project_installer.rs
+++ b/crates/shirabe/src/installer/project_installer.rs
@@ -31,8 +31,8 @@ impl ProjectInstaller {
#[async_trait::async_trait(?Send)]
impl InstallerInterface for ProjectInstaller {
- fn supports(&self, _package_type: &str) -> bool {
- true
+ fn supports(&self, _package_type: &str) -> anyhow::Result<bool> {
+ Ok(true)
}
fn is_installed(
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index d13019e3..701e83cc 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -20,8 +20,8 @@ use crate::plugin::capability::{Capability, CommandProvider};
use crate::plugin::capable::Capable;
use crate::plugin::plugin_interface::PluginInterface;
use crate::repository::{
- InstalledArrayRepository, InstalledFilesystemRepository, RepositoryInterfaceHandle,
- RepositoryManagerInterface,
+ InstalledArrayRepository, InstalledFilesystemRepository, InstalledRepositoryInterfaceHandle,
+ RepositoryInterfaceHandle, RepositoryManagerInterface,
};
use indexmap::IndexMap;
use shirabe_external_packages::symfony::console::command::command::Command;
@@ -292,6 +292,12 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
// The entity is cloned out so no table borrow is held while the handler runs (a
// handler that re-enters register_*_entity would otherwise panic on the RefCell).
let entity = R_TABLE.with(|table| table.borrow().get(&rhandle).cloned());
+ if method_name == "__shirabeClone" {
+ return match entity {
+ Some(entity) => clone_entity(&entity),
+ None => Err(runtime_throw(format!("unknown Rust handle {rhandle}"))),
+ };
+ }
match entity {
Some(RustEntity::Io(io)) => dispatch_io_method(&io, method_name, &args),
Some(RustEntity::Composer(composer)) => {
@@ -317,6 +323,36 @@ impl RustMethodDispatcher for PluginRpcDispatcher<'_> {
}
}
+/// Serves the `__clone` forwarder every proxy stub carries. Only entities whose Rust type
+/// models the PHP clone answer; the rest are an explicit error, so a plugin cloning a live
+/// service never silently ends up with two stubs over one entity.
+fn clone_entity(entity: &RustEntity) -> Result<PluginValue, PhpThrow> {
+ let rhandle = match entity {
+ // `AnyPackage::dup` carries `BasePackage::__clone` (repository reset, id = -1) and the
+ // `RootAliasPackage::__clone` override.
+ RustEntity::Package(package) => {
+ let cloned = package.borrow().dup();
+ register_entity(RustEntity::Package(std::rc::Rc::new(
+ std::cell::RefCell::new(cloned),
+ )))
+ }
+ RustEntity::Composer(_)
+ | RustEntity::Io(_)
+ | RustEntity::InstallationManager(_)
+ | RustEntity::RepositoryManager(_)
+ | RustEntity::Repository(_)
+ | RustEntity::EventDispatcher(_) => {
+ return Err(runtime_throw(
+ "cloning this Rust-side entity over RPC is not supported".to_string(),
+ ));
+ }
+ };
+ Ok(PluginValue::List(vec![
+ PluginValue::Int(rhandle as i64),
+ PluginValue::Int(0),
+ ]))
+}
+
fn dispatch_composer_method(
composer: &ComposerHandle,
method_name: &str,
@@ -437,22 +473,259 @@ fn dispatch_repository_method(
}
}
+/// A PHP string-or-null wire value.
+fn optional_string(value: Option<String>) -> PluginValue {
+ match value {
+ Some(value) => PluginValue::string(value),
+ None => PluginValue::Null,
+ }
+}
+
+fn string_list(values: Vec<String>) -> PluginValue {
+ PluginValue::List(values.into_iter().map(PluginValue::string).collect())
+}
+
+/// `?list<array{url: string, preferred: bool}>` as PHP shapes it.
+fn mirror_list(mirrors: Option<Vec<crate::package::Mirror>>) -> PluginValue {
+ match mirrors {
+ None => PluginValue::Null,
+ Some(mirrors) => PluginValue::List(
+ mirrors
+ .into_iter()
+ .map(|mirror| {
+ PluginValue::Array(IndexMap::from([
+ (b"url".to_vec(), PluginValue::string(mirror.url)),
+ (b"preferred".to_vec(), PluginValue::Bool(mirror.preferred)),
+ ]))
+ })
+ .collect(),
+ ),
+ }
+}
+
+/// An `array<string, mixed>` as PHP shapes it: empty maps cross as a list, since an empty PHP
+/// array is indistinguishable from an empty list on the wire.
+fn string_keyed_map(map: IndexMap<String, PhpMixed>) -> PluginValue {
+ if map.is_empty() {
+ PluginValue::List(Vec::new())
+ } else {
+ PluginValue::from_php_mixed(&PhpMixed::Array(map))
+ }
+}
+
+/// The inverse of `mirror_list`.
+fn decode_mirrors(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<Option<Vec<crate::package::Mirror>>, PhpThrow> {
+ let rows = match value {
+ None | Some(PluginValue::Null) => return Ok(None),
+ Some(PluginValue::List(rows)) => rows.clone(),
+ Some(PluginValue::Array(rows)) => rows.values().cloned().collect(),
+ other => {
+ return Err(runtime_throw(format!(
+ "{method} expects a list of mirrors or null, got {other:?}"
+ )));
+ }
+ };
+ let mut mirrors = Vec::with_capacity(rows.len());
+ for row in rows {
+ let row = match row {
+ PluginValue::Array(row) => row,
+ other => {
+ return Err(runtime_throw(format!(
+ "{method} expects mirror maps, got {other:?}"
+ )));
+ }
+ };
+ let url = match row.get(b"url".as_slice()) {
+ Some(PluginValue::String(url)) => String::from_utf8_lossy(url).into_owned(),
+ other => {
+ return Err(runtime_throw(format!(
+ "{method} expects a string `url` in every mirror, got {other:?}"
+ )));
+ }
+ };
+ let preferred = matches!(
+ row.get(b"preferred".as_slice()),
+ Some(PluginValue::Bool(true))
+ );
+ mirrors.push(crate::package::Mirror { url, preferred });
+ }
+ Ok(Some(mirrors))
+}
+
+fn decode_optional_string(
+ method: &str,
+ value: Option<&PluginValue>,
+) -> Result<Option<String>, PhpThrow> {
+ match value {
+ None | Some(PluginValue::Null) => Ok(None),
+ Some(PluginValue::String(bytes)) => Ok(Some(String::from_utf8_lossy(bytes).into_owned())),
+ other => Err(runtime_throw(format!(
+ "{method} expects a string or null, got {other:?}"
+ ))),
+ }
+}
+
fn dispatch_package_method(
package: &std::rc::Rc<std::cell::RefCell<AnyPackage>>,
method_name: &str,
args: &[PluginValue],
) -> Result<PluginValue, PhpThrow> {
- let package = package.borrow();
- let package = package.as_package_interface();
+ // The link getters return `array<string, Link>`; only the empty case has a wire image so
+ // far (an empty PHP array crosses as a list).
+ //
+ // TODO(plugin): Link is a rust-snapshot value whose constraint field must materialize as a
+ // real composer/semver object in the child; the snapshot encoding does not exist yet.
+ let links = |links: IndexMap<String, crate::package::Link>| -> Result<PluginValue, PhpThrow> {
+ if links.is_empty() {
+ Ok(PluginValue::List(Vec::new()))
+ } else {
+ Err(runtime_throw(format!(
+ "the package method `{method_name}` returns Link values, whose encoding over RPC is not implemented yet"
+ )))
+ }
+ };
+
+ // Mutators borrow mutably and must not hold the borrow across the shared-borrow arms.
+ match method_name {
+ "setId" => {
+ let id = match args.first() {
+ Some(PluginValue::Int(id)) => *id,
+ other => {
+ return Err(runtime_throw(format!(
+ "setId expects an int, got {other:?}"
+ )));
+ }
+ };
+ package.borrow_mut().as_package_interface_mut().set_id(id);
+ return Ok(PluginValue::Null);
+ }
+ "setInstallationSource"
+ | "setSourceReference"
+ | "setSourceUrl"
+ | "setDistUrl"
+ | "setDistType"
+ | "setDistReference"
+ | "setSourceDistReferences" => {
+ let value = decode_optional_string(method_name, args.first())?;
+ let mut borrowed = package.borrow_mut();
+ let package = borrowed.as_package_interface_mut();
+ match method_name {
+ "setInstallationSource" => package.set_installation_source(value),
+ "setSourceReference" => package.set_source_reference(value),
+ "setSourceUrl" => package.set_source_url(value),
+ "setDistUrl" => package.set_dist_url(value),
+ "setDistType" => package.set_dist_type(value),
+ "setDistReference" => package.set_dist_reference(value),
+ _ => package.set_source_dist_references(value.ok_or_else(|| {
+ runtime_throw("setSourceDistReferences expects a reference string".to_string())
+ })?),
+ }
+ return Ok(PluginValue::Null);
+ }
+ "setSourceMirrors" | "setDistMirrors" => {
+ let mirrors = decode_mirrors(method_name, args.first())?;
+ let mut borrowed = package.borrow_mut();
+ let package = borrowed.as_package_interface_mut();
+ if method_name == "setSourceMirrors" {
+ package.set_source_mirrors(mirrors);
+ } else {
+ package.set_dist_mirrors(mirrors);
+ }
+ return Ok(PluginValue::Null);
+ }
+ "setRepository" => {
+ let repository = match args.first() {
+ Some(PluginValue::RustHandle(handle)) => {
+ match R_TABLE.with(|table| table.borrow().get(&handle.rhandle).cloned()) {
+ Some(RustEntity::Repository(repository)) => repository,
+ _ => {
+ return Err(runtime_throw(format!(
+ "setRepository expects a repository handle, got Rust handle {}",
+ handle.rhandle
+ )));
+ }
+ }
+ }
+ other => {
+ return Err(runtime_throw(format!(
+ "setRepository expects a repository argument, got {other:?}"
+ )));
+ }
+ };
+ package
+ .borrow_mut()
+ .as_package_interface_mut()
+ .set_repository(repository)
+ .map_err(|error| runtime_throw(format!("setRepository failed: {error}")))?;
+ return Ok(PluginValue::Null);
+ }
+ "setTransportOptions" => {
+ let options = match args.first() {
+ Some(value) => match value.to_php_mixed().map_err(|error| {
+ runtime_throw(format!(
+ "setTransportOptions could not decode its argument: {error:#}"
+ ))
+ })? {
+ PhpMixed::Array(options) => options,
+ PhpMixed::List(items) if items.is_empty() => IndexMap::new(),
+ other => {
+ return Err(runtime_throw(format!(
+ "setTransportOptions expects an array, got {other:?}"
+ )));
+ }
+ },
+ None => IndexMap::new(),
+ };
+ package
+ .borrow_mut()
+ .as_package_interface_mut()
+ .set_transport_options(options);
+ return Ok(PluginValue::Null);
+ }
+ _ => {}
+ }
+
+ let borrowed = package.borrow();
+ let package = borrowed.as_package_interface();
match method_name {
"getName" => Ok(PluginValue::string(package.get_name().to_string())),
+ "getPrettyName" => Ok(PluginValue::string(package.get_pretty_name().to_string())),
+ "getNames" => {
+ let provides = match args.first() {
+ None => true,
+ Some(PluginValue::Bool(provides)) => *provides,
+ other => {
+ return Err(runtime_throw(format!(
+ "getNames expects a bool provides flag, got {other:?}"
+ )));
+ }
+ };
+ Ok(string_list(package.get_names(provides)))
+ }
+ "getId" => Ok(PluginValue::Int(package.get_id())),
+ "isDev" => Ok(PluginValue::Bool(package.is_dev())),
"getType" => Ok(PluginValue::string(package.get_type())),
+ "getTargetDir" => Ok(optional_string(package.get_target_dir())),
+ "getExtra" => Ok(string_keyed_map(package.get_extra())),
+ "getInstallationSource" => Ok(optional_string(package.get_installation_source())),
+ "getSourceType" => Ok(optional_string(package.get_source_type())),
+ "getSourceUrl" => Ok(optional_string(package.get_source_url())),
+ "getSourceUrls" => Ok(string_list(package.get_source_urls())),
+ "getSourceReference" => Ok(optional_string(package.get_source_reference())),
+ "getSourceMirrors" => Ok(mirror_list(package.get_source_mirrors())),
+ "getDistType" => Ok(optional_string(package.get_dist_type())),
+ "getDistUrl" => Ok(optional_string(package.get_dist_url())),
+ "getDistUrls" => Ok(string_list(package.get_dist_urls())),
+ "getDistReference" => Ok(optional_string(package.get_dist_reference())),
+ "getDistSha1Checksum" => Ok(optional_string(package.get_dist_sha1_checksum())),
+ "getDistMirrors" => Ok(mirror_list(package.get_dist_mirrors())),
+ "getVersion" => Ok(PluginValue::string(package.get_version().to_string())),
"getPrettyVersion" => Ok(PluginValue::string(
package.get_pretty_version().to_string(),
)),
- "getExtra" => Ok(PluginValue::from_php_mixed(&PhpMixed::Array(
- package.get_extra(),
- ))),
"getFullPrettyVersion" => {
let truncate = match args.first() {
None => true,
@@ -477,22 +750,56 @@ fn dispatch_package_method(
package.get_full_pretty_version(truncate, display_mode),
))
}
- "getRequires" => {
- let requires = package.get_requires();
- if requires.is_empty() {
- // An empty PHP array crosses the wire as a list.
+ "getStability" => Ok(PluginValue::string(package.get_stability().to_string())),
+ "getRequires" => links(package.get_requires()),
+ "getConflicts" => links(package.get_conflicts()),
+ "getProvides" => links(package.get_provides()),
+ "getReplaces" => links(package.get_replaces()),
+ "getDevRequires" => links(package.get_dev_requires()),
+ "getSuggests" => {
+ let suggests = package.get_suggests();
+ if suggests.is_empty() {
Ok(PluginValue::List(Vec::new()))
} else {
- // TODO(plugin): Link is a rust-snapshot value whose constraint field must
- // materialize as a real composer/semver object in the child; the snapshot
- // encoding does not exist yet.
- Err(runtime_throw(
- "encoding Link values over RPC is not implemented yet".to_string(),
+ Ok(PluginValue::Array(
+ suggests
+ .into_iter()
+ .map(|(name, description)| {
+ (name.into_bytes(), PluginValue::string(description))
+ })
+ .collect(),
))
}
}
- // TODO(plugin): the remaining PackageInterface surface (setters included) is widened
- // on demand, driven by explicit errors from real plugins.
+ "getAutoload" => Ok(string_keyed_map(package.get_autoload())),
+ "getDevAutoload" => Ok(string_keyed_map(package.get_dev_autoload())),
+ "getIncludePaths" => Ok(string_list(package.get_include_paths())),
+ "getPhpExt" => Ok(match package.get_php_ext() {
+ Some(config) => string_keyed_map(config),
+ None => PluginValue::Null,
+ }),
+ "getRepository" => match package.get_repository() {
+ Some(repository) => repository_handle_value(&repository),
+ None => Ok(PluginValue::Null),
+ },
+ "getBinaries" => Ok(string_list(package.get_binaries())),
+ "getUniqueName" => Ok(PluginValue::string(package.get_unique_name())),
+ "getNotificationUrl" => Ok(optional_string(package.get_notification_url())),
+ "__toString" => Ok(PluginValue::string(package.get_unique_name())),
+ "getPrettyString" => Ok(PluginValue::string(package.get_pretty_string())),
+ "isDefaultBranch" => Ok(PluginValue::Bool(package.is_default_branch())),
+ "getTransportOptions" => Ok(string_keyed_map(package.get_transport_options())),
+ "getReleaseDate" => match package.get_release_date() {
+ None => Ok(PluginValue::Null),
+ // TODO(plugin): a \DateTimeInterface has to materialize as a real PHP object in the
+ // child, which needs a snapshot encoding for value objects.
+ Some(_) => Err(runtime_throw(
+ "encoding the release date over RPC is not implemented yet".to_string(),
+ )),
+ },
+ // TODO(plugin): the concrete-class surface below PackageInterface (`Package`'s setters,
+ // `CompletePackage`'s metadata, `RootPackage`'s root-only state) is widened on demand,
+ // driven by explicit errors from real plugins.
other => Err(runtime_throw(format!(
"the package method `{other}` is not available over RPC yet"
))),
@@ -532,6 +839,35 @@ fn dispatch_installation_manager_method(
None => PluginValue::Null,
})
}
+ "addInstaller" | "removeInstaller" => {
+ let handle = match args.first() {
+ Some(PluginValue::PhpHandle(handle)) => handle.clone(),
+ other => {
+ return Err(runtime_throw(format!(
+ "{method_name} expects an installer object, got {other:?}"
+ )));
+ }
+ };
+ if !php_is_a(&handle, "Composer\\Installer\\InstallerInterface").map_err(|error| {
+ runtime_throw(format!(
+ "{method_name} could not type-check its argument: {error:#}"
+ ))
+ })? {
+ return Err(runtime_throw(format!(
+ "{method_name} expects a Composer\\Installer\\InstallerInterface, got {}",
+ handle.class
+ )));
+ }
+ let phandle = handle.phandle;
+ let installer = php_installer_proxy(handle);
+ if method_name == "addInstaller" {
+ im.borrow().add_installer(installer);
+ } else {
+ im.borrow().remove_installer(&*installer);
+ forget_php_installer_proxy(phandle);
+ }
+ Ok(PluginValue::Null)
+ }
// TODO(plugin): the remaining InstallationManager surface is widened on demand,
// driven by explicit errors from real plugins.
other => Err(runtime_throw(format!(
@@ -942,6 +1278,243 @@ pub(crate) fn php_is_a(handle: &PhpObjHandle, class: &str) -> anyhow::Result<boo
Ok(matches!(value, PluginValue::Bool(true)))
}
+/// Wire value handing a Rust-side repository to the child, interned in the R table.
+pub(crate) fn repository_handle_value(
+ repository: &RepositoryInterfaceHandle,
+) -> Result<PluginValue, PhpThrow> {
+ let class = repository_stub_class(repository)?;
+ let rhandle = register_entity(RustEntity::Repository(repository.clone()));
+ Ok(rust_handle_value(rhandle, class))
+}
+
+/// `InstallerInterface` adapter for an installer entity living in the PHP child process: the
+/// installer a plugin hands to `InstallationManager::addInstaller`, or the class a legacy
+/// `composer-installer` package names. Every call is forwarded as a `CallPhpMethod` RPC.
+#[derive(Debug)]
+pub struct PhpInstallerProxy {
+ pub(crate) handle: PhpObjHandle,
+}
+
+impl PhpInstallerProxy {
+ pub(crate) fn new(handle: PhpObjHandle) -> Self {
+ Self { handle }
+ }
+
+ fn call(&self, method: &str, args: Vec<PluginValue>) -> anyhow::Result<PluginValue> {
+ unwrap_php_result(call_php_method(
+ self.handle.phandle,
+ method,
+ args,
+ Some(&mut PluginRpcDispatcher::default()),
+ ))
+ }
+
+ fn package_arg(package: &PackageInterfaceHandle) -> anyhow::Result<PluginValue> {
+ Ok(package_handle_value(package.as_rc())?)
+ }
+
+ fn optional_package_arg(
+ package: &Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<PluginValue> {
+ match package {
+ Some(package) => Self::package_arg(package),
+ None => Ok(PluginValue::Null),
+ }
+ }
+
+ fn repo_arg(repo: &InstalledRepositoryInterfaceHandle) -> anyhow::Result<PluginValue> {
+ Ok(repository_handle_value(&repo.as_repository_handle())?)
+ }
+
+ /// The `?PromiseInterface` half of the installer contract. A plugin installer that returns
+ /// a real promise needs the promise machinery the RPC boundary does not carry yet, so it is
+ /// an explicit error rather than a silently dropped continuation.
+ fn promise_result(&self, method: &str, value: PluginValue) -> anyhow::Result<Option<PhpMixed>> {
+ match value {
+ PluginValue::Null => Ok(None),
+ other => Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: format!(
+ "{}::{method}() returned a promise, which cannot cross the RPC boundary yet: {other:?}",
+ self.handle.class
+ ),
+ code: 0,
+ })),
+ }
+ }
+
+ fn unsupported_shape(&self, method: &str, value: &PluginValue) -> anyhow::Error {
+ anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: format!(
+ "{}::{method}() returned an unsupported shape over RPC: {value:?}",
+ self.handle.class
+ ),
+ code: 0,
+ })
+ }
+}
+
+#[async_trait::async_trait(?Send)]
+impl crate::installer::InstallerInterface for PhpInstallerProxy {
+ fn supports(&self, package_type: &str) -> anyhow::Result<bool> {
+ match self.call("supports", vec![PluginValue::string(package_type)])? {
+ PluginValue::Bool(supports) => Ok(supports),
+ other => Err(self.unsupported_shape("supports", &other)),
+ }
+ }
+
+ fn is_installed(
+ &self,
+ repo: &InstalledRepositoryInterfaceHandle,
+ package: PackageInterfaceHandle,
+ ) -> anyhow::Result<bool> {
+ let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)?];
+ match self.call("isInstalled", args)? {
+ PluginValue::Bool(installed) => Ok(installed),
+ other => Err(self.unsupported_shape("isInstalled", &other)),
+ }
+ }
+
+ async fn download(
+ &self,
+ package: PackageInterfaceHandle,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>> {
+ let args = vec![
+ Self::package_arg(&package)?,
+ Self::optional_package_arg(&prev_package)?,
+ ];
+ let value = self.call("download", args)?;
+ self.promise_result("download", value)
+ }
+
+ async fn prepare(
+ &self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>> {
+ let args = vec![
+ PluginValue::string(r#type),
+ Self::package_arg(&package)?,
+ Self::optional_package_arg(&prev_package)?,
+ ];
+ let value = self.call("prepare", args)?;
+ self.promise_result("prepare", value)
+ }
+
+ async fn install(
+ &self,
+ repo: &InstalledRepositoryInterfaceHandle,
+ package: PackageInterfaceHandle,
+ ) -> anyhow::Result<Option<PhpMixed>> {
+ let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)?];
+ let value = self.call("install", args)?;
+ self.promise_result("install", value)
+ }
+
+ async fn update(
+ &self,
+ repo: &InstalledRepositoryInterfaceHandle,
+ initial: PackageInterfaceHandle,
+ target: PackageInterfaceHandle,
+ ) -> anyhow::Result<Option<PhpMixed>> {
+ let args = vec![
+ Self::repo_arg(repo)?,
+ Self::package_arg(&initial)?,
+ Self::package_arg(&target)?,
+ ];
+ let value = self.call("update", args)?;
+ self.promise_result("update", value)
+ }
+
+ async fn uninstall(
+ &self,
+ repo: &InstalledRepositoryInterfaceHandle,
+ package: PackageInterfaceHandle,
+ ) -> anyhow::Result<Option<PhpMixed>> {
+ let args = vec![Self::repo_arg(repo)?, Self::package_arg(&package)?];
+ let value = self.call("uninstall", args)?;
+ self.promise_result("uninstall", value)
+ }
+
+ async fn cleanup(
+ &self,
+ r#type: &str,
+ package: PackageInterfaceHandle,
+ prev_package: Option<PackageInterfaceHandle>,
+ ) -> anyhow::Result<Option<PhpMixed>> {
+ let args = vec![
+ PluginValue::string(r#type),
+ Self::package_arg(&package)?,
+ Self::optional_package_arg(&prev_package)?,
+ ];
+ let value = self.call("cleanup", args)?;
+ self.promise_result("cleanup", value)
+ }
+
+ fn get_install_path(&self, package: PackageInterfaceHandle) -> Option<String> {
+ // PHP declares `getInstallPath(): string`; a failure here is a plugin error the
+ // infallible signature cannot carry, so it aborts rather than answering a path that
+ // would silently install the package in the wrong place.
+ let args = vec![Self::package_arg(&package).unwrap_or_else(|error| {
+ panic!(
+ "{}::getInstallPath argument failed: {error:#}",
+ self.handle.class
+ )
+ })];
+ let value = self.call("getInstallPath", args).unwrap_or_else(|error| {
+ panic!(
+ "{}::getInstallPath failed over RPC: {error:#}",
+ self.handle.class
+ )
+ });
+ match value {
+ PluginValue::Null => None,
+ PluginValue::String(path) => Some(String::from_utf8_lossy(&path).into_owned()),
+ other => panic!("{}", self.unsupported_shape("getInstallPath", &other)),
+ }
+ }
+}
+
+impl Drop for PhpInstallerProxy {
+ fn drop(&mut self) {
+ let _ = release_php_handle(self.handle.phandle);
+ }
+}
+
+thread_local! {
+ /// The installer adapters handed to `InstallationManager::addInstaller` over RPC, keyed by
+ /// the entity's phandle. `removeInstaller` arrives carrying the same entity, and the
+ /// manager compares installers by identity, so the adapter it was given has to be found
+ /// again rather than rebuilt.
+ static PHP_INSTALLER_PROXIES: std::cell::RefCell<IndexMap<u64, std::rc::Rc<dyn crate::installer::InstallerInterface>>> =
+ std::cell::RefCell::new(IndexMap::new());
+}
+
+/// The adapter for an installer entity, building it on first sight.
+pub(crate) fn php_installer_proxy(
+ handle: PhpObjHandle,
+) -> std::rc::Rc<dyn crate::installer::InstallerInterface> {
+ PHP_INSTALLER_PROXIES.with(|proxies| {
+ let mut proxies = proxies.borrow_mut();
+ if let Some(existing) = proxies.get(&handle.phandle) {
+ return existing.clone();
+ }
+ let phandle = handle.phandle;
+ let proxy: std::rc::Rc<dyn crate::installer::InstallerInterface> =
+ std::rc::Rc::new(PhpInstallerProxy::new(handle));
+ proxies.insert(phandle, proxy.clone());
+ proxy
+ })
+}
+
+/// Drops the adapter bookkeeping for an installer entity that left the manager.
+fn forget_php_installer_proxy(phandle: u64) {
+ PHP_INSTALLER_PROXIES.with(|proxies| {
+ proxies.borrow_mut().shift_remove(&phandle);
+ });
+}
+
/// `Capability` adapter for a capability entity living in the PHP child process, for
/// capability interfaces that add no methods of their own (the plain
/// `Composer\Plugin\Capability\Capability` marker).
diff --git a/crates/shirabe/src/plugin/plugin_manager.rs b/crates/shirabe/src/plugin/plugin_manager.rs
index 9e52c01f..8f005079 100644
--- a/crates/shirabe/src/plugin/plugin_manager.rs
+++ b/crates/shirabe/src/plugin/plugin_manager.rs
@@ -47,12 +47,13 @@ pub struct PluginManager {
allow_plugin_rules: Option<IndexMap<String, bool>>,
allow_global_plugin_rules: Option<IndexMap<String, bool>>,
running_in_global_dir: bool,
+ plugin_api_version_override: Option<String>,
}
#[derive(Debug)]
pub enum PluginOrInstaller {
Plugin(std::rc::Rc<std::cell::RefCell<dyn PluginInterface>>),
- Installer(Box<dyn InstallerInterface>),
+ Installer(std::rc::Rc<dyn InstallerInterface>),
}
/// PHP `private static $classCounter = 0;`.
@@ -101,9 +102,17 @@ impl PluginManager {
allow_plugin_rules,
allow_global_plugin_rules,
running_in_global_dir: false,
+ plugin_api_version_override: None,
}
}
+ /// For testing only: makes `get_plugin_api_version` report `version` instead of the
+ /// compiled-in constant, the seam PHPUnit obtains from
+ /// `getMockBuilder(PluginManager::class)->onlyMethods(['getPluginApiVersion'])`.
+ pub fn __set_plugin_api_version(&mut self, version: &str) {
+ self.plugin_api_version_override = Some(version.to_string());
+ }
+
pub fn set_running_in_global_dir(&mut self, running_in_global_dir: bool) {
self.running_in_global_dir = running_in_global_dir;
}
@@ -460,16 +469,46 @@ impl PluginManager {
}
if old_installer_plugin {
- // TODO(plugin): legacy composer-installer plugins need the InstallerInterface
- // reverse adapter, which does not exist yet; explicit error until then.
- return Err(RuntimeException {
- message: format!(
- "Shirabe cannot load \"{}\": legacy composer-installer plugins are not supported yet",
- package.get_name()
- ),
- code: 0,
+ if !self.php_runtime_is_a(&class, "Composer\\Installer\\InstallerInterface")? {
+ return Err(RuntimeException {
+ message: format!(
+ "Could not activate plugin \"{}\" as \"{}\" does not implement Composer\\Installer\\InstallerInterface",
+ package.get_name(),
+ class
+ ),
+ code: 0,
+ }
+ .into());
}
- .into());
+ self.io.write_error(&format!(
+ "<warning>Loading \"{}\" {}which is a legacy composer-installer built for Composer 1.x, it is likely to cause issues as you are running Composer 2.x.</warning>",
+ package.get_name(),
+ if is_global_plugin || self.running_in_global_dir {
+ "(installed globally) "
+ } else {
+ ""
+ }
+ ));
+ let composer = self.composer_full();
+ let handle = self.php_runtime_new_object_with_args(
+ &class,
+ vec![
+ crate::plugin::io_handle_value(&self.io)?,
+ crate::plugin::composer_handle_value(&composer),
+ ],
+ )?;
+ let installer = crate::plugin::php_installer_proxy(handle);
+ // A shared borrow: this runs inside `InstallationManager::execute`, which holds
+ // one of its own for the whole run.
+ composer
+ .borrow()
+ .get_installation_manager()
+ .borrow()
+ .add_installer(installer.clone());
+ self.registered_plugins
+ .entry(package.get_name().to_string())
+ .or_default()
+ .push(PluginOrInstaller::Installer(installer));
} else if self.php_runtime_class_exists(&class, true)? {
if !self.php_runtime_is_a(&class, "Composer\\Plugin\\PluginInterface")? {
return Err(RuntimeException {
@@ -569,9 +608,17 @@ impl PluginManager {
/// PHP `new $class()` in the worker, returning the P-table handle of the new entity.
fn php_runtime_new_object(&self, class: &str) -> anyhow::Result<shirabe_php_rpc::PhpObjHandle> {
+ self.php_runtime_new_object_with_args(class, vec![])
+ }
+
+ fn php_runtime_new_object_with_args(
+ &self,
+ class: &str,
+ args: Vec<PluginValue>,
+ ) -> anyhow::Result<shirabe_php_rpc::PhpObjHandle> {
let value = unwrap_php_result(shirabe_php_rpc::new_object(
class,
- vec![],
+ args,
Some(&mut PluginRpcDispatcher::default()),
))?;
match value {
@@ -610,7 +657,7 @@ impl PluginManager {
if let PluginOrInstaller::Installer(inst) =
&self.registered_plugins.get(&name).unwrap()[index]
{
- installation_manager.borrow_mut().remove_installer(&**inst);
+ installation_manager.borrow().remove_installer(&**inst);
}
}
}
@@ -648,7 +695,7 @@ impl PluginManager {
if let PluginOrInstaller::Installer(inst) =
&self.registered_plugins.get(&name).unwrap()[index]
{
- installation_manager.borrow_mut().remove_installer(&**inst);
+ installation_manager.borrow().remove_installer(&**inst);
}
}
}
@@ -659,7 +706,10 @@ impl PluginManager {
/// Returns the version of the internal composer-plugin-api package.
pub(crate) fn get_plugin_api_version(&self) -> String {
- plugin_interface::PLUGIN_API_VERSION.to_string()
+ match &self.plugin_api_version_override {
+ Some(version) => version.clone(),
+ None => plugin_interface::PLUGIN_API_VERSION.to_string(),
+ }
}
/// Adds a plugin, activates it and registers it with the event dispatcher