//! ref: composer/src/Composer/Command/RemoveCommand.php
use indexmap::IndexMap;
use shirabe_external_packages::composer::pcre::Preg;
use shirabe_external_packages::symfony::component::console::exception::InvalidArgumentException;
use shirabe_external_packages::symfony::component::console::input::InputInterface;
use shirabe_external_packages::symfony::component::console::output::OutputInterface;
use shirabe_php_shim::{PhpMixed, UnexpectedValueException, array_map, strtolower};
use crate::advisory::Auditor;
use crate::command::{BaseCommand, BaseCommandData, HasBaseCommandData};
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::factory::Factory;
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;
#[derive(Debug)]
pub struct RemoveCommand {
base_command_data: BaseCommandData,
}
impl RemoveCommand {
pub fn configure(&mut self) {
// TODO(cli-completion): suggest_root_requirement() for `packages` argument
self
.set_name("remove")
.set_aliases(&["rm".to_string(), "uninstall".to_string()])
.set_description("Removes a package from the require or require-dev")
.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(),
])
.set_help(
"The remove command removes a package from the current\n\
list of installed packages\n\n\
php composer.phar remove\n\n\
Read more at https://getcomposer.org/doc/03-cli.md#remove-rm"
);
}
pub fn execute(
&mut self,
input: &dyn InputInterface,
output: &dyn OutputInterface,
) -> anyhow::Result {
if input
.get_argument("packages")
.as_list()
.map(|l| l.is_empty())
.unwrap_or(true)
&& !input.get_option("unused").as_bool().unwrap_or(false)
{
return Err(anyhow::anyhow!(InvalidArgumentException {
message: "Not enough arguments (missing: \"packages\").".to_string(),
code: 0,
}));
}
let mut packages: Vec = input
.get_argument("packages")
.as_list()
.map(|l| {
l.iter()
.filter_map(|v| v.as_string().map(|s| strtolower(s)))
.collect()
})
.unwrap_or_default();
if input.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 locked_packages = composer
.get_locker()
.borrow_mut()
.get_locked_repository(true)?
.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(json_file_for_source, false);
let r#type = if input.get_option("dev").as_bool().unwrap_or(false) {
"require-dev"
} else {
"require"
};
let alt_type = if !input.get_option("dev").as_bool().unwrap_or(false) {
"require-dev"
} else {
"require"
};
let io = self.get_io();
if input
.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), Box::new(PhpMixed::String(canonical)));
}
}
}
let dry_run = input.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() {
if 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,
)
.unwrap_or_default();
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,
)
.unwrap_or_default();
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() {
if 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.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 mut composer_opt = crate::command::composer_full_mut(&composer_opt);
composer_opt
.get_plugin_manager()
.borrow_mut()
.deactivate_installed_plugins();
}
self.reset_composer();
let composer_handle = self.require_composer(None, None)?;
let mut composer = crate::command::composer_full_mut(&composer_handle);
if dry_run {
// TODO(phase-b): composer.get_package() returns &dyn RootPackageInterface; set_requires/set_dev_requires need &mut self; needs shared-ownership refactor
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.remove(name.as_str());
}
}
}
let _ = links.remove("require").unwrap_or_default();
let _ = links.remove("require-dev").unwrap_or_default();
// root_package.set_requires(links.remove("require").unwrap_or_default().into_values().collect());
// root_package.set_dev_requires(links.remove("require-dev").unwrap_or_default().into_values().collect());
}
// TODO(plugin): dispatch CommandEvent(PluginEvents::COMMAND, 'remove', input, output)
let command_event = crate::plugin::CommandEvent::new(
crate::plugin::PluginEvents::COMMAND,
"remove",
input,
output,
);
composer
.get_event_dispatcher()
.borrow_mut()
// TODO(phase-b): dispatch expects Option; CommandEvent is a different type
.dispatch(Some(command_event.get_name()), None);
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.get_option("no-progress").as_bool().unwrap_or(false));
// TODO(phase-b): Installer::create expects std::rc::Rc>; io here is &mut dyn IOInterface
let io_box: std::rc::Rc> =
todo!("share IOInterface as Box");
let mut install = Installer::create(io_box, &composer_handle);
let update_dev_mode = !input.get_option("update-no-dev").as_bool().unwrap_or(false);
let optimize = input
.get_option("optimize-autoloader")
.as_bool()
.unwrap_or(false)
|| composer
.get_config()
.borrow()
.get("optimize-autoloader")
.as_bool()
.unwrap_or(false);
let authoritative = input
.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
.get_option("apcu-autoloader-prefix")
.as_string()
.map(|s| s.to_string());
let apcu = apcu_prefix.is_some()
|| input
.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
.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 =
Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS_NO_ROOT_REQUIRE;
let mut flags = String::new();
if input
.get_option("update-with-all-dependencies")
.as_bool()
.unwrap_or(false)
|| input
.get_option("with-all-dependencies")
.as_bool()
.unwrap_or(false)
{
update_allow_transitive_dependencies = Request::UPDATE_LISTED_WITH_TRANSITIVE_DEPS;
flags += " --with-all-dependencies";
} else if input
.get_option("no-update-with-dependencies")
.as_bool()
.unwrap_or(false)
{
update_allow_transitive_dependencies = Request::UPDATE_ONLY_LISTED;
flags += " --with-dependencies";
}
io.write_error(&format!(
"Running composer update {}{}",
packages.join(" "),
flags
));
install.set_verbose(input.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.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)?);
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)
}
}
impl HasBaseCommandData for RemoveCommand {
fn base_command_data(&self) -> &BaseCommandData {
&self.base_command_data
}
fn base_command_data_mut(&mut self) -> &mut BaseCommandData {
&mut self.base_command_data
}
}