aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src')
-rw-r--r--crates/shirabe/src/command/base_command.rs4
-rw-r--r--crates/shirabe/src/console/application.rs75
-rw-r--r--crates/shirabe/src/factory.rs6
-rw-r--r--crates/shirabe/src/plugin/capability/command_provider.rs3
-rw-r--r--crates/shirabe/src/plugin/php_plugin_proxy.rs94
-rw-r--r--crates/shirabe/src/repository/vcs.rs3
-rw-r--r--crates/shirabe/src/repository/vcs_repository.rs2
7 files changed, 142 insertions, 45 deletions
diff --git a/crates/shirabe/src/command/base_command.rs b/crates/shirabe/src/command/base_command.rs
index 77850e9e..f9682de9 100644
--- a/crates/shirabe/src/command/base_command.rs
+++ b/crates/shirabe/src/command/base_command.rs
@@ -283,7 +283,7 @@ impl Command for BaseCommandData {
}
impl PhpClass for BaseCommandData {
- fn php_class_name(&self) -> &'static str {
+ fn php_class_name(&self) -> String {
// Forwards to the base state's panicking implementation; concrete commands supply
// their own class name through `impl_php_class!`.
self.inner.php_class_name()
@@ -852,7 +852,7 @@ pub fn base_command_initialize(
crate::factory::DisablePlugins::None
};
let composer = if composer.is_none() {
- Factory::create_global(io.clone(), disable_plugins_kind, disable_scripts)
+ Factory::create_global(io.clone(), disable_plugins_kind, disable_scripts)?
} else {
composer
};
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs
index e6cfad35..cf57a5bf 100644
--- a/crates/shirabe/src/console/application.rs
+++ b/crates/shirabe/src/console/application.rs
@@ -416,14 +416,10 @@ impl Application {
match Factory::create(io_for_factory, None, disable_plugins_enum, disable_scripts) {
Ok(c) => self.composer = Some(c.upcast()),
Err(e) => {
- if e.downcast_ref::<JsonValidationException>().is_some()
- || e.downcast_ref::<RuntimeException>().is_some()
+ if e.downcast_ref::<shirabe_php_shim::InvalidArgumentException>()
+ .is_some()
{
if required {
- return Err(e);
- }
- } else {
- if required {
self.io.write_error(&e.to_string());
if self.are_exceptions_caught() {
// PHP calls `exit(1)` here, terminating before parent::run() can
@@ -433,6 +429,19 @@ impl Application {
}
return Err(e);
}
+ } else if e.downcast_ref::<JsonValidationException>().is_some()
+ || e.downcast_ref::<RuntimeException>().is_some()
+ // PHP's `catch (RuntimeException)` also catches subclasses;
+ // NoSslException is the one Factory::create raises.
+ || e.downcast_ref::<NoSslException>().is_some()
+ {
+ if required {
+ return Err(e);
+ }
+ } else {
+ // Anything else (e.g. seld/jsonlint's ParsingException) propagates
+ // regardless of $required, feeding doRun's GithubActionError path.
+ return Err(e);
}
}
}
@@ -584,8 +593,53 @@ impl Application {
fn get_plugin_commands(
&mut self,
) -> anyhow::Result<Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> {
- // TODO(plugin): plugin command discovery is part of the plugin API
- Ok(vec![])
+ let mut commands: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> = vec![];
+
+ let mut composer = self.get_composer(false, Some(false), None)?;
+ if composer.is_none() {
+ let disable_plugins = if self.disable_plugins_by_default {
+ crate::factory::DisablePlugins::All
+ } else {
+ crate::factory::DisablePlugins::None
+ };
+ composer = Factory::create_global(
+ self.io.clone(),
+ disable_plugins,
+ self.disable_scripts_by_default,
+ )?;
+ }
+
+ if let Some(composer) = composer {
+ let composer = composer
+ .as_full()
+ .expect("Factory::create and Factory::create_global build a full Composer");
+ let pm = composer.borrow().get_plugin_manager();
+ let mut ctor_args: IndexMap<String, shirabe_php_rpc::PluginValue> = IndexMap::new();
+ ctor_args.insert(
+ "composer".to_string(),
+ crate::plugin::composer_handle_value(&composer),
+ );
+ ctor_args.insert("io".to_string(), crate::plugin::io_handle_value(&self.io)?);
+ let capabilities = pm.borrow().get_plugin_capabilities(
+ "Composer\\Plugin\\Capability\\CommandProvider",
+ ctor_args,
+ )?;
+ for capability in capabilities {
+ let provider = capability.as_command_provider().expect(
+ "get_plugin_capability builds a CommandProvider adapter for this capability class",
+ );
+ // The is_array / instanceof BaseCommand checks PHP performs on the raw
+ // getCommands value live in the adapter.
+ let new_commands = provider.get_commands()?;
+ commands.extend(
+ new_commands.into_iter().map(|command| {
+ command as std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>
+ }),
+ );
+ }
+ }
+
+ Ok(commands)
}
/// Get the working directory at startup time
@@ -2141,10 +2195,7 @@ impl ApplicationHandle {
for command in plugin_commands {
let cmd_name = command.borrow().get_name().unwrap_or_default();
if application.borrow_mut().has(&cmd_name) {
- // TODO(plugin): PHP uses get_class($command) for the skipped-command class
- // name. Plugin command discovery (get_plugin_commands) is unimplemented, so
- // this loop never runs; wire the concrete class name with the plugin API.
- let cls = String::new();
+ let cls = command.borrow().php_class_name();
io.write_error(&format!("<warning>Plugin command {} ({}) would override a Composer command and has been skipped</warning>", cmd_name, cls));
} else {
self.add(command)?;
diff --git a/crates/shirabe/src/factory.rs b/crates/shirabe/src/factory.rs
index 9a84117e..fe3b5289 100644
--- a/crates/shirabe/src/factory.rs
+++ b/crates/shirabe/src/factory.rs
@@ -903,11 +903,11 @@ impl Factory {
io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
disable_plugins: DisablePlugins,
disable_scripts: bool,
- ) -> Option<PartialComposerHandle> {
+ ) -> anyhow::Result<Option<PartialComposerHandle>> {
let factory = Self::default();
- let config = Self::create_config(Some(io.clone()), None).ok()?;
- factory.create_global_composer(io, &config, disable_plugins, disable_scripts, true)
+ let config = Self::create_config(Some(io.clone()), None)?;
+ Ok(factory.create_global_composer(io, &config, disable_plugins, disable_scripts, true))
}
fn add_local_repository(
diff --git a/crates/shirabe/src/plugin/capability/command_provider.rs b/crates/shirabe/src/plugin/capability/command_provider.rs
index 2453860a..9deb1bce 100644
--- a/crates/shirabe/src/plugin/capability/command_provider.rs
+++ b/crates/shirabe/src/plugin/capability/command_provider.rs
@@ -9,5 +9,6 @@ use crate::plugin::capability::Capability;
/// The sole implementor is the PHP capability proxy (Composer itself never implements a
/// capability), so the method is fallible: the answer crosses the RPC boundary.
pub trait CommandProvider: Capability {
- fn get_commands(&self) -> anyhow::Result<Vec<Box<dyn BaseCommand>>>;
+ fn get_commands(&self)
+ -> anyhow::Result<Vec<std::rc::Rc<std::cell::RefCell<dyn BaseCommand>>>>;
}
diff --git a/crates/shirabe/src/plugin/php_plugin_proxy.rs b/crates/shirabe/src/plugin/php_plugin_proxy.rs
index 9cccb1af..71182477 100644
--- a/crates/shirabe/src/plugin/php_plugin_proxy.rs
+++ b/crates/shirabe/src/plugin/php_plugin_proxy.rs
@@ -910,7 +910,9 @@ impl Capability for PhpCommandProviderProxy {
}
impl CommandProvider for PhpCommandProviderProxy {
- fn get_commands(&self) -> anyhow::Result<Vec<Box<dyn BaseCommand>>> {
+ fn get_commands(
+ &self,
+ ) -> anyhow::Result<Vec<std::rc::Rc<std::cell::RefCell<dyn BaseCommand>>>> {
let value = unwrap_php_result(call_php_method(
self.handle.phandle,
"getCommands",
@@ -936,7 +938,7 @@ impl CommandProvider for PhpCommandProviderProxy {
));
}
};
- let mut commands: Vec<Box<dyn BaseCommand>> = Vec::new();
+ let mut commands: Vec<std::rc::Rc<std::cell::RefCell<dyn BaseCommand>>> = Vec::new();
for item in items {
let command_handle = match item {
PluginValue::PhpHandle(handle) => handle,
@@ -945,7 +947,9 @@ impl CommandProvider for PhpCommandProviderProxy {
if !php_is_a(&command_handle, "Composer\\Command\\BaseCommand")? {
return Err(invalid_command_error(&self.handle));
}
- commands.push(Box::new(PhpCommandProxy::new(command_handle)?));
+ commands.push(std::rc::Rc::new(std::cell::RefCell::new(
+ PhpCommandProxy::new(command_handle)?,
+ )));
}
Ok(commands)
}
@@ -968,9 +972,10 @@ impl Drop for PhpCommandProviderProxy {
}
/// `BaseCommand` adapter for a command entity living in the PHP child process. The Rust-side
-/// command state mirrors the child's (the name is read back over RPC at construction, after
-/// the PHP constructor ran `configure()`); running the command needs the PHP-side Symfony
-/// Application and is an explicit error until that exists.
+/// command state mirrors the child's list metadata (name, description, aliases, hidden flag —
+/// read back over RPC at construction, after the PHP constructor ran `configure()`); running
+/// the command needs the PHP-side Symfony Application and is an explicit error until that
+/// exists.
#[derive(Debug)]
pub struct PhpCommandProxy {
base_command_data: crate::command::BaseCommandData,
@@ -980,32 +985,74 @@ pub struct PhpCommandProxy {
impl PhpCommandProxy {
pub(crate) fn new(handle: PhpObjHandle) -> anyhow::Result<Self> {
let data = crate::command::BaseCommandData::new(None);
- let name = unwrap_php_result(call_php_method(
- handle.phandle,
- "getName",
- Vec::new(),
- Some(&mut PluginRpcDispatcher::default()),
- ))?;
+ // TODO(plugin): the input definition (arguments/options) is not read back yet;
+ // `help` rendering and input parsing for this command need it.
+ let name = Self::call_metadata_getter(&handle, "getName")?;
match name {
PluginValue::Null => {}
PluginValue::String(bytes) => {
Command::set_name(&data, &String::from_utf8_lossy(&bytes))?;
}
- other => {
- return Err(anyhow::anyhow!(shirabe_php_shim::RuntimeException {
- message: format!(
- "{}::getName() returned an unsupported shape over RPC: {other:?}",
- handle.class
- ),
- code: 0,
- }));
+ other => return Err(Self::unsupported_shape(&handle, "getName", &other)),
+ }
+ let description = Self::call_metadata_getter(&handle, "getDescription")?;
+ match description {
+ PluginValue::String(bytes) => {
+ Command::set_description(&data, &String::from_utf8_lossy(&bytes));
+ }
+ other => return Err(Self::unsupported_shape(&handle, "getDescription", &other)),
+ }
+ let aliases = Self::call_metadata_getter(&handle, "getAliases")?;
+ let alias_items = match aliases {
+ PluginValue::List(items) => items,
+ PluginValue::Array(map) => map.into_values().collect(),
+ other => return Err(Self::unsupported_shape(&handle, "getAliases", &other)),
+ };
+ let mut alias_names = Vec::new();
+ for alias in alias_items {
+ match alias {
+ PluginValue::String(bytes) => {
+ alias_names.push(String::from_utf8_lossy(&bytes).into_owned());
+ }
+ other => return Err(Self::unsupported_shape(&handle, "getAliases", &other)),
}
}
+ Command::set_aliases(&data, alias_names)?;
+ let hidden = Self::call_metadata_getter(&handle, "isHidden")?;
+ match hidden {
+ PluginValue::Bool(hidden) => {
+ Command::set_hidden(&data, hidden);
+ }
+ other => return Err(Self::unsupported_shape(&handle, "isHidden", &other)),
+ }
Ok(Self {
base_command_data: data,
handle,
})
}
+
+ fn call_metadata_getter(handle: &PhpObjHandle, method: &str) -> anyhow::Result<PluginValue> {
+ unwrap_php_result(call_php_method(
+ handle.phandle,
+ method,
+ Vec::new(),
+ Some(&mut PluginRpcDispatcher::default()),
+ ))
+ }
+
+ fn unsupported_shape(
+ handle: &PhpObjHandle,
+ method: &str,
+ value: &PluginValue,
+ ) -> anyhow::Error {
+ anyhow::anyhow!(shirabe_php_shim::RuntimeException {
+ message: format!(
+ "{}::{method}() returned an unsupported shape over RPC: {value:?}",
+ handle.class
+ ),
+ code: 0,
+ })
+ }
}
impl Command for PhpCommandProxy {
@@ -1037,11 +1084,8 @@ impl BaseCommand for PhpCommandProxy {
}
impl shirabe_php_shim::PhpClass for PhpCommandProxy {
- fn php_class_name(&self) -> &'static str {
- // TODO(plugin): PhpClass reports only &'static str, but this command's PHP class name
- // is runtime data (`self.handle.class`); callers needing get_class() must read the
- // handle instead.
- panic!("PhpCommandProxy has no static PHP class name; read the handle's class instead")
+ fn php_class_name(&self) -> String {
+ self.handle.class.clone()
}
}
diff --git a/crates/shirabe/src/repository/vcs.rs b/crates/shirabe/src/repository/vcs.rs
index 920fefa3..cb20735c 100644
--- a/crates/shirabe/src/repository/vcs.rs
+++ b/crates/shirabe/src/repository/vcs.rs
@@ -140,7 +140,7 @@ impl VcsDriverKind {
impl PhpClass for VcsDriverKind {
/// Used as the fallback driver name in `getRepoName()`.
- fn php_class_name(&self) -> &'static str {
+ fn php_class_name(&self) -> String {
match self {
VcsDriverKind::GitHub => r"Composer\Repository\Vcs\GitHubDriver",
VcsDriverKind::GitLab => r"Composer\Repository\Vcs\GitLabDriver",
@@ -152,5 +152,6 @@ impl PhpClass for VcsDriverKind {
VcsDriverKind::Fossil => r"Composer\Repository\Vcs\FossilDriver",
VcsDriverKind::Svn => r"Composer\Repository\Vcs\SvnDriver",
}
+ .to_string()
}
}
diff --git a/crates/shirabe/src/repository/vcs_repository.rs b/crates/shirabe/src/repository/vcs_repository.rs
index dd7b4b06..7ce00ce5 100644
--- a/crates/shirabe/src/repository/vcs_repository.rs
+++ b/crates/shirabe/src/repository/vcs_repository.rs
@@ -178,7 +178,7 @@ impl VcsRepository {
.iter()
.find(|(_, v)| **v == kind)
.map(|(name, _)| name.clone())
- .unwrap_or_else(|| kind.php_class_name().to_string()),
+ .unwrap_or_else(|| kind.php_class_name()),
None => String::new(),
};