//! ref: composer/src/Composer/Command/RemoveCommand.php use anyhow::Result; use indexmap::IndexMap; use shirabe_external_packages::composer::pcre::Preg; use shirabe_external_packages::symfony::console::command::command::Command; use shirabe_external_packages::symfony::console::exception::InvalidArgumentException; use shirabe_external_packages::symfony::console::input::InputInterface; use shirabe_external_packages::symfony::console::output::OutputInterface; use shirabe_php_shim::{PhpMixed, UnexpectedValueException, array_map, strtolower}; use std::cell::RefCell; use std::rc::Rc; use crate::advisory::AuditConfig; use crate::advisory::Auditor; use crate::command::base_command::base_command_initialize; use crate::command::{BaseCommand, BaseCommandData}; use crate::composer::PartialComposerHandle; use crate::config::Config; use crate::config::ConfigSourceInterface; use crate::config::JsonConfigSource; use crate::console::input::InputArgument; use crate::console::input::InputOption; use crate::dependency_resolver::Request; use crate::dependency_resolver::UpdateAllowTransitiveDeps; use crate::factory::Factory; use crate::filter::platform_requirement_filter::PlatformRequirementFilterInterface; use crate::installer::Installer; use crate::io::IOInterface; use crate::io::IOInterfaceImmutable; use crate::json::JsonFile; use crate::package::BasePackage; use crate::package::base_package; use crate::repository::CanonicalPackagesTrait; use crate::repository::RepositoryInterface; #[derive(Debug)] pub struct RemoveCommand { base_command_data: BaseCommandData, } impl Default for RemoveCommand { fn default() -> Self { Self::new() } } impl RemoveCommand { pub fn new() -> Self { let command = RemoveCommand { base_command_data: BaseCommandData::new(None), }; command .configure() .expect("RemoveCommand::configure uses static, valid metadata"); command } } impl Command for RemoveCommand { fn configure(&self) -> anyhow::Result<()> { // TODO(cli-completion): suggest_root_requirement() for `packages` argument self.set_name("remove")?; self.set_aliases(vec!["rm".to_string(), "uninstall".to_string()])?; self.set_description("Removes a package from the require or require-dev"); self.set_definition(&[ InputArgument::new("packages", Some(InputArgument::IS_ARRAY), "Packages that should be removed.", None).unwrap().into(), InputOption::new("dev", None, Some(InputOption::VALUE_NONE), "Removes a package from the require-dev section.", None).unwrap().into(), InputOption::new("dry-run", None, Some(InputOption::VALUE_NONE), "Outputs the operations but will not execute anything (implicitly enables --verbose).", None).unwrap().into(), InputOption::new("no-progress", None, Some(InputOption::VALUE_NONE), "Do not output download progress.", None).unwrap().into(), InputOption::new("no-update", None, Some(InputOption::VALUE_NONE), "Disables the automatic update of the dependencies (implies --no-install).", None).unwrap().into(), InputOption::new("no-install", None, Some(InputOption::VALUE_NONE), "Skip the install step after updating the composer.lock file.", None).unwrap().into(), InputOption::new("no-audit", None, Some(InputOption::VALUE_NONE), "Skip the audit step after updating the composer.lock file (can also be set via the COMPOSER_NO_AUDIT=1 env var).", None).unwrap().into(), InputOption::new("audit-format", None, Some(InputOption::VALUE_REQUIRED), "Audit output format. Must be \"table\", \"plain\", \"json\", or \"summary\".", Some(PhpMixed::String(Auditor::FORMAT_SUMMARY.to_string()))).unwrap().into(), InputOption::new("no-security-blocking", None, Some(InputOption::VALUE_NONE), "Allows installing packages with security advisories or that are abandoned (can also be set via the COMPOSER_NO_SECURITY_BLOCKING=1 env var).", None).unwrap().into(), InputOption::new("update-no-dev", None, Some(InputOption::VALUE_NONE), "Run the dependency update with the --no-dev option.", None).unwrap().into(), InputOption::new("update-with-dependencies", Some(PhpMixed::String("w".to_string())), Some(InputOption::VALUE_NONE), "Allows inherited dependencies to be updated with explicit dependencies (can also be set via the COMPOSER_WITH_DEPENDENCIES=1 env var). (Deprecated, is now default behavior)", None).unwrap().into(), InputOption::new("update-with-all-dependencies", Some(PhpMixed::String("W".to_string())), Some(InputOption::VALUE_NONE), "Allows all inherited dependencies to be updated, including those that are root requirements (can also be set via the COMPOSER_WITH_ALL_DEPENDENCIES=1 env var).", None).unwrap().into(), InputOption::new("with-all-dependencies", None, Some(InputOption::VALUE_NONE), "Alias for --update-with-all-dependencies", None).unwrap().into(), InputOption::new("no-update-with-dependencies", None, Some(InputOption::VALUE_NONE), "Does not allow inherited dependencies to be updated with explicit dependencies.", None).unwrap().into(), InputOption::new("minimal-changes", Some(PhpMixed::String("m".to_string())), Some(InputOption::VALUE_NONE), "During an update with -w/-W, only perform absolutely necessary changes to transitive dependencies (can also be set via the COMPOSER_MINIMAL_CHANGES=1 env var).", None).unwrap().into(), InputOption::new("unused", None, Some(InputOption::VALUE_NONE), "Remove all packages which are locked but not required by any other package.", None).unwrap().into(), InputOption::new("ignore-platform-req", None, Some(InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY), "Ignore a specific platform requirement (php & ext- packages).", None).unwrap().into(), InputOption::new("ignore-platform-reqs", None, Some(InputOption::VALUE_NONE), "Ignore all platform requirements (php & ext- packages).", None).unwrap().into(), InputOption::new("optimize-autoloader", Some(PhpMixed::String("o".to_string())), Some(InputOption::VALUE_NONE), "Optimize autoloader during autoloader dump", None).unwrap().into(), InputOption::new("classmap-authoritative", Some(PhpMixed::String("a".to_string())), Some(InputOption::VALUE_NONE), "Autoload classes from the classmap only. Implicitly enables `--optimize-autoloader`.", None).unwrap().into(), InputOption::new("apcu-autoloader", None, Some(InputOption::VALUE_NONE), "Use APCu to cache found/not-found classes.", None).unwrap().into(), InputOption::new("apcu-autoloader-prefix", None, Some(InputOption::VALUE_REQUIRED), "Use a custom prefix for the APCu autoloader cache. Implicitly enables --apcu-autoloader", None).unwrap().into(), ]); self.set_help( "The remove command removes a package from the current\n\ list of installed packages\n\n\ shirabe remove\n\n\ Read more at https://getcomposer.org/doc/03-cli.md#remove-rm", ); Ok(()) } fn execute( &self, input: Rc>, output: Rc>, ) -> anyhow::Result { if input .borrow() .get_argument("packages")? .as_list() .map(|l| l.is_empty()) .unwrap_or(true) && !input .borrow() .get_option("unused")? .as_bool() .unwrap_or(false) { return Err(anyhow::anyhow!(InvalidArgumentException( shirabe_php_shim::InvalidArgumentException { message: "Not enough arguments (missing: \"packages\").".to_string(), code: 0, } ))); } let mut packages: Vec = input .borrow() .get_argument("packages")? .as_list() .map(|l| { l.iter() .filter_map(|v| v.as_string().map(strtolower)) .collect() }) .unwrap_or_default(); if input .borrow() .get_option("unused")? .as_bool() .unwrap_or(false) { let composer = self.require_composer(None, None)?; let mut composer = crate::command::composer_full_mut(&composer); { let locker = composer.get_locker().clone(); let mut locker = locker.borrow_mut(); if !locker.is_locked() { return Err(anyhow::anyhow!(UnexpectedValueException { message: "A valid composer.lock file is required to run this command with --unused" .to_string(), code: 0, })); } } let mut locked_repo = composer .get_locker() .borrow_mut() .get_locked_repository(true)?; let locked_packages = locked_repo.borrow_mut().get_packages()?; let mut required: IndexMap = IndexMap::new(); for link in composer .get_package() .get_requires() .values() .chain(composer.get_package().get_dev_requires().values()) { required.insert(link.get_target().to_string(), true); } let mut locked_packages = locked_packages; loop { let mut found = false; let mut to_remove = vec![]; for (index, package) in locked_packages.iter().enumerate() { for name in package.get_names(true) { if required.contains_key(name.as_str()) { for link in package.get_requires().values() { required.insert(link.get_target().to_string(), true); } found = true; to_remove.push(index); break; } } } for index in to_remove.into_iter().rev() { locked_packages.remove(index); } if !found { break; } } let unused: Vec = locked_packages .iter() .map(|p| p.get_name().to_string()) .collect(); packages.extend(unused); if packages.is_empty() { self.get_io() .write_error("No unused packages to remove"); return Ok(0); } } let file = Factory::get_composer_file()?; let mut json_file = JsonFile::new(file.clone(), None, None)?; let composer_data = json_file.read()?; let composer_backup = std::fs::read_to_string(json_file.get_path())?; let json_file_for_source = JsonFile::new(file.clone(), None, None)?; let mut json = JsonConfigSource::new( std::rc::Rc::new(std::cell::RefCell::new(json_file_for_source)), false, ); let r#type = if input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { "require-dev" } else { "require" }; let alt_type = if !input.borrow().get_option("dev")?.as_bool().unwrap_or(false) { "require-dev" } else { "require" }; let io = self.get_io(); if input .borrow() .get_option("update-with-dependencies")? .as_bool() .unwrap_or(false) { io.write_error("You are using the deprecated option \"update-with-dependencies\". This is now default behaviour. The --no-update-with-dependencies option can be used to remove a package without its dependencies."); } // make sure name checks are done case insensitively let mut composer_data = composer_data; for link_type in ["require", "require-dev"] { if let Some(section) = composer_data .as_array_mut() .and_then(|m| m.get_mut(link_type)) .and_then(|v| v.as_array_mut()) { let entries: Vec<(String, String)> = section .iter() .filter_map(|(k, v)| v.as_string().map(|_| (k.clone(), k.clone()))) .collect(); for (name, canonical) in entries { section.insert(strtolower(&name), PhpMixed::String(canonical)); } } } let dry_run = input .borrow() .get_option("dry-run")? .as_bool() .unwrap_or(false); let mut to_remove: IndexMap> = IndexMap::new(); for package in &packages { let in_type = composer_data .as_array() .and_then(|m| m.get(r#type)) .and_then(|v| v.as_array()) .and_then(|m| m.get(package.as_str())) .and_then(|v| v.as_string()) .map(|s| s.to_string()); let in_alt_type = composer_data .as_array() .and_then(|m| m.get(alt_type)) .and_then(|v| v.as_array()) .and_then(|m| m.get(package.as_str())) .and_then(|v| v.as_string()) .map(|s| s.to_string()); if let Some(canonical_name) = in_type { if dry_run { to_remove .entry(r#type.to_string()) .or_default() .push(canonical_name.clone()); } else { json.remove_link(r#type, &canonical_name); } } else if let Some(canonical_name) = in_alt_type { io.write_error(&format!( "{} could not be found in {} but it is present in {}", canonical_name, r#type, alt_type )); if io.is_interactive() && io.ask_confirmation( format!( "Do you want to remove it from {} [yes]? ", alt_type ), true, ) { if dry_run { to_remove .entry(alt_type.to_string()) .or_default() .push(canonical_name.clone()); } else { json.remove_link(alt_type, &canonical_name); } } } else { let type_keys: Vec = composer_data .as_array() .and_then(|m| m.get(r#type)) .and_then(|v| v.as_array()) .map(|m| m.keys().cloned().collect()) .unwrap_or_default(); let type_keys_refs: Vec<&str> = type_keys.iter().map(|s| s.as_str()).collect(); let matches_in_type = Preg::grep( &base_package::package_name_to_regexp(package), &type_keys_refs, ); let alt_type_keys: Vec = composer_data .as_array() .and_then(|m| m.get(alt_type)) .and_then(|v| v.as_array()) .map(|m| m.keys().cloned().collect()) .unwrap_or_default(); let alt_type_keys_refs: Vec<&str> = alt_type_keys.iter().map(|s| s.as_str()).collect(); let matches_in_alt_type = Preg::grep( &base_package::package_name_to_regexp(package), &alt_type_keys_refs, ); if !type_keys.is_empty() && !matches_in_type.is_empty() { for matched_package in &matches_in_type { if dry_run { to_remove .entry(r#type.to_string()) .or_default() .push(matched_package.clone()); } else { json.remove_link(r#type, matched_package); } } } else if !alt_type_keys.is_empty() && !matches_in_alt_type.is_empty() { for matched_package in &matches_in_alt_type { io.write_error(&format!( "{} could not be found in {} but it is present in {}", matched_package, r#type, alt_type )); if io.is_interactive() && io.ask_confirmation( format!( "Do you want to remove it from {} [yes]? ", alt_type ), true, ) { if dry_run { to_remove .entry(alt_type.to_string()) .or_default() .push(matched_package.clone()); } else { json.remove_link(alt_type, matched_package); } } } } else { io.write_error(&format!( "{} is not required in your composer.json and has not been removed", package )); } } } io.write_error(&format!("{} has been updated", file)); if input .borrow() .get_option("no-update")? .as_bool() .unwrap_or(false) { return Ok(0); } // TODO(plugin): deactivate installed plugins if let Some(composer_opt) = self.try_composer(None, None) { let composer_opt = crate::command::composer_full(&composer_opt); composer_opt .get_plugin_manager() .borrow_mut() .deactivate_installed_plugins()?; } self.reset_composer(); let composer_handle = self.require_composer(None, None)?; let composer = crate::command::composer_full(&composer_handle); if dry_run { let root_package = composer.get_package(); let mut links: IndexMap> = IndexMap::new(); links.insert("require".to_string(), root_package.get_requires().clone()); links.insert( "require-dev".to_string(), root_package.get_dev_requires().clone(), ); for (link_type, names) in &to_remove { for name in names { if let Some(section) = links.get_mut(link_type.as_str()) { section.shift_remove(name.as_str()); } } } root_package.set_requires(links.shift_remove("require").unwrap_or_default()); root_package.set_dev_requires(links.shift_remove("require-dev").unwrap_or_default()); } let mut command_event = crate::plugin::CommandEvent::new( crate::plugin::PluginEvents::COMMAND, "remove", input.clone(), output, ); let command_event_name = command_event.get_name().to_string(); composer .get_event_dispatcher() .borrow_mut() .dispatch(Some(&command_event_name), Some(&mut command_event)); let allow_plugins = composer.get_config().borrow_mut().get("allow-plugins"); let removed_plugins: Vec = if let Some(allow_map) = allow_plugins.as_opt().and_then(|v| v.as_array()) { packages .iter() .filter(|p| allow_map.contains_key(p.as_str())) .cloned() .collect() } else { vec![] }; if !dry_run && allow_plugins.as_opt().and_then(|v| v.as_array()).is_some() && !removed_plugins.is_empty() { let allow_map_len = allow_plugins .as_opt() .and_then(|v| v.as_array()) .map(|m| m.len()) .unwrap_or(0); if allow_map_len == removed_plugins.len() { json.remove_config_setting("allow-plugins"); } else { for plugin in &removed_plugins { json.remove_config_setting(&format!("allow-plugins.{}", plugin)); } } } composer .get_installation_manager() .borrow_mut() .set_output_progress( !input .borrow() .get_option("no-progress")? .as_bool() .unwrap_or(false), ); let mut install = Installer::create(self.get_io().clone(), &composer_handle); let update_dev_mode = !input .borrow() .get_option("update-no-dev")? .as_bool() .unwrap_or(false); let optimize = input .borrow() .get_option("optimize-autoloader")? .as_bool() .unwrap_or(false) || composer .get_config() .borrow() .get("optimize-autoloader") .as_bool() .unwrap_or(false); let authoritative = input .borrow() .get_option("classmap-authoritative")? .as_bool() .unwrap_or(false) || composer .get_config() .borrow() .get("classmap-authoritative") .as_bool() .unwrap_or(false); let apcu_prefix = input .borrow() .get_option("apcu-autoloader-prefix")? .as_string() .map(|s| s.to_string()); let apcu = apcu_prefix.is_some() || input .borrow() .get_option("apcu-autoloader")? .as_bool() .unwrap_or(false) || composer .get_config() .borrow() .get("apcu-autoloader") .as_bool() .unwrap_or(false); let minimal_changes = input .borrow() .get_option("minimal-changes")? .as_bool() .unwrap_or(false) || composer .get_config() .borrow() .get("update-with-minimal-changes") .as_bool() .unwrap_or(false); let mut update_allow_transitive_dependencies = UpdateAllowTransitiveDeps::UpdateListedWithTransitiveDepsNoRootRequire; let mut flags = String::new(); if input .borrow() .get_option("update-with-all-dependencies")? .as_bool() .unwrap_or(false) || input .borrow() .get_option("with-all-dependencies")? .as_bool() .unwrap_or(false) { update_allow_transitive_dependencies = UpdateAllowTransitiveDeps::UpdateListedWithTransitiveDeps; flags += " --with-all-dependencies"; } else if input .borrow() .get_option("no-update-with-dependencies")? .as_bool() .unwrap_or(false) { update_allow_transitive_dependencies = UpdateAllowTransitiveDeps::UpdateOnlyListed; flags += " --with-dependencies"; } io.write_error(&format!( "Running composer update {}{}", packages.join(" "), flags )); install.set_verbose( input .borrow() .get_option("verbose")? .as_bool() .unwrap_or(false), ); install.set_dev_mode(update_dev_mode); install.set_optimize_autoloader(optimize); install.set_class_map_authoritative(authoritative); install.set_apcu_autoloader(apcu, apcu_prefix); install.set_update(true); install.set_install( !input .borrow() .get_option("no-install")? .as_bool() .unwrap_or(false), ); install.set_update_allow_transitive_dependencies(update_allow_transitive_dependencies); install .set_platform_requirement_filter(self.get_platform_requirement_filter(input.clone())?); install.set_dry_run(dry_run); install.set_audit_config( self.create_audit_config(&mut composer.get_config().borrow_mut(), input)?, ); install.set_minimal_update(minimal_changes); // if no lock is present, we do not do a partial update as // this is not supported by the Installer if composer.get_locker().borrow_mut().is_locked() { install.set_update_allow_list(packages.clone()); } let status = install.run()?; if status != 0 { io.write_error(&format!( "\nRemoval failed, reverting {} to its original content.", file )); std::fs::write(json_file.get_path(), &composer_backup)?; } if !dry_run { for package in &packages { if !composer .get_repository_manager() .borrow() .get_local_repository() .find_packages(package, None)? .is_empty() { io.write_error(&format!( "Removal failed, {} is still present, it may be required by another package. See `composer why {}`.", package, package )); return Ok(2); } } } Ok(status) } fn initialize( &self, input: Rc>, output: Rc>, ) -> anyhow::Result<()> { base_command_initialize(self, input, output) } shirabe_external_packages::delegate_command_trait_impls_to_inner!(base_command_data); } impl BaseCommand for RemoveCommand { fn command_data( &self, ) -> &shirabe_external_packages::symfony::console::command::command::CommandData { self.base_command_data.command_data() } crate::delegate_base_command_trait_impls_to_inner!(base_command_data); }