aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe/src/console
diff options
context:
space:
mode:
Diffstat (limited to 'crates/shirabe/src/console')
-rw-r--r--crates/shirabe/src/console/application.rs2278
1 files changed, 1108 insertions, 1170 deletions
diff --git a/crates/shirabe/src/console/application.rs b/crates/shirabe/src/console/application.rs
index d0e82ae..5dd2eeb 100644
--- a/crates/shirabe/src/console/application.rs
+++ b/crates/shirabe/src/console/application.rs
@@ -132,10 +132,9 @@ pub struct Application {
terminal: Terminal,
default_command: String,
single_command: bool,
- initialized: bool,
signal_registry: Option<SignalRegistry>,
signals_to_dispatch_event: Vec<i64>,
-
+ // $initialized is omitted. See ApplicationHandle::init().
pub(crate) composer: Option<PartialComposerHandle>,
pub(crate) io: std::rc::Rc<std::cell::RefCell<dyn IOInterface>>,
has_plugin_commands: bool,
@@ -150,7 +149,13 @@ pub struct Application {
}
impl Application {
- const LOGO: &'static str = " ______\n / ____/___ ____ ___ ____ ____ ________ _____\n / / / __ \\/ __ `__ \\/ __ \\/ __ \\/ ___/ _ \\/ ___/\n/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ /\n\\____/\\____/_/ /_/ /_/ .___/\\____/____/\\___/_/\n /_/\n";
+ const LOGO: &'static str = r#" ______
+ / ____/___ ____ ___ ____ ____ ________ _____
+ / / / __ \/ __ `__ \/ __ \/ __ \/ ___/ _ \/ ___/
+/ /___/ /_/ / / / / / / /_/ / /_/ (__ ) __/ /
+\____/\____/_/ /_/ /_/ .___/\____/____/\___/_/
+ /_/
+"#;
pub fn new(name: String, mut version: String) -> Self {
static SHUTDOWN_REGISTERED: std::sync::OnceLock<()> = std::sync::OnceLock::new();
@@ -205,7 +210,6 @@ impl Application {
terminal: Terminal::new(),
default_command: "list".to_string(),
single_command: false,
- initialized: false,
signal_registry: None,
signals_to_dispatch_event: Vec::new(),
composer: None,
@@ -228,783 +232,14 @@ impl Application {
this
}
- /// Builds the application inside a shared `Rc<RefCell<…>>` and registers the default commands.
- ///
- /// Commands hold a back-pointer to their application, which shares this very `RefCell`. So the
- /// whole run flow is driven through the `Rc` (never a long-lived `borrow_mut`), and command
- /// registration happens here — before any borrow is taken — so `set_application` can borrow the
- /// application without a re-entrant conflict.
- pub fn new_shared(
- name: String,
- version: String,
- ) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<Application>>> {
- let application =
- std::rc::Rc::new(std::cell::RefCell::new(Application::new(name, version)));
- application.borrow_mut().me = std::rc::Rc::downgrade(&application);
- Application::init_shared(&application)?;
- Ok(application)
- }
-
- /// Returns the shared handle to this application set up by `new_shared`. Proxy commands use it to
- /// re-enter the run flow (PHP's `$this->getApplication()->run(...)`).
- pub fn shared(&self) -> std::rc::Rc<std::cell::RefCell<Application>> {
- self.me
- .upgrade()
- .expect("Application must be constructed through new_shared")
- }
-
- /// Registers the default commands on a shared application (PHP's lazy `init()`), executed once
- /// with no application borrow held so each `set_application` can borrow back safely.
- fn init_shared(
- application: &std::rc::Rc<std::cell::RefCell<Application>>,
- ) -> anyhow::Result<()> {
- {
- let mut app = application.borrow_mut();
- if app.initialized {
- return Ok(());
- }
- app.initialized = true;
- }
-
- let commands = application.borrow().get_default_commands();
- for command in commands {
- Application::add_shared(application, command)?;
- }
-
- Ok(())
- }
-
- /// Adds a command object to a shared application (PHP's `add()`), without holding an application
- /// borrow while calling into the command, so `set_application` can borrow back safely.
- pub fn add_shared(
- application: &std::rc::Rc<std::cell::RefCell<Application>>,
- command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>,
- ) -> anyhow::Result<Option<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> {
- command.borrow_mut().set_application(Some(
- application.clone() as std::rc::Rc<std::cell::RefCell<dyn BaseApplication>>
- ));
-
- if !command.borrow().is_enabled() {
- command.borrow_mut().set_application(None);
-
- return Ok(None);
- }
-
- // LazyCommand is intentionally not ported.
- command.borrow().get_definition();
-
- if command.borrow().get_name().is_none() {
- return Err(ConsoleLogicException(shirabe_php_shim::LogicException {
- message: format!(
- "The command defined in \"{}\" cannot have an empty name.",
- PhpMixed::from(shirabe_php_shim::get_debug_type_obj(&command)),
- ),
- code: 0,
- })
- .into());
- }
-
- let name = command.borrow().get_name().unwrap();
- application
- .borrow_mut()
- .commands
- .insert(name, command.clone());
-
- for alias in command.borrow().get_aliases() {
- application
- .borrow_mut()
- .commands
- .insert(alias, command.clone());
- }
-
- Ok(Some(command))
- }
-
- pub fn run(
- application: &std::rc::Rc<std::cell::RefCell<Application>>,
- input: Option<std::rc::Rc<std::cell::RefCell<dyn InputInterface>>>,
- output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
- ) -> anyhow::Result<i32> {
- let output = match output {
- Some(output) => Some(output),
- None => Some(
- std::rc::Rc::new(std::cell::RefCell::new(Factory::create_output()))
- as std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ),
- };
-
- Application::base_run(application, input, output)
- }
-
- pub fn do_run(
- application: &std::rc::Rc<std::cell::RefCell<Application>>,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i32> {
- application.borrow_mut().disable_plugins_by_default = input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["--no-plugins"]), false);
- application.borrow_mut().disable_scripts_by_default = input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["--no-scripts"]), false);
-
- let stdin = shirabe_php_shim::STDIN;
- if Platform::get_env("COMPOSER_TESTS_ARE_RUNNING").as_deref() != Some("1")
- && (Platform::get_env("COMPOSER_NO_INTERACTION").is_some()
- || !Platform::is_tty(Some(stdin)))
- {
- input.borrow_mut().set_interactive(false);
- }
-
- let mut helpers: IndexMap<
- HelperSetKey,
- std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>,
- > = IndexMap::new();
- helpers.insert(
- HelperSetKey::Int(0),
- std::rc::Rc::new(std::cell::RefCell::new(QuestionHelper::default())),
- );
- let helper_set = std::rc::Rc::new(std::cell::RefCell::new(HelperSet::default()));
- HelperSet::new(&helper_set, helpers);
- application.borrow_mut().io = std::rc::Rc::new(std::cell::RefCell::new(ConsoleIO::new(
- input.clone(),
- output.clone(),
- helper_set.borrow().clone(),
- )));
- // Cache the IO so the rest of the flow does not re-borrow the application; command callbacks
- // (e.g. mergeApplicationDefinition) need the application's RefCell free.
- let io = application.borrow().io.clone();
-
- // Register error handler again to pass it the IO instance
- ErrorHandler::register(Some(io.clone()));
-
- if input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["--no-cache"]), false)
- {
- io.write_error3("Disabling cache usage", true, io_interface::DEBUG);
- Platform::put_env(
- "COMPOSER_CACHE_DIR",
- if Platform::is_windows() {
- "nul"
- } else {
- "/dev/null"
- },
- );
- }
-
- // switch working dir
- let new_work_dir = application.borrow().get_new_working_dir(input.clone())?;
- let mut old_working_dir: Option<String> = None;
- if let Some(ref nwd) = new_work_dir {
- old_working_dir = Some(Platform::get_cwd(true).unwrap_or_default());
- chdir(nwd);
- application.borrow_mut().initial_working_directory = getcwd();
- let cwd = Platform::get_cwd(true).unwrap_or_default();
- io.write_error3(
- &format!(
- "Changed CWD to {}",
- if !cwd.is_empty() {
- cwd.clone()
- } else {
- nwd.clone()
- }
- ),
- true,
- io_interface::DEBUG,
- );
- }
-
- // determine command name to be executed without including plugin commands
- let mut command_name: Option<String> = Some(String::new());
- let raw_command_name = application
- .borrow_mut()
- .get_command_name_before_binding(input.clone())?;
- if let Some(ref raw) = raw_command_name {
- let find_result = application.borrow_mut().find(raw);
- match find_result {
- Ok(cmd) => {
- command_name = cmd.borrow().get_name();
- }
- Err(e) => {
- if e.downcast_ref::<CommandNotFoundException>().is_some() {
- // we'll check command validity again later after plugins are loaded
- command_name = None;
- }
- // PHP also catches \InvalidArgumentException here without action
- }
- }
- }
-
- // prompt user for dir change if no composer.json is present in current dir
- let no_composer_json_commands = vec![
- "".to_string(),
- "list".to_string(),
- "init".to_string(),
- "about".to_string(),
- "help".to_string(),
- "diagnose".to_string(),
- "self-update".to_string(),
- "global".to_string(),
- "create-project".to_string(),
- "outdated".to_string(),
- ];
- let use_parent_dir_if_no_json_available =
- application.borrow().get_use_parent_dir_config_value();
- let no_composer_json_commands_pm = PhpMixed::List(
- no_composer_json_commands
- .iter()
- .map(|s| PhpMixed::String(s.clone()))
- .collect(),
- );
- if new_work_dir.is_none()
- && !in_array(
- command_name.as_deref().unwrap_or("").into(),
- &no_composer_json_commands_pm,
- true,
- )
- && !file_exists(&Factory::get_composer_file().unwrap_or_default())
- && use_parent_dir_if_no_json_available.as_bool() != Some(false)
- && (command_name.as_deref() != Some("config")
- || (!input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["--file"]), true)
- && !input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["-f"]), true)))
- && !input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["--help"]), true)
- && !input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["-h"]), true)
- {
- let mut dir = dirname(&Platform::get_cwd(true).unwrap_or_default());
- let home_value = Platform::get_env("HOME")
- .or_else(|| Platform::get_env("USERPROFILE"))
- .unwrap_or_else(|| "/".to_string());
- let home = realpath(&home_value).unwrap_or_default();
-
- // abort when we reach the home dir or top of the filesystem
- while dirname(&dir) != dir && dir != home {
- if file_exists(&format!(
- "{}/{}",
- dir,
- Factory::get_composer_file().unwrap_or_default()
- )) {
- if use_parent_dir_if_no_json_available.as_bool() != Some(true)
- && !io.is_interactive()
- {
- io.write_error(&format!("<info>No composer.json in current directory, to use the one at {} run interactively or set config.use-parent-dir to true</info>", dir));
- break;
- }
- if use_parent_dir_if_no_json_available.as_bool() == Some(true)
- || io.ask_confirmation(format!("<info>No composer.json in current directory, do you want to use the one at {}?</info> [<comment>y,n</comment>]? ", dir), true)
- {
- if use_parent_dir_if_no_json_available.as_bool() == Some(true) {
- io.write_error(&format!("<info>No composer.json in current directory, changing working directory to {}</info>", dir));
- } else {
- io.write_error("<info>Always want to use the parent dir? Use \"composer config --global use-parent-dir true\" to change the default.</info>");
- }
- old_working_dir = Some(Platform::get_cwd(true).unwrap_or_default());
- chdir(&dir);
- }
- break;
- }
- dir = dirname(&dir);
- }
- drop((dir, home));
- }
-
- let needs_sudo_check = !Platform::is_windows()
- && function_exists("exec")
- && Platform::get_env("COMPOSER_ALLOW_SUPERUSER").is_none()
- && !Platform::is_docker();
- let mut is_non_allowed_root = false;
-
- // Clobber sudo credentials if COMPOSER_ALLOW_SUPERUSER is not set before loading plugins
- if needs_sudo_check {
- is_non_allowed_root = application.borrow().is_running_as_root();
-
- if is_non_allowed_root {
- let uid: i64 = Platform::get_env("SUDO_UID")
- .map(|v| v.parse().unwrap_or(0))
- .unwrap_or(0);
- if uid != 0 {
- // Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on
- // ref. https://github.com/composer/composer/issues/5119
- let _ = Silencer::call(|| {
- shirabe_php_shim::exec(
- &format!("sudo -u \\#{} sudo -K > /dev/null 2>&1", uid),
- None,
- None,
- );
- Ok(())
- });
- }
- }
-
- // Silently clobber any remaining sudo leases on the current user as well to avoid privilege escalations
- let _ = Silencer::call(|| {
- shirabe_php_shim::exec("sudo -K > /dev/null 2>&1", None, None);
- Ok(())
- });
- }
-
- // avoid loading plugins/initializing the Composer instance earlier than necessary if no plugin command is needed
- // if showing the version, we never need plugin commands
- let mnp_list = PhpMixed::List(vec![
- PhpMixed::String("".to_string()),
- PhpMixed::String("list".to_string()),
- PhpMixed::String("help".to_string()),
- ]);
- let may_need_plugin_command = !input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["--version", "-V"]), false)
- && (command_name.is_none()
- || in_array(
- command_name.as_deref().unwrap_or("").into(),
- &mnp_list,
- true,
- )
- || (command_name.as_deref() == Some("_complete") && !is_non_allowed_root));
-
- let may_need_script_command = may_need_plugin_command
- || command_name.as_deref() == Some("run-script")
- || raw_command_name != command_name;
-
- if may_need_plugin_command
- && !application.borrow().disable_plugins_by_default
- && !application.borrow().has_plugin_commands
- {
- // at this point plugins are needed, so if we are running as root and it is not allowed we need to prompt
- // if interactive, and abort otherwise
- if is_non_allowed_root {
- io.write_error("<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>");
-
- if io.is_interactive()
- && io.ask_confirmation(
- "<info>Continue as root/super user</info> [<comment>yes</comment>]? "
- .to_string(),
- true,
- )
- {
- // avoid a second prompt later
- is_non_allowed_root = false;
- } else {
- io.write_error("<warning>Aborting as no plugin should be loaded if running as super user is not explicitly allowed</warning>");
-
- return Ok(1);
- }
- }
-
- // TODO(phase-b): the original PHP catches plugin discovery exceptions in a
- // try/catch. The Rust port keeps the loop but skips IO error reporting
- // because get_plugin_commands borrows &mut self, conflicting with io.
- let mut plugin_warnings: Vec<String> = Vec::new();
- match (|| -> anyhow::Result<()> {
- let plugin_commands = application.borrow_mut().get_plugin_commands()?;
- for command in plugin_commands {
- let cmd_name = command.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();
- plugin_warnings.push(format!("<warning>Plugin command {} ({}) would override a Composer command and has been skipped</warning>", cmd_name, cls));
- } else {
- // Compatibility layer for symfony/console <7.4
- // TODO(phase-c): registering a plugin command needs the Symfony
- // Application's typed add()/addCommand(); the external-package stub keeps
- // its registry as PhpMixed/todo!() per the "Symfony stays todo!()" policy.
- let _ = command;
- }
- }
- Ok(())
- })() {
- Ok(_) => {}
- Err(e) => {
- if e.downcast_ref::<NoSslException>().is_some() {
- // suppress these as they are not relevant at this point
- } else if let Some(pe) = e.downcast_ref::<ParsingException>() {
- let details = pe.get_details();
-
- let file = realpath(&Factory::get_composer_file().unwrap_or_default());
-
- let line = details.line;
-
- let mut ghe = GithubActionError::new(io.clone());
- ghe.emit(&pe.message, file.as_deref(), line);
-
- return Err(e);
- } else {
- return Err(e);
- }
- }
- }
- for warning in &plugin_warnings {
- io.write_error(warning);
- }
-
- application.borrow_mut().has_plugin_commands = true;
- }
-
- if !application.borrow().disable_plugins_by_default
- && is_non_allowed_root
- && !io.is_interactive()
- {
- io.write_error("<error>Composer plugins have been disabled for safety in this non-interactive session.</error>");
- io.write_error("<error>Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.</error>");
- application.borrow_mut().disable_plugins_by_default = true;
- }
-
- // determine command name to be executed incl plugin commands, and check if it's a proxy command
- let mut is_proxy_command = false;
- let resolved_command_name = application
- .borrow_mut()
- .get_command_name_before_binding(input.clone())?;
- if let Some(ref name) = resolved_command_name {
- let find_result = application.borrow_mut().find(name);
- if let Ok(command) = find_result {
- command_name = command.borrow().get_name();
- // PHP: $command instanceof Command\BaseCommand && $command->isProxyCommand().
- // The Symfony `Command` trait exposes `is_proxy_command` (default false) so the
- // `dyn SymfonyCommand` registry can answer this; Composer proxy commands override it.
- is_proxy_command = command.borrow().is_proxy_command();
- }
- }
-
- if !is_proxy_command {
- io.write_error3(
- &format!(
- "Running {} ({}) with {} on {}",
- composer::get_version(),
- composer::RELEASE_DATE,
- (if defined("HHVM_VERSION") {
- format!("HHVM {}", shirabe_php_shim::HHVM_VERSION.unwrap_or(""))
- } else {
- format!("PHP {}", PHP_VERSION)
- }),
- (if function_exists("php_uname") {
- format!("{} / {}", php_uname("s"), php_uname("r"))
- } else {
- "Unknown OS".to_string()
- }),
- ),
- true,
- io_interface::DEBUG,
- );
-
- if PHP_VERSION_ID < 70205 {
- io.write_error(&format!("<warning>Composer supports PHP 7.2.5 and above, you will most likely encounter problems with your PHP {}. Upgrading is strongly recommended but you can use Composer 2.2.x LTS as a fallback.</warning>", PHP_VERSION));
- }
-
- if XdebugHandler::is_xdebug_active()
- && Platform::get_env("COMPOSER_DISABLE_XDEBUG_WARN").is_none()
- {
- io.write_error("<warning>Composer is operating slower than normal because you have Xdebug enabled. See https://getcomposer.org/xdebug</warning>");
- }
-
- if defined("COMPOSER_DEV_WARNING_TIME")
- && command_name.as_deref() != Some("self-update")
- && command_name.as_deref() != Some("selfupdate")
- && time() > shirabe_php_shim::composer_dev_warning_time()
- {
- io.write_error(&format!(
- "<warning>Warning: This development build of Composer is over 60 days old. It is recommended to update it by running \"{} self-update\" to get the latest version.</warning>",
- shirabe_php_shim::server_get("PHP_SELF").unwrap_or_default(),
- ));
- }
-
- if is_non_allowed_root
- && command_name.as_deref() != Some("self-update")
- && command_name.as_deref() != Some("selfupdate")
- && command_name.as_deref() != Some("_complete")
- {
- io.write_error("<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>");
-
- if io.is_interactive()
- && !io.ask_confirmation(
- "<info>Continue as root/super user</info> [<comment>yes</comment>]? "
- .to_string(),
- true,
- )
- {
- return Ok(1);
- }
- }
-
- // Check system temp folder for usability as it can cause weird runtime issues otherwise
- let tempfile_msg: Option<String> = Silencer::call(|| -> anyhow::Result<Option<String>> {
- let pid = if function_exists("getmypid") {
- format!("{}-", getmypid())
- } else {
- String::new()
- };
- let tempfile = format!(
- "{}/temp-{}{}",
- sys_get_temp_dir(),
- pid,
- bin2hex(&random_bytes(5))
- );
- if !(file_put_contents(&tempfile, file!().as_bytes()).is_some_and(|n| n > 0)
- && file_get_contents(&tempfile).as_deref() == Some(file!())
- && unlink(&tempfile)
- && !file_exists(&tempfile))
- {
- return Ok(Some(format!("<error>PHP temp directory ({}) does not exist or is not writable to Composer. Set sys_temp_dir in your php.ini</error>", sys_get_temp_dir())));
- }
- Ok(None)
- })
- .ok()
- .flatten();
- if let Some(msg) = tempfile_msg {
- io.write_error(&msg);
- }
-
- // add non-standard scripts as own commands
- let file = Factory::get_composer_file().unwrap_or_default();
- if may_need_script_command && is_file(&file) && Filesystem::is_readable(&file) {
- let composer_json: PhpMixed =
- json_decode(&file_get_contents(&file).unwrap_or_default(), true)
- .unwrap_or(PhpMixed::Null);
- if let Some(arr) = composer_json.as_array()
- && let Some(scripts) = arr.get("scripts").and_then(|v| v.as_array())
- {
- for (script, dummy) in scripts {
- let script_event_const = format!(
- "Composer\\Script\\ScriptEvents::{}",
- str_replace("-", "_", &strtoupper(script))
- );
- if !defined(&script_event_const) {
- if application.borrow_mut().has(script) {
- io.write_error(&format!("<warning>A script named {} would override a Composer command and has been skipped</warning>", script));
- } else {
- let mut description = format!(
- "Runs the {} script as defined in composer.json",
- script
- );
-
- if let Some(desc) = arr
- .get("scripts-descriptions")
- .and_then(|v| v.as_array())
- .and_then(|a| a.get(script))
- .and_then(|v| v.as_string())
- {
- description = desc.to_string();
- }
-
- let aliases: Vec<String> = arr
- .get("scripts-aliases")
- .and_then(|v| v.as_array())
- .and_then(|a| a.get(script))
- .and_then(|v| v.as_list())
- .map(|l| {
- l.iter()
- .filter_map(|v| v.as_string().map(|s| s.to_string()))
- .collect()
- })
- .unwrap_or_default();
-
- let composer_opt =
- application.borrow_mut().get_composer(false, None, None)?;
- if let Some(composer) = composer_opt {
- let composer = crate::command::composer_full(&composer);
- let root_package = composer.get_package();
- let generator = composer.get_autoload_generator().clone();
- let generator = generator.borrow();
-
- let installation_manager = composer.get_installation_manager();
- let package_map = generator.build_package_map(
- &mut installation_manager.borrow_mut(),
- root_package.clone(),
- vec![],
- )?;
- let map = generator.parse_autoloads(
- package_map,
- root_package.clone(),
- PhpMixed::Bool(false),
- );
-
- let loader = generator.create_loader(
- &map,
- composer
- .get_config()
- .borrow()
- .get("vendor-dir")
- .as_string()
- .map(|s| s.to_string()),
- );
- loader.register(false);
- }
-
- // if the command is not an array of commands, and points to a valid SymfonyCommand subclass, import its details directly
- let dummy_str = dummy.as_string().unwrap_or("").to_string();
- let cmd: PhpMixed = if is_string(dummy)
- && shirabe_php_shim::class_exists(&dummy_str)
- && is_subclass_of(
- &PhpMixed::String(dummy_str.clone()),
- "Symfony\\Component\\Console\\Command\\Command",
- true,
- ) {
- if is_subclass_of(
- &PhpMixed::String(dummy_str.clone()),
- "Symfony\\Component\\Console\\SingleCommandApplication",
- true,
- ) {
- io.write_error(&format!("<warning>The script named {} extends SingleCommandApplication which is not compatible with Composer 2.9+, make sure you extend Symfony\\Component\\Console\\Command instead.</warning>", script));
- }
- let mut cmd = shirabe_php_shim::instantiate_class(
- &dummy_str,
- vec![PhpMixed::String(script.clone())],
- );
- // TODO(phase-c): the script's command class is built by
- // reflection (instantiate_class) and stays PhpMixed; the
- // SingleCommandApplication / SymfonyCommand typed registry it
- // belongs to is an external-package todo!() stub.
- // let _ = SingleCommandApplication::new;
-
- // makes sure the command is find()'able by the name defined in composer.json, and the name isn't overridden in its configure()
- // TODO(phase-c): cmd is the PhpMixed result of reflection
- // instantiation; reading/overriding its
- // name/description requires the typed SymfonyCommand model that
- // the Symfony stub does not yet provide.
- let _ = description.clone();
- let _ = &mut cmd;
- cmd
- } else {
- // fallback to usual aliasing behavior
- // TODO(phase-c): ScriptAliasCommand is a typed BaseCommand
- // but this code path stores commands as PhpMixed; it can
- // only be carried as a typed trait object once the Symfony
- // command registry is modelled.
- let _ = ScriptAliasCommand::new(
- script.clone(),
- Some(description.clone()),
- aliases,
- );
- PhpMixed::Null
- };
-
- // Compatibility layer for symfony/console <7.4
- // TODO(phase-c): Application::add() takes Rc<RefCell<dyn
- // SymfonyCommand>>
- // but `cmd` here is the PhpMixed result of reflection-based
- // plugin command instantiation; registering it as a typed
- // command instance is blocked on the Symfony command-registry
- // model (external-package todo!() stub).
- let _ = &cmd;
- todo!(
- "plugin: register reflection-instantiated command on Application::add"
- );
- }
- }
- }
- }
- }
- }
-
- let mut start_time: Option<f64> = None;
- let result_outcome: anyhow::Result<i32> = (|| -> anyhow::Result<i32> {
- if input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["--profile"]), false)
- {
- start_time = Some(microtime());
- // PHP: $this->io->enableDebugging($startTime).
- // TODO(phase-c): enableDebugging exists only on ConsoleIO, not on IOInterface,
- // and self.io is still the NullIO because the ConsoleIO construction above is
- // deferred (Symfony HelperSet/Helper modelling). Once self.io is the real
- // ConsoleIO this becomes a concrete-type call on it.
- let _ = start_time.unwrap();
- }
-
- let result = Application::base_do_run(application, input.clone(), output.clone())?;
-
- if input
- .borrow()
- .has_parameter_option(PhpMixed::from(vec!["--version", "-V"]), true)
- {
- io.write_error(&format!(
- "<info>PHP</info> version <comment>{}</comment> ({})",
- PHP_VERSION, PHP_BINARY,
- ));
- io.write_error(
- "Run the \"diagnose\" command to get more detailed diagnostics output.",
- );
- }
-
- // chdir back to oldWorkingDir if set
- if let Some(ref owd) = old_working_dir
- && !owd.is_empty()
- {
- let owd = owd.clone();
- let _ = Silencer::call(|| {
- chdir(&owd);
- Ok(())
- });
- }
-
- if let Some(st) = start_time {
- io.write_error(&format!(
- "<info>Memory usage: {}MiB (peak: {}MiB), time: {}s</info>",
- round((memory_get_usage() as f64) / 1024.0 / 1024.0, 2),
- round((memory_get_peak_usage(false) as f64) / 1024.0 / 1024.0, 2),
- round(microtime() - st, 2)
- ));
- }
-
- Ok(result)
- })();
-
- let outcome = match result_outcome {
- Ok(r) => Ok(r),
- Err(e) => {
- // PHP's `exit` bypasses parent::doRun()'s catch entirely; re-raise it untouched so
- // the GitHub Actions annotation and error hints below are skipped.
- if e.downcast_ref::<shirabe_php_shim::ExitException>()
- .is_some()
- {
- return Err(e);
- }
- if let Some(see) = e.downcast_ref::<ScriptExecutionException>() {
- if application.borrow().get_disable_plugins_by_default()
- && application.borrow().is_running_as_root()
- && !io.is_interactive()
- {
- io.write_error3("<error>Plugins have been disabled automatically as you are running as root, this may be the cause of the script failure.</error>", true, io_interface::QUIET);
- io.write_error3(
- "<error>See also https://getcomposer.org/root</error>",
- true,
- io_interface::QUIET,
- );
- }
-
- Ok(see.get_code() as i32)
- } else {
- let mut ghe = GithubActionError::new(io.clone());
- ghe.emit(&e.to_string(), None, None);
-
- application.borrow_mut().hint_common_errors(&e, output);
-
- // override TransportException's code for the purpose of parent::run() using it as process exit code
- // as http error codes are all beyond the 255 range of permitted exit codes
- if e.downcast_ref::<TransportException>().is_some() {
- // PHP: ReflectionProperty $reflProp = new \ReflectionProperty($e, 'code');
- // $reflProp->setValue($e, Installer::ERROR_TRANSPORT_EXCEPTION);
- // TODO: reflection-based mutation of the existing exception is not portable;
- // we surface the rewritten code via a fresh TransportException at the call site.
- let _ = Installer::ERROR_TRANSPORT_EXCEPTION;
- }
-
- Err(e)
- }
- }
- };
-
- restore_error_handler();
-
- outcome
+ /// Returns the shared handle to this application set up by `ApplicationHandle::new`. Proxy
+ /// commands use it to re-enter the run flow (PHP's `$this->getApplication()->run(...)`).
+ pub fn shared(&self) -> ApplicationHandle {
+ ApplicationHandle(
+ self.me
+ .upgrade()
+ .expect("Application must be constructed through ApplicationHandle::new"),
+ )
}
fn get_new_working_dir(
@@ -1409,14 +644,7 @@ impl Application {
fn is_running_as_root(&self) -> bool {
function_exists("posix_getuid") && posix_getuid() == 0
}
-}
-/// Methods inherited from `Symfony\Component\Console\Application`. They live in the same `impl`
-/// surface as the Composer overrides above so that polymorphic `self.*` calls dispatch to the
-/// Composer version when one exists. Methods that Composer overrides while still calling `parent::`
-/// are carried here under a `base_` prefix (`base_run`, `base_do_run`, `base_get_help`,
-/// `base_get_default_input_definition`, `base_get_default_commands`).
-impl Application {
pub fn set_command_loader(&mut self, command_loader: Box<dyn CommandLoaderInterface>) {
self.command_loader = Some(command_loader);
}
@@ -1436,254 +664,6 @@ impl Application {
self.signals_to_dispatch_event = signals_to_dispatch_event;
}
- /// Runs the current application (Symfony base; `parent::run`).
- pub fn base_run(
- application: &std::rc::Rc<std::cell::RefCell<Application>>,
- input: Option<std::rc::Rc<std::cell::RefCell<dyn InputInterface>>>,
- output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
- ) -> anyhow::Result<i32> {
- if shirabe_php_shim::function_exists("putenv") {
- let (height, width) = {
- let app = application.borrow();
- (app.terminal.get_height(), app.terminal.get_width())
- };
- shirabe_php_shim::putenv(&format!("LINES={}", height));
- shirabe_php_shim::putenv(&format!("COLUMNS={}", width));
- }
-
- let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = match input {
- None => std::rc::Rc::new(std::cell::RefCell::new(ArgvInput::new(None, None)?)),
- Some(input) => input,
- };
-
- let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = match output {
- None => std::rc::Rc::new(std::cell::RefCell::new(ConsoleOutput::new(
- None, None, None,
- )?)),
- Some(output) => output,
- };
-
- // TODO: PHP installs a temporary `set_exception_handler($renderException)` and cooperates
- // with Symfony's ErrorHandler to keep/restore it. PHP's process-global exception handler
- // stack has no Rust equivalent; the rendering itself is invoked directly in the catch
- // branch below. Review needed for the handler save/restore dance.
- let render_exception =
- |this: &Application,
- e: &anyhow::Error,
- output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>| {
- // if ($output instanceof ConsoleOutputInterface) render to its error output
- // TODO(review): downcasting a `dyn OutputInterface` to `ConsoleOutputInterface`
- // is not directly expressible; the ConsoleOutputInterface branch needs design.
- this.render_throwable(e, output.clone());
- };
-
- let result = (|| -> anyhow::Result<i32> {
- application.borrow_mut().configure_io(&input, &output)?;
-
- let exit_code = Application::do_run(application, input.clone(), output.clone())?;
-
- Ok(exit_code)
- })();
-
- let exit_code = match result {
- Ok(exit_code) => exit_code,
- Err(e) => {
- // PHP's `exit` bypasses Symfony's try/catch: it never renders and forces the exit
- // code it carries. The message, if any, has already been written at the exit site.
- if let Some(exit) = e.downcast_ref::<shirabe_php_shim::ExitException>() {
- return Ok(exit.code as i32);
- }
-
- if !application.borrow().catch_exceptions {
- return Err(e);
- }
-
- render_exception(&application.borrow(), &e, &output);
-
- // $exitCode = $e->getCode();
- // is_numeric($exitCode) ? max(1, (int) $exitCode) : 1
- // TODO(review): anyhow::Error has no PHP-style getCode(); the exit code derived
- // from the exception's `code` field needs the downcast strategy decided.
- let exit_code = shirabe_php_shim::php_exception_get_code(&e);
- if shirabe_php_shim::is_numeric_string(&exit_code.to_string()) {
- if exit_code <= 0 { 1 } else { exit_code }
- } else {
- 1
- }
- }
- };
-
- // finally: handler restore. See TODO above; no-op here.
-
- Ok(exit_code)
- }
-
- /// Runs the current application (Symfony base; `parent::doRun`).
- pub fn base_do_run(
- application: &std::rc::Rc<std::cell::RefCell<Application>>,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i32> {
- if input.borrow().has_parameter_option(
- PhpMixed::from(vec![
- PhpMixed::from("--version".to_string()),
- PhpMixed::from("-V".to_string()),
- ]),
- true,
- ) {
- let long_version = application.borrow().get_long_version();
- output
- .borrow()
- .writeln(&[long_version], output_interface::OUTPUT_NORMAL);
-
- return Ok(0);
- }
-
- // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
- let definition = application.borrow_mut().get_definition();
- match input.borrow_mut().bind(&definition.borrow()) {
- Ok(()) => {}
- Err(e) => {
- // Errors must be ignored, full binding/validation happens later when the command is known.
- if !is_exception_interface(&e) {
- return Err(e);
- }
- }
- }
-
- let mut input = input;
- let mut name = application.borrow().get_command_name(&*input.borrow());
- if input.borrow().has_parameter_option(
- PhpMixed::from(vec![
- PhpMixed::from("--help".to_string()),
- PhpMixed::from("-h".to_string()),
- ]),
- true,
- ) {
- if name.is_none() {
- name = Some("help".to_string());
- let default_command = application.borrow().default_command.clone();
- input = std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(
- vec![(
- PhpMixed::from("command_name".to_string()),
- PhpMixed::from(default_command),
- )],
- None,
- )?));
- } else {
- application.borrow_mut().want_helps = true;
- }
- }
-
- let name = match name {
- Some(name) => name,
- None => {
- let name = application.borrow().default_command.clone();
- let definition = application.borrow_mut().get_definition();
- let command_description = definition
- .borrow()
- .get_argument(&PhpMixed::from("command".to_string()))?
- .get_description()
- .to_string();
- let new_command_argument = InputArgument::new(
- "command".to_string(),
- Some(InputArgument::OPTIONAL),
- command_description,
- PhpMixed::from(name.clone()),
- )?;
- // $definition->setArguments(array_merge($definition->getArguments(),
- // ['command' => new InputArgument('command', InputArgument::OPTIONAL, ...)]))
- let mut merged_arguments: Vec<InputArgument> = Vec::new();
- let mut replaced_command = false;
- for (key, argument) in definition.borrow().get_arguments() {
- if key == "command" {
- merged_arguments.push(new_command_argument.clone());
- replaced_command = true;
- } else {
- merged_arguments.push((**argument).clone());
- }
- }
- if !replaced_command {
- merged_arguments.push(new_command_argument);
- }
- definition.borrow_mut().set_arguments(merged_arguments)?;
-
- name
- }
- };
-
- let command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>;
- application.borrow_mut().running_command = None;
- // the command name MUST be the first element of the input
- let find_result = application.borrow_mut().find(&name);
-
- match find_result {
- Ok(c) => {
- command = c;
- }
- Err(e) => {
- // if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException)
- // || 1 !== count($alternatives = $e->getAlternatives()) || !$input->isInteractive())
- let alternatives: Option<Vec<String>> = downcast_command_not_found(&e)
- .filter(|_| !is_namespace_not_found(&e))
- .map(|cnf| cnf.get_alternatives().clone());
-
- let single_alternative = match &alternatives {
- Some(alts) if alts.len() == 1 => Some(alts[0].clone()),
- _ => None,
- };
-
- if single_alternative.is_none() || !input.borrow().is_interactive() {
- return Err(e);
- }
-
- let alternative = single_alternative.unwrap();
-
- let mut style = SymfonyStyle::new(input.clone(), output.clone());
- output
- .borrow()
- .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL);
- let formatted_block = FormatterHelper::default().format_block(
- FormatBlockMessages::String(format!(
- "Command \"{}\" is not defined.",
- PhpMixed::from(name.clone()),
- )),
- "error",
- true,
- );
- output
- .borrow()
- .writeln(&[formatted_block], output_interface::OUTPUT_NORMAL);
- if !style.confirm(
- &format!(
- "Do you want to run \"{}\" instead? ",
- PhpMixed::from(alternative.clone()),
- ),
- false,
- ) {
- return Ok(1);
- }
-
- command = application.borrow_mut().find(&alternative)?;
- }
- }
-
- // LazyCommand is intentionally not ported.
-
- application.borrow_mut().running_command = Some(command.clone());
- // do_run_command invokes the command's run(), which calls back into the application
- // (mergeApplicationDefinition etc.); it must run with no application borrow held.
- let exit_code = Application::do_run_command(
- application,
- command.clone(),
- input.clone(),
- output.clone(),
- )?;
- application.borrow_mut().running_command = None;
-
- Ok(exit_code)
- }
-
pub fn set_helper_set(&mut self, helper_set: std::rc::Rc<std::cell::RefCell<HelperSet>>) {
self.helper_set = Some(helper_set);
}
@@ -1812,66 +792,6 @@ impl Application {
self.version = version.to_string();
}
- /// Adds an array of command objects.
- ///
- /// If a SymfonyCommand is not enabled it will not be added.
- pub fn add_commands(
- &mut self,
- commands: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>,
- ) -> anyhow::Result<()> {
- for command in commands {
- self.add(command)?;
- }
- Ok(())
- }
-
- /// Adds a command object.
- ///
- /// If a command with the same name already exists, it will be overridden.
- /// If the command is not enabled it will not be added.
- pub fn add(
- &mut self,
- command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>,
- ) -> anyhow::Result<Option<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> {
- self.init()?;
-
- // TODO(review): $command->setApplication($this) needs an Rc<RefCell<Application>> to the
- // current instance. Application is held by value here; the self-reference required to set
- // the command's back-pointer needs the shared-ownership design (Phase C).
- command
- .borrow_mut()
- .set_application(todo!("Rc<RefCell<Application>> of self"));
-
- if !command.borrow().is_enabled() {
- command.borrow_mut().set_application(None);
-
- return Ok(None);
- }
-
- // LazyCommand is intentionally not ported.
- command.borrow().get_definition();
-
- if command.borrow().get_name().is_none() {
- return Err(ConsoleLogicException(shirabe_php_shim::LogicException {
- message: format!(
- "The command defined in \"{}\" cannot have an empty name.",
- PhpMixed::from(shirabe_php_shim::get_debug_type_obj(&command,)),
- ),
- code: 0,
- })
- .into());
- }
-
- let name = command.borrow().get_name().unwrap();
- self.commands.insert(name, command.clone());
-
- for alias in command.borrow().get_aliases() {
- self.commands.insert(alias, command.clone());
- }
-
- Ok(Some(command))
- }
-
/// Returns a registered command by name or alias.
///
/// Throws CommandNotFoundException when given command name does not exist.
@@ -1879,8 +799,6 @@ impl Application {
&mut self,
name: &str,
) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> {
- self.init()?;
-
if !self.has(name) {
return Err(CommandNotFoundException::new(
format!(
@@ -1927,8 +845,6 @@ impl Application {
/// Returns true if the command exists, false otherwise.
pub fn has(&mut self, name: &str) -> bool {
- self.init().unwrap();
-
if self.commands.contains_key(name) {
return true;
}
@@ -1942,6 +858,7 @@ impl Application {
// Rc<RefCell<dyn SymfonyCommand>>; the loader return type needs reconciliation.
let _ = command;
return self
+ .shared()
.add(todo!(
"Rc<RefCell<dyn SymfonyCommand>> from command_loader.get(name)"
))
@@ -2056,8 +973,6 @@ impl Application {
&mut self,
name: &str,
) -> anyhow::Result<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> {
- self.init()?;
-
let mut aliases: IndexMap<String, String> = IndexMap::new();
let commands_snapshot: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>> =
@@ -2269,8 +1184,6 @@ impl Application {
&mut self,
namespace: Option<&str>,
) -> anyhow::Result<IndexMap<String, std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> {
- self.init()?;
-
if namespace.is_none() {
if self.command_loader.is_none() {
return Ok(self.commands.clone());
@@ -2608,66 +1521,6 @@ impl Application {
Ok(())
}
- /// Runs the current command.
- ///
- /// If an event dispatcher has been attached to the application, events are also
- /// dispatched during the life-cycle of the command.
- ///
- /// Returns 0 if everything went fine, or an error code.
- pub fn do_run_command(
- application: &std::rc::Rc<std::cell::RefCell<Application>>,
- command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>,
- input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
- output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
- ) -> anyhow::Result<i32> {
- if let Some(helper_set) = command.borrow().get_helper_set() {
- for (_alias, helper) in helper_set.borrow().get_iterator() {
- // if ($helper instanceof InputAwareInterface) $helper->setInput($input);
- // TODO(review): downcasting a HelperInterface to InputAwareInterface is not
- // expressible without a typed mechanism; needs design.
- let _ = helper;
- let _ = std::marker::PhantomData::<dyn InputAwareInterface>;
- }
- }
-
- if !application.borrow().signals_to_dispatch_event.is_empty() {
- // $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []
- // TODO(review): SymfonyCommand is not a SignalableCommandInterface here; downcast needed.
- let command_signals: Vec<i64> = Vec::new();
- let _ = std::marker::PhantomData::<dyn SignalableCommandInterface>;
-
- if !command_signals.is_empty() {
- if application.borrow().signal_registry.is_none() {
- return Err(ConsoleRuntimeException(shirabe_php_shim::RuntimeException {
- message: "Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(),
- code: 0,
- })
- .into());
- }
-
- if Terminal::has_stty_available() {
- // TODO: registers SIGINT/SIGTERM handlers that restore the stty mode via
- // shell_exec('stty ...'). pcntl signal handlers have no faithful Rust
- // equivalent in Phase A.
- let _stty_mode = shirabe_php_shim::shell_exec("stty -g");
- for _signal in [shirabe_php_shim::SIGINT, shirabe_php_shim::SIGTERM] {
- todo!("register signal handler to restore stty mode");
- }
- }
- }
-
- for _signal in command_signals {
- // $this->signalRegistry->register($signal, [$command, 'handleSignal']);
- todo!("register command->handle_signal as signal handler");
- }
- }
-
- command
- .borrow_mut()
- .run(input.clone(), output.clone())
- .map(|c| c as i32)
- }
-
/// Gets the name of the command based on input.
pub fn get_command_name(&self, input: &dyn InputInterface) -> Option<String> {
if self.single_command {
@@ -2993,19 +1846,1104 @@ impl Application {
namespaces
}
+}
- fn init(&mut self) -> anyhow::Result<()> {
- if self.initialized {
- return Ok(());
- }
- self.initialized = true;
+/// Shared handle to an `Application`.
+///
+/// PHP `Application` instances are shared by reference — commands hold a back-pointer to their
+/// owning application that aliases this very `RefCell`. The whole run flow is therefore driven
+/// through this handle with short-lived borrows, so a command callback never re-borrows the
+/// application while it is already borrowed. Mirrors the Composer handle pattern (`ComposerHandle`).
+#[derive(Debug, Clone)]
+pub struct ApplicationHandle(std::rc::Rc<std::cell::RefCell<Application>>);
- for command in self.get_default_commands() {
+impl ApplicationHandle {
+ /// Builds the application inside a shared handle and registers the default commands.
+ ///
+ /// Commands hold a back-pointer to their application, which shares this very `RefCell`. So the
+ /// whole run flow is driven through the handle (never a long-lived `borrow_mut`), and command
+ /// registration happens here — before any borrow is taken — so `set_application` can borrow the
+ /// application without a re-entrant conflict.
+ pub fn new(name: String, version: String) -> anyhow::Result<ApplicationHandle> {
+ let application = ApplicationHandle(std::rc::Rc::new(std::cell::RefCell::new(
+ Application::new(name, version),
+ )));
+ application.0.borrow_mut().me = std::rc::Rc::downgrade(&application.0);
+ application.init()?;
+ Ok(application)
+ }
+
+ /// Registers the default commands. It is called lazily with $this->initialized flag in PHP,
+ /// but called once from new() in Rust.
+ fn init(&self) -> anyhow::Result<()> {
+ let commands = self.0.borrow().get_default_commands();
+ for command in commands {
self.add(command)?;
}
Ok(())
}
+
+ /// Adds a command object.
+ ///
+ /// If a command with the same name already exists, it will be overridden.
+ /// If the command is not enabled it will not be added.
+ ///
+ /// Takes the shared application handle rather than `&mut self` so no application borrow is held
+ /// while calling into the command, letting `set_application` borrow back safely.
+ pub fn add(
+ &self,
+ command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>,
+ ) -> anyhow::Result<Option<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>> {
+ let application = &self.0;
+ command.borrow_mut().set_application(Some(
+ application.clone() as std::rc::Rc<std::cell::RefCell<dyn BaseApplication>>
+ ));
+
+ if !command.borrow().is_enabled() {
+ command.borrow_mut().set_application(None);
+
+ return Ok(None);
+ }
+
+ // LazyCommand is intentionally not ported.
+ command.borrow().get_definition();
+
+ if command.borrow().get_name().is_none() {
+ return Err(ConsoleLogicException(shirabe_php_shim::LogicException {
+ message: format!(
+ "The command defined in \"{}\" cannot have an empty name.",
+ PhpMixed::from(shirabe_php_shim::get_debug_type_obj(&command)),
+ ),
+ code: 0,
+ })
+ .into());
+ }
+
+ let name = command.borrow().get_name().unwrap();
+ application
+ .borrow_mut()
+ .commands
+ .insert(name, command.clone());
+
+ for alias in command.borrow().get_aliases() {
+ application
+ .borrow_mut()
+ .commands
+ .insert(alias, command.clone());
+ }
+
+ Ok(Some(command))
+ }
+
+ pub fn run(
+ &self,
+ input: Option<std::rc::Rc<std::cell::RefCell<dyn InputInterface>>>,
+ output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
+ ) -> anyhow::Result<i32> {
+ let output = match output {
+ Some(output) => Some(output),
+ None => Some(
+ std::rc::Rc::new(std::cell::RefCell::new(Factory::create_output()))
+ as std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ),
+ };
+
+ self.base_run(input, output)
+ }
+
+ pub fn do_run(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i32> {
+ let application = &self.0;
+ application.borrow_mut().disable_plugins_by_default = input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["--no-plugins"]), false);
+ application.borrow_mut().disable_scripts_by_default = input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["--no-scripts"]), false);
+
+ let stdin = shirabe_php_shim::STDIN;
+ if Platform::get_env("COMPOSER_TESTS_ARE_RUNNING").as_deref() != Some("1")
+ && (Platform::get_env("COMPOSER_NO_INTERACTION").is_some()
+ || !Platform::is_tty(Some(stdin)))
+ {
+ input.borrow_mut().set_interactive(false);
+ }
+
+ let mut helpers: IndexMap<
+ HelperSetKey,
+ std::rc::Rc<std::cell::RefCell<dyn HelperInterface>>,
+ > = IndexMap::new();
+ helpers.insert(
+ HelperSetKey::Int(0),
+ std::rc::Rc::new(std::cell::RefCell::new(QuestionHelper::default())),
+ );
+ let helper_set = std::rc::Rc::new(std::cell::RefCell::new(HelperSet::default()));
+ HelperSet::new(&helper_set, helpers);
+ application.borrow_mut().io = std::rc::Rc::new(std::cell::RefCell::new(ConsoleIO::new(
+ input.clone(),
+ output.clone(),
+ helper_set.borrow().clone(),
+ )));
+ // Cache the IO so the rest of the flow does not re-borrow the application; command callbacks
+ // (e.g. mergeApplicationDefinition) need the application's RefCell free.
+ let io = application.borrow().io.clone();
+
+ // Register error handler again to pass it the IO instance
+ ErrorHandler::register(Some(io.clone()));
+
+ if input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["--no-cache"]), false)
+ {
+ io.write_error3("Disabling cache usage", true, io_interface::DEBUG);
+ Platform::put_env(
+ "COMPOSER_CACHE_DIR",
+ if Platform::is_windows() {
+ "nul"
+ } else {
+ "/dev/null"
+ },
+ );
+ }
+
+ // switch working dir
+ let new_work_dir = application.borrow().get_new_working_dir(input.clone())?;
+ let mut old_working_dir: Option<String> = None;
+ if let Some(ref nwd) = new_work_dir {
+ old_working_dir = Some(Platform::get_cwd(true).unwrap_or_default());
+ chdir(nwd);
+ application.borrow_mut().initial_working_directory = getcwd();
+ let cwd = Platform::get_cwd(true).unwrap_or_default();
+ io.write_error3(
+ &format!(
+ "Changed CWD to {}",
+ if !cwd.is_empty() {
+ cwd.clone()
+ } else {
+ nwd.clone()
+ }
+ ),
+ true,
+ io_interface::DEBUG,
+ );
+ }
+
+ // determine command name to be executed without including plugin commands
+ let mut command_name: Option<String> = Some(String::new());
+ let raw_command_name = application
+ .borrow_mut()
+ .get_command_name_before_binding(input.clone())?;
+ if let Some(ref raw) = raw_command_name {
+ let find_result = application.borrow_mut().find(raw);
+ match find_result {
+ Ok(cmd) => {
+ command_name = cmd.borrow().get_name();
+ }
+ Err(e) => {
+ if e.downcast_ref::<CommandNotFoundException>().is_some() {
+ // we'll check command validity again later after plugins are loaded
+ command_name = None;
+ }
+ // PHP also catches \InvalidArgumentException here without action
+ }
+ }
+ }
+
+ // prompt user for dir change if no composer.json is present in current dir
+ let no_composer_json_commands = vec![
+ "".to_string(),
+ "list".to_string(),
+ "init".to_string(),
+ "about".to_string(),
+ "help".to_string(),
+ "diagnose".to_string(),
+ "self-update".to_string(),
+ "global".to_string(),
+ "create-project".to_string(),
+ "outdated".to_string(),
+ ];
+ let use_parent_dir_if_no_json_available =
+ application.borrow().get_use_parent_dir_config_value();
+ let no_composer_json_commands_pm = PhpMixed::List(
+ no_composer_json_commands
+ .iter()
+ .map(|s| PhpMixed::String(s.clone()))
+ .collect(),
+ );
+ if new_work_dir.is_none()
+ && !in_array(
+ command_name.as_deref().unwrap_or("").into(),
+ &no_composer_json_commands_pm,
+ true,
+ )
+ && !file_exists(&Factory::get_composer_file().unwrap_or_default())
+ && use_parent_dir_if_no_json_available.as_bool() != Some(false)
+ && (command_name.as_deref() != Some("config")
+ || (!input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["--file"]), true)
+ && !input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["-f"]), true)))
+ && !input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["--help"]), true)
+ && !input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["-h"]), true)
+ {
+ let mut dir = dirname(&Platform::get_cwd(true).unwrap_or_default());
+ let home_value = Platform::get_env("HOME")
+ .or_else(|| Platform::get_env("USERPROFILE"))
+ .unwrap_or_else(|| "/".to_string());
+ let home = realpath(&home_value).unwrap_or_default();
+
+ // abort when we reach the home dir or top of the filesystem
+ while dirname(&dir) != dir && dir != home {
+ if file_exists(&format!(
+ "{}/{}",
+ dir,
+ Factory::get_composer_file().unwrap_or_default()
+ )) {
+ if use_parent_dir_if_no_json_available.as_bool() != Some(true)
+ && !io.is_interactive()
+ {
+ io.write_error(&format!("<info>No composer.json in current directory, to use the one at {} run interactively or set config.use-parent-dir to true</info>", dir));
+ break;
+ }
+ if use_parent_dir_if_no_json_available.as_bool() == Some(true)
+ || io.ask_confirmation(format!("<info>No composer.json in current directory, do you want to use the one at {}?</info> [<comment>y,n</comment>]? ", dir), true)
+ {
+ if use_parent_dir_if_no_json_available.as_bool() == Some(true) {
+ io.write_error(&format!("<info>No composer.json in current directory, changing working directory to {}</info>", dir));
+ } else {
+ io.write_error("<info>Always want to use the parent dir? Use \"composer config --global use-parent-dir true\" to change the default.</info>");
+ }
+ old_working_dir = Some(Platform::get_cwd(true).unwrap_or_default());
+ chdir(&dir);
+ }
+ break;
+ }
+ dir = dirname(&dir);
+ }
+ drop((dir, home));
+ }
+
+ let needs_sudo_check = !Platform::is_windows()
+ && function_exists("exec")
+ && Platform::get_env("COMPOSER_ALLOW_SUPERUSER").is_none()
+ && !Platform::is_docker();
+ let mut is_non_allowed_root = false;
+
+ // Clobber sudo credentials if COMPOSER_ALLOW_SUPERUSER is not set before loading plugins
+ if needs_sudo_check {
+ is_non_allowed_root = application.borrow().is_running_as_root();
+
+ if is_non_allowed_root {
+ let uid: i64 = Platform::get_env("SUDO_UID")
+ .map(|v| v.parse().unwrap_or(0))
+ .unwrap_or(0);
+ if uid != 0 {
+ // Silently clobber any sudo credentials on the invoking user to avoid privilege escalations later on
+ // ref. https://github.com/composer/composer/issues/5119
+ let _ = Silencer::call(|| {
+ shirabe_php_shim::exec(
+ &format!("sudo -u \\#{} sudo -K > /dev/null 2>&1", uid),
+ None,
+ None,
+ );
+ Ok(())
+ });
+ }
+ }
+
+ // Silently clobber any remaining sudo leases on the current user as well to avoid privilege escalations
+ let _ = Silencer::call(|| {
+ shirabe_php_shim::exec("sudo -K > /dev/null 2>&1", None, None);
+ Ok(())
+ });
+ }
+
+ // avoid loading plugins/initializing the Composer instance earlier than necessary if no plugin command is needed
+ // if showing the version, we never need plugin commands
+ let mnp_list = PhpMixed::List(vec![
+ PhpMixed::String("".to_string()),
+ PhpMixed::String("list".to_string()),
+ PhpMixed::String("help".to_string()),
+ ]);
+ let may_need_plugin_command = !input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["--version", "-V"]), false)
+ && (command_name.is_none()
+ || in_array(
+ command_name.as_deref().unwrap_or("").into(),
+ &mnp_list,
+ true,
+ )
+ || (command_name.as_deref() == Some("_complete") && !is_non_allowed_root));
+
+ let may_need_script_command = may_need_plugin_command
+ || command_name.as_deref() == Some("run-script")
+ || raw_command_name != command_name;
+
+ if may_need_plugin_command
+ && !application.borrow().disable_plugins_by_default
+ && !application.borrow().has_plugin_commands
+ {
+ // at this point plugins are needed, so if we are running as root and it is not allowed we need to prompt
+ // if interactive, and abort otherwise
+ if is_non_allowed_root {
+ io.write_error("<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>");
+
+ if io.is_interactive()
+ && io.ask_confirmation(
+ "<info>Continue as root/super user</info> [<comment>yes</comment>]? "
+ .to_string(),
+ true,
+ )
+ {
+ // avoid a second prompt later
+ is_non_allowed_root = false;
+ } else {
+ io.write_error("<warning>Aborting as no plugin should be loaded if running as super user is not explicitly allowed</warning>");
+
+ return Ok(1);
+ }
+ }
+
+ // TODO(phase-b): the original PHP catches plugin discovery exceptions in a
+ // try/catch. The Rust port keeps the loop but skips IO error reporting
+ // because get_plugin_commands borrows &mut self, conflicting with io.
+ let mut plugin_warnings: Vec<String> = Vec::new();
+ match (|| -> anyhow::Result<()> {
+ let plugin_commands = application.borrow_mut().get_plugin_commands()?;
+ for command in plugin_commands {
+ let cmd_name = command.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();
+ plugin_warnings.push(format!("<warning>Plugin command {} ({}) would override a Composer command and has been skipped</warning>", cmd_name, cls));
+ } else {
+ // Compatibility layer for symfony/console <7.4
+ // TODO(phase-c): registering a plugin command needs the Symfony
+ // Application's typed add()/addCommand(); the external-package stub keeps
+ // its registry as PhpMixed/todo!() per the "Symfony stays todo!()" policy.
+ let _ = command;
+ }
+ }
+ Ok(())
+ })() {
+ Ok(_) => {}
+ Err(e) => {
+ if e.downcast_ref::<NoSslException>().is_some() {
+ // suppress these as they are not relevant at this point
+ } else if let Some(pe) = e.downcast_ref::<ParsingException>() {
+ let details = pe.get_details();
+
+ let file = realpath(&Factory::get_composer_file().unwrap_or_default());
+
+ let line = details.line;
+
+ let mut ghe = GithubActionError::new(io.clone());
+ ghe.emit(&pe.message, file.as_deref(), line);
+
+ return Err(e);
+ } else {
+ return Err(e);
+ }
+ }
+ }
+ for warning in &plugin_warnings {
+ io.write_error(warning);
+ }
+
+ application.borrow_mut().has_plugin_commands = true;
+ }
+
+ if !application.borrow().disable_plugins_by_default
+ && is_non_allowed_root
+ && !io.is_interactive()
+ {
+ io.write_error("<error>Composer plugins have been disabled for safety in this non-interactive session.</error>");
+ io.write_error("<error>Set COMPOSER_ALLOW_SUPERUSER=1 if you want to allow plugins to run as root/super user.</error>");
+ application.borrow_mut().disable_plugins_by_default = true;
+ }
+
+ // determine command name to be executed incl plugin commands, and check if it's a proxy command
+ let mut is_proxy_command = false;
+ let resolved_command_name = application
+ .borrow_mut()
+ .get_command_name_before_binding(input.clone())?;
+ if let Some(ref name) = resolved_command_name {
+ let find_result = application.borrow_mut().find(name);
+ if let Ok(command) = find_result {
+ command_name = command.borrow().get_name();
+ // PHP: $command instanceof Command\BaseCommand && $command->isProxyCommand().
+ // The Symfony `Command` trait exposes `is_proxy_command` (default false) so the
+ // `dyn SymfonyCommand` registry can answer this; Composer proxy commands override it.
+ is_proxy_command = command.borrow().is_proxy_command();
+ }
+ }
+
+ if !is_proxy_command {
+ io.write_error3(
+ &format!(
+ "Running {} ({}) with {} on {}",
+ composer::get_version(),
+ composer::RELEASE_DATE,
+ (if defined("HHVM_VERSION") {
+ format!("HHVM {}", shirabe_php_shim::HHVM_VERSION.unwrap_or(""))
+ } else {
+ format!("PHP {}", PHP_VERSION)
+ }),
+ (if function_exists("php_uname") {
+ format!("{} / {}", php_uname("s"), php_uname("r"))
+ } else {
+ "Unknown OS".to_string()
+ }),
+ ),
+ true,
+ io_interface::DEBUG,
+ );
+
+ if PHP_VERSION_ID < 70205 {
+ io.write_error(&format!("<warning>Composer supports PHP 7.2.5 and above, you will most likely encounter problems with your PHP {}. Upgrading is strongly recommended but you can use Composer 2.2.x LTS as a fallback.</warning>", PHP_VERSION));
+ }
+
+ if XdebugHandler::is_xdebug_active()
+ && Platform::get_env("COMPOSER_DISABLE_XDEBUG_WARN").is_none()
+ {
+ io.write_error("<warning>Composer is operating slower than normal because you have Xdebug enabled. See https://getcomposer.org/xdebug</warning>");
+ }
+
+ if defined("COMPOSER_DEV_WARNING_TIME")
+ && command_name.as_deref() != Some("self-update")
+ && command_name.as_deref() != Some("selfupdate")
+ && time() > shirabe_php_shim::composer_dev_warning_time()
+ {
+ io.write_error(&format!(
+ "<warning>Warning: This development build of Composer is over 60 days old. It is recommended to update it by running \"{} self-update\" to get the latest version.</warning>",
+ shirabe_php_shim::server_get("PHP_SELF").unwrap_or_default(),
+ ));
+ }
+
+ if is_non_allowed_root
+ && command_name.as_deref() != Some("self-update")
+ && command_name.as_deref() != Some("selfupdate")
+ && command_name.as_deref() != Some("_complete")
+ {
+ io.write_error("<warning>Do not run Composer as root/super user! See https://getcomposer.org/root for details</warning>");
+
+ if io.is_interactive()
+ && !io.ask_confirmation(
+ "<info>Continue as root/super user</info> [<comment>yes</comment>]? "
+ .to_string(),
+ true,
+ )
+ {
+ return Ok(1);
+ }
+ }
+
+ // Check system temp folder for usability as it can cause weird runtime issues otherwise
+ let tempfile_msg: Option<String> = Silencer::call(|| -> anyhow::Result<Option<String>> {
+ let pid = if function_exists("getmypid") {
+ format!("{}-", getmypid())
+ } else {
+ String::new()
+ };
+ let tempfile = format!(
+ "{}/temp-{}{}",
+ sys_get_temp_dir(),
+ pid,
+ bin2hex(&random_bytes(5))
+ );
+ if !(file_put_contents(&tempfile, file!().as_bytes()).is_some_and(|n| n > 0)
+ && file_get_contents(&tempfile).as_deref() == Some(file!())
+ && unlink(&tempfile)
+ && !file_exists(&tempfile))
+ {
+ return Ok(Some(format!("<error>PHP temp directory ({}) does not exist or is not writable to Composer. Set sys_temp_dir in your php.ini</error>", sys_get_temp_dir())));
+ }
+ Ok(None)
+ })
+ .ok()
+ .flatten();
+ if let Some(msg) = tempfile_msg {
+ io.write_error(&msg);
+ }
+
+ // add non-standard scripts as own commands
+ let file = Factory::get_composer_file().unwrap_or_default();
+ if may_need_script_command && is_file(&file) && Filesystem::is_readable(&file) {
+ let composer_json: PhpMixed =
+ json_decode(&file_get_contents(&file).unwrap_or_default(), true)
+ .unwrap_or(PhpMixed::Null);
+ if let Some(arr) = composer_json.as_array()
+ && let Some(scripts) = arr.get("scripts").and_then(|v| v.as_array())
+ {
+ for (script, dummy) in scripts {
+ let script_event_const = format!(
+ "Composer\\Script\\ScriptEvents::{}",
+ str_replace("-", "_", &strtoupper(script))
+ );
+ if !defined(&script_event_const) {
+ if application.borrow_mut().has(script) {
+ io.write_error(&format!("<warning>A script named {} would override a Composer command and has been skipped</warning>", script));
+ } else {
+ let mut description = format!(
+ "Runs the {} script as defined in composer.json",
+ script
+ );
+
+ if let Some(desc) = arr
+ .get("scripts-descriptions")
+ .and_then(|v| v.as_array())
+ .and_then(|a| a.get(script))
+ .and_then(|v| v.as_string())
+ {
+ description = desc.to_string();
+ }
+
+ let aliases: Vec<String> = arr
+ .get("scripts-aliases")
+ .and_then(|v| v.as_array())
+ .and_then(|a| a.get(script))
+ .and_then(|v| v.as_list())
+ .map(|l| {
+ l.iter()
+ .filter_map(|v| v.as_string().map(|s| s.to_string()))
+ .collect()
+ })
+ .unwrap_or_default();
+
+ let composer_opt =
+ application.borrow_mut().get_composer(false, None, None)?;
+ if let Some(composer) = composer_opt {
+ let composer = crate::command::composer_full(&composer);
+ let root_package = composer.get_package();
+ let generator = composer.get_autoload_generator().clone();
+ let generator = generator.borrow();
+
+ let installation_manager = composer.get_installation_manager();
+ let package_map = generator.build_package_map(
+ &mut installation_manager.borrow_mut(),
+ root_package.clone(),
+ vec![],
+ )?;
+ let map = generator.parse_autoloads(
+ package_map,
+ root_package.clone(),
+ PhpMixed::Bool(false),
+ );
+
+ let loader = generator.create_loader(
+ &map,
+ composer
+ .get_config()
+ .borrow()
+ .get("vendor-dir")
+ .as_string()
+ .map(|s| s.to_string()),
+ );
+ loader.register(false);
+ }
+
+ // if the command is not an array of commands, and points to a valid SymfonyCommand subclass, import its details directly
+ let dummy_str = dummy.as_string().unwrap_or("").to_string();
+ let cmd: PhpMixed = if is_string(dummy)
+ && shirabe_php_shim::class_exists(&dummy_str)
+ && is_subclass_of(
+ &PhpMixed::String(dummy_str.clone()),
+ "Symfony\\Component\\Console\\Command\\Command",
+ true,
+ ) {
+ if is_subclass_of(
+ &PhpMixed::String(dummy_str.clone()),
+ "Symfony\\Component\\Console\\SingleCommandApplication",
+ true,
+ ) {
+ io.write_error(&format!("<warning>The script named {} extends SingleCommandApplication which is not compatible with Composer 2.9+, make sure you extend Symfony\\Component\\Console\\Command instead.</warning>", script));
+ }
+ let mut cmd = shirabe_php_shim::instantiate_class(
+ &dummy_str,
+ vec![PhpMixed::String(script.clone())],
+ );
+ // TODO(phase-c): the script's command class is built by
+ // reflection (instantiate_class) and stays PhpMixed; the
+ // SingleCommandApplication / SymfonyCommand typed registry it
+ // belongs to is an external-package todo!() stub.
+ // let _ = SingleCommandApplication::new;
+
+ // makes sure the command is find()'able by the name defined in composer.json, and the name isn't overridden in its configure()
+ // TODO(phase-c): cmd is the PhpMixed result of reflection
+ // instantiation; reading/overriding its
+ // name/description requires the typed SymfonyCommand model that
+ // the Symfony stub does not yet provide.
+ let _ = description.clone();
+ let _ = &mut cmd;
+ cmd
+ } else {
+ // fallback to usual aliasing behavior
+ // TODO(phase-c): ScriptAliasCommand is a typed BaseCommand
+ // but this code path stores commands as PhpMixed; it can
+ // only be carried as a typed trait object once the Symfony
+ // command registry is modelled.
+ let _ = ScriptAliasCommand::new(
+ script.clone(),
+ Some(description.clone()),
+ aliases,
+ );
+ PhpMixed::Null
+ };
+
+ // Compatibility layer for symfony/console <7.4
+ // TODO(phase-c): Application::add() takes Rc<RefCell<dyn
+ // SymfonyCommand>>
+ // but `cmd` here is the PhpMixed result of reflection-based
+ // plugin command instantiation; registering it as a typed
+ // command instance is blocked on the Symfony command-registry
+ // model (external-package todo!() stub).
+ let _ = &cmd;
+ todo!(
+ "plugin: register reflection-instantiated command on Application::add"
+ );
+ }
+ }
+ }
+ }
+ }
+ }
+
+ let mut start_time: Option<f64> = None;
+ let result_outcome: anyhow::Result<i32> = (|| -> anyhow::Result<i32> {
+ if input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["--profile"]), false)
+ {
+ start_time = Some(microtime());
+ // PHP: $this->io->enableDebugging($startTime).
+ // TODO(phase-c): enableDebugging exists only on ConsoleIO, not on IOInterface,
+ // and self.io is still the NullIO because the ConsoleIO construction above is
+ // deferred (Symfony HelperSet/Helper modelling). Once self.io is the real
+ // ConsoleIO this becomes a concrete-type call on it.
+ let _ = start_time.unwrap();
+ }
+
+ let result = self.base_do_run(input.clone(), output.clone())?;
+
+ if input
+ .borrow()
+ .has_parameter_option(PhpMixed::from(vec!["--version", "-V"]), true)
+ {
+ io.write_error(&format!(
+ "<info>PHP</info> version <comment>{}</comment> ({})",
+ PHP_VERSION, PHP_BINARY,
+ ));
+ io.write_error(
+ "Run the \"diagnose\" command to get more detailed diagnostics output.",
+ );
+ }
+
+ // chdir back to oldWorkingDir if set
+ if let Some(ref owd) = old_working_dir
+ && !owd.is_empty()
+ {
+ let owd = owd.clone();
+ let _ = Silencer::call(|| {
+ chdir(&owd);
+ Ok(())
+ });
+ }
+
+ if let Some(st) = start_time {
+ io.write_error(&format!(
+ "<info>Memory usage: {}MiB (peak: {}MiB), time: {}s</info>",
+ round((memory_get_usage() as f64) / 1024.0 / 1024.0, 2),
+ round((memory_get_peak_usage(false) as f64) / 1024.0 / 1024.0, 2),
+ round(microtime() - st, 2)
+ ));
+ }
+
+ Ok(result)
+ })();
+
+ let outcome = match result_outcome {
+ Ok(r) => Ok(r),
+ Err(e) => {
+ // PHP's `exit` bypasses parent::doRun()'s catch entirely; re-raise it untouched so
+ // the GitHub Actions annotation and error hints below are skipped.
+ if e.downcast_ref::<shirabe_php_shim::ExitException>()
+ .is_some()
+ {
+ return Err(e);
+ }
+ if let Some(see) = e.downcast_ref::<ScriptExecutionException>() {
+ if application.borrow().get_disable_plugins_by_default()
+ && application.borrow().is_running_as_root()
+ && !io.is_interactive()
+ {
+ io.write_error3("<error>Plugins have been disabled automatically as you are running as root, this may be the cause of the script failure.</error>", true, io_interface::QUIET);
+ io.write_error3(
+ "<error>See also https://getcomposer.org/root</error>",
+ true,
+ io_interface::QUIET,
+ );
+ }
+
+ Ok(see.get_code() as i32)
+ } else {
+ let mut ghe = GithubActionError::new(io.clone());
+ ghe.emit(&e.to_string(), None, None);
+
+ application.borrow_mut().hint_common_errors(&e, output);
+
+ // override TransportException's code for the purpose of parent::run() using it as process exit code
+ // as http error codes are all beyond the 255 range of permitted exit codes
+ if e.downcast_ref::<TransportException>().is_some() {
+ // PHP: ReflectionProperty $reflProp = new \ReflectionProperty($e, 'code');
+ // $reflProp->setValue($e, Installer::ERROR_TRANSPORT_EXCEPTION);
+ // TODO: reflection-based mutation of the existing exception is not portable;
+ // we surface the rewritten code via a fresh TransportException at the call site.
+ let _ = Installer::ERROR_TRANSPORT_EXCEPTION;
+ }
+
+ Err(e)
+ }
+ }
+ };
+
+ restore_error_handler();
+
+ outcome
+ }
+}
+
+impl ApplicationHandle {
+ /// Runs the current application (Symfony base; `parent::run`).
+ pub fn base_run(
+ &self,
+ input: Option<std::rc::Rc<std::cell::RefCell<dyn InputInterface>>>,
+ output: Option<std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>>,
+ ) -> anyhow::Result<i32> {
+ let application = &self.0;
+ if shirabe_php_shim::function_exists("putenv") {
+ let (height, width) = {
+ let app = application.borrow();
+ (app.terminal.get_height(), app.terminal.get_width())
+ };
+ shirabe_php_shim::putenv(&format!("LINES={}", height));
+ shirabe_php_shim::putenv(&format!("COLUMNS={}", width));
+ }
+
+ let input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>> = match input {
+ None => std::rc::Rc::new(std::cell::RefCell::new(ArgvInput::new(None, None)?)),
+ Some(input) => input,
+ };
+
+ let output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>> = match output {
+ None => std::rc::Rc::new(std::cell::RefCell::new(ConsoleOutput::new(
+ None, None, None,
+ )?)),
+ Some(output) => output,
+ };
+
+ // TODO: PHP installs a temporary `set_exception_handler($renderException)` and cooperates
+ // with Symfony's ErrorHandler to keep/restore it. PHP's process-global exception handler
+ // stack has no Rust equivalent; the rendering itself is invoked directly in the catch
+ // branch below. Review needed for the handler save/restore dance.
+ let render_exception =
+ |this: &Application,
+ e: &anyhow::Error,
+ output: &std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>| {
+ // if ($output instanceof ConsoleOutputInterface) render to its error output
+ // TODO(review): downcasting a `dyn OutputInterface` to `ConsoleOutputInterface`
+ // is not directly expressible; the ConsoleOutputInterface branch needs design.
+ this.render_throwable(e, output.clone());
+ };
+
+ let result = (|| -> anyhow::Result<i32> {
+ application.borrow_mut().configure_io(&input, &output)?;
+
+ let exit_code = self.do_run(input.clone(), output.clone())?;
+
+ Ok(exit_code)
+ })();
+
+ let exit_code = match result {
+ Ok(exit_code) => exit_code,
+ Err(e) => {
+ // PHP's `exit` bypasses Symfony's try/catch: it never renders and forces the exit
+ // code it carries. The message, if any, has already been written at the exit site.
+ if let Some(exit) = e.downcast_ref::<shirabe_php_shim::ExitException>() {
+ return Ok(exit.code as i32);
+ }
+
+ if !application.borrow().catch_exceptions {
+ return Err(e);
+ }
+
+ render_exception(&application.borrow(), &e, &output);
+
+ // $exitCode = $e->getCode();
+ // is_numeric($exitCode) ? max(1, (int) $exitCode) : 1
+ // TODO(review): anyhow::Error has no PHP-style getCode(); the exit code derived
+ // from the exception's `code` field needs the downcast strategy decided.
+ let exit_code = shirabe_php_shim::php_exception_get_code(&e);
+ if shirabe_php_shim::is_numeric_string(&exit_code.to_string()) {
+ if exit_code <= 0 { 1 } else { exit_code }
+ } else {
+ 1
+ }
+ }
+ };
+
+ // finally: handler restore. See TODO above; no-op here.
+
+ Ok(exit_code)
+ }
+
+ /// Runs the current application (Symfony base; `parent::doRun`).
+ pub fn base_do_run(
+ &self,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i32> {
+ let application = &self.0;
+ if input.borrow().has_parameter_option(
+ PhpMixed::from(vec![
+ PhpMixed::from("--version".to_string()),
+ PhpMixed::from("-V".to_string()),
+ ]),
+ true,
+ ) {
+ let long_version = application.borrow().get_long_version();
+ output
+ .borrow()
+ .writeln(&[long_version], output_interface::OUTPUT_NORMAL);
+
+ return Ok(0);
+ }
+
+ // Makes ArgvInput::getFirstArgument() able to distinguish an option from an argument.
+ let definition = application.borrow_mut().get_definition();
+ match input.borrow_mut().bind(&definition.borrow()) {
+ Ok(()) => {}
+ Err(e) => {
+ // Errors must be ignored, full binding/validation happens later when the command is known.
+ if !is_exception_interface(&e) {
+ return Err(e);
+ }
+ }
+ }
+
+ let mut input = input;
+ let mut name = application.borrow().get_command_name(&*input.borrow());
+ if input.borrow().has_parameter_option(
+ PhpMixed::from(vec![
+ PhpMixed::from("--help".to_string()),
+ PhpMixed::from("-h".to_string()),
+ ]),
+ true,
+ ) {
+ if name.is_none() {
+ name = Some("help".to_string());
+ let default_command = application.borrow().default_command.clone();
+ input = std::rc::Rc::new(std::cell::RefCell::new(ArrayInput::new(
+ vec![(
+ PhpMixed::from("command_name".to_string()),
+ PhpMixed::from(default_command),
+ )],
+ None,
+ )?));
+ } else {
+ application.borrow_mut().want_helps = true;
+ }
+ }
+
+ let name = match name {
+ Some(name) => name,
+ None => {
+ let name = application.borrow().default_command.clone();
+ let definition = application.borrow_mut().get_definition();
+ let command_description = definition
+ .borrow()
+ .get_argument(&PhpMixed::from("command".to_string()))?
+ .get_description()
+ .to_string();
+ let new_command_argument = InputArgument::new(
+ "command".to_string(),
+ Some(InputArgument::OPTIONAL),
+ command_description,
+ PhpMixed::from(name.clone()),
+ )?;
+ // $definition->setArguments(array_merge($definition->getArguments(),
+ // ['command' => new InputArgument('command', InputArgument::OPTIONAL, ...)]))
+ let mut merged_arguments: Vec<InputArgument> = Vec::new();
+ let mut replaced_command = false;
+ for (key, argument) in definition.borrow().get_arguments() {
+ if key == "command" {
+ merged_arguments.push(new_command_argument.clone());
+ replaced_command = true;
+ } else {
+ merged_arguments.push((**argument).clone());
+ }
+ }
+ if !replaced_command {
+ merged_arguments.push(new_command_argument);
+ }
+ definition.borrow_mut().set_arguments(merged_arguments)?;
+
+ name
+ }
+ };
+
+ let command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>;
+ application.borrow_mut().running_command = None;
+ // the command name MUST be the first element of the input
+ let find_result = application.borrow_mut().find(&name);
+
+ match find_result {
+ Ok(c) => {
+ command = c;
+ }
+ Err(e) => {
+ // if (!($e instanceof CommandNotFoundException && !$e instanceof NamespaceNotFoundException)
+ // || 1 !== count($alternatives = $e->getAlternatives()) || !$input->isInteractive())
+ let alternatives: Option<Vec<String>> = downcast_command_not_found(&e)
+ .filter(|_| !is_namespace_not_found(&e))
+ .map(|cnf| cnf.get_alternatives().clone());
+
+ let single_alternative = match &alternatives {
+ Some(alts) if alts.len() == 1 => Some(alts[0].clone()),
+ _ => None,
+ };
+
+ if single_alternative.is_none() || !input.borrow().is_interactive() {
+ return Err(e);
+ }
+
+ let alternative = single_alternative.unwrap();
+
+ let mut style = SymfonyStyle::new(input.clone(), output.clone());
+ output
+ .borrow()
+ .writeln(&["".to_string()], output_interface::OUTPUT_NORMAL);
+ let formatted_block = FormatterHelper::default().format_block(
+ FormatBlockMessages::String(format!(
+ "Command \"{}\" is not defined.",
+ PhpMixed::from(name.clone()),
+ )),
+ "error",
+ true,
+ );
+ output
+ .borrow()
+ .writeln(&[formatted_block], output_interface::OUTPUT_NORMAL);
+ if !style.confirm(
+ &format!(
+ "Do you want to run \"{}\" instead? ",
+ PhpMixed::from(alternative.clone()),
+ ),
+ false,
+ ) {
+ return Ok(1);
+ }
+
+ command = application.borrow_mut().find(&alternative)?;
+ }
+ }
+
+ // LazyCommand is intentionally not ported.
+
+ application.borrow_mut().running_command = Some(command.clone());
+ // do_run_command invokes the command's run(), which calls back into the application
+ // (mergeApplicationDefinition etc.); it must run with no application borrow held.
+ let exit_code = self.do_run_command(command.clone(), input.clone(), output.clone())?;
+ application.borrow_mut().running_command = None;
+
+ Ok(exit_code)
+ }
+
+ /// Adds an array of command objects.
+ ///
+ /// If a SymfonyCommand is not enabled it will not be added.
+ pub fn add_commands(
+ &self,
+ commands: Vec<std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>>,
+ ) -> anyhow::Result<()> {
+ for command in commands {
+ self.add(command)?;
+ }
+ Ok(())
+ }
+
+ /// Runs the current command.
+ ///
+ /// If an event dispatcher has been attached to the application, events are also
+ /// dispatched during the life-cycle of the command.
+ ///
+ /// Returns 0 if everything went fine, or an error code.
+ pub fn do_run_command(
+ &self,
+ command: std::rc::Rc<std::cell::RefCell<dyn SymfonyCommand>>,
+ input: std::rc::Rc<std::cell::RefCell<dyn InputInterface>>,
+ output: std::rc::Rc<std::cell::RefCell<dyn OutputInterface>>,
+ ) -> anyhow::Result<i32> {
+ let application = &self.0;
+ if let Some(helper_set) = command.borrow().get_helper_set() {
+ for (_alias, helper) in helper_set.borrow().get_iterator() {
+ // if ($helper instanceof InputAwareInterface) $helper->setInput($input);
+ // TODO(review): downcasting a HelperInterface to InputAwareInterface is not
+ // expressible without a typed mechanism; needs design.
+ let _ = helper;
+ let _ = std::marker::PhantomData::<dyn InputAwareInterface>;
+ }
+ }
+
+ if !application.borrow().signals_to_dispatch_event.is_empty() {
+ // $commandSignals = $command instanceof SignalableCommandInterface ? $command->getSubscribedSignals() : []
+ // TODO(review): SymfonyCommand is not a SignalableCommandInterface here; downcast needed.
+ let command_signals: Vec<i64> = Vec::new();
+ let _ = std::marker::PhantomData::<dyn SignalableCommandInterface>;
+
+ if !command_signals.is_empty() {
+ if application.borrow().signal_registry.is_none() {
+ return Err(ConsoleRuntimeException(shirabe_php_shim::RuntimeException {
+ message: "Unable to subscribe to signal events. Make sure that the `pcntl` extension is installed and that \"pcntl_*\" functions are not disabled by your php.ini's \"disable_functions\" directive.".to_string(),
+ code: 0,
+ })
+ .into());
+ }
+
+ if Terminal::has_stty_available() {
+ // TODO: registers SIGINT/SIGTERM handlers that restore the stty mode via
+ // shell_exec('stty ...'). pcntl signal handlers have no faithful Rust
+ // equivalent in Phase A.
+ let _stty_mode = shirabe_php_shim::shell_exec("stty -g");
+ for _signal in [shirabe_php_shim::SIGINT, shirabe_php_shim::SIGTERM] {
+ todo!("register signal handler to restore stty mode");
+ }
+ }
+ }
+
+ for _signal in command_signals {
+ // $this->signalRegistry->register($signal, [$command, 'handleSignal']);
+ todo!("register command->handle_signal as signal handler");
+ }
+ }
+
+ command
+ .borrow_mut()
+ .run(input.clone(), output.clone())
+ .map(|c| c as i32)
+ }
}
impl BaseApplication for Application {